From e09f0fc39c8fe17f404cbdb7a05067a8d49367ac Mon Sep 17 00:00:00 2001 From: David Leong <116610336+leongdl@users.noreply.github.com> Date: Sat, 1 Aug 2026 19:34:49 -0700 Subject: [PATCH] test: Add WRAP_ACTIONS conformance fixtures for coverage gaps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 25 fixtures closing gaps from the RFC 0008 coverage audit: absent-by-design host vars, macro-by-emitter matrix, stderr forwarding, variable scope negatives (incl. in-expression and in-let variants), the four missing all-or-nothing hook subsets, nesting depth 2, variables-only inner env, failure-path breadth, and the FEATURE_BUNDLE_1-gated fmtstring positive twin. Suite: 72 -> 97 passing. Spec-correct fixtures that FAIL against current implementations are split to the companion branch conformance-wrap-actions-expected-failures. Review: quorum-review fixes — WTASK_HOOK_RAN sentinel pins the wrapped path in the stderr-forwarding fixture; dropped a command-echo-prone and a vacuous forbidden line; replaced gratuitous repr_py with bare references in the three variable-scope invalids; corrected the let-scope framing per Template Schemas 3.6.2; documented the task-order assumption, the FEATURE_BUNDLE_1 requirement of the fmtstring accept fixture, and the macro-consuming-runtime caveat; README no longer references the expected-failures PR's proposed/ directory. Signed-off-by: David Leong <116610336+leongdl@users.noreply.github.com> --- .../2023-09/WRAP_ACTIONS/README.md | 47 ++++++++++++ ...-in-expr-funcall-outside-hook.invalid.yaml | 31 ++++++++ ...pedaction-in-let-outside-hook.invalid.yaml | 34 +++++++++ ...ppedenv-name-in-onwraptaskrun.invalid.yaml | 25 +++++++ ...edstep-name-in-onwrapenventer.invalid.yaml | 25 +++++++ ...pedstep-name-in-onwrapenvexit.invalid.yaml | 24 ++++++ ...-timeout-and-mode-with-feature-bundle.yaml | 40 ++++++++++ ...ter-and-exit-missing-task-run.invalid.yaml | 20 +++++ ...3--wrap-only-onwrap-env-enter.invalid.yaml | 17 +++++ ....3--wrap-only-onwrap-env-exit.invalid.yaml | 17 +++++ ...ap-run-and-exit-missing-enter.invalid.yaml | 19 +++++ ...d-grand-child-stdout-loses-macro.test.yaml | 62 ++++++++++++++++ ...p-environment-excludes-host-vars.test.yaml | 68 +++++++++++++++++ ...-exit-hook-own-onexit-still-runs.test.yaml | 66 +++++++++++++++++ ...and-child-fails-under-enter-hook.test.yaml | 65 +++++++++++++++++ .../wrap-job-env-wraps-step-env.test.yaml | 56 ++++++++++++++ .../jobs/wrap-no-wrap-control.test.yaml | 37 ++++++++++ ...enjd-env-from-wrap-script-itself.test.yaml | 55 ++++++++++++++ ...-env-grand-child-under-exit-hook.test.yaml | 73 +++++++++++++++++++ ...sk-grand-child-visible-next-task.test.yaml | 63 ++++++++++++++++ ...progress-status-macros-forwarded.test.yaml | 60 +++++++++++++++ ...tderr-forwarded-from-grand-child.test.yaml | 55 ++++++++++++++ ...s-wrappedenv-name-per-invocation.test.yaml | 71 ++++++++++++++++++ ...-steps-wrappedstep-name-per-step.test.yaml | 56 ++++++++++++++ ...ap-unwrapped-parity-task-failure.test.yaml | 22 ++++++ ...ables-only-inner-env-skips-hooks.test.yaml | 50 +++++++++++++ 26 files changed, 1158 insertions(+) create mode 100644 conformance-tests/2023-09/WRAP_ACTIONS/env_templates/4--wrappedaction-in-expr-funcall-outside-hook.invalid.yaml create mode 100644 conformance-tests/2023-09/WRAP_ACTIONS/env_templates/4--wrappedaction-in-let-outside-hook.invalid.yaml create mode 100644 conformance-tests/2023-09/WRAP_ACTIONS/env_templates/4--wrappedenv-name-in-onwraptaskrun.invalid.yaml create mode 100644 conformance-tests/2023-09/WRAP_ACTIONS/env_templates/4--wrappedstep-name-in-onwrapenventer.invalid.yaml create mode 100644 conformance-tests/2023-09/WRAP_ACTIONS/env_templates/4--wrappedstep-name-in-onwrapenvexit.invalid.yaml create mode 100644 conformance-tests/2023-09/WRAP_ACTIONS/env_templates/4.2--wrap-fmtstring-timeout-and-mode-with-feature-bundle.yaml create mode 100644 conformance-tests/2023-09/WRAP_ACTIONS/env_templates/4.3--wrap-enter-and-exit-missing-task-run.invalid.yaml create mode 100644 conformance-tests/2023-09/WRAP_ACTIONS/env_templates/4.3--wrap-only-onwrap-env-enter.invalid.yaml create mode 100644 conformance-tests/2023-09/WRAP_ACTIONS/env_templates/4.3--wrap-only-onwrap-env-exit.invalid.yaml create mode 100644 conformance-tests/2023-09/WRAP_ACTIONS/env_templates/4.3--wrap-run-and-exit-missing-enter.invalid.yaml create mode 100644 conformance-tests/2023-09/WRAP_ACTIONS/jobs/wrap-discarded-grand-child-stdout-loses-macro.test.yaml create mode 100644 conformance-tests/2023-09/WRAP_ACTIONS/jobs/wrap-environment-excludes-host-vars.test.yaml create mode 100644 conformance-tests/2023-09/WRAP_ACTIONS/jobs/wrap-failed-exit-hook-own-onexit-still-runs.test.yaml create mode 100644 conformance-tests/2023-09/WRAP_ACTIONS/jobs/wrap-grand-child-fails-under-enter-hook.test.yaml create mode 100644 conformance-tests/2023-09/WRAP_ACTIONS/jobs/wrap-job-env-wraps-step-env.test.yaml create mode 100644 conformance-tests/2023-09/WRAP_ACTIONS/jobs/wrap-no-wrap-control.test.yaml create mode 100644 conformance-tests/2023-09/WRAP_ACTIONS/jobs/wrap-openjd-env-from-wrap-script-itself.test.yaml create mode 100644 conformance-tests/2023-09/WRAP_ACTIONS/jobs/wrap-openjd-env-grand-child-under-exit-hook.test.yaml create mode 100644 conformance-tests/2023-09/WRAP_ACTIONS/jobs/wrap-openjd-env-task-grand-child-visible-next-task.test.yaml create mode 100644 conformance-tests/2023-09/WRAP_ACTIONS/jobs/wrap-progress-status-macros-forwarded.test.yaml create mode 100644 conformance-tests/2023-09/WRAP_ACTIONS/jobs/wrap-stderr-forwarded-from-grand-child.test.yaml create mode 100644 conformance-tests/2023-09/WRAP_ACTIONS/jobs/wrap-two-inner-envs-wrappedenv-name-per-invocation.test.yaml create mode 100644 conformance-tests/2023-09/WRAP_ACTIONS/jobs/wrap-two-steps-wrappedstep-name-per-step.test.yaml create mode 100644 conformance-tests/2023-09/WRAP_ACTIONS/jobs/wrap-unwrapped-parity-task-failure.test.yaml create mode 100644 conformance-tests/2023-09/WRAP_ACTIONS/jobs/wrap-variables-only-inner-env-skips-hooks.test.yaml diff --git a/conformance-tests/2023-09/WRAP_ACTIONS/README.md b/conformance-tests/2023-09/WRAP_ACTIONS/README.md index fd5a1bdf..e236f3c3 100644 --- a/conformance-tests/2023-09/WRAP_ACTIONS/README.md +++ b/conformance-tests/2023-09/WRAP_ACTIONS/README.md @@ -160,6 +160,53 @@ rule beyond action args (`env_templates/`): └── 4--wrappedaction-in-embedded-file.invalid.yaml ``` +A second audit round added fixtures for the remaining coverage gaps +(RFC 0008 test-coverage review): + +``` +WRAP_ACTIONS/ +├── env_templates/ +│ │ # Variable scope negatives (RFC §Template variables) +│ ├── 4--wrappedenv-name-in-onwraptaskrun.invalid.yaml +│ ├── 4--wrappedstep-name-in-onwrapenventer.invalid.yaml +│ ├── 4--wrappedstep-name-in-onwrapenvexit.invalid.yaml +│ ├── 4--wrappedaction-in-expr-funcall-outside-hook.invalid.yaml +│ ├── 4--wrappedaction-in-let-outside-hook.invalid.yaml +│ │ # All-or-nothing rule — remaining reject subsets +│ ├── 4.3--wrap-only-onwrap-env-enter.invalid.yaml +│ ├── 4.3--wrap-only-onwrap-env-exit.invalid.yaml +│ ├── 4.3--wrap-enter-and-exit-missing-task-run.invalid.yaml +│ ├── 4.3--wrap-run-and-exit-missing-enter.invalid.yaml +│ │ # FEATURE_BUNDLE_1 accept-side twin of the gated-form rejects +│ └── 4.2--wrap-fmtstring-timeout-and-mode-with-feature-bundle.yaml +├── jobs/ +│ │ # Absent-by-design: host env vars excluded (RFC §Host environment +│ │ # variables and embedded file paths) +│ ├── wrap-environment-excludes-host-vars.test.yaml +│ │ # Macro propagation × emitter (RFC §Stdout forwarding) +│ ├── wrap-openjd-env-from-wrap-script-itself.test.yaml +│ ├── wrap-openjd-env-task-grand-child-visible-next-task.test.yaml +│ ├── wrap-openjd-env-grand-child-under-exit-hook.test.yaml +│ ├── wrap-progress-status-macros-forwarded.test.yaml +│ ├── wrap-stderr-forwarded-from-grand-child.test.yaml +│ ├── wrap-discarded-grand-child-stdout-loses-macro.test.yaml +│ │ # Nesting depth 2 and the nothing-to-replace rule +│ ├── wrap-two-inner-envs-wrappedenv-name-per-invocation.test.yaml +│ ├── wrap-variables-only-inner-env-skips-hooks.test.yaml +│ │ # Failure-path breadth (RFC §Failure semantics, §Lifecycle) +│ ├── wrap-failed-exit-hook-own-onexit-still-runs.test.yaml +│ ├── wrap-grand-child-fails-under-enter-hook.test.yaml +│ ├── wrap-unwrapped-parity-task-failure.test.yaml +│ │ # Session composition (How Jobs Are Run) +│ ├── wrap-two-steps-wrappedstep-name-per-step.test.yaml +│ ├── wrap-job-env-wraps-step-env.test.yaml +│ └── wrap-no-wrap-control.test.yaml +``` + +(A `jobs/proposed/` directory of spec-correct fixtures that fail against the +current reference implementations is added separately by the +expected-failures PR; it is documented there.) + Most execution tests use POSIX shell commands (`sh`, `bash`, `echo`, `printf`) and are gated to `runOn: [posix]`. Fixtures that carry no `runOn` gate use `python`, the suite's portable interpreter — including diff --git a/conformance-tests/2023-09/WRAP_ACTIONS/env_templates/4--wrappedaction-in-expr-funcall-outside-hook.invalid.yaml b/conformance-tests/2023-09/WRAP_ACTIONS/env_templates/4--wrappedaction-in-expr-funcall-outside-hook.invalid.yaml new file mode 100644 index 00000000..e44ea507 --- /dev/null +++ b/conformance-tests/2023-09/WRAP_ACTIONS/env_templates/4--wrappedaction-in-expr-funcall-outside-hook.invalid.yaml @@ -0,0 +1,31 @@ +# RFC 0008 Template variables: "Templates MUST NOT reference +# WrappedAction.* outside the three wrap hooks. Schedulers MUST reject +# templates that violate this scope rule." Here the out-of-scope reference +# is buried inside an EXPR function call — repr_py(WrappedAction.Command) +# in the env's own onEnter — not written as a bare {{WrappedAction.X}} +# format string. Catches a validator that only scope-checks direct +# variable interpolations and does not walk expression arguments (the +# same blind spot as the historical Task.File.Run.name bug, which behaved +# differently inside a function call than as a direct reference). Single +# defect: the template is valid except for the WrappedAction.Command +# reference inside the function call in onEnter. +specificationVersion: environment-2023-09 +extensions: +- WRAP_ACTIONS +- EXPR +environment: + name: WrapActionInFuncall + script: + actions: + onEnter: + command: python + args: ["-c", "print({{repr_py(WrappedAction.Command)}})"] + onWrapEnvEnter: + command: python + args: ["-c", "print({{repr_py(WrappedAction.Command)}})"] + onWrapTaskRun: + command: python + args: ["-c", "print({{repr_py(WrappedAction.Command)}})"] + onWrapEnvExit: + command: python + args: ["-c", "print({{repr_py(WrappedAction.Command)}})"] diff --git a/conformance-tests/2023-09/WRAP_ACTIONS/env_templates/4--wrappedaction-in-let-outside-hook.invalid.yaml b/conformance-tests/2023-09/WRAP_ACTIONS/env_templates/4--wrappedaction-in-let-outside-hook.invalid.yaml new file mode 100644 index 00000000..71b06416 --- /dev/null +++ b/conformance-tests/2023-09/WRAP_ACTIONS/env_templates/4--wrappedaction-in-let-outside-hook.invalid.yaml @@ -0,0 +1,34 @@ +# RFC 0008 Template variables: "Templates MUST NOT reference +# WrappedAction.* outside the three wrap hooks. Schedulers MUST reject +# templates that violate this scope rule." Here the out-of-scope reference +# is buried inside a script-level `let` binding. Per Template Schemas +# 3.6.2, .let's symbol scope does not include +# WrappedAction.* at all — the binding itself is the violation (script- +# level let is evaluated once at environment entry and shared by ALL of +# the script's actions, including onEnter/onExit where WrappedAction.* +# never exists). Catches a validator that scope-checks format strings in +# action fields but never walks let-binding expressions (the +# `let`-indirection blind spot). The template is valid except for the +# WrappedAction-derived binding and its consumption. +specificationVersion: environment-2023-09 +extensions: +- WRAP_ACTIONS +- EXPR +environment: + name: WrapActionInLet + script: + let: + - wrapped_cmd = WrappedAction.Command + actions: + onEnter: + command: python + args: ["-c", "print({{repr_py(wrapped_cmd)}})"] + onWrapEnvEnter: + command: python + args: ["-c", "print({{repr_py(wrapped_cmd)}})"] + onWrapTaskRun: + command: python + args: ["-c", "print({{repr_py(wrapped_cmd)}})"] + onWrapEnvExit: + command: python + args: ["-c", "print({{repr_py(wrapped_cmd)}})"] diff --git a/conformance-tests/2023-09/WRAP_ACTIONS/env_templates/4--wrappedenv-name-in-onwraptaskrun.invalid.yaml b/conformance-tests/2023-09/WRAP_ACTIONS/env_templates/4--wrappedenv-name-in-onwraptaskrun.invalid.yaml new file mode 100644 index 00000000..158d0109 --- /dev/null +++ b/conformance-tests/2023-09/WRAP_ACTIONS/env_templates/4--wrappedenv-name-in-onwraptaskrun.invalid.yaml @@ -0,0 +1,25 @@ +# RFC 0008 Template variables: WrappedEnv.Name is available only in +# onWrapEnvEnter and onWrapEnvExit. "Templates MUST NOT reference +# WrappedEnv.* outside onWrapEnvEnter and onWrapEnvExit. Schedulers MUST +# reject templates that violate this scope rule." Referencing it in +# onWrapTaskRun — a wrap hook, but the wrong one — must be rejected at +# validation time. Catches a validator that treats all three hooks as one +# scope. Single defect: the template is valid except for the WrappedEnv.Name +# reference in onWrapTaskRun. +specificationVersion: environment-2023-09 +extensions: +- WRAP_ACTIONS +- EXPR +environment: + name: WrapEnvNameInTaskRun + script: + actions: + onWrapEnvEnter: + command: python + args: ["-c", "print(r'{{WrappedEnv.Name}}')"] + onWrapTaskRun: + command: python + args: ["-c", "print(r'{{WrappedEnv.Name}}')"] + onWrapEnvExit: + command: python + args: ["-c", "print(r'{{WrappedEnv.Name}}')"] diff --git a/conformance-tests/2023-09/WRAP_ACTIONS/env_templates/4--wrappedstep-name-in-onwrapenventer.invalid.yaml b/conformance-tests/2023-09/WRAP_ACTIONS/env_templates/4--wrappedstep-name-in-onwrapenventer.invalid.yaml new file mode 100644 index 00000000..a1746879 --- /dev/null +++ b/conformance-tests/2023-09/WRAP_ACTIONS/env_templates/4--wrappedstep-name-in-onwrapenventer.invalid.yaml @@ -0,0 +1,25 @@ +# RFC 0008 Template variables: WrappedStep.Name is available only in +# onWrapTaskRun. "Templates MUST NOT reference ... WrappedStep.* outside +# onWrapTaskRun. Schedulers MUST reject templates that violate this scope +# rule." Referencing it in onWrapEnvEnter must be rejected at validation +# time — an environment's lifecycle action has no associated step. Catches +# a validator that injects WrappedStep.* into all three hook scopes. +# Single defect: the template is valid except for the WrappedStep.Name +# reference in onWrapEnvEnter. +specificationVersion: environment-2023-09 +extensions: +- WRAP_ACTIONS +- EXPR +environment: + name: WrapStepNameInEnter + script: + actions: + onWrapEnvEnter: + command: python + args: ["-c", "print(r'{{WrappedStep.Name}}')"] + onWrapTaskRun: + command: python + args: ["-c", "print(r'{{WrappedStep.Name}}')"] + onWrapEnvExit: + command: python + args: ["-c", "print(r'{{WrappedEnv.Name}}')"] diff --git a/conformance-tests/2023-09/WRAP_ACTIONS/env_templates/4--wrappedstep-name-in-onwrapenvexit.invalid.yaml b/conformance-tests/2023-09/WRAP_ACTIONS/env_templates/4--wrappedstep-name-in-onwrapenvexit.invalid.yaml new file mode 100644 index 00000000..9f342389 --- /dev/null +++ b/conformance-tests/2023-09/WRAP_ACTIONS/env_templates/4--wrappedstep-name-in-onwrapenvexit.invalid.yaml @@ -0,0 +1,24 @@ +# RFC 0008 Template variables: WrappedStep.Name is available only in +# onWrapTaskRun. Referencing it in onWrapEnvExit must be rejected at +# validation time. The exit hook is the twin of the enter hook, but a +# validator can plausibly scope the two env hooks differently (exit hooks +# fire during teardown when a "current step" may exist in the runtime's +# state), so both directions are pinned. Single defect: the template is +# valid except for the WrappedStep.Name reference in onWrapEnvExit. +specificationVersion: environment-2023-09 +extensions: +- WRAP_ACTIONS +- EXPR +environment: + name: WrapStepNameInExit + script: + actions: + onWrapEnvEnter: + command: python + args: ["-c", "print(r'{{WrappedEnv.Name}}')"] + onWrapTaskRun: + command: python + args: ["-c", "print(r'{{WrappedStep.Name}}')"] + onWrapEnvExit: + command: python + args: ["-c", "print(r'{{WrappedStep.Name}}')"] diff --git a/conformance-tests/2023-09/WRAP_ACTIONS/env_templates/4.2--wrap-fmtstring-timeout-and-mode-with-feature-bundle.yaml b/conformance-tests/2023-09/WRAP_ACTIONS/env_templates/4.2--wrap-fmtstring-timeout-and-mode-with-feature-bundle.yaml new file mode 100644 index 00000000..7393d3f6 --- /dev/null +++ b/conformance-tests/2023-09/WRAP_ACTIONS/env_templates/4.2--wrap-fmtstring-timeout-and-mode-with-feature-bundle.yaml @@ -0,0 +1,40 @@ +# RFC 0008 Cancelation behavior + Template Schemas 5/5.3 (FEATURE_BUNDLE_1): +# the positive twin of 4.2--wrap-fmtstring-timeout-no-feature-bundle and +# wrap-cancelation-fmtstring-mode-no-feature-bundle. A format-string +# `timeout` and a format-string `cancelation.mode` on a wrap hook are the +# FEATURE_BUNDLE_1-gated forms of the round-trip forwarding pattern; with +# the extension listed, validation MUST ACCEPT them. Catches a validator +# that rejects the format-string forms unconditionally ("format strings in +# timeout are not allowed") instead of gating them on the extension — the +# reject-side fixtures alone cannot distinguish "gated" from "never +# permitted". +# +# NOTE: unlike its WRAP_ACTIONS siblings, this valid fixture additionally +# requires FEATURE_BUNDLE_1 support. The runner does not gate fixtures by +# declared extensions, so an implementation with WRAP_ACTIONS+EXPR but no +# FEATURE_BUNDLE_1 will (correctly, per its own capabilities) reject this +# template and fail the fixture. +specificationVersion: environment-2023-09 +extensions: +- WRAP_ACTIONS +- EXPR +- FEATURE_BUNDLE_1 +environment: + name: WrapFmtStringGatedAccepted + script: + actions: + onWrapEnvEnter: + command: echo + args: ["{{WrappedEnv.Name}}"] + timeout: "{{WrappedAction.Timeout}}" + onWrapTaskRun: + command: echo + args: ["{{WrappedAction.Command}}"] + timeout: "{{WrappedAction.Timeout}}" + cancelation: + mode: "{{WrappedAction.Cancelation.Mode}}" + notifyPeriodInSeconds: "{{WrappedAction.Cancelation.NotifyPeriodInSeconds}}" + onWrapEnvExit: + command: echo + args: ["{{WrappedEnv.Name}}"] + timeout: "{{WrappedAction.Timeout}}" diff --git a/conformance-tests/2023-09/WRAP_ACTIONS/env_templates/4.3--wrap-enter-and-exit-missing-task-run.invalid.yaml b/conformance-tests/2023-09/WRAP_ACTIONS/env_templates/4.3--wrap-enter-and-exit-missing-task-run.invalid.yaml new file mode 100644 index 00000000..90c12820 --- /dev/null +++ b/conformance-tests/2023-09/WRAP_ACTIONS/env_templates/4.3--wrap-enter-and-exit-missing-task-run.invalid.yaml @@ -0,0 +1,20 @@ +# RFC 0008 all-or-nothing rule: defining onWrapEnvEnter and onWrapEnvExit +# without onWrapTaskRun is invalid. This is the "env lifecycle only" subset +# an author writes when they only want to intercept environment setup and +# teardown — plausible, but rejected: the rule guarantees execution context +# parity across every wrapped lifecycle phase, tasks included. Single +# defect: the template is valid except for the missing onWrapTaskRun. +specificationVersion: environment-2023-09 +extensions: +- WRAP_ACTIONS +- EXPR +environment: + name: EnterExitNoTaskRun + script: + actions: + onWrapEnvEnter: + command: echo + args: ["{{WrappedEnv.Name}}"] + onWrapEnvExit: + command: echo + args: ["{{WrappedEnv.Name}}"] diff --git a/conformance-tests/2023-09/WRAP_ACTIONS/env_templates/4.3--wrap-only-onwrap-env-enter.invalid.yaml b/conformance-tests/2023-09/WRAP_ACTIONS/env_templates/4.3--wrap-only-onwrap-env-enter.invalid.yaml new file mode 100644 index 00000000..f5651095 --- /dev/null +++ b/conformance-tests/2023-09/WRAP_ACTIONS/env_templates/4.3--wrap-only-onwrap-env-enter.invalid.yaml @@ -0,0 +1,17 @@ +# RFC 0008 all-or-nothing rule: an environment that defines any wrap hook +# must define all three (onWrapEnvEnter, onWrapTaskRun, onWrapEnvExit). +# Defining only onWrapEnvEnter is invalid. Catches a validator that only +# checks for the presence of onWrapTaskRun when deciding whether the +# all-or-nothing rule applies. Single defect: the template is valid except +# for the missing onWrapTaskRun and onWrapEnvExit hooks. +specificationVersion: environment-2023-09 +extensions: +- WRAP_ACTIONS +- EXPR +environment: + name: OnlyWrapEnter + script: + actions: + onWrapEnvEnter: + command: echo + args: ["{{WrappedEnv.Name}}"] diff --git a/conformance-tests/2023-09/WRAP_ACTIONS/env_templates/4.3--wrap-only-onwrap-env-exit.invalid.yaml b/conformance-tests/2023-09/WRAP_ACTIONS/env_templates/4.3--wrap-only-onwrap-env-exit.invalid.yaml new file mode 100644 index 00000000..cbbbf2ff --- /dev/null +++ b/conformance-tests/2023-09/WRAP_ACTIONS/env_templates/4.3--wrap-only-onwrap-env-exit.invalid.yaml @@ -0,0 +1,17 @@ +# RFC 0008 all-or-nothing rule: an environment that defines any wrap hook +# must define all three (onWrapEnvEnter, onWrapTaskRun, onWrapEnvExit). +# Defining only onWrapEnvExit is invalid. Catches a validator that treats +# the exit hook as optional teardown rather than part of the atomic hook +# set. Single defect: the template is valid except for the missing +# onWrapEnvEnter and onWrapTaskRun hooks. +specificationVersion: environment-2023-09 +extensions: +- WRAP_ACTIONS +- EXPR +environment: + name: OnlyWrapExit + script: + actions: + onWrapEnvExit: + command: echo + args: ["{{WrappedEnv.Name}}"] diff --git a/conformance-tests/2023-09/WRAP_ACTIONS/env_templates/4.3--wrap-run-and-exit-missing-enter.invalid.yaml b/conformance-tests/2023-09/WRAP_ACTIONS/env_templates/4.3--wrap-run-and-exit-missing-enter.invalid.yaml new file mode 100644 index 00000000..1b01727e --- /dev/null +++ b/conformance-tests/2023-09/WRAP_ACTIONS/env_templates/4.3--wrap-run-and-exit-missing-enter.invalid.yaml @@ -0,0 +1,19 @@ +# RFC 0008 all-or-nothing rule: defining onWrapTaskRun and onWrapEnvExit +# without onWrapEnvEnter is invalid. Catches a validator that only demands +# the hooks needed for the phases it has seen so far (tasks and teardown) +# instead of the full atomic set. Single defect: the template is valid +# except for the missing onWrapEnvEnter. +specificationVersion: environment-2023-09 +extensions: +- WRAP_ACTIONS +- EXPR +environment: + name: RunExitNoEnter + script: + actions: + onWrapTaskRun: + command: echo + args: ["{{WrappedAction.Command}}"] + onWrapEnvExit: + command: echo + args: ["{{WrappedEnv.Name}}"] diff --git a/conformance-tests/2023-09/WRAP_ACTIONS/jobs/wrap-discarded-grand-child-stdout-loses-macro.test.yaml b/conformance-tests/2023-09/WRAP_ACTIONS/jobs/wrap-discarded-grand-child-stdout-loses-macro.test.yaml new file mode 100644 index 00000000..22583db9 --- /dev/null +++ b/conformance-tests/2023-09/WRAP_ACTIONS/jobs/wrap-discarded-grand-child-stdout-loses-macro.test.yaml @@ -0,0 +1,62 @@ +# RFC 0008 "Stdout forwarding and macro propagation" (negative control): +# runtimes MUST scan the WRAP SCRIPT's stdout for macros and "MUST NOT +# scan the grand-child's stdout directly". If the wrap script discards the +# grand-child's stdout (here: redirected to DEVNULL), a macro the +# grand-child emitted never reaches the wrap script's stdout, so the +# runtime must never see it and the variable must NOT surface in +# WrappedAction.Environment. Catches a runtime that locates and scans the +# grand-child process's output stream directly — the design the RFC +# explicitly rejected ("would require locating the grand-child across +# heterogeneous runtimes"). +template: + specificationVersion: jobtemplate-2023-09 + name: WrapDiscardedGrandChildStdoutLosesMacro + jobEnvironments: + - name: InnerEnv + script: + actions: + onEnter: + command: python + args: ["-c", "print('openjd_env: LOST_VAR=should-not-surface')"] + steps: + - name: Step1 + script: + actions: + onRun: + command: echo + args: ["placeholder"] +environments: +- specificationVersion: environment-2023-09 + extensions: + - WRAP_ACTIONS + - EXPR + environment: + name: WrapEnv + script: + actions: + # Run the wrapped onEnter but discard its stdout: its macro line + # must be lost. + onWrapEnvEnter: + command: python + args: + - "-c" + - "import subprocess,sys; sys.exit(subprocess.run([{{repr_py(WrappedAction.Command)}}]+{{repr_py(WrappedAction.Args)}}, stdout=subprocess.DEVNULL).returncode)" + onWrapTaskRun: + command: python + args: + - "-c" + - "for e in {{repr_py(WrappedAction.Environment)}}:\n print('WAENV='+e)\nprint('WAENV_DUMP_DONE')" + onWrapEnvExit: + command: python + args: + - "-c" + - "import subprocess,sys; sys.exit(subprocess.run([{{repr_py(WrappedAction.Command)}}]+{{repr_py(WrappedAction.Args)}}).returncode)" +expected: + output: + - WAENV_DUMP_DONE + # Only the WAENV= form is forbidden: the raw macro string + # "LOST_VAR=should-not-surface" also occurs inside the resolved wrap-hook + # command line (via repr_py(WrappedAction.Args)), so forbidding it would + # false-fail any runner that echoes resolved commands into the log. + forbidden: + - WAENV=LOST_VAR diff --git a/conformance-tests/2023-09/WRAP_ACTIONS/jobs/wrap-environment-excludes-host-vars.test.yaml b/conformance-tests/2023-09/WRAP_ACTIONS/jobs/wrap-environment-excludes-host-vars.test.yaml new file mode 100644 index 00000000..7be2597a --- /dev/null +++ b/conformance-tests/2023-09/WRAP_ACTIONS/jobs/wrap-environment-excludes-host-vars.test.yaml @@ -0,0 +1,68 @@ +# RFC 0008 "Host environment variables and embedded file paths": +# WrappedAction.Environment carries ONLY session-defined variables +# (openjd_env exports and entered environments' declarative variables: +# maps). Host-inherited variables — HOME, PATH, OPENJD_*, etc. — MUST NOT +# be surfaced; forwarding them is the wrap environment's explicit +# responsibility. Catches a runtime that builds the forwarded list from +# os.environ (wholesale or merged with session variables): with session +# variables present, such a runtime passes every inclusion fixture while +# silently leaking the host environment. The forbidden: lines pin the +# exclusion. +template: + specificationVersion: jobtemplate-2023-09 + name: WrapEnvironmentExcludesHostVars + jobEnvironments: + - name: InnerEnv + variables: + STATIC_VAR: static-val + script: + actions: + onEnter: + command: python + args: ["-c", "print('openjd_env: DYNAMIC_VAR=dyn-val')"] + steps: + - name: Step1 + script: + actions: + onRun: + command: echo + args: ["placeholder"] +environments: +- specificationVersion: environment-2023-09 + extensions: + - WRAP_ACTIONS + - EXPR + environment: + name: WrapEnv + script: + actions: + # Forward the inner env's onEnter so its openjd_env macro is + # captured from the wrap script's stdout. + onWrapEnvEnter: + command: python + args: + - "-c" + - "import subprocess,sys; sys.exit(subprocess.run([{{repr_py(WrappedAction.Command)}}]+{{repr_py(WrappedAction.Args)}}).returncode)" + # Dump every forwarded entry. Session variables must be present; + # host variables must not. + onWrapTaskRun: + command: python + args: + - "-c" + - "for e in {{repr_py(WrappedAction.Environment)}}:\n print('WAENV='+e)\nprint('WAENV_DUMP_DONE')" + onWrapEnvExit: + command: python + args: + - "-c" + - "import subprocess,sys; sys.exit(subprocess.run([{{repr_py(WrappedAction.Command)}}]+{{repr_py(WrappedAction.Args)}}).returncode)" +expected: + output: + - WAENV=STATIC_VAR=static-val + - WAENV=DYNAMIC_VAR=dyn-val + - WAENV_DUMP_DONE + forbidden: + - WAENV=HOME= + - WAENV=PATH= + - WAENV=Path= + - WAENV=USER= + - WAENV=OPENJD diff --git a/conformance-tests/2023-09/WRAP_ACTIONS/jobs/wrap-failed-exit-hook-own-onexit-still-runs.test.yaml b/conformance-tests/2023-09/WRAP_ACTIONS/jobs/wrap-failed-exit-hook-own-onexit-still-runs.test.yaml new file mode 100644 index 00000000..0d807e55 --- /dev/null +++ b/conformance-tests/2023-09/WRAP_ACTIONS/jobs/wrap-failed-exit-hook-own-onexit-still-runs.test.yaml @@ -0,0 +1,66 @@ +# RFC 0008 "Failure semantics" + "Lifecycle and cleanup guarantees", +# scenario row 3: "onWrapEnvExit fails → Wrapping environment's onExit +# still runs." A failed onWrapEnvExit is an inner onExit failure; the +# session handles it exactly as an unwrapped onExit failure, and the +# wrapping environment's own onExit MUST still run on top — a runner that +# aborts teardown at the failed exit hook leaks whatever the wrap env's +# onEnter allocated (the container). taskFailure (without exitCode) pins +# that the run surfaces the teardown failure; the specific code cannot be +# pinned portably because teardown-phase failures are not reported through +# the "exited with code: N" line the runner's exitCode assertion parses +# (openjd-rs reports them as "onExit failed: exit code: N"). +template: + specificationVersion: jobtemplate-2023-09 + name: WrapFailedExitHookOwnOnExitStillRuns + jobEnvironments: + - name: InnerEnv + script: + actions: + onEnter: + command: python + args: ["-c", "print('INNER_ENTER_BODY')"] + onExit: + command: python + args: ["-c", "print('INNER_EXIT_BODY')"] + steps: + - name: Step1 + script: + actions: + onRun: + command: python + args: ["-c", "print('TASK_BODY_RAN')"] +environments: +- specificationVersion: environment-2023-09 + extensions: + - WRAP_ACTIONS + - EXPR + environment: + name: WrapEnv + script: + actions: + onEnter: + command: python + args: ["-c", "print('WRAPENV_OWN_ENTER_RAN')"] + onWrapEnvEnter: + command: python + args: + - "-c" + - "import subprocess,sys; sys.exit(subprocess.run([{{repr_py(WrappedAction.Command)}}]+{{repr_py(WrappedAction.Args)}}).returncode)" + onWrapTaskRun: + command: python + args: + - "-c" + - "import subprocess,sys; sys.exit(subprocess.run([{{repr_py(WrappedAction.Command)}}]+{{repr_py(WrappedAction.Args)}}).returncode)" + # The exit hook itself fails. + onWrapEnvExit: + command: python + args: ["-c", "import sys; print('WRAP_EXIT_FAILING'); sys.exit(5)"] + onExit: + command: python + args: ["-c", "print('WRAPENV_OWN_EXIT_RAN')"] +expected: + taskFailure: {} + output: + - TASK_BODY_RAN + - WRAP_EXIT_FAILING + - WRAPENV_OWN_EXIT_RAN diff --git a/conformance-tests/2023-09/WRAP_ACTIONS/jobs/wrap-grand-child-fails-under-enter-hook.test.yaml b/conformance-tests/2023-09/WRAP_ACTIONS/jobs/wrap-grand-child-fails-under-enter-hook.test.yaml new file mode 100644 index 00000000..1689a3c8 --- /dev/null +++ b/conformance-tests/2023-09/WRAP_ACTIONS/jobs/wrap-grand-child-fails-under-enter-hook.test.yaml @@ -0,0 +1,65 @@ +# RFC 0008 "Failure semantics": "A failed onWrapEnvEnter is an inner +# onEnter failure", and exit-status propagation applies at env-lifecycle +# positions too — the existing propagation fixtures only fail a task's +# onRun, and the existing failed-enter fixture fails the HOOK itself. +# Here the hook is healthy and the GRAND-CHILD (the wrapped onEnter's +# process) exits 9; the wrap script propagates it, making the enter fail +# with the grand-child's code. Cleanup then follows scenario row 1: +# onWrapEnvExit for the entered-attempted inner env runs, and the wrapping +# env's own onExit runs. The task must never start. Catches a runtime that +# treats a wrapped env-enter's propagated failure differently from a +# direct hook failure, or that loses the grand-child's specific exit code. +template: + specificationVersion: jobtemplate-2023-09 + name: WrapGrandChildFailsUnderEnterHook + jobEnvironments: + - name: InnerEnv + script: + actions: + onEnter: + command: python + args: ["-c", "import sys; print('INNER_ENTER_STARTED'); sys.exit(9)"] + onExit: + command: python + args: ["-c", "print('INNER_EXIT_BODY')"] + steps: + - name: Step1 + script: + actions: + onRun: + command: python + args: ["-c", "print('TASK_BODY_RAN')"] +environments: +- specificationVersion: environment-2023-09 + extensions: + - WRAP_ACTIONS + - EXPR + environment: + name: WrapEnv + script: + actions: + onWrapEnvEnter: + command: python + args: + - "-c" + - "import subprocess,sys; sys.exit(subprocess.run([{{repr_py(WrappedAction.Command)}}]+{{repr_py(WrappedAction.Args)}}).returncode)" + onWrapTaskRun: + command: python + args: + - "-c" + - "import subprocess,sys; sys.exit(subprocess.run([{{repr_py(WrappedAction.Command)}}]+{{repr_py(WrappedAction.Args)}}).returncode)" + onWrapEnvExit: + command: python + args: ["-c", "print('WRAP_EXIT_RAN_FOR_'+{{repr_py(WrappedEnv.Name)}})"] + onExit: + command: python + args: ["-c", "print('WRAPENV_OWN_EXIT_RAN')"] +expected: + taskFailure: + exitCode: 9 + output: + - INNER_ENTER_STARTED + - WRAP_EXIT_RAN_FOR_InnerEnv + - WRAPENV_OWN_EXIT_RAN + forbidden: + - TASK_BODY_RAN diff --git a/conformance-tests/2023-09/WRAP_ACTIONS/jobs/wrap-job-env-wraps-step-env.test.yaml b/conformance-tests/2023-09/WRAP_ACTIONS/jobs/wrap-job-env-wraps-step-env.test.yaml new file mode 100644 index 00000000..881974b6 --- /dev/null +++ b/conformance-tests/2023-09/WRAP_ACTIONS/jobs/wrap-job-env-wraps-step-env.test.yaml @@ -0,0 +1,56 @@ +# RFC 0008 "Wrap ordering with multiple environments": a wrapping +# environment intercepts the lifecycle actions of environments INNER to +# it. A job-scoped wrap env is outer to every step-scoped env, so a +# stepEnvironment's onEnter/onExit must run via the job env's +# onWrapEnvEnter/onWrapEnvExit with WrappedEnv.Name naming the step env. +# The existing placement fixtures put the wrap env itself in +# jobEnvironments or stepEnvironments; none cross the two scopes. Catches +# a runtime that only wraps environments in the same scope tier as the +# wrapping environment. +template: + specificationVersion: jobtemplate-2023-09 + name: WrapJobEnvWrapsStepEnv + extensions: + - WRAP_ACTIONS + - EXPR + jobEnvironments: + - name: JobWrapEnv + script: + actions: + onWrapEnvEnter: + command: python + args: ["-c", "print('WENTER='+{{repr_py(WrappedEnv.Name)}})"] + onWrapTaskRun: + command: python + args: + - "-c" + - "import subprocess,sys\nprint('WTASK_RAN')\nsys.exit(subprocess.run([{{repr_py(WrappedAction.Command)}}]+{{repr_py(WrappedAction.Args)}}).returncode)" + onWrapEnvExit: + command: python + args: ["-c", "print('WEXIT='+{{repr_py(WrappedEnv.Name)}})"] + steps: + - name: Step1 + stepEnvironments: + - name: InnerStepEnv + script: + actions: + onEnter: + command: python + args: ["-c", "print('STEP_ENV_ENTER_BODY')"] + onExit: + command: python + args: ["-c", "print('STEP_ENV_EXIT_BODY')"] + script: + actions: + onRun: + command: python + args: ["-c", "print('TASK_BODY_RAN')"] +expected: + output: + - WENTER=InnerStepEnv + - WEXIT=InnerStepEnv + - WTASK_RAN + - TASK_BODY_RAN + forbidden: + - STEP_ENV_ENTER_BODY + - STEP_ENV_EXIT_BODY diff --git a/conformance-tests/2023-09/WRAP_ACTIONS/jobs/wrap-no-wrap-control.test.yaml b/conformance-tests/2023-09/WRAP_ACTIONS/jobs/wrap-no-wrap-control.test.yaml new file mode 100644 index 00000000..d1c81ce9 --- /dev/null +++ b/conformance-tests/2023-09/WRAP_ACTIONS/jobs/wrap-no-wrap-control.test.yaml @@ -0,0 +1,37 @@ +# RFC 0008 (interception control): the same inner-env-plus-task shape the +# wrapped fixtures use, with NO wrapping environment anywhere. Everything +# must run normally: the inner env's own onEnter/onExit bodies and the +# task body all execute, and nothing wrap-shaped appears. This is the +# baseline that gives the wrapped fixtures' forbidden: assertions their +# meaning — a runtime whose "unwrapped" path already suppressed inner +# lifecycle bodies would make the interception fixtures pass vacuously. +template: + specificationVersion: jobtemplate-2023-09 + name: WrapNoWrapControl + jobEnvironments: + - name: InnerEnv + script: + actions: + onEnter: + command: python + args: ["-c", "print('INNER_ENTER_BODY')"] + onExit: + command: python + args: ["-c", "print('INNER_EXIT_BODY')"] + steps: + - name: Step1 + script: + actions: + onRun: + command: python + args: ["-c", "print('TASK_BODY_RAN')"] +expected: + output: + - INNER_ENTER_BODY + - TASK_BODY_RAN + - INNER_EXIT_BODY + forbidden: + - WENTER= + - WEXIT= + - WTASK_RAN + - WrappedAction diff --git a/conformance-tests/2023-09/WRAP_ACTIONS/jobs/wrap-openjd-env-from-wrap-script-itself.test.yaml b/conformance-tests/2023-09/WRAP_ACTIONS/jobs/wrap-openjd-env-from-wrap-script-itself.test.yaml new file mode 100644 index 00000000..caeb9835 --- /dev/null +++ b/conformance-tests/2023-09/WRAP_ACTIONS/jobs/wrap-openjd-env-from-wrap-script-itself.test.yaml @@ -0,0 +1,55 @@ +# RFC 0008 "Stdout forwarding and macro propagation": "A wrap script MAY +# emit stdout macros directly." A macro emitted by the wrap script itself +# — here `openjd_env:` from onWrapEnvEnter, the Basic Example's +# DOCKER_CONTAINER_ID pattern — must be recognized by the runtime and +# surface in WrappedAction.Environment for subsequent wrapped actions. +# This is an EFFECT-based recognition test (the variable observably lands +# in the forwarded list), not a text-presence check. Catches a runtime +# that scans only forwarded grand-child output, or only unwrapped actions' +# stdout, for macros. +template: + specificationVersion: jobtemplate-2023-09 + name: WrapOpenjdEnvFromWrapScriptItself + jobEnvironments: + - name: InnerEnv + script: + actions: + onEnter: + command: python + args: ["-c", "print('INNER_ENTER_BODY')"] + steps: + - name: Step1 + script: + actions: + onRun: + command: echo + args: ["placeholder"] +environments: +- specificationVersion: environment-2023-09 + extensions: + - WRAP_ACTIONS + - EXPR + environment: + name: WrapEnv + script: + actions: + # The wrap script itself emits the macro; the wrapped onEnter is + # intentionally not executed (interception replaces it). + onWrapEnvEnter: + command: python + args: + - "-c" + - "print('openjd_env: WRAP_SELF_VAR=from-wrap-script')" + onWrapTaskRun: + command: python + args: + - "-c" + - "for e in {{repr_py(WrappedAction.Environment)}}:\n print('WAENV='+e)" + onWrapEnvExit: + command: python + args: + - "-c" + - "import subprocess,sys; sys.exit(subprocess.run([{{repr_py(WrappedAction.Command)}}]+{{repr_py(WrappedAction.Args)}}).returncode)" +expected: + output: + - WAENV=WRAP_SELF_VAR=from-wrap-script diff --git a/conformance-tests/2023-09/WRAP_ACTIONS/jobs/wrap-openjd-env-grand-child-under-exit-hook.test.yaml b/conformance-tests/2023-09/WRAP_ACTIONS/jobs/wrap-openjd-env-grand-child-under-exit-hook.test.yaml new file mode 100644 index 00000000..956c8443 --- /dev/null +++ b/conformance-tests/2023-09/WRAP_ACTIONS/jobs/wrap-openjd-env-grand-child-under-exit-hook.test.yaml @@ -0,0 +1,73 @@ +# RFC 0008 "Stdout forwarding and macro propagation": macros are +# recognized from wrap-hook stdout in the EXIT phase too — "any earlier +# action in the same session" includes actions run via onWrapEnvExit. +# Two inner environments: the inner-most (EnvBeta) exits first and its +# wrapped onExit grand-child exports EXIT_VAR; the outer inner env +# (EnvAlpha) exits after it, and its exit hook's WrappedAction.Environment +# must contain EXIT_VAR. Catches a runtime that stops harvesting macros +# once teardown begins. LIFO exit order is pinned through membership +# causality alone: each hook dumps before running its wrapped action, so +# EXIT_VAR can only ever appear in a dump if EnvBeta's onExit ran before +# EnvAlpha's dump — i.e. exits are inner-most first. +template: + specificationVersion: jobtemplate-2023-09 + name: WrapOpenjdEnvGrandChildUnderExitHook + jobEnvironments: + - name: EnvAlpha + script: + actions: + onEnter: + command: python + args: ["-c", "print('ALPHA_ENTER_BODY')"] + onExit: + command: python + args: ["-c", "print('ALPHA_EXIT_BODY')"] + - name: EnvBeta + script: + actions: + onEnter: + command: python + args: ["-c", "print('BETA_ENTER_BODY')"] + onExit: + command: python + args: ["-c", "print('openjd_env: EXIT_VAR=set-during-exit')"] + steps: + - name: Step1 + script: + actions: + onRun: + command: echo + args: ["placeholder"] +environments: +- specificationVersion: environment-2023-09 + extensions: + - WRAP_ACTIONS + - EXPR + environment: + name: WrapEnv + script: + actions: + onWrapEnvEnter: + command: python + args: + - "-c" + - "import subprocess,sys; sys.exit(subprocess.run([{{repr_py(WrappedAction.Command)}}]+{{repr_py(WrappedAction.Args)}}).returncode)" + onWrapTaskRun: + command: python + args: + - "-c" + - "import subprocess,sys; sys.exit(subprocess.run([{{repr_py(WrappedAction.Command)}}]+{{repr_py(WrappedAction.Args)}}).returncode)" + # Dump the forwarded variables tagged with which env is exiting, + # then run the wrapped onExit (whose macro is then forwarded). + onWrapEnvExit: + command: python + args: + - "-c" + - "import subprocess,sys\nfor e in {{repr_py(WrappedAction.Environment)}}:\n print('WAENV['+{{repr_py(WrappedEnv.Name)}}+']='+e)\nsys.exit(subprocess.run([{{repr_py(WrappedAction.Command)}}]+{{repr_py(WrappedAction.Args)}}).returncode)" +expected: + output: + - "WAENV[EnvAlpha]=EXIT_VAR=set-during-exit" + # No forbidden WAENV[EnvBeta] line: EnvBeta's dump runs before its own + # wrapped onExit by fixture construction (args are interpolated at + # launch), so no runtime behavior could ever produce it — such a line + # would be vacuous. diff --git a/conformance-tests/2023-09/WRAP_ACTIONS/jobs/wrap-openjd-env-task-grand-child-visible-next-task.test.yaml b/conformance-tests/2023-09/WRAP_ACTIONS/jobs/wrap-openjd-env-task-grand-child-visible-next-task.test.yaml new file mode 100644 index 00000000..137fef4e --- /dev/null +++ b/conformance-tests/2023-09/WRAP_ACTIONS/jobs/wrap-openjd-env-task-grand-child-visible-next-task.test.yaml @@ -0,0 +1,63 @@ +# RFC 0008 "Stdout forwarding and macro propagation": the runtime MUST +# include in WrappedAction.Environment every openjd_env-defined variable +# emitted by ANY earlier action in the same session — including a task's +# onRun that ran via onWrapTaskRun. Task 1's grand-child exports a +# variable; task 2's wrap hook must see it in WrappedAction.Environment. +# Catches a runtime that only harvests macros from environment lifecycle +# actions, or that rebuilds the forwarded list per step rather than +# accumulating per session. The forbidden: line additionally pins that +# each hook's variable snapshot is taken before its own wrapped action +# runs (TASK2_VAR is exported by the last task, so no later dump can +# contain it). +# +# ASSUMPTION: tasks execute in ascending parameter order (1 then 2). The +# spec leaves task scheduling order to the scheduler; every known runner +# executes a single-session range in ascending order, but a conformant +# scheduler running tasks reversed would falsely fail this fixture (it +# would not wrongly pass). +template: + specificationVersion: jobtemplate-2023-09 + name: WrapOpenjdEnvTaskGrandChildVisibleNextTask + steps: + - name: Step1 + parameterSpace: + taskParameterDefinitions: + - name: N + type: INT + range: [1, 2] + script: + actions: + onRun: + command: python + args: ["-c", "print('openjd_env: TASK{{Task.Param.N}}_VAR=set-by-task-{{Task.Param.N}}')"] +environments: +- specificationVersion: environment-2023-09 + extensions: + - WRAP_ACTIONS + - EXPR + environment: + name: WrapEnv + script: + actions: + onWrapEnvEnter: + command: python + args: + - "-c" + - "import subprocess,sys; sys.exit(subprocess.run([{{repr_py(WrappedAction.Command)}}]+{{repr_py(WrappedAction.Args)}}).returncode)" + # Dump the forwarded variables, then run the wrapped task so its + # macro is forwarded through this script's stdout. + onWrapTaskRun: + command: python + args: + - "-c" + - "import subprocess,sys\nfor e in {{repr_py(WrappedAction.Environment)}}:\n print('WAENV='+e)\nsys.exit(subprocess.run([{{repr_py(WrappedAction.Command)}}]+{{repr_py(WrappedAction.Args)}}).returncode)" + onWrapEnvExit: + command: python + args: + - "-c" + - "import subprocess,sys; sys.exit(subprocess.run([{{repr_py(WrappedAction.Command)}}]+{{repr_py(WrappedAction.Args)}}).returncode)" +expected: + output: + - WAENV=TASK1_VAR=set-by-task-1 + forbidden: + - WAENV=TASK2_VAR diff --git a/conformance-tests/2023-09/WRAP_ACTIONS/jobs/wrap-progress-status-macros-forwarded.test.yaml b/conformance-tests/2023-09/WRAP_ACTIONS/jobs/wrap-progress-status-macros-forwarded.test.yaml new file mode 100644 index 00000000..67f361b9 --- /dev/null +++ b/conformance-tests/2023-09/WRAP_ACTIONS/jobs/wrap-progress-status-macros-forwarded.test.yaml @@ -0,0 +1,60 @@ +# RFC 0008 "Stdout forwarding and macro propagation": the runtime scans +# the wrap script's stdout for ALL OpenJD macros — openjd_progress and +# openjd_status included — whether the wrap script emits them itself or +# forwards them from the wrapped grand-child. +# +# Honest scope note: this conformance runner observes only the session +# log; recognition of progress/status has no further observable EFFECT +# there (unlike openjd_env, whose recognition is pinned effect-wise by +# sibling fixtures). What this fixture pins is the MUST that the wrap +# layer forwards macro-shaped lines VERBATIM — a wrap layer or runtime +# that consumes, reorders into, buffers away, or transforms +# openjd_progress/openjd_status lines (e.g. stripping recognized macros +# from the log) fails. Recognition-as-effect belongs in implementation +# tests. Caveat: a runtime that recognizes these macros and deliberately +# consumes them out of its log is arguably conformant with the RFC yet +# fails this fixture; if such a runtime appears, this fixture should be +# relaxed rather than the runtime changed. +template: + specificationVersion: jobtemplate-2023-09 + name: WrapProgressStatusMacrosForwarded + steps: + - name: Step1 + script: + actions: + onRun: + command: python + args: + - "-c" + - "print('openjd_progress: 55.5'); print('openjd_status: STATUS_FROM_GRAND_CHILD')" +environments: +- specificationVersion: environment-2023-09 + extensions: + - WRAP_ACTIONS + - EXPR + environment: + name: WrapEnv + script: + actions: + onWrapEnvEnter: + command: python + args: + - "-c" + - "import subprocess,sys; sys.exit(subprocess.run([{{repr_py(WrappedAction.Command)}}]+{{repr_py(WrappedAction.Args)}}).returncode)" + # The wrap script emits a status macro itself, then forwards the + # grand-child's stdout (which carries progress + status macros). + onWrapTaskRun: + command: python + args: + - "-c" + - "import subprocess,sys\nprint('openjd_status: STATUS_FROM_WRAP_SCRIPT')\nsys.exit(subprocess.run([{{repr_py(WrappedAction.Command)}}]+{{repr_py(WrappedAction.Args)}}).returncode)" + onWrapEnvExit: + command: python + args: + - "-c" + - "import subprocess,sys; sys.exit(subprocess.run([{{repr_py(WrappedAction.Command)}}]+{{repr_py(WrappedAction.Args)}}).returncode)" +expected: + output: + - "openjd_status: STATUS_FROM_WRAP_SCRIPT" + - "openjd_progress: 55.5" + - "openjd_status: STATUS_FROM_GRAND_CHILD" diff --git a/conformance-tests/2023-09/WRAP_ACTIONS/jobs/wrap-stderr-forwarded-from-grand-child.test.yaml b/conformance-tests/2023-09/WRAP_ACTIONS/jobs/wrap-stderr-forwarded-from-grand-child.test.yaml new file mode 100644 index 00000000..c91a2135 --- /dev/null +++ b/conformance-tests/2023-09/WRAP_ACTIONS/jobs/wrap-stderr-forwarded-from-grand-child.test.yaml @@ -0,0 +1,55 @@ +# RFC 0008 "Stdout forwarding and macro propagation": "Wrap scripts MUST +# forward the wrapped process's stdout AND STDERR to their own stdout and +# stderr verbatim, without buffering, filtering, or transformation." +# stderr forwarding has no other fixture. The grand-child writes a marker +# to stderr; the wrap script launches it with inherited streams; the +# marker must reach the session log. Catches a wrap-layer runtime (or a +# conformance implementation of the reference pattern) that captures or +# swallows the grand-child's stderr while forwarding only stdout. +template: + specificationVersion: jobtemplate-2023-09 + name: WrapStderrForwardedFromGrandChild + steps: + - name: Step1 + script: + actions: + onRun: + command: python + args: + - "-c" + - "import sys; print('STDERR_MARKER_FROM_GRAND_CHILD', file=sys.stderr); print('STDOUT_BODY_RAN')" +environments: +- specificationVersion: environment-2023-09 + extensions: + - WRAP_ACTIONS + - EXPR + environment: + name: WrapEnv + script: + actions: + onWrapEnvEnter: + command: python + args: + - "-c" + - "import subprocess,sys; sys.exit(subprocess.run([{{repr_py(WrappedAction.Command)}}]+{{repr_py(WrappedAction.Args)}}).returncode)" + # subprocess.run without capture: the grand-child inherits the + # wrap script's stdout and stderr, i.e. verbatim forwarding. + # WTASK_HOOK_RAN pins that the wrapped path was actually taken: a + # runtime that accepts WRAP_ACTIONS but never fires the hooks would + # run onRun directly and still emit both body markers, so without + # this sentinel the fixture would pass vacuously. + onWrapTaskRun: + command: python + args: + - "-c" + - "import subprocess,sys; print('WTASK_HOOK_RAN'); sys.exit(subprocess.run([{{repr_py(WrappedAction.Command)}}]+{{repr_py(WrappedAction.Args)}}).returncode)" + onWrapEnvExit: + command: python + args: + - "-c" + - "import subprocess,sys; sys.exit(subprocess.run([{{repr_py(WrappedAction.Command)}}]+{{repr_py(WrappedAction.Args)}}).returncode)" +expected: + output: + - WTASK_HOOK_RAN + - STDERR_MARKER_FROM_GRAND_CHILD + - STDOUT_BODY_RAN diff --git a/conformance-tests/2023-09/WRAP_ACTIONS/jobs/wrap-two-inner-envs-wrappedenv-name-per-invocation.test.yaml b/conformance-tests/2023-09/WRAP_ACTIONS/jobs/wrap-two-inner-envs-wrappedenv-name-per-invocation.test.yaml new file mode 100644 index 00000000..b001677d --- /dev/null +++ b/conformance-tests/2023-09/WRAP_ACTIONS/jobs/wrap-two-inner-envs-wrappedenv-name-per-invocation.test.yaml @@ -0,0 +1,71 @@ +# RFC 0008 "Modifications to How Jobs Are Run": wrap hooks intercept the +# lifecycle actions of EVERY inner environment — no fixture elsewhere +# stacks two inner environments under one wrap env. Both inner envs' +# onEnter and onExit must each be replaced by the corresponding hook, with +# WrappedEnv.Name carrying the right environment name on each invocation. +# Catches a runtime that wraps only the first (or last) inner env, or that +# caches WrappedEnv.Name across invocations. Membership assertions only — +# the runner cannot assert order, so LIFO exit sequencing is pinned +# separately (wrap-openjd-env-grand-child-under-exit-hook). +template: + specificationVersion: jobtemplate-2023-09 + name: WrapTwoInnerEnvsWrappedEnvNamePerInvocation + jobEnvironments: + - name: EnvAlpha + script: + actions: + onEnter: + command: python + args: ["-c", "print('ALPHA_ENTER_BODY')"] + onExit: + command: python + args: ["-c", "print('ALPHA_EXIT_BODY')"] + - name: EnvBeta + script: + actions: + onEnter: + command: python + args: ["-c", "print('BETA_ENTER_BODY')"] + onExit: + command: python + args: ["-c", "print('BETA_EXIT_BODY')"] + steps: + - name: Step1 + script: + actions: + onRun: + command: python + args: ["-c", "print('TASK_BODY_RAN')"] +environments: +- specificationVersion: environment-2023-09 + extensions: + - WRAP_ACTIONS + - EXPR + environment: + name: WrapEnv + script: + actions: + # Print-only hooks: the wrapped bodies are replaced and NOT + # executed, so the *_BODY sentinels must not appear. + onWrapEnvEnter: + command: python + args: ["-c", "print('WENTER='+{{repr_py(WrappedEnv.Name)}})"] + onWrapTaskRun: + command: python + args: ["-c", "print('WTASK_RAN')"] + onWrapEnvExit: + command: python + args: ["-c", "print('WEXIT='+{{repr_py(WrappedEnv.Name)}})"] +expected: + output: + - WENTER=EnvAlpha + - WENTER=EnvBeta + - WEXIT=EnvAlpha + - WEXIT=EnvBeta + - WTASK_RAN + forbidden: + - ALPHA_ENTER_BODY + - BETA_ENTER_BODY + - ALPHA_EXIT_BODY + - BETA_EXIT_BODY + - TASK_BODY_RAN diff --git a/conformance-tests/2023-09/WRAP_ACTIONS/jobs/wrap-two-steps-wrappedstep-name-per-step.test.yaml b/conformance-tests/2023-09/WRAP_ACTIONS/jobs/wrap-two-steps-wrappedstep-name-per-step.test.yaml new file mode 100644 index 00000000..9848e3ee --- /dev/null +++ b/conformance-tests/2023-09/WRAP_ACTIONS/jobs/wrap-two-steps-wrappedstep-name-per-step.test.yaml @@ -0,0 +1,56 @@ +# RFC 0008 "Modifications to How Jobs Are Run": "Tasks from any Step in a +# Job can run within the same Session" — and every task's onRun is +# intercepted by onWrapTaskRun, with WrappedStep.Name carrying the step of +# the task CURRENTLY being wrapped. Two steps run in one session; the hook +# must fire for both and see a different WrappedStep.Name each time. +# Catches a runtime that binds WrappedStep.Name once per session (stale +# name for the second step) or that stops wrapping after the first step +# completes. Membership assertions only; no ordering is asserted. +template: + specificationVersion: jobtemplate-2023-09 + name: WrapTwoStepsWrappedStepNamePerStep + steps: + - name: StepOne + script: + actions: + onRun: + command: python + args: ["-c", "print('TASK_ONE_BODY')"] + - name: StepTwo + script: + actions: + onRun: + command: python + args: ["-c", "print('TASK_TWO_BODY')"] +environments: +- specificationVersion: environment-2023-09 + extensions: + - WRAP_ACTIONS + - EXPR + environment: + name: WrapEnv + script: + actions: + onWrapEnvEnter: + command: python + args: + - "-c" + - "import subprocess,sys; sys.exit(subprocess.run([{{repr_py(WrappedAction.Command)}}]+{{repr_py(WrappedAction.Args)}}).returncode)" + # Tag each invocation with the wrapped step's name, then run the + # wrapped task. + onWrapTaskRun: + command: python + args: + - "-c" + - "import subprocess,sys\nprint('WSTEP='+{{repr_py(WrappedStep.Name)}})\nsys.exit(subprocess.run([{{repr_py(WrappedAction.Command)}}]+{{repr_py(WrappedAction.Args)}}).returncode)" + onWrapEnvExit: + command: python + args: + - "-c" + - "import subprocess,sys; sys.exit(subprocess.run([{{repr_py(WrappedAction.Command)}}]+{{repr_py(WrappedAction.Args)}}).returncode)" +expected: + output: + - WSTEP=StepOne + - WSTEP=StepTwo + - TASK_ONE_BODY + - TASK_TWO_BODY diff --git a/conformance-tests/2023-09/WRAP_ACTIONS/jobs/wrap-unwrapped-parity-task-failure.test.yaml b/conformance-tests/2023-09/WRAP_ACTIONS/jobs/wrap-unwrapped-parity-task-failure.test.yaml new file mode 100644 index 00000000..f1254557 --- /dev/null +++ b/conformance-tests/2023-09/WRAP_ACTIONS/jobs/wrap-unwrapped-parity-task-failure.test.yaml @@ -0,0 +1,22 @@ +# RFC 0008 "Failure semantics" (parity control): "The session handles +# each failure exactly as it would in the unwrapped case." This is the +# UNWRAPPED twin of wrap-exit-status-python — the same failing task (exit +# 42) with no wrap environment at all. The pair pins the parity claim: +# both runs must surface a task failure with the identical exit code. A +# divergence in either direction (wrap layer altering the code, or the +# unwrapped baseline behaving differently than the wrapped fixture +# assumes) breaks exactly one of the two fixtures and points at the side +# that changed. +template: + specificationVersion: jobtemplate-2023-09 + name: WrapUnwrappedParityTaskFailure + steps: + - name: Step1 + script: + actions: + onRun: + command: python + args: ["-c", "import sys; sys.exit(42)"] +expected: + taskFailure: + exitCode: 42 diff --git a/conformance-tests/2023-09/WRAP_ACTIONS/jobs/wrap-variables-only-inner-env-skips-hooks.test.yaml b/conformance-tests/2023-09/WRAP_ACTIONS/jobs/wrap-variables-only-inner-env-skips-hooks.test.yaml new file mode 100644 index 00000000..73533e75 --- /dev/null +++ b/conformance-tests/2023-09/WRAP_ACTIONS/jobs/wrap-variables-only-inner-env-skips-hooks.test.yaml @@ -0,0 +1,50 @@ +# RFC 0008 "Nothing-to-replace rule": an inner environment that "defines +# no `script` at all (a `variables:`-only environment)" has nothing to +# replace, so the runtime "MUST NOT run the corresponding wrap hook for +# that environment" — neither onWrapEnvEnter nor onWrapEnvExit. A hook is +# a replacement, not a lifecycle notification. Catches a runtime that +# fires wrap hooks per environment-stack transition instead of per +# replaced action (WrappedAction.Command/Args would carry no meaningful +# values). The positive assertions pin that the variables: map is still +# applied — it surfaces in WrappedAction.Environment for the task. +template: + specificationVersion: jobtemplate-2023-09 + name: WrapVariablesOnlyInnerEnvSkipsHooks + jobEnvironments: + - name: VarsOnlyEnv + variables: + STATIC_FROM_VARS: vars-value + steps: + - name: Step1 + script: + actions: + onRun: + command: echo + args: ["placeholder"] +environments: +- specificationVersion: environment-2023-09 + extensions: + - WRAP_ACTIONS + - EXPR + environment: + name: WrapEnv + script: + actions: + onWrapEnvEnter: + command: python + args: ["-c", "print('WENTER='+{{repr_py(WrappedEnv.Name)}})"] + onWrapTaskRun: + command: python + args: + - "-c" + - "for e in {{repr_py(WrappedAction.Environment)}}:\n print('WAENV='+e)\nprint('WTASK_RAN')" + onWrapEnvExit: + command: python + args: ["-c", "print('WEXIT='+{{repr_py(WrappedEnv.Name)}})"] +expected: + output: + - WTASK_RAN + - WAENV=STATIC_FROM_VARS=vars-value + forbidden: + - WENTER=VarsOnlyEnv + - WEXIT=VarsOnlyEnv