Files
wiicompiled/translator/tests/Translator.Tests/TranslateRecursiveRuntimeConfigTests.cs
T
patchzyy ec226e8348 init
2026-08-23 17:10:50 +02:00

211 lines
8.1 KiB
C#

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<TargetInvocationException>(() => 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<int> 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;
}
}