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 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
- click.echo(f"Connecting to {self.api_url} using {auth_type}")
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="artefacts",
247
- help="[Experimental] Run the job using the image name passed here. Only used when running with --in-container set.",
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
- image_exist = ctx.invoke(containers.check, name=with_image)
286
- if not no_rebuild or not image_exist:
287
- if image_exist:
288
- announce = "Rebuilding container image to prepare the run..."
289
- else:
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, path=".", dockerfile="Dockerfile", name=with_image
297
+ containers.build,
298
+ root=".",
294
299
  )
295
- click.echo("Build complete.\n")
296
- click.echo("Starting the container run...")
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
- warpconfig = read_config(config)
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
- raise click.ClickException(f"Job {jobname} not defined")
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: f{job_type}")
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
- raise click.ClickException(
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
- raise click.ClickException(
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
- raise click.ClickException(
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("performing dry run")
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
- raise click.ClickException("artefacts failed to execute the tests")
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
- raise click.ClickException(
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
- raise click.ClickException(
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("performing dry run")
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
- raise click.ClickException("run command not specified for scenario")
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("performing dry run")
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"Missing linked GitHub repository. Please link a GitHub repository at {dashboard_url}/settings"
592
+ f"Billing issue, please go to {dashboard_url}/settings to correct: {result['error']}"
562
593
  )
563
594
 
564
- raise click.ClickException(
565
- f"Error getting project info: {result['message']}"
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 = ""
@@ -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, where a Dockerfile is available.",
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="File name of the container definition file. Defaults to the standard Dockerfile inside the project root (see --path)",
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 container",
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(ctx: click.Context, path: str, dockerfile: str, name: str):
36
- if not os.path.exists(os.path.join(path, dockerfile)):
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"No {dockerfile} found here. I cannot build the container."
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
- if name is None:
41
- name = "artefacts"
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
- image, _ = handler.build(path=path, name=name)
44
- print(f"Package complete in image: {image}")
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 config file.",
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, image: str, jobname: str, config: str, with_gui: bool):
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=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
- return_code = run_and_save_logs(
61
- " ".join(command),
62
- shell=True,
63
- executable="/bin/bash",
64
- env={
65
- **os.environ,
66
- **{
67
- "ROS_LOG_DIR": ros_log_dir,
68
- "ARTEFACTS_SCENARIO_PARAMS_FILE": TMP_SCENARIO_PARAMS_YAML,
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
- output_path=os.path.join(run.output_path, "test_process_log.txt"),
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, output_path, shell=False, executable=None, env=None, cwd=None
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
 
@@ -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
- "suite": "unittest.suite.TestSuite",
56
- "errors": 1,
57
- "failures": 0,
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
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '0.7.0'
21
- __version_tuple__ = version_tuple = (0, 7, 0)
20
+ __version__ = version = '0.7.2'
21
+ __version_tuple__ = version_tuple = (0, 7, 2)
@@ -1,14 +1,16 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: artefacts_cli
3
- Version: 0.7.0
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
  [![Documentation](https://img.shields.io/badge/documentation-blue.svg?style=flat-square)](https://docs.artefacts.com/)
45
- [![Code style: Black-compatible with Ruff](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
47
+ [![Ruff Style](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](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=tEknMMi7WhAPnq8NlGgLuLqqoCDWssZqs1lB3eNlVg4,22571
4
- artefacts/cli/app_containers.py,sha256=dsyzN8UzGNwxkhV8BsFK7Sz9EOL6Se3YpeiUgzC2qic,3099
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=9Ax_WQIOV_cohKz3H1eo1LnWiahiaqxO8r99doMmhEc,4466
13
- artefacts/cli/utils.py,sha256=GJM2QdF_ewzPWr8NzO5BbrhyZdSSk9DNX12Nbwc4bnI,3814
14
- artefacts/cli/utils_ros.py,sha256=3EFoMrzBdlhLc-wAL3mmS5sSw_pACkurYhssKHqYJsI,2089
15
- artefacts/cli/version.py,sha256=itvIHlqPKoO_13qf_yPD2pmcp0U4z1s19vvBGZM927Q,511
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=fsGTzpj7Sj7ykCBxzaYlIt_so1yfWJ2j6ktxsWjvdvY,4073
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.0.dist-info/METADATA,sha256=bYCIMBdVBOLHHW38dmsUqXj-hoFczntRhszgykDm_eM,3034
21
- artefacts_cli-0.7.0.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
22
- artefacts_cli-0.7.0.dist-info/entry_points.txt,sha256=nlTXRzilNjccbi53FgaRWCQPkG-pv61HRkaCkrKjlec,58
23
- artefacts_cli-0.7.0.dist-info/top_level.txt,sha256=FdaMV1C9m36MWa-2Stm5xVODv7hss_nRYNwR83j_7ow,10
24
- artefacts_cli-0.7.0.dist-info/RECORD,,
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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.8.0)
2
+ Generator: setuptools (76.1.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5