1111
1212import pytest
1313import xa11y
14- from yaml import safe_load
14+ from yaml import safe_dump , safe_load
1515from deadline_test_fixtures .job_bundle import (
1616 JobBundleCase ,
1717 assert_valid_job_bundle ,
@@ -767,17 +767,50 @@ def test_job_specific_take_selection(
767767 )
768768
769769
770- # The directory holding the pre-GUI hook fixture (hooks.yaml + pregui_hook.py). Pointed at by
771- # DEADLINE_HOOKS_DIR when launching C4D for the hook case; gated by settings.allow_environment_hooks.
772- _PREGUI_HOOKS_DIR = Path (__file__ ).parent / "fixtures" / "pregui_hooks"
770+ # The committed pre-GUI hook script. Its hooks.yaml is NOT committed: it is generated per-run by
771+ # _materialize_pregui_hooks_dir so the hook's `command` can be this interpreter's absolute path
772+ # (see that helper for why a static `command` is not portable). DEADLINE_HOOKS_DIR points at the
773+ # generated dir; the run is gated by settings.allow_environment_hooks.
774+ _PREGUI_HOOK_SCRIPT = Path (__file__ ).parent / "fixtures" / "pregui_hooks" / "pregui_hook.py"
773775
774- # The values fixtures/pregui_hooks/ pregui_hook.py emits. Kept next to the test as the single source
775- # of truth for the assertions; must match that script.
776+ # The values pregui_hook.py emits. Kept next to the test as the single source of truth for the
777+ # assertions; must match that script.
776778_HOOK_JOB_NAME = "PREGUI RAN"
777779_HOOK_DESCRIPTION = "populated by pre-GUI hook"
778780_HOOK_PRIORITY = 88
779781
780782
783+ def _materialize_pregui_hooks_dir (dest : Path ) -> None :
784+ """Write a ``hooks.yaml`` under ``dest`` that runs the committed ``pregui_hook.py`` with the
785+ interpreter currently running the tests (``sys.executable``).
786+
787+ The manifest cannot be a static committed file with a portable ``command``. deadline-cloud
788+ resolves a hook ``command`` as an absolute path, then relative to the hooks dir, then via
789+ ``shutil.which`` on PATH (``deadline.client.job_bundle._hooks._executor``) — it does not expand
790+ environment variables. A bare ``python`` is not a reliable interpreter name: on macOS only
791+ ``python3`` exists (the system ``python`` went away with Python 2), and on every platform
792+ whether it resolves depends on the PATH Cinema 4D happened to inherit, not on this fixture. When
793+ it fails to resolve, the hook silently never runs and the failure surfaces as the value
794+ assertions below failing rather than a clear "hook could not be launched".
795+
796+ Writing ``sys.executable`` (an absolute path that exists on this machine — and ``pregui_hook.py``
797+ needs only the stdlib, so any interpreter works) makes the hook resolve on every platform.
798+ ``args`` references the committed script by absolute path, so only ``hooks.yaml`` lives here.
799+ """
800+ dest .mkdir (parents = True , exist_ok = True )
801+ manifest = {
802+ "version" : "1.0" ,
803+ "preGUI" : [
804+ {
805+ "command" : sys .executable ,
806+ "args" : [str (_PREGUI_HOOK_SCRIPT )],
807+ "timeout" : 60 ,
808+ }
809+ ],
810+ }
811+ (dest / "hooks.yaml" ).write_text (safe_dump (manifest , sort_keys = False ), encoding = "utf-8" )
812+
813+
781814def _bundle_parameter_values (actual_dir : Path ) -> dict :
782815 """Read the exported bundle's parameter_values.yaml into a {name: value} dict."""
783816 params_file = actual_dir / "parameter_values.yaml"
@@ -795,9 +828,10 @@ def test_pre_gui_hook(
795828 This is the GUI counterpart to the headless ``test_pre_gui_hooks`` unit tests: those check the
796829 ``apply_pre_gui_output`` mapping in isolation, whereas this drives the *real* submitter end to
797830 end and proves the wiring holds through an actual export. It launches Cinema 4D with
798- ``DEADLINE_HOOKS_DIR`` pointing at ``fixtures/pregui_hooks`` and ``allow_environment_hooks`` /
799- ``auto_accept`` enabled, so the shipped submitter runs the hook (``run_pre_gui_hooks``) before
800- building the dialog and applies its output (``apply_pre_gui_output``). The hook emits a fixed
831+ ``DEADLINE_HOOKS_DIR`` pointing at a per-run hooks dir (see ``_materialize_pregui_hooks_dir``)
832+ and ``allow_environment_hooks`` / ``auto_accept`` enabled, so the shipped submitter runs the
833+ hook (``run_pre_gui_hooks``) before building the dialog and applies its output
834+ (``apply_pre_gui_output``). A marker file proves the hook actually ran. The hook emits a fixed
801835 name, description, and ``deadline:priority``; the same Export path the render cases use then
802836 writes a job bundle, and we assert those three values survived into it:
803837
@@ -815,44 +849,67 @@ def test_pre_gui_hook(
815849
816850 scene_path = _build_cinema4d_scene (cinema4d_location , case_folder , actual_dir , case )
817851
818- # Enable the env-hook path in the config the fixture wrote, and point C4D at the hooks dir .
852+ # Enable the env-hook path in the config the fixture wrote.
819853 _enable_environment_hooks (deadline_farm ["env_overlay" ])
820854
821- _export_job_bundle_via_submitter (
822- cinema4d_location = cinema4d_location ,
823- scene_path = scene_path ,
824- job_bundle_generated = actual_dir ,
825- deadline_farm = deadline_farm ,
826- configure = None ,
827- extra_env = {"DEADLINE_HOOKS_DIR" : str (_PREGUI_HOOKS_DIR )},
828- )
855+ # Generate the hooks dir (so `command` is sys.executable, resolvable on every platform) and
856+ # give the hook a marker path so we can prove it actually ran. The dir is temporary and always
857+ # cleaned up; on failure `actual_dir` (below) is what stays behind for inspection.
858+ hooks_dir = Path (tempfile .mkdtemp (prefix = "c4d-pregui-hooks-" ))
859+ marker_path = hooks_dir / "hook_ran.marker"
860+ try :
861+ _materialize_pregui_hooks_dir (hooks_dir )
862+
863+ _export_job_bundle_via_submitter (
864+ cinema4d_location = cinema4d_location ,
865+ scene_path = scene_path ,
866+ job_bundle_generated = actual_dir ,
867+ deadline_farm = deadline_farm ,
868+ configure = None ,
869+ extra_env = {
870+ "DEADLINE_HOOKS_DIR" : str (hooks_dir ),
871+ "DEADLINE_CLOUD_PREGUI_MARKER" : str (marker_path ),
872+ },
873+ )
829874
830- # The submitter reached the mock, not real AWS. The hook itself is a local subprocess that runs
831- # before any AWS call, so no new mock routes are needed; still confirm nothing hit an unmocked
832- # route (a hook misfire that changed the submit path would surface here).
833- backend = deadline_farm ["backend" ]
834- log (f"mock backend call_counts: { dict (backend .call_counts )} " )
835- assert (
836- backend .unmatched_requests == []
837- ), f"submitter hit routes the mock doesn't implement: { backend .unmatched_requests } "
875+ # The submitter reached the mock, not real AWS. The hook itself is a local subprocess that
876+ # runs before any AWS call, so no new mock routes are needed; still confirm nothing hit an
877+ # unmocked route (a hook misfire that changed the submit path would surface here).
878+ backend = deadline_farm ["backend" ]
879+ log (f"mock backend call_counts: { dict (backend .call_counts )} " )
880+ assert (
881+ backend .unmatched_requests == []
882+ ), f"submitter hit routes the mock doesn't implement: { backend .unmatched_requests } "
883+
884+ # Prove the hook subprocess ran at all before trusting the exported values. This separates
885+ # "the hook never launched" (discovery / interpreter resolution failure) from "the hook ran
886+ # but its output wasn't wired into the bundle" — the value assertions below can't tell those
887+ # two apart on their own.
888+ assert marker_path .is_file (), (
889+ f"pre-GUI hook never ran: no marker at { marker_path } (DEADLINE_HOOKS_DIR={ hooks_dir } ). "
890+ "The submitter didn't execute the hook subprocess — look at hook discovery / "
891+ "interpreter resolution, not the output-mapping wiring."
892+ )
838893
839- # The exported bundle must carry the hook's output.
840- assert_valid_job_bundle (actual_dir / "template.yaml" )
841- template = safe_load ((actual_dir / "template.yaml" ).read_text (encoding = "utf-8" ))
842- params = _bundle_parameter_values (actual_dir )
894+ # The exported bundle must carry the hook's output.
895+ assert_valid_job_bundle (actual_dir / "template.yaml" )
896+ template = safe_load ((actual_dir / "template.yaml" ).read_text (encoding = "utf-8" ))
897+ params = _bundle_parameter_values (actual_dir )
843898
844- assert template .get ("name" ) == _HOOK_JOB_NAME , (
845- f"pre-GUI hook name did not reach the bundle: template name is "
846- f"{ template .get ('name' )!r} , expected { _HOOK_JOB_NAME !r} "
847- )
848- assert template .get ("description" ) == _HOOK_DESCRIPTION , (
849- f"pre-GUI hook description did not reach the bundle: template description is "
850- f"{ template .get ('description' )!r} , expected { _HOOK_DESCRIPTION !r} "
851- )
852- assert params .get ("deadline:priority" ) == _HOOK_PRIORITY , (
853- f"pre-GUI hook priority did not reach the bundle: parameter_values has "
854- f"{ params .get ('deadline:priority' )!r} , expected { _HOOK_PRIORITY } "
855- )
899+ assert template .get ("name" ) == _HOOK_JOB_NAME , (
900+ f"pre-GUI hook ran but its name did not reach the bundle: template name is "
901+ f"{ template .get ('name' )!r} , expected { _HOOK_JOB_NAME !r} "
902+ )
903+ assert template .get ("description" ) == _HOOK_DESCRIPTION , (
904+ f"pre-GUI hook ran but its description did not reach the bundle: template description is "
905+ f"{ template .get ('description' )!r} , expected { _HOOK_DESCRIPTION !r} "
906+ )
907+ assert params .get ("deadline:priority" ) == _HOOK_PRIORITY , (
908+ f"pre-GUI hook ran but its priority did not reach the bundle: parameter_values has "
909+ f"{ params .get ('deadline:priority' )!r} , expected { _HOOK_PRIORITY } "
910+ )
856911
857- # Clean up if the test was successful
858- rmtree (actual_dir , ignore_errors = True )
912+ # Clean up if the test was successful
913+ rmtree (actual_dir , ignore_errors = True )
914+ finally :
915+ rmtree (hooks_dir , ignore_errors = True )
0 commit comments