Files
sharpemu/tests/SharpEmu.Libs.Tests/Json/JsonValueExportsTests.cs
José Luis Caravaca Carretero df53ff59d9 [Json] Implement sce::Json::Value and String (construct / set / destroy) (#169)
* [Json] Implement sce::Json::Value and Json::String construct/set/destroy

libSceJson previously only had the Initializer/MemAllocator setup path.
The Value and String classes themselves were entirely absent, so a
Prospero title that builds a JSON tree (Quake PPSA01880 does, to shape
a web-API request) hit unresolved imports and faulted on the call. The
imports it left unresolved right before its access violation are exactly
these Value ctors/setters and String ctor/dtor.

Model the Value/String payload host-side (JsonObjectHeap), keyed by the
guest `this` pointer, following the handle-shadow pattern already used
by Ngs2Exports. The guest object bytes are deliberately not written:
these objects are usually stack-allocated with an unknown real layout,
and writing a guessed layout risks smashing an adjacent stack canary
(the same hazard the AudioOut2 context-param note in this tree records).
Constructors and setters follow the Itanium ABI and return `this` in rax,
which is correct whether the real setter returns void or Value&.

Covered NIDs (complete-object C1/D1 variants, matching the observed
imports): Value(default/bool/long/ulong/double/ValueType/char*/String),
Value::~Value, Value::set(bool/long/ulong/double/ValueType/char*/String),
Value::clear, String(char*/default/copy), String::~String.

Only the payload the guest can reach through library methods is modelled;
direct guest reads of the object bytes are out of scope and would need
observed layout evidence.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* [Tests] Add SharpEmu.Libs.Tests covering the Json Value/String exports

First test project for SharpEmu.Libs (xunit), the SharpEmu.Libs.Tests
layout the maintainer already agreed to in issue #36.

- A FakeCpuMemory (single contiguous region) drives the exports at the
  CpuContext level with no live guest.
- Direct-call tests: ctor/setter round-trips for bool/int/uint/double
  (read from xmm0)/char*/String/ValueType, destructor cleanup, and the
  graceful-degradation paths (missing String shadow and a faulting char*
  pointer both fall back to the empty string instead of throwing).
- Registration test: a real ModuleManager scans SharpEmu.Libs and the
  nine NIDs Quake left unresolved now resolve to the libSceJson exports
  and dispatch cleanly (returns `this` in rax).

InternalsVisibleTo exposes JsonObjectHeap to the test assembly. The test
project's packages.lock.json is committed for CI locked-mode restore;
CI does not run tests yet, left as a maintainer decision.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* [Json] Add Initializer::setGlobalNullAccessCallback

Quake calls it during kexPSNWebAPI::Initialize and treats the
not-found error as fatal for the whole Np Web API bring-up. Store the
guest hook (never invoked by this HLE: shadows degrade to defaults
instead of dereferencing missing members) and return success.

Verified against the dump: the "setGlobalNullAccessCallback failed
(0x80020002)" line is gone and kexPSNWebAPI::Initialize now logs
"Np Web API Initialized"; the next blockers are sceNpAuthCreateRequest
and sceUserServiceInitialize ordering, outside libSceJson.

Also pins both Json test classes to one xunit collection: they share
JsonObjectHeap statics and parallel class execution raced ResetForTests
against a running test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 01:49:33 +03:00

189 lines
6.0 KiB
C#

// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using SharpEmu.Libs.Json;
using Xunit;
namespace SharpEmu.Libs.Tests.Json;
// JsonObjectHeap is shared static state; both Json test classes join one collection so xUnit
// does not run them in parallel against it.
[Collection("JsonObjectHeap")]
public sealed class JsonValueExportsTests
{
private const ulong ThisAddress = 0x1_0000_0000;
private const ulong StringAddress = 0x1_0000_1000;
private const ulong TextAddress = 0x1_0000_2000;
private readonly FakeCpuMemory _memory = new(0x1_0000_0000, 0x10000);
private readonly CpuContext _ctx;
public JsonValueExportsTests()
{
JsonObjectHeap.ResetForTests();
_ctx = new CpuContext(_memory, Generation.Gen5);
}
[Fact]
public void ValueDefaultConstructor_RegistersNullAndReturnsThis()
{
_ctx[CpuRegister.Rdi] = ThisAddress;
JsonValueExports.ValueDefaultConstructor(_ctx);
Assert.Equal(ThisAddress, _ctx[CpuRegister.Rax]);
Assert.Equal(JsonValueKind.Null, JsonObjectHeap.Values[ThisAddress].Kind);
}
[Theory]
[InlineData(0UL, false)]
[InlineData(1UL, true)]
[InlineData(0xFFFF_FF00UL, false)] // only the low byte is the bool; 0x00 low byte => false
public void ValueSetBoolean_StoresLowByte(ulong raw, bool expected)
{
_ctx[CpuRegister.Rdi] = ThisAddress;
_ctx[CpuRegister.Rsi] = raw;
JsonValueExports.ValueSetBoolean(_ctx);
var state = JsonObjectHeap.Values[ThisAddress];
Assert.Equal(JsonValueKind.Boolean, state.Kind);
Assert.Equal(expected, state.Boolean);
Assert.Equal(ThisAddress, _ctx[CpuRegister.Rax]);
}
[Fact]
public void ValueSetInteger_RoundTripsSignedValue()
{
_ctx[CpuRegister.Rdi] = ThisAddress;
_ctx[CpuRegister.Rsi] = unchecked((ulong)-42L);
JsonValueExports.ValueSetInteger(_ctx);
var state = JsonObjectHeap.Values[ThisAddress];
Assert.Equal(JsonValueKind.Integer, state.Kind);
Assert.Equal(-42L, state.Integer);
}
[Fact]
public void ValueSetUnsigned_RoundTripsFullWidth()
{
_ctx[CpuRegister.Rdi] = ThisAddress;
_ctx[CpuRegister.Rsi] = ulong.MaxValue;
JsonValueExports.ValueSetUnsigned(_ctx);
var state = JsonObjectHeap.Values[ThisAddress];
Assert.Equal(JsonValueKind.UInteger, state.Kind);
Assert.Equal(ulong.MaxValue, state.UnsignedInteger);
}
[Fact]
public void ValueSetReal_ReadsDoubleFromXmm0()
{
_ctx[CpuRegister.Rdi] = ThisAddress;
_ctx.SetXmmRegister(0, unchecked((ulong)BitConverter.DoubleToInt64Bits(3.14159)), 0);
JsonValueExports.ValueSetReal(_ctx);
var state = JsonObjectHeap.Values[ThisAddress];
Assert.Equal(JsonValueKind.Real, state.Kind);
Assert.Equal(3.14159, state.Real, precision: 10);
}
[Fact]
public void ValueSetCString_ReadsGuestString()
{
_memory.WriteCString(TextAddress, "hello json");
_ctx[CpuRegister.Rdi] = ThisAddress;
_ctx[CpuRegister.Rsi] = TextAddress;
JsonValueExports.ValueSetCString(_ctx);
var state = JsonObjectHeap.Values[ThisAddress];
Assert.Equal(JsonValueKind.String, state.Kind);
Assert.Equal("hello json", state.Text);
}
[Fact]
public void ValueSetType_KeepsRawGuestEnumValue()
{
_ctx[CpuRegister.Rdi] = ThisAddress;
_ctx[CpuRegister.Rsi] = 7;
JsonValueExports.ValueSetType(_ctx);
var state = JsonObjectHeap.Values[ThisAddress];
Assert.Equal(JsonValueKind.ExplicitType, state.Kind);
Assert.Equal(7u, state.ExplicitType);
}
[Fact]
public void StringConstructThenValueSetString_CopiesText()
{
_memory.WriteCString(TextAddress, "from string object");
_ctx[CpuRegister.Rdi] = StringAddress;
_ctx[CpuRegister.Rsi] = TextAddress;
JsonValueExports.StringCStringConstructor(_ctx);
Assert.Equal("from string object", JsonObjectHeap.Strings[StringAddress]);
Assert.Equal(StringAddress, _ctx[CpuRegister.Rax]);
_ctx[CpuRegister.Rdi] = ThisAddress;
_ctx[CpuRegister.Rsi] = StringAddress;
JsonValueExports.ValueSetString(_ctx);
var state = JsonObjectHeap.Values[ThisAddress];
Assert.Equal(JsonValueKind.String, state.Kind);
Assert.Equal("from string object", state.Text);
}
[Fact]
public void ValueSetString_MissingStringShadow_DegradesToEmpty()
{
_ctx[CpuRegister.Rdi] = ThisAddress;
_ctx[CpuRegister.Rsi] = StringAddress; // never constructed
JsonValueExports.ValueSetString(_ctx);
var state = JsonObjectHeap.Values[ThisAddress];
Assert.Equal(JsonValueKind.String, state.Kind);
Assert.Equal(string.Empty, state.Text);
}
[Fact]
public void Destructors_RemoveShadowState()
{
_ctx[CpuRegister.Rdi] = ThisAddress;
JsonValueExports.ValueDefaultConstructor(_ctx);
_ctx[CpuRegister.Rdi] = StringAddress;
JsonValueExports.StringDefaultConstructor(_ctx);
Assert.True(JsonObjectHeap.Values.ContainsKey(ThisAddress));
Assert.True(JsonObjectHeap.Strings.ContainsKey(StringAddress));
_ctx[CpuRegister.Rdi] = ThisAddress;
JsonValueExports.ValueDestructor(_ctx);
_ctx[CpuRegister.Rdi] = StringAddress;
JsonValueExports.StringDestructor(_ctx);
Assert.False(JsonObjectHeap.Values.ContainsKey(ThisAddress));
Assert.False(JsonObjectHeap.Strings.ContainsKey(StringAddress));
Assert.Equal(0UL, _ctx[CpuRegister.Rax]);
}
[Fact]
public void ValueSetCString_FaultingPointer_DegradesToEmptyString()
{
_ctx[CpuRegister.Rdi] = ThisAddress;
_ctx[CpuRegister.Rsi] = 0xDEAD_0000_0000; // outside the mapped region
JsonValueExports.ValueSetCString(_ctx);
var state = JsonObjectHeap.Values[ThisAddress];
Assert.Equal(JsonValueKind.String, state.Kind);
Assert.Equal(string.Empty, state.Text);
}
}