Building JobSpec objects#

A job generator ultimately returns runnable work items to canary. In this plugin, those work items are JobSpec objects.

What the YAML plugin sets#

For each test entry, the plugin populates a spec with:

  • file_root and file_path: where the test came from;

  • family: the logical test name (the key under tests:);

  • keywords: copied from YAML (if present);

  • attributes: stores description under attributes["description"]; and

  • command: the concrete command line to execute.

Parameter expansion#

When a YAML entry provides a parameters mapping, the plugin computes the Cartesian product of parameter values and emits one spec per combination:

    def lock(self, on_options: list[str] | None = None) -> list[canary.JobSpec]:
        """Take the cartesian product of parameters and from each combination create a test case."""

        with open(self.file, "r") as fh:
            fd = yaml.safe_load(fh)
        fd = yaml_schema.validate(fd)

        specs: list[canary.ResolvedSpec] = []
        for name, details in fd["tests"].items():
            kwds: dict[str, Any] = dict(
                file_root=Path(self.root),
                file_path=Path(self.path),
                family=name,
                keywords=details.get("keywords", []),
                attributes={"description": details.get("description")},
            )
            script = details["script"]
            sh = canary.filesystem.which("sh", required=True)
            if parameters := details.get("parameters"):
                keys = list(parameters.keys())
                for values in product(*parameters.values()):
                    p = kwds["parameters"] = dict(zip(keys, values))
                    shell_cmds: list[str] = [Template(_).safe_substitute(**p) for _ in script]
                    kwds["command"] = [sh, "-c", "set -e\n" + "\n".join(shell_cmds)]
                    spec = canary.JobSpec(**kwds)
                    specs.append(spec)
            else:
                kwds["command"] = [sh, "-c", "set -e\n" + "\n".join(script)]
                spec = canary.JobSpec(**kwds)
                specs.append(spec)

        return specs

For each combination, the plugin sets spec.parameters and expands the script lines using the parameter values.

Command construction (sh -c)#

The plugin runs the YAML script by constructing a shell command:

  • it finds a shell with canary.filesystem.which() (required);

  • it passes the script using sh -c; and

  • it prefixes the script with set -e so failures propagate via exit code.

This construction happens in lock() when setting kwds["command"]:

    def lock(self, on_options: list[str] | None = None) -> list[canary.JobSpec]:
        """Take the cartesian product of parameters and from each combination create a test case."""

        with open(self.file, "r") as fh:
            fd = yaml.safe_load(fh)
        fd = yaml_schema.validate(fd)

        specs: list[canary.ResolvedSpec] = []
        for name, details in fd["tests"].items():
            kwds: dict[str, Any] = dict(
                file_root=Path(self.root),
                file_path=Path(self.path),
                family=name,
                keywords=details.get("keywords", []),
                attributes={"description": details.get("description")},
            )
            script = details["script"]
            sh = canary.filesystem.which("sh", required=True)
            if parameters := details.get("parameters"):
                keys = list(parameters.keys())
                for values in product(*parameters.values()):
                    p = kwds["parameters"] = dict(zip(keys, values))
                    shell_cmds: list[str] = [Template(_).safe_substitute(**p) for _ in script]
                    kwds["command"] = [sh, "-c", "set -e\n" + "\n".join(shell_cmds)]
                    spec = canary.JobSpec(**kwds)
                    specs.append(spec)
            else:
                kwds["command"] = [sh, "-c", "set -e\n" + "\n".join(script)]
                spec = canary.JobSpec(**kwds)
                specs.append(spec)

        return specs

Template substitution#

When parameters are present, each script line is expanded using string.Template and safe_substitute. Placeholders in the YAML should use the ${name} form:

script:
  - echo "a=${a}"

Note

safe_substitute leaves unknown placeholders unchanged, which can be helpful during incremental development of a format. If you prefer strict behavior, use substitute instead.