artefacts-cli 0.7.0__py3-none-any.whl → 0.7.2__py3-none-any.whl
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/app.py +90 -50
- artefacts/cli/app_containers.py +80 -14
- artefacts/cli/containers/docker.py +5 -1
- artefacts/cli/ros2.py +98 -14
- artefacts/cli/utils.py +15 -1
- artefacts/cli/utils_ros.py +21 -15
- artefacts/cli/version.py +2 -2
- {artefacts_cli-0.7.0.dist-info → artefacts_cli-0.7.2.dist-info}/METADATA +4 -2
- {artefacts_cli-0.7.0.dist-info → artefacts_cli-0.7.2.dist-info}/RECORD +12 -12
- {artefacts_cli-0.7.0.dist-info → artefacts_cli-0.7.2.dist-info}/WHEEL +1 -1
- {artefacts_cli-0.7.0.dist-info → artefacts_cli-0.7.2.dist-info}/entry_points.txt +0 -0
- {artefacts_cli-0.7.0.dist-info → artefacts_cli-0.7.2.dist-info}/top_level.txt +0 -0
artefacts/cli/app.py
CHANGED
@@ -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,8 +246,8 @@ 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
253
|
"--no-rebuild",
|
@@ -281,38 +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
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
announce = "Building container image to prepare the run..."
|
291
|
-
click.echo(announce)
|
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:
|
292
296
|
ctx.invoke(
|
293
|
-
containers.build,
|
297
|
+
containers.build,
|
298
|
+
root=".",
|
294
299
|
)
|
295
|
-
click.echo("
|
296
|
-
click.echo("
|
300
|
+
click.echo(f"[{jobname}] Container image ready")
|
301
|
+
click.echo(f"[{jobname}] Run in container")
|
297
302
|
return ctx.invoke(
|
298
303
|
containers.run,
|
299
|
-
image=with_image,
|
300
304
|
jobname=jobname,
|
301
305
|
config=config,
|
302
306
|
with_gui=with_gui,
|
303
307
|
)
|
304
308
|
|
305
|
-
|
306
|
-
|
307
|
-
project_id = warpconfig["project"]
|
308
|
-
api_conf = APIConf(project_id)
|
309
|
-
click.echo(f"Starting tests for {project_id}")
|
309
|
+
api_conf = APIConf(project_id, jobname)
|
310
|
+
click.echo(f"[{jobname}] Starting tests")
|
310
311
|
if jobname not in warpconfig["jobs"]:
|
311
|
-
|
312
|
+
click.secho(f"[{jobname}] Error: Job name not defined", err=True, bold=True)
|
313
|
+
raise click.Abort()
|
312
314
|
jobconf = warpconfig["jobs"][jobname]
|
313
315
|
job_type = jobconf.get("type", "test")
|
314
316
|
if job_type not in ["test"]:
|
315
|
-
click.echo(f"Job type not supported:
|
317
|
+
click.echo(f"[{jobname}] Job type not supported: {job_type}")
|
316
318
|
return
|
317
319
|
|
318
320
|
framework = jobconf["runtime"].get("framework", None)
|
@@ -321,19 +323,19 @@ def run(
|
|
321
323
|
if framework in DEPRECATED_FRAMEWORKS.keys():
|
322
324
|
migrated_framework = DEPRECATED_FRAMEWORKS[framework]
|
323
325
|
click.echo(
|
324
|
-
f"The selected framework '{framework}' is deprecated. Using '{migrated_framework}' instead."
|
326
|
+
f"[{jobname}] The selected framework '{framework}' is deprecated. Using '{migrated_framework}' instead."
|
325
327
|
)
|
326
328
|
framework = migrated_framework
|
327
329
|
|
328
330
|
if framework not in SUPPORTED_FRAMEWORKS:
|
329
331
|
click.echo(
|
330
|
-
f"WARNING: framework: '{framework}' is not officially supported. Attempting run."
|
332
|
+
f"[{jobname}] WARNING: framework: '{framework}' is not officially supported. Attempting run."
|
331
333
|
)
|
332
334
|
|
333
335
|
batch_index = os.environ.get("AWS_BATCH_JOB_ARRAY_INDEX", None)
|
334
336
|
if batch_index is not None:
|
335
337
|
batch_index = int(batch_index)
|
336
|
-
click.echo(f"AWS BATCH ARRAY DETECTED, batch_index={batch_index}")
|
338
|
+
click.echo(f"[{jobname}] AWS BATCH ARRAY DETECTED, batch_index={batch_index}")
|
337
339
|
scenarios, first = generate_scenarios(jobconf, batch_index)
|
338
340
|
context = None
|
339
341
|
execution_context = getpass.getuser() + "@" + platform.node()
|
@@ -356,30 +358,46 @@ def run(
|
|
356
358
|
first,
|
357
359
|
)
|
358
360
|
except AuthenticationError:
|
359
|
-
|
360
|
-
"Unable to authenticate (Stage: Job initialisation), please check your project name and API key"
|
361
|
+
click.secho(
|
362
|
+
f"[{jobname}] Unable to authenticate (Stage: Job initialisation), please check your project name and API key",
|
363
|
+
err=True,
|
364
|
+
bold=True,
|
361
365
|
)
|
366
|
+
raise click.Abort()
|
362
367
|
|
363
368
|
job_success = True
|
364
369
|
for scenario_n, scenario in enumerate(scenarios):
|
365
370
|
click.echo(
|
366
|
-
f"Starting scenario {scenario_n + 1}/{len(scenarios)}: {scenario['name']}"
|
371
|
+
f"[{jobname}] Starting scenario {scenario_n + 1}/{len(scenarios)}: {scenario['name']}"
|
367
372
|
)
|
368
373
|
try:
|
369
374
|
run = warpjob.new_run(scenario)
|
370
375
|
except AuthenticationError:
|
371
|
-
|
372
|
-
"Unable to authenticate (Stage: Job run), please check your project name and API key"
|
376
|
+
click.secho(
|
377
|
+
f"[{jobname}] Unable to authenticate (Stage: Job run), please check your project name and API key",
|
378
|
+
err=True,
|
379
|
+
bold=True,
|
373
380
|
)
|
381
|
+
raise click.Abort()
|
374
382
|
if framework is not None and framework.startswith("ros2:"):
|
375
383
|
from artefacts.cli.ros2 import run_ros2_tests
|
384
|
+
from artefacts.cli.utils_ros import get_TestSuite_error_result
|
376
385
|
|
377
386
|
if "ros_testfile" not in run.params:
|
378
|
-
|
379
|
-
"Test launch file not specified for ros2 project"
|
387
|
+
click.secho(
|
388
|
+
f"[{jobname}] Test launch file not specified for ros2 project",
|
389
|
+
err=True,
|
390
|
+
bold=True,
|
391
|
+
)
|
392
|
+
result = get_TestSuite_error_result(
|
393
|
+
scenario["name"],
|
394
|
+
"launch_test file not specified error",
|
395
|
+
f"Please specify a `ros_testfile` in the artefacts.yaml scenario configuration.",
|
380
396
|
)
|
397
|
+
run.log_tests_results([result], False)
|
398
|
+
run.stop()
|
381
399
|
if dryrun:
|
382
|
-
click.echo("
|
400
|
+
click.echo(f"[{jobname}] Performing dry run")
|
383
401
|
results, success = {}, True
|
384
402
|
else:
|
385
403
|
try:
|
@@ -388,25 +406,36 @@ def run(
|
|
388
406
|
warpjob.stop()
|
389
407
|
warpjob.log_tests_result(False)
|
390
408
|
click.secho(e, bold=True, err=True)
|
391
|
-
|
409
|
+
click.secho(
|
410
|
+
f"[{jobname}] artefacts failed to execute the tests",
|
411
|
+
err=True,
|
412
|
+
bold=True,
|
413
|
+
)
|
414
|
+
raise click.Abort()
|
392
415
|
if success is None:
|
393
416
|
run.stop()
|
394
417
|
warpjob.stop()
|
395
418
|
warpjob.log_tests_result(job_success)
|
396
|
-
|
397
|
-
"Not able to execute tests. Make sure that ROS2 is sourced and that your launch file syntax is correct."
|
419
|
+
click.secho(
|
420
|
+
f"[{jobname}] Not able to execute tests. Make sure that ROS2 is sourced and that your launch file syntax is correct.",
|
421
|
+
err=True,
|
422
|
+
bold=True,
|
398
423
|
)
|
424
|
+
raise click.Abort()
|
399
425
|
if not success:
|
400
426
|
job_success = False
|
401
427
|
elif framework is not None and framework.startswith("ros1:"):
|
402
428
|
from artefacts.cli.ros1 import run_ros1_tests
|
403
429
|
|
404
430
|
if "ros_testfile" not in run.params:
|
405
|
-
|
406
|
-
"Test launch file not specified for ros1 project"
|
431
|
+
click.secho(
|
432
|
+
f"[{jobname}] Test launch file not specified for ros1 project",
|
433
|
+
err=True,
|
434
|
+
bold=True,
|
407
435
|
)
|
436
|
+
raise click.Abort()
|
408
437
|
if dryrun:
|
409
|
-
click.echo("
|
438
|
+
click.echo(f"[{jobname}] Performing dry run")
|
410
439
|
results, success = {}, True
|
411
440
|
else:
|
412
441
|
results, success = run_ros1_tests(run)
|
@@ -416,9 +445,14 @@ def run(
|
|
416
445
|
from artefacts.cli.other import run_other_tests
|
417
446
|
|
418
447
|
if "run" not in run.params:
|
419
|
-
|
448
|
+
click.secho(
|
449
|
+
f"[{jobname}] run command not specified for scenario",
|
450
|
+
err=True,
|
451
|
+
bold=True,
|
452
|
+
)
|
453
|
+
raise click.Abort()
|
420
454
|
if dryrun:
|
421
|
-
click.echo("
|
455
|
+
click.echo(f"[{jobname}] Performing dry run")
|
422
456
|
results, success = {}, True
|
423
457
|
else:
|
424
458
|
results, success = run_other_tests(run)
|
@@ -432,7 +466,7 @@ def run(
|
|
432
466
|
|
433
467
|
run.stop()
|
434
468
|
warpjob.log_tests_result(job_success)
|
435
|
-
click.echo("Done")
|
469
|
+
click.echo(f"[{jobname}] Done")
|
436
470
|
time.sleep(random.random() * 1)
|
437
471
|
|
438
472
|
warpjob.stop()
|
@@ -553,17 +587,23 @@ def run_remote(config, description, jobname, skip_validation=False):
|
|
553
587
|
f"Missing access! Please make sure your API key is added at {dashboard_url}/settings"
|
554
588
|
)
|
555
589
|
|
556
|
-
if
|
557
|
-
upload_urls_response.status_code == 401
|
558
|
-
and result["message"] == "no linked repository"
|
559
|
-
):
|
590
|
+
if upload_urls_response.status_code == 402:
|
560
591
|
raise click.ClickException(
|
561
|
-
f"
|
592
|
+
f"Billing issue, please go to {dashboard_url}/settings to correct: {result['error']}"
|
562
593
|
)
|
563
594
|
|
564
|
-
|
565
|
-
|
566
|
-
|
595
|
+
if "message" in result:
|
596
|
+
raise click.ClickException(
|
597
|
+
f"Error getting project info: {result['message']}"
|
598
|
+
)
|
599
|
+
elif "error" in result:
|
600
|
+
raise click.ClickException(
|
601
|
+
f"Error getting project info: {result['error']}"
|
602
|
+
)
|
603
|
+
else:
|
604
|
+
raise click.ClickException(
|
605
|
+
f"Error getting project info: {upload_urls_response.status_code} {upload_urls_response.reason}. Response text: {upload_urls_response.text}."
|
606
|
+
)
|
567
607
|
|
568
608
|
upload_urls = upload_urls_response.json()["upload_urls"]
|
569
609
|
url = ""
|
artefacts/cli/app_containers.py
CHANGED
@@ -1,7 +1,10 @@
|
|
1
1
|
import os
|
2
|
+
from pathlib import Path
|
2
3
|
|
3
4
|
import click
|
4
5
|
|
6
|
+
from c2d.core import Converter
|
7
|
+
|
5
8
|
from artefacts.cli.constants import DEFAULT_API_URL
|
6
9
|
from artefacts.cli.utils import config_validation, read_config
|
7
10
|
from artefacts.cli.containers.utils import ContainerMgr
|
@@ -19,29 +22,93 @@ def containers(ctx: click.Context, debug: bool):
|
|
19
22
|
@click.option(
|
20
23
|
"--path",
|
21
24
|
default=".",
|
22
|
-
help="Path to the root of the project
|
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.",
|
23
31
|
)
|
24
32
|
@click.option(
|
25
33
|
"--dockerfile",
|
26
34
|
default="Dockerfile",
|
27
|
-
help="
|
35
|
+
help="Path to a custom Dockerfile. Defaults to Dockerfile under `path` (see option of the same name).",
|
28
36
|
)
|
29
37
|
@click.option(
|
30
38
|
"--name",
|
31
39
|
required=False,
|
32
|
-
help="Name for the generated
|
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`",
|
33
47
|
)
|
34
48
|
@click.pass_context
|
35
|
-
def build(
|
36
|
-
|
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:
|
37
60
|
raise click.ClickException(
|
38
|
-
f"
|
61
|
+
f"Project config file not found: {config}. Please provide an Artefacts configuration file to proceed (running `artefacts init` allows to generate one)."
|
39
62
|
)
|
40
|
-
|
41
|
-
|
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
|
+
)
|
42
105
|
handler = ContainerMgr()
|
43
|
-
|
44
|
-
|
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.")
|
45
112
|
|
46
113
|
|
47
114
|
@containers.command()
|
@@ -59,13 +126,12 @@ def check(ctx: click.Context, name: str):
|
|
59
126
|
|
60
127
|
|
61
128
|
@containers.command()
|
62
|
-
@click.argument("image")
|
63
129
|
@click.argument("jobname")
|
64
130
|
@click.option(
|
65
131
|
"--config",
|
66
132
|
callback=config_validation,
|
67
133
|
default="artefacts.yaml",
|
68
|
-
help="Artefacts
|
134
|
+
help="Path to the Artefacts configuration file. It defaults to `./artefacts.yaml`",
|
69
135
|
)
|
70
136
|
@click.option(
|
71
137
|
"--with-gui",
|
@@ -74,7 +140,7 @@ def check(ctx: click.Context, name: str):
|
|
74
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.",
|
75
141
|
)
|
76
142
|
@click.pass_context
|
77
|
-
def run(ctx: click.Context,
|
143
|
+
def run(ctx: click.Context, jobname: str, config: str, with_gui: bool):
|
78
144
|
try:
|
79
145
|
artefacts_config = read_config(config)
|
80
146
|
except FileNotFoundError:
|
@@ -82,7 +148,7 @@ def run(ctx: click.Context, image: str, jobname: str, config: str, with_gui: boo
|
|
82
148
|
project = artefacts_config["project"]
|
83
149
|
handler = ContainerMgr()
|
84
150
|
params = dict(
|
85
|
-
image=
|
151
|
+
image=f"{project.strip().lower()}/{jobname}",
|
86
152
|
project=project,
|
87
153
|
jobname=jobname,
|
88
154
|
with_gui=with_gui,
|
@@ -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"]
|
artefacts/cli/ros2.py
CHANGED
@@ -4,10 +4,58 @@ import os
|
|
4
4
|
import shutil
|
5
5
|
|
6
6
|
from .utils import run_and_save_logs
|
7
|
-
from .utils_ros import parse_tests_results
|
7
|
+
from .utils_ros import parse_tests_results, get_TestSuite_error_result
|
8
8
|
from .parameters import TMP_SCENARIO_PARAMS_YAML
|
9
9
|
|
10
10
|
|
11
|
+
# custom exceptions raised when trying to run ros2 tests
|
12
|
+
class Launch_test_CmdNotFoundError(FileNotFoundError):
|
13
|
+
pass
|
14
|
+
|
15
|
+
|
16
|
+
class LaunchTestFileNotFoundError(FileNotFoundError):
|
17
|
+
pass
|
18
|
+
|
19
|
+
|
20
|
+
class BadLaunchTestFileError(Exception):
|
21
|
+
pass
|
22
|
+
|
23
|
+
|
24
|
+
def ros2_run_and_save_logs(
|
25
|
+
args, output_path, shell=False, executable=None, env=None, cwd=None
|
26
|
+
):
|
27
|
+
try:
|
28
|
+
return_code, stdout, stderr = run_and_save_logs(
|
29
|
+
args,
|
30
|
+
output_path,
|
31
|
+
shell=shell,
|
32
|
+
executable=executable,
|
33
|
+
env=env,
|
34
|
+
cwd=cwd,
|
35
|
+
with_output=True,
|
36
|
+
)
|
37
|
+
except FileNotFoundError:
|
38
|
+
raise Launch_test_CmdNotFoundError(
|
39
|
+
f"Running {args} failed. Please check that `launch_test` is installed and in the path."
|
40
|
+
)
|
41
|
+
if return_code == 2:
|
42
|
+
# check the proc stderr for `launch_test: error: Test file '[filename]' does not exist`
|
43
|
+
|
44
|
+
if "does not exist" in stderr:
|
45
|
+
raise LaunchTestFileNotFoundError(
|
46
|
+
f"Running {args} failed. Please check that the launch file exists."
|
47
|
+
)
|
48
|
+
if "launch_test: error: " in stderr:
|
49
|
+
# example errors:
|
50
|
+
# "has no attribute 'generate_test_description'"
|
51
|
+
# "error: name 'xxx' is not defined"
|
52
|
+
raise BadLaunchTestFileError(
|
53
|
+
f"Running {args} failed. Check that the launch_test file syntax is correct."
|
54
|
+
)
|
55
|
+
|
56
|
+
return return_code
|
57
|
+
|
58
|
+
|
11
59
|
def generate_scenario_parameter_output(params: dict, param_file: str):
|
12
60
|
"""
|
13
61
|
Store `params` in `param_file` and convert to ros2 param file nested format,
|
@@ -57,19 +105,54 @@ def run_ros2_tests(run):
|
|
57
105
|
# Main: test execution
|
58
106
|
# shell=True required to support command list items that are strings with spaces
|
59
107
|
# (this way, scenario["ros_testfile"] can be either a path to the launch file or '<package_name> <launch_name>')
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
108
|
+
try:
|
109
|
+
return_code = ros2_run_and_save_logs(
|
110
|
+
" ".join(command),
|
111
|
+
shell=True,
|
112
|
+
executable="/bin/bash",
|
113
|
+
env={
|
114
|
+
**os.environ,
|
115
|
+
**{
|
116
|
+
"ROS_LOG_DIR": ros_log_dir,
|
117
|
+
"ARTEFACTS_SCENARIO_PARAMS_FILE": TMP_SCENARIO_PARAMS_YAML,
|
118
|
+
},
|
69
119
|
},
|
70
|
-
|
71
|
-
|
72
|
-
|
120
|
+
output_path=os.path.join(run.output_path, "test_process_log.txt"),
|
121
|
+
)
|
122
|
+
except Launch_test_CmdNotFoundError as e:
|
123
|
+
# raise Exception(
|
124
|
+
# f"Running {scenario['ros_testfile']} failed. Please check that the launch file exists."
|
125
|
+
# )
|
126
|
+
# closes the run properly and mark as errored
|
127
|
+
# dict matching junit xml format for test execution error
|
128
|
+
result = get_TestSuite_error_result(
|
129
|
+
scenario["ros_testfile"],
|
130
|
+
"launch_test command not found",
|
131
|
+
"Please check that launch_test is installed and in the path. Exception: {e}",
|
132
|
+
)
|
133
|
+
results = [result]
|
134
|
+
run.log_tests_results(results, False)
|
135
|
+
return results, False
|
136
|
+
except LaunchTestFileNotFoundError as e:
|
137
|
+
result = get_TestSuite_error_result(
|
138
|
+
scenario["ros_testfile"],
|
139
|
+
"launch_test file not found",
|
140
|
+
f"Please check that the `ros_testfile` config is correct. Exception: {e}",
|
141
|
+
)
|
142
|
+
results = [result]
|
143
|
+
run.log_tests_results(results, False)
|
144
|
+
return results, False
|
145
|
+
except BadLaunchTestFileError as e:
|
146
|
+
launch_test_file = scenario["ros_testfile"]
|
147
|
+
result = get_TestSuite_error_result(
|
148
|
+
scenario["ros_testfile"],
|
149
|
+
"launch_test file syntax error",
|
150
|
+
f"Please check that the file specified in `ros_testfile` config is a valid ros_test file. You may be able to identify issues by doing `launch_test {launch_test_file}`. Exception: {e}",
|
151
|
+
)
|
152
|
+
results = [result]
|
153
|
+
run.log_tests_results(results, False)
|
154
|
+
return results, False
|
155
|
+
|
73
156
|
if return_code == 2:
|
74
157
|
raise Exception(
|
75
158
|
f"Running {scenario['ros_testfile']} failed. Please check that the launch file exists."
|
@@ -82,10 +165,11 @@ def run_ros2_tests(run):
|
|
82
165
|
|
83
166
|
# parse xml generated by launch_test
|
84
167
|
results, success = parse_tests_results(test_result_file_path)
|
168
|
+
# upload logs anyway to help user debug
|
169
|
+
run.log_artifacts(run.output_path)
|
85
170
|
if success is None:
|
86
171
|
run.log_tests_results(results, False)
|
87
172
|
return results, success
|
88
|
-
run.log_artifacts(run.output_path)
|
89
173
|
|
90
174
|
# upload any additional files in the folders specified by the user in artefacts.yaml
|
91
175
|
for output in scenario.get("output_dirs", []):
|
artefacts/cli/utils.py
CHANGED
@@ -12,7 +12,13 @@ from artefacts.cli import WarpRun
|
|
12
12
|
|
13
13
|
|
14
14
|
def run_and_save_logs(
|
15
|
-
args,
|
15
|
+
args,
|
16
|
+
output_path,
|
17
|
+
shell=False,
|
18
|
+
executable=None,
|
19
|
+
env=None,
|
20
|
+
cwd=None,
|
21
|
+
with_output=False,
|
16
22
|
):
|
17
23
|
"""
|
18
24
|
Run a command and save stdout and stderr to a file in output_path
|
@@ -20,6 +26,7 @@ def run_and_save_logs(
|
|
20
26
|
Note: explicitly list used named params instead of using **kwargs to avoid typing issue: https://github.com/microsoft/pyright/issues/455#issuecomment-780076232
|
21
27
|
"""
|
22
28
|
output_file = open(output_path, "wb")
|
29
|
+
|
23
30
|
proc = subprocess.Popen(
|
24
31
|
args,
|
25
32
|
stdout=subprocess.PIPE, # Capture stdout
|
@@ -30,17 +37,24 @@ def run_and_save_logs(
|
|
30
37
|
cwd=cwd,
|
31
38
|
)
|
32
39
|
# write test-process stdout and stderr into file and stdout
|
40
|
+
stderr_content = ""
|
41
|
+
stdout_content = ""
|
33
42
|
if proc.stdout:
|
34
43
|
for line in proc.stdout:
|
35
44
|
decoded_line = line.decode()
|
36
45
|
sys.stdout.write(decoded_line)
|
37
46
|
output_file.write(line)
|
47
|
+
stdout_content += decoded_line
|
38
48
|
if proc.stderr:
|
49
|
+
output_file.write("[STDERR]\n".encode())
|
39
50
|
for line in proc.stderr:
|
40
51
|
decoded_line = line.decode()
|
41
52
|
sys.stderr.write(decoded_line)
|
42
53
|
output_file.write(line)
|
54
|
+
stderr_content += decoded_line
|
43
55
|
proc.wait()
|
56
|
+
if with_output:
|
57
|
+
return proc.returncode, stdout_content, stderr_content
|
44
58
|
return proc.returncode
|
45
59
|
|
46
60
|
|
artefacts/cli/utils_ros.py
CHANGED
@@ -6,6 +6,22 @@ class FailureElement(Element):
|
|
6
6
|
message = Attr()
|
7
7
|
|
8
8
|
|
9
|
+
def get_TestSuite_error_result(test_suite_name, name, error_msg):
|
10
|
+
return {
|
11
|
+
"suite": test_suite_name,
|
12
|
+
"errors": 1,
|
13
|
+
"failures": 0,
|
14
|
+
"tests": 1,
|
15
|
+
"details": [
|
16
|
+
{
|
17
|
+
"name": name,
|
18
|
+
"failure_message": error_msg,
|
19
|
+
"result": "failure",
|
20
|
+
}
|
21
|
+
],
|
22
|
+
}
|
23
|
+
|
24
|
+
|
9
25
|
def parse_tests_results(file):
|
10
26
|
def parse_suite(suite):
|
11
27
|
nonlocal success, results
|
@@ -50,19 +66,9 @@ def parse_tests_results(file):
|
|
50
66
|
except Exception as e:
|
51
67
|
print(f"[Exception in parse_tests_results] {e}")
|
52
68
|
print("Test result xml could not be loaded, marking success as False")
|
53
|
-
results =
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
"tests": 1,
|
59
|
-
"details": [
|
60
|
-
{
|
61
|
-
"name": "Error parsing XML test results",
|
62
|
-
"failure_message": f"The test may have timed out. Exception: {e}",
|
63
|
-
"result": "failure",
|
64
|
-
}
|
65
|
-
],
|
66
|
-
}
|
67
|
-
]
|
69
|
+
results = get_TestSuite_error_result(
|
70
|
+
"unittest.suite.TestSuite",
|
71
|
+
"Error parsing XML test results",
|
72
|
+
f"The test may have timed out. Exception: {e}",
|
73
|
+
)
|
68
74
|
return results, None
|
artefacts/cli/version.py
CHANGED
@@ -1,14 +1,16 @@
|
|
1
1
|
Metadata-Version: 2.2
|
2
2
|
Name: artefacts_cli
|
3
|
-
Version: 0.7.
|
3
|
+
Version: 0.7.2
|
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,7 +1,7 @@
|
|
1
1
|
artefacts/__init__.py,sha256=VLmogtpRQeJjQjAORV8ClSJ5qF-57Hxx3apvgy9H1zk,76
|
2
2
|
artefacts/cli/__init__.py,sha256=pt8OK66hMeQUxT9iLcvzYIIjFGrPS63ecWo8hS0T2qQ,11980
|
3
|
-
artefacts/cli/app.py,sha256=
|
4
|
-
artefacts/cli/app_containers.py,sha256=
|
3
|
+
artefacts/cli/app.py,sha256=lTdfJsNCdUh8A0V-MvvYUNoB1rgHAMpXxInqPgPeC4g,24241
|
4
|
+
artefacts/cli/app_containers.py,sha256=AOTiXjyJzYZn6JqfW26N3ehGQWcEVQ5mEmKDdp9a6tQ,5636
|
5
5
|
artefacts/cli/bagparser.py,sha256=FE_QaztC9pg4hQzTjGSdyve6mzZbHJbyqa3wqvZSbxE,3702
|
6
6
|
artefacts/cli/constants.py,sha256=bvsVDwqkAc49IZN7j6k6IL6EG87bECHd_VINtKJqbv8,320
|
7
7
|
artefacts/cli/errors.py,sha256=BiCRo3IwVjtEotaFtmwsGTZiX-TRE69KqLrEQItLsag,34
|
@@ -9,16 +9,16 @@ artefacts/cli/logger.py,sha256=MP8WDImHA3BKVsn55BMWtGP5-aCmXl5ViVPtIo3jKk4,242
|
|
9
9
|
artefacts/cli/other.py,sha256=7NvzlspvG0zF7sryR-QznwdLupXLln1BKWxHB9VuEcc,1160
|
10
10
|
artefacts/cli/parameters.py,sha256=msf2aG-tmw0ahxwrPpB2W6KqdMj5A-nw9DPG9flkHTg,788
|
11
11
|
artefacts/cli/ros1.py,sha256=rKepZckAuy5O_qraF2CW5GiTmTZHar7LRD4pvESy6T0,9622
|
12
|
-
artefacts/cli/ros2.py,sha256=
|
13
|
-
artefacts/cli/utils.py,sha256=
|
14
|
-
artefacts/cli/utils_ros.py,sha256=
|
15
|
-
artefacts/cli/version.py,sha256=
|
12
|
+
artefacts/cli/ros2.py,sha256=RGAK228jjbWzcJW-ONGmkAl5QGvHC39SMij9GUthUTY,7587
|
13
|
+
artefacts/cli/utils.py,sha256=oy56o3N361srwhIvbMxwSPg8I_-tC7xcWtTSINTF2rE,4125
|
14
|
+
artefacts/cli/utils_ros.py,sha256=ucJrIMLcTh26ioduj3xiozgxqXZghkyTMHWI9BsHNjI,2156
|
15
|
+
artefacts/cli/version.py,sha256=d8Q6A3AJbbiXbr7TTxrk_DMbAQYVhMa2jrNwZsIl2LM,511
|
16
16
|
artefacts/cli/containers/__init__.py,sha256=K0efkJXNCqXH-qYBqhCE_8zVUCHbVmeuKH-y_fE8s4M,2254
|
17
|
-
artefacts/cli/containers/docker.py,sha256=
|
17
|
+
artefacts/cli/containers/docker.py,sha256=R0yA-aIZCyYWN7gzim_Dhn1owpKI9ekMu6qbz5URYbQ,4311
|
18
18
|
artefacts/cli/containers/utils.py,sha256=bILX0uvazUJq7hoqKk4ztRzI_ZerYs04XQdKdx1ltjk,2002
|
19
19
|
artefacts/wrappers/artefacts_ros1_meta.launch,sha256=9tN7_0xLH8jW27KYFerhF3NuWDx2dED3ks_qoGVZAPw,1412
|
20
|
-
artefacts_cli-0.7.
|
21
|
-
artefacts_cli-0.7.
|
22
|
-
artefacts_cli-0.7.
|
23
|
-
artefacts_cli-0.7.
|
24
|
-
artefacts_cli-0.7.
|
20
|
+
artefacts_cli-0.7.2.dist-info/METADATA,sha256=BLLhtZXxpR49NRGgSHH9qVu4bImVgt5KJLFbHZsKxFk,3183
|
21
|
+
artefacts_cli-0.7.2.dist-info/WHEEL,sha256=beeZ86-EfXScwlR_HKu4SllMC9wUEj_8Z_4FJ3egI2w,91
|
22
|
+
artefacts_cli-0.7.2.dist-info/entry_points.txt,sha256=nlTXRzilNjccbi53FgaRWCQPkG-pv61HRkaCkrKjlec,58
|
23
|
+
artefacts_cli-0.7.2.dist-info/top_level.txt,sha256=FdaMV1C9m36MWa-2Stm5xVODv7hss_nRYNwR83j_7ow,10
|
24
|
+
artefacts_cli-0.7.2.dist-info/RECORD,,
|
File without changes
|
File without changes
|