artefacts-cli 0.6.19__tar.gz → 0.7.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- artefacts_cli-0.7.1/CHANGELOG.md +57 -0
- {artefacts_cli-0.6.19 → artefacts_cli-0.7.1}/PKG-INFO +4 -2
- {artefacts_cli-0.6.19 → artefacts_cli-0.7.1}/README.md +1 -1
- artefacts_cli-0.7.1/artefacts/__init__.py +4 -0
- {artefacts_cli-0.6.19 → artefacts_cli-0.7.1}/artefacts/cli/app.py +59 -39
- artefacts_cli-0.7.1/artefacts/cli/app_containers.py +165 -0
- {artefacts_cli-0.6.19 → artefacts_cli-0.7.1}/artefacts/cli/containers/docker.py +5 -1
- {artefacts_cli-0.6.19 → artefacts_cli-0.7.1}/artefacts/cli/utils.py +17 -0
- {artefacts_cli-0.6.19 → artefacts_cli-0.7.1}/artefacts/cli/version.py +9 -4
- {artefacts_cli-0.6.19 → artefacts_cli-0.7.1}/artefacts_cli.egg-info/PKG-INFO +4 -2
- {artefacts_cli-0.6.19 → artefacts_cli-0.7.1}/artefacts_cli.egg-info/SOURCES.txt +3 -0
- {artefacts_cli-0.6.19 → artefacts_cli-0.7.1}/artefacts_cli.egg-info/requires.txt +1 -0
- {artefacts_cli-0.6.19 → artefacts_cli-0.7.1}/pyproject.toml +2 -0
- artefacts_cli-0.7.1/tests/cli/test_app_containers.py +36 -0
- {artefacts_cli-0.6.19 → artefacts_cli-0.7.1}/tests/cli/test_cli.py +29 -25
- artefacts_cli-0.7.1/tests/cli/test_utils.py +24 -0
- {artefacts_cli-0.6.19 → artefacts_cli-0.7.1}/tests/conftest.py +16 -0
- artefacts_cli-0.6.19/artefacts/cli/app_containers.py +0 -99
- artefacts_cli-0.6.19/tests/cli/test_app_containers.py +0 -53
- {artefacts_cli-0.6.19 → artefacts_cli-0.7.1}/README_INTERNAL.md +0 -0
- {artefacts_cli-0.6.19 → artefacts_cli-0.7.1}/artefacts/cli/__init__.py +0 -0
- {artefacts_cli-0.6.19 → artefacts_cli-0.7.1}/artefacts/cli/bagparser.py +0 -0
- {artefacts_cli-0.6.19 → artefacts_cli-0.7.1}/artefacts/cli/constants.py +0 -0
- {artefacts_cli-0.6.19 → artefacts_cli-0.7.1}/artefacts/cli/containers/__init__.py +0 -0
- {artefacts_cli-0.6.19 → artefacts_cli-0.7.1}/artefacts/cli/containers/utils.py +0 -0
- {artefacts_cli-0.6.19 → artefacts_cli-0.7.1}/artefacts/cli/errors.py +0 -0
- {artefacts_cli-0.6.19 → artefacts_cli-0.7.1}/artefacts/cli/logger.py +0 -0
- {artefacts_cli-0.6.19 → artefacts_cli-0.7.1}/artefacts/cli/other.py +0 -0
- {artefacts_cli-0.6.19 → artefacts_cli-0.7.1}/artefacts/cli/parameters.py +0 -0
- {artefacts_cli-0.6.19 → artefacts_cli-0.7.1}/artefacts/cli/ros1.py +0 -0
- {artefacts_cli-0.6.19 → artefacts_cli-0.7.1}/artefacts/cli/ros2.py +0 -0
- {artefacts_cli-0.6.19 → artefacts_cli-0.7.1}/artefacts/cli/utils_ros.py +0 -0
- {artefacts_cli-0.6.19 → artefacts_cli-0.7.1}/artefacts/wrappers/artefacts_ros1_meta.launch +0 -0
- {artefacts_cli-0.6.19 → artefacts_cli-0.7.1}/artefacts.yaml +0 -0
- {artefacts_cli-0.6.19 → artefacts_cli-0.7.1}/artefacts_cli.egg-info/dependency_links.txt +0 -0
- {artefacts_cli-0.6.19 → artefacts_cli-0.7.1}/artefacts_cli.egg-info/entry_points.txt +0 -0
- {artefacts_cli-0.6.19 → artefacts_cli-0.7.1}/artefacts_cli.egg-info/top_level.txt +0 -0
- {artefacts_cli-0.6.19 → artefacts_cli-0.7.1}/bin/release +0 -0
- {artefacts_cli-0.6.19 → artefacts_cli-0.7.1}/infra-tests/test_run_remote.yaml +0 -0
- {artefacts_cli-0.6.19 → artefacts_cli-0.7.1}/infra-tests/turtlesim1/ros_workspace/src/turtle_odometry/CMakeLists.txt +0 -0
- {artefacts_cli-0.6.19 → artefacts_cli-0.7.1}/infra-tests/turtlesim1/ros_workspace/src/turtle_odometry/launch/test_meta.launch +0 -0
- {artefacts_cli-0.6.19 → artefacts_cli-0.7.1}/infra-tests/turtlesim1/ros_workspace/src/turtle_odometry/launch/test_turtle.launch +0 -0
- {artefacts_cli-0.6.19 → artefacts_cli-0.7.1}/infra-tests/turtlesim1/ros_workspace/src/turtle_odometry/launch/turtle_odometry.launch +0 -0
- {artefacts_cli-0.6.19 → artefacts_cli-0.7.1}/infra-tests/turtlesim1/ros_workspace/src/turtle_odometry/package.xml +0 -0
- {artefacts_cli-0.6.19 → artefacts_cli-0.7.1}/infra-tests/turtlesim1/ros_workspace/src/turtle_odometry/setup.py +0 -0
- {artefacts_cli-0.6.19 → artefacts_cli-0.7.1}/infra-tests/turtlesim1/ros_workspace/src/turtle_odometry/src/TestTurtle.py +0 -0
- {artefacts_cli-0.6.19 → artefacts_cli-0.7.1}/infra-tests/turtlesim1/ros_workspace/src/turtle_odometry/src/__init__.py +0 -0
- {artefacts_cli-0.6.19 → artefacts_cli-0.7.1}/infra-tests/turtlesim1/ros_workspace/src/turtle_odometry/src/turtle_odom.py +0 -0
- {artefacts_cli-0.6.19 → artefacts_cli-0.7.1}/infra-tests/turtlesim1/ros_workspace/src/turtle_odometry/src/turtle_post_process.py +0 -0
- {artefacts_cli-0.6.19 → artefacts_cli-0.7.1}/infra-tests/turtlesim1/ros_workspace/src/turtle_odometry/src/turtle_trajectory.py +0 -0
- {artefacts_cli-0.6.19 → artefacts_cli-0.7.1}/infra-tests/turtlesim1/ros_workspace/src/turtle_odometry/test/viz_turtle_odom.xml +0 -0
- {artefacts_cli-0.6.19 → artefacts_cli-0.7.1}/infra-tests/turtlesim2/launch_turtle.py +0 -0
- {artefacts_cli-0.6.19 → artefacts_cli-0.7.1}/infra-tests/turtlesim2/sample_node.py +0 -0
- {artefacts_cli-0.6.19 → artefacts_cli-0.7.1}/pytest.ini +0 -0
- {artefacts_cli-0.6.19 → artefacts_cli-0.7.1}/setup.cfg +0 -0
- {artefacts_cli-0.6.19 → artefacts_cli-0.7.1}/tests/__init__.py +0 -0
- {artefacts_cli-0.6.19 → artefacts_cli-0.7.1}/tests/cli/__init__.py +0 -0
- {artefacts_cli-0.6.19 → artefacts_cli-0.7.1}/tests/cli/containers/__init__.py +0 -0
- {artefacts_cli-0.6.19 → artefacts_cli-0.7.1}/tests/cli/containers/test_utils.py +0 -0
- {artefacts_cli-0.6.19 → artefacts_cli-0.7.1}/tests/cli/test_config_validation.py +0 -0
- {artefacts_cli-0.6.19 → artefacts_cli-0.7.1}/tests/cli/test_other.py +0 -0
- {artefacts_cli-0.6.19 → artefacts_cli-0.7.1}/tests/cli/test_parameters.py +0 -0
- {artefacts_cli-0.6.19 → artefacts_cli-0.7.1}/tests/cli/test_ros1.py +0 -0
- {artefacts_cli-0.6.19 → artefacts_cli-0.7.1}/tests/cli/test_ros2.py +0 -0
- {artefacts_cli-0.6.19 → artefacts_cli-0.7.1}/tests/cli/test_warp.py +0 -0
- {artefacts_cli-0.6.19 → artefacts_cli-0.7.1}/tests/fixtures/artefacts_deprecated.yaml +0 -0
- {artefacts_cli-0.6.19 → artefacts_cli-0.7.1}/tests/fixtures/artefacts_ros1.yaml +0 -0
- {artefacts_cli-0.6.19 → artefacts_cli-0.7.1}/tests/fixtures/warp-env-param.yaml +0 -0
- {artefacts_cli-0.6.19 → artefacts_cli-0.7.1}/tests/fixtures/warp.yaml +0 -0
- {artefacts_cli-0.6.19 → artefacts_cli-0.7.1}/tests/test_config_validation.py +0 -0
- {artefacts_cli-0.6.19 → artefacts_cli-0.7.1}/tests/utils/docker_mock.py +0 -0
@@ -0,0 +1,57 @@
|
|
1
|
+
# Changelog
|
2
|
+
|
3
|
+
All notable changes to this project will be documented in this file.
|
4
|
+
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
7
|
+
|
8
|
+
## [Unreleased]
|
9
|
+
|
10
|
+
### Added
|
11
|
+
|
12
|
+
- Run in containers with only an artefacts.yaml configuration file. No need to
|
13
|
+
write a Dockerfile in many standard situations.
|
14
|
+
|
15
|
+
### Changed
|
16
|
+
|
17
|
+
- New logging messages and format.
|
18
|
+
|
19
|
+
|
20
|
+
## [0.7.1] - 2025-03-14
|
21
|
+
|
22
|
+
### Added
|
23
|
+
|
24
|
+
- Partial CHANGELOG with information on the day we start SemVer and the current
|
25
|
+
0.7.0. More detail to come inbetween, but we will focus on the future.
|
26
|
+
|
27
|
+
### Changed
|
28
|
+
|
29
|
+
- Replace Ruff shield for the original Black one.
|
30
|
+
|
31
|
+
|
32
|
+
## [0.7.0] - 2025-02-25
|
33
|
+
|
34
|
+
### Added
|
35
|
+
|
36
|
+
- Default upload directory to automatically include output from the Artefacts
|
37
|
+
toolkit.
|
38
|
+
|
39
|
+
### Changed
|
40
|
+
|
41
|
+
- Always rebuild container images before run. These incremental rebuilds avoid
|
42
|
+
existing confusion when running an updated code base without rebuilding.
|
43
|
+
- Separate CD workflow from PyPi publication testing: For reusability and
|
44
|
+
direct invocation.
|
45
|
+
|
46
|
+
|
47
|
+
## [0.5.8] - 2024-08-19
|
48
|
+
|
49
|
+
### Added
|
50
|
+
|
51
|
+
- Beginning of semantic versioning.
|
52
|
+
- Local metrics errors do not block publication of results.
|
53
|
+
- Introduction of Black formatting.
|
54
|
+
|
55
|
+
[unreleased]: https://github.com/art-e-fact/artefacts-client/compare/0.7.0...HEAD
|
56
|
+
[0.7.0]: https://github.com/art-e-fact/artefacts-client/releases/tag/0.7.0
|
57
|
+
[0.5.8]: https://github.com/art-e-fact/artefacts-client/releases/tag/0.5.8
|
@@ -1,14 +1,16 @@
|
|
1
1
|
Metadata-Version: 2.2
|
2
2
|
Name: artefacts_cli
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.7.1
|
4
4
|
Author-email: FD <fabian@artefacts.com>, AGC <alejandro@artefacts.com>, TN <tomo@artefacts.com>, EP <eric@artefacts.com>
|
5
5
|
Project-URL: Homepage, https://github.com/art-e-fact/artefacts-client
|
6
6
|
Project-URL: Bug Tracker, https://github.com/art-e-fact/artefacts-client/issues
|
7
|
+
Project-URL: Changelog, https://github.com/art-e-fact/artefacts-client/CHANGELOG.md
|
7
8
|
Classifier: Programming Language :: Python :: 3
|
8
9
|
Classifier: License :: OSI Approved :: Apache Software License
|
9
10
|
Classifier: Operating System :: OS Independent
|
10
11
|
Requires-Python: >=3.8
|
11
12
|
Description-Content-Type: text/markdown
|
13
|
+
Requires-Dist: artefacts-c2d>=1.7.1
|
12
14
|
Requires-Dist: artefacts-copava>=0.1.11
|
13
15
|
Requires-Dist: click>=8.0.4
|
14
16
|
Requires-Dist: gitignore_parser>=0.1.11
|
@@ -42,7 +44,7 @@ Requires-Dist: twine; extra == "dev"
|
|
42
44
|
CLI to the Artefacts platform.
|
43
45
|
|
44
46
|
[](https://docs.artefacts.com/)
|
45
|
-
[](https://github.com/astral-sh/ruff)
|
46
48
|
|
47
49
|
## Requirements
|
48
50
|
|
@@ -3,7 +3,7 @@
|
|
3
3
|
CLI to the Artefacts platform.
|
4
4
|
|
5
5
|
[](https://docs.artefacts.com/)
|
6
|
-
[](https://github.com/astral-sh/ruff)
|
7
7
|
|
8
8
|
## Requirements
|
9
9
|
|
@@ -20,7 +20,7 @@ from gitignore_parser import parse_gitignore
|
|
20
20
|
from artefacts.cli import init_job, generate_scenarios, AuthenticationError, __version__
|
21
21
|
from artefacts.cli import app_containers as containers
|
22
22
|
from artefacts.cli.constants import DEPRECATED_FRAMEWORKS, SUPPORTED_FRAMEWORKS
|
23
|
-
from artefacts.cli.utils import
|
23
|
+
from artefacts.cli.utils import add_output_from_default, config_validation, read_config
|
24
24
|
|
25
25
|
HOME = os.path.expanduser("~")
|
26
26
|
CONFIG_DIR = f"{HOME}/.artefacts"
|
@@ -71,7 +71,7 @@ def get_artefacts_api_url(project_profile):
|
|
71
71
|
|
72
72
|
|
73
73
|
class APIConf:
|
74
|
-
def __init__(self, project_name):
|
74
|
+
def __init__(self, project_name: str, job_name: str = None) -> None:
|
75
75
|
config = get_conf_from_file()
|
76
76
|
if project_name in config:
|
77
77
|
profile = config[project_name]
|
@@ -96,7 +96,10 @@ class APIConf:
|
|
96
96
|
self.headers["User-Agent"] = (
|
97
97
|
f"ArtefactsClient/{__version__} ({platform.platform()}/{platform.python_version()})"
|
98
98
|
)
|
99
|
-
|
99
|
+
if job_name:
|
100
|
+
click.echo(f"[{job_name}] Connecting to {self.api_url} using {auth_type}")
|
101
|
+
else:
|
102
|
+
click.echo(f"Connecting to {self.api_url} using {auth_type}")
|
100
103
|
|
101
104
|
|
102
105
|
def validate_artefacts_config(config_file: str) -> dict:
|
@@ -243,14 +246,14 @@ def hello(project_name):
|
|
243
246
|
)
|
244
247
|
@click.option(
|
245
248
|
"--with-image",
|
246
|
-
default=
|
247
|
-
help="[
|
249
|
+
default=None,
|
250
|
+
help="[Deprecated and unused from 0.8.0; Image names are now internally managed] Run the job using the image name passed here. Only used when running with --in-container set.",
|
248
251
|
)
|
249
252
|
@click.option(
|
250
|
-
"--rebuild
|
253
|
+
"--no-rebuild",
|
251
254
|
is_flag=True,
|
252
255
|
default=False,
|
253
|
-
help="[Experimental]
|
256
|
+
help="[Experimental] Override the default behaviour to always rebuild the container image (as we assume incremental testing).",
|
254
257
|
)
|
255
258
|
@click.option(
|
256
259
|
"--with-gui",
|
@@ -272,7 +275,7 @@ def run(
|
|
272
275
|
skip_validation=False,
|
273
276
|
in_container: bool = False,
|
274
277
|
with_image: str = "artefacts",
|
275
|
-
|
278
|
+
no_rebuild: bool = False,
|
276
279
|
with_gui: bool = False,
|
277
280
|
):
|
278
281
|
"""
|
@@ -281,30 +284,37 @@ def run(
|
|
281
284
|
* Directly in the shell by default.
|
282
285
|
* Inside a packaged container when using the --in-container option.
|
283
286
|
"""
|
287
|
+
warpconfig = read_config(config)
|
288
|
+
project_id = warpconfig["project"]
|
289
|
+
|
284
290
|
if in_container:
|
285
|
-
|
291
|
+
click.echo("#" * 80)
|
292
|
+
click.echo(f"# Job {jobname}".ljust(79, " ") + "#")
|
293
|
+
click.echo("#" * 80)
|
294
|
+
click.echo(f"[{jobname}] Checking container image")
|
295
|
+
if not no_rebuild:
|
286
296
|
ctx.invoke(
|
287
|
-
containers.build,
|
297
|
+
containers.build,
|
298
|
+
root=".",
|
288
299
|
)
|
300
|
+
click.echo(f"[{jobname}] Container image ready")
|
301
|
+
click.echo(f"[{jobname}] Run in container")
|
289
302
|
return ctx.invoke(
|
290
303
|
containers.run,
|
291
|
-
image=with_image,
|
292
304
|
jobname=jobname,
|
293
305
|
config=config,
|
294
306
|
with_gui=with_gui,
|
295
307
|
)
|
296
308
|
|
297
|
-
|
298
|
-
|
299
|
-
project_id = warpconfig["project"]
|
300
|
-
api_conf = APIConf(project_id)
|
301
|
-
click.echo(f"Starting tests for {project_id}")
|
309
|
+
api_conf = APIConf(project_id, jobname)
|
310
|
+
click.echo(f"[{jobname}] Starting tests")
|
302
311
|
if jobname not in warpconfig["jobs"]:
|
303
|
-
|
312
|
+
click.echo(f"[{jobname}] Error: Job name not defined")
|
313
|
+
raise click.ClickException()
|
304
314
|
jobconf = warpconfig["jobs"][jobname]
|
305
315
|
job_type = jobconf.get("type", "test")
|
306
316
|
if job_type not in ["test"]:
|
307
|
-
click.echo(f"Job type not supported:
|
317
|
+
click.echo(f"[{jobname}] Job type not supported: {job_type}")
|
308
318
|
return
|
309
319
|
|
310
320
|
framework = jobconf["runtime"].get("framework", None)
|
@@ -313,19 +323,19 @@ def run(
|
|
313
323
|
if framework in DEPRECATED_FRAMEWORKS.keys():
|
314
324
|
migrated_framework = DEPRECATED_FRAMEWORKS[framework]
|
315
325
|
click.echo(
|
316
|
-
f"The selected framework '{framework}' is deprecated. Using '{migrated_framework}' instead."
|
326
|
+
f"[{jobname}] The selected framework '{framework}' is deprecated. Using '{migrated_framework}' instead."
|
317
327
|
)
|
318
328
|
framework = migrated_framework
|
319
329
|
|
320
330
|
if framework not in SUPPORTED_FRAMEWORKS:
|
321
331
|
click.echo(
|
322
|
-
f"WARNING: framework: '{framework}' is not officially supported. Attempting run."
|
332
|
+
f"[{jobname}] WARNING: framework: '{framework}' is not officially supported. Attempting run."
|
323
333
|
)
|
324
334
|
|
325
335
|
batch_index = os.environ.get("AWS_BATCH_JOB_ARRAY_INDEX", None)
|
326
336
|
if batch_index is not None:
|
327
337
|
batch_index = int(batch_index)
|
328
|
-
click.echo(f"AWS BATCH ARRAY DETECTED, batch_index={batch_index}")
|
338
|
+
click.echo(f"[{jobname}] AWS BATCH ARRAY DETECTED, batch_index={batch_index}")
|
329
339
|
scenarios, first = generate_scenarios(jobconf, batch_index)
|
330
340
|
context = None
|
331
341
|
execution_context = getpass.getuser() + "@" + platform.node()
|
@@ -348,30 +358,33 @@ def run(
|
|
348
358
|
first,
|
349
359
|
)
|
350
360
|
except AuthenticationError:
|
351
|
-
|
352
|
-
"Unable to authenticate (Stage: Job initialisation), please check your project name and API key"
|
361
|
+
click.echo(
|
362
|
+
f"[{jobname}] Unable to authenticate (Stage: Job initialisation), please check your project name and API key"
|
353
363
|
)
|
364
|
+
raise click.ClickException()
|
354
365
|
|
355
366
|
job_success = True
|
356
367
|
for scenario_n, scenario in enumerate(scenarios):
|
357
368
|
click.echo(
|
358
|
-
f"Starting scenario {scenario_n + 1}/{len(scenarios)}: {scenario['name']}"
|
369
|
+
f"[{jobname}] Starting scenario {scenario_n + 1}/{len(scenarios)}: {scenario['name']}"
|
359
370
|
)
|
360
371
|
try:
|
361
372
|
run = warpjob.new_run(scenario)
|
362
373
|
except AuthenticationError:
|
363
|
-
|
364
|
-
"Unable to authenticate (Stage: Job run), please check your project name and API key"
|
374
|
+
click.echo(
|
375
|
+
f"[{jobname}] Unable to authenticate (Stage: Job run), please check your project name and API key"
|
365
376
|
)
|
377
|
+
raise click.ClickException()
|
366
378
|
if framework is not None and framework.startswith("ros2:"):
|
367
379
|
from artefacts.cli.ros2 import run_ros2_tests
|
368
380
|
|
369
381
|
if "ros_testfile" not in run.params:
|
370
|
-
|
371
|
-
"Test launch file not specified for ros2 project"
|
382
|
+
click.echo(
|
383
|
+
f"[{jobname}] Test launch file not specified for ros2 project"
|
372
384
|
)
|
385
|
+
raise click.ClickException()
|
373
386
|
if dryrun:
|
374
|
-
click.echo("
|
387
|
+
click.echo(f"[{jobname}] Performing dry run")
|
375
388
|
results, success = {}, True
|
376
389
|
else:
|
377
390
|
try:
|
@@ -380,25 +393,28 @@ def run(
|
|
380
393
|
warpjob.stop()
|
381
394
|
warpjob.log_tests_result(False)
|
382
395
|
click.secho(e, bold=True, err=True)
|
383
|
-
|
396
|
+
click.echo(f"[{jobname}] artefacts failed to execute the tests")
|
397
|
+
raise click.ClickException()
|
384
398
|
if success is None:
|
385
399
|
run.stop()
|
386
400
|
warpjob.stop()
|
387
401
|
warpjob.log_tests_result(job_success)
|
388
|
-
|
389
|
-
"Not able to execute tests. Make sure that ROS2 is sourced and that your launch file syntax is correct."
|
402
|
+
click.echo(
|
403
|
+
f"[{jobname}] Not able to execute tests. Make sure that ROS2 is sourced and that your launch file syntax is correct."
|
390
404
|
)
|
405
|
+
raise click.ClickException()
|
391
406
|
if not success:
|
392
407
|
job_success = False
|
393
408
|
elif framework is not None and framework.startswith("ros1:"):
|
394
409
|
from artefacts.cli.ros1 import run_ros1_tests
|
395
410
|
|
396
411
|
if "ros_testfile" not in run.params:
|
397
|
-
|
398
|
-
"Test launch file not specified for ros1 project"
|
412
|
+
click.echo(
|
413
|
+
f"[{jobname}] Test launch file not specified for ros1 project"
|
399
414
|
)
|
415
|
+
raise click.ClickException()
|
400
416
|
if dryrun:
|
401
|
-
click.echo("
|
417
|
+
click.echo(f"[{jobname}] Performing dry run")
|
402
418
|
results, success = {}, True
|
403
419
|
else:
|
404
420
|
results, success = run_ros1_tests(run)
|
@@ -408,9 +424,10 @@ def run(
|
|
408
424
|
from artefacts.cli.other import run_other_tests
|
409
425
|
|
410
426
|
if "run" not in run.params:
|
411
|
-
|
427
|
+
click.echo(f"[{jobname}] run command not specified for scenario")
|
428
|
+
raise click.ClickException()
|
412
429
|
if dryrun:
|
413
|
-
click.echo("
|
430
|
+
click.echo(f"[{jobname}] Performing dry run")
|
414
431
|
results, success = {}, True
|
415
432
|
else:
|
416
433
|
results, success = run_other_tests(run)
|
@@ -419,9 +436,12 @@ def run(
|
|
419
436
|
if type(run.params.get("metrics", [])) is str:
|
420
437
|
run.log_metrics()
|
421
438
|
|
439
|
+
# Add for upload any default output generated by the run
|
440
|
+
add_output_from_default(run)
|
441
|
+
|
422
442
|
run.stop()
|
423
443
|
warpjob.log_tests_result(job_success)
|
424
|
-
click.echo("Done")
|
444
|
+
click.echo(f"[{jobname}] Done")
|
425
445
|
time.sleep(random.random() * 1)
|
426
446
|
|
427
447
|
warpjob.stop()
|
@@ -551,7 +571,7 @@ def run_remote(config, description, jobname, skip_validation=False):
|
|
551
571
|
)
|
552
572
|
|
553
573
|
raise click.ClickException(
|
554
|
-
f"Error getting project info: {
|
574
|
+
f"Error getting project info: {upload_urls_response.status_code} {upload_urls_response.reason}. Response text: {upload_urls_response.text}."
|
555
575
|
)
|
556
576
|
|
557
577
|
upload_urls = upload_urls_response.json()["upload_urls"]
|
@@ -0,0 +1,165 @@
|
|
1
|
+
import os
|
2
|
+
from pathlib import Path
|
3
|
+
|
4
|
+
import click
|
5
|
+
|
6
|
+
from c2d.core import Converter
|
7
|
+
|
8
|
+
from artefacts.cli.constants import DEFAULT_API_URL
|
9
|
+
from artefacts.cli.utils import config_validation, read_config
|
10
|
+
from artefacts.cli.containers.utils import ContainerMgr
|
11
|
+
|
12
|
+
|
13
|
+
@click.group()
|
14
|
+
@click.option("--debug/--no-debug", default=False)
|
15
|
+
@click.pass_context
|
16
|
+
def containers(ctx: click.Context, debug: bool):
|
17
|
+
ctx.ensure_object(dict)
|
18
|
+
ctx.obj["debug"] = debug
|
19
|
+
|
20
|
+
|
21
|
+
@containers.command()
|
22
|
+
@click.option(
|
23
|
+
"--path",
|
24
|
+
default=".",
|
25
|
+
help="[Deprecated since 0.8.0; please see --root] Path to the root of the project.",
|
26
|
+
)
|
27
|
+
@click.option(
|
28
|
+
"--root",
|
29
|
+
default=".",
|
30
|
+
help="Path to the root of the project.",
|
31
|
+
)
|
32
|
+
@click.option(
|
33
|
+
"--dockerfile",
|
34
|
+
default="Dockerfile",
|
35
|
+
help="Path to a custom Dockerfile. Defaults to Dockerfile under `path` (see option of the same name).",
|
36
|
+
)
|
37
|
+
@click.option(
|
38
|
+
"--name",
|
39
|
+
required=False,
|
40
|
+
help="[Deprecated since 0.8.0; not used and will disappear after 0.8.0] Name for the generated image",
|
41
|
+
)
|
42
|
+
@click.option(
|
43
|
+
"--config",
|
44
|
+
callback=config_validation,
|
45
|
+
default="artefacts.yaml",
|
46
|
+
help="Path to the Artefacts configuration file. It defaults to `./artefacts.yaml`",
|
47
|
+
)
|
48
|
+
@click.pass_context
|
49
|
+
def build(
|
50
|
+
ctx: click.Context,
|
51
|
+
path: str,
|
52
|
+
root: str,
|
53
|
+
dockerfile: str,
|
54
|
+
name: str,
|
55
|
+
config: str,
|
56
|
+
):
|
57
|
+
try:
|
58
|
+
artefacts_config = read_config(config)
|
59
|
+
except FileNotFoundError:
|
60
|
+
raise click.ClickException(
|
61
|
+
f"Project config file not found: {config}. Please provide an Artefacts configuration file to proceed (running `artefacts init` allows to generate one)."
|
62
|
+
)
|
63
|
+
prefix = artefacts_config["project"].strip().lower()
|
64
|
+
dockerfiles = []
|
65
|
+
if os.path.exists(dockerfile):
|
66
|
+
for job_name in artefacts_config["jobs"]:
|
67
|
+
dockerfiles.append(
|
68
|
+
dict(
|
69
|
+
path=root,
|
70
|
+
dockerfile=dockerfile,
|
71
|
+
name=f"{prefix}/{job_name.strip().lower()}",
|
72
|
+
)
|
73
|
+
)
|
74
|
+
else:
|
75
|
+
# The split on `prefix` is to ensure there is no slash (project names are org/project) confusing the path across supported OS.
|
76
|
+
dest_root = (
|
77
|
+
Path.home()
|
78
|
+
/ Path(".artefacts")
|
79
|
+
/ Path("projects")
|
80
|
+
/ Path(*(prefix.split("/")))
|
81
|
+
/ Path("containers")
|
82
|
+
)
|
83
|
+
if not dest_root.exists():
|
84
|
+
click.echo(
|
85
|
+
f"No {dockerfile} found here. Let's generate one per scenario based on artefacts.yaml. They will be available under the `{dest_root}` folder and used from there."
|
86
|
+
)
|
87
|
+
# No condition on generating the Dockerfiles as:
|
88
|
+
# - Fast
|
89
|
+
# - We consider entirely managed, so any manual change should be ignored.
|
90
|
+
scenarios = Converter().process(config, as_text=False)
|
91
|
+
for idx, df in enumerate(scenarios.values()):
|
92
|
+
job_name = df.job_name.strip().lower()
|
93
|
+
dest = dest_root / Path(job_name)
|
94
|
+
dest.mkdir(parents=True, exist_ok=True)
|
95
|
+
_dockerfile = os.path.join(dest, "Dockerfile")
|
96
|
+
df.dump(_dockerfile)
|
97
|
+
click.echo(f"[{job_name}] Using generated Dockerfile at: {_dockerfile}")
|
98
|
+
dockerfiles.append(
|
99
|
+
dict(
|
100
|
+
path=root,
|
101
|
+
dockerfile=_dockerfile,
|
102
|
+
name=f"{prefix}/{job_name}",
|
103
|
+
)
|
104
|
+
)
|
105
|
+
handler = ContainerMgr()
|
106
|
+
if len(dockerfiles) > 0:
|
107
|
+
for specs in dockerfiles:
|
108
|
+
# No condition on building the images, as relatively fast when already exists, and straightforward logic.
|
109
|
+
image, _ = handler.build(**specs)
|
110
|
+
else:
|
111
|
+
click.echo("No Dockerfile, nothing to do.")
|
112
|
+
|
113
|
+
|
114
|
+
@containers.command()
|
115
|
+
@click.argument("name")
|
116
|
+
@click.pass_context
|
117
|
+
def check(ctx: click.Context, name: str):
|
118
|
+
if name is None:
|
119
|
+
name = "artefacts"
|
120
|
+
handler = ContainerMgr()
|
121
|
+
result = handler.check(name)
|
122
|
+
if ctx.parent is None:
|
123
|
+
# Print only if the command is called directly.
|
124
|
+
print(f"Package {name} exists and ready to use.")
|
125
|
+
return result
|
126
|
+
|
127
|
+
|
128
|
+
@containers.command()
|
129
|
+
@click.argument("jobname")
|
130
|
+
@click.option(
|
131
|
+
"--config",
|
132
|
+
callback=config_validation,
|
133
|
+
default="artefacts.yaml",
|
134
|
+
help="Path to the Artefacts configuration file. It defaults to `./artefacts.yaml`",
|
135
|
+
)
|
136
|
+
@click.option(
|
137
|
+
"--with-gui",
|
138
|
+
"with_gui",
|
139
|
+
default=False,
|
140
|
+
help="Show any GUI if any is created by the test runs. By default, UI elements are run but hidden---only test logs are returned. Please note GUI often assume an X11 environment, typically with Qt, so this may not work without a appropriate environment.",
|
141
|
+
)
|
142
|
+
@click.pass_context
|
143
|
+
def run(ctx: click.Context, jobname: str, config: str, with_gui: bool):
|
144
|
+
try:
|
145
|
+
artefacts_config = read_config(config)
|
146
|
+
except FileNotFoundError:
|
147
|
+
raise click.ClickException(f"Project config file not found: {config}")
|
148
|
+
project = artefacts_config["project"]
|
149
|
+
handler = ContainerMgr()
|
150
|
+
params = dict(
|
151
|
+
image=f"{project.strip().lower()}/{jobname}",
|
152
|
+
project=project,
|
153
|
+
jobname=jobname,
|
154
|
+
with_gui=with_gui,
|
155
|
+
# Hidden settings primarily useful to Artefacts developers
|
156
|
+
api_url=os.environ.get("ARTEFACTS_API_URL", DEFAULT_API_URL),
|
157
|
+
api_key=os.environ.get("ARTEFACTS_KEY", None),
|
158
|
+
)
|
159
|
+
container, logs = handler.run(**params)
|
160
|
+
if container:
|
161
|
+
print(f"Package run complete: Container Id for inspection: {container['Id']}")
|
162
|
+
else:
|
163
|
+
print("Package run failed:")
|
164
|
+
for entry in logs:
|
165
|
+
print("\t- " + entry)
|
@@ -22,6 +22,10 @@ class DockerManager(CMgr):
|
|
22
22
|
|
23
23
|
def build(self, **kwargs) -> Tuple[str, Generator]:
|
24
24
|
kwargs["tag"] = kwargs.pop("name")
|
25
|
+
# Ensure `path` is a string, the Docker package does not support pathlib.
|
26
|
+
kwargs["path"] = str(kwargs.pop("path"))
|
27
|
+
# Remove intermediate containers
|
28
|
+
kwargs["rm"] = True
|
25
29
|
logs = []
|
26
30
|
img_id = None
|
27
31
|
for entry in self.client.build(**kwargs):
|
@@ -32,7 +36,7 @@ class DockerManager(CMgr):
|
|
32
36
|
if "stream" in data:
|
33
37
|
line = data["stream"].strip()
|
34
38
|
if not line.startswith("---") and len(line) > 0:
|
35
|
-
print(line)
|
39
|
+
print(f"[{kwargs['tag'].split('/')[-1]}] {line}")
|
36
40
|
logs.append(line)
|
37
41
|
elif "aux" in data and "ID" in data["aux"]:
|
38
42
|
img_id = data["aux"]["ID"]
|
@@ -1,4 +1,5 @@
|
|
1
1
|
import os
|
2
|
+
from pathlib import Path
|
2
3
|
import subprocess
|
3
4
|
import sys
|
4
5
|
from typing import Union
|
@@ -6,6 +7,8 @@ from typing import Union
|
|
6
7
|
import click
|
7
8
|
|
8
9
|
import artefacts_copava as copava
|
10
|
+
from artefacts import ARTEFACTS_DEFAULT_OUTPUT_DIR
|
11
|
+
from artefacts.cli import WarpRun
|
9
12
|
|
10
13
|
|
11
14
|
def run_and_save_logs(
|
@@ -97,3 +100,17 @@ def pretty_print_config_error(
|
|
97
100
|
# Must not happen, so broad definition, but we want to know fast.
|
98
101
|
raise Exception(f"Unacceptable data type for config error formatting: {errors}")
|
99
102
|
return output
|
103
|
+
|
104
|
+
|
105
|
+
def add_output_from_default(run: WarpRun) -> None:
|
106
|
+
"""
|
107
|
+
Add every file found under ARTEFACTS_DEFAULT_OUTPUT_DIR to the set of files
|
108
|
+
uploaded to Artefacts for the run argument.
|
109
|
+
|
110
|
+
The default folder is created either directly, or more generally by Artefacts
|
111
|
+
toolkit libraries.
|
112
|
+
"""
|
113
|
+
if ARTEFACTS_DEFAULT_OUTPUT_DIR.exists() and ARTEFACTS_DEFAULT_OUTPUT_DIR.is_dir():
|
114
|
+
for root, dirs, files in os.walk(ARTEFACTS_DEFAULT_OUTPUT_DIR):
|
115
|
+
for file in files:
|
116
|
+
run.log_artifacts(Path(root) / Path(file))
|
@@ -1,8 +1,13 @@
|
|
1
|
-
# file generated by
|
1
|
+
# file generated by setuptools-scm
|
2
2
|
# don't change, don't track in version control
|
3
|
+
|
4
|
+
__all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
|
5
|
+
|
3
6
|
TYPE_CHECKING = False
|
4
7
|
if TYPE_CHECKING:
|
5
|
-
from typing import Tuple
|
8
|
+
from typing import Tuple
|
9
|
+
from typing import Union
|
10
|
+
|
6
11
|
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
7
12
|
else:
|
8
13
|
VERSION_TUPLE = object
|
@@ -12,5 +17,5 @@ __version__: str
|
|
12
17
|
__version_tuple__: VERSION_TUPLE
|
13
18
|
version_tuple: VERSION_TUPLE
|
14
19
|
|
15
|
-
__version__ = version = '0.
|
16
|
-
__version_tuple__ = version_tuple = (0,
|
20
|
+
__version__ = version = '0.7.1'
|
21
|
+
__version_tuple__ = version_tuple = (0, 7, 1)
|
@@ -1,14 +1,16 @@
|
|
1
1
|
Metadata-Version: 2.2
|
2
2
|
Name: artefacts_cli
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.7.1
|
4
4
|
Author-email: FD <fabian@artefacts.com>, AGC <alejandro@artefacts.com>, TN <tomo@artefacts.com>, EP <eric@artefacts.com>
|
5
5
|
Project-URL: Homepage, https://github.com/art-e-fact/artefacts-client
|
6
6
|
Project-URL: Bug Tracker, https://github.com/art-e-fact/artefacts-client/issues
|
7
|
+
Project-URL: Changelog, https://github.com/art-e-fact/artefacts-client/CHANGELOG.md
|
7
8
|
Classifier: Programming Language :: Python :: 3
|
8
9
|
Classifier: License :: OSI Approved :: Apache Software License
|
9
10
|
Classifier: Operating System :: OS Independent
|
10
11
|
Requires-Python: >=3.8
|
11
12
|
Description-Content-Type: text/markdown
|
13
|
+
Requires-Dist: artefacts-c2d>=1.7.1
|
12
14
|
Requires-Dist: artefacts-copava>=0.1.11
|
13
15
|
Requires-Dist: click>=8.0.4
|
14
16
|
Requires-Dist: gitignore_parser>=0.1.11
|
@@ -42,7 +44,7 @@ Requires-Dist: twine; extra == "dev"
|
|
42
44
|
CLI to the Artefacts platform.
|
43
45
|
|
44
46
|
[](https://docs.artefacts.com/)
|
45
|
-
[](https://github.com/astral-sh/ruff)
|
46
48
|
|
47
49
|
## Requirements
|
48
50
|
|
@@ -1,8 +1,10 @@
|
|
1
|
+
CHANGELOG.md
|
1
2
|
README.md
|
2
3
|
README_INTERNAL.md
|
3
4
|
artefacts.yaml
|
4
5
|
pyproject.toml
|
5
6
|
pytest.ini
|
7
|
+
artefacts/__init__.py
|
6
8
|
artefacts/cli/__init__.py
|
7
9
|
artefacts/cli/app.py
|
8
10
|
artefacts/cli/app_containers.py
|
@@ -54,6 +56,7 @@ tests/cli/test_other.py
|
|
54
56
|
tests/cli/test_parameters.py
|
55
57
|
tests/cli/test_ros1.py
|
56
58
|
tests/cli/test_ros2.py
|
59
|
+
tests/cli/test_utils.py
|
57
60
|
tests/cli/test_warp.py
|
58
61
|
tests/cli/containers/__init__.py
|
59
62
|
tests/cli/containers/test_utils.py
|
@@ -19,6 +19,7 @@ classifiers = [
|
|
19
19
|
"Operating System :: OS Independent",
|
20
20
|
]
|
21
21
|
dependencies = [
|
22
|
+
"artefacts-c2d>=1.7.1",
|
22
23
|
"artefacts-copava>=0.1.11",
|
23
24
|
"click>=8.0.4",
|
24
25
|
"gitignore_parser>=0.1.11",
|
@@ -63,6 +64,7 @@ dev = [
|
|
63
64
|
[project.urls]
|
64
65
|
"Homepage" = "https://github.com/art-e-fact/artefacts-client"
|
65
66
|
"Bug Tracker" = "https://github.com/art-e-fact/artefacts-client/issues"
|
67
|
+
"Changelog" = "https://github.com/art-e-fact/artefacts-client/CHANGELOG.md"
|
66
68
|
|
67
69
|
[project.scripts]
|
68
70
|
artefacts = "artefacts.cli.app:artefacts"
|
@@ -0,0 +1,36 @@
|
|
1
|
+
import os
|
2
|
+
from uuid import uuid4
|
3
|
+
|
4
|
+
|
5
|
+
from artefacts.cli.app_containers import containers
|
6
|
+
|
7
|
+
|
8
|
+
def test_container_package_exists(cli_runner):
|
9
|
+
result = cli_runner.invoke(containers, [])
|
10
|
+
assert result.exit_code == 0
|
11
|
+
|
12
|
+
|
13
|
+
def test_container_package_build_specific_dockerfile(
|
14
|
+
cli_runner, dockerfile_available, docker_mocker
|
15
|
+
):
|
16
|
+
dockerfile = "non_standard_dockerfile"
|
17
|
+
result = cli_runner.invoke(containers, ["build", "--dockerfile", dockerfile])
|
18
|
+
dockerfile_available.assert_any_call(dockerfile)
|
19
|
+
assert result.exit_code == 0
|
20
|
+
|
21
|
+
|
22
|
+
def test_container_package_build(
|
23
|
+
cli_runner, dockerfile_available, docker_mocker, sample_artefacts_config
|
24
|
+
):
|
25
|
+
before = len(docker_mocker.images())
|
26
|
+
result = cli_runner.invoke(containers, ["build"])
|
27
|
+
assert result.exit_code == 0
|
28
|
+
assert len(docker_mocker.images()) == before + len(sample_artefacts_config["jobs"])
|
29
|
+
for job_name in sample_artefacts_config["jobs"]:
|
30
|
+
# Check the images exist, with name following our naming convention project/job_name
|
31
|
+
assert (
|
32
|
+
docker_mocker.get_image(
|
33
|
+
f"{sample_artefacts_config['project'].lower()}/{job_name}"
|
34
|
+
)
|
35
|
+
is not None
|
36
|
+
)
|
@@ -13,14 +13,6 @@ from artefacts.cli.app import (
|
|
13
13
|
)
|
14
14
|
|
15
15
|
|
16
|
-
@pytest.fixture(scope="class")
|
17
|
-
def project_with_key(cli_runner):
|
18
|
-
project_name = "_pytest-project_"
|
19
|
-
add_key_to_conf(project_name, "MYAPIKEY")
|
20
|
-
yield project_name
|
21
|
-
cli_runner.invoke(delete, [project_name])
|
22
|
-
|
23
|
-
|
24
16
|
def test_hello(cli_runner):
|
25
17
|
project_name = "_pytest-project_"
|
26
18
|
result = cli_runner.invoke(hello, [project_name])
|
@@ -60,9 +52,9 @@ def test_run_with_conf_invalid_jobname(cli_runner, project_with_key):
|
|
60
52
|
)
|
61
53
|
assert result.exit_code == 1
|
62
54
|
assert result.output == (
|
63
|
-
"Connecting to https://app.artefacts.com/api using ApiKey\n"
|
64
|
-
f"Starting tests
|
65
|
-
"Error: Job
|
55
|
+
"[invalid_job_name] Connecting to https://app.artefacts.com/api using ApiKey\n"
|
56
|
+
f"[invalid_job_name] Starting tests\n"
|
57
|
+
"[invalid_job_name] Error: Job name not defined\n"
|
66
58
|
)
|
67
59
|
|
68
60
|
|
@@ -72,13 +64,13 @@ def test_run_with_conf(cli_runner, project_with_key):
|
|
72
64
|
)
|
73
65
|
assert result.exit_code == 0
|
74
66
|
assert result.output == (
|
75
|
-
"Connecting to https://app.artefacts.com/api using ApiKey\n"
|
76
|
-
f"Starting tests
|
77
|
-
"Starting scenario 1/2: basic-tests\n"
|
78
|
-
"
|
79
|
-
"Starting scenario 2/2: other-tests\n"
|
80
|
-
"
|
81
|
-
"Done\n"
|
67
|
+
"[simple_job] Connecting to https://app.artefacts.com/api using ApiKey\n"
|
68
|
+
f"[simple_job] Starting tests\n"
|
69
|
+
"[simple_job] Starting scenario 1/2: basic-tests\n"
|
70
|
+
"[simple_job] Performing dry run\n"
|
71
|
+
"[simple_job] Starting scenario 2/2: other-tests\n"
|
72
|
+
"[simple_job] Performing dry run\n"
|
73
|
+
"[simple_job] Done\n"
|
82
74
|
)
|
83
75
|
|
84
76
|
|
@@ -89,13 +81,13 @@ def test_run_with_mode_ros1(cli_runner, project_with_key):
|
|
89
81
|
)
|
90
82
|
assert result.exit_code == 0
|
91
83
|
assert result.output == (
|
92
|
-
"Connecting to https://app.artefacts.com/api using ApiKey\n"
|
93
|
-
"Starting tests
|
94
|
-
"Starting scenario 1/2: basic-tests\n"
|
95
|
-
"
|
96
|
-
"Starting scenario 2/2: other-tests\n"
|
97
|
-
"
|
98
|
-
"Done\n"
|
84
|
+
"[simple_job] Connecting to https://app.artefacts.com/api using ApiKey\n"
|
85
|
+
"[simple_job] Starting tests\n"
|
86
|
+
"[simple_job] Starting scenario 1/2: basic-tests\n"
|
87
|
+
"[simple_job] Performing dry run\n"
|
88
|
+
"[simple_job] Starting scenario 2/2: other-tests\n"
|
89
|
+
"[simple_job] Performing dry run\n"
|
90
|
+
"[simple_job] Done\n"
|
99
91
|
)
|
100
92
|
|
101
93
|
|
@@ -139,3 +131,15 @@ def test_run_remote_with_conf_invalid_jobname(cli_runner, project_with_key):
|
|
139
131
|
def test_APIConf(project_with_key):
|
140
132
|
conf = APIConf(project_with_key)
|
141
133
|
assert conf.headers["Authorization"] == "ApiKey MYAPIKEY"
|
134
|
+
|
135
|
+
|
136
|
+
def test_upload_default_dir(cli_runner, project_with_key, mocker):
|
137
|
+
# Note the patch applies to the object loaded in app, rather than the original in utils.
|
138
|
+
# https://docs.python.org/3/library/unittest.mock.html#where-to-patch
|
139
|
+
sut = mocker.patch("artefacts.cli.app.add_output_from_default")
|
140
|
+
result = cli_runner.invoke(
|
141
|
+
run, ["simple_job", "--config", "tests/fixtures/warp.yaml", "--dryrun"]
|
142
|
+
)
|
143
|
+
assert result.exit_code == 0
|
144
|
+
# Called twice in this config.
|
145
|
+
assert sut.call_count == 2
|
@@ -0,0 +1,24 @@
|
|
1
|
+
import pytest
|
2
|
+
|
3
|
+
from artefacts.cli import WarpJob, WarpRun
|
4
|
+
from artefacts.cli.app import APIConf
|
5
|
+
from artefacts.cli.utils import add_output_from_default
|
6
|
+
|
7
|
+
|
8
|
+
@pytest.fixture
|
9
|
+
def new_run(project_with_key):
|
10
|
+
job = WarpJob(
|
11
|
+
project_with_key, APIConf(project_with_key), "jobname", {}, dryrun=True
|
12
|
+
)
|
13
|
+
return WarpRun(job=job, scenario={}, run_n=0)
|
14
|
+
|
15
|
+
|
16
|
+
def test_adds_nothing_on_missing_default_output(new_run, mocker, project_with_key):
|
17
|
+
path = mocker.patch("artefacts.cli.utils.ARTEFACTS_DEFAULT_OUTPUT_DIR")
|
18
|
+
mocked = {
|
19
|
+
"exists.return_value": False,
|
20
|
+
"is_dir.return_value": True,
|
21
|
+
}
|
22
|
+
path.configure_mock(**mocked)
|
23
|
+
add_output_from_default(new_run)
|
24
|
+
assert len(new_run.uploads) == 0
|
@@ -6,6 +6,9 @@ from click.testing import CliRunner
|
|
6
6
|
|
7
7
|
from tests.utils import docker_mock
|
8
8
|
|
9
|
+
from artefacts.cli.app import add_key_to_conf, delete
|
10
|
+
from artefacts.cli.utils import read_config
|
11
|
+
|
9
12
|
|
10
13
|
def dockerfile_presence(mocker, value: bool):
|
11
14
|
original = os.path.exists
|
@@ -39,3 +42,16 @@ def docker_mocker(module_mocker):
|
|
39
42
|
@pytest.fixture(scope="module")
|
40
43
|
def cli_runner():
|
41
44
|
return CliRunner()
|
45
|
+
|
46
|
+
|
47
|
+
@pytest.fixture(scope="class")
|
48
|
+
def project_with_key(cli_runner):
|
49
|
+
project_name = "_pytest-project_"
|
50
|
+
add_key_to_conf(project_name, "MYAPIKEY")
|
51
|
+
yield project_name
|
52
|
+
cli_runner.invoke(delete, [project_name])
|
53
|
+
|
54
|
+
|
55
|
+
@pytest.fixture(scope="session")
|
56
|
+
def sample_artefacts_config():
|
57
|
+
return read_config(os.path.join(os.path.dirname(__file__), "..", "artefacts.yaml"))
|
@@ -1,99 +0,0 @@
|
|
1
|
-
import os
|
2
|
-
|
3
|
-
import click
|
4
|
-
|
5
|
-
from artefacts.cli.constants import DEFAULT_API_URL
|
6
|
-
from artefacts.cli.utils import config_validation, read_config
|
7
|
-
from artefacts.cli.containers.utils import ContainerMgr
|
8
|
-
|
9
|
-
|
10
|
-
@click.group()
|
11
|
-
@click.option("--debug/--no-debug", default=False)
|
12
|
-
@click.pass_context
|
13
|
-
def containers(ctx: click.Context, debug: bool):
|
14
|
-
ctx.ensure_object(dict)
|
15
|
-
ctx.obj["debug"] = debug
|
16
|
-
|
17
|
-
|
18
|
-
@containers.command()
|
19
|
-
@click.option(
|
20
|
-
"--path",
|
21
|
-
default=".",
|
22
|
-
help="Path to the root of the project, where a Dockerfile is available.",
|
23
|
-
)
|
24
|
-
@click.option(
|
25
|
-
"--dockerfile",
|
26
|
-
default="Dockerfile",
|
27
|
-
help="File name of the container definition file. Defaults to the standard Dockerfile inside the project root (see --path)",
|
28
|
-
)
|
29
|
-
@click.option(
|
30
|
-
"--name",
|
31
|
-
required=False,
|
32
|
-
help="Name for the generated container",
|
33
|
-
)
|
34
|
-
@click.pass_context
|
35
|
-
def build(ctx: click.Context, path: str, dockerfile: str, name: str):
|
36
|
-
if not os.path.exists(os.path.join(path, dockerfile)):
|
37
|
-
raise click.ClickException(
|
38
|
-
f"No {dockerfile} found here. I cannot build the container."
|
39
|
-
)
|
40
|
-
if name is None:
|
41
|
-
name = "artefacts"
|
42
|
-
handler = ContainerMgr()
|
43
|
-
image, _ = handler.build(path=path, name=name)
|
44
|
-
print(f"Package complete in image: {image}")
|
45
|
-
|
46
|
-
|
47
|
-
@containers.command()
|
48
|
-
@click.argument("name")
|
49
|
-
@click.pass_context
|
50
|
-
def check(ctx: click.Context, name: str):
|
51
|
-
if name is None:
|
52
|
-
name = "artefacts"
|
53
|
-
handler = ContainerMgr()
|
54
|
-
result = handler.check(name)
|
55
|
-
if ctx.parent is None:
|
56
|
-
# Print only if the command is called directly.
|
57
|
-
print(f"Package {name} exists and ready to use.")
|
58
|
-
return result
|
59
|
-
|
60
|
-
|
61
|
-
@containers.command()
|
62
|
-
@click.argument("image")
|
63
|
-
@click.argument("jobname")
|
64
|
-
@click.option(
|
65
|
-
"--config",
|
66
|
-
callback=config_validation,
|
67
|
-
default="artefacts.yaml",
|
68
|
-
help="Artefacts config file.",
|
69
|
-
)
|
70
|
-
@click.option(
|
71
|
-
"--with-gui",
|
72
|
-
"with_gui",
|
73
|
-
default=False,
|
74
|
-
help="Show any GUI if any is created by the test runs. By default, UI elements are run but hidden---only test logs are returned. Please note GUI often assume an X11 environment, typically with Qt, so this may not work without a appropriate environment.",
|
75
|
-
)
|
76
|
-
@click.pass_context
|
77
|
-
def run(ctx: click.Context, image: str, jobname: str, config: str, with_gui: bool):
|
78
|
-
try:
|
79
|
-
artefacts_config = read_config(config)
|
80
|
-
except FileNotFoundError:
|
81
|
-
raise click.ClickException(f"Project config file not found: {config}")
|
82
|
-
project = artefacts_config["project"]
|
83
|
-
handler = ContainerMgr()
|
84
|
-
params = dict(
|
85
|
-
image=image,
|
86
|
-
project=project,
|
87
|
-
jobname=jobname,
|
88
|
-
with_gui=with_gui,
|
89
|
-
# Hidden settings primarily useful to Artefacts developers
|
90
|
-
api_url=os.environ.get("ARTEFACTS_API_URL", DEFAULT_API_URL),
|
91
|
-
api_key=os.environ.get("ARTEFACTS_KEY", None),
|
92
|
-
)
|
93
|
-
container, logs = handler.run(**params)
|
94
|
-
if container:
|
95
|
-
print(f"Package run complete: Container Id for inspection: {container['Id']}")
|
96
|
-
else:
|
97
|
-
print("Package run failed:")
|
98
|
-
for entry in logs:
|
99
|
-
print("\t- " + entry)
|
@@ -1,53 +0,0 @@
|
|
1
|
-
import os
|
2
|
-
from uuid import uuid4
|
3
|
-
|
4
|
-
|
5
|
-
from artefacts.cli.app_containers import containers
|
6
|
-
|
7
|
-
|
8
|
-
def test_container_package_exists(cli_runner):
|
9
|
-
result = cli_runner.invoke(containers, [])
|
10
|
-
assert result.exit_code == 0
|
11
|
-
|
12
|
-
|
13
|
-
def test_container_package_build_specific_dockerfile(
|
14
|
-
cli_runner, dockerfile_available, docker_mocker
|
15
|
-
):
|
16
|
-
dockerfile = "non_standard_dockerfile"
|
17
|
-
result = cli_runner.invoke(containers, ["build", "--dockerfile", dockerfile])
|
18
|
-
dockerfile_available.assert_any_call(os.path.join(".", dockerfile))
|
19
|
-
assert result.exit_code == 0
|
20
|
-
|
21
|
-
|
22
|
-
def test_container_package_build_specific_dockerfile_missing(
|
23
|
-
cli_runner, dockerfile_not_available
|
24
|
-
):
|
25
|
-
dockerfile = "non_standard_dockerfile"
|
26
|
-
result = cli_runner.invoke(containers, ["build", "--dockerfile", dockerfile])
|
27
|
-
dockerfile_not_available.assert_any_call(os.path.join(".", dockerfile))
|
28
|
-
assert result.exit_code == 1
|
29
|
-
assert (
|
30
|
-
result.output.strip()
|
31
|
-
== f"Error: No {dockerfile} found here. I cannot build the container."
|
32
|
-
)
|
33
|
-
|
34
|
-
|
35
|
-
def test_container_package_build_specific_image_name(
|
36
|
-
cli_runner, dockerfile_available, docker_mocker
|
37
|
-
):
|
38
|
-
name = str(uuid4())
|
39
|
-
before = len(docker_mocker.images())
|
40
|
-
result = cli_runner.invoke(containers, ["build", "--name", name])
|
41
|
-
assert result.exit_code == 0
|
42
|
-
assert len(docker_mocker.images()) == before + 1
|
43
|
-
assert docker_mocker.get_image(name).Repository == name
|
44
|
-
|
45
|
-
|
46
|
-
def test_container_package_build_default_image_name(
|
47
|
-
cli_runner, dockerfile_available, docker_mocker
|
48
|
-
):
|
49
|
-
before = len(docker_mocker.images())
|
50
|
-
result = cli_runner.invoke(containers, ["build"])
|
51
|
-
assert result.exit_code == 0
|
52
|
-
assert len(docker_mocker.images()) == before + 1
|
53
|
-
assert docker_mocker.get_image("artefacts") is not None
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|