unitytk 0.0.15__tar.gz → 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. {unitytk-0.0.15 → unitytk-0.1.0}/MANIFEST.in +1 -1
  2. {unitytk-0.0.15/unitytk.egg-info → unitytk-0.1.0}/PKG-INFO +2 -2
  3. {unitytk-0.0.15 → unitytk-0.1.0}/pyproject.toml +10 -7
  4. {unitytk-0.0.15 → unitytk-0.1.0}/test/test_articulated_rig_runtime.py +89 -0
  5. {unitytk-0.0.15 → unitytk-0.1.0}/test/test_audio_events_integration.py +11 -16
  6. {unitytk-0.0.15 → unitytk-0.1.0}/test/test_base_test.py +85 -0
  7. {unitytk-0.0.15 → unitytk-0.1.0}/test/test_clip_name_contract_integration.py +10 -15
  8. {unitytk-0.0.15 → unitytk-0.1.0}/test/test_controller_runtime_integration.py +1 -16
  9. unitytk-0.1.0/test/test_deploy_templates.py +1141 -0
  10. {unitytk-0.0.15 → unitytk-0.1.0}/test/test_emissive_groups_integration.py +5 -16
  11. {unitytk-0.0.15 → unitytk-0.1.0}/test/test_fade_window_preservation.py +12 -6
  12. {unitytk-0.0.15 → unitytk-0.1.0}/test/test_keyed_visibility.py +17 -15
  13. unitytk-0.1.0/test/test_lightmap_metadata_integration.py +1081 -0
  14. {unitytk-0.0.15 → unitytk-0.1.0}/test/test_multi_section_audio_integration.py +16 -9
  15. {unitytk-0.0.15 → unitytk-0.1.0}/test/test_render_effects_e2e.py +87 -28
  16. {unitytk-0.0.15 → unitytk-0.1.0}/test/test_render_opacity_controller.py +12 -13
  17. {unitytk-0.0.15 → unitytk-0.1.0}/test/test_render_opacity_e2e.py +22 -22
  18. {unitytk-0.0.15 → unitytk-0.1.0}/test/test_render_opacity_integration.py +14 -14
  19. {unitytk-0.0.15 → unitytk-0.1.0}/test/test_shadow_plane_integration.py +5 -2
  20. {unitytk-0.0.15 → unitytk-0.1.0}/test/test_shadow_plane_runtime_integration.py +108 -2
  21. {unitytk-0.0.15 → unitytk-0.1.0}/test/test_shared_material_standalone.py +10 -13
  22. {unitytk-0.0.15 → unitytk-0.1.0}/test/test_shot_metadata_integration.py +345 -45
  23. {unitytk-0.0.15 → unitytk-0.1.0}/test/test_shots_audio_sidebyside_integration.py +12 -6
  24. {unitytk-0.0.15 → unitytk-0.1.0}/test/test_standalone_enduser.py +14 -28
  25. {unitytk-0.0.15 → unitytk-0.1.0}/unitytk/__init__.py +1 -1
  26. {unitytk-0.0.15 → unitytk-0.1.0}/unitytk/template_deployer.py +413 -135
  27. {unitytk-0.0.15/unitytk/templates → unitytk-0.1.0/unitytk/templates/ArticulatedRig}/ArticulatedRigController.cs +143 -83
  28. unitytk-0.1.0/unitytk/templates/ArticulatedRig/ArticulatedRigController.cs.meta +11 -0
  29. unitytk-0.1.0/unitytk/templates/ArticulatedRig/ArticulatedRigPointerGrab.cs.meta +11 -0
  30. unitytk-0.1.0/unitytk/templates/ArticulatedRig/ArticulationRecord.g.cs +199 -0
  31. unitytk-0.1.0/unitytk/templates/ArticulatedRig/ArticulationRecord.g.cs.meta +11 -0
  32. unitytk-0.1.0/unitytk/templates/ArticulatedRig/XR/ArticulatedRigXRGrab.cs.meta +11 -0
  33. unitytk-0.1.0/unitytk/templates/ArticulatedRig/XR/ArticulatedRigXRSetup.cs.meta +11 -0
  34. unitytk-0.1.0/unitytk/templates/ArticulatedRig/XR/M3trik.Unitytk.XR.asmdef.meta +7 -0
  35. unitytk-0.1.0/unitytk/templates/ArticulatedRig/XR.meta +8 -0
  36. unitytk-0.1.0/unitytk/templates/ArticulatedRig.meta +8 -0
  37. unitytk-0.1.0/unitytk/templates/AudioEvent/AudioEventController.cs.meta +11 -0
  38. unitytk-0.1.0/unitytk/templates/AudioEvent.meta +8 -0
  39. unitytk-0.1.0/unitytk/templates/EmissiveGroup/EmissiveGroupController.cs.meta +11 -0
  40. unitytk-0.1.0/unitytk/templates/EmissiveGroup/EmissiveGroups.hlsl.meta +7 -0
  41. unitytk-0.1.0/unitytk/templates/EmissiveGroup/EmissiveGroupsLit.shader.meta +9 -0
  42. unitytk-0.1.0/unitytk/templates/EmissiveGroup.meta +8 -0
  43. unitytk-0.1.0/unitytk/templates/LightmapMetadata/LightmapMetadataController.cs +1127 -0
  44. unitytk-0.1.0/unitytk/templates/LightmapMetadata/LightmapMetadataController.cs.meta +11 -0
  45. unitytk-0.1.0/unitytk/templates/LightmapMetadata.meta +8 -0
  46. unitytk-0.1.0/unitytk/templates/M3trik.Unitytk.asmdef.meta +7 -0
  47. {unitytk-0.0.15 → unitytk-0.1.0}/unitytk/templates/README.md +95 -26
  48. unitytk-0.1.0/unitytk/templates/README.md.meta +7 -0
  49. {unitytk-0.0.15/unitytk/templates → unitytk-0.1.0/unitytk/templates/RenderEffects}/RenderEffectsController.cs +52 -20
  50. unitytk-0.1.0/unitytk/templates/RenderEffects/RenderEffectsController.cs.meta +11 -0
  51. unitytk-0.1.0/unitytk/templates/RenderEffects.meta +8 -0
  52. unitytk-0.1.0/unitytk/templates/ShadowPlane/ShadowPlane.shader.meta +9 -0
  53. unitytk-0.1.0/unitytk/templates/ShadowPlane/ShadowPlaneController.cs.meta +11 -0
  54. unitytk-0.1.0/unitytk/templates/ShadowPlane/ShadowPlaneHorizon.hlsl.meta +7 -0
  55. unitytk-0.1.0/unitytk/templates/ShadowPlane.meta +8 -0
  56. {unitytk-0.0.15/unitytk/templates → unitytk-0.1.0/unitytk/templates/ShotMetadata}/ShotMetadataController.cs +135 -3
  57. unitytk-0.1.0/unitytk/templates/ShotMetadata/ShotMetadataController.cs.meta +11 -0
  58. unitytk-0.1.0/unitytk/templates/ShotMetadata.meta +8 -0
  59. unitytk-0.1.0/unitytk/templates/UnitytkSettings.cs.meta +11 -0
  60. {unitytk-0.0.15 → unitytk-0.1.0/unitytk.egg-info}/PKG-INFO +2 -2
  61. unitytk-0.1.0/unitytk.egg-info/SOURCES.txt +88 -0
  62. unitytk-0.1.0/unitytk.egg-info/requires.txt +1 -0
  63. unitytk-0.0.15/test/test_deploy_templates.py +0 -620
  64. unitytk-0.0.15/test/test_lightmap_metadata_integration.py +0 -588
  65. unitytk-0.0.15/unitytk/templates/LightmapMetadataController.cs +0 -551
  66. unitytk-0.0.15/unitytk.egg-info/SOURCES.txt +0 -60
  67. unitytk-0.0.15/unitytk.egg-info/requires.txt +0 -1
  68. {unitytk-0.0.15 → unitytk-0.1.0}/LICENSE +0 -0
  69. {unitytk-0.0.15 → unitytk-0.1.0}/docs/README.md +0 -0
  70. {unitytk-0.0.15 → unitytk-0.1.0}/setup.cfg +0 -0
  71. {unitytk-0.0.15 → unitytk-0.1.0}/test/test_asset_delivery.py +0 -0
  72. {unitytk-0.0.15 → unitytk-0.1.0}/test/test_bool_to_fade_curve.py +0 -0
  73. {unitytk-0.0.15 → unitytk-0.1.0}/test/test_launcher.py +0 -0
  74. {unitytk-0.0.15 → unitytk-0.1.0}/test/test_run_tests.py +0 -0
  75. {unitytk-0.0.15 → unitytk-0.1.0}/test/test_scene_builder.py +0 -0
  76. {unitytk-0.0.15 → unitytk-0.1.0}/test/test_temp_projects.py +0 -0
  77. {unitytk-0.0.15 → unitytk-0.1.0}/unitytk/asset_delivery.py +0 -0
  78. {unitytk-0.0.15 → unitytk-0.1.0}/unitytk/launcher.py +0 -0
  79. {unitytk-0.0.15 → unitytk-0.1.0}/unitytk/scene_builder.py +0 -0
  80. {unitytk-0.0.15/unitytk/templates → unitytk-0.1.0/unitytk/templates/ArticulatedRig}/ArticulatedRigPointerGrab.cs +0 -0
  81. {unitytk-0.0.15/unitytk/templates/ArticulatedRigXR → unitytk-0.1.0/unitytk/templates/ArticulatedRig/XR}/ArticulatedRigXRGrab.cs +0 -0
  82. {unitytk-0.0.15/unitytk/templates/ArticulatedRigXR → unitytk-0.1.0/unitytk/templates/ArticulatedRig/XR}/ArticulatedRigXRSetup.cs +0 -0
  83. {unitytk-0.0.15/unitytk/templates/ArticulatedRigXR → unitytk-0.1.0/unitytk/templates/ArticulatedRig/XR}/M3trik.Unitytk.XR.asmdef +0 -0
  84. {unitytk-0.0.15/unitytk/templates → unitytk-0.1.0/unitytk/templates/AudioEvent}/AudioEventController.cs +0 -0
  85. {unitytk-0.0.15/unitytk/templates → unitytk-0.1.0/unitytk/templates/EmissiveGroup}/EmissiveGroupController.cs +0 -0
  86. {unitytk-0.0.15/unitytk/templates → unitytk-0.1.0/unitytk/templates/EmissiveGroup}/EmissiveGroups.hlsl +0 -0
  87. {unitytk-0.0.15/unitytk/templates → unitytk-0.1.0/unitytk/templates/EmissiveGroup}/EmissiveGroupsLit.shader +0 -0
  88. {unitytk-0.0.15 → unitytk-0.1.0}/unitytk/templates/M3trik.Unitytk.asmdef +0 -0
  89. {unitytk-0.0.15/unitytk/templates → unitytk-0.1.0/unitytk/templates/ShadowPlane}/ShadowPlane.shader +0 -0
  90. {unitytk-0.0.15/unitytk/templates → unitytk-0.1.0/unitytk/templates/ShadowPlane}/ShadowPlaneController.cs +0 -0
  91. {unitytk-0.0.15/unitytk/templates → unitytk-0.1.0/unitytk/templates/ShadowPlane}/ShadowPlaneHorizon.hlsl +0 -0
  92. {unitytk-0.0.15 → unitytk-0.1.0}/unitytk/templates/UnitytkSettings.cs +0 -0
  93. {unitytk-0.0.15 → unitytk-0.1.0}/unitytk/templates/__init__.py +0 -0
  94. {unitytk-0.0.15 → unitytk-0.1.0}/unitytk.egg-info/dependency_links.txt +0 -0
  95. {unitytk-0.0.15 → unitytk-0.1.0}/unitytk.egg-info/top_level.txt +0 -0
@@ -1,4 +1,4 @@
1
1
  include LICENSE
2
2
  include docs/README.md
3
3
  include pyproject.toml
4
- recursive-include unitytk *.ui *.gif *.png *.json *.txt *.cs *.hlsl *.shader *.asmdef
4
+ recursive-include unitytk *.ui *.gif *.png *.json *.txt *.cs *.hlsl *.shader *.asmdef *.meta
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: unitytk
3
- Version: 0.0.15
3
+ Version: 0.1.0
4
4
  Summary: A modular Python toolkit for Unity interaction.
5
5
  Author-email: Ryan Simpson <m3trik@outlook.com>
6
6
  License: MIT
@@ -13,7 +13,7 @@ Classifier: Operating System :: POSIX :: Linux
13
13
  Requires-Python: >=3.9
14
14
  Description-Content-Type: text/markdown
15
15
  License-File: LICENSE
16
- Requires-Dist: pythontk>=0.12.0
16
+ Requires-Dist: pythontk>=0.13.0
17
17
  Dynamic: license-file
18
18
 
19
19
  [![Tests](https://img.shields.io/badge/Tests-146%20passed-brightgreen.svg)](../test/)
@@ -17,12 +17,15 @@ classifiers = [
17
17
  ]
18
18
  # The floor is HAND-RATCHETED: unitytk is off-chain from m3trik/push.ps1, so no
19
19
  # release cascade bumps it.
20
- # pythontk 0.12.0, the release this one ships and was tested with. The hard
21
- # minimum is 0.8.84: UnityFinder.find_editors scans through
22
- # `AppLauncher.scan_install_dirs` (new in 0.8.72) and needs its pattern order to
23
- # be a priority (0.8.84), so the Hub's chosen install location wins over the
24
- # default. Below 0.8.72 the scan fails, is logged, and no editor is found.
25
- dependencies = ["pythontk>=0.12.0"]
20
+ # pythontk 0.13.0, the release this one ships and was tested with: the suite
21
+ # holds the templates to its `Conformance` cases and its articulation solve's
22
+ # halving step (both new in 0.13.0). The hard minimum is 0.11.3:
23
+ # TemplateDeployer builds its deprecation notices at import through
24
+ # `Deprecation.values(..., since=)` (new in 0.11.3). UnityFinder.find_editors
25
+ # needs 0.8.84: it scans through `AppLauncher.scan_install_dirs` (new in
26
+ # 0.8.72) and needs its pattern order to be a priority (0.8.84), so the Hub's
27
+ # chosen install location wins over the default.
28
+ dependencies = ["pythontk>=0.13.0"]
26
29
 
27
30
  [project.urls]
28
31
  Homepage = "https://github.com/m3trik/unitytk"
@@ -37,7 +40,7 @@ where = ["."]
37
40
  include = ["unitytk*"]
38
41
 
39
42
  [tool.setuptools.package-data]
40
- "*" = ["*.ui", "*.json", "*.txt", "*.md", "*.cs", "*.hlsl", "*.shader", "*.asmdef"]
43
+ "*" = ["*.ui", "*.json", "*.txt", "*.md", "*.cs", "*.hlsl", "*.shader", "*.asmdef", "*.meta"]
41
44
 
42
45
  [tool.pytest.ini_options]
43
46
  testpaths = ["test"]
@@ -69,6 +69,7 @@ from base_test import EnvSkip, TempProjects, UnityEnv
69
69
 
70
70
  import pythontk as ptk
71
71
  from unitytk import UnityLauncher, UnityFinder, TemplateDeployer
72
+ from unitytk.template_deployer import UPM_PACKAGE_NAME
72
73
 
73
74
 
74
75
  def _unity_available() -> bool:
@@ -433,9 +434,17 @@ class ControllerScenario:
433
434
  }
434
435
  )
435
436
  s_end = self.path_states()[-1]
437
+
438
+ def cut_short(**field):
439
+ """The record with one joint's ``t`` or ``q`` cut short."""
440
+ rig = json.loads(json.dumps(self.rig))
441
+ rig["joints"][1].update(field)
442
+ return json.dumps(dict(self.payload, rigs=[rig]))
443
+
436
444
  return {
437
445
  "record": json.dumps(self.payload),
438
446
  "record_newer": json.dumps(dict(self.payload, version=2)),
447
+ "records_short": [cut_short(t=[1.0]), cut_short(q=[0.0, 0.0])],
439
448
  "root": {
440
449
  "p": list(self.ROOT["p"]),
441
450
  "q": list(self.ROOT["q"]),
@@ -784,6 +793,7 @@ public class ArticulatedRigVerifier
784
793
  catch (Exception e) { Put("inputs_error", S(e.ToString())); }
785
794
  if (inputs != null)
786
795
  {
796
+ Run("guids", () => Guids(L(inputs["guids"])));
787
797
  Run("conformance", () => Conformance(O(inputs["doc"])));
788
798
  Run("invalid", () => Invalid(L(inputs["invalid"])));
789
799
  Run("boundary", () => Boundary(O(inputs["boundary"])));
@@ -806,6 +816,15 @@ public class ArticulatedRigVerifier
806
816
  catch (Exception e) { Put(section + "_error", S(e.ToString())); }
807
817
  }
808
818
 
819
+ // ---- 0. fixed GUIDs ---------------------------------------------------
820
+ // The editor's own answer, per deployed asset path (files and folders):
821
+ // the GUID committed in the bundle's .meta, unless Unity re-minted it.
822
+ static void Guids(IList<object> paths)
823
+ {
824
+ var rows = paths.Select(p => S((string)p) + ": " + S(AssetDatabase.AssetPathToGUID((string)p)));
825
+ Put("guids", "{" + string.Join(", ", rows) + "}");
826
+ }
827
+
809
828
  // ---- 1. conformance ---------------------------------------------------
810
829
  static void Conformance(IDictionary<string, object> doc)
811
830
  {
@@ -1113,6 +1132,20 @@ public class ArticulatedRigVerifier
1113
1132
  var futureCtrl = future.AddComponent<ArticulatedRigController>();
1114
1133
  futureCtrl.record = (string)c["record_newer"];
1115
1134
  Put("ctrl_newer", "{\"resolved\": " + B(futureCtrl.Resolve()) + ", \"rigs\": " + futureCtrl.RigCount + "}");
1135
+
1136
+ // A joint whose t or q is cut short is refused with a warning, never
1137
+ // thrown out of Resolve (the importer calls it: the import would fail).
1138
+ var cutRows = new List<string>();
1139
+ foreach (var raw in L(c["records_short"]))
1140
+ {
1141
+ var cut = new GameObject("CutShortModel");
1142
+ cut.transform.SetParent(root, false);
1143
+ var cutCtrl = cut.AddComponent<ArticulatedRigController>();
1144
+ cutCtrl.record = (string)raw;
1145
+ try { cutRows.Add("{\"resolved\": " + B(cutCtrl.Resolve()) + ", \"rigs\": " + cutCtrl.RigCount + "}"); }
1146
+ catch (Exception thrown) { cutRows.Add("{\"threw\": " + S(thrown.GetType().Name) + "}"); }
1147
+ }
1148
+ Put("ctrl_short", "[" + string.Join(",", cutRows) + "]");
1116
1149
  UnityEngine.Object.DestroyImmediate(root.gameObject);
1117
1150
  }
1118
1151
 
@@ -1407,6 +1440,22 @@ def _compile_errors(log_path: str) -> str:
1407
1440
  return "\n".join(errors[:20])
1408
1441
 
1409
1442
 
1443
+ #: The embedded package's root as an AssetDatabase path.
1444
+ _PACKAGE_ASSET_ROOT = f"Packages/{UPM_PACKAGE_NAME}"
1445
+
1446
+
1447
+ def _bundle_assets():
1448
+ """Every deployed bundle file and folder, as "/"-joined package paths."""
1449
+ names = TemplateDeployer._bundle_names()
1450
+ return list(names) + TemplateDeployer._folders(names)
1451
+
1452
+
1453
+ def _committed_meta(name):
1454
+ """Bundle asset *name*'s committed .meta, LF (as the deployer writes it)."""
1455
+ ref = TemplateDeployer._bundle_ref(name + ".meta")
1456
+ return ref.read_bytes().replace(b"\r\n", b"\n")
1457
+
1458
+
1410
1459
  def _run_unity(test, temp_dir, execute_method, prepare, timeout=600):
1411
1460
  """Create a project, let *prepare(project_path)* stage it, run the
1412
1461
  verifier; returns the parsed results (or skips on the license wall)."""
@@ -1552,6 +1601,7 @@ class TestArticulatedRigRuntime(unittest.TestCase):
1552
1601
  variants[key] = os.path.join(cls.temp_dir, f"fixture_{key}.fbx.txt")
1553
1602
  cls.fixture.fbx(**kwargs).write(variants[key])
1554
1603
  inputs = {
1604
+ "guids": [f"{_PACKAGE_ASSET_ROOT}/{n}" for n in _bundle_assets()],
1555
1605
  "doc": cls.doc,
1556
1606
  "invalid": _invalid_rigs(),
1557
1607
  "boundary": cls.boundary,
@@ -1586,6 +1636,34 @@ class TestArticulatedRigRuntime(unittest.TestCase):
1586
1636
  cls.unity = unity
1587
1637
  logger.info("Unity %s", unity.get("unity_version"))
1588
1638
 
1639
+ @EnvSkip.unless(_unity_available(), "no Unity editor found")
1640
+ def test_01b_the_editor_reads_the_committed_guids(self):
1641
+ """Fixed GUIDs, proven by the editor rather than by reading files:
1642
+ AssetDatabase names every deployed script, shader, asmdef, the README
1643
+ and every component folder (the XR sub-assembly's included) by the
1644
+ GUID committed in the bundle's .meta -- and the import left those
1645
+ .meta files byte-identical, so a project never churns them."""
1646
+ got = self._section("guids")
1647
+ expected = {
1648
+ f"{_PACKAGE_ASSET_ROOT}/{name}": re.search(
1649
+ rb"^guid: ([0-9a-f]{32})$", _committed_meta(name), re.M
1650
+ )
1651
+ .group(1)
1652
+ .decode()
1653
+ for name in _bundle_assets()
1654
+ }
1655
+ self.assertEqual(got, expected, "Unity named a deployed asset differently")
1656
+ package = TemplateDeployer.package_dir(
1657
+ os.path.join(self.temp_dir, "UnityProject")
1658
+ )
1659
+ rewritten = []
1660
+ for name in _bundle_assets():
1661
+ path = os.path.join(package, *name.split("/")) + ".meta"
1662
+ with open(path, "rb") as fh:
1663
+ if fh.read() != _committed_meta(name):
1664
+ rewritten.append(name)
1665
+ self.assertEqual(rewritten, [], "Unity rewrote these .meta files on import")
1666
+
1589
1667
  # ---- Step 2: conformance -------------------------------------------------
1590
1668
 
1591
1669
  def _conformance(self):
@@ -2082,6 +2160,17 @@ class TestArticulatedRigRuntime(unittest.TestCase):
2082
2160
  in): nothing binds."""
2083
2161
  self.assertEqual(self._section("ctrl_newer"), {"resolved": False, "rigs": 0})
2084
2162
 
2163
+ @EnvSkip.unless(_unity_available(), "no Unity editor found")
2164
+ def test_16c_controller_refuses_a_joint_cut_short(self):
2165
+ """A joint whose ``t`` holds fewer than 3 values, or ``q`` fewer than
2166
+ 4, is a rig the runtime refuses with a warning: nothing binds, and
2167
+ nothing is thrown out of ``Resolve``, which the importer calls. The
2168
+ reference has no such check (a short ``q`` is an IndexError there), so
2169
+ this is the port's own contract, not a ``_invalid_rigs`` case."""
2170
+ self.assertEqual(
2171
+ self._section("ctrl_short"), [{"resolved": False, "rigs": 0}] * 2
2172
+ )
2173
+
2085
2174
  @EnvSkip.unless(_unity_available(), "no Unity editor found")
2086
2175
  def test_17_xr_adapter_inert_without_xri(self):
2087
2176
  """Without the XR Interaction Toolkit the XR assembly is excluded by
@@ -66,8 +66,8 @@ maya.standalone.initialize(name="python")
66
66
  import maya.cmds as cmds
67
67
  import maya.mel as mel
68
68
 
69
- from mayatk.audio_utils._audio_utils import AudioUtils
70
- from mayatk.audio_utils.audio_clips._audio_clips import AudioClips
69
+ from mayatk import AudioUtils
70
+ from mayatk import AudioClips
71
71
 
72
72
  output_dir = sys.argv[1]
73
73
  fbx_path = os.path.join(output_dir, "audio_events_test.fbx")
@@ -113,7 +113,10 @@ if not cmds.pluginInfo("fbxmaya", query=True, loaded=True):
113
113
  cmds.loadPlugin("fbxmaya")
114
114
 
115
115
  mel.eval("FBXResetExport")
116
- mel.eval("FBXExportInAscii -v true")
116
+ # Binary, as every production writer ships: Unity's ASCII FBX reader
117
+ # drops the whole scene (or truncates the string) once a carrier channel
118
+ # passes ~6 KB, and the handoff record alone is past that.
119
+ mel.eval("FBXExportInAscii -v false")
117
120
  mel.eval(f'FBXExport -f "{fbx_path.replace(os.sep, "/")}"')
118
121
 
119
122
  assert os.path.exists(fbx_path), f"FBX was not created at {fbx_path}"
@@ -133,7 +136,10 @@ with open(os.path.join(output_dir, "maya_results.json"), "w") as f:
133
136
  print("\n" + "=" * 60)
134
137
  print("MAYA EXPORT COMPLETE")
135
138
  print("=" * 60)
136
- maya.standalone.uninitialize()
139
+ # Not maya.standalone.uninitialize(): Maya's static destructors fault at DLL
140
+ # detach, filing a crash dump and an untitled[Recovered].ma per run.
141
+ import pythontk as ptk
142
+ ptk.ProcessExit.hard_exit(0)
137
143
  '''
138
144
 
139
145
 
@@ -561,18 +567,7 @@ class TestAudioEventsIntegration(unittest.TestCase):
561
567
  else:
562
568
  logger.info("Unity log: no AudioEvent/Warning/Error lines.")
563
569
 
564
- if not os.path.exists(results_path):
565
- # A batch-licensing failure is environmental (not a code bug) — skip.
566
- if UnityLauncher.is_license_failure(returncode, full_log):
567
- logger.info(
568
- "Unity batch license failure (rc=%s) — see %s",
569
- returncode, log_path,
570
- )
571
- self.skipTest(UnityEnv.license_reason())
572
- self.fail(
573
- f"unity_results.json not created (Unity rc={returncode}) — "
574
- f"check {log_path}"
575
- )
570
+ UnityEnv.require_results(self, results_path, returncode, log_path)
576
571
  with open(results_path) as f:
577
572
  self.__class__.unity_results = json.load(f)
578
573
 
@@ -20,6 +20,9 @@ green without having verified anything:
20
20
  * :class:`base_test.UserPropsProbe` -- one C# user-properties postprocessor
21
21
  shared by the five FBX-import suites, with ONE carrier-matching rule for its
22
22
  C# and Python halves (they had drifted to ``==`` vs ``startswith``).
23
+ * :class:`base_test.TemplateSources` -- the suites that stage templates by hand
24
+ resolve them by bundle path in one place, so a moved template fails loudly
25
+ instead of one suite silently copying nothing.
23
26
 
24
27
  :class:`PublicHygieneTest` is the permanent guard for the 2026-08-17 rename:
25
28
  client program identifiers must not come back into tracked Python/C# source.
@@ -37,6 +40,7 @@ from base_test import (
37
40
  EnvSkip,
38
41
  MayaEnv,
39
42
  SuiteOptions,
43
+ TemplateSources,
40
44
  TestAssets,
41
45
  UnityEnv,
42
46
  UserPropsProbe,
@@ -334,6 +338,45 @@ class UnityEnvTest(unittest.TestCase):
334
338
  f"{offenders}",
335
339
  )
336
340
 
341
+ def test_require_results_skips_unlicensed_and_fails_a_clean_run(self):
342
+ """The check every batch verifier makes: results there, nothing to do;
343
+ none from an unlicensed editor, an environmental skip; none from a
344
+ clean run, a failure. Asserting the file alone failed every such suite
345
+ on a machine signed out of the Hub."""
346
+ import pythontk as ptk
347
+
348
+ store = ptk.TempArtifacts("unitytk_require_results", policy="scoped")
349
+ self.addCleanup(store.cleanup)
350
+ root = store.dir_path()
351
+ present = os.path.join(root, "results.json")
352
+ with open(present, "w", encoding="utf-8") as fh:
353
+ fh.write("{}")
354
+ missing = os.path.join(root, "missing.json")
355
+ log = os.path.join(root, "unity_log.txt")
356
+ with open(log, "w", encoding="utf-8") as fh:
357
+ fh.write("Batchmode quit successfully invoked\n")
358
+
359
+ class Case(unittest.TestCase):
360
+ def test_present(self):
361
+ UnityEnv.require_results(self, present, 0, log)
362
+
363
+ def test_unlicensed(self):
364
+ UnityEnv.require_results(self, missing, 198, log)
365
+
366
+ def test_clean_run_wrote_nothing(self):
367
+ UnityEnv.require_results(self, missing, 0, log)
368
+
369
+ result = _run_case(Case)
370
+ self.assertEqual(
371
+ [reason for _t, reason in result.skipped], [UnityEnv.license_reason()]
372
+ )
373
+ self.assertEqual(
374
+ [t.id().rsplit(".", 1)[-1] for t, _tb in result.failures],
375
+ ["test_clean_run_wrote_nothing"],
376
+ )
377
+ self.assertEqual(result.errors, [])
378
+ self.assertEqual(result.testsRun, 3)
379
+
337
380
 
338
381
  class SuiteOptionsTest(unittest.TestCase):
339
382
  """The opt-in switches are named once, neutrally."""
@@ -395,6 +438,48 @@ class TestAssetsTest(unittest.TestCase):
395
438
  )
396
439
 
397
440
 
441
+ class TemplateSourcesTest(unittest.TestCase):
442
+ """Hand-staged templates resolve by bundle path, in one place."""
443
+
444
+ def test_every_bundle_path_resolves_in_the_checkout(self):
445
+ from unitytk import TemplateDeployer
446
+
447
+ names = TemplateDeployer._bundle_names()
448
+ self.assertTrue(TemplateSources.available(names))
449
+ for name in names:
450
+ self.assertTrue(os.path.isfile(TemplateSources.path(name)), name)
451
+
452
+ def test_copy_flat_copies_side_by_side_and_never_skips(self):
453
+ import pythontk as ptk
454
+
455
+ store = ptk.TempArtifacts("unitytk_template_sources", policy="scoped")
456
+ self.addCleanup(store.cleanup)
457
+ dest = os.path.join(store.dir_path(), "Scripts")
458
+ copied = TemplateSources.copy_flat(
459
+ ["UnitytkSettings.cs", "RenderEffects/RenderEffectsController.cs"], dest
460
+ )
461
+ self.assertEqual(
462
+ copied,
463
+ [
464
+ os.path.join(dest, "UnitytkSettings.cs"),
465
+ os.path.join(dest, "RenderEffectsController.cs"),
466
+ ],
467
+ )
468
+ self.assertTrue(all(os.path.isfile(p) for p in copied))
469
+ with self.assertRaises(FileNotFoundError): # a bare, pre-move name
470
+ TemplateSources.copy_flat(["RenderEffectsController.cs"], dest)
471
+
472
+ def test_no_suite_joins_the_templates_dir_itself(self):
473
+ """Five suites joined their own templates dir with a bare file name;
474
+ one of them silently skipped a copy that no longer resolved."""
475
+ offenders = []
476
+ for path in _source_files(".py", exclude=("base_test.py",)):
477
+ with open(path, encoding="utf-8", errors="replace") as fh:
478
+ if re.search(r"""["']unitytk["'],\s*["']templates["']""", fh.read()):
479
+ offenders.append(os.path.relpath(path, REPO_ROOT))
480
+ self.assertEqual(offenders, [], f"templates dir joined by hand in: {offenders}")
481
+
482
+
398
483
  class UserPropsProbeTest(unittest.TestCase):
399
484
  """One postprocessor template, one carrier-matching rule on both sides."""
400
485
 
@@ -65,7 +65,7 @@ import maya.cmds as cmds
65
65
  import maya.mel as mel
66
66
 
67
67
  import pythontk as ptk
68
- from mayatk.anim_utils.shots._shots import ShotStore
68
+ from mayatk import ShotStore
69
69
  from mayatk.env_utils.fbx_utils import FbxUtils
70
70
  from mayatk.node_utils.data_nodes import DataNodes
71
71
 
@@ -104,7 +104,10 @@ takes = ptk.SceneRecords.declared_takes(
104
104
 
105
105
  if not cmds.pluginInfo("fbxmaya", query=True, loaded=True):
106
106
  cmds.loadPlugin("fbxmaya")
107
- mel.eval("FBXExportInAscii -v true")
107
+ # Binary, as every production writer ships: Unity's ASCII FBX reader
108
+ # drops the whole scene (or truncates the string) once a carrier channel
109
+ # passes ~6 KB, and the handoff record alone is past that.
110
+ mel.eval("FBXExportInAscii -v false")
108
111
  mel.eval('FBXExport -f "%s"' % fbx_path)
109
112
  assert os.path.exists(fbx_path), "FBX not created: %s" % fbx_path
110
113
 
@@ -119,7 +122,10 @@ results = {
119
122
  with open(os.path.join(output_dir, "maya_results.json"), "w") as f:
120
123
  json.dump(results, f, indent=2)
121
124
  print("MAYA CLIP-NAME CONTRACT EXPORT COMPLETE — keys:", results["clip_keys"])
122
- maya.standalone.uninitialize()
125
+ # Not maya.standalone.uninitialize(): Maya's static destructors fault at DLL
126
+ # detach, filing a crash dump and an untitled[Recovered].ma per run.
127
+ import pythontk as ptk
128
+ ptk.ProcessExit.hard_exit(0)
123
129
  '''
124
130
 
125
131
 
@@ -297,18 +303,7 @@ class TestClipNameContract(unittest.TestCase):
297
303
  proc.kill()
298
304
  raise TimeoutError("Unity timed out")
299
305
 
300
- if not os.path.exists(results_path):
301
- log_text = ""
302
- if os.path.exists(log_path):
303
- with open(log_path, encoding="utf-8", errors="ignore") as fh:
304
- log_text = fh.read()
305
- if UnityLauncher.is_license_failure(returncode, log_text):
306
- logger.info(
307
- "Unity batch license failure (rc=%s) — see %s",
308
- returncode, log_path,
309
- )
310
- self.skipTest(UnityEnv.license_reason())
311
- self.fail(f"unity_results.json not created (rc={returncode}) — {log_path}")
306
+ UnityEnv.require_results(self, results_path, returncode, log_path)
312
307
  with open(results_path) as f:
313
308
  self.__class__.unity_results = json.load(f)
314
309
  logger.info(f"Unity results:\n{json.dumps(self.__class__.unity_results, indent=2)}")
@@ -215,22 +215,7 @@ class TestControllerRuntimeIntegration(unittest.TestCase):
215
215
  proc.kill()
216
216
  raise TimeoutError("Unity timed out")
217
217
 
218
- if not os.path.exists(results_path):
219
- log_text = ""
220
- if os.path.exists(log_path):
221
- with open(log_path, encoding="utf-8", errors="ignore") as fh:
222
- log_text = fh.read()
223
- if UnityLauncher.is_license_failure(returncode, log_text):
224
- logger.info(
225
- "Unity batch license failure (rc=%s) — see %s",
226
- returncode,
227
- log_path,
228
- )
229
- self.skipTest(UnityEnv.license_reason())
230
- self.fail(
231
- f"unity_results.json not created (Unity rc={returncode}) — "
232
- f"check {log_path}"
233
- )
218
+ UnityEnv.require_results(self, results_path, returncode, log_path)
234
219
  with open(results_path) as f:
235
220
  self.__class__.unity_results = json.load(f)
236
221
  logger.info(