[Tests] Cover controller ports, user exports, trials and open-file renames

Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
This commit is contained in:
ParantezTech 2026-10-08 18:25:29 +03:00
parent f945215935
commit 9d530f63f9
4 changed files with 273 additions and 3 deletions

View File

@ -0,0 +1,102 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using SharpEmu.Libs.Kernel;
using Xunit;
namespace SharpEmu.Libs.Tests.Kernel;
[Collection(KernelMemoryCompatStateCollection.Name)]
public sealed class KernelFileRenameTests
{
private const ulong MemoryBase = 0x1_0000_0000;
private const ulong SourcePathAddress = MemoryBase + 0x100;
private const ulong DestinationPathAddress = MemoryBase + 0x300;
private const ulong ReadBufferAddress = MemoryBase + 0x500;
[Fact]
public void KernelRename_ReplacesDestinationWhileSourceDescriptorRemainsOpen()
{
var identity = Guid.NewGuid().ToString("N");
var mountPoint = $"/rename-{identity}";
var hostRoot = Path.Combine(Path.GetTempPath(), $"sharpemu-rename-{identity}");
var sourceHostPath = Path.Combine(hostRoot, "STEMP000.DAT");
var destinationHostPath = Path.Combine(hostRoot, "SDATA000.DAT");
var sourceGuestPath = $"{mountPoint}/STEMP000.DAT";
var destinationGuestPath = $"{mountPoint}/SDATA000.DAT";
byte[] payload = [0x10, 0x20, 0x30, 0x40];
var fileDescriptor = -1;
Directory.CreateDirectory(hostRoot);
File.WriteAllBytes(sourceHostPath, payload);
File.WriteAllBytes(destinationHostPath, [0xFF]);
KernelMemoryCompatExports.RegisterGuestPathMount(mountPoint, hostRoot);
var memory = new FakeCpuMemory(MemoryBase, 0x1000);
var context = new CpuContext(memory, Generation.Gen5);
memory.WriteCString(SourcePathAddress, sourceGuestPath);
memory.WriteCString(DestinationPathAddress, destinationGuestPath);
try
{
context[CpuRegister.Rdi] = SourcePathAddress;
context[CpuRegister.Rsi] = 0x2;
Assert.Equal(
(int)OrbisGen2Result.ORBIS_GEN2_OK,
KernelExports.KernelOpen(context));
fileDescriptor = unchecked((int)context[CpuRegister.Rax]);
Assert.True(fileDescriptor >= 3);
context[CpuRegister.Rdi] = SourcePathAddress;
context[CpuRegister.Rsi] = DestinationPathAddress;
Assert.Equal(
(int)OrbisGen2Result.ORBIS_GEN2_OK,
KernelMemoryCompatExports.KernelRename(context));
Assert.False(File.Exists(sourceHostPath));
var renamedPayload = new byte[payload.Length];
using (var destinationStream = new FileStream(
destinationHostPath,
FileMode.Open,
FileAccess.Read,
FileShare.ReadWrite | FileShare.Delete))
{
destinationStream.ReadExactly(renamedPayload);
}
Assert.Equal(payload, renamedPayload);
context[CpuRegister.Rdi] = unchecked((uint)fileDescriptor);
context[CpuRegister.Rsi] = ReadBufferAddress;
context[CpuRegister.Rdx] = unchecked((uint)payload.Length);
Assert.Equal(
(int)OrbisGen2Result.ORBIS_GEN2_OK,
KernelMemoryCompatExports.KernelReadUnderscore(context));
Assert.Equal((ulong)payload.Length, context[CpuRegister.Rax]);
Span<byte> actual = stackalloc byte[payload.Length];
Assert.True(memory.TryRead(ReadBufferAddress, actual));
Assert.True(actual.SequenceEqual(payload));
context[CpuRegister.Rdi] = unchecked((uint)fileDescriptor);
Assert.Equal(
(int)OrbisGen2Result.ORBIS_GEN2_OK,
KernelMemoryCompatExports.KernelClose(context));
fileDescriptor = -1;
Assert.Equal(payload, File.ReadAllBytes(destinationHostPath));
}
finally
{
if (fileDescriptor >= 0)
{
context[CpuRegister.Rdi] = unchecked((uint)fileDescriptor);
_ = KernelMemoryCompatExports.KernelClose(context);
}
_ = KernelMemoryCompatExports.UnregisterGuestPathMount(mountPoint);
if (Directory.Exists(hostRoot))
{
Directory.Delete(hostRoot, recursive: true);
}
}
}
}

View File

@ -12,6 +12,29 @@ public sealed class NpEntitlementAccessExportsTests
{
private const ulong MemoryBase = 0x1_0000_0000;
[Fact]
public void GetGameTrialsFlagReportsAFullGameAndOnlyWritesOneWord()
{
var memory = new FakeCpuMemory(MemoryBase, 0x100);
var context = new CpuContext(memory, Generation.Gen5);
Assert.True(memory.TryWrite(MemoryBase, new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 }));
context[CpuRegister.Rdi] = MemoryBase;
Assert.Equal(0, NpEntitlementAccessExports.NpEntitlementAccessGetGameTrialsFlag(context));
Span<byte> actual = stackalloc byte[8];
Assert.True(memory.TryRead(MemoryBase, actual));
Assert.Equal(new byte[] { 0, 0, 0, 0, 5, 6, 7, 8 }, actual.ToArray());
}
[Theory]
[InlineData(0UL, unchecked((int)0x817D0002))]
[InlineData(0x2_0000_0000UL, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT)]
public void GetGameTrialsFlagValidatesOutputMemory(ulong address, int expected)
{
var context = new CpuContext(new FakeCpuMemory(MemoryBase, 0x100), Generation.Gen5);
context[CpuRegister.Rdi] = address;
Assert.Equal(expected, NpEntitlementAccessExports.NpEntitlementAccessGetGameTrialsFlag(context));
}
[Theory]
[InlineData("GHOST2APP0000000")]
[InlineData("GHOST2BASE000000")]

View File

@ -7,7 +7,7 @@ using Xunit;
namespace SharpEmu.Libs.Tests.Pad;
public sealed class PadExportsTests
public sealed class PadExportsTests : IDisposable
{
private const ulong Base = 0x1_0000_0000;
private const int InvalidHandle = unchecked((int)0x80920003);
@ -17,29 +17,42 @@ public sealed class PadExportsTests
public PadExportsTests()
{
PadExports.ResetOpenedPadForTests();
_ctx = new CpuContext(_memory, Generation.Gen5);
}
[Theory]
public void Dispose() => PadExports.ResetOpenedPadForTests();
[NativeX64Theory]
[InlineData(0, 0)]
[InlineData(1, 0)]
[InlineData(2, InvalidHandle)]
[InlineData(-1, InvalidHandle)]
public void SetTiltCorrectionState_ValidatesHandle(int handle, int expected)
{
if (handle == 1)
{
Assert.Equal(1, OpenPad(0x10000000, 0));
}
_ctx[CpuRegister.Rdi] = unchecked((ulong)handle);
Assert.Equal(expected, PadExports.PadSetTiltCorrectionState(_ctx));
}
// ABI: int scePadResetOrientation(int32_t handle) — handle only, no out
// parameter, so the only failure mode is a bad handle.
[Theory]
[NativeX64Theory]
[InlineData(0, 0)]
[InlineData(1, 0)]
[InlineData(2, InvalidHandle)]
[InlineData(-1, InvalidHandle)]
public void ResetOrientation_ValidatesHandle(int handle, int expected)
{
if (handle == 1)
{
Assert.Equal(1, OpenPad(0x10000000, 0));
}
_ctx[CpuRegister.Rdi] = unchecked((ulong)handle);
Assert.Equal(expected, PadExports.PadResetOrientation(_ctx));
Assert.Equal(unchecked((ulong)expected), _ctx[CpuRegister.Rax]);
@ -132,4 +145,78 @@ public sealed class PadExportsTests
PadExports.ResetOpenedPadForTests();
}
}
[NativeX64Fact]
public void SpecialAndRemotePortsHaveIndependentLiveHandles()
{
const int noHandle = unchecked((int)0x80920008);
const int alreadyOpened = unchecked((int)0x80920004);
const ulong dataAddress = Base + 0x200;
Assert.Equal(0, PadExports.PadInit(_ctx));
_ctx[CpuRegister.Rdi] = 0xFF;
_ctx[CpuRegister.Rsi] = 16;
_ctx[CpuRegister.Rdx] = 0;
Assert.Equal(noHandle, PadExports.PadGetHandle(_ctx));
var standard = OpenPad(0x10000000, 0);
var special = OpenPad(0x10000000, 2);
var remote = OpenPad(0xFF, 16);
Assert.Equal(1, standard);
Assert.Equal(2, special);
Assert.Equal(3, remote);
Assert.Equal(alreadyOpened, OpenPad(0xFF, 16));
Assert.Equal(remote, PadExports.PadGetHandle(_ctx));
foreach (var handle in new[] { standard, special, remote })
{
_ctx[CpuRegister.Rdi] = unchecked((ulong)handle);
_ctx[CpuRegister.Rsi] = dataAddress;
Assert.Equal(0, PadExports.PadGetControllerInformation(_ctx));
Assert.Equal(0, PadExports.PadReadState(_ctx));
}
_ctx[CpuRegister.Rdi] = unchecked((ulong)remote);
Assert.Equal(0, PadExports.PadClose(_ctx));
Assert.Equal(InvalidHandle, PadExports.PadReadState(_ctx));
Assert.Equal(InvalidHandle, PadExports.PadClose(_ctx));
_ctx[CpuRegister.Rdi] = 0xFF;
_ctx[CpuRegister.Rsi] = 16;
_ctx[CpuRegister.Rdx] = 0;
Assert.Equal(noHandle, PadExports.PadGetHandle(_ctx));
_ctx[CpuRegister.Rdi] = 0x10000000;
_ctx[CpuRegister.Rsi] = 0;
Assert.Equal(standard, PadExports.PadGetHandle(_ctx));
_ctx[CpuRegister.Rsi] = 2;
Assert.Equal(special, PadExports.PadGetHandle(_ctx));
Assert.True(OpenPad(0xFF, 16) > remote);
}
[NativeX64Theory]
[InlineData(-1, 0, 0, unchecked((int)0x80920008))]
[InlineData(0x10000001, 0, 0, unchecked((int)0x80920007))]
[InlineData(0x10000000, 1, 0, unchecked((int)0x80920007))]
[InlineData(0x10000000, 16, 0, unchecked((int)0x80920007))]
[InlineData(0xFF, 0, 0, unchecked((int)0x80920007))]
[InlineData(0xFF, 16, 1, unchecked((int)0x80920007))]
public void Open_RejectsUnsupportedTuples(int userId, int type, int index, int expected)
{
PadExports.PadInit(_ctx);
_ctx[CpuRegister.Rdi] = unchecked((ulong)userId);
_ctx[CpuRegister.Rsi] = unchecked((ulong)type);
_ctx[CpuRegister.Rdx] = unchecked((ulong)index);
_ctx[CpuRegister.Rcx] = 0;
Assert.Equal(expected, PadExports.PadOpen(_ctx));
}
private int OpenPad(int userId, int type)
{
PadExports.PadInit(_ctx);
_ctx[CpuRegister.Rdi] = unchecked((ulong)userId);
_ctx[CpuRegister.Rsi] = unchecked((ulong)type);
_ctx[CpuRegister.Rdx] = 0;
_ctx[CpuRegister.Rcx] = 0;
return PadExports.PadOpen(_ctx);
}
}

View File

@ -0,0 +1,58 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using SharpEmu.Libs.UserService;
using Xunit;
namespace SharpEmu.Libs.Tests.UserService;
public sealed class UserServiceExportsTests
{
[Theory]
[InlineData(0x10000000)]
[InlineData(1)]
public void GetUserNumberWritesOneBasedSlotWithoutOverwritingAdjacentData(int userId)
{
const ulong address = 0x1_0000_0000;
var memory = new FakeCpuMemory(address, 0x100);
var ctx = new CpuContext(memory, Generation.Gen5);
Assert.True(memory.TryWrite(address, new byte[] { 0xEE, 0xEE, 0xEE, 0xEE, 0xAA, 0xBB, 0xCC, 0xDD }));
ctx[CpuRegister.Rdi] = unchecked((ulong)userId);
ctx[CpuRegister.Rsi] = address;
Assert.Equal(0, UserServiceExports.UserServiceGetUserNumber(ctx));
Span<byte> actual = stackalloc byte[8];
Assert.True(memory.TryRead(address, actual));
Assert.Equal(new byte[] { 1, 0, 0, 0, 0xAA, 0xBB, 0xCC, 0xDD }, actual.ToArray());
}
[Theory]
[InlineData(0x10000001, 0x1_0000_0000UL, unchecked((int)0x80960009))]
[InlineData(0x10000000, 0UL, unchecked((int)0x80960005))]
[InlineData(0x10000000, 0x2_0000_0000UL, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT)]
public void GetUserNumberValidatesUserAndOutput(int userId, ulong address, int expected)
{
var ctx = new CpuContext(new FakeCpuMemory(0x1_0000_0000, 0x100), Generation.Gen5);
ctx[CpuRegister.Rdi] = unchecked((ulong)userId);
ctx[CpuRegister.Rsi] = address;
Assert.Equal(expected, UserServiceExports.UserServiceGetUserNumber(ctx));
}
[Fact]
public void TerminateResetsTheLoginEvent()
{
const ulong address = 0x1_0000_0000;
var memory = new FakeCpuMemory(address, 0x100);
var ctx = new CpuContext(memory, Generation.Gen5);
Assert.Equal(0, UserServiceExports.UserServiceTerminate(ctx));
Assert.Equal(0, UserServiceExports.UserServiceInitialize(ctx));
ctx[CpuRegister.Rdi] = address;
Assert.Equal(0, UserServiceExports.UserServiceGetEvent(ctx));
Assert.Equal(unchecked((int)0x80960007), UserServiceExports.UserServiceGetEvent(ctx));
Assert.Equal(0, UserServiceExports.UserServiceTerminate(ctx));
Assert.Equal(0UL, ctx[CpuRegister.Rax]);
Assert.Equal(0, UserServiceExports.UserServiceInitialize(ctx));
Assert.Equal(0, UserServiceExports.UserServiceGetEvent(ctx));
Assert.Equal(0, UserServiceExports.UserServiceTerminate(ctx));
}
}