Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions THIRD-PARTY-LICENSES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2535,8 +2535,8 @@ limitations under the License.
** libc; version 0.2.189 -- https://crates.io/crates/libc
** manyhow-macros; version 0.11.4 -- https://crates.io/crates/manyhow-macros
** openjd-expr; version 0.7.0 -- https://crates.io/crates/openjd-expr
** openjd-model; version 0.7.0 -- https://crates.io/crates/openjd-model
** openjd-sessions; version 0.5.7 -- https://crates.io/crates/openjd-sessions
** openjd-model; version 0.7.1 -- https://crates.io/crates/openjd-model
** openjd-sessions; version 0.5.8 -- https://crates.io/crates/openjd-sessions
** pin-project-lite; version 0.2.17 -- https://crates.io/crates/pin-project-lite
** portable-atomic; version 1.15.0 -- https://crates.io/crates/portable-atomic
** proc-macro2; version 1.0.107 -- https://crates.io/crates/proc-macro2
Expand Down
4 changes: 2 additions & 2 deletions rust-bindings/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ crate-type = ["cdylib", "rlib"]

[dependencies]
openjd-expr = "0.7.0"
openjd-model = "0.7.0"
openjd-sessions = "0.5.7"
openjd-model = "0.7.1"
Comment thread
leongdl marked this conversation as resolved.
openjd-sessions = "0.5.8"
tokio = { version = "1", features = ["rt-multi-thread"] }
uuid = { version = "1", features = ["v4"] }
serde_json = "1"
Expand Down
20 changes: 20 additions & 0 deletions test/openjd/model_v0/v2023_09/test_job_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,26 @@ class TestJobTemplate:
},
id="job and step env names all differ",
),
pytest.param(
{
"specificationVersion": "jobtemplate-2023-09",
"name": "Foo",
"steps": [
{
"name": "StepOne",
"script": STEP_SCRIPT,
"stepEnvironments": [{"name": "StepEnv", "script": ENV_SCRIPT}],
},
{
"name": "StepTwo",
"script": STEP_SCRIPT,
"stepEnvironments": [{"name": "StepEnv", "script": ENV_SCRIPT}],
},
],
"jobEnvironments": [{"name": "JobEnv", "script": ENV_SCRIPT}],
},
id="step env name reused across steps",
),
),
)
def test_parse_success(self, data: dict[str, Any]) -> None:
Expand Down
99 changes: 99 additions & 0 deletions test/openjd/model_v1/test_parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import pytest

from openjd.model._v1 import (
CallerLimits,
decode_environment_template,
decode_environment_template_str,
decode_job_template,
Expand Down Expand Up @@ -405,3 +406,101 @@ def test_json_explicit(self) -> None:
def test_invalid_yaml_raises(self) -> None:
with pytest.raises(DecodeValidationError):
decode_environment_template_str(": not a mapping")


class TestStepEnvironmentNameScope(object):
"""A Step Environment's ``name`` is scoped to the Step that defines it (Template
Schemas §3 StepTemplate, §4 Environment): unique within that Step's list, and
distinct from every Job Environment. Different Steps may reuse a name.

openjd-model 0.7.1 (openjd-rs#381) relaxed an over-strict check that held every
environment name in the template in one set, so the second Step to declare
``StepEnv`` was rejected. The v0 path always accepted this; this is the v1 path,
which had no coverage.

The error-text assertions below pin the v1 wording as it stands. It differs from
v0 on purpose-of-record, not by design: v0 reports the step-vs-job rule as
``Name X must differ from the names of Environments defined at the root of the
template.`` at ``step[i] -> stepEnvironments[j] -> name``, while v1 reports both
the per-Step and the step-vs-job rule as ``duplicate environment name: 'X'`` at
``steps[i] -> stepEnvironments[j]``. Aligning the two is an openjd-rs concern;
these assertions only guard against the relaxation dropping a rule.
"""

@staticmethod
def _environment(name: str) -> dict[str, Any]:
return {
"name": name,
"script": {"actions": {"onEnter": {"command": "echo", "args": [name]}}},
}

@classmethod
def _step(cls, name: str, environment_names: list[str]) -> dict[str, Any]:
return {
"name": name,
"stepEnvironments": [cls._environment(n) for n in environment_names],
"script": {"actions": {"onRun": {"command": "echo", "args": [name]}}},
}

@classmethod
def _template(cls, steps: list[dict[str, Any]]) -> dict[str, Any]:
return {
"specificationVersion": "jobtemplate-2023-09",
"name": "T",
"jobEnvironments": [cls._environment("JobEnv")],
"steps": steps,
}

def test_same_name_across_steps_is_accepted(self) -> None:
Comment thread
leongdl marked this conversation as resolved.
"""Four Steps each declare ``StepEnv``. Only one Step's environments are ever
active in a Session, so these names never collide."""
template = self._template([self._step(f"Step{i}", ["StepEnv"]) for i in range(4)])
job_template = decode_job_template(template=template, supported_extensions=[])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The relaxation is only exercised at decode_job_template. Nothing covers create_job on the same template, which is where a name collision would actually bite.

decode_job_template validates the JobTemplate/StepTemplate shapes. But the environment names that matter operationally are the ones on the instantiated JobStep.step_environments (src/openjd/_openjd_rs.pyi:2226) and Job.job_environments (:1166), reached via create_job, and then handed to Session.enter_environment (rust-bindings/src/sessions/session.rs:443) which tracks them by name in environments_entered (:117, :407).

That instantiation path is a second place a name set could be held. openjd-rs#381 split one template-wide set into a job set plus a per-Step set on the decode side; if create_job (or preprocess_job_parameters, which py_create_job routes through at rust-bindings/src/model/create_job_fns.rs:106) holds its own collection keyed by environment name, decode_job_template would now accept the four-StepEnv template while create_job on that same template still fails — or worse, silently collapses four distinct Environment objects into one. test_same_name_across_steps_is_accepted returns before ever finding out; it reads job_template.steps, not a created Job.

test/openjd/model_v1/test_create_job.py has no stepEnvironments at all (grep is empty across all 1583 lines), so there is no existing case that would catch this incidentally. Extending test_same_name_across_steps_is_accepted to feed its decoded template through create_job and assert the four Steps still each carry their own StepEnv would close the gap cheaply — it reuses the template already built by _template, and it is the assertion that actually demonstrates the reused name is usable rather than merely parseable.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checked the instantiation path in openjd-rs 0.7.1 for the hypothesized name-keyed collection; there is none. create_job handles step environments positionally: instantiate.rs maps each step's stepEnvironments Vec element-by-element through convert_environment (no name lookup), and the only pass over all environments in create_job/mod.rs is the max_environment_size check, where env.name appears solely in the error message. The only HashSet in that module collects accessed symbol names, not environment names. On the session side, environments_entered is name-keyed but a session only ever enters a single step's environments, so cross-step reuse cannot coexist there — which is the spec rationale for allowing it. Leaving this PR scoped to the decode-side relaxation it ships; a create_job-level pin would be guarding a structure that does not exist today.

names = [[e.name for e in (s.step_environments or [])] for s in job_template.steps]
assert names == [["StepEnv"]] * 4
Comment thread
leongdl marked this conversation as resolved.

def test_duplicate_within_one_step_is_rejected(self) -> None:
"""Control for §3 rule 1: the per-Step uniqueness check must survive the relaxation."""
template = self._template(
[self._step("Step0", ["StepEnv"]), self._step("Step1", ["StepEnv", "StepEnv"])]
)
with pytest.raises(ModelValidationError) as excinfo:
decode_job_template(template=template, supported_extensions=[])
message = str(excinfo.value)
assert "steps[1] -> stepEnvironments[1]" in message
assert "duplicate environment name: 'StepEnv'" in message
Comment thread
leongdl marked this conversation as resolved.

def test_step_env_named_like_job_env_is_rejected(self) -> None:
"""Control for §3 rule 2: a Step Environment may not reuse a Job Environment name."""
template = self._template(
[self._step("Step0", ["StepEnv"]), self._step("Step1", ["JobEnv"])]
)
with pytest.raises(ModelValidationError) as excinfo:
decode_job_template(template=template, supported_extensions=[])
message = str(excinfo.value)
assert "steps[1] -> stepEnvironments[0]" in message
assert "duplicate environment name: 'JobEnv'" in message
Comment thread
leongdl marked this conversation as resolved.

def test_duplicate_job_env_names_is_rejected(self) -> None:
"""Control for §4 uniqueness within ``jobEnvironments``. The relaxation split one
template-wide set into a job set plus a per-Step set; this pins the job set."""
template = self._template([self._step("Step0", ["StepEnv"])])
template["jobEnvironments"] = [self._environment("JobEnv"), self._environment("JobEnv")]
with pytest.raises(ModelValidationError) as excinfo:
decode_job_template(template=template, supported_extensions=[])
message = str(excinfo.value)
assert "jobEnvironments[1]" in message
assert "duplicate environment name: 'JobEnv'" in message

def test_max_env_count_counts_repeated_names_separately(self) -> None:
"""``max_env_count`` bounds the number of environments, not the number of distinct
names. Four Steps each declaring ``StepEnv`` plus ``JobEnv`` is 5 environments
under 2 names, so a limit of 4 must reject; counting distinct names would not."""
template = self._template([self._step(f"Step{i}", ["StepEnv"]) for i in range(4)])
with pytest.raises(ModelValidationError) as excinfo:
decode_job_template(
template=template,
supported_extensions=[],
caller_limits=CallerLimits(max_env_count=4),
)
assert "total environments (5) exceeds caller limit of 4" in str(excinfo.value)
Loading