artefacts-cli 0.7.1__tar.gz → 0.7.3__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.
Files changed (71) hide show
  1. {artefacts_cli-0.7.1 → artefacts_cli-0.7.3}/CHANGELOG.md +16 -0
  2. {artefacts_cli-0.7.1/artefacts_cli.egg-info → artefacts_cli-0.7.3}/PKG-INFO +2 -2
  3. {artefacts_cli-0.7.1 → artefacts_cli-0.7.3}/artefacts/cli/app.py +60 -29
  4. {artefacts_cli-0.7.1 → artefacts_cli-0.7.3}/artefacts/cli/ros2.py +106 -15
  5. {artefacts_cli-0.7.1 → artefacts_cli-0.7.3}/artefacts/cli/utils.py +15 -1
  6. {artefacts_cli-0.7.1 → artefacts_cli-0.7.3}/artefacts/cli/utils_ros.py +21 -15
  7. {artefacts_cli-0.7.1 → artefacts_cli-0.7.3}/artefacts/cli/version.py +2 -2
  8. {artefacts_cli-0.7.1 → artefacts_cli-0.7.3/artefacts_cli.egg-info}/PKG-INFO +2 -2
  9. {artefacts_cli-0.7.1 → artefacts_cli-0.7.3}/artefacts_cli.egg-info/SOURCES.txt +1 -0
  10. {artefacts_cli-0.7.1 → artefacts_cli-0.7.3}/tests/cli/test_cli.py +6 -6
  11. artefacts_cli-0.7.3/tests/cli/test_ros2.py +100 -0
  12. {artefacts_cli-0.7.1 → artefacts_cli-0.7.3}/tests/cli/test_utils.py +15 -1
  13. artefacts_cli-0.7.3/tests/fixtures/bad_launch_test.py +0 -0
  14. {artefacts_cli-0.7.1 → artefacts_cli-0.7.3}/tests/fixtures/warp.yaml +19 -0
  15. artefacts_cli-0.7.1/tests/cli/test_ros2.py +0 -41
  16. {artefacts_cli-0.7.1 → artefacts_cli-0.7.3}/README.md +0 -0
  17. {artefacts_cli-0.7.1 → artefacts_cli-0.7.3}/README_INTERNAL.md +0 -0
  18. {artefacts_cli-0.7.1 → artefacts_cli-0.7.3}/artefacts/__init__.py +0 -0
  19. {artefacts_cli-0.7.1 → artefacts_cli-0.7.3}/artefacts/cli/__init__.py +0 -0
  20. {artefacts_cli-0.7.1 → artefacts_cli-0.7.3}/artefacts/cli/app_containers.py +0 -0
  21. {artefacts_cli-0.7.1 → artefacts_cli-0.7.3}/artefacts/cli/bagparser.py +0 -0
  22. {artefacts_cli-0.7.1 → artefacts_cli-0.7.3}/artefacts/cli/constants.py +0 -0
  23. {artefacts_cli-0.7.1 → artefacts_cli-0.7.3}/artefacts/cli/containers/__init__.py +0 -0
  24. {artefacts_cli-0.7.1 → artefacts_cli-0.7.3}/artefacts/cli/containers/docker.py +0 -0
  25. {artefacts_cli-0.7.1 → artefacts_cli-0.7.3}/artefacts/cli/containers/utils.py +0 -0
  26. {artefacts_cli-0.7.1 → artefacts_cli-0.7.3}/artefacts/cli/errors.py +0 -0
  27. {artefacts_cli-0.7.1 → artefacts_cli-0.7.3}/artefacts/cli/logger.py +0 -0
  28. {artefacts_cli-0.7.1 → artefacts_cli-0.7.3}/artefacts/cli/other.py +0 -0
  29. {artefacts_cli-0.7.1 → artefacts_cli-0.7.3}/artefacts/cli/parameters.py +0 -0
  30. {artefacts_cli-0.7.1 → artefacts_cli-0.7.3}/artefacts/cli/ros1.py +0 -0
  31. {artefacts_cli-0.7.1 → artefacts_cli-0.7.3}/artefacts/wrappers/artefacts_ros1_meta.launch +0 -0
  32. {artefacts_cli-0.7.1 → artefacts_cli-0.7.3}/artefacts.yaml +0 -0
  33. {artefacts_cli-0.7.1 → artefacts_cli-0.7.3}/artefacts_cli.egg-info/dependency_links.txt +0 -0
  34. {artefacts_cli-0.7.1 → artefacts_cli-0.7.3}/artefacts_cli.egg-info/entry_points.txt +0 -0
  35. {artefacts_cli-0.7.1 → artefacts_cli-0.7.3}/artefacts_cli.egg-info/requires.txt +0 -0
  36. {artefacts_cli-0.7.1 → artefacts_cli-0.7.3}/artefacts_cli.egg-info/top_level.txt +0 -0
  37. {artefacts_cli-0.7.1 → artefacts_cli-0.7.3}/bin/release +0 -0
  38. {artefacts_cli-0.7.1 → artefacts_cli-0.7.3}/infra-tests/test_run_remote.yaml +0 -0
  39. {artefacts_cli-0.7.1 → artefacts_cli-0.7.3}/infra-tests/turtlesim1/ros_workspace/src/turtle_odometry/CMakeLists.txt +0 -0
  40. {artefacts_cli-0.7.1 → artefacts_cli-0.7.3}/infra-tests/turtlesim1/ros_workspace/src/turtle_odometry/launch/test_meta.launch +0 -0
  41. {artefacts_cli-0.7.1 → artefacts_cli-0.7.3}/infra-tests/turtlesim1/ros_workspace/src/turtle_odometry/launch/test_turtle.launch +0 -0
  42. {artefacts_cli-0.7.1 → artefacts_cli-0.7.3}/infra-tests/turtlesim1/ros_workspace/src/turtle_odometry/launch/turtle_odometry.launch +0 -0
  43. {artefacts_cli-0.7.1 → artefacts_cli-0.7.3}/infra-tests/turtlesim1/ros_workspace/src/turtle_odometry/package.xml +0 -0
  44. {artefacts_cli-0.7.1 → artefacts_cli-0.7.3}/infra-tests/turtlesim1/ros_workspace/src/turtle_odometry/setup.py +0 -0
  45. {artefacts_cli-0.7.1 → artefacts_cli-0.7.3}/infra-tests/turtlesim1/ros_workspace/src/turtle_odometry/src/TestTurtle.py +0 -0
  46. {artefacts_cli-0.7.1 → artefacts_cli-0.7.3}/infra-tests/turtlesim1/ros_workspace/src/turtle_odometry/src/__init__.py +0 -0
  47. {artefacts_cli-0.7.1 → artefacts_cli-0.7.3}/infra-tests/turtlesim1/ros_workspace/src/turtle_odometry/src/turtle_odom.py +0 -0
  48. {artefacts_cli-0.7.1 → artefacts_cli-0.7.3}/infra-tests/turtlesim1/ros_workspace/src/turtle_odometry/src/turtle_post_process.py +0 -0
  49. {artefacts_cli-0.7.1 → artefacts_cli-0.7.3}/infra-tests/turtlesim1/ros_workspace/src/turtle_odometry/src/turtle_trajectory.py +0 -0
  50. {artefacts_cli-0.7.1 → artefacts_cli-0.7.3}/infra-tests/turtlesim1/ros_workspace/src/turtle_odometry/test/viz_turtle_odom.xml +0 -0
  51. {artefacts_cli-0.7.1 → artefacts_cli-0.7.3}/infra-tests/turtlesim2/launch_turtle.py +0 -0
  52. {artefacts_cli-0.7.1 → artefacts_cli-0.7.3}/infra-tests/turtlesim2/sample_node.py +0 -0
  53. {artefacts_cli-0.7.1 → artefacts_cli-0.7.3}/pyproject.toml +0 -0
  54. {artefacts_cli-0.7.1 → artefacts_cli-0.7.3}/pytest.ini +0 -0
  55. {artefacts_cli-0.7.1 → artefacts_cli-0.7.3}/setup.cfg +0 -0
  56. {artefacts_cli-0.7.1 → artefacts_cli-0.7.3}/tests/__init__.py +0 -0
  57. {artefacts_cli-0.7.1 → artefacts_cli-0.7.3}/tests/cli/__init__.py +0 -0
  58. {artefacts_cli-0.7.1 → artefacts_cli-0.7.3}/tests/cli/containers/__init__.py +0 -0
  59. {artefacts_cli-0.7.1 → artefacts_cli-0.7.3}/tests/cli/containers/test_utils.py +0 -0
  60. {artefacts_cli-0.7.1 → artefacts_cli-0.7.3}/tests/cli/test_app_containers.py +0 -0
  61. {artefacts_cli-0.7.1 → artefacts_cli-0.7.3}/tests/cli/test_config_validation.py +0 -0
  62. {artefacts_cli-0.7.1 → artefacts_cli-0.7.3}/tests/cli/test_other.py +0 -0
  63. {artefacts_cli-0.7.1 → artefacts_cli-0.7.3}/tests/cli/test_parameters.py +0 -0
  64. {artefacts_cli-0.7.1 → artefacts_cli-0.7.3}/tests/cli/test_ros1.py +0 -0
  65. {artefacts_cli-0.7.1 → artefacts_cli-0.7.3}/tests/cli/test_warp.py +0 -0
  66. {artefacts_cli-0.7.1 → artefacts_cli-0.7.3}/tests/conftest.py +0 -0
  67. {artefacts_cli-0.7.1 → artefacts_cli-0.7.3}/tests/fixtures/artefacts_deprecated.yaml +0 -0
  68. {artefacts_cli-0.7.1 → artefacts_cli-0.7.3}/tests/fixtures/artefacts_ros1.yaml +0 -0
  69. {artefacts_cli-0.7.1 → artefacts_cli-0.7.3}/tests/fixtures/warp-env-param.yaml +0 -0
  70. {artefacts_cli-0.7.1 → artefacts_cli-0.7.3}/tests/test_config_validation.py +0 -0
  71. {artefacts_cli-0.7.1 → artefacts_cli-0.7.3}/tests/utils/docker_mock.py +0 -0
@@ -16,6 +16,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
16
16
 
17
17
  - New logging messages and format.
18
18
 
19
+ ## [0.7.3] - 2025-03-26
20
+
21
+ ### Fixed
22
+
23
+ - Handle nested ROS params in the configuration file.
24
+
25
+ ## [0.7.2] - 2025-03-19
26
+
27
+ ### Fixed
28
+
29
+ - Fixed error handling (bug from misuse of Click's `ClickException`).
30
+
31
+ ### Changed
32
+
33
+ - Improved error handling and messages.
34
+
19
35
 
20
36
  ## [0.7.1] - 2025-03-14
21
37
 
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: artefacts_cli
3
- Version: 0.7.1
3
+ Version: 0.7.3
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
@@ -309,8 +309,8 @@ def run(
309
309
  api_conf = APIConf(project_id, jobname)
310
310
  click.echo(f"[{jobname}] Starting tests")
311
311
  if jobname not in warpconfig["jobs"]:
312
- click.echo(f"[{jobname}] Error: Job name not defined")
313
- raise click.ClickException()
312
+ click.secho(f"[{jobname}] Error: Job name not defined", err=True, bold=True)
313
+ raise click.Abort()
314
314
  jobconf = warpconfig["jobs"][jobname]
315
315
  job_type = jobconf.get("type", "test")
316
316
  if job_type not in ["test"]:
@@ -358,10 +358,12 @@ def run(
358
358
  first,
359
359
  )
360
360
  except AuthenticationError:
361
- click.echo(
362
- f"[{jobname}] 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,
363
365
  )
364
- raise click.ClickException()
366
+ raise click.Abort()
365
367
 
366
368
  job_success = True
367
369
  for scenario_n, scenario in enumerate(scenarios):
@@ -371,18 +373,29 @@ def run(
371
373
  try:
372
374
  run = warpjob.new_run(scenario)
373
375
  except AuthenticationError:
374
- click.echo(
375
- f"[{jobname}] 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,
376
380
  )
377
- raise click.ClickException()
381
+ raise click.Abort()
378
382
  if framework is not None and framework.startswith("ros2:"):
379
383
  from artefacts.cli.ros2 import run_ros2_tests
384
+ from artefacts.cli.utils_ros import get_TestSuite_error_result
380
385
 
381
386
  if "ros_testfile" not in run.params:
382
- click.echo(
383
- f"[{jobname}] 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.",
384
396
  )
385
- raise click.ClickException()
397
+ run.log_tests_results([result], False)
398
+ run.stop()
386
399
  if dryrun:
387
400
  click.echo(f"[{jobname}] Performing dry run")
388
401
  results, success = {}, True
@@ -393,26 +406,34 @@ def run(
393
406
  warpjob.stop()
394
407
  warpjob.log_tests_result(False)
395
408
  click.secho(e, bold=True, err=True)
396
- click.echo(f"[{jobname}] artefacts failed to execute the tests")
397
- raise click.ClickException()
409
+ click.secho(
410
+ f"[{jobname}] artefacts failed to execute the tests",
411
+ err=True,
412
+ bold=True,
413
+ )
414
+ raise click.Abort()
398
415
  if success is None:
399
416
  run.stop()
400
417
  warpjob.stop()
401
418
  warpjob.log_tests_result(job_success)
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."
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,
404
423
  )
405
- raise click.ClickException()
424
+ raise click.Abort()
406
425
  if not success:
407
426
  job_success = False
408
427
  elif framework is not None and framework.startswith("ros1:"):
409
428
  from artefacts.cli.ros1 import run_ros1_tests
410
429
 
411
430
  if "ros_testfile" not in run.params:
412
- click.echo(
413
- f"[{jobname}] 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,
414
435
  )
415
- raise click.ClickException()
436
+ raise click.Abort()
416
437
  if dryrun:
417
438
  click.echo(f"[{jobname}] Performing dry run")
418
439
  results, success = {}, True
@@ -424,8 +445,12 @@ def run(
424
445
  from artefacts.cli.other import run_other_tests
425
446
 
426
447
  if "run" not in run.params:
427
- click.echo(f"[{jobname}] run command not specified for scenario")
428
- raise click.ClickException()
448
+ click.secho(
449
+ f"[{jobname}] run command not specified for scenario",
450
+ err=True,
451
+ bold=True,
452
+ )
453
+ raise click.Abort()
429
454
  if dryrun:
430
455
  click.echo(f"[{jobname}] Performing dry run")
431
456
  results, success = {}, True
@@ -562,17 +587,23 @@ def run_remote(config, description, jobname, skip_validation=False):
562
587
  f"Missing access! Please make sure your API key is added at {dashboard_url}/settings"
563
588
  )
564
589
 
565
- if (
566
- upload_urls_response.status_code == 401
567
- and result["message"] == "no linked repository"
568
- ):
590
+ if upload_urls_response.status_code == 402:
569
591
  raise click.ClickException(
570
- 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']}"
571
593
  )
572
594
 
573
- raise click.ClickException(
574
- f"Error getting project info: {upload_urls_response.status_code} {upload_urls_response.reason}. Response text: {upload_urls_response.text}."
575
- )
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
+ )
576
607
 
577
608
  upload_urls = upload_urls_response.json()["upload_urls"]
578
609
  url = ""
@@ -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,
@@ -22,7 +70,14 @@ def generate_scenario_parameter_output(params: dict, param_file: str):
22
70
  return
23
71
  if node not in content:
24
72
  content[node] = {"ros__parameters": {}}
25
- content[node]["ros__parameters"][pname] = v
73
+ # handles nested keys for params in the form of dot notation
74
+ current_level = content[node]["ros__parameters"]
75
+ keys = pname.split(".")
76
+ for key in keys[:-1]:
77
+ if key not in current_level:
78
+ current_level[key] = {}
79
+ current_level = current_level[key]
80
+ current_level[keys[-1]] = v
26
81
  with open(param_file, "w") as f:
27
82
  yaml.dump(content, f)
28
83
 
@@ -57,19 +112,54 @@ def run_ros2_tests(run):
57
112
  # Main: test execution
58
113
  # shell=True required to support command list items that are strings with spaces
59
114
  # (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,
115
+ try:
116
+ return_code = ros2_run_and_save_logs(
117
+ " ".join(command),
118
+ shell=True,
119
+ executable="/bin/bash",
120
+ env={
121
+ **os.environ,
122
+ **{
123
+ "ROS_LOG_DIR": ros_log_dir,
124
+ "ARTEFACTS_SCENARIO_PARAMS_FILE": TMP_SCENARIO_PARAMS_YAML,
125
+ },
69
126
  },
70
- },
71
- output_path=os.path.join(run.output_path, "test_process_log.txt"),
72
- )
127
+ output_path=os.path.join(run.output_path, "test_process_log.txt"),
128
+ )
129
+ except Launch_test_CmdNotFoundError as e:
130
+ # raise Exception(
131
+ # f"Running {scenario['ros_testfile']} failed. Please check that the launch file exists."
132
+ # )
133
+ # closes the run properly and mark as errored
134
+ # dict matching junit xml format for test execution error
135
+ result = get_TestSuite_error_result(
136
+ scenario["ros_testfile"],
137
+ "launch_test command not found",
138
+ "Please check that launch_test is installed and in the path. Exception: {e}",
139
+ )
140
+ results = [result]
141
+ run.log_tests_results(results, False)
142
+ return results, False
143
+ except LaunchTestFileNotFoundError as e:
144
+ result = get_TestSuite_error_result(
145
+ scenario["ros_testfile"],
146
+ "launch_test file not found",
147
+ f"Please check that the `ros_testfile` config is correct. Exception: {e}",
148
+ )
149
+ results = [result]
150
+ run.log_tests_results(results, False)
151
+ return results, False
152
+ except BadLaunchTestFileError as e:
153
+ launch_test_file = scenario["ros_testfile"]
154
+ result = get_TestSuite_error_result(
155
+ scenario["ros_testfile"],
156
+ "launch_test file syntax error",
157
+ 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}",
158
+ )
159
+ results = [result]
160
+ run.log_tests_results(results, False)
161
+ return results, False
162
+
73
163
  if return_code == 2:
74
164
  raise Exception(
75
165
  f"Running {scenario['ros_testfile']} failed. Please check that the launch file exists."
@@ -82,10 +172,11 @@ def run_ros2_tests(run):
82
172
 
83
173
  # parse xml generated by launch_test
84
174
  results, success = parse_tests_results(test_result_file_path)
175
+ # upload logs anyway to help user debug
176
+ run.log_artifacts(run.output_path)
85
177
  if success is None:
86
178
  run.log_tests_results(results, False)
87
179
  return results, success
88
- run.log_artifacts(run.output_path)
89
180
 
90
181
  # upload any additional files in the folders specified by the user in artefacts.yaml
91
182
  for output in scenario.get("output_dirs", []):
@@ -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
@@ -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.1'
21
- __version_tuple__ = version_tuple = (0, 7, 1)
20
+ __version__ = version = '0.7.3'
21
+ __version_tuple__ = version_tuple = (0, 7, 3)
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: artefacts_cli
3
- Version: 0.7.1
3
+ Version: 0.7.3
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
@@ -62,6 +62,7 @@ tests/cli/containers/__init__.py
62
62
  tests/cli/containers/test_utils.py
63
63
  tests/fixtures/artefacts_deprecated.yaml
64
64
  tests/fixtures/artefacts_ros1.yaml
65
+ tests/fixtures/bad_launch_test.py
65
66
  tests/fixtures/warp-env-param.yaml
66
67
  tests/fixtures/warp.yaml
67
68
  tests/utils/docker_mock.py
@@ -47,14 +47,14 @@ def test_run(cli_runner):
47
47
 
48
48
 
49
49
  def test_run_with_conf_invalid_jobname(cli_runner, project_with_key):
50
- result = cli_runner.invoke(
51
- run, ["invalid_job_name", "--config", "tests/fixtures/warp.yaml"]
52
- )
50
+ job_name = "invalid_job_name"
51
+ result = cli_runner.invoke(run, [job_name, "--config", "tests/fixtures/warp.yaml"])
53
52
  assert result.exit_code == 1
54
53
  assert result.output == (
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"
54
+ f"[{job_name}] Connecting to https://app.artefacts.com/api using ApiKey\n"
55
+ f"[{job_name}] Starting tests\n"
56
+ f"[{job_name}] Error: Job name not defined\n"
57
+ "Aborted!\n"
58
58
  )
59
59
 
60
60
 
@@ -0,0 +1,100 @@
1
+ import os
2
+ import yaml
3
+ from unittest.mock import patch
4
+ import pytest
5
+
6
+ from artefacts.cli import WarpJob, WarpRun
7
+ from artefacts.cli.app import APIConf
8
+ from artefacts.cli.ros2 import (
9
+ generate_scenario_parameter_output,
10
+ run_ros2_tests,
11
+ ros2_run_and_save_logs,
12
+ )
13
+ from artefacts.cli.ros2 import (
14
+ Launch_test_CmdNotFoundError,
15
+ LaunchTestFileNotFoundError,
16
+ BadLaunchTestFileError,
17
+ )
18
+
19
+
20
+ def test_generate_parameter_output(tmp_path):
21
+ params = {
22
+ "turtle/speed": 5,
23
+ "turtle/color.rgb.r": 255,
24
+ "controller_server/FollowPath.critics": ["RotateToGoal", "Oscillation"],
25
+ }
26
+ file_path = tmp_path / "params.yaml"
27
+ generate_scenario_parameter_output(params, file_path)
28
+ with open(file_path) as f:
29
+ ros2_params = yaml.load(f, Loader=yaml.Loader)
30
+ assert ros2_params == {
31
+ "turtle": {
32
+ "ros__parameters": {
33
+ "speed": 5,
34
+ "color": {"rgb": {"r": 255}},
35
+ }
36
+ },
37
+ "controller_server": {
38
+ "ros__parameters": {
39
+ "FollowPath": {"critics": ["RotateToGoal", "Oscillation"]}
40
+ }
41
+ },
42
+ }
43
+
44
+
45
+ @patch("os.path.exists", return_value=False)
46
+ @patch("artefacts.cli.ros2.ros2_run_and_save_logs")
47
+ @pytest.mark.ros2
48
+ def test_passing_launch_arguments(mock_ros2_run_and_save_logs, _mock_exists):
49
+ os.environ["ARTEFACTS_JOB_ID"] = "test_job_id"
50
+ os.environ["ARTEFACTS_KEY"] = "test_key"
51
+ job = WarpJob("test_project_id", APIConf("sdfs"), "test_jobname", {}, dryrun=True)
52
+ scenario = {
53
+ "ros_testfile": "test.launch.py",
54
+ "launch_arguments": {"arg1": "val1", "arg2": "val2"},
55
+ }
56
+ run = WarpRun(job, scenario, 0)
57
+
58
+ run_ros2_tests(run)
59
+
60
+ mock_ros2_run_and_save_logs.assert_called_once()
61
+ assert (
62
+ " test.launch.py arg1:=val1 arg2:=val2"
63
+ in mock_ros2_run_and_save_logs.call_args[0][0]
64
+ ), (
65
+ "Launch arguments should be passed to the test command after the launch file path"
66
+ )
67
+
68
+
69
+ @pytest.mark.ros2
70
+ def test_run_and_save_logs_missing_ros2_launchtest():
71
+ filename = "missing_launchtest.test.py"
72
+ command = [
73
+ "launch_test",
74
+ filename,
75
+ ]
76
+ with pytest.raises(LaunchTestFileNotFoundError):
77
+ return_code = ros2_run_and_save_logs(
78
+ " ".join(command),
79
+ shell=True,
80
+ executable="/bin/bash",
81
+ env=os.environ,
82
+ output_path="/tmp/test_log.txt",
83
+ )
84
+
85
+
86
+ @pytest.mark.ros2
87
+ def test_run_and_save_logs_bad_ros2_launchtest():
88
+ filename = "bad_launch_test.py"
89
+ command = [
90
+ "launch_test",
91
+ f"tests/fixtures/{filename}",
92
+ ]
93
+ with pytest.raises(BadLaunchTestFileError):
94
+ return_code = ros2_run_and_save_logs(
95
+ " ".join(command),
96
+ shell=True,
97
+ executable="/bin/bash",
98
+ env=os.environ,
99
+ output_path="/tmp/test_log.txt",
100
+ )
@@ -1,8 +1,10 @@
1
1
  import pytest
2
+ import subprocess
3
+ import os
2
4
 
3
5
  from artefacts.cli import WarpJob, WarpRun
4
6
  from artefacts.cli.app import APIConf
5
- from artefacts.cli.utils import add_output_from_default
7
+ from artefacts.cli.utils import add_output_from_default, run_and_save_logs
6
8
 
7
9
 
8
10
  @pytest.fixture
@@ -22,3 +24,15 @@ def test_adds_nothing_on_missing_default_output(new_run, mocker, project_with_ke
22
24
  path.configure_mock(**mocked)
23
25
  add_output_from_default(new_run)
24
26
  assert len(new_run.uploads) == 0
27
+
28
+
29
+ @pytest.mark.ros2
30
+ def test_run_and_save_logs_missing_launch_test_command():
31
+ filename = "launchtest.test.py"
32
+ command = [
33
+ "launch_test",
34
+ filename,
35
+ ]
36
+ # launch_test won't be in the path, and an error will be raised
37
+ with pytest.raises(FileNotFoundError):
38
+ run_and_save_logs(" ".join(command), output_path="/tmp/test_log.txt")
File without changes
@@ -1,6 +1,25 @@
1
1
  version: 0.1.0
2
2
  project: _pytest-project_
3
3
  jobs:
4
+ missing_launchtest:
5
+ type: test
6
+ runtime:
7
+ simulator: turtlesim
8
+ framework: ros2:humble
9
+ scenarios:
10
+ settings:
11
+ - name: missing_launchtest
12
+ ros_testfile: missing_launchtests.py
13
+ bad_launchtest:
14
+ type: test
15
+ runtime:
16
+ simulator: turtlesim
17
+ framework: ros2:humble
18
+ scenarios:
19
+ settings:
20
+ - name: bad_launchtest
21
+ ros_testfile: bad_launch_test.py
22
+
4
23
  simple_job:
5
24
  type: test
6
25
  runtime:
@@ -1,41 +0,0 @@
1
- import os
2
- import yaml
3
- from unittest.mock import patch
4
- import pytest
5
-
6
- from artefacts.cli import WarpJob, WarpRun
7
- from artefacts.cli.app import APIConf
8
- from artefacts.cli.ros2 import generate_scenario_parameter_output, run_ros2_tests
9
-
10
-
11
- def test_generate_parameter_output(tmp_path):
12
- params = {"turtle/speed": 5}
13
- file_path = tmp_path / "params.yaml"
14
- generate_scenario_parameter_output(params, file_path)
15
- with open(file_path) as f:
16
- ros2_params = yaml.load(f, Loader=yaml.Loader)
17
- assert ros2_params == {"turtle": {"ros__parameters": {"speed": 5}}}
18
-
19
-
20
- @patch("os.path.exists", return_value=False)
21
- @patch("artefacts.cli.ros2.run_and_save_logs")
22
- @pytest.mark.ros2
23
- def test_passing_launch_arguments(mock_run_and_save_logs, _mock_exists):
24
- os.environ["ARTEFACTS_JOB_ID"] = "test_job_id"
25
- os.environ["ARTEFACTS_KEY"] = "test_key"
26
- job = WarpJob("test_project_id", APIConf("sdfs"), "test_jobname", {}, dryrun=True)
27
- scenario = {
28
- "ros_testfile": "test.launch.py",
29
- "launch_arguments": {"arg1": "val1", "arg2": "val2"},
30
- }
31
- run = WarpRun(job, scenario, 0)
32
-
33
- run_ros2_tests(run)
34
-
35
- mock_run_and_save_logs.assert_called_once()
36
- assert (
37
- " test.launch.py arg1:=val1 arg2:=val2"
38
- in mock_run_and_save_logs.call_args[0][0]
39
- ), (
40
- "Launch arguments should be passed to the test command after the launch file path"
41
- )
File without changes
File without changes
File without changes
File without changes