From 3d3b2413c471774d2df97276c2c4388f83d7b1fe Mon Sep 17 00:00:00 2001 From: Brad Wilson Date: Wed, 30 Oct 2024 11:49:47 -0700 Subject: [PATCH] Back-fill tests for VsDiscoverySink --- .../Sinks/VsDiscoverySink.cs | 53 ++---------- .../Sinks/VsDiscoverySinkTests.cs | 85 +++++++++++++++++++ .../Utility/SpyLoggerHelper.cs | 14 +++ .../Utility/SpyMessageLogger.cs | 13 +++ .../Utility/TestData.cs | 53 ++++++++++++ 5 files changed, 170 insertions(+), 48 deletions(-) create mode 100644 test/test.xunit.runner.visualstudio/Sinks/VsDiscoverySinkTests.cs create mode 100644 test/test.xunit.runner.visualstudio/Utility/SpyLoggerHelper.cs create mode 100644 test/test.xunit.runner.visualstudio/Utility/SpyMessageLogger.cs create mode 100644 test/test.xunit.runner.visualstudio/Utility/TestData.cs diff --git a/src/xunit.runner.visualstudio/Sinks/VsDiscoverySink.cs b/src/xunit.runner.visualstudio/Sinks/VsDiscoverySink.cs index 1dcfb093..acd27ed9 100644 --- a/src/xunit.runner.visualstudio/Sinks/VsDiscoverySink.cs +++ b/src/xunit.runner.visualstudio/Sinks/VsDiscoverySink.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Globalization; -using System.Linq.Expressions; using System.Security.Cryptography; using System.Text; using System.Threading; @@ -22,7 +21,6 @@ public sealed class VsDiscoverySink : IVsDiscoverySink, IDisposable const int MaximumDisplayNameLength = 447; const int TestCaseBatchSize = 100; - static readonly Action? addTraitThunk = GetAddTraitThunk(); static readonly Uri uri = new(Constants.ExecutorUri); readonly Func cancelThunk; @@ -71,7 +69,7 @@ public void Dispose() => { if (testCase.TestClassName is null) { - logger.LogErrorWithSource(source, "Error creating Visual Studio test case for {0}: TestClassWithNamespace is null", testCase.TestCaseDisplayName); + logger.LogErrorWithSource(source, "Error creating Visual Studio test case for {0}: TestClassName is null", testCase.TestCaseDisplayName); return null; } @@ -103,14 +101,10 @@ public void Dispose() => result.CodeFilePath = testCase.SourceFilePath; result.LineNumber = testCase.SourceLineNumber.GetValueOrDefault(); - if (addTraitThunk is not null) - { - var traits = testCase.Traits; - - foreach (var key in traits.Keys) - foreach (var value in traits[key]) - addTraitThunk(result, key, value); - } + var traits = testCase.Traits; + foreach (var key in traits.Keys) + foreach (var value in traits[key]) + result.Traits.Add(key, value); return result; } @@ -143,43 +137,6 @@ public int Finish() return TotalTests; } - static Action? GetAddTraitThunk() - { - try - { - var testCaseType = typeof(VsTestCase); - var stringType = typeof(string); - -#if NETCOREAPP - var property = testCaseType.GetRuntimeProperty("Traits"); -#else - var property = testCaseType.GetProperty("Traits"); -#endif - if (property is null) - return null; - -#if NETCOREAPP - var method = property.PropertyType.GetRuntimeMethod("Add", [typeof(string), typeof(string)]); -#else - var method = property.PropertyType.GetMethod("Add", [typeof(string), typeof(string)]); -#endif - if (method is null) - return null; - - var thisParam = Expression.Parameter(testCaseType, "this"); - var nameParam = Expression.Parameter(stringType, "name"); - var valueParam = Expression.Parameter(stringType, "value"); - var instance = Expression.Property(thisParam, property); - var body = Expression.Call(instance, method, [nameParam, valueParam]); - - return Expression.Lambda>(body, thisParam, nameParam, valueParam).Compile(); - } - catch (Exception) - { - return null; - } - } - void HandleCancellation(MessageHandlerArgs args) { if (cancelThunk()) diff --git a/test/test.xunit.runner.visualstudio/Sinks/VsDiscoverySinkTests.cs b/test/test.xunit.runner.visualstudio/Sinks/VsDiscoverySinkTests.cs new file mode 100644 index 00000000..ac46ddc1 --- /dev/null +++ b/test/test.xunit.runner.visualstudio/Sinks/VsDiscoverySinkTests.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Xunit; +using Xunit.Runner.VisualStudio; + +public class VsDiscoverySinkTests +{ + public class CreateVsTestCase + { + readonly SpyLoggerHelper logger = SpyLoggerHelper.Create(); + readonly TestPlatformContext testPlatformContext = new TestPlatformContext { DesignMode = false }; + + [Fact] + public void MustSetTestClassName() + { + var testCase = TestData.TestCaseDiscovered(testClassName: null); + + var vsTestCase = VsDiscoverySink.CreateVsTestCase("source", testCase, logger, testPlatformContext); + + Assert.Null(vsTestCase); + var message = Assert.Single(logger.Messages); + Assert.Equal("[Error] [xUnit.net 00:00:00.00] source: Error creating Visual Studio test case for test-case-display-name: TestClassName is null", message); + } + + [Theory] + [InlineData(false, null)] + [InlineData(true, "serialization")] + public void StandardData( + bool designMode, + string? expectedSerialization) + { + var testCase = TestData.TestCaseDiscovered( + sourceFilePath: "/source/file.cs", + sourceLineNumber: 42, + traits: new Dictionary> + { + { "foo", ["baz", "bar"] }, + { "biff", ["42"] }, + } + ); + var testPlatformContext = new TestPlatformContext { DesignMode = designMode }; + + var vsTestCase = VsDiscoverySink.CreateVsTestCase("source", testCase, logger, testPlatformContext); + + Assert.NotNull(vsTestCase); + + // Standard VSTest properties + Assert.Equal("/source/file.cs", vsTestCase.CodeFilePath); + Assert.Equal("test-case-display-name", vsTestCase.DisplayName); + Assert.Equal(Constants.ExecutorUri, vsTestCase.ExecutorUri.OriginalString); + Assert.Equal("test-class-name.test-method", vsTestCase.FullyQualifiedName); + Assert.NotEqual(Guid.Empty, vsTestCase.Id); // Computed at runtime, just need to ensure it's set + Assert.Equal(42, vsTestCase.LineNumber); + Assert.Equal("source", vsTestCase.Source); + Assert.Collection( + vsTestCase.Traits.Select(t => $"'{t.Name}' = '{t.Value}'").OrderBy(x => x), + trait => Assert.Equal("'biff' = '42'", trait), + trait => Assert.Equal("'foo' = 'bar'", trait), + trait => Assert.Equal("'foo' = 'baz'", trait) + ); + + // xUnit.net extension properties + Assert.Equal(expectedSerialization, vsTestCase.GetPropertyValue(VsTestRunner.TestCaseSerializationProperty)); + Assert.Equal("test-case-id", vsTestCase.GetPropertyValue(VsTestRunner.TestCaseUniqueIDProperty)); + Assert.Equal(false, vsTestCase.GetPropertyValue(VsTestRunner.TestCaseExplicitProperty)); + } + + [Theory] + [InlineData(null, "test-method")] + [InlineData(new[] { "Type1", "Type2" }, "test-method(Type1,Type2)")] + public void SetsManagedTypeAndMethodProperties( + string[]? parameterTypes, + string expectedManagedMethodName) + { + var testCase = TestData.TestCaseDiscovered(testMethodParameterTypes: parameterTypes); + + var vsTestCase = VsDiscoverySink.CreateVsTestCase("source", testCase, logger, testPlatformContext); + + Assert.NotNull(vsTestCase); + Assert.Equal("test-class-name", vsTestCase.GetPropertyValue(VsTestRunner.ManagedTypeProperty)); + Assert.Equal(expectedManagedMethodName, vsTestCase.GetPropertyValue(VsTestRunner.ManagedMethodProperty)); + } + } +} diff --git a/test/test.xunit.runner.visualstudio/Utility/SpyLoggerHelper.cs b/test/test.xunit.runner.visualstudio/Utility/SpyLoggerHelper.cs new file mode 100644 index 00000000..fe934cbd --- /dev/null +++ b/test/test.xunit.runner.visualstudio/Utility/SpyLoggerHelper.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using System.Diagnostics; +using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; + +namespace Xunit.Runner.VisualStudio; + +public class SpyLoggerHelper(SpyMessageLogger logger, Stopwatch stopwatch) : + LoggerHelper(logger, stopwatch) +{ + public IReadOnlyCollection Messages => logger.Messages; + + public static SpyLoggerHelper Create() => + new(new(), new()); +} diff --git a/test/test.xunit.runner.visualstudio/Utility/SpyMessageLogger.cs b/test/test.xunit.runner.visualstudio/Utility/SpyMessageLogger.cs new file mode 100644 index 00000000..65408330 --- /dev/null +++ b/test/test.xunit.runner.visualstudio/Utility/SpyMessageLogger.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; + +public class SpyMessageLogger : IMessageLogger +{ + public readonly List Messages = []; + + public void SendMessage( + TestMessageLevel testMessageLevel, + string message) => + Messages.Add($"[{testMessageLevel}] {message}"); +} diff --git a/test/test.xunit.runner.visualstudio/Utility/TestData.cs b/test/test.xunit.runner.visualstudio/Utility/TestData.cs new file mode 100644 index 00000000..7a7f84f5 --- /dev/null +++ b/test/test.xunit.runner.visualstudio/Utility/TestData.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using Xunit.Runner.Common; +using Xunit.Sdk; + +internal static class TestData +{ + static readonly IReadOnlyDictionary> EmptyTraits = new Dictionary>(); + + public static ITestCaseDiscovered TestCaseDiscovered( + string assemblyUniqueID = "assembly-id", + bool @explicit = false, + string serialization = "serialization", + string? skipReason = null, + string? sourceFilePath = null, + int? sourceLineNumber = null, + string testCaseDisplayName = "test-case-display-name", + string testCaseUniqueID = "test-case-id", + int? testClassMetadataToken = null, + string? testClassName = "test-class-name", + string? testClassNamespace = null, + string? testClassSimpleName = "test-class-simple-name", + string? testClassUniqueID = "test-class-id", + string testCollectionUniqueID = "test-collection-id", + int? testMethodMetadataToken = null, + string? testMethodName = "test-method", + string[]? testMethodParameterTypes = null, + string? testMethodReturnType = null, + string? testMethodUniqueID = "test-method-id", + IReadOnlyDictionary>? traits = null) => + new TestCaseDiscovered + { + AssemblyUniqueID = assemblyUniqueID, + Explicit = @explicit, + Serialization = serialization, + SkipReason = skipReason, + SourceFilePath = sourceFilePath, + SourceLineNumber = sourceLineNumber, + TestCaseDisplayName = testCaseDisplayName, + TestCaseUniqueID = testCaseUniqueID, + TestClassMetadataToken = testClassMetadataToken, + TestClassName = testClassName, + TestClassNamespace = testClassNamespace, + TestClassSimpleName = testClassSimpleName, + TestClassUniqueID = testClassUniqueID, + TestCollectionUniqueID = testCollectionUniqueID, + TestMethodMetadataToken = testMethodMetadataToken, + TestMethodName = testMethodName, + TestMethodParameterTypesVSTest = testMethodParameterTypes, + TestMethodReturnTypeVSTest = testMethodReturnType, + TestMethodUniqueID = testMethodUniqueID, + Traits = traits ?? EmptyTraits, + }; +}