Skip to content

Commit 3d3b241

Browse files
committed
Back-fill tests for VsDiscoverySink
1 parent e2d5be2 commit 3d3b241

File tree

5 files changed

+170
-48
lines changed

5 files changed

+170
-48
lines changed

src/xunit.runner.visualstudio/Sinks/VsDiscoverySink.cs

Lines changed: 5 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Globalization;
4-
using System.Linq.Expressions;
54
using System.Security.Cryptography;
65
using System.Text;
76
using System.Threading;
@@ -22,7 +21,6 @@ public sealed class VsDiscoverySink : IVsDiscoverySink, IDisposable
2221
const int MaximumDisplayNameLength = 447;
2322
const int TestCaseBatchSize = 100;
2423

25-
static readonly Action<VsTestCase, string, string>? addTraitThunk = GetAddTraitThunk();
2624
static readonly Uri uri = new(Constants.ExecutorUri);
2725

2826
readonly Func<bool> cancelThunk;
@@ -71,7 +69,7 @@ public void Dispose() =>
7169
{
7270
if (testCase.TestClassName is null)
7371
{
74-
logger.LogErrorWithSource(source, "Error creating Visual Studio test case for {0}: TestClassWithNamespace is null", testCase.TestCaseDisplayName);
72+
logger.LogErrorWithSource(source, "Error creating Visual Studio test case for {0}: TestClassName is null", testCase.TestCaseDisplayName);
7573
return null;
7674
}
7775

@@ -103,14 +101,10 @@ public void Dispose() =>
103101
result.CodeFilePath = testCase.SourceFilePath;
104102
result.LineNumber = testCase.SourceLineNumber.GetValueOrDefault();
105103

106-
if (addTraitThunk is not null)
107-
{
108-
var traits = testCase.Traits;
109-
110-
foreach (var key in traits.Keys)
111-
foreach (var value in traits[key])
112-
addTraitThunk(result, key, value);
113-
}
104+
var traits = testCase.Traits;
105+
foreach (var key in traits.Keys)
106+
foreach (var value in traits[key])
107+
result.Traits.Add(key, value);
114108

115109
return result;
116110
}
@@ -143,43 +137,6 @@ public int Finish()
143137
return TotalTests;
144138
}
145139

146-
static Action<VsTestCase, string, string>? GetAddTraitThunk()
147-
{
148-
try
149-
{
150-
var testCaseType = typeof(VsTestCase);
151-
var stringType = typeof(string);
152-
153-
#if NETCOREAPP
154-
var property = testCaseType.GetRuntimeProperty("Traits");
155-
#else
156-
var property = testCaseType.GetProperty("Traits");
157-
#endif
158-
if (property is null)
159-
return null;
160-
161-
#if NETCOREAPP
162-
var method = property.PropertyType.GetRuntimeMethod("Add", [typeof(string), typeof(string)]);
163-
#else
164-
var method = property.PropertyType.GetMethod("Add", [typeof(string), typeof(string)]);
165-
#endif
166-
if (method is null)
167-
return null;
168-
169-
var thisParam = Expression.Parameter(testCaseType, "this");
170-
var nameParam = Expression.Parameter(stringType, "name");
171-
var valueParam = Expression.Parameter(stringType, "value");
172-
var instance = Expression.Property(thisParam, property);
173-
var body = Expression.Call(instance, method, [nameParam, valueParam]);
174-
175-
return Expression.Lambda<Action<VsTestCase, string, string>>(body, thisParam, nameParam, valueParam).Compile();
176-
}
177-
catch (Exception)
178-
{
179-
return null;
180-
}
181-
}
182-
183140
void HandleCancellation(MessageHandlerArgs args)
184141
{
185142
if (cancelThunk())
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using Xunit;
5+
using Xunit.Runner.VisualStudio;
6+
7+
public class VsDiscoverySinkTests
8+
{
9+
public class CreateVsTestCase
10+
{
11+
readonly SpyLoggerHelper logger = SpyLoggerHelper.Create();
12+
readonly TestPlatformContext testPlatformContext = new TestPlatformContext { DesignMode = false };
13+
14+
[Fact]
15+
public void MustSetTestClassName()
16+
{
17+
var testCase = TestData.TestCaseDiscovered(testClassName: null);
18+
19+
var vsTestCase = VsDiscoverySink.CreateVsTestCase("source", testCase, logger, testPlatformContext);
20+
21+
Assert.Null(vsTestCase);
22+
var message = Assert.Single(logger.Messages);
23+
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);
24+
}
25+
26+
[Theory]
27+
[InlineData(false, null)]
28+
[InlineData(true, "serialization")]
29+
public void StandardData(
30+
bool designMode,
31+
string? expectedSerialization)
32+
{
33+
var testCase = TestData.TestCaseDiscovered(
34+
sourceFilePath: "/source/file.cs",
35+
sourceLineNumber: 42,
36+
traits: new Dictionary<string, IReadOnlyCollection<string>>
37+
{
38+
{ "foo", ["baz", "bar"] },
39+
{ "biff", ["42"] },
40+
}
41+
);
42+
var testPlatformContext = new TestPlatformContext { DesignMode = designMode };
43+
44+
var vsTestCase = VsDiscoverySink.CreateVsTestCase("source", testCase, logger, testPlatformContext);
45+
46+
Assert.NotNull(vsTestCase);
47+
48+
// Standard VSTest properties
49+
Assert.Equal("/source/file.cs", vsTestCase.CodeFilePath);
50+
Assert.Equal("test-case-display-name", vsTestCase.DisplayName);
51+
Assert.Equal(Constants.ExecutorUri, vsTestCase.ExecutorUri.OriginalString);
52+
Assert.Equal("test-class-name.test-method", vsTestCase.FullyQualifiedName);
53+
Assert.NotEqual(Guid.Empty, vsTestCase.Id); // Computed at runtime, just need to ensure it's set
54+
Assert.Equal(42, vsTestCase.LineNumber);
55+
Assert.Equal("source", vsTestCase.Source);
56+
Assert.Collection(
57+
vsTestCase.Traits.Select(t => $"'{t.Name}' = '{t.Value}'").OrderBy(x => x),
58+
trait => Assert.Equal("'biff' = '42'", trait),
59+
trait => Assert.Equal("'foo' = 'bar'", trait),
60+
trait => Assert.Equal("'foo' = 'baz'", trait)
61+
);
62+
63+
// xUnit.net extension properties
64+
Assert.Equal(expectedSerialization, vsTestCase.GetPropertyValue(VsTestRunner.TestCaseSerializationProperty));
65+
Assert.Equal("test-case-id", vsTestCase.GetPropertyValue(VsTestRunner.TestCaseUniqueIDProperty));
66+
Assert.Equal(false, vsTestCase.GetPropertyValue(VsTestRunner.TestCaseExplicitProperty));
67+
}
68+
69+
[Theory]
70+
[InlineData(null, "test-method")]
71+
[InlineData(new[] { "Type1", "Type2" }, "test-method(Type1,Type2)")]
72+
public void SetsManagedTypeAndMethodProperties(
73+
string[]? parameterTypes,
74+
string expectedManagedMethodName)
75+
{
76+
var testCase = TestData.TestCaseDiscovered(testMethodParameterTypes: parameterTypes);
77+
78+
var vsTestCase = VsDiscoverySink.CreateVsTestCase("source", testCase, logger, testPlatformContext);
79+
80+
Assert.NotNull(vsTestCase);
81+
Assert.Equal("test-class-name", vsTestCase.GetPropertyValue(VsTestRunner.ManagedTypeProperty));
82+
Assert.Equal(expectedManagedMethodName, vsTestCase.GetPropertyValue(VsTestRunner.ManagedMethodProperty));
83+
}
84+
}
85+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using System.Collections.Generic;
2+
using System.Diagnostics;
3+
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging;
4+
5+
namespace Xunit.Runner.VisualStudio;
6+
7+
public class SpyLoggerHelper(SpyMessageLogger logger, Stopwatch stopwatch) :
8+
LoggerHelper(logger, stopwatch)
9+
{
10+
public IReadOnlyCollection<string> Messages => logger.Messages;
11+
12+
public static SpyLoggerHelper Create() =>
13+
new(new(), new());
14+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using System.Collections.Generic;
2+
3+
namespace Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging;
4+
5+
public class SpyMessageLogger : IMessageLogger
6+
{
7+
public readonly List<string> Messages = [];
8+
9+
public void SendMessage(
10+
TestMessageLevel testMessageLevel,
11+
string message) =>
12+
Messages.Add($"[{testMessageLevel}] {message}");
13+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
using System.Collections.Generic;
2+
using Xunit.Runner.Common;
3+
using Xunit.Sdk;
4+
5+
internal static class TestData
6+
{
7+
static readonly IReadOnlyDictionary<string, IReadOnlyCollection<string>> EmptyTraits = new Dictionary<string, IReadOnlyCollection<string>>();
8+
9+
public static ITestCaseDiscovered TestCaseDiscovered(
10+
string assemblyUniqueID = "assembly-id",
11+
bool @explicit = false,
12+
string serialization = "serialization",
13+
string? skipReason = null,
14+
string? sourceFilePath = null,
15+
int? sourceLineNumber = null,
16+
string testCaseDisplayName = "test-case-display-name",
17+
string testCaseUniqueID = "test-case-id",
18+
int? testClassMetadataToken = null,
19+
string? testClassName = "test-class-name",
20+
string? testClassNamespace = null,
21+
string? testClassSimpleName = "test-class-simple-name",
22+
string? testClassUniqueID = "test-class-id",
23+
string testCollectionUniqueID = "test-collection-id",
24+
int? testMethodMetadataToken = null,
25+
string? testMethodName = "test-method",
26+
string[]? testMethodParameterTypes = null,
27+
string? testMethodReturnType = null,
28+
string? testMethodUniqueID = "test-method-id",
29+
IReadOnlyDictionary<string, IReadOnlyCollection<string>>? traits = null) =>
30+
new TestCaseDiscovered
31+
{
32+
AssemblyUniqueID = assemblyUniqueID,
33+
Explicit = @explicit,
34+
Serialization = serialization,
35+
SkipReason = skipReason,
36+
SourceFilePath = sourceFilePath,
37+
SourceLineNumber = sourceLineNumber,
38+
TestCaseDisplayName = testCaseDisplayName,
39+
TestCaseUniqueID = testCaseUniqueID,
40+
TestClassMetadataToken = testClassMetadataToken,
41+
TestClassName = testClassName,
42+
TestClassNamespace = testClassNamespace,
43+
TestClassSimpleName = testClassSimpleName,
44+
TestClassUniqueID = testClassUniqueID,
45+
TestCollectionUniqueID = testCollectionUniqueID,
46+
TestMethodMetadataToken = testMethodMetadataToken,
47+
TestMethodName = testMethodName,
48+
TestMethodParameterTypesVSTest = testMethodParameterTypes,
49+
TestMethodReturnTypeVSTest = testMethodReturnType,
50+
TestMethodUniqueID = testMethodUniqueID,
51+
Traits = traits ?? EmptyTraits,
52+
};
53+
}

0 commit comments

Comments
 (0)