Skip to content

test: make single-task job fixtures assert their own output - #174

Open
leongdl wants to merge 2 commits into
OpenJobDescription:mainlinefrom
leongdl:conformance-single-task-self-assert
Open

test: make single-task job fixtures assert their own output#174
leongdl wants to merge 2 commits into
OpenJobDescription:mainlinefrom
leongdl:conformance-single-task-self-assert

Conversation

@leongdl

@leongdl leongdl commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

What changed

187 single-task job fixtures across base, EXPR, FEATURE_BUNDLE_1 and
REDACTED_ENV_VARS now assert their own output, and run_openjd_cli_tests.py now
requires a valid job test to exit 0.

1160 passed, 0 failed on openjd-rs and on openjd-cli 0.7.6. The diff is purely
additive: 187 files, +7856, −0.

Why

expected.output is only checkable by a runner that can read the implementation's
stdout. An implementation that can only observe task status — a service rather than
a CLI whose pipe the runner holds — gets "a task ran and exited 0", not "it produced the
right answer". {{Param.Version}} resolving to 2.0 instead of 1.0 exits 0 and
passes.

Each instrumented case's onRun takes one leading argument, an
OpenJDConformanceAssert embedded file, followed by its original command and args:

      actions:
        onRun:
          command: python
          args:
          - '{{Task.File.OpenJDConformanceAssert}}'
          - python
          - -c
          - print(r'OUTPUT:{{Param.Version}}')

The wrapper runs sys.argv[1:], echoes the output verbatim, compares it against
literals baked into itself, and exits non-zero on mismatch. expected.output /
expected.forbidden are untouched, so a log-scanning runner is unaffected — both
mechanisms now check the same thing by different means.

In-idiom rather than new: expected.taskFailure already lets exit status carry a
verdict.

928 of 1554 declared assertion lines are now checked inside the task:

bundle cases instrumented lines checked declared
base 167 61 114 453
EXPR 142 119 802 903
FEATURE_BUNDLE_1 13 6 10 16
REDACTED_ENV_VARS 8 1 2 23
WRAP_ACTIONS 65 0 0 139
TASK_CHUNKING 7 0 0 20

Why the argv is passed through rather than embedded

Embedding the original argv in the wrapper's source works for base and would have
quietly gutted several EXPR cases. expr1.3.2--list-flattens-in-args asserts
ARG0:--widthCOUNT:10; its subject is how the implementation expands an args
list, flattening a list value into separate arguments and skipping a null one. Freezing
argv into the wrapper moves that expansion from the implementation into the fixture, so
the case keeps passing while testing nothing.

Passing argv through leaves the expansion where it belongs, and removes every escaping
hazard embedding had.

Two false passes found on the way

The runner never checked exit status for a valid case. A mutation probe — corrupt
what the job prints, leave the expected literal alone — passed:

EXPECTED = ["OUTPUT:hello"]   job prints OUTPUT:WRONG
-> OPENJD_CONFORMANCE_ASSERT_FAILED: missing expected output: OUTPUT:hello
-> Process exited with code: 1
-> ✓

Two independent causes, both fixed. The runner ignored returncode when no
expected.taskFailure was declared, so the verdict rested entirely on substring
matching; and the wrapper's diagnostic echoed the expected literal, so the log scan
matched the error message. Diagnostics now report expected output line 1 of 2 not found and never the text. The probe reports on both CLIs now.

The exit-status requirement is a hole in the shared runner independent of the fixtures,
so it is a separate commit.

46 wrappers that never ran. All 47 instrumentable WRAP_ACTIONS cases passed after
instrumentation, then the success marker showed 46 of them had not executed the wrapper
at all: onWrapTaskRun replaces the task action, so a hook like
args: ["-c", "print('TIMEOUT={{WrappedAction.Timeout}}')"] never runs
WrappedAction.Command. Shipping those would have added verification that looks present
and cannot fail — the same failure one layer up. WRAP_ACTIONS is excluded wholesale.

That is what OPENJD_CONFORMANCE_ASSERT_OK: <n> expected, <m> forbidden is for, and why
it is worth keeping.

Deliberately left status-only

left out why
Multi-task cases Each task sees only its own output; a per-task exit code cannot assert that N lines appeared across N tasks. Needs aggregation outside the job.
9 redaction cases 4--openjd-redacted-env expects SECRET_IS:********, the implementation's redaction of what the task printed. The task sees the real value. unset-takes-precedence is the one that does self-assert, because there the variable really is unset.
4 env-output cases 7.3--env-file-reference asserts a line printed by a job environment's onEnter; its task prints something else.
All of WRAP_ACTIONS Above.
wrap-no-args It asserts WrappedAction.Args surfaces as an empty list when the action has no args. Adding an argument changes the subject. Encoded as a rule: an action with no args is never instrumented.
expected.taskFailure cases Already carry their verdict in exit status.

Every one of these except the first was found by instrumenting the case and watching it
fail, or watching it pass without asserting — not by reading the fixture.

Testing

result
openjd-rs 2023-09/* 1160 passed, 0 failed
openjd-cli 0.7.6 2023-09/* 1160 passed, 0 failed
Assertion confirmed to have executed 180 of 187 on both CLIs
Mutation probe on both; before the fixes
Original argv, command and expected preserved 187 of 187, checked against the parent commit

The 7 not confirmed are Windows-only and skipped on a POSIX host
(7.3--path-param-mapping-windows, 7.3--rawparam-no-mapping-windows, four
expr2.3.2--uri-*-windows, 6.1--end-of-line-auto-windows). Each has a POSIX sibling
that passes and differs only in which literal list the wrapper selects, so the risk is
low — but they are genuinely unverified until CI runs them on windows-latest.

Signed-off-by: David Leong <leongdl@amazon.com>
Signed-off-by: David Leong <leongdl@amazon.com>
- name: OpenJDConformanceAssert
type: TEXT
data: |
# Self-asserting conformance wrapper. Runs `sys.argv[1:]` -- this case's original

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Almost the whole diff appears to repeat this boilerplate. This likely reduces the readability of these tests for someone casually browsing these files. Are there other ways we could approach this?

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.

Yeah, thanks to claude Fable :) I have a new steering prompt to DELETE all this cruff.

Sorry I have not had time to review this one but it is necessary to make the tests robust and useful for backend.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants