diff --git a/Source/Mockolate/MockRegistry.Interactions.cs b/Source/Mockolate/MockRegistry.Interactions.cs index 75492975..4abefa46 100644 --- a/Source/Mockolate/MockRegistry.Interactions.cs +++ b/Source/Mockolate/MockRegistry.Interactions.cs @@ -592,6 +592,7 @@ public bool SetProperty(int memberId, string propertyName, T value) RecordPropertySetter(memberId, propertyName, value); } + // Stryker disable once Boolean : forceReinitWhenFound only has an observable effect when defaultValueGenerator is non-null. Here it is null, so the only side effect of flipping to true is an extra InitializeWith(null) call that early-returns under the _isUserInitialized || _isInitialized guard inside PropertySetup.InitializeValue. PropertySetup matchingSetup = ResolvePropertySetup(propertyName, null, null, false); ((IInteractivePropertySetup)matchingSetup).InvokeSetter(null, value, Behavior); @@ -638,6 +639,7 @@ public bool SetPropertyFast(int memberId, int setterMemberId, string property } else { + // Stryker disable once Boolean : same equivalence as the SetProperty(int, string, T) path — defaultValueGenerator is null here, so flipping forceReinit only triggers an InitializeWith(null) call that early-returns under the _isUserInitialized || _isInitialized guard. matchingSetup = ResolvePropertySetup(propertyName, null, null, false); } diff --git a/Tests/Mockolate.Internal.Tests/Registry/MockRegistryTests.cs b/Tests/Mockolate.Internal.Tests/Registry/MockRegistryTests.cs index 5de536bc..b5dbdc34 100644 --- a/Tests/Mockolate.Internal.Tests/Registry/MockRegistryTests.cs +++ b/Tests/Mockolate.Internal.Tests/Registry/MockRegistryTests.cs @@ -583,6 +583,22 @@ int Base() await That(result).IsEqualTo(99); } + [Fact] + public async Task WithNullSnapshotAtMemberIdSlot_ShouldFallBackToColdPathWithoutThrowing() + { + // The member-id table is allocated to length 6 by registering at index 5, leaving indices + // 0..4 holding null entries. Reading from such a null slot must fall through to the cold + // path rather than dereferencing the null PropertySetup reference. + MockRegistry registry = new(MockBehavior.Default, new FastMockInteractions(0)); + PropertySetup unrelatedSetup = new(registry, "Q"); + unrelatedSetup.InitializeWith(99); + registry.SetupProperty(5, unrelatedSetup); + + int result = registry.GetPropertyFast(0, "P", _ => 7); + + await That(result).IsEqualTo(7); + } + [Fact] public async Task WithoutSnapshotSetup_ShouldFallBackToColdPath() { @@ -620,6 +636,55 @@ int Generator(MockBehavior _) public sealed class SetPropertyFastTests { + [Fact] + public async Task WhenMemberIdSetupDiffersFromDictSetup_ShouldUseMemberIdSetupInDefaultScope() + { + // Pins the member-id-table lookup block inside SetPropertyFast. The first registration goes + // into both the member-id table[2] and the Properties dictionary; the second (without a + // memberId) replaces only the dictionary entry. With both entries in place but distinct + // SkippingBaseClass overrides, the hot path must surface the snapshot setup, not the dict. + MockRegistry registry = new(MockBehavior.Default, new FastMockInteractions(0)); + + PropertySetup snapshotSetup = new(registry, "P"); + snapshotSetup.InitializeWith(0); + ((IPropertySetup)snapshotSetup).SkippingBaseClass(); + registry.SetupProperty(2, snapshotSetup); + + PropertySetup dictSetup = new(registry, "P"); + dictSetup.InitializeWith(0); + ((IPropertySetup)dictSetup).SkippingBaseClass(false); + registry.SetupProperty(dictSetup); + + bool skipBase = registry.SetPropertyFast(2, 3, "P", 42); + + await That(skipBase).IsTrue(); + } + + [Fact] + public async Task WithActiveScenario_ShouldRouteToScenarioSetupOverMemberIdTableSetup() + { + // Pins the IsNullOrEmpty(Scenario) guard at the top of SetPropertyFast: when a scenario is + // active, the member-id table (which only ever holds default-scope setups) must NOT be + // consulted. The scenario-scoped setup overrides via SkippingBaseClass(true) so we can tell + // which setup was invoked. + MockRegistry registry = new(MockBehavior.Default, new FastMockInteractions(0)); + + PropertySetup defaultSetup = new(registry, "P"); + defaultSetup.InitializeWith(0); + ((IPropertySetup)defaultSetup).SkippingBaseClass(false); + registry.SetupProperty(2, defaultSetup); + + PropertySetup scenarioSetup = new(registry, "P"); + scenarioSetup.InitializeWith(0); + ((IPropertySetup)scenarioSetup).SkippingBaseClass(); + registry.SetupProperty(2, "myScenario", scenarioSetup); + + registry.TransitionTo("myScenario"); + bool skipBase = registry.SetPropertyFast(2, 3, "P", 42); + + await That(skipBase).IsTrue(); + } + [Fact] public async Task WithoutSnapshotSetup_ShouldFallBackToResolveSetup() {