Skip to content

Commit 58c6c0b

Browse files
author
Alan Christie
committed
Initial logic
1 parent 6b6a452 commit 58c6c0b

File tree

14 files changed

+682
-0
lines changed

14 files changed

+682
-0
lines changed

.github/workflows/build.yaml

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
---
2+
name: build
3+
4+
# -----------------
5+
# Control variables (GitHub Secrets)
6+
# -----------------
7+
#
8+
# At the GitHub 'organisation' or 'project' level you must have the following
9+
# GitHub 'Repository Secrets' defined (i.e. via 'Settings -> Secrets'): -
10+
#
11+
# (none)
12+
#
13+
# -----------
14+
# Environment (GitHub Environments)
15+
# -----------
16+
#
17+
# Environment (none)
18+
19+
on:
20+
- push
21+
22+
jobs:
23+
build:
24+
runs-on: ubuntu-latest
25+
strategy:
26+
matrix:
27+
python-version:
28+
- '3.10'
29+
steps:
30+
- name: Checkout
31+
uses: actions/checkout@v2
32+
- name: Set up Python ${{ matrix.python-version }}
33+
uses: actions/setup-python@v2
34+
with:
35+
python-version: ${{ matrix.python-version }}
36+
- name: Install requirements
37+
run: |
38+
python -m pip install --upgrade pip
39+
pip install -r requirements.txt
40+
pip install -r build-requirements.txt
41+
pip install -r package-requirements.txt
42+
- name: Test
43+
run: |
44+
pyroma .
45+
- name: Build
46+
run: |
47+
python setup.py bdist_wheel

.github/workflows/publish.yaml

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
---
2+
name: publish
3+
4+
# Actions for any tag.
5+
6+
# -----------------
7+
# Control variables (GitHub Secrets)
8+
# -----------------
9+
#
10+
# At the GitHub 'organisation' or 'project' level you must have the following
11+
# GitHub 'Repository Secrets' defined (i.e. via 'Settings -> Secrets'): -
12+
#
13+
# PYPI_USERNAME
14+
# PYPI_TOKEN
15+
#
16+
# -----------
17+
# Environment (GitHub Environments)
18+
# -----------
19+
#
20+
# Environment (none)
21+
22+
on:
23+
push:
24+
tags:
25+
- '**'
26+
27+
jobs:
28+
build-and-publish:
29+
runs-on: ubuntu-latest
30+
steps:
31+
- name: Checkout
32+
uses: actions/checkout@v2
33+
- name: Inject slug/short variables
34+
uses: rlespinasse/[email protected]
35+
- name: Set up Python
36+
uses: actions/setup-python@v2
37+
with:
38+
python-version: '3.10'
39+
- name: Install dependencies
40+
run: |
41+
python -m pip install --upgrade pip
42+
pip install -r build-requirements.txt
43+
pip install -r package-requirements.txt
44+
- name: Build
45+
run: |
46+
pyroma .
47+
python setup.py bdist_wheel
48+
- name: Publish
49+
uses: pypa/gh-action-pypi-publish@release/v1
50+
with:
51+
user: ${{ secrets.PYPI_USERNAME }}
52+
password: ${{ secrets.PYPI_TOKEN }}

.gitignore

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
.idea
2+
.pytest_cache
3+
.coverage
4+
*.egg-info
5+
**/__pycache__
6+
venv
7+
build
8+
dist
9+
10+
jote/data
11+
jote/data-manager

MANIFEST.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
include LICENSE

README.rst

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
Informatics Matters Job Tester
2+
==============================
3+
4+
The Job Tester (``jote``) is used to run unit tests located in
5+
Data Manager Job implementation repositories against the Job's
6+
container image.
7+
8+
Job implementations are required to provide a Job Definition (in the
9+
Job repository's ``data-manager`` directory) and at least one test, located in
10+
the repository's ``data-manager/tests`` directory. ``jote`` runs the tests
11+
but also ensures the repository structure meets the Data Manager requirements.
12+
13+
Tests are dined in the Job definition file in the block where the the test
14+
is defined. Here's a snippet illustrating a test called ``simple-execution``
15+
that defines an input option defining a file and some other command
16+
options along with a ``checks`` section that defines the exit criteria
17+
of a successful test::
18+
19+
jobs:
20+
[...]
21+
shard:
22+
[...]
23+
tests:
24+
simple-execution:
25+
inputs:
26+
inputFile: data/100000.smi
27+
options:
28+
outputFile: diverse.smi
29+
count: 100
30+
checks:
31+
exitCode: 0
32+
outputs:
33+
- name: diverse.smi
34+
exists: true
35+
lineCount: 100
36+
37+
Installation
38+
------------
39+
40+
Pyconf is published on `PyPI`_ and can be installed from
41+
there::
42+
43+
pip install im-jote
44+
45+
This is a Python 3 utility, so try to run it from a recent (ideally 3.10)
46+
Python environment.
47+
48+
To use the utility you will need to have installed `Docker`_
49+
and `docker-compose`_.
50+
51+
.. _PyPI: https://pypi.org/project/im-jote/
52+
.. _Docker: https://docs.docker.com/get-docker/
53+
.. _docker-compose: https://pypi.org/project/docker-compose/
54+
55+
Running tests
56+
-------------
57+
58+
Run ``jote`` from the root of a clone of the Data Manager Job implementation
59+
repository that you want to test::
60+
61+
jote
62+
63+
You can display the utility's help with::
64+
65+
jote --help
66+
67+
Get in touch
68+
------------
69+
70+
- Report bugs, suggest features or view the source code `on GitHub`_.
71+
72+
.. _on GitHub: https://github.com/informaticsmatters/data-manager-job-tester

build-requirements.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
pyroma == 3.2
2+
pytest == 6.2.5
3+
pytest-cov == 3.0.0

jote/__init__.py

Whitespace-only changes.

jote/__main__.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#!/usr/bin/env python3
2+
3+
"""An entry-point that allows the module to be executed.
4+
This also simplifies the distribution as this is the
5+
entry-point for the console script (see setup.py).
6+
"""
7+
8+
import sys
9+
from .jote import main as jote_main
10+
11+
12+
def main():
13+
"""The entry-point of the component."""
14+
return jote_main()
15+
16+
17+
if __name__ == '__main__':
18+
sys.exit(main())

jote/compose.py

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
"""The Job Tester 'compose' module.
2+
3+
This module is responsible for injecting a docker-compose file into the
4+
repository of the Data Manager Job repository under test. It also
5+
executes docker-compose and can remove the test directory.
6+
"""
7+
import os
8+
import shutil
9+
import subprocess
10+
from typing import Dict, Optional, Tuple
11+
12+
_INSTANCE_DIRECTORY: str = '.instance-88888888-8888-8888-8888-888888888888'
13+
14+
_COMPOSE_CONTENT: str = """---
15+
version: '3.8'
16+
services:
17+
job:
18+
image: {image}
19+
command: {command}
20+
environment:
21+
- DM_INSTANCE_DIRECTORY={instance_directory}
22+
volumes:
23+
- {test_path}:{project_directory}
24+
deploy:
25+
resources:
26+
limits:
27+
cpus: 1
28+
memory: 1G
29+
"""
30+
31+
# A default, 30 minute timeout
32+
_DEFAULT_TEST_TIMEOUT: int = 30 * 60
33+
34+
# The docker-compose version (for the first test)
35+
_COMPOSE_VERSION: Optional[str] = None
36+
37+
38+
def _get_docker_compose_version() -> str:
39+
40+
result: subprocess.CompletedProcess =\
41+
subprocess.run(['docker-compose', 'version'],
42+
capture_output=True, timeout=4)
43+
44+
# stdout will contain the version on the first line: -
45+
# "docker-compose version 1.29.2, build unknown"
46+
# Ignore the first 23 characters of the first line...
47+
return result.stdout.decode("utf-8").split('\n')[0][23:]
48+
49+
50+
def get_test_path(test_name: str) -> str:
51+
"""Returns the path to the root directory for a given test.
52+
"""
53+
cwd: str = os.getcwd()
54+
return f'{cwd}/data-manager/jote/{test_name}'
55+
56+
57+
def create(test_name: str,
58+
image: str,
59+
project_directory: str,
60+
command: str) -> str:
61+
"""Writes a docker-compose file
62+
and creates the test directory structure returning the
63+
full path to the test (project) directory.
64+
"""
65+
global _COMPOSE_VERSION
66+
67+
print('# Creating test environment...')
68+
69+
# Do we have the docker-compose version the user's installed?
70+
if not _COMPOSE_VERSION:
71+
_COMPOSE_VERSION = _get_docker_compose_version()
72+
print(f'# docker-compose ({_COMPOSE_VERSION})')
73+
74+
# Make the test directory...
75+
test_path: str = get_test_path(test_name)
76+
project_path: str = f'{test_path}/project'
77+
inst_path: str = f'{project_path}/{_INSTANCE_DIRECTORY}'
78+
if not os.path.exists(inst_path):
79+
os.makedirs(inst_path)
80+
81+
# Write the Docker compose content to a file to the test directory
82+
variables: Dict[str, str] = {'test_path': project_path,
83+
'image': image,
84+
'command': command,
85+
'project_directory': project_directory,
86+
'instance_directory': _INSTANCE_DIRECTORY}
87+
compose_content: str = _COMPOSE_CONTENT.format(**variables)
88+
compose_path: str = f'{test_path}/docker-compose.yml'
89+
with open(compose_path, 'wt') as compose_file:
90+
compose_file.write(compose_content)
91+
92+
print('# Created')
93+
94+
return project_path
95+
96+
97+
def run(test_name: str) -> Tuple[int, str, str]:
98+
"""Runs the container for the test, expecting the docker-compose file
99+
written by the 'create()'. The container exit code is returned to the
100+
caller along with the stdout and stderr content.
101+
A non-zero exit code does not necessarily mean the test has failed.
102+
"""
103+
104+
print('# Executing the test ("docker-compose up")...')
105+
106+
cwd = os.getcwd()
107+
os.chdir(get_test_path(test_name))
108+
109+
timeout: int = _DEFAULT_TEST_TIMEOUT
110+
try:
111+
# Run the container
112+
# and then cleanup
113+
test: subprocess.CompletedProcess =\
114+
subprocess.run(['docker-compose', 'up',
115+
'--exit-code-from', 'job',
116+
'--abort-on-container-exit'],
117+
capture_output=True,
118+
timeout=timeout)
119+
_ = subprocess.run(['docker-compose', 'down'],
120+
capture_output=True,
121+
timeout=120)
122+
finally:
123+
os.chdir(cwd)
124+
125+
print(f'# Executed ({test.returncode})')
126+
127+
return test.returncode,\
128+
test.stdout.decode("utf-8"),\
129+
test.stderr.decode("utf-8")
130+
131+
132+
def delete(test_name: str, quiet: bool = False) -> None:
133+
"""Deletes a test directory created by 'crete()'.
134+
"""
135+
print(f'# Deleting the test...')
136+
137+
test_path: str = get_test_path(test_name)
138+
if os.path.exists(test_path):
139+
shutil.rmtree(test_path)
140+
141+
print('# Deleted')

0 commit comments

Comments
 (0)