unitytk 0.0.7__tar.gz → 0.0.8__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 (51) hide show
  1. {unitytk-0.0.7/unitytk.egg-info → unitytk-0.0.8}/PKG-INFO +2 -2
  2. {unitytk-0.0.7 → unitytk-0.0.8}/docs/README.md +1 -1
  3. unitytk-0.0.8/test/test_deploy_templates.py +412 -0
  4. {unitytk-0.0.7 → unitytk-0.0.8}/unitytk/__init__.py +3 -2
  5. unitytk-0.0.8/unitytk/template_deployer.py +628 -0
  6. {unitytk-0.0.7 → unitytk-0.0.8}/unitytk/templates/README.md +14 -4
  7. {unitytk-0.0.7 → unitytk-0.0.8/unitytk.egg-info}/PKG-INFO +2 -2
  8. unitytk-0.0.7/test/test_deploy_templates.py +0 -198
  9. unitytk-0.0.7/unitytk/template_deployer.py +0 -237
  10. {unitytk-0.0.7 → unitytk-0.0.8}/LICENSE +0 -0
  11. {unitytk-0.0.7 → unitytk-0.0.8}/MANIFEST.in +0 -0
  12. {unitytk-0.0.7 → unitytk-0.0.8}/pyproject.toml +0 -0
  13. {unitytk-0.0.7 → unitytk-0.0.8}/setup.cfg +0 -0
  14. {unitytk-0.0.7 → unitytk-0.0.8}/test/test_asset_delivery.py +0 -0
  15. {unitytk-0.0.7 → unitytk-0.0.8}/test/test_audio_events_integration.py +0 -0
  16. {unitytk-0.0.7 → unitytk-0.0.8}/test/test_bool_to_fade_curve.py +0 -0
  17. {unitytk-0.0.7 → unitytk-0.0.8}/test/test_c130_fcr_integration.py +0 -0
  18. {unitytk-0.0.7 → unitytk-0.0.8}/test/test_c5m_visibility.py +0 -0
  19. {unitytk-0.0.7 → unitytk-0.0.8}/test/test_clip_name_contract_integration.py +0 -0
  20. {unitytk-0.0.7 → unitytk-0.0.8}/test/test_controller_runtime_integration.py +0 -0
  21. {unitytk-0.0.7 → unitytk-0.0.8}/test/test_emissive_groups_integration.py +0 -0
  22. {unitytk-0.0.7 → unitytk-0.0.8}/test/test_fade_window_preservation.py +0 -0
  23. {unitytk-0.0.7 → unitytk-0.0.8}/test/test_launcher.py +0 -0
  24. {unitytk-0.0.7 → unitytk-0.0.8}/test/test_lightmap_metadata_integration.py +0 -0
  25. {unitytk-0.0.7 → unitytk-0.0.8}/test/test_render_opacity_controller.py +0 -0
  26. {unitytk-0.0.7 → unitytk-0.0.8}/test/test_render_opacity_e2e.py +0 -0
  27. {unitytk-0.0.7 → unitytk-0.0.8}/test/test_render_opacity_integration.py +0 -0
  28. {unitytk-0.0.7 → unitytk-0.0.8}/test/test_scene_builder.py +0 -0
  29. {unitytk-0.0.7 → unitytk-0.0.8}/test/test_shadow_plane_integration.py +0 -0
  30. {unitytk-0.0.7 → unitytk-0.0.8}/test/test_shared_material_standalone.py +0 -0
  31. {unitytk-0.0.7 → unitytk-0.0.8}/test/test_shot_metadata_integration.py +0 -0
  32. {unitytk-0.0.7 → unitytk-0.0.8}/test/test_shots_audio_sidebyside_integration.py +0 -0
  33. {unitytk-0.0.7 → unitytk-0.0.8}/test/test_standalone_enduser.py +0 -0
  34. {unitytk-0.0.7 → unitytk-0.0.8}/unitytk/asset_delivery.py +0 -0
  35. {unitytk-0.0.7 → unitytk-0.0.8}/unitytk/launcher.py +0 -0
  36. {unitytk-0.0.7 → unitytk-0.0.8}/unitytk/scene_builder.py +0 -0
  37. {unitytk-0.0.7 → unitytk-0.0.8}/unitytk/templates/AudioEventController.cs +0 -0
  38. {unitytk-0.0.7 → unitytk-0.0.8}/unitytk/templates/EmissiveGroupController.cs +0 -0
  39. {unitytk-0.0.7 → unitytk-0.0.8}/unitytk/templates/EmissiveGroups.hlsl +0 -0
  40. {unitytk-0.0.7 → unitytk-0.0.8}/unitytk/templates/EmissiveGroupsLit.shader +0 -0
  41. {unitytk-0.0.7 → unitytk-0.0.8}/unitytk/templates/LightmapMetadataController.cs +0 -0
  42. {unitytk-0.0.7 → unitytk-0.0.8}/unitytk/templates/M3trik.Unitytk.asmdef +0 -0
  43. {unitytk-0.0.7 → unitytk-0.0.8}/unitytk/templates/RenderOpacityController.cs +0 -0
  44. {unitytk-0.0.7 → unitytk-0.0.8}/unitytk/templates/ShadowPlaneController.cs +0 -0
  45. {unitytk-0.0.7 → unitytk-0.0.8}/unitytk/templates/ShotMetadataController.cs +0 -0
  46. {unitytk-0.0.7 → unitytk-0.0.8}/unitytk/templates/UnitytkSettings.cs +0 -0
  47. {unitytk-0.0.7 → unitytk-0.0.8}/unitytk/templates/__init__.py +0 -0
  48. {unitytk-0.0.7 → unitytk-0.0.8}/unitytk.egg-info/SOURCES.txt +0 -0
  49. {unitytk-0.0.7 → unitytk-0.0.8}/unitytk.egg-info/dependency_links.txt +0 -0
  50. {unitytk-0.0.7 → unitytk-0.0.8}/unitytk.egg-info/requires.txt +0 -0
  51. {unitytk-0.0.7 → unitytk-0.0.8}/unitytk.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: unitytk
3
- Version: 0.0.7
3
+ Version: 0.0.8
4
4
  Summary: A modular Python toolkit for Unity interaction.
5
5
  Author-email: Ryan Simpson <m3trik@outlook.com>
6
6
  License: MIT
@@ -15,7 +15,7 @@ License-File: LICENSE
15
15
  Requires-Dist: pythontk>=0.7.71
16
16
  Dynamic: license-file
17
17
 
18
- [![Tests](https://img.shields.io/badge/Tests-124%20passed-brightgreen.svg)](../test/)
18
+ [![Tests](https://img.shields.io/badge/Tests-140%20passed-brightgreen.svg)](../test/)
19
19
 
20
20
  # unitytk
21
21
 
@@ -1,4 +1,4 @@
1
- [![Tests](https://img.shields.io/badge/Tests-124%20passed-brightgreen.svg)](../test/)
1
+ [![Tests](https://img.shields.io/badge/Tests-140%20passed-brightgreen.svg)](../test/)
2
2
 
3
3
  # unitytk
4
4
 
@@ -0,0 +1,412 @@
1
+ # !/usr/bin/python
2
+ # coding=utf-8
3
+ """Tests for the embedded-UPM-package template deployment (no Unity needed).
4
+
5
+ The C# controller templates are compile-coupled: every controller calls the
6
+ shared ``ImportGate`` and ``CarrierImport`` classes that only
7
+ ``UnitytkSettings.cs`` defines, so a Unity project must hold the *core* plus
8
+ whichever controllers it deploys to compile. These tests pin that contract on
9
+ the Python side — the only side that can be tested without a licensed Unity
10
+ install — including the per-component selection the DCC panels' script
11
+ checklist drives.
12
+ """
13
+ import json
14
+ import os
15
+ import shutil
16
+ import tempfile
17
+ import unittest
18
+ from unittest import mock
19
+
20
+ import unitytk
21
+ from unitytk import TemplateDeployer
22
+ from unitytk.template_deployer import UPM_PACKAGE_NAME
23
+
24
+
25
+ class DeployTemplateTest(unittest.TestCase):
26
+ def setUp(self):
27
+ self.tmp = tempfile.mkdtemp(prefix="unitytk_deploy_")
28
+ self.project = os.path.join(self.tmp, "Project")
29
+ os.makedirs(os.path.join(self.project, "Assets"))
30
+ self.package = os.path.join(self.project, "Packages", UPM_PACKAGE_NAME)
31
+
32
+ def tearDown(self):
33
+ shutil.rmtree(self.tmp, ignore_errors=True)
34
+
35
+ def test_single_template_round_trips(self):
36
+ dest = os.path.join(self.package, "UnitytkSettings.cs")
37
+ written = TemplateDeployer.deploy("UnitytkSettings.cs", dest)
38
+ self.assertTrue(os.path.isfile(written))
39
+ with open(written, encoding="utf-8") as fh:
40
+ self.assertIn("class ImportGate", fh.read())
41
+
42
+ def test_deploy_package_ships_the_full_coupled_set(self):
43
+ """Every bundled controller, shader asset, the asmdef, the catalog
44
+ README and a generated package.json land — no partial deploy."""
45
+ written = TemplateDeployer.deploy_package(self.project)
46
+ names = sorted(os.path.basename(p) for p in written)
47
+ self.assertEqual(
48
+ names,
49
+ [
50
+ "AudioEventController.cs",
51
+ "EmissiveGroupController.cs",
52
+ "EmissiveGroups.hlsl",
53
+ "EmissiveGroupsLit.shader",
54
+ "LightmapMetadataController.cs",
55
+ "M3trik.Unitytk.asmdef",
56
+ "README.md",
57
+ "RenderOpacityController.cs",
58
+ "ShadowPlaneController.cs",
59
+ "ShotMetadataController.cs",
60
+ "UnitytkSettings.cs",
61
+ "package.json",
62
+ ],
63
+ )
64
+ for p in written:
65
+ self.assertTrue(os.path.isfile(p))
66
+ self.assertGreater(os.path.getsize(p), 0)
67
+ self.assertTrue(
68
+ os.path.normcase(p).startswith(os.path.normcase(self.package)),
69
+ f"{p} escaped the package folder",
70
+ )
71
+
72
+ def test_package_manifest_matches_the_release(self):
73
+ """package.json carries the UPM identity + this unitytk version, so
74
+ the Package Manager row is truthful and updates version-bump cleanly."""
75
+ TemplateDeployer.deploy_package(self.project)
76
+ with open(os.path.join(self.package, "package.json"), encoding="utf-8") as fh:
77
+ manifest = json.load(fh)
78
+ self.assertEqual(manifest["name"], UPM_PACKAGE_NAME)
79
+ self.assertEqual(manifest["version"], unitytk.__version__)
80
+ self.assertTrue(manifest["displayName"])
81
+ self.assertTrue(manifest["unity"])
82
+
83
+ def test_deploy_package_rejects_a_non_unity_folder(self):
84
+ """Guards against scattering the package into a wrong path."""
85
+ with self.assertRaises(ValueError):
86
+ TemplateDeployer.deploy_package(self.tmp)
87
+
88
+ def test_shader_pair_travels_together(self):
89
+ """``EmissiveGroupsLit.shader`` includes the .hlsl by RELATIVE path, so
90
+ deploying one without the other is a shader compile error in Unity."""
91
+ written = TemplateDeployer.deploy_package(self.project)
92
+ names = {os.path.basename(p) for p in written}
93
+ self.assertIn("EmissiveGroups.hlsl", names)
94
+ shader = os.path.join(self.package, "EmissiveGroupsLit.shader")
95
+ with open(shader, encoding="utf-8") as fh:
96
+ self.assertIn('#include "EmissiveGroups.hlsl"', fh.read())
97
+
98
+ # ---------------------------------------------------------------- components
99
+ def test_components_derive_from_the_bundle(self):
100
+ """One required core plus one optional component per controller, keyed
101
+ and labelled off the filename — a controller added to the bundle
102
+ surfaces in the panels' checklist with no registry to update."""
103
+ comps = TemplateDeployer.components()
104
+ core = comps[0]
105
+ self.assertEqual(core.key, "core")
106
+ self.assertTrue(core.required)
107
+ self.assertIn("UnitytkSettings.cs", core.files)
108
+ self.assertIn("M3trik.Unitytk.asmdef", core.files)
109
+ # No controller hides in core (each is its own opt-in component).
110
+ self.assertFalse([n for n in core.files if n.endswith("Controller.cs")])
111
+
112
+ optional = {c.key: c for c in comps[1:]}
113
+ self.assertEqual(
114
+ sorted(optional),
115
+ [
116
+ "audio_event",
117
+ "emissive_group",
118
+ "lightmap_metadata",
119
+ "render_opacity",
120
+ "shadow_plane",
121
+ "shot_metadata",
122
+ ],
123
+ )
124
+ self.assertEqual(optional["shot_metadata"].label, "Shot Metadata")
125
+ self.assertFalse(any(c.required for c in optional.values()))
126
+ # Every bundled file belongs to exactly one component.
127
+ owned = [n for c in comps for n in c.files]
128
+ self.assertEqual(sorted(owned), sorted(TemplateDeployer._bundle_names()))
129
+ self.assertEqual(len(owned), len(set(owned)))
130
+
131
+ def test_shader_pair_belongs_to_its_controller(self):
132
+ """``EmissiveGroupsLit.shader`` includes the .hlsl by RELATIVE path, so
133
+ the pair travels with the controller that needs it — never on its own,
134
+ never left behind."""
135
+ emissive = {c.key: c for c in TemplateDeployer.components()}["emissive_group"]
136
+ self.assertEqual(
137
+ sorted(emissive.files),
138
+ [
139
+ "EmissiveGroupController.cs",
140
+ "EmissiveGroups.hlsl",
141
+ "EmissiveGroupsLit.shader",
142
+ ],
143
+ )
144
+
145
+ def test_satellite_goes_to_the_longest_matching_controller(self):
146
+ """Ownership is longest-stem-wins, so adding an `EmissiveController.cs`
147
+ beside `EmissiveGroupController.cs` can't give the shader pair two
148
+ owners -- a doubly-owned file deploys twice but survives the first
149
+ uninstall, desyncing deploy from remove."""
150
+ bundle = [
151
+ "EmissiveController.cs",
152
+ "EmissiveGroupController.cs",
153
+ "EmissiveGroups.hlsl",
154
+ "EmissiveGroupsLit.shader",
155
+ "UnitytkSettings.cs",
156
+ ]
157
+ with mock.patch.object(TemplateDeployer, "_bundle_names", staticmethod(lambda: bundle)):
158
+ comps = {c.key: c for c in TemplateDeployer.components()}
159
+ self.assertEqual(comps["emissive"].files, ("EmissiveController.cs",))
160
+ self.assertEqual(
161
+ comps["emissive_group"].files,
162
+ (
163
+ "EmissiveGroupController.cs",
164
+ "EmissiveGroups.hlsl",
165
+ "EmissiveGroupsLit.shader",
166
+ ),
167
+ )
168
+ owned = [n for c in comps.values() for n in c.files]
169
+ self.assertEqual(len(owned), len(set(owned)), "a file has two owners")
170
+
171
+ def test_keys_and_labels_survive_an_acronym(self):
172
+ """Names are split on real word boundaries, so a `UVLayoutController`
173
+ reads as 'UV Layout' / `uv_layout` rather than 'U V Layout'."""
174
+ self.assertEqual(TemplateDeployer._component_key("UVLayout"), "uv_layout")
175
+ self.assertEqual(TemplateDeployer._component_label("UVLayout"), "UV Layout")
176
+ self.assertEqual(TemplateDeployer._component_key("ShotMetadata"), "shot_metadata")
177
+
178
+ def test_selected_deploy_carries_core_and_nothing_else(self):
179
+ """A checked subset deploys those components + the core they compile
180
+ against, and leaves the unchecked ones out of the project."""
181
+ written = TemplateDeployer.deploy_package(self.project, ["audio_event"])
182
+ names = sorted(os.path.basename(p) for p in written)
183
+ self.assertEqual(
184
+ names,
185
+ [
186
+ "AudioEventController.cs",
187
+ "M3trik.Unitytk.asmdef",
188
+ "README.md",
189
+ "UnitytkSettings.cs",
190
+ "package.json",
191
+ ],
192
+ )
193
+
194
+ def test_selected_deploy_is_additive(self):
195
+ """Deploying a second component updates in place — it never removes the
196
+ one already there (Install / Update, not Sync)."""
197
+ TemplateDeployer.deploy_package(self.project, ["audio_event"])
198
+ TemplateDeployer.deploy_package(self.project, ["shot_metadata"])
199
+ present = set(os.listdir(self.package))
200
+ self.assertIn("AudioEventController.cs", present)
201
+ self.assertIn("ShotMetadataController.cs", present)
202
+
203
+ def test_unknown_component_raises_before_writing(self):
204
+ """A typo must not silently deploy a smaller set than asked for."""
205
+ with self.assertRaises(ValueError):
206
+ TemplateDeployer.deploy_package(self.project, ["audio_events"])
207
+ self.assertFalse(os.path.isdir(self.package))
208
+
209
+ def test_selective_uninstall_keeps_the_rest(self):
210
+ TemplateDeployer.deploy_package(self.project)
211
+ removed = TemplateDeployer.uninstall_package(self.project, ["emissive_group"])
212
+ self.assertEqual(
213
+ sorted(os.path.basename(p) for p in removed),
214
+ [
215
+ "EmissiveGroupController.cs",
216
+ "EmissiveGroups.hlsl",
217
+ "EmissiveGroupsLit.shader",
218
+ ],
219
+ )
220
+ present = set(os.listdir(self.package))
221
+ self.assertNotIn("EmissiveGroupController.cs", present)
222
+ self.assertIn("AudioEventController.cs", present)
223
+ self.assertIn("UnitytkSettings.cs", present)
224
+
225
+ def test_removing_the_last_component_takes_the_package(self):
226
+ """Core alone gates nothing — the folder goes with the last controller
227
+ rather than leaving a settings page for no channels."""
228
+ TemplateDeployer.deploy_package(self.project, ["audio_event"])
229
+ TemplateDeployer.uninstall_package(self.project, ["audio_event"])
230
+ self.assertFalse(os.path.isdir(self.package))
231
+
232
+ def test_core_cannot_be_uninstalled_on_its_own(self):
233
+ TemplateDeployer.deploy_package(self.project)
234
+ with self.assertRaises(ValueError):
235
+ TemplateDeployer.uninstall_package(self.project, ["core"])
236
+ self.assertTrue(os.path.isfile(os.path.join(self.package, "UnitytkSettings.cs")))
237
+
238
+ def test_status_reports_not_installed_then_up_to_date(self):
239
+ """The panels' Status action: absent → installed=False with the full
240
+ bundle missing; after deploy → current version, nothing missing."""
241
+ before = TemplateDeployer.status(self.project)
242
+ self.assertFalse(before["installed"])
243
+ self.assertFalse(before["up_to_date"])
244
+ self.assertIn("UnitytkSettings.cs", before["missing"])
245
+
246
+ TemplateDeployer.deploy_package(self.project)
247
+ after = TemplateDeployer.status(self.project)
248
+ self.assertTrue(after["installed"])
249
+ self.assertEqual(after["version"], unitytk.__version__)
250
+ self.assertEqual(after["missing"], [])
251
+ self.assertTrue(after["up_to_date"])
252
+
253
+ def test_status_flags_a_partial_deploy(self):
254
+ """A deleted file shows in ``missing`` + ``incomplete`` and clears
255
+ ``up_to_date`` — a component is only valid whole."""
256
+ TemplateDeployer.deploy_package(self.project)
257
+ os.remove(os.path.join(self.package, "UnitytkSettings.cs"))
258
+ info = TemplateDeployer.status(self.project)
259
+ self.assertEqual(info["missing"], ["UnitytkSettings.cs"])
260
+ self.assertEqual(info["incomplete"], ["UnitytkSettings.cs"])
261
+ self.assertFalse(info["up_to_date"])
262
+
263
+ def test_status_reports_each_component(self):
264
+ """The panels' checklist report: deployed components tick, opted-out
265
+ ones don't, and opting out is NOT a broken install."""
266
+ TemplateDeployer.deploy_package(self.project, ["audio_event"])
267
+ info = TemplateDeployer.status(self.project)
268
+ states = {c["key"]: c["installed"] for c in info["components"]}
269
+ self.assertTrue(states["core"])
270
+ self.assertTrue(states["audio_event"])
271
+ self.assertFalse(states["shot_metadata"])
272
+ # Not deploying a channel is a choice, not a partial deploy.
273
+ self.assertEqual(info["incomplete"], [])
274
+ self.assertTrue(info["up_to_date"])
275
+ self.assertIn("ShotMetadataController.cs", info["missing"])
276
+
277
+ def test_status_flags_a_controller_with_no_core(self):
278
+ """A controller left without the ImportGate it compiles against is
279
+ broken even though each file set is individually 'whole'."""
280
+ TemplateDeployer.deploy_package(self.project, ["audio_event"])
281
+ for name in ("UnitytkSettings.cs", "M3trik.Unitytk.asmdef", "README.md"):
282
+ os.remove(os.path.join(self.package, name))
283
+ info = TemplateDeployer.status(self.project)
284
+ self.assertIn("UnitytkSettings.cs", info["incomplete"])
285
+ self.assertFalse(info["up_to_date"])
286
+
287
+ # ---------------------------------------------------------------- run_action
288
+ def test_run_action_status_lists_every_component(self):
289
+ """One shared report for the Maya / Blender / standalone panels: a
290
+ header line plus a ticked row per component."""
291
+ TemplateDeployer.deploy_package(self.project, ["audio_event"])
292
+ lines = TemplateDeployer.run_action(self.project, "status")
293
+ self.assertTrue(all(level == "info" for level, _ in lines))
294
+ text = "\n".join(m for _, m in lines)
295
+ self.assertIn(f"deployed v{unitytk.__version__}", text)
296
+ self.assertIn("[x] Audio Event", text)
297
+ self.assertIn("[ ] Shot Metadata", text)
298
+
299
+ def test_run_action_install_and_uninstall_the_checked_set(self):
300
+ install = TemplateDeployer.run_action(
301
+ self.project, "install", ["audio_event"]
302
+ )
303
+ self.assertEqual([lvl for lvl, _ in install], ["info"])
304
+ self.assertIn("Audio Event", install[0][1])
305
+ self.assertTrue(
306
+ os.path.isfile(os.path.join(self.package, "AudioEventController.cs"))
307
+ )
308
+
309
+ uninstall = TemplateDeployer.run_action(
310
+ self.project, "uninstall", ["audio_event"]
311
+ )
312
+ self.assertEqual([lvl for lvl, _ in uninstall], ["info"])
313
+ self.assertFalse(os.path.isdir(self.package))
314
+
315
+ def test_run_action_rejects_an_empty_selection(self):
316
+ """Nothing checked is a mistake to report, not a silent no-op (while
317
+ ``None`` still means the full set)."""
318
+ for action in ("install", "uninstall"):
319
+ lines = TemplateDeployer.run_action(self.project, action, [])
320
+ self.assertEqual([lvl for lvl, _ in lines], ["error"])
321
+ self.assertFalse(os.path.isdir(self.package))
322
+
323
+ def test_run_action_reports_failures_instead_of_raising(self):
324
+ """The panels log what came back; a bad key or action must not escape
325
+ as an exception into the DCC's event loop."""
326
+ for lines in (
327
+ TemplateDeployer.run_action(self.project, "install", ["nope"]),
328
+ TemplateDeployer.run_action(self.project, "frobnicate"),
329
+ TemplateDeployer.run_action(self.tmp, "install"), # not a Unity project
330
+ ):
331
+ self.assertEqual([lvl for lvl, _ in lines], ["error"])
332
+
333
+ def test_uninstall_removes_only_our_package(self):
334
+ """Uninstall deletes the deployed folder (True), no-ops when absent
335
+ (False), and REFUSES a folder whose package.json is not ours — never
336
+ delete a directory we merely expected to own."""
337
+ self.assertFalse(TemplateDeployer.uninstall_package(self.project))
338
+
339
+ TemplateDeployer.deploy_package(self.project)
340
+ self.assertTrue(TemplateDeployer.uninstall_package(self.project))
341
+ self.assertFalse(os.path.isdir(self.package))
342
+
343
+ os.makedirs(self.package)
344
+ with open(os.path.join(self.package, "package.json"), "w") as fh:
345
+ json.dump({"name": "com.example.other"}, fh)
346
+ with self.assertRaises(ValueError):
347
+ TemplateDeployer.uninstall_package(self.project)
348
+ self.assertTrue(os.path.isdir(self.package), "foreign folder untouched")
349
+
350
+ def test_deploy_package_never_writes_project_config(self):
351
+ """Per-project settings live in ProjectSettings/UnitytkSettings.json,
352
+ owned by the project — deploying must not create or clobber them."""
353
+ TemplateDeployer.deploy_package(self.project)
354
+ self.assertFalse(
355
+ os.path.isfile(
356
+ os.path.join(self.project, "ProjectSettings", "UnitytkSettings.json")
357
+ )
358
+ )
359
+
360
+ # Every shared class lives in UnitytkSettings.cs; the other controllers
361
+ # must reference it (definer set / referencer set per class). Shader
362
+ # assets (.hlsl/.shader) carry no C# and are excluded.
363
+ COUPLED_DEFINER = {"UnitytkSettings.cs"}
364
+ COUPLED_REFERENCERS = {
365
+ "AudioEventController.cs",
366
+ "EmissiveGroupController.cs",
367
+ "LightmapMetadataController.cs",
368
+ "RenderOpacityController.cs",
369
+ "ShadowPlaneController.cs",
370
+ "ShotMetadataController.cs",
371
+ }
372
+
373
+ def _coupling_surface(self, shared_class):
374
+ """Deploy the set; return ``(definers, referencers)`` of *shared_class*
375
+ across the C# templates only."""
376
+ written = [
377
+ p for p in TemplateDeployer.deploy_package(self.project)
378
+ if p.endswith(".cs")
379
+ ]
380
+ definers, referencers = set(), set()
381
+ for p in written:
382
+ with open(p, encoding="utf-8") as fh:
383
+ text = fh.read()
384
+ name = os.path.basename(p)
385
+ if f"class {shared_class}" in text:
386
+ definers.add(name)
387
+ elif shared_class in text:
388
+ referencers.add(name)
389
+ return definers, referencers
390
+
391
+ def test_exactly_one_import_gate_definition_in_the_set(self):
392
+ """One template defines ``ImportGate``; every other one references it.
393
+
394
+ Guards the compile-coupling as templates evolve: a second definition
395
+ would be a duplicate-class CS error, a dropped reference would mean a
396
+ controller stopped honoring the channel toggles + FBX allowlist.
397
+ """
398
+ definers, referencers = self._coupling_surface("ImportGate")
399
+ self.assertEqual(definers, self.COUPLED_DEFINER)
400
+ self.assertEqual(referencers, self.COUPLED_REFERENCERS)
401
+
402
+ def test_exactly_one_carrier_import_definition_in_the_set(self):
403
+ """One template defines ``CarrierImport``; every other one references
404
+ it — same coupling contract as ``ImportGate`` (both live in
405
+ ``UnitytkSettings.cs``)."""
406
+ definers, referencers = self._coupling_surface("CarrierImport")
407
+ self.assertEqual(definers, self.COUPLED_DEFINER)
408
+ self.assertEqual(referencers, self.COUPLED_REFERENCERS)
409
+
410
+
411
+ if __name__ == "__main__":
412
+ unittest.main(verbosity=2)
@@ -1,8 +1,8 @@
1
- __version__ = "0.0.7"
1
+ __version__ = "0.0.8"
2
2
 
3
3
  from .launcher import UnityLauncher, UnityFinder
4
4
  from .scene_builder import SceneBuilder
5
- from .template_deployer import TemplateDeployer
5
+ from .template_deployer import TemplateDeployer, PackageComponent
6
6
  from .asset_delivery import CopyToAssetsDeliverer, FileToUnityBridge
7
7
 
8
8
  __all__ = [
@@ -10,6 +10,7 @@ __all__ = [
10
10
  "UnityFinder",
11
11
  "SceneBuilder",
12
12
  "TemplateDeployer",
13
+ "PackageComponent",
13
14
  "CopyToAssetsDeliverer",
14
15
  "FileToUnityBridge",
15
16
  ]