Files
sharpemu/tests/SharpEmu.SourceGenerators.Tests/SysAbiExportGeneratorTests.cs
T
2026-08-25 22:32:57 +02:00

187 lines
8.2 KiB
C#

// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using Xunit;
namespace SharpEmu.SourceGenerators.Tests;
public sealed class SysAbiExportGeneratorTests
{
private const string HandlerSource = """
using SharpEmu.HLE;
namespace TestExports;
public static class SampleExports
{
[SysAbiExport(Nid = "Zxa0VhQVTsk", ExportName = "sceKernelWaitSema", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
public static int WaitSema(CpuContext ctx) => 0;
// NID omitted on purpose: the generator must derive it from the name.
[SysAbiExport(ExportName = "sceKernelSignalSema", Target = Generation.Gen5)]
public static int SignalSema(CpuContext ctx) => 0;
// Parameterless handler shape: must be wrapped to the SysAbiFunction contract.
[SysAbiExport(Nid = "ekNvsT22rsY", ExportName = "sceAudioOutOpen")]
public static int Open() => 0;
// Typed handler shape: the generator emits the SysV register thunk.
[SysAbiExport(Nid = "12wOHk8ywb0", ExportName = "sceKernelPollSema")]
public static int PollSema(CpuContext ctx, uint handle, int needCount) => 0;
// All four integer kinds across all six argument registers.
[SysAbiExport(Nid = "4DM06U2BNEY", ExportName = "sceKernelCancelSema")]
public static int CancelSema(CpuContext ctx, uint a, int b, ulong c, long d, uint e, int f) => 0;
// Guest string marshalling: the thunk reads the pointer before the handler.
[SysAbiExport(Nid = "1G3lF1Gg1k8", ExportName = "sceKernelOpen")]
public static int KernelOpen(CpuContext ctx, [GuestCString(4096)] string path, int flags) => 0;
// A single fail-closed handler may back a catalog of LLE-preferred exports.
[SysAbiExport(Nid = "5fbPUzoA2fM", ExportName = "sceLleFirst", Target = Generation.Gen5, LibraryName = "libSceLle", PreferLle = true)]
[SysAbiExport(Nid = "L9NfM+f4f1Y", ExportName = "sceLleSecond", Target = Generation.Gen5, LibraryName = "libSceLle", PreferLle = true)]
public static int LleFallback(CpuContext ctx) => -1;
}
""";
[Fact]
public void GeneratedRegistryCompilesAgainstRealHleTypes()
{
var compilation = RoslynTestHost.Compile(HandlerSource);
var (updated, generated) = RoslynTestHost.RunGenerator(compilation);
Assert.NotEqual(string.Empty, generated);
RoslynTestHost.AssertCompiles(updated);
}
[Fact]
public void RegistryContainsDeclaredDerivedAndWrappedExports()
{
var (_, generated) = RoslynTestHost.RunGenerator(RoslynTestHost.Compile(HandlerSource));
// Declared NID passes through verbatim.
Assert.Contains("\"Zxa0VhQVTsk\"", generated, StringComparison.Ordinal);
Assert.Contains("global::TestExports.SampleExports.WaitSema", generated, StringComparison.Ordinal);
// Omitted NID is derived from the export name at compile time.
Assert.Contains("\"4czppHBiriw\"", generated, StringComparison.Ordinal);
// Parameterless handlers are adapted to the SysAbiFunction shape.
Assert.Contains("static _ => global::TestExports.SampleExports.Open()", generated, StringComparison.Ordinal);
}
[Fact]
public void TypedHandlersGetSysVRegisterThunks()
{
var (_, generated) = RoslynTestHost.RunGenerator(RoslynTestHost.Compile(HandlerSource));
// Parameters map positionally to RDI/RSI/... with the same unchecked-cast idiom
// hand-written handlers use; ulong reads the register raw.
Assert.Contains(
"static ctx => global::TestExports.SampleExports.PollSema(ctx, " +
"unchecked((uint)ctx[global::SharpEmu.HLE.CpuRegister.Rdi]), " +
"unchecked((int)ctx[global::SharpEmu.HLE.CpuRegister.Rsi]))",
generated,
StringComparison.Ordinal);
Assert.Contains(
"static ctx => global::TestExports.SampleExports.CancelSema(ctx, " +
"unchecked((uint)ctx[global::SharpEmu.HLE.CpuRegister.Rdi]), " +
"unchecked((int)ctx[global::SharpEmu.HLE.CpuRegister.Rsi]), " +
"ctx[global::SharpEmu.HLE.CpuRegister.Rdx], " +
"unchecked((long)ctx[global::SharpEmu.HLE.CpuRegister.Rcx]), " +
"unchecked((uint)ctx[global::SharpEmu.HLE.CpuRegister.R8]), " +
"unchecked((int)ctx[global::SharpEmu.HLE.CpuRegister.R9]))",
generated,
StringComparison.Ordinal);
}
[Fact]
public void GuestCStringParametersAreMarshalledWithAFaultPath()
{
var (_, generated) = RoslynTestHost.RunGenerator(RoslynTestHost.Compile(HandlerSource));
// The string is read from the pointer in RDI before the handler runs, and a
// failed read returns MEMORY_FAULT to the guest without invoking the handler.
Assert.Contains(
"if (!ctx.TryReadNullTerminatedUtf8(ctx[global::SharpEmu.HLE.CpuRegister.Rdi], 4096, out var guestString0))",
generated,
StringComparison.Ordinal);
Assert.Contains(
"return ctx.SetReturn(global::SharpEmu.HLE.OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);",
generated,
StringComparison.Ordinal);
Assert.Contains(
"return global::TestExports.SampleExports.KernelOpen(ctx, guestString0, " +
"unchecked((int)ctx[global::SharpEmu.HLE.CpuRegister.Rsi]));",
generated,
StringComparison.Ordinal);
}
[Fact]
public void GenerationFilteringMatchesTheReflectionScanSemantics()
{
var (_, generated) = RoslynTestHost.RunGenerator(RoslynTestHost.Compile(HandlerSource));
// The Add helper reproduces ResolveExportInfo: None inherits the registration
// generation, and exports outside the registration generation are skipped.
Assert.Contains("attributeTarget == global::SharpEmu.HLE.Generation.None ? registrationGeneration : attributeTarget", generated, StringComparison.Ordinal);
Assert.Contains("(target & registrationGeneration) == 0", generated, StringComparison.Ordinal);
}
[Fact]
public void MultipleLlePreferredAttributesShareOneFailClosedHandler()
{
var (_, generated) = RoslynTestHost.RunGenerator(RoslynTestHost.Compile(HandlerSource));
Assert.Contains("\"5fbPUzoA2fM\"", generated, StringComparison.Ordinal);
Assert.Contains("\"L9NfM+f4f1Y\"", generated, StringComparison.Ordinal);
Assert.Equal(
2,
generated.Split("global::TestExports.SampleExports.LleFallback", StringSplitOptions.None).Length - 1);
Assert.Contains(
", true, global::TestExports.SampleExports.LleFallback",
generated,
StringComparison.Ordinal);
}
[Fact]
public void AssemblyWithoutExportsEmitsNoRegistry()
{
// Referencing the analyzer must not mint a colliding
// SharpEmu.Generated.SysAbiExportRegistry type in export-free assemblies.
const string noExports = """
public static class PlainCode
{
public static int Nothing() => 0;
}
""";
var (_, generated) = RoslynTestHost.RunGenerator(RoslynTestHost.Compile(noExports));
Assert.Equal(string.Empty, generated);
}
[Fact]
public void InvalidHandlersAreSkippedNotEmitted()
{
const string invalid = """
using SharpEmu.HLE;
namespace TestExports;
public static class BrokenExports
{
[SysAbiExport(ExportName = "sceKernelUsleep")]
public static long WrongReturn(CpuContext ctx) => 0;
[SysAbiExport(ExportName = "sceKernelGettimeofday")]
private static int Inaccessible(CpuContext ctx) => 0;
}
""";
var (updated, generated) = RoslynTestHost.RunGenerator(RoslynTestHost.Compile(invalid));
Assert.DoesNotContain("WrongReturn", generated, StringComparison.Ordinal);
Assert.DoesNotContain("Inaccessible", generated, StringComparison.Ordinal);
RoslynTestHost.AssertCompiles(updated);
}
}