mirror of
https://github.com/xeghosted/OrbisForge.git
synced 2026-10-06 10:00:39 +02:00
Add xUnit tests for OrbisForge.Core, wire into CI
This commit is contained in:
1 parent
c465a18806
commit
baea90ea77
8 files changed
+431
No files matched your search
@@ -8,6 +8,20 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
unit-tests:
|
||||
name: Unit tests (OrbisForge.Core)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install .NET SDK
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '8.0.x'
|
||||
|
||||
- name: Run tests
|
||||
run: dotnet test tests/OrbisForge.Core.Tests --logger "console;verbosity=normal"
|
||||
|
||||
generate-and-build:
|
||||
name: Generate + build (${{ matrix.name }})
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -3,3 +3,4 @@ obj/
|
||||
publish/
|
||||
.vs/
|
||||
*.user
|
||||
TestResults/
|
||||
@@ -4,6 +4,9 @@
|
||||
<Project Path="src/OrbisForge.Core/OrbisForge.Core.csproj" />
|
||||
<Project Path="src/OrbisForge.Gui/OrbisForge.Gui.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/tests/">
|
||||
<Project Path="tests/OrbisForge.Core.Tests/OrbisForge.Core.Tests.csproj" />
|
||||
</Folder>
|
||||
<Project Path="external/UIFramework/src/UIFramework.Controls/UIFramework.Controls.csproj" />
|
||||
<Project Path="external/UIFramework/src/UIFramework.Core/UIFramework.Core.csproj" />
|
||||
</Solution>
|
||||
@@ -85,9 +85,21 @@ dotnet publish src/OrbisForge.Cli -c Release -r win-x64 --self-contained -p:Pu
|
||||
dotnet publish src/OrbisForge.Cli -c Release -r linux-x64 --self-contained -p:PublishSingleFile=true -o publish/linux-x64
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
```
|
||||
dotnet test tests/OrbisForge.Core.Tests
|
||||
```
|
||||
|
||||
Unit tests cover `OrbisForge.Core`'s scaffolding logic directly (file sets per target type,
|
||||
template content, sanitization, warnings) — fast, no toolchain required. CI's generate-and-build
|
||||
matrix is the complementary end-to-end layer: it proves the *output* actually compiles against
|
||||
the real OpenOrbis toolchain.
|
||||
|
||||
## Project layout
|
||||
|
||||
- `src/OrbisForge.Core` — scaffolding logic (multi-targeted `net8.0`/`netstandard2.0`)
|
||||
- `tests/OrbisForge.Core.Tests` — xUnit tests for Core
|
||||
- `src/OrbisForge.Cli` — cross-platform console scaffolder
|
||||
- `src/OrbisForge.Gui` — Windows wizard (WinForms + UIFramework)
|
||||
- `external/UIFramework` — git submodule
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||
<PackageReference Include="xunit" Version="2.9.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\OrbisForge.Core\OrbisForge.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,60 @@
|
||||
using OrbisForge.Core;
|
||||
using Xunit;
|
||||
|
||||
namespace OrbisForge.Core.Tests;
|
||||
|
||||
public class OrbisTargetTypeTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(OrbisTargetType.Exe, "EXE")]
|
||||
[InlineData(OrbisTargetType.Prx, "PRX")]
|
||||
[InlineData(OrbisTargetType.Sprx, "SPRX")]
|
||||
public void ToCMakeString_ReturnsExpectedToken(OrbisTargetType type, string expected)
|
||||
{
|
||||
Assert.Equal(expected, type.ToCMakeString());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(OrbisTargetType.Exe, false)]
|
||||
[InlineData(OrbisTargetType.Prx, true)]
|
||||
[InlineData(OrbisTargetType.Sprx, true)]
|
||||
public void IsLibrary_OnlyTrueForPrxAndSprx(OrbisTargetType type, bool expected)
|
||||
{
|
||||
Assert.Equal(expected, type.IsLibrary());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("exe", OrbisTargetType.Exe)]
|
||||
[InlineData("EXE", OrbisTargetType.Exe)]
|
||||
[InlineData("bin", OrbisTargetType.Exe)]
|
||||
[InlineData(" prx ", OrbisTargetType.Prx)]
|
||||
[InlineData("SPRX", OrbisTargetType.Sprx)]
|
||||
public void TryParse_AcceptsKnownAliasesCaseInsensitively(string text, OrbisTargetType expected)
|
||||
{
|
||||
Assert.True(OrbisTargetTypeExtensions.TryParse(text, out var type));
|
||||
Assert.Equal(expected, type);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData("elf")]
|
||||
[InlineData("dll")]
|
||||
public void TryParse_RejectsUnknownText(string text)
|
||||
{
|
||||
Assert.False(OrbisTargetTypeExtensions.TryParse(text, out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ArtifactRelativePath_Exe_IsEbootBin()
|
||||
{
|
||||
Assert.Equal("build/eboot.bin", OrbisTargetType.Exe.ArtifactRelativePath("anything"));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(OrbisTargetType.Prx, "my_lib", "build/my_lib.prx")]
|
||||
[InlineData(OrbisTargetType.Sprx, "my_plugin", "build/my_plugin.sprx")]
|
||||
public void ArtifactRelativePath_Libraries_UseNameInFilename(OrbisTargetType type, string name, string expected)
|
||||
{
|
||||
Assert.Equal(expected, type.ArtifactRelativePath(name));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
using System.Text.Json;
|
||||
using OrbisForge.Core;
|
||||
using Xunit;
|
||||
|
||||
namespace OrbisForge.Core.Tests;
|
||||
|
||||
// xUnit creates a fresh instance of this class per test method, so the constructor/Dispose
|
||||
// pair gives each test its own isolated temp directory without any shared-state risk.
|
||||
public class ProjectGeneratorTests : IDisposable
|
||||
{
|
||||
private readonly string _outputDir;
|
||||
|
||||
public ProjectGeneratorTests()
|
||||
{
|
||||
_outputDir = Path.Combine(Path.GetTempPath(), "orbisforge-tests", Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(_outputDir);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
try { Directory.Delete(_outputDir, recursive: true); } catch { /* best effort cleanup */ }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Generate_Exe_WritesMainCppAndCoreFiles()
|
||||
{
|
||||
var result = ProjectGenerator.Generate(new ProjectOptions
|
||||
{
|
||||
Name = "my_exe",
|
||||
Type = OrbisTargetType.Exe,
|
||||
OutputDirectory = _outputDir,
|
||||
});
|
||||
|
||||
Assert.Equal(Path.Combine(_outputDir, "my_exe"), result.RootDirectory);
|
||||
AssertFileExists(result, "src/main.cpp");
|
||||
AssertFileExists(result, "CMakeLists.txt");
|
||||
AssertFileExists(result, "CMakePresets.json");
|
||||
AssertFileExists(result, "cmake/oo-ps4-toolchain.cmake");
|
||||
AssertFileExists(result, "cmake/OrbisTargets.cmake");
|
||||
AssertFileExists(result, "build.bat");
|
||||
AssertFileExists(result, "build.sh");
|
||||
AssertFileExists(result, ".vscode/tasks.json");
|
||||
AssertFileExists(result, ".vscode/c_cpp_properties.json");
|
||||
AssertFileExists(result, ".gitignore");
|
||||
AssertFileExists(result, "README.md");
|
||||
|
||||
AssertFileMissing(result, "src/my_exe.cpp");
|
||||
AssertFileMissing(result, "deploy.bat");
|
||||
AssertFileMissing(result, "sce_sys/icon0.png");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(OrbisTargetType.Prx)]
|
||||
[InlineData(OrbisTargetType.Sprx)]
|
||||
public void Generate_Library_WritesNamedSourceAndHeaderInsteadOfMain(OrbisTargetType type)
|
||||
{
|
||||
var result = ProjectGenerator.Generate(new ProjectOptions
|
||||
{
|
||||
Name = "my_lib",
|
||||
Type = type,
|
||||
OutputDirectory = _outputDir,
|
||||
});
|
||||
|
||||
AssertFileExists(result, "src/my_lib.cpp");
|
||||
AssertFileExists(result, "src/my_lib.h");
|
||||
AssertFileMissing(result, "src/main.cpp");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(OrbisTargetType.Exe, "TYPE EXE")]
|
||||
[InlineData(OrbisTargetType.Prx, "TYPE PRX")]
|
||||
[InlineData(OrbisTargetType.Sprx, "TYPE SPRX")]
|
||||
public void Generate_CMakeLists_DeclaresCorrectType(OrbisTargetType type, string expectedSnippet)
|
||||
{
|
||||
var result = ProjectGenerator.Generate(new ProjectOptions
|
||||
{
|
||||
Name = "typed_project",
|
||||
Type = type,
|
||||
OutputDirectory = _outputDir,
|
||||
});
|
||||
|
||||
var content = ReadFile(result, "CMakeLists.txt");
|
||||
Assert.Contains(expectedSnippet, content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Generate_WithPtypeOverride_FlowsIntoCMakeLists()
|
||||
{
|
||||
var result = ProjectGenerator.Generate(new ProjectOptions
|
||||
{
|
||||
Name = "custom_ptype",
|
||||
Type = OrbisTargetType.Sprx,
|
||||
OutputDirectory = _outputDir,
|
||||
PType = "secure_module",
|
||||
});
|
||||
|
||||
var content = ReadFile(result, "CMakeLists.txt");
|
||||
Assert.Contains("PTYPE secure_module", content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Generate_WithoutPkg_DoesNotAddPackagingBlockOrAssets()
|
||||
{
|
||||
var result = ProjectGenerator.Generate(new ProjectOptions
|
||||
{
|
||||
Name = "no_pkg",
|
||||
Type = OrbisTargetType.Exe,
|
||||
OutputDirectory = _outputDir,
|
||||
IncludePackaging = false,
|
||||
});
|
||||
|
||||
var content = ReadFile(result, "CMakeLists.txt");
|
||||
Assert.DoesNotContain("add_orbis_package", content);
|
||||
AssertFileMissing(result, "sce_sys/icon0.png");
|
||||
Assert.Empty(result.Warnings);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Generate_PkgOnPrx_IsIgnoredByGenerator()
|
||||
{
|
||||
// ProjectGenerator only wires packaging for EXE targets even if the flag is set --
|
||||
// the CLI/GUI are expected to reject this combination before calling Generate, but
|
||||
// Generate itself should not blow up or silently produce a broken PRX+pkg mix.
|
||||
var result = ProjectGenerator.Generate(new ProjectOptions
|
||||
{
|
||||
Name = "prx_with_pkg_flag",
|
||||
Type = OrbisTargetType.Prx,
|
||||
OutputDirectory = _outputDir,
|
||||
IncludePackaging = true,
|
||||
});
|
||||
|
||||
AssertFileMissing(result, "sce_sys/icon0.png");
|
||||
var content = ReadFile(result, "CMakeLists.txt");
|
||||
Assert.DoesNotContain("add_orbis_package", content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Generate_PkgWithMissingToolchain_WarnsPerAssetAndStillWritesOtherFiles()
|
||||
{
|
||||
var bogusToolchain = Path.Combine(_outputDir, "does-not-exist");
|
||||
|
||||
var result = ProjectGenerator.Generate(new ProjectOptions
|
||||
{
|
||||
Name = "pkg_project",
|
||||
Type = OrbisTargetType.Exe,
|
||||
OutputDirectory = _outputDir,
|
||||
IncludePackaging = true,
|
||||
ToolchainPathOverride = bogusToolchain,
|
||||
});
|
||||
|
||||
Assert.Equal(4, result.Warnings.Count); // icon0.png, right.sprx, libSceFios2.prx, libc.prx
|
||||
Assert.All(result.Warnings, w => Assert.Contains("not found, skipped", w));
|
||||
|
||||
var content = ReadFile(result, "CMakeLists.txt");
|
||||
Assert.Contains("add_orbis_package", content);
|
||||
AssertFileExists(result, "src/main.cpp"); // rest of generation still completed
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Generate_WithDeployHost_WritesDeployScriptsWithConfiguredValues()
|
||||
{
|
||||
var result = ProjectGenerator.Generate(new ProjectOptions
|
||||
{
|
||||
Name = "deploy_me",
|
||||
Type = OrbisTargetType.Exe,
|
||||
OutputDirectory = _outputDir,
|
||||
DeployHost = "192.168.1.50",
|
||||
DeployPort = 2121,
|
||||
DeployRemotePath = "/data/homebrew/deploy_me/",
|
||||
});
|
||||
|
||||
AssertFileExists(result, "deploy.bat");
|
||||
AssertFileExists(result, "deploy.sh");
|
||||
|
||||
var bat = ReadFile(result, "deploy.bat");
|
||||
Assert.Contains("ftp://192.168.1.50:2121/data/homebrew/deploy_me/", bat);
|
||||
Assert.Contains(@"build\eboot.bin", bat);
|
||||
|
||||
var sh = ReadFile(result, "deploy.sh");
|
||||
Assert.Contains("ftp://192.168.1.50:2121/data/homebrew/deploy_me/", sh);
|
||||
Assert.Contains("build/eboot.bin", sh);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Generate_WithoutDeployHost_DoesNotWriteDeployScripts()
|
||||
{
|
||||
var result = ProjectGenerator.Generate(new ProjectOptions
|
||||
{
|
||||
Name = "no_deploy",
|
||||
Type = OrbisTargetType.Exe,
|
||||
OutputDirectory = _outputDir,
|
||||
});
|
||||
|
||||
AssertFileMissing(result, "deploy.bat");
|
||||
AssertFileMissing(result, "deploy.sh");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Generate_GitignoreExcludesBuildDirectory()
|
||||
{
|
||||
var result = ProjectGenerator.Generate(new ProjectOptions
|
||||
{
|
||||
Name = "gi_project",
|
||||
Type = OrbisTargetType.Exe,
|
||||
OutputDirectory = _outputDir,
|
||||
});
|
||||
|
||||
var content = ReadFile(result, ".gitignore");
|
||||
Assert.Contains("build/", content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Generate_VsCodeFiles_AreValidJson()
|
||||
{
|
||||
var result = ProjectGenerator.Generate(new ProjectOptions
|
||||
{
|
||||
Name = "vscode_project",
|
||||
Type = OrbisTargetType.Exe,
|
||||
OutputDirectory = _outputDir,
|
||||
});
|
||||
|
||||
// JsonDocument.Parse throws on malformed JSON -- that's the assertion.
|
||||
using var tasks = JsonDocument.Parse(ReadFile(result, ".vscode/tasks.json"));
|
||||
using var cpp = JsonDocument.Parse(ReadFile(result, ".vscode/c_cpp_properties.json"));
|
||||
|
||||
Assert.True(tasks.RootElement.TryGetProperty("tasks", out _));
|
||||
Assert.True(cpp.RootElement.TryGetProperty("configurations", out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Generate_SanitizesNameForDirectoryAndCMakeTarget()
|
||||
{
|
||||
var result = ProjectGenerator.Generate(new ProjectOptions
|
||||
{
|
||||
Name = "my cool game!",
|
||||
Type = OrbisTargetType.Exe,
|
||||
OutputDirectory = _outputDir,
|
||||
});
|
||||
|
||||
Assert.Equal(Path.Combine(_outputDir, "my_cool_game_"), result.RootDirectory);
|
||||
var content = ReadFile(result, "CMakeLists.txt");
|
||||
Assert.Contains("project(my_cool_game_ CXX)", content);
|
||||
// The human-readable name is preserved verbatim elsewhere (e.g. the printf greeting).
|
||||
Assert.Contains("my cool game!", ReadFile(result, "src/main.cpp"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Generate_FilesWrittenList_MatchesActualFilesOnDisk()
|
||||
{
|
||||
var result = ProjectGenerator.Generate(new ProjectOptions
|
||||
{
|
||||
Name = "verify_list",
|
||||
Type = OrbisTargetType.Sprx,
|
||||
OutputDirectory = _outputDir,
|
||||
DeployHost = "10.0.0.1",
|
||||
});
|
||||
|
||||
Assert.NotEmpty(result.FilesWritten);
|
||||
Assert.All(result.FilesWritten, f => Assert.True(File.Exists(f), $"Listed but missing on disk: {f}"));
|
||||
}
|
||||
|
||||
private static void AssertFileExists(GeneratedProject result, string relativePath)
|
||||
{
|
||||
var full = Path.Combine(result.RootDirectory, relativePath.Replace('/', Path.DirectorySeparatorChar));
|
||||
Assert.True(File.Exists(full), $"Expected file not found: {relativePath}");
|
||||
}
|
||||
|
||||
private static void AssertFileMissing(GeneratedProject result, string relativePath)
|
||||
{
|
||||
var full = Path.Combine(result.RootDirectory, relativePath.Replace('/', Path.DirectorySeparatorChar));
|
||||
Assert.False(File.Exists(full), $"Expected file NOT to exist: {relativePath}");
|
||||
}
|
||||
|
||||
private static string ReadFile(GeneratedProject result, string relativePath)
|
||||
{
|
||||
var full = Path.Combine(result.RootDirectory, relativePath.Replace('/', Path.DirectorySeparatorChar));
|
||||
return File.ReadAllText(full);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using OrbisForge.Core;
|
||||
using Xunit;
|
||||
|
||||
namespace OrbisForge.Core.Tests;
|
||||
|
||||
public class ProjectOptionsTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("my_project", "my_project")]
|
||||
[InlineData("my project", "my_project")]
|
||||
[InlineData(" spaced ", "spaced")]
|
||||
[InlineData("a/b\\c", "a_b_c")]
|
||||
[InlineData("kebab-case_2", "kebab-case_2")]
|
||||
[InlineData("", "orbis_project")]
|
||||
[InlineData(" ", "orbis_project")]
|
||||
[InlineData("!!!", "___")]
|
||||
public void SanitizedName_ReplacesNonAlphanumericSeparators(string input, string expected)
|
||||
{
|
||||
var options = new ProjectOptions { Name = input };
|
||||
Assert.Equal(expected, options.SanitizedName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SanitizedName_PreservesUnicodeLetters()
|
||||
{
|
||||
// char.IsLetterOrDigit is Unicode-aware, so accented letters pass through as-is
|
||||
// rather than getting collapsed to underscores -- documenting that, not just assuming it.
|
||||
var options = new ProjectOptions { Name = "héllo" };
|
||||
Assert.Equal("héllo", options.SanitizedName);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null, false)]
|
||||
[InlineData("", false)]
|
||||
[InlineData(" ", false)]
|
||||
[InlineData("192.168.1.50", true)]
|
||||
public void IncludeDeploy_TracksWhetherHostIsSet(string? host, bool expected)
|
||||
{
|
||||
var options = new ProjectOptions { Name = "x", DeployHost = host };
|
||||
Assert.Equal(expected, options.IncludeDeploy);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user