@@ -76,13 +76,14 @@ test/integ/test_scenes/<scene_name>/
7676| ` physical ` | Physical | Basic physical renderer test |
7777| ` phy_apos_path ` | Physical | Path with special characters (apostrophes) |
7878| ` physical_chunking ` | Physical | Frame chunking across tasks |
79+ | ` physical_custom_fps ` | Physical | Custom frames-per-second setting |
7980| ` physical_multi_takes ` | Physical | Multiple takes rendering |
8081| ` physical_textured ` | Physical | Scene with textures |
8182| ` physical_tiles ` | Physical | Tile rendering |
8283| ` redshift ` | Redshift | Basic Redshift render |
8384| ` redshift_takes ` | Redshift | Redshift with multiple takes |
8485| ` redshift_textured ` | Redshift | Redshift with textures |
85- | ` redshift_textured_with_nonascii_characters ` | Redshift | Non-ASCII path handling |
86+ | ` redshift_textured_nonascii ` | Redshift | Non-ASCII path handling |
8687| ` redshift_tiles ` | Redshift | Redshift tile rendering |
8788
8889### Adding a new test scene
@@ -129,7 +130,7 @@ pytest (parent process)
129130 │ └─ builds the subprocess env overlay (endpoint override, dummy creds,
130131 │ telemetry opt-out, isolated HOME, DEADLINE_CLOUD_MOCK_MODE=1)
131132 │
132- ├─ build_cinema4d_scene() ── runs c4dpy input/scene.py ──▶ cube .c4d (in actual/)
133+ ├─ build_cinema4d_scene() ── runs c4dpy input/scene.py ──▶ <case> .c4d (in actual/)
133134 │
134135 └─ launches Cinema 4D GUI (child process)
135136 │ env: overlay above + two plugin dirs + python path
@@ -139,7 +140,7 @@ pytest (parent process)
139140 │ on C4DPL_PROGRAM_STARTED (mock mode):
140141 │ 1. patch socket.getaddrinfo (management.* → 127.0.0.1)
141142 │ 2. patch os.startfile → no-op (no Explorer popup)
142- │ 3. LoadDocument(cube .c4d)
143+ │ 3. LoadDocument(<case> .c4d)
143144 │ 4. CallCommand(SUBMITTER_PLUGIN_ID) ◀─ opens real submitter
144145 │
145146 └─ Qt submitter dialog appears ──── AWS_ENDPOINT_URL_DEADLINE ───▶ mock
@@ -190,17 +191,40 @@ test_cases/<name>/
190191└── actual/ # gitignored — runtime output; kept on failure
191192```
192193
193- Cases are ** registered explicitly** in the ` _CASES ` list in ` test_cinema4d.py `
194- — adding the folder is not enough, you add its name to the list. The expected
195- bundle works on every platform and is ** not** farm-specific (the mock provides
196- fake, stable farm/queue IDs and no queue environments), so the expected files
197- are portable and don't need per-farm regeneration.
194+ Cases are ** registered explicitly** in ` test_cinema4d.py ` , either in ` _CASES ` or
195+ in a dedicated parametrized test — adding the folder is not enough. Most cases
196+ use ` expected/{job_bundle,renders}/ ` ; a parametrized case can keep one scene and
197+ configurator while storing each variant under
198+ ` expected/<variant>/{job_bundle,renders}/ ` . The expected bundle works on every
199+ platform and is ** not** farm-specific (the mock provides fake, stable farm/queue
200+ IDs and no queue environments), so the expected files are portable and don't
201+ need per-farm regeneration.
198202
199203` input/scene.py ` runs inside ** c4dpy** (Cinema 4D's headless Python) and is saved
200- into ` actual/ ` (not ` input/ ` ), so the render path ` renders/$prj ` resolves into
201- ` actual/renders/ ` . The render comparison derives its directory from the bundle's
202- ` OutputPath ` param, so a ` configure.py ` that overrides the output path is
203- followed automatically.
204+ into ` actual/ ` (not ` input/ ` ), so render paths under ` renders/ ` resolve into
205+ ` actual/renders/ ` . Cases may override the output filename, but keep this
206+ directory stable for render comparison.
207+
208+ #### Focused settings coverage
209+
210+ Each settings case changes only the named setting group before exporting:
211+
212+ | Case | Covered controls |
213+ | ------| ------------------|
214+ | ` shared_job_settings ` | Job name, priority, maximum failed tasks, maximum retries |
215+ | ` job_specific_output_path ` | Override Output Path and path value |
216+ | ` job_specific_multi_pass_path ` | Override Multi-Pass Path and path value |
217+ | ` job_specific_take_selection ` | Current, Main, and All Takes modes and their generated steps |
218+ | ` physical_multi_takes ` | All Takes naming, truncation, and deduplication edge cases |
219+ | ` job_specific_frame_range ` | Override Frame Range and frame expression |
220+ | ` job_specific_detailed_logging ` | Detailed logging |
221+ | ` job_specific_timeouts ` | Task Run, Cinema 4D launch, and Cinema 4D shutdown timeouts |
222+ | ` job_specific_save_project_with_assets ` | Save project with assets |
223+ | ` job_specific_task_chunking ` | Frames per chunk and target chunk duration |
224+ | ` job_specific_tile_rendering ` | Tile rendering, columns, and rows |
225+
226+ The environment-gated ** Include Adaptor Wheels** developer option is excluded
227+ because it is not present in the customer-facing dialog.
204228
205229#### Finding selectors (harvesting locators for a ` configure.py ` )
206230
@@ -228,6 +252,29 @@ cross-platform widget behavior and selector gotchas are documented in
228252` submitter_ui.py ` . Prefer the reusable helpers exposed by ` submitter_ui.py ` over
229253raw ` descendant(...) ` calls.
230254
255+ #### Watching a run manually (observation delays)
256+
257+ Two env vars slow a run down so a human can follow it. Both default to ` 0 `
258+ (off) — never leave them set for normal runs:
259+
260+ - ` DIALOG_CONFIG_OBSERVE_DELAY_S=<seconds> ` — pause after ** every dialog
261+ interaction** (tab switch, checkbox, text field, each spin-button step).
262+ Implemented by ` _observe_pause() ` in ` deadline_test_fixtures.xa11y.controls ` ,
263+ so every ` submitter_ui.py ` helper inherits it.
264+ - ` ARTIFACT_REVIEW_DELAY_S=<seconds> ` — pause in ` _run_integ_case ` after all
265+ assertions pass but ** before ` actual/ ` is cleaned up** , so the exported
266+ bundle and renders can be inspected. The log prints the directory to look in.
267+
268+ ``` powershell
269+ # Windows PowerShell: 5s per interaction, 60s artifact review
270+ $env:DIALOG_CONFIG_OBSERVE_DELAY_S=5; $env:ARTIFACT_REVIEW_DELAY_S=60
271+ hatch -e integ-xa11y run python -m pytest --no-cov test/integ_xa11y/test_cinema4d.py --numprocesses=0 -s -k <case>
272+ $env:DIALOG_CONFIG_OBSERVE_DELAY_S=$null; $env:ARTIFACT_REVIEW_DELAY_S=$null
273+ ```
274+
275+ Related: ` MOCK_DEADLINE_RESPONSE_DELAY_S ` (default ` 0.3 ` ) sets the mock
276+ backend's per-response latency to approximate the real farm.
277+
231278#### Offline mock architecture
232279
233280The suite runs ** fully offline** — no real AWS, no login, no farm. The test hook
@@ -272,14 +319,14 @@ hatch run integ-xa11y:test # all xa11y integ tests
272319The ` integ-xa11y:test ` script hardcodes the ` test/integ_xa11y ` path and
273320` --numprocesses=1 ` . Beware: any args you pass * replace* the path (hatch
274321` {args:test/integ_xa11y} ` falls back to the global ` testpaths = ["test"] ` ), so
275- ` hatch run integ-xa11y:test -k cube ` would scan the whole ` test/ ` tree. To
322+ ` hatch run integ-xa11y:test -k physical ` would scan the whole ` test/ ` tree. To
276323filter or run in-process (e.g. to see C4D/xa11y stdout, which xdist hides), call
277324pytest directly with an explicit path — the test spawns its own subprocesses
278325regardless of ` --numprocesses ` :
279326
280327``` bash
281328hatch -e integ-xa11y run pytest --no-cov test/integ_xa11y/test_cinema4d.py \
282- --numprocesses=0 -s -k cube
329+ --numprocesses=0 -s -k physical
283330```
284331
285332#### Platform support matrix
@@ -315,7 +362,7 @@ exactly the three files (`template.yaml`, `parameter_values.yaml`,
315362#### Adding / changing a case
316363
317364** Plain case (no UI interaction):**
318- 1 . Create ` test_cases/<name>/input/scene.py ` (model it on ` cube ` 's).
365+ 1 . Create ` test_cases/<name>/input/scene.py ` (model it on ` physical ` 's).
3193662 . Add ` "<name>" ` to the ` _CASES ` list in ` test_cinema4d.py ` .
3203673 . Run it once (it fails — ` expected/ ` is empty), capture the golden (below), re-run.
321368
@@ -332,12 +379,15 @@ def configure(dialog):
332379```
333380
334381It runs after the dialog settles and before Export. See ` submitter_ui.py ` for
335- the helpers and gotchas, and ` cube ` 's ` input/configure.py ` for a worked example.
382+ the helpers and gotchas, and ` shared_job_settings ` 's ` input/configure.py ` for a worked example.
336383
337384** Capturing the golden bundle (manual):** after a run, the generated bundle is in
338385the case's ` actual/ ` . Copy the three files into ` expected/job_bundle/ ` , replacing
339386the absolute prefix up to (not including) ` deadline-cloud-for-cinema-4d ` with
340- ` PATH_TO_BE_REPLACED ` :
387+ ` PATH_TO_BE_REPLACED ` , and ** strip ` jobEnvironments ` from ` template.yaml ` ** —
388+ it embeds the detailed-logging scripts (hundreds of lines that change with
389+ unrelated code edits), and the comparison ignores it anyway
390+ (` ignored_template_keys ` ), so committing it is pure noise:
341391
342392``` bash
343393case=< name>
@@ -347,6 +397,15 @@ for f in template.yaml parameter_values.yaml asset_references.yaml; do
347397 test/integ_xa11y/test_cases/$case /actual/$f \
348398 > test/integ_xa11y/test_cases/$case /expected/job_bundle/$f
349399done
400+ python - << 'EOF '
401+ import io, os, yaml
402+ from deadline.client.job_bundle._yaml import deadline_yaml_dump
403+ p = f"test/integ_xa11y/test_cases/{os.environ['case']}/expected/job_bundle/template.yaml"
404+ data = yaml.safe_load(open(p, encoding="utf-8"))
405+ data.pop("jobEnvironments", None)
406+ buf = io.StringIO(); deadline_yaml_dump(data, buf)
407+ open(p, "w", encoding="utf-8", newline="\n").write(buf.getvalue())
408+ EOF
350409```
351410
352411The submitter sometimes emits ` parameter_values.yaml ` as single-line JSON; the
0 commit comments