Skip to content

Commit bd0935e

Browse files
Add unittest helper (#89)
* Use a git log command that works on git 1.8.x and 2.x * Use a git log command that works on git 1.8.x and 2.x * Use a git log command that works on git 1.8.x and 2.x * Use a git log command that works on git 1.8.x and 2.x * Use a git log command that works on git 1.8.x and 2.x * Add type check fixes. * Add type check fixes. * build fix * build fix * build fix * Add release tagging * Add release tagging Minor updates * Add release tagging * Add release tagging * Bandit checks * Git tagging fix * Git tagging fix * Git tagging fix * Make tagging work if ssh is setup by the screwdriver server setup. * Support the old package publish environment setting variable so old templates don't break. * Don't run the validation on centos 5 since it doesn't work. * Don't run the validation on centos 5 since it doesn't work. * Remove debug print * Don't override the package version * Display a message when changing the metadata in screwdriver. * - Make the release tagger use the Versioner class to get the metadata version so there aren't multiple implementations of the same thing. - Update the Versioner base class to allow specifying the meta command as an argument. To simplify writing tests. * Don't output the bytestring as output but the string instead * Don't run the validation on centos 5 since it doesn't work. * Type validation fix and codestyle fix
1 parent db6989b commit bd0935e

28 files changed

Lines changed: 632 additions & 126 deletions

changelog.d/75.feature.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added new validation script screwdrivercd_validate_unittest that will run tests using tox.

docs/Validation_Helpers.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,3 +103,39 @@ This example runs the type check with enforcement enabled.
103103
| --------- | ----------- |
104104
| reports/type_validation | Report files generated by the mypy command |
105105

106+
## Unit Test Validation - screwdrivercd_validate_unittest
107+
108+
The `screwdrivercd_validate_unittest` command runs unittests via the [tox](https://tox.readthedocs.io) tool.
109+
110+
### Environment Settings
111+
112+
All settings for the `scrwedrivercd_validate_unittest` command are specified via environment variables.
113+
114+
The following settings are supported:
115+
116+
| Setting | Default Value | Description |
117+
| ------------------------ | --------------------------- | --------------------------------------------------- |
118+
| BASE_PYTHON | python3 | Python interpreter to use |
119+
| TOX_ARGS | | Additional tox command arguments |
120+
| TOX_ENVLIST | | Only run tests with specific envlist from the tox configuration |
121+
122+
### Example
123+
124+
This example runs the tox command only using a python 3.8 (py38) virtualenv.
125+
126+
!!! note "screwdriver.yaml - With unittest check"
127+
```yaml
128+
version: 4
129+
jobs:
130+
type_check:
131+
template: python/validate_unittest
132+
environment:
133+
TOX_ARGS: -v
134+
TOX_ENVLIST: py38
135+
```
136+
137+
### Artifacts
138+
139+
| Directory | Description |
140+
| --------- | ----------- |
141+
| logs/tox | All of the tox log files |

screwdriver.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ jobs:
4949
DOCUMENTATION_PUBLISH: False
5050
requires: [~pr]
5151

52-
generate_version:
52+
version:
5353
template: python/generate_version
5454
requires: [validate_test, validate_lint, validate_codestyle, validate_dependencies, validate_security, validate_type]
5555

@@ -58,7 +58,7 @@ jobs:
5858
environment:
5959
PUBLISH: True
6060
TWINE_REPOSITORY_URL: https://test.pypi.org/legacy/
61-
requires: [generate_version]
61+
requires: [version]
6262

6363
verify_test_package:
6464
template: python/validate_pypi_package

setup.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ console_scripts =
7676
screwdrivercd_validate_package_quality=screwdrivercd.validation.validate_package_quality:main
7777
screwdrivercd_validate_style=screwdrivercd.validation.validate_style:main
7878
screwdrivercd_validate_type=screwdrivercd.validation.validate_type:main
79+
screwdrivercd_validate_unittest=screwdrivercd.validation.validate_unittest:main
7980

8081
screwdrivercd.documentation.plugin =
8182
base = screwdrivercd.documentation.plugin:DocumentationPlugin

src/screwdrivercd/changelog/__main__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"""
44
screwdrivercd.changelog module entrypoint
55
"""
6-
from .generate import main
76

87

9-
main()
8+
def main():
9+
pass

src/screwdrivercd/documentation/plugin.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ def get_clone_dir(self) -> str:
224224

225225
def git_add_all(self):
226226
"""
227-
Run 'git add' on all the files in the currecnt directory.
227+
Run 'git add' on all the files in the current directory.
228228
"""
229229
self._log_message('\n- Adding all files in the current directory to git', self.publish_log_filename)
230230
for filename in os.listdir('.'):

src/screwdrivercd/installdeps/installer.py

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
class Installer():
2121
"""Generic Package Installer
2222
"""
23-
name: str = 'generic'
23+
name: str = 'echo'
2424
""":str: The name of the installer class"""
2525

2626
install_command: List[str] = ['echo', 'fake', 'install']
@@ -91,7 +91,7 @@ def deps_config_keys(self):
9191
return ['deps']
9292

9393
@property
94-
def has_dependencies(self) -> bool:
94+
def has_dependencies(self) -> bool: # pragma: no cover
9595
"""
9696
Check if the installer has dependencies to install in the current environment.
9797
@@ -109,7 +109,7 @@ def has_dependencies(self) -> bool:
109109
return False
110110

111111
@property
112-
def is_supported(self) -> bool:
112+
def is_supported(self) -> bool: # pragma: no cover
113113
"""
114114
Check for the installer utility and return True if it's present, False otherwise
115115
@@ -124,7 +124,7 @@ def is_supported(self) -> bool:
124124
return False
125125

126126
@property
127-
def plugin_configuration(self):
127+
def plugin_configuration(self): # pragma: no cover
128128
"""
129129
Return the plugin configuration dictionary if it is found
130130
@@ -151,13 +151,14 @@ def _handle_custom_settings(self):
151151
-------
152152
"""
153153

154-
def add_repos(self):
154+
def add_repos(self): # pragma: no cover
155155
"""
156156
Add repositories to the host configuration
157157
"""
158158
if not self.supports_repositories:
159159
LOG.debug(f'Install plugin {self.config_section} does not support repositories')
160160
return
161+
161162
repos = copy.copy(self.default_repos)
162163
repos.update(self.plugin_configuration.get('repos', {}))
163164

@@ -192,13 +193,13 @@ def add_repo(self, repo_name, repo_url): # pragma: no cover
192193

193194
def determine_bin_directory(self):
194195
"""
195-
Determine the bin_dir for the default pip command based on the environment
196+
Determine the bin_dir for the default install command based on the environment
196197
"""
197198
if self.bin_dir or self.install_command[0].startswith('/'):
198199
LOG.debug(f'The command bin_dir {self.bin_dir} is already set')
199200
return
200201

201-
if self.plugin_configuration and self.plugin_configuration.get('bin_dir', None):
202+
if self.plugin_configuration and self.plugin_configuration.get('bin_dir', None): # pragma: no cover
202203
LOG.debug('Setting the configuartion directory from the configuration')
203204
self.bin_dir = self.plugin_configuration['bin_dir']
204205

@@ -276,19 +277,20 @@ def install_dependencies(self) -> List[str]:
276277
# LOG.debug(f'Processing dependencies {dependencies!r}')
277278
invalid += self.invalid_dependencies(dependencies, config_key=config_key)
278279
if invalid:
279-
if self.print_error_output:
280+
if self.print_error_output: # pragma: no cover
280281
print(colored('Invalid %r dependencies %r specified' % (config_key, invalid), 'red'), flush=True)
281282
else:
282283
LOG.error(colored('Invalid %r dependencies %r specified' % (config_key, invalid), 'red'))
283284
if self.exit_on_missing:
284285
break
285286
dependencies = list(set(dependencies) - set(invalid))
286287

287-
if self.print_output:
288-
print(f'Installing dependencies {dependencies!r}')
289-
else:
290-
LOG.debug(f'Installing dependencies: {dependencies!r}')
291-
installed += self.install(dependencies, config_key=config_key)
288+
if dependencies:
289+
if self.print_output:
290+
print(f'Installing dependencies {dependencies!r}')
291+
else:
292+
LOG.debug(f'Installing dependencies: {dependencies!r}')
293+
installed += self.install(dependencies, config_key=config_key)
292294
return installed
293295

294296
def install(self, dependencies, config_key=None):
@@ -315,7 +317,7 @@ def install(self, dependencies, config_key=None):
315317
LOG.debug('Running command: %r', ' '.join(command))
316318
try:
317319
output = subprocess.check_output(command, stderr=subprocess.STDOUT) # nosec - All subprocess calls use full path
318-
except subprocess.CalledProcessError as error:
320+
except subprocess.CalledProcessError as error: # pragma: no cover
319321
if self.print_error_output:
320322
print(colored(f'Install command {" ".join(command)!r} failed', "red"), flush=True)
321323
print(error.stdout.decode().strip())
@@ -326,7 +328,7 @@ def install(self, dependencies, config_key=None):
326328
LOG.error(error.stdout.decode.strip())
327329
return []
328330

329-
if self.print_output:
331+
if self.print_output: # pragma: no cover
330332
print(output.decode().strip())
331333
else: # pragma: no cover
332334
LOG.debug(output.decode().strip())

src/screwdrivercd/repo/release.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ def main(meta_command: str='meta') -> int:
5151
print('Tagging is disabled for this job')
5252
return 0
5353

54-
if not os.environ.get('SSH_AUTH_SOCK', ''): # If ssh-agent isn't already configured, we use the GIT_DEPLOY_KEY
54+
# If ssh-agent isn't already configured, use the GIT_DEPLOY_KEY
55+
if not os.environ.get('SSH_AUTH_SOCK', ''): # pragma: no cover
5556
if not os.environ.get('GIT_DEPLOY_KEY', ''):
5657
print('Git deployment key is not present, cannot commit tags to the git repo')
5758
return 0

src/screwdrivercd/utility/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@
77
from .screwdriver import create_artifact_directory
88

99

10-
__all__ = ['contextmanagers', 'environment', 'exceptions', 'package', 'run', 'screwdriver']
10+
__all__ = ['contextmanagers', 'environment', 'exceptions', 'output', 'package', 'run', 'screwdriver', 'tox']

src/screwdrivercd/utility/contextmanagers.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,13 @@
55
"""
66
import logging
77
import os
8-
from contextlib import contextmanager
8+
import signal
9+
from contextlib import contextmanager, ContextDecorator
10+
from datetime import timedelta
911
from tempfile import TemporaryDirectory
12+
from typing import Optional, Union
13+
14+
from .exceptions import TimeoutError
1015

1116

1217
logger = logging.getLogger(__name__)
@@ -57,3 +62,42 @@ def InTemporaryDirectory():
5762
with TemporaryDirectory() as tempdir:
5863
with working_dir(tempdir):
5964
yield tempdir
65+
66+
67+
class Timeout(ContextDecorator):
68+
"""
69+
A contextmanager that will timeout after a specific datetime.timedelta
70+
"""
71+
use_alarm: Union[bool, None] = False
72+
_old_handler = None
73+
74+
def __init__(self, timeout: Optional[timedelta]=None, use_alarm: Union[bool, None]=None):
75+
self.timeout = timeout
76+
self.use_alarm = use_alarm
77+
if use_alarm is None:
78+
# use_alarm wasn't specified, set the value based on support for signal.setitimer()
79+
if hasattr(signal, 'setitimer'):
80+
self.use_alarm = False
81+
else: # pragma: no cover
82+
self.use_alarm = True
83+
84+
def _timeout_handler(self, signum, frame):
85+
raise TimeoutError(f'Timeout after {self.timeout}')
86+
87+
def __enter__(self):
88+
if self.timeout:
89+
if self.use_alarm:
90+
self._old_handler = signal.signal(signal.SIGALRM, self._timeout_handler)
91+
signal.alarm(self.timeout.seconds)
92+
else:
93+
self._old_handler = signal.signal(signal.SIGALRM, self._timeout_handler)
94+
signal.setitimer(signal.ITIMER_REAL, float(self.timeout.microseconds) / 1000000)
95+
96+
def __exit__(self, exc_type, exc_val, exc_tb):
97+
if self.timeout:
98+
if self.use_alarm:
99+
signal.alarm(0)
100+
else:
101+
signal.setitimer(signal.ITIMER_REAL, 0)
102+
if self._old_handler:
103+
signal.signal(signal.SIGALRM, self._old_handler)

0 commit comments

Comments
 (0)