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, ".vscode/settings.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"); } [Fact] public void Generate_GoldHenPlugin_WritesPluginSourceWithoutHeaderOrMain() { var result = ProjectGenerator.Generate(new ProjectOptions { Name = "notify_plugin", Type = OrbisTargetType.GoldHenPlugin, OutputDirectory = _outputDir, }); AssertFileExists(result, "src/notify_plugin.cpp"); AssertFileMissing(result, "src/notify_plugin.h"); AssertFileMissing(result, "src/main.cpp"); var source = ReadFile(result, "src/notify_plugin.cpp"); // The loader reaches these through crtprx.o's _init, which resolves them by // their unmangled names -- without extern "C" the C++ compiler would mangle // them and the plugin would link but never load. Assert.Contains("extern \"C\"", source); Assert.Contains("int module_start(", source); Assert.Contains("int module_stop(", source); Assert.Contains("sceKernelSendNotificationRequest", source); } [Theory] [InlineData(OrbisTargetType.Exe, "TYPE EXE")] [InlineData(OrbisTargetType.Prx, "TYPE PRX")] [InlineData(OrbisTargetType.Sprx, "TYPE SPRX")] [InlineData(OrbisTargetType.GoldHenPlugin, "TYPE GHPLUGIN")] 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")); using var settings = JsonDocument.Parse(ReadFile(result, ".vscode/settings.json")); Assert.True(tasks.RootElement.TryGetProperty("tasks", out _)); Assert.True(cpp.RootElement.TryGetProperty("configurations", out _)); Assert.True(settings.RootElement.TryGetProperty("cmake.configureOnOpen", out var configureOnOpen)); Assert.False(configureOnOpen.GetBoolean()); } [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); } }