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.
- {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/CHANGELOG.md +13 -0
- {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/PKG-INFO +2 -2
- {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/artefacts/cli/__init__.py +8 -5
- {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/artefacts/cli/app.py +14 -0
- {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/artefacts/cli/app_containers.py +21 -1
- artefacts_cli-0.8.0/artefacts/cli/logger.py +26 -0
- {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/artefacts/cli/other.py +3 -0
- {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/artefacts/cli/ros2.py +30 -14
- {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/artefacts/cli/utils_ros.py +2 -2
- {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/artefacts/cli/version.py +2 -2
- {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/artefacts_cli.egg-info/PKG-INFO +2 -2
- {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/artefacts_cli.egg-info/SOURCES.txt +1 -0
- {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/tests/cli/test_ros2.py +69 -3
- artefacts_cli-0.8.0/tests/cli/test_utils_ros.py +9 -0
- artefacts_cli-0.7.2/artefacts/cli/logger.py +0 -10
- {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/README.md +0 -0
- {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/README_INTERNAL.md +0 -0
- {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/artefacts/__init__.py +0 -0
- {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/artefacts/cli/bagparser.py +0 -0
- {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/artefacts/cli/constants.py +0 -0
- {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/artefacts/cli/containers/__init__.py +0 -0
- {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/artefacts/cli/containers/docker.py +0 -0
- {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/artefacts/cli/containers/utils.py +0 -0
- {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/artefacts/cli/errors.py +0 -0
- {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/artefacts/cli/parameters.py +0 -0
- {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/artefacts/cli/ros1.py +0 -0
- {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/artefacts/cli/utils.py +0 -0
- {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/artefacts/wrappers/artefacts_ros1_meta.launch +0 -0
- {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/artefacts.yaml +0 -0
- {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/artefacts_cli.egg-info/dependency_links.txt +0 -0
- {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/artefacts_cli.egg-info/entry_points.txt +0 -0
- {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/artefacts_cli.egg-info/requires.txt +0 -0
- {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/artefacts_cli.egg-info/top_level.txt +0 -0
- {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/bin/release +0 -0
- {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/infra-tests/test_run_remote.yaml +0 -0
- {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/infra-tests/turtlesim1/ros_workspace/src/turtle_odometry/CMakeLists.txt +0 -0
- {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/infra-tests/turtlesim1/ros_workspace/src/turtle_odometry/launch/test_meta.launch +0 -0
- {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/infra-tests/turtlesim1/ros_workspace/src/turtle_odometry/launch/test_turtle.launch +0 -0
- {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/infra-tests/turtlesim1/ros_workspace/src/turtle_odometry/launch/turtle_odometry.launch +0 -0
- {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/infra-tests/turtlesim1/ros_workspace/src/turtle_odometry/package.xml +0 -0
- {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/infra-tests/turtlesim1/ros_workspace/src/turtle_odometry/setup.py +0 -0
- {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/infra-tests/turtlesim1/ros_workspace/src/turtle_odometry/src/TestTurtle.py +0 -0
- {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/infra-tests/turtlesim1/ros_workspace/src/turtle_odometry/src/__init__.py +0 -0
- {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/infra-tests/turtlesim1/ros_workspace/src/turtle_odometry/src/turtle_odom.py +0 -0
- {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
- {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/infra-tests/turtlesim1/ros_workspace/src/turtle_odometry/src/turtle_trajectory.py +0 -0
- {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
- {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/infra-tests/turtlesim2/launch_turtle.py +0 -0
- {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/infra-tests/turtlesim2/sample_node.py +0 -0
- {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/pyproject.toml +0 -0
- {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/pytest.ini +0 -0
- {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/setup.cfg +0 -0
- {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/tests/__init__.py +0 -0
- {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/tests/cli/__init__.py +0 -0
- {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/tests/cli/containers/__init__.py +0 -0
- {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/tests/cli/containers/test_utils.py +0 -0
- {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/tests/cli/test_app_containers.py +0 -0
- {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/tests/cli/test_cli.py +0 -0
- {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/tests/cli/test_config_validation.py +0 -0
- {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/tests/cli/test_other.py +0 -0
- {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/tests/cli/test_parameters.py +0 -0
- {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/tests/cli/test_ros1.py +0 -0
- {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/tests/cli/test_utils.py +0 -0
- {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/tests/cli/test_warp.py +0 -0
- {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/tests/conftest.py +0 -0
- {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/tests/fixtures/artefacts_deprecated.yaml +0 -0
- {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/tests/fixtures/artefacts_ros1.yaml +0 -0
- {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/tests/fixtures/bad_launch_test.py +0 -0
- {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/tests/fixtures/warp-env-param.yaml +0 -0
- {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/tests/fixtures/warp.yaml +0 -0
- {artefacts_cli-0.7.2 → artefacts_cli-0.8.0}/tests/test_config_validation.py +0 -0
- {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.
|
1
|
+
Metadata-Version: 2.4
|
2
2
|
Name: artefacts_cli
|
3
|
-
Version: 0.
|
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(
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
#
|
179
|
-
|
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
|
-
|
185
|
-
run.log_artifacts(
|
197
|
+
rosbag_dir = new_rosbags.pop()
|
198
|
+
run.log_artifacts(rosbag_dir, "rosbag")
|
186
199
|
if "metrics" in run.params:
|
187
|
-
#
|
188
|
-
|
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"{
|
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
|
-
|
218
|
+
run.logger.error(f"Metric {metric} not found in rosbag, skipping.")
|
203
219
|
except TypeError or IndexError:
|
204
|
-
|
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
|
-
|
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
|
74
|
+
return [result], None
|
@@ -1,6 +1,6 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.4
|
2
2
|
Name: artefacts_cli
|
3
|
-
Version: 0.
|
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
|
@@ -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 = {
|
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 == {
|
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)
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|