Skip to content

Commit 288af0f

Browse files
committed
chore: refactor CI tests
1 parent 34ad559 commit 288af0f

7 files changed

Lines changed: 332 additions & 257 deletions

.assets/ci/scripts/prepare-unity-sample-import-smoke.ps1

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,31 @@ function Get-RequiredString {
9191
return [string]$property.Value
9292
}
9393

94+
function Write-Utf8NoBomFile {
95+
param(
96+
[string]$Path,
97+
[AllowEmptyString()]
98+
[string]$Content
99+
)
100+
101+
$encoding = [System.Text.UTF8Encoding]::new($false)
102+
[System.IO.File]::WriteAllText($Path, $Content, $encoding)
103+
}
104+
105+
function ConvertTo-JsonText {
106+
param(
107+
[Parameter(ValueFromPipeline = $true)]
108+
[object]$InputObject,
109+
110+
[int]$Depth = 2
111+
)
112+
113+
process {
114+
$json = $InputObject | ConvertTo-Json -Depth $Depth
115+
return ($json -join [System.Environment]::NewLine) + [System.Environment]::NewLine
116+
}
117+
}
118+
94119
function ConvertTo-GitFilePackageUrl {
95120
param(
96121
[string]$Path,
@@ -192,17 +217,17 @@ $manifest = [ordered]@{
192217
-LocalPackageUrl $packageUrl
193218
}
194219

195-
$manifest |
196-
ConvertTo-Json -Depth 8 |
197-
Set-Content -LiteralPath (Join-Path $resolvedProjectPath "Packages/manifest.json") -Encoding UTF8
220+
Write-Utf8NoBomFile `
221+
-Path (Join-Path $resolvedProjectPath "Packages/manifest.json") `
222+
-Content ($manifest | ConvertTo-JsonText -Depth 8)
198223

199224
$smokeConfig = [ordered]@{
200225
packageName = $PackageName
201226
expectedSampleAsmdef = $ExpectedSampleAsmdef
202227
}
203228

204-
$smokeConfig |
205-
ConvertTo-Json -Depth 4 |
206-
Set-Content -LiteralPath (Join-Path $resolvedProjectPath "Assets/GridForgeSampleImportSmokeConfig.json") -Encoding UTF8
229+
Write-Utf8NoBomFile `
230+
-Path (Join-Path $resolvedProjectPath "Assets/GridForgeSampleImportSmokeConfig.json") `
231+
-Content ($smokeConfig | ConvertTo-JsonText -Depth 4)
207232

208233
Write-Output "Created GridForge sample import smoke project at $resolvedProjectPath for $PackageName."

.assets/ci/unity-sample-import-smoke/Assets/Editor/GridForgeSampleImportSmokeBootstrap.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,15 @@ private static bool ImportSampleOnce()
3636
return false;
3737
}
3838

39+
var authoringSamplePath = $"Packages/{config.packageName}/Samples/GridforgeDemo";
40+
if (AssetDatabase.IsValidFolder(authoringSamplePath))
41+
{
42+
SessionState.SetString(ImportedPathKey, authoringSamplePath);
43+
SessionState.SetBool(ImportAttemptedKey, true);
44+
UnityEngine.Debug.Log($"GRIDFORGE_SAMPLE_SMOKE using authoring sample at {authoringSamplePath}");
45+
return true;
46+
}
47+
3948
var sample = Sample.FindByPackage(packageInfo.name, packageInfo.version)
4049
.FirstOrDefault(candidate => string.Equals(candidate.displayName, SampleDisplayName, StringComparison.Ordinal));
4150
if (string.IsNullOrEmpty(sample.displayName))

.assets/ci/unity-sample-import-smoke/Assets/Tests/EditMode/GridForgeSampleImportSmokeTests.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,19 @@ public void DemoSceneSampleWasImported()
1717
var importedPath = SessionState.GetString(ImportedPathKey, string.Empty);
1818
Assert.That(importedPath, Is.Not.Empty, "Sample import did not record an imported path.");
1919

20-
var importedAsmdefPath = Path.Combine(importedPath, config.expectedSampleAsmdef);
20+
var importedAsmdefPath = Path.Combine(ToAbsoluteProjectPath(importedPath), config.expectedSampleAsmdef);
2121
Assert.That(File.Exists(importedAsmdefPath), Is.True, $"Imported sample asmdef was not found: {importedAsmdefPath}");
2222
}
2323

24+
private static string ToAbsoluteProjectPath(string path)
25+
{
26+
if (Path.IsPathRooted(path))
27+
return path;
28+
29+
var projectRoot = Path.GetFullPath(Path.Combine(UnityEngine.Application.dataPath, ".."));
30+
return Path.Combine(projectRoot, path);
31+
}
32+
2433
private static SmokeConfig LoadConfig()
2534
{
2635
var configPath = Path.Combine(UnityEngine.Application.dataPath, ConfigAssetPath);
Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
using System;
2+
using System.IO;
3+
using System.Linq;
4+
using System.Reflection;
5+
using NUnit.Framework;
6+
using UnityEditor;
7+
using UnityEngine;
8+
9+
public sealed class GridForgeSampleWorkflowSmokeTests
10+
{
11+
private const string ConfigAssetPath = "GridForgeSampleImportSmokeConfig.json";
12+
private const string ImportedPathKey = "GridForge.SampleImportSmoke.ImportedPath";
13+
private const int RectangularPrism = 0;
14+
private const int HexPrism = 1;
15+
private const int Dense = 0;
16+
private const int Sparse = 1;
17+
private const int PhysicalAndMissing = 1;
18+
19+
[Test]
20+
public void SceneGridManagerExposeWorkflowChoices()
21+
{
22+
var sampleAssemblyName = Path.GetFileNameWithoutExtension(LoadConfig().expectedSampleAsmdef);
23+
Type workflowType = FindType(sampleAssemblyName, "GridForgeSampleWorkflow");
24+
Type managerType = FindType(sampleAssemblyName, "SceneGridManager");
25+
26+
CollectionAssert.AreEqual(
27+
new[]
28+
{
29+
"DenseRectangular",
30+
"DenseHex",
31+
"SparseRectangular",
32+
"SparseHex",
33+
"MixedTopologyDiagnostics"
34+
},
35+
Enum.GetNames(workflowType));
36+
37+
Assert.NotNull(managerType.GetProperty("Workflow", BindingFlags.Instance | BindingFlags.Public));
38+
Assert.NotNull(managerType.GetMethod("ApplyAuthoringToWorld", BindingFlags.Instance | BindingFlags.Public));
39+
}
40+
41+
[Test]
42+
public void WorkflowPrefabsUseAuthoringComponents()
43+
{
44+
string prefabRoot = $"{GetSampleAssetRoot()}/Prefabs";
45+
46+
AssertWorkflowConfig(
47+
$"{prefabRoot}/DenseRectangular.prefab",
48+
workflowIndex: 0,
49+
expectedGridCount: 1,
50+
expectedTopologyKind: RectangularPrism,
51+
expectedStorageKind: Dense,
52+
expectedSparseCount: 0);
53+
54+
AssertWorkflowConfig(
55+
$"{prefabRoot}/DenseHex.prefab",
56+
workflowIndex: 1,
57+
expectedGridCount: 1,
58+
expectedTopologyKind: HexPrism,
59+
expectedStorageKind: Dense,
60+
expectedSparseCount: 0);
61+
62+
AssertWorkflowConfig(
63+
$"{prefabRoot}/SparseRectangular.prefab",
64+
workflowIndex: 2,
65+
expectedGridCount: 1,
66+
expectedTopologyKind: RectangularPrism,
67+
expectedStorageKind: Sparse,
68+
expectedSparseCount: 5);
69+
70+
AssertWorkflowConfig(
71+
$"{prefabRoot}/SparseHex.prefab",
72+
workflowIndex: 3,
73+
expectedGridCount: 1,
74+
expectedTopologyKind: HexPrism,
75+
expectedStorageKind: Sparse,
76+
expectedSparseCount: 5);
77+
78+
AssertWorkflowConfig(
79+
$"{prefabRoot}/MixedTopologyDiagnostics.prefab",
80+
workflowIndex: 4,
81+
expectedGridCount: 4,
82+
expectedTopologyKind: null,
83+
expectedStorageKind: null,
84+
expectedSparseCount: null);
85+
}
86+
87+
[Test]
88+
public void MixedDiagnosticsPrefabShowsPhysicalAndMissingSparseCells()
89+
{
90+
GameObject prefab = AssertAssetExists<GameObject>(
91+
$"{GetSampleAssetRoot()}/Prefabs/MixedTopologyDiagnostics.prefab");
92+
93+
Component debugger = AssertComponent(prefab, "GridForge.Utility.GridDebugger");
94+
SerializedObject serializedDebugger = new(debugger);
95+
96+
Assert.IsTrue(serializedDebugger.FindProperty("_showGrid").boolValue);
97+
Assert.AreEqual(PhysicalAndMissing, serializedDebugger.FindProperty("_addressMode").enumValueIndex);
98+
Assert.IsTrue(serializedDebugger.FindProperty("_limitQueryBounds").boolValue);
99+
Assert.AreEqual(512, serializedDebugger.FindProperty("_maxCells").intValue);
100+
101+
AssertComponent(prefab, "GridForge.Utility.GridTracerTests");
102+
AssertComponent(prefab, "GridForge.Utility.GridForgeUnityLogger");
103+
}
104+
105+
private static void AssertWorkflowConfig(
106+
string prefabPath,
107+
int workflowIndex,
108+
int expectedGridCount,
109+
int? expectedTopologyKind,
110+
int? expectedStorageKind,
111+
int? expectedSparseCount)
112+
{
113+
GameObject prefab = AssertAssetExists<GameObject>(prefabPath);
114+
Component manager = AssertComponent(prefab, "SceneGridManager");
115+
Component saver = AssertComponent(prefab, "GridForge.Configuration.GridConfigurationSaver");
116+
AssertComponent(prefab, "GridForge.Unity.GridWorldComponent");
117+
118+
SerializedObject serializedManager = new(manager);
119+
Assert.AreEqual(workflowIndex, serializedManager.FindProperty("_workflow").enumValueIndex);
120+
121+
object configs = GetPropertyValue(saver, "SavedGridConfigurations");
122+
Assert.AreEqual(expectedGridCount, GetCount(configs));
123+
124+
if (!expectedTopologyKind.HasValue || !expectedStorageKind.HasValue)
125+
return;
126+
127+
object config = GetIndexedValue(configs, 0);
128+
Assert.AreEqual(expectedTopologyKind.Value, ToEnumIndex(GetPropertyValue(config, "TopologyKind")));
129+
Assert.AreEqual(expectedStorageKind.Value, ToEnumIndex(GetPropertyValue(config, "StorageKind")));
130+
131+
object sparseVoxels = GetPropertyValue(config, "ConfiguredSparseVoxels");
132+
object sparseIndices = GetPropertyValue(sparseVoxels, "Indices");
133+
Assert.AreEqual(expectedSparseCount.GetValueOrDefault(), GetCount(sparseIndices));
134+
}
135+
136+
private static T AssertAssetExists<T>(string assetPath)
137+
where T : UnityEngine.Object
138+
{
139+
T asset = AssetDatabase.LoadAssetAtPath<T>(assetPath);
140+
Assert.NotNull(asset, $"Missing sample asset: {assetPath}");
141+
return asset;
142+
}
143+
144+
private static string GetSampleAssetRoot()
145+
{
146+
string importedPath = SessionState.GetString(ImportedPathKey, string.Empty);
147+
Assert.That(importedPath, Is.Not.Empty, "Sample import did not record an imported path.");
148+
149+
string assetPath = ToAssetDatabasePath(importedPath);
150+
Assert.That(AssetDatabase.IsValidFolder(assetPath), Is.True, $"Imported sample folder was not found: {assetPath}");
151+
return assetPath;
152+
}
153+
154+
private static string ToAssetDatabasePath(string path)
155+
{
156+
string normalizedPath = path.Replace('\\', '/');
157+
if (!Path.IsPathRooted(normalizedPath))
158+
return normalizedPath;
159+
160+
string projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, ".."))
161+
.Replace('\\', '/')
162+
.TrimEnd('/') + "/";
163+
string fullPath = Path.GetFullPath(path).Replace('\\', '/');
164+
165+
if (fullPath.StartsWith(projectRoot, StringComparison.OrdinalIgnoreCase))
166+
return fullPath.Substring(projectRoot.Length);
167+
168+
return normalizedPath;
169+
}
170+
171+
private static Component AssertComponent(GameObject owner, string fullNameOrName)
172+
{
173+
Component component = owner
174+
.GetComponents<Component>()
175+
.FirstOrDefault(candidate =>
176+
candidate != null &&
177+
(candidate.GetType().FullName == fullNameOrName ||
178+
candidate.GetType().Name == fullNameOrName));
179+
180+
Assert.NotNull(component, $"{owner.name} is missing {fullNameOrName}.");
181+
return component;
182+
}
183+
184+
private static Type FindType(string assemblyName, string typeName)
185+
{
186+
Assembly assembly = AppDomain.CurrentDomain
187+
.GetAssemblies()
188+
.FirstOrDefault(candidate => candidate.GetName().Name == assemblyName);
189+
190+
Assert.NotNull(assembly, $"Unable to find loaded assembly {assemblyName}.");
191+
192+
Type type = assembly.GetTypes().FirstOrDefault(candidate => candidate.Name == typeName);
193+
Assert.NotNull(type, $"Unable to find {typeName} in {assemblyName}.");
194+
return type;
195+
}
196+
197+
private static object GetPropertyValue(object owner, string propertyName)
198+
{
199+
Type type = owner.GetType();
200+
PropertyInfo property = type.GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public);
201+
Assert.NotNull(property, $"{type.FullName} should expose {propertyName}.");
202+
return property.GetValue(owner);
203+
}
204+
205+
private static object GetIndexedValue(object owner, int index)
206+
{
207+
if (owner is Array array)
208+
return array.GetValue(index);
209+
210+
Type type = owner.GetType();
211+
PropertyInfo indexer = type.GetProperty("Item", BindingFlags.Instance | BindingFlags.Public);
212+
Assert.NotNull(indexer, $"{type.FullName} should expose an integer indexer.");
213+
return indexer.GetValue(owner, new object[] { index });
214+
}
215+
216+
private static int GetCount(object owner)
217+
{
218+
if (owner is Array array)
219+
return array.Length;
220+
221+
Type type = owner.GetType();
222+
PropertyInfo count = type.GetProperty("Count", BindingFlags.Instance | BindingFlags.Public);
223+
Assert.NotNull(count, $"{type.FullName} should expose Count.");
224+
return (int)count.GetValue(owner);
225+
}
226+
227+
private static int ToEnumIndex(object enumValue)
228+
{
229+
return Convert.ToInt32(enumValue);
230+
}
231+
232+
private static SmokeConfig LoadConfig()
233+
{
234+
string configPath = Path.Combine(Application.dataPath, ConfigAssetPath);
235+
Assert.That(File.Exists(configPath), Is.True, $"Sample import smoke config was not found: {configPath}");
236+
237+
SmokeConfig config = JsonUtility.FromJson<SmokeConfig>(File.ReadAllText(configPath));
238+
Assert.That(config, Is.Not.Null, $"Unable to parse sample import smoke config: {configPath}");
239+
Assert.That(config.expectedSampleAsmdef, Is.Not.Empty, $"Config must define expectedSampleAsmdef: {ConfigAssetPath}");
240+
return config;
241+
}
242+
243+
[Serializable]
244+
private sealed class SmokeConfig
245+
{
246+
public string expectedSampleAsmdef;
247+
}
248+
}

0 commit comments

Comments
 (0)