forked from PassivePicasso/ThunderKit
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathImportProjectSettingsTests.cs
More file actions
200 lines (180 loc) · 9.81 KB
/
Copy pathImportProjectSettingsTests.cs
File metadata and controls
200 lines (180 loc) · 9.81 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using NUnit.Framework;
using ThunderKit.Core.Config.Common;
using ThunderKit.Core.Utilities;
using UnityEditor;
using UnityEngine;
namespace ThunderKitTests
{
// Tier B-ish — ProjectSettings import (real export, generated fixtures).
//
// Drives ImportProjectSettings.ExportProjectSettings against a per-version
// globalgamemanagers and the committed AssetRipper class data (classdata.tpk),
// exercising the full version-resolution + YAML export path. Deterministic and offline
// (no network, no AssetDatabase mutation), so it runs in CI across the whole Unity
// editor matrix rather than being tagged [Category("Integration")].
//
// Cross-version contract, split by what is guaranteed:
// * On EVERY editor: resolving a class database from the tpk for the running Unity
// version and loading the game's globalgamemanagers must succeed (no
// UnsupportedClassDataException); any files produced must be valid. This proves the
// tpk acquisition/closest-version selection works on each matrix editor.
// * On a MATCHING editor (running major.minor == a fixture's): the core settings must
// actually export. ThunderKit assumes the importing editor matches the game's build
// version, so full behavioral correctness is asserted only there.
//
// Fixtures are auto-discovered under Fixtures/GlobalGameManagers/<unity-version>/ and
// are produced by the generate-project-settings-fixtures workflow — one
// globalgamemanagers built by each matrix editor, so every matrix job exercises an
// exact class-data match. See Fixtures/GlobalGameManagers/README.md.
[TestFixture]
public class ImportProjectSettingsTests
{
const string FixturesFolderSuffix = "/Tests/Editor/Fixtures";
// Settings present in every Unity version's globalgamemanagers as simple, stable
// value types. On an exact (or near) class-data match they always export, so they
// are the default "must export" set when a fixture does not pin its own
// expectedSettings. PlayerSettings (ProjectSettings) and GraphicsSettings are
// deliberately excluded: their layout shifts even across patch releases.
static readonly string[] CoreSettings =
{
"AudioManager", "TimeManager", "TagManager", "DynamicsManager", "QualitySettings",
};
[Serializable]
class FixtureManifest
{
public string unityVersion;
public string description;
public string[] expectedSettings;
}
// Absolute path to the Fixtures directory, located via the committed classdata.tpk
// so it works whether the package is embedded (Assets/Packages) or in the
// PackageCache. Null when the fixtures are absent (e.g. Git LFS not pulled).
static string FixturesRoot()
{
foreach (var guid in AssetDatabase.FindAssets("classdata"))
{
var assetPath = AssetDatabase.GUIDToAssetPath(guid);
if (assetPath.EndsWith($"{FixturesFolderSuffix}/classdata.tpk", StringComparison.OrdinalIgnoreCase))
return Path.GetDirectoryName(Path.GetFullPath(assetPath));
}
return null;
}
// One case per fixture folder that has both a globalgamemanagers and a fixture.json.
// Yields a single Ignored case when no fixtures are present so the suite stays green
// rather than erroring on an empty TestCaseSource.
static IEnumerable<TestCaseData> Fixtures()
{
var root = FixturesRoot();
var ggmRoot = root == null ? null : Path.Combine(root, "GlobalGameManagers");
var any = false;
if (ggmRoot != null && Directory.Exists(ggmRoot))
{
foreach (var dir in Directory.GetDirectories(ggmRoot))
{
var ggm = Path.Combine(dir, "globalgamemanagers");
var manifest = Path.Combine(dir, "fixture.json");
if (File.Exists(ggm) && File.Exists(manifest))
{
any = true;
yield return new TestCaseData(dir).SetName($"Export_{Path.GetFileName(dir)}");
}
}
}
if (!any)
yield return new TestCaseData((string)null)
.Ignore("No ProjectSettings fixtures found under Tests/Editor/Fixtures/GlobalGameManagers (Git LFS not pulled?).");
}
[Test, TestCaseSource(nameof(Fixtures))]
public void ExportProjectSettings_ResolvesAcrossVersions(string fixtureDir)
{
var root = FixturesRoot();
Assert.That(root, Is.Not.Null, "Fixtures root could not be located.");
var tpkPath = Path.Combine(root, "classdata.tpk");
Assert.That(File.Exists(tpkPath), Is.True, $"Missing class data fixture: {tpkPath}");
var globalGameManagersPath = Path.Combine(fixtureDir, "globalgamemanagers");
if (IsLfsPointer(tpkPath) || IsLfsPointer(globalGameManagersPath))
Assert.Ignore("Binary fixtures are unresolved Git LFS pointers; run `git lfs pull` to fetch them.");
var manifest = JsonUtility.FromJson<FixtureManifest>(File.ReadAllText(Path.Combine(fixtureDir, "fixture.json")));
var outputDirectory = Path.Combine(
Directory.GetCurrentDirectory(), "Temp",
$"ImportProjectSettingsTests_{Path.GetFileName(fixtureDir)}");
if (Directory.Exists(outputDirectory))
Directory.Delete(outputDirectory, true);
var importer = ScriptableObject.CreateInstance<ImportProjectSettings>();
List<string> written;
try
{
// Mirrors production: the export is driven by the *editor's* Unity version,
// which selects the closest class database the tpk provides.
written = importer.ExportProjectSettings(
tpkPath, globalGameManagersPath, outputDirectory, Application.unityVersion);
}
finally
{
UnityEngine.Object.DestroyImmediate(importer);
}
try
{
// Guaranteed on every editor: resolution + class DB load succeeded and the
// per-setting export never throws (failures are skipped, not raised).
Assert.That(written, Is.Not.Null, "Export returned null instead of a (possibly empty) file list.");
foreach (var file in written)
{
Assert.That(File.Exists(file), Is.True, $"Export reported '{file}' but it does not exist.");
Assert.That(new FileInfo(file).Length, Is.GreaterThan(0), $"Exported setting '{file}' is empty.");
var firstLine = File.ReadLines(file).FirstOrDefault() ?? string.Empty;
Assert.That(firstLine, Does.StartWith("%YAML").Or.StartWith("---"),
$"Exported setting '{file}' is not YAML (first line: '{firstLine}').");
}
var exportedNames = new HashSet<string>(
written.Select(Path.GetFileNameWithoutExtension), StringComparer.OrdinalIgnoreCase);
if (RunningEditorMatches(manifest.unityVersion))
{
// Same major.minor as the captured game: the export must be correct.
Assert.That(written.Count, Is.GreaterThan(0),
$"No settings exported on matching editor {Application.unityVersion}.");
var expectedSettings = manifest.expectedSettings != null && manifest.expectedSettings.Length > 0
? manifest.expectedSettings
: CoreSettings;
foreach (var expected in expectedSettings)
Assert.That(exportedNames, Contains.Item(expected),
$"Expected setting '{expected}' was not exported under matching editor {Application.unityVersion}. " +
$"Exported: {string.Join(", ", exportedNames)}.");
}
else
{
// Different version: type layouts may diverge, so some (or all) settings
// can be skipped. We only require that resolution succeeded and whatever
// was produced is valid (asserted above). Surface the count for triage.
Assert.Pass($"Editor {Application.unityVersion} differs from fixture {manifest.unityVersion}; " +
$"resolution succeeded, {written.Count} setting(s) exported.");
}
}
finally
{
if (Directory.Exists(outputDirectory))
Directory.Delete(outputDirectory, true);
}
}
// A Git LFS pointer is a small text stub beginning with the LFS version line.
// Real fixtures are multi-KB binaries, so a tiny file starting with that line
// means LFS content was never fetched.
static bool IsLfsPointer(string path)
{
if (!File.Exists(path) || new FileInfo(path).Length > 1024)
return false;
return (File.ReadLines(path).FirstOrDefault() ?? string.Empty)
.StartsWith("version https://git-lfs", StringComparison.Ordinal);
}
static bool RunningEditorMatches(string fixtureUnityVersion)
{
return ClassDataManager.TryParseUnityVersion(Application.unityVersion, out var em, out var en, out _)
&& ClassDataManager.TryParseUnityVersion(fixtureUnityVersion, out var fm, out var fn, out _)
&& em == fm && en == fn;
}
}
}