-
Notifications
You must be signed in to change notification settings - Fork 20
feat(launching): launch the native BGFX client against retail archives #332
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 19 commits
Commits
Show all changes
22 commits
Select commit
Hold shift + click to select a range
0ec4d28
feat(launching): surface native launch failures and set the loader se…
bobtista d0bbc6b
test(launching): verify the real native client launches through GenHub
bobtista 7083aa2
test(launching): serialize real-client launches around the engine ins…
bobtista ad273e7
feat(launching): reach retail archives via install-path environment i…
bobtista 602fd2f
fix(launching): stop setting DYLD_LIBRARY_PATH and LD_LIBRARY_PATH
bobtista caf278a
feat(launching): validate retail archive roots before spawning the en…
bobtista b41e5ee
fix(launching): capture complete stderr when a launch fails
bobtista 58429d5
chore: add stderr capture regression test and clear stack build warnings
bobtista 8120e5e
chore(workspace): order static helper before instance members
bobtista 3e40fa8
chore(workspace): remove trailing class separator
bobtista 461dcdb
chore: clear native launch style warnings
bobtista 08d5bac
chore(workspace): keep member order valid after the executable materi…
bobtista bdb0b19
fix(launching): scope archive-root validation to non-Windows and norm…
bobtista 5ac6b3a
test(launching): scope the archive-root rejection tests to non-Windows
bobtista 00a2bc6
fix(launching): scope archive validation to the launching game and re…
bobtista aeaa1c0
fix(launching): validate the base Generals root when launching Zero Hour
bobtista 7e6f8ce
docs(launching): correct why an absent Generals root is tolerated and…
bobtista 1da85e5
docs(launching): correct the engine-behaviour claim behind archive-ro…
bobtista 69460ea
test(launching): validate the fixture override and recognise versione…
bobtista 3eccb33
fix(launching): match retail archives case-insensitively when validat…
bobtista cf056e7
fix(launching): ground archive-root failure messages in the verified …
bobtista d1adeeb
chore(launching): reference launch constants in tests and fix stale n…
bobtista File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| namespace GenHub.Core.Constants; | ||
|
|
||
| /// <summary> | ||
| /// The contract between GenHub and a non-Windows engine build for locating retail archives. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// A non-Windows engine reads <c>InstallPath</c> through <c>GetStringFromRegistry</c>, which | ||
| /// on these platforms consults these variables before anything else, then mounts | ||
| /// <c>*.big</c> from those roots in addition to the working directory. The names are | ||
| /// therefore an external contract: changing one silently stops the engine finding content, | ||
| /// with no error from either side. | ||
| /// <para> | ||
| /// Windows resolves install paths from the registry and does not read these, so nothing is | ||
| /// set — and nothing should be validated against them — on that platform. | ||
| /// </para> | ||
| /// </remarks> | ||
| public static class RetailArchiveConstants | ||
| { | ||
| /// <summary>Environment variable naming the Zero Hour retail directory.</summary> | ||
| public const string ZeroHourInstallPathVariable = "CNC_ZH_INSTALLPATH"; | ||
|
|
||
| /// <summary>Environment variable naming the Generals retail directory.</summary> | ||
| public const string GeneralsInstallPathVariable = "CNC_GENERALS_INSTALLPATH"; | ||
|
|
||
| /// <summary> | ||
| /// Search pattern for the archives the engine mounts from a retail root. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// Used as an existence sentinel rather than matching a specific filename, which varies | ||
| /// by localisation and version. | ||
| /// </remarks> | ||
| public const string ArchiveSearchPattern = "*.big"; | ||
|
|
||
| /// <summary> | ||
| /// Every retail archive root variable, Zero Hour first. | ||
| /// </summary> | ||
| public static readonly string[] InstallPathVariables = | ||
| [ | ||
| ZeroHourInstallPathVariable, | ||
| GeneralsInstallPathVariable, | ||
| ]; | ||
| } |
104 changes: 104 additions & 0 deletions
104
GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/BoundedErrorBufferTests.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| using System.Reflection; | ||
| using GenHub.Features.GameProfiles.Infrastructure; | ||
| using Xunit; | ||
|
|
||
| namespace GenHub.Tests.Core.Features.GameProfiles; | ||
|
|
||
| /// <summary> | ||
| /// Tests for the bounded stderr capture used to explain a failed launch. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// The buffer is a private nested type; it is exercised through reflection rather than | ||
| /// being made public, because it is an implementation detail of process management and | ||
| /// its behaviour only matters through the diagnostics it produces. | ||
| /// </remarks> | ||
| public class BoundedErrorBufferTests | ||
| { | ||
| private static readonly Type BufferType = | ||
| typeof(GameProcessManager).GetNestedType("BoundedErrorBuffer", BindingFlags.NonPublic)!; | ||
|
|
||
| /// <summary> | ||
| /// The startup context is where the cause usually is, so the first lines must survive | ||
| /// even when far more output follows than the buffer retains. | ||
| /// </summary> | ||
| [Fact] | ||
| public void Append_RetainsBothTheHeadAndTheTail() | ||
| { | ||
| var buffer = CreateBuffer(); | ||
|
|
||
| Append(buffer, "dyld: library not loaded"); | ||
| for (var i = 0; i < 200; i++) | ||
| { | ||
| Append(buffer, $"noise line {i}"); | ||
| } | ||
|
|
||
| Append(buffer, "Abort trap: 6"); | ||
|
|
||
| var text = buffer.ToString()!; | ||
|
|
||
| Assert.Contains("dyld: library not loaded", text); | ||
| Assert.Contains("Abort trap: 6", text); | ||
| Assert.Contains("omitted", text); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// A single pathological line must not be retained in full. | ||
| /// </summary> | ||
| [Fact] | ||
| public void Append_TruncatesAnOverlongLine() | ||
| { | ||
| var buffer = CreateBuffer(); | ||
|
|
||
| Append(buffer, new string('x', 10_000)); | ||
|
|
||
| var text = buffer.ToString()!; | ||
|
|
||
| Assert.Contains("line truncated", text); | ||
| Assert.True(text.Length < 10_000, "The overlong line was retained in full."); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// A null line is the framework's end-of-stream signal, not content. | ||
| /// </summary> | ||
| [Fact] | ||
| public void Append_TreatsNullAsEndOfStreamRatherThanContent() | ||
| { | ||
| var buffer = CreateBuffer(); | ||
|
|
||
| Assert.False(EndOfStreamReached(buffer)); | ||
|
|
||
| Append(buffer, "something failed"); | ||
| Append(buffer, null); | ||
|
|
||
| Assert.True(EndOfStreamReached(buffer)); | ||
| Assert.Equal("something failed", buffer.ToString()); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Total retained output stays bounded regardless of how much arrives. | ||
| /// </summary> | ||
| [Fact] | ||
| public void Append_BoundsTotalRetainedOutput() | ||
| { | ||
| var buffer = CreateBuffer(); | ||
|
|
||
| for (var i = 0; i < 5_000; i++) | ||
| { | ||
| Append(buffer, new string('y', 500)); | ||
| } | ||
|
|
||
| Assert.True( | ||
| buffer.ToString()!.Length < 128 * 1024, | ||
| "Retained output grew beyond the cap."); | ||
| } | ||
|
|
||
| private static object CreateBuffer() => Activator.CreateInstance(BufferType, nonPublic: true)!; | ||
|
|
||
| private static void Append(object buffer, string? line) => | ||
| BufferType.GetMethod("Append", BindingFlags.NonPublic | BindingFlags.Instance)! | ||
| .Invoke(buffer, [line]); | ||
|
|
||
| private static bool EndOfStreamReached(object buffer) => | ||
| (bool)BufferType.GetProperty("EndOfStreamReached", BindingFlags.NonPublic | BindingFlags.Instance)! | ||
| .GetValue(buffer)!; | ||
| } |
74 changes: 74 additions & 0 deletions
74
GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/NativeClientFixture.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| using System.Linq; | ||
| using System.Runtime.InteropServices; | ||
|
|
||
| namespace GenHub.Tests.Core.Features.GameProfiles; | ||
|
|
||
| /// <summary> | ||
| /// Locates the local native client these integration tests launch. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// Shared by every test that spawns the real engine, so discovery stays in one place: all | ||
| /// of them skip unless a client is present, and diverging on how it is found would make | ||
| /// some skip while others fail. | ||
| /// </remarks> | ||
| public static class NativeClientFixture | ||
| { | ||
| /// <summary>Environment variable overriding the discovered directory.</summary> | ||
| public const string EnvironmentOverride = "GENHUB_NATIVE_CLIENT_DIR"; | ||
|
|
||
| /// <summary>The engine executable's filename.</summary> | ||
| public const string BinaryName = "generalszh"; | ||
|
|
||
| /// <summary> | ||
| /// Gets the native client directory, or <c>null</c> when these tests should skip. | ||
| /// </summary> | ||
| public static string? Directory | ||
| { | ||
| get | ||
| { | ||
| if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) | ||
| { | ||
| return null; | ||
| } | ||
|
|
||
| var configured = Environment.GetEnvironmentVariable(EnvironmentOverride); | ||
| if (!string.IsNullOrWhiteSpace(configured)) | ||
| { | ||
| // Validated the same way as the discovered default below. A directory that | ||
| // exists but holds no engine binary is a misconfigured override, and these | ||
| // tests should skip rather than fail on it. | ||
| return File.Exists(Path.Combine(configured, BinaryName)) ? configured : null; | ||
| } | ||
|
|
||
| var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); | ||
| var defaultDirectory = Path.Combine(home, "TheSuperHackers", "GeneralsZH"); | ||
|
|
||
| return File.Exists(Path.Combine(defaultDirectory, BinaryName)) ? defaultDirectory : null; | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Determines whether a filename is one of the engine's dynamic libraries. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// Covers macOS <c>.dylib</c> and both Linux forms: unversioned <c>.so</c> and versioned | ||
| /// <c>.so.0</c> / <c>.so.0.1.0</c>, which are the common shape of a shipped Linux library | ||
| /// and which a plain <c>.so</c> suffix test misses. | ||
| /// </remarks> | ||
| /// <param name="fileName">The filename to test.</param> | ||
| /// <returns><c>true</c> when the file is a dynamic library.</returns> | ||
| public static bool IsDynamicLibrary(string fileName) | ||
| { | ||
| if (fileName.EndsWith(".dylib", StringComparison.OrdinalIgnoreCase) | ||
| || fileName.EndsWith(".so", StringComparison.OrdinalIgnoreCase)) | ||
| { | ||
| return true; | ||
| } | ||
|
|
||
| // Matched on ".so." rather than ".so" so an earlier coincidental ".so" — inside | ||
| // ".sound", say — cannot shadow the real suffix and end the search early. | ||
| var versioned = fileName.IndexOf(".so.", StringComparison.OrdinalIgnoreCase); | ||
| return versioned >= 0 | ||
| && fileName[(versioned + 4)..].All(c => char.IsDigit(c) || c == '.'); | ||
| } | ||
| } | ||
43 changes: 43 additions & 0 deletions
43
GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/NativeClientFixtureTests.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| using GenHub.Tests.Core.Features.GameProfiles; | ||
| using Xunit; | ||
|
|
||
| namespace GenHub.Tests.Core.Features.GameProfiles; | ||
|
|
||
| /// <summary> | ||
| /// Tests for the shared native-client fixture helper. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// The integration tests that use it all skip without a local engine install, so the | ||
| /// library predicate would otherwise never be exercised on CI — including the versioned | ||
| /// Linux forms, which a plain ".so" suffix test silently misses. | ||
| /// </remarks> | ||
| public class NativeClientFixtureTests | ||
| { | ||
| /// <summary> | ||
| /// Recognises macOS and both Linux shapes, unversioned and versioned. | ||
| /// </summary> | ||
| /// <param name="fileName">The candidate filename.</param> | ||
| [Theory] | ||
| [InlineData("libSDL3.dylib")] | ||
| [InlineData("libSDL3.so")] | ||
| [InlineData("libSDL3.so.0")] | ||
| [InlineData("libSDL3.so.0.1.0")] | ||
| [InlineData("libstdc++.so.6")] | ||
| [InlineData("libavcodec.58.so.4")] | ||
| [InlineData("mylib.sound.so.1")] | ||
| public void IsDynamicLibrary_WithLibraryNames_ReturnsTrue(string fileName) | ||
| => Assert.True(NativeClientFixture.IsDynamicLibrary(fileName)); | ||
|
|
||
| /// <summary> | ||
| /// Rejects the engine binary, data files, and names that merely contain ".so". | ||
| /// </summary> | ||
| /// <param name="fileName">The candidate filename.</param> | ||
| [Theory] | ||
| [InlineData("generalszh")] | ||
| [InlineData("INIZH.big")] | ||
| [InlineData("readme.txt")] | ||
| [InlineData("resources.sound")] | ||
| [InlineData("libfoo.sox")] | ||
| public void IsDynamicLibrary_WithNonLibraryNames_ReturnsFalse(string fileName) | ||
| => Assert.False(NativeClientFixture.IsDynamicLibrary(fileName)); | ||
| } |
21 changes: 21 additions & 0 deletions
21
GenHub/GenHub.Tests/GenHub.Tests.Core/Features/GameProfiles/NativeClientLaunchCollection.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| using Xunit; | ||
|
|
||
| using System.Runtime.InteropServices; | ||
|
|
||
| namespace GenHub.Tests.Core.Features.GameProfiles; | ||
|
|
||
| /// <summary> | ||
| /// Serialises the tests that launch a real native game client. | ||
| /// <para> | ||
| /// The engine enforces a single running instance, so two of these in parallel produce a | ||
| /// spurious failure: the second launch is refused by the first. That is engine behaviour | ||
| /// rather than a test defect, and it has a product consequence — GenHub cannot run two | ||
| /// native profiles simultaneously. | ||
| /// </para> | ||
| /// </summary> | ||
| [CollectionDefinition(Name, DisableParallelization = true)] | ||
| public class NativeClientLaunchCollection | ||
| { | ||
| /// <summary>The xUnit collection name.</summary> | ||
| public const string Name = "Native client launch"; | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.