Bladeren bron

test(inventory): add focused regression coverage

Add a standalone Visual Studio 2026 test target covering opcode 1901 decoding, class-art selection, fixed-table replacement, bucket/equipment mapping validation, Family-4 resync staging, and exclusive service outcomes.

Register the test project in the solution and update clang-tidy's repository header filter and allowed standard-library headers so the refactored production and test sources share the same strict checks.
Thomas Shields 3 weken geleden
bovenliggende
commit
f418464b0a
4 gewijzigde bestanden met toevoegingen van 285 en 2 verwijderingen
  1. 2 2
      .clang-tidy
  2. 6 0
      Sunrise.sln
  3. 58 0
      tests/Sunrise.Tests.vcxproj
  4. 219 0
      tests/regression_tests.cpp

+ 2 - 2
.clang-tidy

@@ -68,11 +68,11 @@ Checks: >
   readability-redundant-string-cstr,
   readability-suspicious-call-argument
 WarningsAsErrors: '*'
-HeaderFilterRegex: '.*[\\/]prod_client[\\/]Sunrise[\\/]src[\\/].*'
+HeaderFilterRegex: '.*[\\/]Sunrise[\\/]src[\\/].*'
 SystemHeaders: false
 ExtraArgsBefore:
   - -Wdocumentation
 FormatStyle: file
 CheckOptions:
-  portability-restrict-system-includes.Includes: '-*,Windows.h,WinSock2.h,WS2tcpip.h,MSWSock.h,WinDNS.h,TlHelp32.h,Shellapi.h,bcrypt.h,d3d11.h,detours.h,dxgi.h,imgui.h,imgui_impl_dx11.h,imgui_impl_win32.h,intrin.h,algorithm,array,atomic,bit,bitset,cctype,charconv,chrono,climits,cmath,cstdarg,cstddef,cstdint,cstdio,cstdlib,cstring,cwchar,limits,new,optional,span,string_view,type_traits,utility,vector'
+  portability-restrict-system-includes.Includes: '-*,Windows.h,WinSock2.h,WS2tcpip.h,MSWSock.h,WinDNS.h,TlHelp32.h,Shellapi.h,bcrypt.h,d3d11.h,detours.h,dxgi.h,imgui.h,imgui_impl_dx11.h,imgui_impl_win32.h,intrin.h,algorithm,array,atomic,bit,bitset,cctype,charconv,chrono,climits,cmath,cstdarg,cstddef,cstdint,cstdio,cstdlib,cstring,cwchar,limits,memory,new,optional,span,string_view,type_traits,utility,variant,vector'
 ...

+ 6 - 0
Sunrise.sln

@@ -5,6 +5,8 @@ VisualStudioVersion = 18.0.30412.183
 MinimumVisualStudioVersion = 10.0.40219.1
 Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Sunrise", "Sunrise\Sunrise.vcxproj", "{A665726B-97A2-4E35-B518-0FC207D3DA1B}"
 EndProject
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Sunrise.Tests", "tests\Sunrise.Tests.vcxproj", "{9D2A5866-9D2F-4A07-A88C-6E713A355C94}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|x64 = Debug|x64
@@ -15,6 +17,10 @@ Global
 		{A665726B-97A2-4E35-B518-0FC207D3DA1B}.Debug|x64.Build.0 = Debug|x64
 		{A665726B-97A2-4E35-B518-0FC207D3DA1B}.Release|x64.ActiveCfg = Release|x64
 		{A665726B-97A2-4E35-B518-0FC207D3DA1B}.Release|x64.Build.0 = Release|x64
+		{9D2A5866-9D2F-4A07-A88C-6E713A355C94}.Debug|x64.ActiveCfg = Debug|x64
+		{9D2A5866-9D2F-4A07-A88C-6E713A355C94}.Debug|x64.Build.0 = Debug|x64
+		{9D2A5866-9D2F-4A07-A88C-6E713A355C94}.Release|x64.ActiveCfg = Release|x64
+		{9D2A5866-9D2F-4A07-A88C-6E713A355C94}.Release|x64.Build.0 = Release|x64
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE

+ 58 - 0
tests/Sunrise.Tests.vcxproj

@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <ItemGroup Label="ProjectConfigurations">
+    <ProjectConfiguration Include="Debug|x64"><Configuration>Debug</Configuration><Platform>x64</Platform></ProjectConfiguration>
+    <ProjectConfiguration Include="Release|x64"><Configuration>Release</Configuration><Platform>x64</Platform></ProjectConfiguration>
+  </ItemGroup>
+  <PropertyGroup Label="Globals">
+    <VCProjectVersion>17.0</VCProjectVersion>
+    <ProjectGuid>{9D2A5866-9D2F-4A07-A88C-6E713A355C94}</ProjectGuid>
+    <RootNamespace>sunrise_tests</RootNamespace>
+    <WindowsTargetPlatformVersion>10.0.26100.0</WindowsTargetPlatformVersion>
+    <ProjectName>Sunrise.Tests</ProjectName>
+  </PropertyGroup>
+  <Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
+  <PropertyGroup Label="Configuration">
+    <ConfigurationType>Application</ConfigurationType>
+    <PlatformToolset>v145</PlatformToolset>
+    <CharacterSet>Unicode</CharacterSet>
+  </PropertyGroup>
+  <PropertyGroup Condition="'$(Configuration)'=='Debug'" Label="Configuration"><UseDebugLibraries>true</UseDebugLibraries></PropertyGroup>
+  <PropertyGroup Condition="'$(Configuration)'=='Release'" Label="Configuration"><UseDebugLibraries>false</UseDebugLibraries></PropertyGroup>
+  <Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
+  <PropertyGroup>
+    <OutDir>$(ProjectDir)..\build\$(Platform)\$(Configuration)\</OutDir>
+    <IntDir>$(ProjectDir)..\build\obj\$(Platform)\$(Configuration)\tests\</IntDir>
+  </PropertyGroup>
+  <ItemDefinitionGroup>
+    <ClCompile>
+      <LanguageStandard>stdcpp20</LanguageStandard>
+      <WarningLevel>Level4</WarningLevel>
+      <TreatWarningAsError>true</TreatWarningAsError>
+      <ConformanceMode>true</ConformanceMode>
+      <MultiProcessorCompilation>false</MultiProcessorCompilation>
+      <AdditionalOptions>/utf-8 %(AdditionalOptions)</AdditionalOptions>
+      <AdditionalIncludeDirectories>$(ProjectDir)..\Sunrise\src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
+      <PreprocessorDefinitions>WIN32;_WINDOWS;WIN32_LEAN_AND_MEAN;NOMINMAX;%(PreprocessorDefinitions)</PreprocessorDefinitions>
+    </ClCompile>
+  </ItemDefinitionGroup>
+  <ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'">
+    <ClCompile><Optimization>Disabled</Optimization><RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary></ClCompile>
+    <Link><GenerateDebugInformation>true</GenerateDebugInformation></Link>
+  </ItemDefinitionGroup>
+  <ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
+    <ClCompile><Optimization>MaxSpeed</Optimization><FunctionLevelLinking>true</FunctionLevelLinking><RuntimeLibrary>MultiThreaded</RuntimeLibrary></ClCompile>
+    <Link><GenerateDebugInformation>true</GenerateDebugInformation><OptimizeReferences>true</OptimizeReferences></Link>
+  </ItemDefinitionGroup>
+  <ItemGroup>
+    <ClCompile Include="regression_tests.cpp" />
+    <ClCompile Include="..\Sunrise\src\middleware\encoding\bit_reader.cpp" />
+    <ClCompile Include="..\Sunrise\src\middleware\content\packages\tables\definition_index_table.cpp" />
+    <ClCompile Include="..\Sunrise\src\middleware\content\packages\tables\item_appearance_reader.cpp" />
+    <ClCompile Include="..\Sunrise\src\middleware\web_service\messages\opcode1901_codec.cpp" />
+    <ClCompile Include="..\Sunrise\src\server\bap\encrypted\queuez\queuez_state_validation.cpp" />
+    <ClCompile Include="..\Sunrise\src\server\bap\encrypted\queuez\staging\queuez_family_refresh_staging.cpp" />
+    <ClCompile Include="..\Sunrise\src\state\build_data\inventory\buckets\inventory_bucket_catalog.cpp" />
+  </ItemGroup>
+  <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
+</Project>

+ 219 - 0
tests/regression_tests.cpp

@@ -0,0 +1,219 @@
+#include <array>
+#include <cstddef>
+#include <cstdint>
+#include <cstdio>
+#include <cstring>
+#include <span>
+
+#include "middleware/content/packages/tables/definition_index_table.h"
+#include "middleware/content/packages/tables/items.h"
+#include "middleware/datagen/character_record/appearance/internal.h"
+#include "middleware/queuez/queuez_update.h"
+#include "middleware/web_service/messages/opcode1901.h"
+#include "server/bap/encrypted/queuez/queuez_state_validation.h"
+#include "server/web_service/web_service_runtime.h"
+#include "state/build_data/inventory/buckets/inventory_bucket_catalog.h"
+#include "state/build_data/table.h"
+
+namespace {
+
+int failures{};
+
+void expect(bool value, const char* label) {
+    if (!value) {
+        std::fprintf(stderr, "FAIL %s\n", label);
+        ++failures;
+    }
+}
+
+template <typename Value>
+void write_at(std::span<std::byte> bytes, std::size_t offset, Value value) {
+    std::memcpy(bytes.data() + offset, &value, sizeof value);
+}
+
+void test_opcode1901_fixture() {
+    using namespace sunrise::middleware::web_service;
+    using namespace sunrise::middleware::web_service::messages;
+    constexpr std::array<std::uint8_t, 24> wire{
+        0x19, 0xA8, 0x28, 0x56, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08,
+    };
+    Message message{
+        opcode1901::kOpcode, 7, {reinterpret_cast<const std::byte*>(wire.data()), wire.size()}};
+    opcode1901::Request decoded{};
+    expect(opcode1901::parse_request(message, decoded), "opcode1901 captured fixture parses");
+    expect(decoded.plugDefinitionIndex == 6786, "opcode1901 plug index");
+    expect(decoded.canonicalSocketKind == 5 && decoded.modelSocketKind == 0,
+           "opcode1901 socket kinds");
+    expect(decoded.socketIndex == 5 && decoded.auxiliary == 0, "opcode1901 socket descriptor");
+    expect(decoded.equipmentSelector == 8, "opcode1901 equipment selector");
+
+    auto malformed = wire;
+    malformed[0] = 0;
+    message.payload = {reinterpret_cast<const std::byte*>(malformed.data()), malformed.size()};
+    expect(!opcode1901::parse_request(message, decoded), "opcode1901 rejects wrong count");
+    expect(decoded.plugDefinitionIndex == 0 && decoded.equipmentSelector == 0,
+           "opcode1901 clears rejected output");
+}
+
+void test_class_art_selection() {
+    namespace appearance = sunrise::middleware::datagen::character_record::appearance;
+    namespace details = sunrise::state::build_data::items::details;
+    details::Definition definition{};
+    definition.artArrangementIndices = {100, 101, details::kUnavailableArtIndex, 103};
+    expect(appearance::select_art_arrangement(definition, sunrise::state::CharacterClass::titan)
+               == 101,
+           "Titan art selects class row");
+    expect(appearance::select_art_arrangement(definition, sunrise::state::CharacterClass::hunter)
+               == 100,
+           "missing Hunter art falls back to generic");
+    expect(appearance::select_art_arrangement(definition, sunrise::state::CharacterClass::warlock)
+               == 103,
+           "Warlock art selects class row");
+}
+
+void test_class_art_extraction() {
+    namespace tables = sunrise::middleware::content::packages::tables;
+    namespace items = tables::items;
+    std::array<std::byte, 384> blob{};
+    constexpr std::size_t art = 192;
+    constexpr std::size_t header = 304;
+    constexpr std::size_t data = header + 16;
+    write_at(
+        blob, tables::kArtBlockOffset, static_cast<std::int64_t>(art - tables::kArtBlockOffset));
+    write_at(blob, art, std::uint64_t{3});
+    write_at(blob, art + 8, static_cast<std::int64_t>(header - (art + 8)));
+    write_at(blob, header - 4, std::uint32_t{0x80800000});
+    write_at(blob, header, std::uint64_t{3});
+    write_at(blob, header + 8, tables::kArtRowClass);
+    write_at(blob, art + 88, std::uint16_t{77});
+    write_at(blob, data, std::int8_t{-1});
+    write_at(blob, data + 2, std::uint16_t{100});
+    write_at(blob, data + 4, std::int8_t{0});
+    write_at(blob, data + 6, std::uint16_t{101});
+    write_at(blob, data + 8, std::int8_t{2});
+    write_at(blob, data + 10, std::uint16_t{103});
+
+    items::Row row{};
+    items::read_appearance(blob, row);
+    expect(row.gearArtIndex == 77, "appearance extraction reads gear art");
+    expect(row.artArrangementIndices[0] == 100 && row.artArrangementIndices[1] == 101
+               && row.artArrangementIndices[2] == items::kUnavailableArtIndex
+               && row.artArrangementIndices[3] == 103,
+           "appearance extraction keeps generic and class rows");
+}
+
+void test_table_replacement() {
+    struct Row {
+        std::uint32_t value{};
+    };
+    sunrise::state::build_data::Table<Row, 8> table{};
+    constexpr std::array<Row, 4> first{{{1}, {2}, {3}, {4}}};
+    constexpr std::array<Row, 2> second{{{9}, {8}}};
+    expect(table.replace(first), "table accepts first replacement");
+    expect(table.replace(second), "table accepts shorter replacement");
+    std::array<Row, 8> snapshot{};
+    std::size_t count = 0;
+    expect(table.snapshot(snapshot, count) && count == second.size(), "table publishes new count");
+    expect(snapshot[0].value == second[0].value && snapshot[1].value == second[1].value,
+           "table publishes new prefix");
+    expect(table.reset(3).size() == 3 && table.count() == 3, "table reset resizes safely");
+    table.clear();
+    expect(table.count() == 0, "table clear publishes empty state");
+}
+
+void test_bucket_equipment_mapping_validation() {
+    namespace buckets = sunrise::state::build_data::inventory::buckets;
+    constexpr std::array<buckets::Descriptor, 2> mapped{{
+        {0, buckets::ArraySelector::character, 0, 10, 7, 0},
+        {13, buckets::ArraySelector::profile, 75, 50, buckets::kUnavailableEquipmentSlot, 0},
+    }};
+    expect(buckets::valid(mapped), "bucket table accepts a unique extracted equipment slot");
+
+    auto unmapped = mapped;
+    unmapped[0].equipmentSlot = buckets::kUnavailableEquipmentSlot;
+    expect(!buckets::valid(unmapped), "bucket table rejects an all-unmapped cache domain");
+
+    auto duplicate = mapped;
+    duplicate[1].equipmentSlot = mapped[0].equipmentSlot;
+    expect(!buckets::valid(duplicate), "bucket table rejects duplicate equipment slots");
+}
+
+void test_family4_resync() {
+    namespace queuez = sunrise::server::bap::encrypted::queuez;
+    namespace wire = sunrise::middleware::queuez;
+    queuez::SessionState before{};
+    before.family4RootSoid = 0x100;
+    before.family4Version = 3;
+    before.family4ResidentCount = 2;
+    before.family4Active = true;
+    before.family4Residents[0] = {0x100, 10};
+    before.family4Residents[1] = {0x101, 11};
+    expect(queuez::valid(before), "Family4 before state is valid");
+
+    constexpr std::array<wire::Object, 3> objects{{
+        {10, 0x100, wire::Encoding::oodle, {}},
+        {11, 0x101, wire::Encoding::oodle, {}},
+        {12, 0x202, wire::Encoding::oodle, {}},
+    }};
+    const wire::Family refresh{
+        queuez::kAccountFamilyType, 0x100, 4, wire::kFullSnapshotFlag, objects};
+    queuez::SessionState after{};
+    expect(queuez::stage_family4_refresh(before, refresh, after),
+           "Family4 next-version refresh stages");
+    expect(after.family4Version == 4 && after.family4ResidentCount == 3,
+           "Family4 refresh advances version and manifest");
+    expect(after.family4Residents[2].objectSoid == 0x202, "Family4 refresh records added resident");
+
+    wire::Family stale = refresh;
+    stale.version = before.family4Version;
+    queuez::SessionState rejected{};
+    expect(!queuez::stage_family4_refresh(before, stale, rejected),
+           "Family4 rejects stale refresh version");
+    expect(rejected.family4Version == before.family4Version
+               && rejected.family4ResidentCount == before.family4ResidentCount,
+           "rejected refresh leaves exact before image");
+
+    auto duplicateObjects = objects;
+    duplicateObjects[2].version = duplicateObjects[1].version;
+    wire::Family duplicate = refresh;
+    duplicate.objects = duplicateObjects;
+    expect(!queuez::stage_family4_refresh(before, duplicate, rejected),
+           "Family4 rejects duplicate resident identity");
+}
+
+void test_outcome_exclusivity() {
+    using namespace sunrise;
+    server::web_service::Outcome outcome{};
+    state::PendingItemState itemState{};
+    itemState.prepared = true;
+    outcome.mutation = itemState;
+    expect(server::web_service::mutation_if<state::PendingItemState>(outcome) != nullptr,
+           "Outcome stores item-state transaction");
+    expect(server::web_service::mutation_if<state::PendingSocketPlug>(outcome) == nullptr,
+           "Outcome exposes only one transaction alternative");
+    state::PendingSocketPlug socket{};
+    socket.prepared = true;
+    outcome.mutation = socket;
+    expect(server::web_service::mutation_if<state::PendingItemState>(outcome) == nullptr
+               && server::web_service::mutation_if<state::PendingSocketPlug>(outcome) != nullptr,
+           "Outcome replacement cannot retain a second mutation");
+}
+
+} // namespace
+
+int main() {
+    test_opcode1901_fixture();
+    test_class_art_selection();
+    test_class_art_extraction();
+    test_table_replacement();
+    test_bucket_equipment_mapping_validation();
+    test_family4_resync();
+    test_outcome_exclusivity();
+    if (failures != 0) {
+        std::fprintf(stderr, "%d regression test(s) failed\n", failures);
+        return 1;
+    }
+    std::puts("All Sunrise regression tests passed");
+    return 0;
+}