Skip to content

Commit 5b6db61

Browse files
test: Use SaveBundle instead of ExportBundle button for integ tests. (#538)
Signed-off-by: Karthik Bekal Pattathana <133984042+karthikbekalp@users.noreply.github.com>
1 parent 77796d9 commit 5b6db61

4 files changed

Lines changed: 43 additions & 87 deletions

File tree

requirements-testing.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ numpy >= 2.2.6, < 2.5; python_version < "3.12"
1313
pillow == 12.*
1414
openjd-cli == 0.7.*
1515
fonttools >= 4.0.0
16-
deadline-cloud-test-fixtures >= 0.18.18
16+
deadline-cloud-test-fixtures >= 0.18.20
1717
# Drives the real submitter dialog via the OS accessibility tree.
1818
# Used by test/integ/.
1919
xa11y >= 0.13.0

test/AGENTS.md

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ When adding new features or fixing bugs, always add unit tests.
4343
Drives the **real Deadline Cloud submitter dialog** with
4444
[xa11y](https://xa11y.dev). It launches the real Cinema 4D GUI with
4545
the real, unmodified plugin, opens the submitter as a user does, drives it via
46-
the OS accessibility tree, clicks **Export bundle**, and runs the same bundle/
46+
the OS accessibility tree, clicks **Save bundle as**, and runs the same bundle/
4747
render assertions as the previous suite.
4848

4949
This file is the single source of truth for the suite — there is no separate
@@ -79,7 +79,7 @@ pytest (parent process)
7979
│ xa11y drives it via the OS accessibility tree
8080
│ - wait for dialog, wait for queue-env loading
8181
│ - run the case's configure(dialog) (optional)
82-
│ - press "Export bundle"
82+
│ - press "Save bundle as"
8383
8484
bundle written to <job_history_dir>/<YYYY-mm>/<bundle-name>/
8585
@@ -115,7 +115,7 @@ test_cases/<name>/
115115
├── input/ # what you author
116116
│ ├── scene.py # required — builds <name>.c4d (runs in c4dpy)
117117
│ └── configure.py # OPTIONAL — configure(dialog) drives the dialog
118-
│ # before Export (runs in pytest + xa11y)
118+
│ # before clicking "Save bundle as" (pytest + xa11y)
119119
├── expected/ # what we compare against
120120
│ ├── job_bundle/ # golden template/parameter_values/asset_references
121121
│ └── renders/ # version-specific golden PNGs (Windows-only compare)
@@ -192,7 +192,7 @@ A `configure.py` drives widgets by their accessibility **role + name** (e.g.
192192
guess these names — and they differ between macOS (AX) and Windows (UIA), so a
193193
configurator must be verified on both. Harvest the live names with the built-in
194194
dump mode, which prints both settings tabs' accessibility trees and then stops
195-
(no Export):
195+
(without saving a bundle):
196196

197197
```bash
198198
# macOS
@@ -267,7 +267,8 @@ production.
267267
the bundle folder on Windows (it would linger/pile up across runs).
268268
- **Empty queue environments**: the mock returns an empty `ListQueueEnvironments`,
269269
so there are no Conda parameter widgets for `OpenJDParametersWidget.rebuild_ui`
270-
to recreate mid-Export and thus no reload race. Consequence: the exported
270+
to recreate while the bundle is being saved and thus no reload race.
271+
Consequence: the exported
271272
bundle carries no `CondaPackages` / `CondaChannels`, and the expected bundles
272273
omit them too.
273274

@@ -334,7 +335,7 @@ exactly the three files (`template.yaml`, `parameter_values.yaml`,
334335
generated files in `actual/`.
335336
4. Capture the golden bundle, then rerun the focused case.
336337

337-
**Configured case (drive the dialog before Export):** same, plus an
338+
**Configured case (drive the dialog before clicking Save bundle as):** same, plus an
338339
`input/configure.py` with a top-level `configure(dialog)` using the
339340
`submitter_ui` page-object:
340341

@@ -346,7 +347,7 @@ def configure(dialog):
346347
ui.set_detailed_logging(dialog, True)
347348
```
348349

349-
It runs after the dialog settles and before Export. Reuse helpers from
350+
It runs after the dialog settles and before clicking **Save bundle as**. Reuse helpers from
350351
`submitter_ui.py`; add a C4D-specific helper there, plus a focused unit test in
351352
`test_submitter_ui.py`, when no helper exists. Do not guess accessibility
352353
selectors. Use `DIALOG_DUMP=1`, then verify new selectors on Windows and macOS.
@@ -385,7 +386,7 @@ hook would silently not run. The test enables `settings.allow_environment_hooks`
385386
(so the submitter sources `DEADLINE_HOOKS_DIR`) and `settings.auto_accept` (so
386387
hooks run without the Qt confirmation prompt) in the config the `deadline_farm`
387388
fixture wrote, points `DEADLINE_HOOKS_DIR` at that generated dir via the launch
388-
env, Exports, then asserts a marker file proves the hook actually ran before
389+
env, clicks **Save bundle as**, then asserts a marker file proves the hook actually ran before
389390
asserting the emitted `name`/`description` reached `template.yaml` and
390391
`deadline:priority` reached `parameter_values.yaml` (the marker separates "hook
391392
never launched" from "hook ran but output wasn't wired in"). To change what the

test/integ/conftest.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,12 +131,14 @@ def deadline_farm(tmp_path):
131131
config_path = tmp_path / "deadline.config"
132132
home_dir = tmp_path / "home"
133133
job_history_dir = tmp_path / "job_history"
134+
job_history_dir.mkdir()
134135

135136
write_deadline_config(
136137
config_path,
137138
farm_id=backend.farm_id,
138139
queue_id=backend.queue_id,
139140
job_history_dir=job_history_dir,
141+
job_bundle_default_directory=job_history_dir,
140142
)
141143
env_overlay = build_mock_environment(
142144
dict(os.environ),

test/integ/test_cinema4d.py

Lines changed: 31 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
)
1919
from deadline_test_fixtures.xa11y import (
2020
SharedSubmitterDialog,
21+
dismiss_bundle_saved_popup,
2122
find_accessibility_app,
2223
)
2324
from yaml import safe_dump, safe_load
@@ -45,16 +46,15 @@
4546
# Selector strings pinned by inspecting the live UIA tree at runtime
4647
# (see commit history for tree dumps). UIA on Windows surfaces the Qt
4748
# QApplication display name as the dialog's accessible name — *not*
48-
# Qt's windowTitle. The Submit/Export-bundle buttons keep their visible
49+
# Qt's windowTitle. The Submit/Save-bundle-as buttons keep their visible
4950
# labels as accessible names.
5051
#
51-
# Export button: deadline-cloud/src/deadline/client/ui/dialogs/
52+
# Save button: deadline-cloud/src/deadline/client/ui/dialogs/
5253
# submit_job_to_deadline_dialog.py:250
5354
# Both the UIA App hosting the dialog and the dialog window itself surface this
5455
# same name (the QApplication display name + version), so it serves as the
5556
# prefix for matching either one.
5657
_DIALOG_NAME_PREFIX = "Deadline Cloud Cinema4D Submitter"
57-
_EXPORT_BUTTON_NAME = "Export bundle"
5858

5959
_C4D_BOOT_TIMEOUT_S = 180.0
6060
_DIALOG_VISIBLE_TIMEOUT_S = 60.0
@@ -67,7 +67,7 @@
6767
}
6868

6969
# A per-scene hook to drive the submitter dialog (switch tabs, set parameters,
70-
# toggle options) after it has loaded but before Export bundle is pressed. It
70+
# toggle options) after it has loaded but before Save bundle as is pressed. It
7171
# receives the dialog locator; use dialog.descendant("role[name='...']") to
7272
# reach widgets and .set_value()/.press()/.toggle()/.select() to interact. A
7373
# scene with no configurator exports with the dialog's default settings.
@@ -348,11 +348,11 @@ def _wait_for_submitter_dialog(dialog_app):
348348

349349
def _wait_for_queue_environment_loading(dialog_app) -> None:
350350
"""Wait for the queue-environment loading caption to clear. Non-fatal if it
351-
times out — Export bundle may still be clickable.
351+
times out — Save bundle as may still be clickable.
352352
353353
The mock returns no queue environments, so the caption ("Loading Queue
354354
Environments...") should appear briefly and clear almost immediately; we
355-
still wait for it to confirm the dialog has settled before pressing Export.
355+
still wait for it to confirm the dialog has settled before saving.
356356
"""
357357
log("waiting for queue environment loading to finish")
358358
loading = dialog_app.locator(
@@ -367,18 +367,15 @@ def _wait_for_queue_environment_loading(dialog_app) -> None:
367367
log(f"loading-text wait failed (non-fatal): {e!r}")
368368

369369

370-
def _press_export_bundle(dialog, dialog_app) -> None:
371-
"""Wait for the Export bundle button to be visible and enabled, then
372-
press it."""
373-
log(f"waiting for button: {_EXPORT_BUTTON_NAME!r}")
374-
export_btn = SharedSubmitterDialog(dialog).button(_EXPORT_BUTTON_NAME)
375-
export_btn.wait_visible(timeout=_DIALOG_VISIBLE_TIMEOUT_S)
376-
export_btn.wait_enabled(timeout=_DIALOG_VISIBLE_TIMEOUT_S)
377-
log("pressing Export bundle")
370+
def _save_bundle_locally(dialog, dialog_app) -> None:
371+
"""Open Save bundle as, choose Local, and confirm the modal save."""
372+
log("saving bundle locally through Save bundle as")
378373
try:
379-
export_btn.press()
374+
SharedSubmitterDialog(dialog, app_root=dialog_app).save_bundle_locally(
375+
timeout=_DIALOG_VISIBLE_TIMEOUT_S
376+
)
380377
except Exception:
381-
log("Export bundle press failed; final dialog tree:")
378+
log("Save bundle as flow failed; final dialog tree:")
382379
try:
383380
print(dialog_app.dump())
384381
except Exception as e: # noqa: BLE001 - preserve the original export error
@@ -388,50 +385,6 @@ def _press_export_bundle(dialog, dialog_app) -> None:
388385
raise
389386

390387

391-
def _dismiss_success_popup(pid: int) -> None:
392-
"""Dismiss the "Saved the submission as a job bundle" popup promptly.
393-
394-
The popup is deadline-cloud's success QMessageBox. Two things make it
395-
awkward to match (both confirmed by dumping the live AX tree):
396-
397-
* It is hosted by the **Cinema 4D** app, NOT the separate submitter-dialog
398-
app, so searching ``dialog_app`` never finds it.
399-
* Its title ("Cinema4D job submission") is not exposed as the dialog's AX
400-
name on macOS -- the role is ``dialog`` with an empty name. The reliable
401-
anchor is the message body static_text, which starts with "Saved the
402-
submission as a job bundle".
403-
404-
So we poll the accessibility roots belonging to the launched Cinema 4D
405-
process for a dialog/window/sheet containing that body text and press its
406-
OK button. Restricting by PID avoids querying unrelated applications whose
407-
accessibility providers may be unresponsive. Best-effort and non-fatal:
408-
the bundle is already on disk and asserted separately.
409-
"""
410-
log("dismissing success popup ('Saved the submission as a job bundle')")
411-
deadline_t = time.monotonic() + 5.0
412-
while time.monotonic() < deadline_t:
413-
try:
414-
for app in xa11y.App.list():
415-
if app.pid != pid:
416-
continue
417-
ok = app.locator(
418-
"dialog button[name='OK'], "
419-
"window button[name='OK'], "
420-
"sheet button[name='OK']"
421-
)
422-
# Only treat it as our popup if the success body text is present
423-
# in the same app, so we don't press an unrelated OK button.
424-
body = app.locator("static_text[name^='Saved the submission as a job bundle']")
425-
if body.exists() and ok.exists():
426-
ok.press()
427-
log("success popup dismissed (OK)")
428-
return
429-
except Exception as e: # noqa: BLE001 - polling retries transient accessibility errors
430-
log(f"success-popup scan raised (retrying): {e!r}")
431-
time.sleep(0.25)
432-
log("success popup not found within 5s (non-fatal, bundle already exported)")
433-
434-
435388
def _dump_settings_tabs(dialog) -> None:
436389
"""Dump each settings tab's accessibility tree (DIALOG_DUMP=1 diagnostic).
437390
@@ -472,39 +425,40 @@ def _drive_submitter_ui(
472425
Waits for the dialog, lets queue-environment loading settle (the mock
473426
returns no queue environments, so there are no Conda parameter widgets to
474427
rebuild and thus no reload race), runs the optional per-scene `configure`
475-
hook to adjust the dialog (tabs, parameters), presses Export bundle,
428+
hook to adjust the dialog (tabs, parameters), saves the bundle locally,
476429
dismisses the success popup, then reads the completed bundle from
477430
`history_dir`.
478431
479-
`configure` runs after the dialog settles and before Export. When it is None
480-
the dialog is exported with its default settings.
432+
`configure` runs after the dialog settles and before the save flow. When it
433+
is None the dialog is exported with its default settings.
481434
"""
482435
dialog_app = _resolve_dialog_app(proc)
483436
dialog = _wait_for_submitter_dialog(dialog_app)
484437
_wait_for_queue_environment_loading(dialog_app)
485438
# Diagnostic harvest: DIALOG_DUMP=1 dumps each settings tab's tree and raises,
486439
# so you can capture the live accessibility names (which differ across macOS
487-
# AX and Windows UIA) without hand-editing a configurator. Skips Export.
440+
# AX and Windows UIA) without hand-editing a configurator. Skips saving.
488441
if os.environ.get("DIALOG_DUMP") == "1":
489442
_dump_settings_tabs(dialog)
490-
raise AssertionError("DIALOG_DUMP=1: dumped settings tabs, skipping export")
443+
raise AssertionError("DIALOG_DUMP=1: dumped settings tabs, skipping save")
491444
if configure is not None:
492445
log("running per-scene dialog configurator")
493446
configure(dialog)
494-
_press_export_bundle(dialog, dialog_app)
447+
_save_bundle_locally(dialog, dialog_app)
495448

496-
# The submitter writes the bundle files and only then shows the success
497-
# popup (on_export_bundle in submit_job_to_deadline_dialog.py), so once the
498-
# popup is up the bundle is complete on disk and we can read it directly.
499-
_dismiss_success_popup(proc.pid)
449+
# The submitter writes the bundle files before showing the success popup,
450+
# so once the popup is up the bundle is complete on disk.
451+
log("dismissing success popup ('Bundle saved to:')")
452+
if dismiss_bundle_saved_popup(proc.pid):
453+
log("success popup dismissed (OK)")
454+
else:
455+
log("success popup not found within 5s (non-fatal, bundle already exported)")
500456
# Note: on Windows the submitter would normally open the bundle folder in
501457
# File Explorer (os.startfile); the sidecar plugin suppresses that in mock
502458
# mode, so there's no Explorer window to clean up here.
503459

504460
staged_bundle = find_complete_job_bundle(history_dir)
505-
assert (
506-
staged_bundle is not None
507-
), f"success popup shown but no complete bundle found under {history_dir}"
461+
assert staged_bundle is not None, f"no complete bundle found under {history_dir}"
508462
log(f"bundle found: {staged_bundle}")
509463
return staged_bundle
510464

@@ -524,11 +478,10 @@ def _export_job_bundle_via_submitter(
524478
``_build_launch_env``); the pre-GUI hook case uses it to set
525479
``DEADLINE_HOOKS_DIR``.
526480
527-
The submitter exports via create_job_history_bundle_dir, which always
528-
writes under <history_dir>/<YYYY-mm>/<bundle-name>/. The history dir is set
529-
in the temp deadline config the `deadline_farm` fixture wrote (read inside
530-
the C4D subprocess), so the bundle lands under that dir; we then copy its
531-
files flat into `job_bundle_generated` for validation.
481+
The temp Deadline config uses ``job_history_dir`` as the default local
482+
bundle directory. The test selects Local in the Save bundle as dialog, so
483+
the bundle lands under that directory; we then copy its files flat into
484+
`job_bundle_generated` for validation.
532485
533486
Owns all cleanup: the C4D subprocess is always killed and its diagnostic
534487
log echoed before the staging dir is removed, even on failure.

0 commit comments

Comments
 (0)