diff --git a/src/Platform/Microsoft.Testing.Extensions.VideoRecorder/VideoRecorderSessionHandler.cs b/src/Platform/Microsoft.Testing.Extensions.VideoRecorder/VideoRecorderSessionHandler.cs index 2b95051bf4..0e58553feb 100644 --- a/src/Platform/Microsoft.Testing.Extensions.VideoRecorder/VideoRecorderSessionHandler.cs +++ b/src/Platform/Microsoft.Testing.Extensions.VideoRecorder/VideoRecorderSessionHandler.cs @@ -125,7 +125,7 @@ public Task ConsumeAsync(IDataProducer dataProducer, IData value, CancellationTo // A node carries a single state property in practice; use FirstOrDefault rather than // SingleOrDefault so a malformed producer can't throw out of the consumer pump. - TestNodeStateProperty? state = update.TestNode.Properties.OfType().FirstOrDefault(); + TestNodeStateProperty? state = update.TestNode.Properties.FirstOrDefault(); if (state is null) { return Task.CompletedTask; @@ -477,7 +477,7 @@ private static bool OverlapsAnyFailedWindow(VideoSegment segment, double[] faile private (DateTimeOffset Start, DateTimeOffset End) ResolveTiming(TestNodeUpdateMessage update, string testUid) { - TimingProperty? timing = update.TestNode.Properties.OfType().FirstOrDefault(); + TimingProperty? timing = update.TestNode.Properties.FirstOrDefault(); if (timing is not null) { return (timing.GlobalTiming.StartTime, timing.GlobalTiming.EndTime); diff --git a/src/Platform/Microsoft.Testing.Platform/Messages/PropertyBag.cs b/src/Platform/Microsoft.Testing.Platform/Messages/PropertyBag.cs index 2027f78d76..be855f9bad 100644 --- a/src/Platform/Microsoft.Testing.Platform/Messages/PropertyBag.cs +++ b/src/Platform/Microsoft.Testing.Platform/Messages/PropertyBag.cs @@ -213,6 +213,44 @@ public bool Any() return found; } + /// + /// Returns the first property of the type, or default if none is found. + /// Unlike , this method does not throw when multiple properties of the + /// same type are present — it simply returns the first one encountered. + /// + /// The type of the property. + /// The first property of the given type, or default if none is found. + public TProperty? FirstOrDefault() + where TProperty : IProperty + { + if (_testNodeStateProperty is TProperty testNodeStateProperty) + { + return testNodeStateProperty; + } + + // We don't want to walk the linked list if we know that we're looking for a TestNodeStateProperty. + if (typeof(TestNodeStateProperty).IsAssignableFrom(typeof(TProperty))) + { + return default; + } + + // Direct linked-list walk: avoids the array allocation from OfType() and the subsequent + // LINQ FirstOrDefault() call. Early-returns on the first match so no duplicate tracking + // is needed (unlike SingleOrDefault()). + Property? current = _property; + while (current is not null) + { + if (current.Current is TProperty match) + { + return match; + } + + current = current.Next; + } + + return default; + } + /// /// Returns the only property of the type, and throws an exception if there is not exactly one element. /// diff --git a/src/Platform/Microsoft.Testing.Platform/PublicAPI/PublicAPI.Unshipped.txt b/src/Platform/Microsoft.Testing.Platform/PublicAPI/PublicAPI.Unshipped.txt index ce5518ec82..f76dff0c1b 100644 --- a/src/Platform/Microsoft.Testing.Platform/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/Platform/Microsoft.Testing.Platform/PublicAPI/PublicAPI.Unshipped.txt @@ -1,5 +1,6 @@ #nullable enable [TPEXP]Microsoft.Testing.Platform.Extensions.IBlockingDataConsumer +Microsoft.Testing.Platform.Extensions.Messages.PropertyBag.FirstOrDefault() -> TProperty? [TPEXP]Microsoft.Testing.Platform.Extensions.TestHostControllers.ITestHostHandle [TPEXP]Microsoft.Testing.Platform.Extensions.TestHostControllers.ITestHostHandle.ExitCode.get -> int [TPEXP]Microsoft.Testing.Platform.Extensions.TestHostControllers.ITestHostHandle.HasExited.get -> bool diff --git a/test/UnitTests/Microsoft.Testing.Platform.UnitTests/Messages/PropertyBagTests.cs b/test/UnitTests/Microsoft.Testing.Platform.UnitTests/Messages/PropertyBagTests.cs index 8c6aed9105..edd371b596 100644 --- a/test/UnitTests/Microsoft.Testing.Platform.UnitTests/Messages/PropertyBagTests.cs +++ b/test/UnitTests/Microsoft.Testing.Platform.UnitTests/Messages/PropertyBagTests.cs @@ -95,6 +95,81 @@ public void SingleOrDefault_Should_Return_CorrectObject() Assert.ThrowsExactly(property.SingleOrDefault); } + [TestMethod] + public void FirstOrDefault_Should_Return_TestNodeStateProperty_WhenPresent() + { + PropertyBag property = new(); + property.Add(PassedTestNodeStateProperty.CachedInstance); + + // Exercises the _testNodeStateProperty fast path. + Assert.AreEqual(PassedTestNodeStateProperty.CachedInstance, property.FirstOrDefault()); + } + + [TestMethod] + public void FirstOrDefault_Should_Return_Null_WhenTestNodeStateProperty_NotPresent() + { + PropertyBag property = new(); + property.Add(new DummyProperty()); + + // Exercises the IsAssignableFrom early-return without walking the linked list. + Assert.IsNull(property.FirstOrDefault()); + } + + [TestMethod] + public void FirstOrDefault_Should_Return_Null_WhenSubtype_NotPresent() + { + PropertyBag property = new(); + property.Add(new FailedTestNodeStateProperty()); + + // Exercises the subtype guard: asking for a different TestNodeStateProperty subtype must return null. + Assert.IsNull(property.FirstOrDefault()); + } + + [TestMethod] + public void FirstOrDefault_Should_Return_CorrectObject_WhenSingleMatchExists() + { + PropertyBag property = new(); + DummyProperty prop = new(); + property.Add(prop); + property.Add(PassedTestNodeStateProperty.CachedInstance); + + Assert.AreEqual(prop, property.FirstOrDefault()); + } + + [TestMethod] + public void FirstOrDefault_Should_Return_FirstObject_WhenMultipleMatchesExist() + { + PropertyBag property = new(); + DummyProperty prop1 = new(); + DummyProperty prop2 = new(); + property.Add(prop1); + property.Add(prop2); + + // Unlike SingleOrDefault, FirstOrDefault must NOT throw when multiple matches exist. + DummyProperty? result = property.FirstOrDefault(); + Assert.IsNotNull(result); + Assert.IsTrue(result == prop1 || result == prop2); + } + + [TestMethod] + public void FirstOrDefault_Should_Return_Null_WhenNoMatchExists() + { + PropertyBag property = new(); + property.Add(new DummyProperty()); + + // Exercises the linked-list miss path. + Assert.IsNull(property.FirstOrDefault()); + } + + [TestMethod] + public void FirstOrDefault_Should_Return_Null_WhenBagIsEmpty() + { + PropertyBag property = new(); + + Assert.IsNull(property.FirstOrDefault()); + Assert.IsNull(property.FirstOrDefault()); + } + [TestMethod] public void Single_Should_Return_CorrectObject() {