using System.Reflection; using System.Threading.Tasks; using Translator.Cli.Configuration; using Translator.Core.Mods; using Xunit; namespace Translator.Tests; public sealed class TranslateRecursiveRuntimeConfigTests { [Fact] public void UnsupportedOpcodeFailsStrictTranslation() { var root = Path.Combine(Path.GetTempPath(), $"translator-unsupported-opcode-{Guid.NewGuid():N}"); Directory.CreateDirectory(root); try { const uint entry = 0x80001000u; File.WriteAllBytes( Path.Combine(root, "main.dol"), SyntheticDolFactory.CreateBytes( entry, sections: [SyntheticDolFactory.Text( 0, entry, 0x04000000u, // unassigned primary opcode; deliberately unsupported by the lifter 0x4E800020u)])); // blr var projectPath = Path.Combine(root, "recomp.yml"); File.WriteAllText( projectPath, """ schema_version: 1 project: id: unsupported-opcode-test memory: sda_base: 0x80002000 sda2_base: 0x80003000 inputs: dol: path: main.dol translation: entry_points: [0x80001000] output: root: generated """); Assert.False(TranslationProjectConfig.Load(projectPath).Translation.AllowUnsupportedInstructions); var strictError = Assert.Throws(() => InvokeCli( "translate-recursive", $"0x{entry:X8}", "--project", projectPath, "--outdir", Path.Combine(root, "strict", "functions"), "--output-metadata", Path.Combine(root, "strict", "metadata.json"), "--threads", "1")); Assert.Contains("UNIMPLEMENTED", strictError.InnerException?.ToString(), StringComparison.Ordinal); } finally { if (Directory.Exists(root)) Directory.Delete(root, recursive: true); } } [Fact] public void RepeatedRunsReuseUnchangedOutputsAndPruneStaleFiles() { var root = Path.Combine(Path.GetTempPath(), $"translator-recursive-prune-{Guid.NewGuid():N}"); Directory.CreateDirectory(root); try { const uint entry = 0x80001000u; File.WriteAllBytes( Path.Combine(root, "main.dol"), SyntheticDolFactory.CreateBytes( entry, sections: [SyntheticDolFactory.Text( 0, entry, 0x60000000u, // nop 0x60000000u, // nop 0x4E800020u)])); // blr var projectPath = Path.Combine(root, "recomp.yml"); File.WriteAllText( projectPath, """ schema_version: 1 project: id: recursive-prune-test display_name: Recursive Prune Test memory: sda_base: 0x80002000 sda2_base: 0x80003000 inputs: dol: path: main.dol translation: entry_points: [0x80001000] output: root: generated """); var stagedFunctions = Path.Combine(root, "staging", "functions"); var stagedOutputMetadata = Path.Combine(root, "staging", "functions_metadata.json"); var exitCode = InvokeCli( "translate-recursive", $"0x{entry:X8}", "--project", projectPath, "--outdir", stagedFunctions, "--output-metadata", stagedOutputMetadata, "--threads", "1"); Assert.Equal(0, exitCode); var firstMetadata = BaseTranslationOutputMetadataFile.Read(stagedOutputMetadata); Assert.Null(firstMetadata.TranslationIdentityHash); Assert.Equal(0, firstMetadata.Quality.UnsupportedInstructionCount); Assert.Equal(0, firstMetadata.Quality.InvalidSsaFunctionCount); Assert.Single(firstMetadata.Functions); Assert.Single(Directory.GetFiles(stagedFunctions, "*.cpp")); var generatedPath = Directory.GetFiles(stagedFunctions, "*.cpp").Single(); var unchangedTimestamp = new DateTime(2004, 5, 6, 7, 8, 10, DateTimeKind.Utc); File.SetLastWriteTimeUtc(generatedPath, unchangedTimestamp); // A populated output tree is not an input to recursive discovery. // This stale filename used to become a synthetic function boundary // and truncate the second translation at entry + 4. File.WriteAllText(Path.Combine(stagedFunctions, $"func_{entry + 4:X8}.cpp"), "stale output hint"); var metadataTimestamp = new DateTime(2004, 5, 6, 7, 8, 12, DateTimeKind.Utc); File.SetLastWriteTimeUtc(stagedOutputMetadata, metadataTimestamp); var secondExitCode = InvokeCli( "translate-recursive", $"0x{entry:X8}", "--project", projectPath, "--outdir", stagedFunctions, "--output-metadata", stagedOutputMetadata, "--threads", "1"); Assert.Equal(0, secondExitCode); Assert.Equal(unchangedTimestamp, File.GetLastWriteTimeUtc(generatedPath)); Assert.Equal(metadataTimestamp, File.GetLastWriteTimeUtc(stagedOutputMetadata)); var stalePath = Path.Combine(stagedFunctions, "func_80001004.cpp"); var unlistedPath = Path.Combine(stagedFunctions, "unlisted.cpp"); File.WriteAllText(stalePath, "// stale"); File.WriteAllText(unlistedPath, "// not translator-owned"); BaseTranslationOutputMetadataFile.WriteIfChangedAtomic( stagedOutputMetadata, BaseTranslationOutputMetadata.Create( firstMetadata.Functions.Concat([ new BaseTranslationFunctionMetadata( "func_80001004.cpp", 8, new string('e', 64), 0x80001004u, []) ]), TranslationQualityMetadata.Clean)); var pruneExitCode = InvokeCli( "translate-recursive", $"0x{entry:X8}", "--project", projectPath, "--outdir", stagedFunctions, "--output-metadata", stagedOutputMetadata, "--prune-stale", "--threads", "1"); Assert.Equal(0, pruneExitCode); Assert.False(File.Exists(stalePath)); Assert.True(File.Exists(unlistedPath)); } finally { if (Directory.Exists(root)) { Directory.Delete(root, recursive: true); } } } private static int InvokeCli(params string[] args) { var entryPoint = typeof(TranslationProjectConfig).Assembly.EntryPoint; Assert.NotNull(entryPoint); var result = entryPoint!.Invoke(null, new object?[] { args }); return result switch { int exitCode => exitCode, Task intTask => intTask.GetAwaiter().GetResult(), Task task => CompleteTask(task), null => 0, _ => throw new InvalidOperationException($"Unexpected CLI entry point result type: {result.GetType().FullName}") }; } private static int CompleteTask(Task task) { task.GetAwaiter().GetResult(); return 0; } }