Skip to content

Commit d36df90

Browse files
committed
Tests: convert audit-flagged tests into TDD bug reproducers and strengthen oracles
Audit follow-up pass over the test suite, squashed from four commits. No engine or production code is touched: red tests reproduce real defects and stay failing until the engine is fixed; the rest of the strengthened tests pin the contract where the engine is already correct. - Rewrite the masking tests in the 12 masks-bug classes to assert the correct, domain-expected behavior instead of pinning the engine's broken output: AlertProcessingService inverted Greater/Less operators; degenerate Renko bricks and the Heikin-Ashi Open outside [Low, High]; the candle leak after SubscriptionFinished when Count is exhausted; VolumeProfile Value Area boundary defects; Level1DepthBuilderManager leaking the managed depth subscription id; invalid MarketDepth.Sparse books; OfflineMessageAdapter addressing the replaced order Done to the wrong transaction id; OrderLogMessageAdapter dropping the subscription id on built ticks; OrderMatcher FOK destructive book consumption and empty ghost levels; SecurityProvider dropping Skip in the lookup-all paging branch; Statistics double-counting cumulative commission; SubscriptionOnlineManager misrouting hist+live history and unsubscribe errors. - Strengthen the audit's suspicious classes (vacuous, tautological, inert and weak oracles) into load-bearing tests and reproduce the residual core bugs: the FillGaps wrapper missing OwnInnerAdapter so Dispose does not cascade; BasketRoutingManager duplicating the parent subscription id and dropping data for pinned-adapter subscriptions; continuous-contract volume not reset on rollover; Import losing sub-second precision and the missing board ExpiryTime mapping; MarketOrderAlgo.IsFinished stuck after Cancel; ClearDatesCacheAsync not invalidating the cached dates; string condition parameters dropped from snapshot serialization; StorageBuffer buffering PositionChange with EnabledPositions off; StrategyParamHelper distorted float/double optimization ranges, off-by-one iteration count and bool GetRandom; the Renko volume profile counting tick volume once per brick; hist+live children never receiving their own subscription error; InMemorySecurityStorage ignoring the forced flag and never matching code-only ids; DecomposedStrategy.CanAttach claiming foreign orders. - Unify Absolute protective units as an offset from the protected entry price (the same entry +/- level formula the engine already applies to Percent) and pin that contract consistently across the protection tests; the offset tests fail against the current raw-price-level engine behavior. Add real activation oracles for MarketOrderFlag and the limit close-by-timeout path. - Reproduce the remaining audit bugs that need test doubles and add the first ServerProtectiveBehaviour coverage via a mock adapter: the self-created RemoteMarketDataDrive transport adapter never disposed; BasketMessageAdapter dropping out-data for known pinned-adapter subscriptions; the CandleBuilderManager Count leak for candles built from ticks; the ServerProtectiveBehaviour isTake flag hard-coded to the condition interface so stop-only registrations are mislabeled as takes. Absolute protective levels are verified as offsets from entry, each side on its own registration.
1 parent b5abd7e commit d36df90

31 files changed

Lines changed: 3588 additions & 617 deletions

Tests/AdapterWrapperPipelineBuilderTests.cs

Lines changed: 259 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,26 @@ public async Task Build_WithSupportOrderBookIncrements_IncludesOrderBookIncremen
439439
IsTrue(HasWrapper<OrderBookIncrementMessageAdapter>(result));
440440
}
441441

442+
[TestMethod]
443+
public async Task Build_WithSuppressOrderBookIncrements_NoOrderBookIncrementAdapter()
444+
{
445+
var builder = CreateBuilder();
446+
// Even though the inner adapter reports IsSupportOrderBookIncrements, SuppressOrderBookIncrements
447+
// must veto the OrderBookIncrementMessageAdapter (builder line:
448+
// "!config.SuppressOrderBookIncrements && (SupportBuildingFromOrderLog || IsSupportOrderBookIncrements)").
449+
var inner = new TestPipelineAdapter { SupportOrderBookIncrements = true };
450+
var config = CreateDefaultConfig() with
451+
{
452+
SuppressOrderBookIncrements = true,
453+
SupportBuildingFromOrderLog = false,
454+
};
455+
456+
var result = await builder.BuildAsync(inner, config, CancellationToken);
457+
458+
IsNotNull(result);
459+
IsFalse(HasWrapper<OrderBookIncrementMessageAdapter>(result));
460+
}
461+
442462
[TestMethod]
443463
public async Task Build_WithSupportOrderBookTruncate_IncludesOrderBookTruncateAdapter()
444464
{
@@ -504,6 +524,36 @@ public async Task Build_WithPositionsEmulationRequired_IncludesPositionAdapter()
504524
IsTrue(HasWrapper<PositionMessageAdapter>(result));
505525
}
506526

527+
[TestMethod]
528+
public async Task Build_WithPositionsEmulationFalse_IncludesPositionAdapter()
529+
{
530+
var builder = CreateBuilder();
531+
// The builder branch is "IsPositionsEmulationRequired is bool isPosEmu": a non-null
532+
// false value still matches, so the PositionMessageAdapter must be added (it is then
533+
// configured with emulation disabled). This distinguishes false (add) from null (skip).
534+
var inner = new TestPipelineAdapter { PositionsEmulation = false };
535+
var config = CreateDefaultConfig();
536+
537+
var result = await builder.BuildAsync(inner, config, CancellationToken);
538+
539+
IsNotNull(result);
540+
IsTrue(HasWrapper<PositionMessageAdapter>(result));
541+
}
542+
543+
[TestMethod]
544+
public async Task Build_WithPositionsEmulationNull_NoPositionAdapter()
545+
{
546+
var builder = CreateBuilder();
547+
// null means "not applicable": the pattern "is bool" fails, so no PositionMessageAdapter.
548+
var inner = new TestPipelineAdapter { PositionsEmulation = null };
549+
var config = CreateDefaultConfig();
550+
551+
var result = await builder.BuildAsync(inner, config, CancellationToken);
552+
553+
IsNotNull(result);
554+
IsFalse(HasWrapper<PositionMessageAdapter>(result));
555+
}
556+
507557
[TestMethod]
508558
public async Task Build_WithFillGapsBehaviour_IncludesFillGapsAdapter()
509559
{
@@ -519,6 +569,17 @@ public async Task Build_WithFillGapsBehaviour_IncludesFillGapsAdapter()
519569

520570
IsNotNull(result);
521571
IsTrue(HasWrapper<FillGapsMessageAdapter>(result));
572+
573+
// FillGaps is added last, so it is the outermost wrapper of the whole pipeline.
574+
// Here it wraps the Heartbeat wrapper (IsHeartbeatOn => true), i.e. it owns another
575+
// MessageAdapterWrapper. Per the OwnInnerAdapter contract (IMessageAdapterWrapper:
576+
// "if (OwnInnerAdapter) InnerAdapter.Dispose()"), the consumer disposes only the
577+
// outermost wrapper and relies on the cascade. Therefore the FillGaps wrapper MUST
578+
// own its inner wrapper, otherwise the inner pipeline (Heartbeat timers, channels, ...)
579+
// leaks on Dispose. Assert the correct/contracted behavior.
580+
var fillGapsWrapper = (FillGapsMessageAdapter)result;
581+
IsTrue(fillGapsWrapper.InnerAdapter is MessageAdapterWrapper, "FillGaps should wrap another wrapper in this configuration");
582+
IsTrue(fillGapsWrapper.OwnInnerAdapter, "FillGaps wrapper must own its inner wrapper so Dispose cascades through the pipeline");
522583
}
523584

524585
[TestMethod]
@@ -540,17 +601,175 @@ public async Task Build_WithExtendedInfoStorage_IncludesExtendedInfoStorageAdapt
540601

541602
#endregion
542603

604+
#region Previously Uncovered Branches
605+
606+
[TestMethod]
607+
public async Task Build_WithUseChannels_IncludesChannelAdapter()
608+
{
609+
var builder = CreateBuilder();
610+
// TestPipelineAdapter (MessageAdapter) reports UseInChannel/UseOutChannel == true by default,
611+
// so adapter.UseChannels() is true; together with config.UseChannels the ChannelMessageAdapter
612+
// must be added. Construction only wires channel event handlers, it does not start any loop.
613+
var inner = new TestPipelineAdapter();
614+
var config = CreateDefaultConfig() with
615+
{
616+
UseChannels = true,
617+
};
618+
619+
var result = await builder.BuildAsync(inner, config, CancellationToken);
620+
621+
IsNotNull(result);
622+
IsTrue(HasWrapper<ChannelMessageAdapter>(result));
623+
}
624+
625+
[TestMethod]
626+
public async Task Build_WithoutUseChannels_NoChannelAdapter()
627+
{
628+
var builder = CreateBuilder();
629+
var inner = new TestPipelineAdapter();
630+
var config = CreateDefaultConfig() with
631+
{
632+
UseChannels = false,
633+
};
634+
635+
var result = await builder.BuildAsync(inner, config, CancellationToken);
636+
637+
IsNotNull(result);
638+
IsFalse(HasWrapper<ChannelMessageAdapter>(result));
639+
}
640+
641+
[TestMethod]
642+
public async Task Build_WithGenerateOrderBookFromLevel1_IncludesLevel1DepthBuilderAdapter()
643+
{
644+
var builder = CreateBuilder();
645+
// Branch requires Level1 supported AND MarketDepth NOT supported. The default test adapter
646+
// supports both, so remove MarketDepth to satisfy the condition.
647+
var inner = new TestPipelineAdapter();
648+
inner.RemoveSupportedMarketDataType(DataType.MarketDepth);
649+
var config = CreateDefaultConfig() with
650+
{
651+
GenerateOrderBookFromLevel1 = true,
652+
};
653+
654+
var result = await builder.BuildAsync(inner, config, CancellationToken);
655+
656+
IsNotNull(result);
657+
IsTrue(HasWrapper<Level1DepthBuilderAdapter>(result));
658+
}
659+
660+
[TestMethod]
661+
public async Task Build_WithGenerateOrderBookFromLevel1_MarketDepthSupported_NoLevel1DepthBuilderAdapter()
662+
{
663+
var builder = CreateBuilder();
664+
// MarketDepth is supported (default), so the builder must NOT add Level1DepthBuilderAdapter
665+
// even though GenerateOrderBookFromLevel1 is requested.
666+
var inner = new TestPipelineAdapter();
667+
var config = CreateDefaultConfig() with
668+
{
669+
GenerateOrderBookFromLevel1 = true,
670+
};
671+
672+
var result = await builder.BuildAsync(inner, config, CancellationToken);
673+
674+
IsNotNull(result);
675+
IsFalse(HasWrapper<Level1DepthBuilderAdapter>(result));
676+
}
677+
678+
[TestMethod]
679+
public async Task Build_WithLevel1Extend_IncludesLevel1ExtendBuilderAdapter()
680+
{
681+
var builder = CreateBuilder();
682+
// Branch requires Level1 NOT supported. The default test adapter supports Level1, so remove it.
683+
var inner = new TestPipelineAdapter();
684+
inner.RemoveSupportedMarketDataType(DataType.Level1);
685+
var config = CreateDefaultConfig() with
686+
{
687+
Level1Extend = true,
688+
};
689+
690+
var result = await builder.BuildAsync(inner, config, CancellationToken);
691+
692+
IsNotNull(result);
693+
IsTrue(HasWrapper<Level1ExtendBuilderAdapter>(result));
694+
}
695+
696+
[TestMethod]
697+
public async Task Build_WithLevel1Extend_Level1Supported_NoLevel1ExtendBuilderAdapter()
698+
{
699+
var builder = CreateBuilder();
700+
// Level1 is supported (default), so Level1ExtendBuilderAdapter must NOT be added.
701+
var inner = new TestPipelineAdapter();
702+
var config = CreateDefaultConfig() with
703+
{
704+
Level1Extend = true,
705+
};
706+
707+
var result = await builder.BuildAsync(inner, config, CancellationToken);
708+
709+
IsNotNull(result);
710+
IsFalse(HasWrapper<Level1ExtendBuilderAdapter>(result));
711+
}
712+
713+
[TestMethod]
714+
public async Task Build_WithSupportStorage_IncludesStorageAdapter()
715+
{
716+
var builder = CreateBuilder();
717+
// StorageMessageAdapter is added only when SupportStorage is set AND the processor has a
718+
// non-null StorageRegistry. Provide a real in-memory registry to satisfy both.
719+
var inner = new TestPipelineAdapter();
720+
var registry = new StorageRegistry();
721+
var storageProcessor = new StorageProcessor(new StorageCoreSettings { StorageRegistry = registry }, new CandleBuilderProvider(new InMemoryExchangeInfoProvider()));
722+
var config = CreateDefaultConfig() with
723+
{
724+
SupportStorage = true,
725+
StorageProcessor = storageProcessor,
726+
};
727+
728+
var result = await builder.BuildAsync(inner, config, CancellationToken);
729+
730+
IsNotNull(result);
731+
IsTrue(HasWrapper<StorageMessageAdapter>(result));
732+
}
733+
734+
[TestMethod]
735+
public async Task Build_WithSupportStorage_NoStorageRegistry_NoStorageAdapter()
736+
{
737+
var builder = CreateBuilder();
738+
// SupportStorage requested but the processor reports no StorageRegistry: the adapter must
739+
// NOT be added (guards against a NullReferenceException-prone configuration).
740+
var inner = new TestPipelineAdapter();
741+
var storageProcessor = new StorageProcessor(new StorageCoreSettings(), new CandleBuilderProvider(new InMemoryExchangeInfoProvider()));
742+
var config = CreateDefaultConfig() with
743+
{
744+
SupportStorage = true,
745+
StorageProcessor = storageProcessor,
746+
};
747+
748+
var result = await builder.BuildAsync(inner, config, CancellationToken);
749+
750+
IsNotNull(result);
751+
IsFalse(HasWrapper<StorageMessageAdapter>(result));
752+
}
753+
754+
#endregion
755+
543756
#region OwnInnerAdapter Flag
544757

545758
[TestMethod]
546759
public async Task Build_OwnInnerAdapterFlagSetCorrectly()
547760
{
548761
var builder = CreateBuilder();
549762
var inner = new TestPipelineAdapter();
763+
var fillGaps = new Mock<IFillGapsBehaviour>();
550764
var config = CreateDefaultConfig() with
551765
{
552766
SupportOffline = true,
553767
LatencyManager = new LatencyManager(new LatencyManagerState()),
768+
// FillGaps is the only builder branch that does NOT call ApplyOwnInner, so it
769+
// exercises the outermost wrapper whose OwnInnerAdapter flag must be set. Including
770+
// it here ensures the contract is verified for every branch, not just the ones that
771+
// already call ApplyOwnInner.
772+
FillGapsBehaviour = fillGaps.Object,
554773
};
555774

556775
var result = await builder.BuildAsync(inner, config, CancellationToken);
@@ -596,14 +815,52 @@ public async Task Build_CorrectWrapperOrder_HeartbeatFirst()
596815

597816
var result = await builder.BuildAsync(inner, config, CancellationToken);
598817

599-
// First wrapper from outside should be FillGaps (if enabled), then other wrappers,
600-
// and Heartbeat should be closest to the inner adapter (first added)
818+
// Heartbeat is added first by the builder, so it must be closest to the inner adapter
819+
// (last in the outer->inner list returned by GetWrapperTypes). No FillGaps in this config.
601820
var wrapperTypes = GetWrapperTypes(result).ToList();
602821

603822
// The HeartbeatMessageAdapter should be at the end of the list (closest to inner adapter)
604823
AreEqual(typeof(HeartbeatMessageAdapter), wrapperTypes.Last());
605824
}
606825

826+
[TestMethod]
827+
public async Task Build_RelativeWrapperOrder_IsStable()
828+
{
829+
var builder = CreateBuilder();
830+
var inner = new TestPipelineAdapter { SupportSubscriptions = true, SupportExecutionsPnL = false };
831+
var config = CreateDefaultConfig() with
832+
{
833+
PnLManager = new PnLManager(),
834+
CommissionManager = new CommissionManager(),
835+
};
836+
837+
var result = await builder.BuildAsync(inner, config, CancellationToken);
838+
839+
// GetWrapperTypes returns the chain from the outermost wrapper to the innermost.
840+
// Wrappers are added inner-to-outer in builder source order, so a wrapper added later
841+
// sits more to the outside (smaller index in this list).
842+
var wrapperTypes = GetWrapperTypes(result).ToList();
843+
844+
int IndexOf<T>()
845+
{
846+
var idx = wrapperTypes.IndexOf(typeof(T));
847+
IsTrue(idx >= 0, $"{typeof(T).Name} expected in the pipeline");
848+
return idx;
849+
}
850+
851+
// SubscriptionMessageAdapter is added after SubscriptionOnlineMessageAdapter, so it must be
852+
// the more outer (closer to the consumer) of the two subscription wrappers.
853+
IsTrue(IndexOf<SubscriptionMessageAdapter>() < IndexOf<SubscriptionOnlineMessageAdapter>(),
854+
"SubscriptionMessageAdapter must wrap (be outside) SubscriptionOnlineMessageAdapter");
855+
856+
// CommissionMessageAdapter is added after PnLMessageAdapter, so Commission is more outer than PnL.
857+
IsTrue(IndexOf<CommissionMessageAdapter>() < IndexOf<PnLMessageAdapter>(),
858+
"CommissionMessageAdapter must wrap (be outside) PnLMessageAdapter");
859+
860+
// Heartbeat is added first, so it stays innermost (last in the outer->inner list).
861+
AreEqual(typeof(HeartbeatMessageAdapter), wrapperTypes.Last());
862+
}
863+
607864
[TestMethod]
608865
public async Task Build_CorrectWrapperOrder_OfflineAfterHeartbeat()
609866
{

0 commit comments

Comments
 (0)