Skip to content

test: timezone information was dropped when parsing the cancel NotifyEnd#261

Merged
epmog merged 1 commit into
OpenJobDescription:mainlinefrom
epmog:fix_timezone_in_notify
Aug 21, 2025
Merged

test: timezone information was dropped when parsing the cancel NotifyEnd#261
epmog merged 1 commit into
OpenJobDescription:mainlinefrom
epmog:fix_timezone_in_notify

Conversation

@epmog

@epmog epmog commented Aug 21, 2025

Copy link
Copy Markdown
Contributor

What was the problem/requirement? (What/Why)

1 test was failing locally for me on my macOS machine - test_cancel_notify.

FAILED test/openjd/sessions/test_runner_base.py::TestScriptRunnerBase::test_cancel_notify - assert datetime.timedelta(seconds=18001, microseconds=361076) < datetime.timedelta(seconds=3)

hint: 18000seconds is 5 hours or GMT-5

At first i thought there was some nuance in datetimes on macOS vs. other OSes. But it turns out that this fails on Windows too if the timezone for the machine isn't UTC.

What was the solution? (How)

We were dropping the timezone information ('Z') from the cancel notify file and so python was interpreting it as the local time before then changing to UTC. Didn't discover this previously since all of our runners are on UTC.

What is the impact of this change?

All non-cross-user tests run locally on macOS (and other OSes if you're not in UTC)!

How was this change tested?

hatch run test
...
==== 472 passed, 26 skipped, 15 xfailed in 44.25s ====

Was this change documented?

N/A

Is this a breaking change?

No

Does this change impact security?

No


By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

@epmog
epmog requested a review from a team as a code owner August 21, 2025 16:32
lucaseck
lucaseck previously approved these changes Aug 21, 2025
Signed-off-by: Morgan Epp <60796713+epmog@users.noreply.github.com>
@sonarqubecloud

Copy link
Copy Markdown

@epmog
epmog enabled auto-merge (squash) August 21, 2025 17:10
@epmog
epmog merged commit 6de1e73 into OpenJobDescription:mainline Aug 21, 2025
30 of 31 checks passed
leongdl added a commit to leongdl/openjd-sessions-for-python that referenced this pull request Jul 16, 2026
Change WrappedAction.Cancelation.Mode from an empty-string sentinel to
None (string?) when the wrapped action defines no <Cancelation>,
matching the EXPR semantics for optional data and the int? typing of
Cancelation.NotifyPeriodInSeconds. None renders as the empty string in
format-string interpolation, and the nullness is observable via EXPR
null-coalescing.

Tests assert `is None` for the undeclared case and add an execution
test that the null mode interpolates as MODE=<>, mirroring the updated
wrap-cancelation-mode-null-when-no-cancelation conformance fixture in
openjd-specifications and the Rust change in openjd-rs.

Addresses review feedback on openjd-rs PR OpenJobDescription#261 (discussion
r3597572812) and openjd-specifications PR OpenJobDescription#148 (r3597560515).

Signed-off-by: David Leong <116610336+leongdl@users.noreply.github.com>
leongdl added a commit to leongdl/openjd-sessions-for-python that referenced this pull request Jul 16, 2026
Change WrappedAction.Cancelation.Mode from an empty-string sentinel to
None (string?) when the wrapped action defines no <Cancelation>,
matching the EXPR semantics for optional data and the int? typing of
Cancelation.NotifyPeriodInSeconds. None renders as the empty string in
format-string interpolation, and the nullness is observable via EXPR
null-coalescing.

Also address review findings on the wrap-action implementation:

- Fail gracefully when WrappedAction.* injection cannot resolve the
  wrapped action's format strings (e.g. a wrapped onRun referencing
  {{Task.File.*}} or onEnter referencing {{Env.File.*}} — embedded
  files are not materialized on the wrap path yet, a known limitation
  shared with the Rust runtime). The action now FAILs through the
  normal callback path via a new _fail_action_before_start helper and
  the session transitions to READY_ENDING, instead of a raw
  FormatStringError escaping the public API — which, for
  enter_environment, previously left the session stuck in RUNNING
  with no runner. Also covers non-integer FEATURE_BUNDLE_1
  timeout/notifyPeriod resolutions.
- WrappedAction.Environment now carries only openjd_env-defined
  variables per RFC 0008, excluding the environment's declarative
  variables: map seed (parity with the Rust runtime, which was
  already correct).
- Log a warning when a wrap environment is active but run_task() was
  not given step_name, since {{WrappedStep.Name}} renders empty.
- Hoist the Template Schemas 5.3.2 notify-period defaults (120/30)
  into shared constants used by the seeding and both cancel paths.
- Guard _run_wrap_hook against unknown hook names so a typo cannot
  become a silent SUCCESS no-op.

Tests assert `is None` for the undeclared cancelation case, the
injection-failure and READY_ENDING behavior, the openjd_env-only
Environment contents, and the hook-name guard. All 53 WRAP_ACTIONS
conformance tests pass against the Python CLI.

Addresses review feedback on openjd-rs PR OpenJobDescription#261 (discussion
r3597572812) and openjd-specifications PR OpenJobDescription#148 (r3597560515).

Signed-off-by: David Leong <116610336+leongdl@users.noreply.github.com>
leongdl added a commit to leongdl/openjd-sessions-for-python that referenced this pull request Jul 17, 2026
Change WrappedAction.Cancelation.Mode from an empty-string sentinel to
None (string?) when the wrapped action defines no <Cancelation>,
matching the EXPR semantics for optional data and the int? typing of
Cancelation.NotifyPeriodInSeconds. None renders as the empty string in
format-string interpolation, and the nullness is observable via EXPR
null-coalescing.

Also address review findings on the wrap-action implementation:

- Fail gracefully when WrappedAction.* injection cannot resolve the
  wrapped action's format strings (e.g. a wrapped onRun referencing
  {{Task.File.*}} or onEnter referencing {{Env.File.*}} — embedded
  files are not materialized on the wrap path yet, a known limitation
  shared with the Rust runtime). The action now FAILs through the
  normal callback path via a new _fail_action_before_start helper and
  the session transitions to READY_ENDING, instead of a raw
  FormatStringError escaping the public API — which, for
  enter_environment, previously left the session stuck in RUNNING
  with no runner. Also covers non-integer FEATURE_BUNDLE_1
  timeout/notifyPeriod resolutions.
- WrappedAction.Environment now carries only openjd_env-defined
  variables per RFC 0008, excluding the environment's declarative
  variables: map seed (parity with the Rust runtime, which was
  already correct).
- WrappedAction.Environment is flattened cumulatively in
  environment-entry order, so a later openjd_env set yields one
  effective entry (not a duplicate) and a later unset removes the
  name — matching the real subprocess environment and Rust's single
  cumulative env_vars map.
- On the onWrapEnvExit path, the openjd_env list is captured before
  the exiting environment is removed from tracking, so the wrapped
  environment's own openjd_env variables appear in
  WrappedAction.Environment just as they do in the real subprocess
  environment.
- Log a warning when a wrap environment is active but run_task() was
  not given step_name, since {{WrappedStep.Name}} renders empty.
- Hoist the Template Schemas 5.3.2 notify-period defaults (120/30)
  into shared constants used by the seeding and both cancel paths.
- Guard _run_wrap_hook against unknown hook names so a typo cannot
  become a silent SUCCESS no-op.

Tests assert `is None` for the undeclared cancelation case, the
injection-failure and READY_ENDING behavior, the openjd_env-only
Environment contents (including override/unset flattening and the
exit-path capture), and the hook-name guard. All 53 WRAP_ACTIONS
conformance tests pass against the Python CLI.

Addresses review feedback on openjd-rs PR OpenJobDescription#261 (discussion
r3597572812) and openjd-specifications PR OpenJobDescription#148 (r3597560515).

Signed-off-by: David Leong <116610336+leongdl@users.noreply.github.com>
leongdl added a commit to leongdl/openjd-sessions-for-python that referenced this pull request Jul 17, 2026
Runtime half of RFC 0008 cancelation/timeout round-trip forwarding
(openjd-rs PR OpenJobDescription#261 review discussion r3597453516; model change in
openjd-model-for-python's CancelationMethodDeferred):

    timeout: "{{WrappedAction.Timeout}}"
    cancelation:
      mode: "{{WrappedAction.Cancelation.Mode}}"
      notifyPeriodInSeconds: "{{WrappedAction.Cancelation.NotifyPeriodInSeconds}}"

The problem: format strings are normally delay-processed, but a
cancelation's `mode` is the schema selector — the parser needs it at
parse time, while a forwarded value only exists at run time. The model
now carries such a mode as CancelationMethodDeferred, and this change
is where the deferred decision lands: a new shared
resolve_effective_cancelation helper (see its docstring for the full
story) resolves the mode against the live symbol table right when it
is needed. Null (the wrapped action declared no <Cancelation>) drops
the whole cancelation object; a resolved TERMINATE rejects a non-null
notify period; anything else fails. Notify periods resolve with int?
whole-field semantics (null = field omitted, positional default).

- The seed path (_inject_wrapped_cancelation_symbols) and both cancel
  enforcement paths (StepScriptRunner.cancel,
  EnvironmentScriptRunner.cancel) resolve through the same helper, so
  the values wrap scripts see always match what the runtime enforces.
  Cancels fall back to Terminate with a warning if resolution fails,
  since a cancel must always proceed.
- WrappedAction.Timeout is now int? — None (null) when the wrapped
  action specifies no timeout — replacing the 0 sentinel, per the spec
  change; whole-field forwarding drops the field so the hook
  position's default applies.
- _run_action now resolves a FormatString timeout against the symtab
  right before launch (previously it passed the FormatString object
  straight into timedelta), treating a null resolution as "no timeout
  field" and failing the action on non-positive-integer resolutions.

Tests updated for the int?/None timeout; all wrap/cancel suites pass,
and all 58 WRAP_ACTIONS conformance fixtures (including the new
wrap-cancelation-roundtrip-* and timeout-null cases) pass against the
Python CLI.

Signed-off-by: David Leong <116610336+leongdl@users.noreply.github.com>
leongdl added a commit to leongdl/openjd-sessions-for-python that referenced this pull request Jul 17, 2026
Runtime half of RFC 0008 cancelation/timeout round-trip forwarding
(openjd-rs PR OpenJobDescription#261 review discussion r3597453516; model change in
openjd-model-for-python's CancelationMethodDeferred):

    timeout: "{{WrappedAction.Timeout}}"
    cancelation:
      mode: "{{WrappedAction.Cancelation.Mode}}"
      notifyPeriodInSeconds: "{{WrappedAction.Cancelation.NotifyPeriodInSeconds}}"

The problem: format strings are normally delay-processed, but a
cancelation's `mode` is the schema selector — the parser needs it at
parse time, while a forwarded value only exists at run time. The model
now carries such a mode as CancelationMethodDeferred, and this change
is where the deferred decision lands: a new shared
resolve_effective_cancelation helper (see its docstring for the full
story) resolves the mode against the live symbol table right when it
is needed. Null (the wrapped action declared no <Cancelation>) drops
the whole cancelation object; a resolved TERMINATE rejects a non-null
notify period; anything else fails. Notify periods resolve with int?
whole-field semantics (null = field omitted, positional default).

- The seed path (_inject_wrapped_cancelation_symbols) and both cancel
  enforcement paths (StepScriptRunner.cancel,
  EnvironmentScriptRunner.cancel) resolve through the same helper, so
  the values wrap scripts see always match what the runtime enforces.
  Cancels fall back to Terminate with a warning if resolution fails,
  since a cancel must always proceed.
- WrappedAction.Timeout is now int? — None (null) when the wrapped
  action specifies no timeout — replacing the 0 sentinel, per the spec
  change; whole-field forwarding drops the field so the hook
  position's default applies.
- _run_action now resolves a FormatString timeout against the symtab
  right before launch (previously it passed the FormatString object
  straight into timedelta), treating a null resolution as "no timeout
  field" and failing the action on non-positive-integer resolutions.

Tests updated for the int?/None timeout; all wrap/cancel suites pass,
and all 58 WRAP_ACTIONS conformance fixtures (including the new
wrap-cancelation-roundtrip-* and timeout-null cases) pass against the
Python CLI.

Signed-off-by: David Leong <116610336+leongdl@users.noreply.github.com>
leongdl added a commit to leongdl/openjd-sessions-for-python that referenced this pull request Jul 17, 2026
Runtime half of RFC 0008 cancelation/timeout round-trip forwarding
(openjd-rs PR OpenJobDescription#261 review discussion r3597453516; model change in
openjd-model-for-python's CancelationMethodDeferred):

    timeout: "{{WrappedAction.Timeout}}"
    cancelation:
      mode: "{{WrappedAction.Cancelation.Mode}}"
      notifyPeriodInSeconds: "{{WrappedAction.Cancelation.NotifyPeriodInSeconds}}"

The problem: format strings are normally delay-processed, but a
cancelation's `mode` is the schema selector — the parser needs it at
parse time, while a forwarded value only exists at run time. The model
now carries such a mode as CancelationMethodDeferred, and this change
is where the deferred decision lands: a new shared
resolve_effective_cancelation helper (see its docstring for the full
story) resolves the mode against the live symbol table right when it
is needed. Null (the wrapped action declared no <Cancelation>) drops
the whole cancelation object; a resolved TERMINATE rejects a non-null
notify period; anything else fails. Notify periods resolve with int?
whole-field semantics (null = field omitted, positional default).

- The seed path (_inject_wrapped_cancelation_symbols) and both cancel
  enforcement paths (StepScriptRunner.cancel,
  EnvironmentScriptRunner.cancel) resolve through the same helper, so
  the values wrap scripts see always match what the runtime enforces.
  Cancels fall back to Terminate with a warning if resolution fails,
  since a cancel must always proceed.
- WrappedAction.Timeout is now int? — None (null) when the wrapped
  action specifies no timeout — replacing the 0 sentinel, per the spec
  change; whole-field forwarding drops the field so the hook
  position's default applies.
- _run_action now resolves a FormatString timeout against the symtab
  right before launch (previously it passed the FormatString object
  straight into timedelta), treating a null resolution as "no timeout
  field" and failing the action on non-positive-integer resolutions.

Tests updated for the int?/None timeout; all wrap/cancel suites pass,
and all 58 WRAP_ACTIONS conformance fixtures (including the new
wrap-cancelation-roundtrip-* and timeout-null cases) pass against the
Python CLI.

Signed-off-by: David Leong <116610336+leongdl@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants