artefacts-cli 0.7.2__tar.gz → 0.8.0__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 (72) hide show
  1. {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/CHANGELOG.md +13 -0
  2. {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/PKG-INFO +2 -2
  3. {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/artefacts/cli/__init__.py +8 -5
  4. {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/artefacts/cli/app.py +14 -0
  5. {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/artefacts/cli/app_containers.py +21 -1
  6. artefacts_cli-0.8.0/artefacts/cli/logger.py +26 -0
  7. {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/artefacts/cli/other.py +3 -0
  8. {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/artefacts/cli/ros2.py +30 -14
  9. {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/artefacts/cli/utils_ros.py +2 -2
  10. {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/artefacts/cli/version.py +2 -2
  11. {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/artefacts_cli.egg-info/PKG-INFO +2 -2
  12. {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/artefacts_cli.egg-info/SOURCES.txt +1 -0
  13. {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/tests/cli/test_ros2.py +69 -3
  14. artefacts_cli-0.8.0/tests/cli/test_utils_ros.py +9 -0
  15. artefacts_cli-0.7.2/artefacts/cli/logger.py +0 -10
  16. {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/README.md +0 -0
  17. {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/README_INTERNAL.md +0 -0
  18. {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/artefacts/__init__.py +0 -0
  19. {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/artefacts/cli/bagparser.py +0 -0
  20. {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/artefacts/cli/constants.py +0 -0
  21. {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/artefacts/cli/containers/__init__.py +0 -0
  22. {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/artefacts/cli/containers/docker.py +0 -0
  23. {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/artefacts/cli/containers/utils.py +0 -0
  24. {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/artefacts/cli/errors.py +0 -0
  25. {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/artefacts/cli/parameters.py +0 -0
  26. {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/artefacts/cli/ros1.py +0 -0
  27. {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/artefacts/cli/utils.py +0 -0
  28. {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/artefacts/wrappers/artefacts_ros1_meta.launch +0 -0
  29. {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/artefacts.yaml +0 -0
  30. {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/artefacts_cli.egg-info/dependency_links.txt +0 -0
  31. {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/artefacts_cli.egg-info/entry_points.txt +0 -0
  32. {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/artefacts_cli.egg-info/requires.txt +0 -0
  33. {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/artefacts_cli.egg-info/top_level.txt +0 -0
  34. {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/bin/release +0 -0
  35. {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/infra-tests/test_run_remote.yaml +0 -0
  36. {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/infra-tests/turtlesim1/ros_workspace/src/turtle_odometry/CMakeLists.txt +0 -0
  37. {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/infra-tests/turtlesim1/ros_workspace/src/turtle_odometry/launch/test_meta.launch +0 -0
  38. {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/infra-tests/turtlesim1/ros_workspace/src/turtle_odometry/launch/test_turtle.launch +0 -0
  39. {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/infra-tests/turtlesim1/ros_workspace/src/turtle_odometry/launch/turtle_odometry.launch +0 -0
  40. {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/infra-tests/turtlesim1/ros_workspace/src/turtle_odometry/package.xml +0 -0
  41. {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/infra-tests/turtlesim1/ros_workspace/src/turtle_odometry/setup.py +0 -0
  42. {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/infra-tests/turtlesim1/ros_workspace/src/turtle_odometry/src/TestTurtle.py +0 -0
  43. {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/infra-tests/turtlesim1/ros_workspace/src/turtle_odometry/src/__init__.py +0 -0
  44. {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/infra-tests/turtlesim1/ros_workspace/src/turtle_odometry/src/turtle_odom.py +0 -0
  45. {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/infra-tests/turtlesim1/ros_workspace/src/turtle_odometry/src/turtle_post_process.py +0 -0
  46. {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/infra-tests/turtlesim1/ros_workspace/src/turtle_odometry/src/turtle_trajectory.py +0 -0
  47. {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/infra-tests/turtlesim1/ros_workspace/src/turtle_odometry/test/viz_turtle_odom.xml +0 -0
  48. {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/infra-tests/turtlesim2/launch_turtle.py +0 -0
  49. {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/infra-tests/turtlesim2/sample_node.py +0 -0
  50. {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/pyproject.toml +0 -0
  51. {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/pytest.ini +0 -0
  52. {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/setup.cfg +0 -0
  53. {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/tests/__init__.py +0 -0
  54. {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/tests/cli/__init__.py +0 -0
  55. {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/tests/cli/containers/__init__.py +0 -0
  56. {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/tests/cli/containers/test_utils.py +0 -0
  57. {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/tests/cli/test_app_containers.py +0 -0
  58. {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/tests/cli/test_cli.py +0 -0
  59. {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/tests/cli/test_config_validation.py +0 -0
  60. {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/tests/cli/test_other.py +0 -0
  61. {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/tests/cli/test_parameters.py +0 -0
  62. {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/tests/cli/test_ros1.py +0 -0
  63. {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/tests/cli/test_utils.py +0 -0
  64. {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/tests/cli/test_warp.py +0 -0
  65. {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/tests/conftest.py +0 -0
  66. {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/tests/fixtures/artefacts_deprecated.yaml +0 -0
  67. {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/tests/fixtures/artefacts_ros1.yaml +0 -0
  68. {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/tests/fixtures/bad_launch_test.py +0 -0
  69. {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/tests/fixtures/warp-env-param.yaml +0 -0
  70. {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/tests/fixtures/warp.yaml +0 -0
  71. {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/tests/test_config_validation.py +0 -0
  72. {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/tests/utils/docker_mock.py +0 -0
@@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.8.0] - 2025-04-04
11
+
10
12
  ### Added
11
13
 
12
14
  - Run in containers with only an artefacts.yaml configuration file. No need to
@@ -16,6 +18,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
16
18
 
17
19
  - New logging messages and format.
18
20
 
21
+ ### Fixed
22
+
23
+ - Logging correctly filters between logs for stderr and stdout
24
+ - Client now correctly handles rosbags not saved to the top level of a project.
25
+ - Fixed error formatting of test error(s).
26
+
27
+ ## [0.7.3] - 2025-03-26
28
+
29
+ ### Fixed
30
+
31
+ - Handle nested ROS params in the configuration file.
19
32
 
20
33
  ## [0.7.2] - 2025-03-19
21
34
 
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: artefacts_cli
3
- Version: 0.7.2
3
+ Version: 0.8.0
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
@@ -133,6 +133,7 @@ class WarpRun:
133
133
  self.output_path = self.params.get(
134
134
  "output_path", f"{self.job.output_path}/{self.run_n}"
135
135
  )
136
+ self.logger = logger
136
137
  os.makedirs(self.output_path, exist_ok=True)
137
138
  data = {
138
139
  "job_id": job.job_id,
@@ -155,10 +156,10 @@ class WarpRun:
155
156
  if response.status_code != 200:
156
157
  if response.status_code == 403:
157
158
  msg = response.json()["message"]
158
- logger.warning(msg)
159
+ self.logger.warning(msg)
159
160
  raise AuthenticationError(msg)
160
- logger.warning(f"Error on scenario creation: {response.status_code}")
161
- logger.warning(response.text)
161
+ self.logger.warning(f"Error on scenario creation: {response.status_code}")
162
+ self.logger.warning(response.text)
162
163
  raise AuthenticationError(str(response.status_code))
163
164
  return
164
165
 
@@ -275,9 +276,11 @@ class WarpRun:
275
276
  files=files,
276
277
  )
277
278
  except OverflowError:
278
- logger.warning(f"File too large: {file_name} could not be uploaded")
279
+ self.logger.warning(
280
+ f"File too large: {file_name} could not be uploaded"
281
+ )
279
282
  except Exception as e:
280
- logger.warning(f"Error uploading {file_name}: {e}, skipping")
283
+ self.logger.warning(f"Error uploading {file_name}: {e}, skipping")
281
284
 
282
285
 
283
286
  def init_job(
@@ -244,6 +244,11 @@ def hello(project_name):
244
244
  default=False,
245
245
  help='[Experimental] Run the job inside a package container. The container image is build if it does not exist yet, with default name as "artefacts" (please use --with-image to override the image name). This option overrides (for now) --dryrun, --nosim, --noisolation and --description.',
246
246
  )
247
+ @click.option(
248
+ "--dockerfile",
249
+ default="Dockerfile",
250
+ help="[Experimental] Path to a custom Dockerfile. Defaults to Dockerfile in the run directory. This flag is only used together with `--in-container`",
251
+ )
247
252
  @click.option(
248
253
  "--with-image",
249
254
  default=None,
@@ -274,6 +279,7 @@ def run(
274
279
  description="",
275
280
  skip_validation=False,
276
281
  in_container: bool = False,
282
+ dockerfile: str = "Dockerfile",
277
283
  with_image: str = "artefacts",
278
284
  no_rebuild: bool = False,
279
285
  with_gui: bool = False,
@@ -283,6 +289,12 @@ def run(
283
289
 
284
290
  * Directly in the shell by default.
285
291
  * Inside a packaged container when using the --in-container option.
292
+
293
+ In container mode:
294
+ * Images are built automatically if missing.
295
+ * Currently 1 image per job found in artefacts.yaml.
296
+ * Images are rebuilt at each run (relatively fast when no change).
297
+ * `dockerfile` allows to specify an alternative Dockerfile.
286
298
  """
287
299
  warpconfig = read_config(config)
288
300
  project_id = warpconfig["project"]
@@ -296,6 +308,8 @@ def run(
296
308
  ctx.invoke(
297
309
  containers.build,
298
310
  root=".",
311
+ dockerfile=dockerfile,
312
+ only=[jobname],
299
313
  )
300
314
  click.echo(f"[{jobname}] Container image ready")
301
315
  click.echo(f"[{jobname}] Run in container")
@@ -1,5 +1,6 @@
1
1
  import os
2
2
  from pathlib import Path
3
+ from typing import Optional
3
4
 
4
5
  import click
5
6
 
@@ -45,6 +46,13 @@ def containers(ctx: click.Context, debug: bool):
45
46
  default="artefacts.yaml",
46
47
  help="Path to the Artefacts configuration file. It defaults to `./artefacts.yaml`",
47
48
  )
49
+ @click.option(
50
+ "--only",
51
+ required=False,
52
+ type=Optional[list],
53
+ default=None,
54
+ help="Optional list of job names to process. The default is to process all jobs.",
55
+ )
48
56
  @click.pass_context
49
57
  def build(
50
58
  ctx: click.Context,
@@ -53,6 +61,7 @@ def build(
53
61
  dockerfile: str,
54
62
  name: str,
55
63
  config: str,
64
+ only: Optional[list] = None,
56
65
  ):
57
66
  try:
58
67
  artefacts_config = read_config(config)
@@ -63,7 +72,11 @@ def build(
63
72
  prefix = artefacts_config["project"].strip().lower()
64
73
  dockerfiles = []
65
74
  if os.path.exists(dockerfile):
66
- for job_name in artefacts_config["jobs"]:
75
+ if only:
76
+ jobs = only
77
+ else:
78
+ jobs = artefacts_config["jobs"]
79
+ for job_name in jobs:
67
80
  dockerfiles.append(
68
81
  dict(
69
82
  path=root,
@@ -71,6 +84,11 @@ def build(
71
84
  name=f"{prefix}/{job_name.strip().lower()}",
72
85
  )
73
86
  )
87
+ elif dockerfile != "Dockerfile" and not os.path.exists(dockerfile):
88
+ # The user asks explicitly for using a specific Dockerfile, so fast fail if we cannot find it
89
+ raise click.ClickException(
90
+ f"Dockerfile `{dockerfile}` not found. Please ensure the file exits. Automatic Dockerfile generation may also work by dropping the --dockerfile option."
91
+ )
74
92
  else:
75
93
  # The split on `prefix` is to ensure there is no slash (project names are org/project) confusing the path across supported OS.
76
94
  dest_root = (
@@ -90,6 +108,8 @@ def build(
90
108
  scenarios = Converter().process(config, as_text=False)
91
109
  for idx, df in enumerate(scenarios.values()):
92
110
  job_name = df.job_name.strip().lower()
111
+ if only and job_name not in only:
112
+ continue
93
113
  dest = dest_root / Path(job_name)
94
114
  dest.mkdir(parents=True, exist_ok=True)
95
115
  _dockerfile = os.path.join(dest, "Dockerfile")
@@ -0,0 +1,26 @@
1
+ import logging
2
+ import sys
3
+
4
+ logger = logging.getLogger("artefacts")
5
+ logger.setLevel(logging.INFO) # Allow INFO and above
6
+
7
+
8
+ # INFO and WARNING messages go to stdout
9
+ class InfoWarningFilter(logging.Filter):
10
+ def filter(self, record):
11
+ return record.levelno in [logging.INFO, logging.WARNING]
12
+
13
+
14
+ info_handler = logging.StreamHandler(stream=sys.stdout)
15
+ info_handler.setLevel(logging.INFO)
16
+ info_handler.addFilter(InfoWarningFilter())
17
+ info_handler.setFormatter(logging.Formatter("%(message)s"))
18
+
19
+ # ERROR and above go to stderr
20
+ error_handler = logging.StreamHandler(stream=sys.stderr)
21
+ error_handler.setLevel(logging.ERROR)
22
+ error_handler.setFormatter(logging.Formatter("%(levelname)s: %(message)s"))
23
+
24
+ logger.addHandler(info_handler)
25
+ logger.addHandler(error_handler)
26
+ logger.propagate = False
@@ -16,6 +16,9 @@ def generate_parameter_output(params: dict):
16
16
 
17
17
 
18
18
  def run_other_tests(run):
19
+ """Note: parameter names will be set as environment variables
20
+ (must be letters, numbers and underscores), and saved into yaml and json files
21
+ """
19
22
  scenario = run.params
20
23
  if "params" in scenario:
21
24
  generate_parameter_output(scenario["params"])
@@ -2,7 +2,6 @@ import yaml
2
2
  from glob import glob
3
3
  import os
4
4
  import shutil
5
-
6
5
  from .utils import run_and_save_logs
7
6
  from .utils_ros import parse_tests_results, get_TestSuite_error_result
8
7
  from .parameters import TMP_SCENARIO_PARAMS_YAML
@@ -70,7 +69,14 @@ def generate_scenario_parameter_output(params: dict, param_file: str):
70
69
  return
71
70
  if node not in content:
72
71
  content[node] = {"ros__parameters": {}}
73
- content[node]["ros__parameters"][pname] = v
72
+ # handles nested keys for params in the form of dot notation
73
+ current_level = content[node]["ros__parameters"]
74
+ keys = pname.split(".")
75
+ for key in keys[:-1]:
76
+ if key not in current_level:
77
+ current_level[key] = {}
78
+ current_level = current_level[key]
79
+ current_level[keys[-1]] = v
74
80
  with open(param_file, "w") as f:
75
81
  yaml.dump(content, f)
76
82
 
@@ -86,8 +92,11 @@ def run_ros2_tests(run):
86
92
  generate_scenario_parameter_output(
87
93
  run.params["params"], TMP_SCENARIO_PARAMS_YAML
88
94
  )
89
-
90
- preexisting_rosbags = glob("rosbag2*")
95
+ # We look for the directory as drilling down will find both
96
+ # the directory as well as the rosbag itself.
97
+ preexisting_rosbags = [
98
+ path for path in glob("**/rosbag2*", recursive=True) if os.path.isdir(path)
99
+ ]
91
100
  test_result_file_path = f"{run.output_path}/tests_junit.xml"
92
101
  launch_arguments = [
93
102
  f"{k}:={v}" for k, v in run.params.get("launch_arguments", {}).items()
@@ -175,33 +184,40 @@ def run_ros2_tests(run):
175
184
  for output in scenario.get("output_dirs", []):
176
185
  run.log_artifacts(output)
177
186
 
178
- # check if any rosbag was created
179
- rosbags = glob("rosbag2*")
187
+ # Checks for a new rosbag after test completions. We look for the directory
188
+ # as drilling down will find both the directory as well as the rosbag itself.
189
+ rosbags = [
190
+ path for path in glob("**/rosbag2*", recursive=True) if os.path.isdir(path)
191
+ ]
180
192
  new_rosbags = set(rosbags).difference(set(preexisting_rosbags))
181
193
  from artefacts.cli.bagparser import BagFileParser
182
194
 
195
+ run.logger.info(f"Found new rosbags: {new_rosbags}")
183
196
  if len(new_rosbags) > 0:
184
- rosbag_path = new_rosbags.pop()
185
- run.log_artifacts(rosbag_path, "rosbag")
197
+ rosbag_dir = new_rosbags.pop()
198
+ run.log_artifacts(rosbag_dir, "rosbag")
186
199
  if "metrics" in run.params:
187
- # TODO should go inside BagFileParser?
188
- db_files = glob(f"{rosbag_path}/*.mcap") # Ros2 Default
200
+ # Search for database files in the directory
201
+ # TODO: should go inside BagFileParser?
202
+ db_files = glob(f"{rosbag_dir}/*.mcap") # Ros2 Default
189
203
  if not db_files:
190
- db_files = glob(f"{rosbag_path}/*.db3") # Legacy
204
+ db_files = glob(f"{rosbag_dir}/*.db3") # Legacy
191
205
  if not db_files:
192
206
  raise FileNotFoundError(
193
- "No .mcap or .db3 files found in the specified path."
207
+ "No .mcap or .db3 files found in the specified path. Attempted to find in "
208
+ f"{rosbag_dir}/*.mcap and {rosbag_dir}/*.db3"
194
209
  )
195
210
  db_file = db_files[0]
211
+
196
212
  bag = BagFileParser(db_file)
197
213
  for metric in run.params["metrics"]:
198
214
  try:
199
215
  last_value = bag.get_last_message(metric)[1].data
200
216
  run.log_metric(metric, last_value)
201
217
  except KeyError:
202
- print(f"Metric {metric} not found in rosbag, skipping.")
218
+ run.logger.error(f"Metric {metric} not found in rosbag, skipping.")
203
219
  except TypeError or IndexError:
204
- print(
220
+ run.logger.error(
205
221
  f"Metric {metric} not found. Is it being published?. Skipping."
206
222
  )
207
223
 
@@ -66,9 +66,9 @@ def parse_tests_results(file):
66
66
  except Exception as e:
67
67
  print(f"[Exception in parse_tests_results] {e}")
68
68
  print("Test result xml could not be loaded, marking success as False")
69
- results = get_TestSuite_error_result(
69
+ result = get_TestSuite_error_result(
70
70
  "unittest.suite.TestSuite",
71
71
  "Error parsing XML test results",
72
72
  f"The test may have timed out. Exception: {e}",
73
73
  )
74
- return results, None
74
+ return [result], 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.2'
21
- __version_tuple__ = version_tuple = (0, 7, 2)
20
+ __version__ = version = '0.8.0'
21
+ __version_tuple__ = version_tuple = (0, 8, 0)
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: artefacts_cli
3
- Version: 0.7.2
3
+ Version: 0.8.0
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
@@ -57,6 +57,7 @@ tests/cli/test_parameters.py
57
57
  tests/cli/test_ros1.py
58
58
  tests/cli/test_ros2.py
59
59
  tests/cli/test_utils.py
60
+ tests/cli/test_utils_ros.py
60
61
  tests/cli/test_warp.py
61
62
  tests/cli/containers/__init__.py
62
63
  tests/cli/containers/test_utils.py
@@ -1,6 +1,6 @@
1
1
  import os
2
2
  import yaml
3
- from unittest.mock import patch
3
+ from unittest.mock import patch, MagicMock
4
4
  import pytest
5
5
 
6
6
  from artefacts.cli import WarpJob, WarpRun
@@ -18,12 +18,28 @@ from artefacts.cli.ros2 import (
18
18
 
19
19
 
20
20
  def test_generate_parameter_output(tmp_path):
21
- params = {"turtle/speed": 5}
21
+ params = {
22
+ "turtle/speed": 5,
23
+ "turtle/color.rgb.r": 255,
24
+ "controller_server/FollowPath.critics": ["RotateToGoal", "Oscillation"],
25
+ }
22
26
  file_path = tmp_path / "params.yaml"
23
27
  generate_scenario_parameter_output(params, file_path)
24
28
  with open(file_path) as f:
25
29
  ros2_params = yaml.load(f, Loader=yaml.Loader)
26
- assert ros2_params == {"turtle": {"ros__parameters": {"speed": 5}}}
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
+ }
27
43
 
28
44
 
29
45
  @patch("os.path.exists", return_value=False)
@@ -82,3 +98,53 @@ def test_run_and_save_logs_bad_ros2_launchtest():
82
98
  env=os.environ,
83
99
  output_path="/tmp/test_log.txt",
84
100
  )
101
+
102
+
103
+ @patch("artefacts.cli.ros2.glob")
104
+ @patch("os.path.isdir", return_value=True)
105
+ @patch("artefacts.cli.bagparser.BagFileParser")
106
+ @patch("artefacts.cli.ros2.parse_tests_results", return_value=([], True))
107
+ @patch("artefacts.cli.ros2.run_and_save_logs", return_value=(0, "", ""))
108
+ @pytest.mark.ros2
109
+ def test_rosbag_discovered_and_metric_logged(
110
+ mock_run_logs, mock_parse, mock_bag_parser, mock_isdir, mock_glob
111
+ ):
112
+ # Setup test environment
113
+ os.environ["ARTEFACTS_JOB_ID"] = "test_job_id"
114
+ os.environ["ARTEFACTS_KEY"] = "test_key"
115
+
116
+ job = WarpJob(
117
+ "test_project_id", APIConf("test_url"), "test_jobname", {}, dryrun=True
118
+ )
119
+ scenario = {"ros_testfile": "test_launch.py", "metrics": ["topic1"]}
120
+ run = WarpRun(job, scenario, 0)
121
+
122
+ # Patch the methods to verify
123
+ run.log_artifacts = MagicMock()
124
+ run.log_metric = MagicMock()
125
+
126
+ # mock returns
127
+ preexisting_rosbags = [
128
+ "src/my_test_folder/rosbag2_existing",
129
+ "src/venv/some_rosbag_package/rosbag2_existing",
130
+ ]
131
+ all_rosbags = [
132
+ "src/my_test_folder/rosbag2_existing",
133
+ "src/venv/some_rosbag_package/rosbag2_existing",
134
+ "src/my_test_folder/rosbag2_new",
135
+ ]
136
+ bag_files = ["src/my_test_folder/rosbag2_new/test.mcap"]
137
+ mock_glob.side_effect = [preexisting_rosbags, all_rosbags, bag_files]
138
+
139
+ # BagFileParser mock
140
+ mock_bag = MagicMock()
141
+ mock_bag.get_last_message.return_value = (None, MagicMock(data=42.0))
142
+ mock_bag_parser.return_value = mock_bag
143
+
144
+ run_ros2_tests(run)
145
+
146
+ # Assert the right new rosbag directory was found
147
+ run.log_artifacts.assert_any_call("src/my_test_folder/rosbag2_new", "rosbag")
148
+
149
+ # Assert the right metric was logged
150
+ run.log_metric.assert_called_with("topic1", 42.0)
@@ -0,0 +1,9 @@
1
+ from artefacts.cli.utils_ros import parse_tests_results
2
+
3
+
4
+ def test_parse_tests_results():
5
+ # Create a XML file for testing
6
+ test_file = "test.xml"
7
+ results, _ = parse_tests_results(test_file)
8
+ assert type(results) is list
9
+ assert len(results) == 1
@@ -1,10 +0,0 @@
1
- import logging
2
- import sys
3
-
4
- logger = logging.getLogger()
5
-
6
- default_handler = logging.StreamHandler(stream=sys.stderr)
7
- default_handler.setLevel(logging.ERROR)
8
- default_handler.setFormatter(logging.Formatter())
9
-
10
- logger.addHandler(default_handler)
File without changes
File without changes
File without changes
File without changes