Skip to content

Commit 58a5ad6

Browse files
committed
feat: isolated search paths for fixture tests, force system rediscovery
- Add JsScriptSearchPaths.RemoveAllSources/RestoreSources for test isolation - Add IntegrationTestHarness.UseIsolatedSearchPath — removes game scripts, registers only fixture path, forces JsSystemRunner to re-discover - Add JsSystemRunner.ForceRediscovery — resets LastVmVersion to trigger re-init on next OnUpdate - Fix character_input fixture: export onUpdate (system script, not entity script) - Rework JsCharacterControllerE2ETests to create entity with components only (system discovers it via query, no script attachment) - Clean JsGameCodegen~ build artifacts, add .gitignore
1 parent fc18932 commit 58a5ad6

189 files changed

Lines changed: 153 additions & 4719 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Editor/Integrations/IntegrationTestHarness.cs

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
#if UNITY_EDITOR
21
using System;
2+
using System.Collections.Generic;
33
using System.IO;
44
using System.Runtime.InteropServices;
55
using System.Text;
@@ -99,6 +99,13 @@ public static string CompileFixtures(string fixturesPath)
9999
/// </summary>
100100
public static SearchPathScope UseSearchPath(string absolutePath) => new(absolutePath);
101101

102+
/// <summary>
103+
/// Registers a fixture path as the ONLY search path, removing all others
104+
/// (StreamingAssets, TscBuild). Prevents game scripts from interfering with fixtures.
105+
/// Restores original paths on dispose.
106+
/// </summary>
107+
public static IsolatedSearchPathScope UseIsolatedSearchPath(string absolutePath) => new(absolutePath);
108+
102109
public sealed class SearchPathScope : IDisposable
103110
{
104111
readonly string m_Path;
@@ -112,6 +119,49 @@ public SearchPathScope(string path)
112119
public void Dispose() => JsScriptSearchPaths.RemoveSearchPath(m_Path);
113120
}
114121

122+
public sealed class IsolatedSearchPathScope : IDisposable
123+
{
124+
readonly List<(string path, string sourceId, int priority)> m_Saved;
125+
readonly string m_Path;
126+
127+
public IsolatedSearchPathScope(string path)
128+
{
129+
m_Path = path;
130+
m_Saved = JsScriptSearchPaths.RemoveAllSources();
131+
JsScriptSearchPaths.AddSearchPath(path, 0);
132+
133+
// Force JsSystemRunner to re-discover systems from the isolated path
134+
var world = Unity.Entities.World.DefaultGameObjectInjectionWorld;
135+
if (world != null)
136+
{
137+
var handle = world.Unmanaged.GetExistingUnmanagedSystem<Entities.Systems.JsSystemRunner>();
138+
if (handle != Unity.Entities.SystemHandle.Null)
139+
{
140+
ref var sysState = ref world.Unmanaged.ResolveSystemStateRef(handle);
141+
Entities.Systems.JsSystemRunner.ForceRediscovery(ref sysState);
142+
}
143+
}
144+
}
145+
146+
public void Dispose()
147+
{
148+
JsScriptSearchPaths.RemoveSearchPath(m_Path);
149+
JsScriptSearchPaths.RestoreSources(m_Saved);
150+
151+
// Force re-discovery with restored paths
152+
var world = Unity.Entities.World.DefaultGameObjectInjectionWorld;
153+
if (world != null)
154+
{
155+
var handle = world.Unmanaged.GetExistingUnmanagedSystem<Entities.Systems.JsSystemRunner>();
156+
if (handle != Unity.Entities.SystemHandle.Null)
157+
{
158+
ref var sysState = ref world.Unmanaged.ResolveSystemStateRef(handle);
159+
Entities.Systems.JsSystemRunner.ForceRediscovery(ref sysState);
160+
}
161+
}
162+
}
163+
}
164+
115165
// ── Entity Creation ──
116166

117167
/// <summary>
@@ -250,4 +300,3 @@ static unsafe void EvalVoidInternal(UnityJS.QJS.JSContext ctx, string code)
250300
}
251301
}
252302
}
253-
#endif

Editor/JsHotReloadSystem.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
#if UNITY_EDITOR
21
namespace UnityJS.Editor
32
{
43
using System;
@@ -115,4 +114,3 @@ static void DisposeWatcher(ref FileSystemWatcher watcher)
115114
}
116115
}
117116
}
118-
#endif

Editor/JsPropertyParser.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
#if UNITY_EDITOR
21
namespace UnityJS.Editor
32
{
43
using System.Collections.Generic;
@@ -248,4 +247,3 @@ static bool TryParseFloat(string s, out float result)
248247
}
249248
}
250249
}
251-
#endif

Editor/JsScriptAuthoringEditor.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
#if UNITY_EDITOR
21
namespace UnityJS.Editor
32
{
43
using System.Collections.Generic;
@@ -308,4 +307,3 @@ static string GetRelativePath(string fullPath, string basePath)
308307
}
309308
}
310309
}
311-
#endif

Editor/TscBuildPreprocessor.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
#if UNITY_EDITOR
21
using System.Collections.Generic;
32
using System.IO;
43
using UnityEditor.Build;
@@ -71,4 +70,3 @@ public void OnPostprocessBuild(BuildReport report)
7170
}
7271
}
7372
}
74-
#endif

Editor/TscCompiler.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
#if UNITY_EDITOR
21
using System;
32
using System.Collections.Generic;
43
using System.Diagnostics;
@@ -201,4 +200,3 @@ public bool RecompileIfStale()
201200
static void RecompileMenu() => Instance?.Recompile();
202201
}
203202
}
204-
#endif

Editor/TscFileWatcher.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
#if UNITY_EDITOR
21
using System;
32
using System.Collections.Generic;
43
using System.IO;
@@ -161,4 +160,3 @@ var jsFile in Directory.GetFiles(componentsDir, "*.js", SearchOption.AllDirector
161160
}
162161
}
163162
}
164-
#endif

Editor/TscStatusBarIndicator.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
#if UNITY_EDITOR
21
using System;
32
using System.Reflection;
43
using System.Text;
@@ -216,4 +215,3 @@ static void OnClick()
216215
}
217216
}
218217
}
219-
#endif

Integrations/CharacterController/Fixtures~/systems/character_input.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const charQuery = query()
1818
.withAll(ECSCharacterControl, ECSCharacterStats, ECSCharacterState)
1919
.build();
2020

21-
export function onTick(state: UpdateState): void {
21+
export function onUpdate(state: UpdateState): void {
2222
const dt = state.deltaTime;
2323
const ti: TestInput = (globalThis as { _testInput?: TestInput })._testInput ?? {};
2424

Integrations/CharacterController/Tests/EditMode/JsCharacterControllerE2ETests.cs

Lines changed: 53 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -2,59 +2,84 @@ namespace UnityJS.Integration.CharacterController.EditModeTests
22
{
33
using System.Collections;
44
using NUnit.Framework;
5+
using Unity.Collections;
56
using Unity.Entities;
67
using Unity.Mathematics;
8+
using Unity.Transforms;
79
using UnityEngine;
810
using UnityEngine.TestTools;
11+
using UnityJS.Entities.Components;
12+
using UnityJS.Entities.Core;
913
using UnityJS.Integrations.Editor;
1014
using UnityJS.Runtime;
1115

1216
/// <summary>
13-
/// E2E tests verifying CharacterController integration: fixture script reads
14-
/// _testInput and writes ECSCharacterControl, stamina, and jump state.
15-
/// No scene loading — entities created programmatically.
17+
/// E2E tests verifying CharacterController integration.
18+
/// The fixture character_input system script reads _testInput and writes
19+
/// ECSCharacterControl/Stats on matching entities.
1620
/// </summary>
1721
[TestFixture]
1822
public class JsCharacterControllerE2ETests
1923
{
24+
/// <summary>
25+
/// Creates an entity with character components and a JsEntityId.
26+
/// No script attached — the system script finds it via ecs.query.
27+
/// </summary>
28+
static Entity CreateCharacterEntity(EntityManager em)
29+
{
30+
var types = new NativeList<ComponentType>(6, Allocator.Temp);
31+
types.Add(ComponentType.ReadWrite<JsEntityId>());
32+
types.Add(ComponentType.ReadWrite<LocalTransform>());
33+
types.Add(ComponentType.ReadWrite<ECSCharacterControl>());
34+
types.Add(ComponentType.ReadWrite<ECSCharacterStats>());
35+
types.Add(ComponentType.ReadWrite<ECSCharacterState>());
36+
types.Add(ComponentType.ReadWrite<ECSCharacterFixedInput>());
37+
var entity = em.CreateEntity(types.AsArray());
38+
types.Dispose();
39+
40+
var entityId = JsEntityRegistry.AllocateId();
41+
JsEntityRegistry.RegisterImmediate(entity, entityId, em);
42+
em.SetComponentData(entity, new JsEntityId { value = entityId });
43+
em.SetComponentData(entity, LocalTransform.FromPosition(float3.zero));
44+
em.SetComponentData(entity, ECSCharacterStats.Default());
45+
46+
return entity;
47+
}
48+
2049
[UnityTest]
2150
public IEnumerator CharacterInput_MoveVector_WrittenFromTestInput()
2251
{
52+
// Register fixture search path BEFORE play mode so JsSystemRunner discovers the system
2353
var fixturesPath = IntegrationTestHarness.GetFixturesPath(
2454
"Integrations/CharacterController/Fixtures~"
2555
);
2656
var compiledPath = IntegrationTestHarness.CompileFixtures(fixturesPath);
2757

2858
yield return new EnterPlayMode();
2959

30-
using var searchPath = IntegrationTestHarness.UseSearchPath(compiledPath);
31-
var em = World.DefaultGameObjectInjectionWorld.EntityManager;
32-
var entity = IntegrationTestHarness.CreateScriptedEntity(
33-
em,
34-
"systems/character_input",
35-
ComponentType.ReadWrite<ECSCharacterControl>(),
36-
ComponentType.ReadWrite<ECSCharacterStats>(),
37-
ComponentType.ReadWrite<ECSCharacterState>(),
38-
ComponentType.ReadWrite<ECSCharacterFixedInput>()
39-
);
40-
em.SetComponentData(entity, ECSCharacterStats.Default());
60+
// Register search path so system discovery finds systems/character_input
61+
using var searchPath = IntegrationTestHarness.UseIsolatedSearchPath(compiledPath);
62+
63+
var world = World.DefaultGameObjectInjectionWorld;
64+
var em = world.EntityManager;
65+
66+
// Create target entity with character components (system finds it via query)
67+
var entity = CreateCharacterEntity(em);
4168

42-
// Wait for fulfillment + query pipeline:
43-
// Frame 1: fulfillment processes request
44-
// Frame 2: script ticks, query registers as pending
45-
// Frame 3: JsSystemRunner flushes pending query, precomputes
46-
// Frame 4: script ticks with query results
47-
// Frame 5+: stable
69+
// Wait for system discovery + query pipeline:
70+
// Frame 1: JsSystemRunner.EnsureVmReady discovers and loads systems/character_input
71+
// Frame 2: system's onUpdate runs, query registers as pending
72+
// Frame 3: PrewarmComponentQueries creates EntityQuery, precomputes
73+
// Frame 4+: system's query returns the entity, writes moveVector
4874
for (var frame = 0; frame < 10; frame++)
4975
yield return null;
5076

5177
IntegrationTestHarness.AssertNoJsErrors("character input setup");
52-
IntegrationTestHarness.AssertEntitiesFulfilled(em, entity);
5378

5479
// Inject movement input
5580
IntegrationTestHarness.SetTestInput(moveX: 1f, moveZ: 0.5f);
5681

57-
// Let the script tick with the input applied
82+
// Let the system tick with input applied
5883
for (var frame = 0; frame < 10; frame++)
5984
yield return null;
6085

@@ -79,30 +104,21 @@ public IEnumerator CharacterInput_Sprint_DrainsStamina()
79104

80105
yield return new EnterPlayMode();
81106

82-
using var searchPath = IntegrationTestHarness.UseSearchPath(compiledPath);
83-
var em = World.DefaultGameObjectInjectionWorld.EntityManager;
84-
var entity = IntegrationTestHarness.CreateScriptedEntity(
85-
em,
86-
"systems/character_input",
87-
ComponentType.ReadWrite<ECSCharacterControl>(),
88-
ComponentType.ReadWrite<ECSCharacterStats>(),
89-
ComponentType.ReadWrite<ECSCharacterState>(),
90-
ComponentType.ReadWrite<ECSCharacterFixedInput>()
91-
);
92-
em.SetComponentData(entity, ECSCharacterStats.Default());
107+
using var searchPath = IntegrationTestHarness.UseIsolatedSearchPath(compiledPath);
93108

94-
// Wait for fulfillment + query pipeline
109+
var world = World.DefaultGameObjectInjectionWorld;
110+
var em = world.EntityManager;
111+
var entity = CreateCharacterEntity(em);
112+
113+
// Wait for system discovery + query pipeline
95114
for (var frame = 0; frame < 10; frame++)
96115
yield return null;
97116

98-
IntegrationTestHarness.AssertEntitiesFulfilled(em, entity);
99-
100117
var initialStamina = em.GetComponentData<ECSCharacterStats>(entity).stamina;
101118

102119
// Inject sprint input
103120
IntegrationTestHarness.SetTestInput(moveX: 1f, sprint: true);
104121

105-
// Let the script tick with sprint active
106122
for (var frame = 0; frame < 10; frame++)
107123
yield return null;
108124

@@ -162,23 +178,5 @@ public IEnumerator MultiplePlayModeCycles_CharacterIntegration_Stable()
162178
yield return new ExitPlayMode();
163179
}
164180
}
165-
static unsafe string EvalString(JsRuntimeManager vm, string code)
166-
{
167-
var sourceBytes = System.Text.Encoding.UTF8.GetBytes(code + '\0');
168-
var fileBytes = System.Text.Encoding.UTF8.GetBytes("<diag>\0");
169-
fixed (byte* pSrc = sourceBytes, pFile = fileBytes)
170-
{
171-
var result = UnityJS.QJS.QJS.JS_Eval(vm.Context, pSrc, sourceBytes.Length - 1, pFile,
172-
UnityJS.QJS.QJS.JS_EVAL_TYPE_GLOBAL);
173-
if (UnityJS.QJS.QJS.IsException(result))
174-
{
175-
UnityJS.QJS.QJS.JS_FreeValue(vm.Context, UnityJS.QJS.QJS.JS_GetException(vm.Context));
176-
return "<exception>";
177-
}
178-
var str = UnityJS.QJS.QJS.ToManagedString(vm.Context, result);
179-
UnityJS.QJS.QJS.JS_FreeValue(vm.Context, result);
180-
return str ?? "<null>";
181-
}
182-
}
183181
}
184182
}

0 commit comments

Comments
 (0)