diff --git a/README.md b/README.md index c785d8d..c3461e1 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,24 @@ A base configuration can itself extend another one. Paths (`patch_dir`, `rosdistro_snapshot`, `rosdistro_additional_recipes`, `conda_index` files) are relative to the file that sets them. +## Package tests + +`tests/.yaml` (next to `vinca.yaml`, or next to a configuration it extends) +becomes the `tests` section of the package's recipe. Selectors that test `ros_distro` +are resolved by vinca, so one test file can serve several distributions; all other +selectors are left to rattler-build: + +```yaml +tests: + - script: + - if: ros_distro in ["humble", "jazzy"] + then: + - if: osx + then: export ROS_LOCALHOST_ONLY=1 && launch_test smoke_test.py + else: + - launch_test smoke_test.py +``` + ## Package naming The optional `package_name_mode` setting controls the transition from legacy distro-qualified names such as `ros-humble-rclcpp` to ROS-major-version names such as `ros2-rclcpp`: diff --git a/vinca/main.py b/vinca/main.py index 96d210a..dd06b2c 100644 --- a/vinca/main.py +++ b/vinca/main.py @@ -51,6 +51,7 @@ source_reference, ) from .template import write_recipe, write_recipe_package +from .v1_selectors import evaluate_distro_selectors unsatisfied_deps = set() distro = None @@ -499,8 +500,10 @@ def parse_package(pkg, distro, vinca_conf, path): if test := vinca_conf.get("_tests", {}).get(final_name): # parse as yaml - text = test.read_text() - test_content = ruamel.yaml.safe_load(text) + text = test.read_text(encoding="utf-8") + test_content = evaluate_distro_selectors( + ruamel.yaml.safe_load(text), ros_distro=config.ros_distro + ) recipe["test"] = test_content for p in pkg["authors"]: diff --git a/vinca/template.py b/vinca/template.py index 6aa5c1e..1a13046 100644 --- a/vinca/template.py +++ b/vinca/template.py @@ -16,6 +16,7 @@ get_pkg_additional_info, get_pkg_build_number, ) +from vinca.v1_selectors import evaluate_distro_selectors from vinca.variants import VariantsMode, get_recipe_variants, get_variants_mode TEMPLATE = """\ @@ -146,8 +147,10 @@ def write_recipe(source, outputs, vinca_conf, distro, single_file=True): test := vinca_conf["_tests"].get(package_name) ): print("Using test: ", test) - text = test.read_text() - test_content = yaml.safe_load(text) + text = test.read_text(encoding="utf-8") + test_content = evaluate_distro_selectors( + yaml.safe_load(text), ros_distro=config.ros_distro + ) meta["tests"] = test_content["tests"] recipe_dir = (Path("recipes") / package_name).absolute() diff --git a/vinca/test_distro_selectors.py b/vinca/test_distro_selectors.py new file mode 100644 index 0000000..7ebc0c9 --- /dev/null +++ b/vinca/test_distro_selectors.py @@ -0,0 +1,32 @@ +from vinca.v1_selectors import evaluate_distro_selectors + +TEST = { + "tests": [ + { + "script": [ + { + "if": 'ros_distro in ["humble", "jazzy"]', + "then": [{"if": "osx", "then": "old-osx"}], + "else": [{"if": "osx", "then": "new-osx"}], + }, + {"if": 'ros_distro == "humble"', "then": ["humble-only"]}, + {"if": "linux", "then": "linux-cmd"}, + ] + } + ] +} + + +def test_distro_selectors_are_resolved_platform_ones_kept(): + humble = evaluate_distro_selectors(TEST, ros_distro="humble") + assert humble["tests"][0]["script"] == [ + {"if": "osx", "then": "old-osx"}, + "humble-only", + {"if": "linux", "then": "linux-cmd"}, + ] + + rolling = evaluate_distro_selectors(TEST, ros_distro="rolling") + assert rolling["tests"][0]["script"] == [ + {"if": "osx", "then": "new-osx"}, + {"if": "linux", "then": "linux-cmd"}, + ] diff --git a/vinca/v1_selectors.py b/vinca/v1_selectors.py index 49720c7..b9da3c8 100644 --- a/vinca/v1_selectors.py +++ b/vinca/v1_selectors.py @@ -168,3 +168,44 @@ def evaluate_selectors( ctx = _platform_flags(target_platform) return _process_node(copy.deepcopy(data), ctx) + + +def _process_distro_node(node: Any, ctx: Mapping[str, Any]) -> Any | None: + """Like _process_node, but only for selectors that mention ``ros_distro``; + other selectors (e.g. platform ones) are left for rattler-build.""" + if ( + isinstance(node, Mapping) + and "if" in node + and "then" in node + and "ros_distro" in str(node["if"]) + ): + branch = "then" if _eval_condition(str(node["if"]), ctx) else "else" + if branch in node: + return _process_distro_node(node[branch], ctx) + return None + if isinstance(node, MutableMapping): + mapping_out: dict[str, Any] = {} + for k, v in node.items(): + new = _process_distro_node(v, ctx) + if new is not None: + mapping_out[k] = new + return mapping_out + if isinstance(node, Sequence) and not isinstance(node, (str, bytes)): + sequence_out: list[Any] = [] + for item in node: + new = _process_distro_node(item, ctx) + if new is None: + continue + if isinstance(new, list): + sequence_out.extend(new) + else: + sequence_out.append(new) + return sequence_out + return node + + +def evaluate_distro_selectors(data: Any, *, ros_distro: str | None) -> Any: + """Return *data* with the selectors that test ``ros_distro`` evaluated, e.g. + ``- if: ros_distro in ["humble", "jazzy"]``, so one test file can serve several + distributions. Selectors without ``ros_distro`` are kept unchanged.""" + return _process_distro_node(copy.deepcopy(data), {"ros_distro": ros_distro})