From c10de50fba126bdcf6c4a3123a3829d1fa2b095c Mon Sep 17 00:00:00 2001 From: Spencer Farley <2847259+farlee2121@users.noreply.github.com> Date: Wed, 4 Oct 2023 18:12:04 -0500 Subject: [PATCH 1/2] Support running single theory cases in xunit in test explorer --- src/Components/TestExplorer.fs | 63 ++++++++++++++++++++++++++++------ 1 file changed, 52 insertions(+), 11 deletions(-) diff --git a/src/Components/TestExplorer.fs b/src/Components/TestExplorer.fs index 1ac94dab..40dc8632 100644 --- a/src/Components/TestExplorer.fs +++ b/src/Components/TestExplorer.fs @@ -75,6 +75,10 @@ module ListExt = let mapPartitioned f (left, right) = (left |> List.map f), (right |> List.map f) +module Dict = + let tryGet (d: Collections.Generic.IDictionary<'key, 'value>) (key) : 'value option = + if d.ContainsKey(key) then Some d[key] else None + module CancellationToken = let mergeTokens (tokens: CancellationToken list) = let tokenSource = vscode.CancellationTokenSource.Create() @@ -227,6 +231,12 @@ module TestFrameworkId = [] let MsTest = "MSTest" + [] + let XUnit = "XUnit" + + [] + let Expecto = "Expecto" + type TestResult = { FullTestName: string Outcome: TestResultOutcome @@ -270,6 +280,10 @@ module TrxParser = Some TestFrameworkId.NUnit else if String.startWith "executor://mstest" adapterTypeName then Some TestFrameworkId.MsTest + else if String.startWith "executor://xunit" adapterTypeName then + Some TestFrameworkId.XUnit + else if String.startWith "executor://yolodev" adapterTypeName then + Some TestFrameworkId.Expecto else None @@ -1065,7 +1079,7 @@ module TestDiscovery = trxTestsPerProject |> Array.map (fun (project, trxDefs) -> let projectPath = ProjectPath.ofString project.Project - let heirarchy = TrxParser.inferHierarchy trxDefs + let hierarchy = TrxParser.inferHierarchy trxDefs let fromTrxDef (hierarchy: TestName.NameHierarchy) = // NOTE: A project could have multiple test frameworks, but we only track NUnit for now to work around a defect @@ -1086,11 +1100,10 @@ module TestDiscovery = TestItem.fromNamedHierarchy testItemFactory tryGetLocation projectPath hierarchy - let projectTests = heirarchy |> Array.map fromTrxDef + let projectTests = hierarchy |> Array.map fromTrxDef TestItem.fromProject testItemFactory projectPath project.Info.TargetFramework projectTests) - treeItems module Interactions = @@ -1178,17 +1191,21 @@ module Interactions = let testToFilterExpression (test: TestItem) = let fullTestName = TestItem.getFullName test.id - let fullName = escapeFilterExpression fullTestName + let escapedTestName = escapeFilterExpression fullTestName - if fullName.Contains(" ") && test.TestFramework = TestFrameworkId.NUnit then + if escapedTestName.Contains(" ") && test.TestFramework = TestFrameworkId.NUnit then // workaround for https://github.com/nunit/nunit3-vs-adapter/issues/876 // Potentially we are going to run multiple tests that match this filter - let testPart = fullName.Split(' ').[0] + let testPart = escapedTestName.Split(' ').[0] $"(FullyQualifiedName~{testPart})" + else if test.TestFramework = TestFrameworkId.XUnit then + // NOTE: using DisplayName allows single theory cases to be run for xUnit + let operator = if test.children.size = 0 then "=" else "~" + $"(DisplayName{operator}{escapedTestName})" else if test.children.size = 0 then - $"(FullyQualifiedName={fullName})" + $"(FullyQualifiedName={escapedTestName})" else - $"(FullyQualifiedName~{fullName})" + $"(FullyQualifiedName~{escapedTestName})" let filterExpression = tests |> Array.map testToFilterExpression |> String.concat "|" @@ -1280,7 +1297,6 @@ module Interactions = tryFind "Expected:", tryFind "But was:" - { FullTestName = trxResult.UnitTest.FullName Outcome = !!trxResult.UnitTestResult.Outcome ErrorMessage = trxResult.UnitTestResult.Output.ErrorInfo.Message @@ -1410,10 +1426,18 @@ module Interactions = |> Option.map (fun p -> p.kind = TestRunProfileKind.Debug) |> Option.defaultValue false + let hasIncludeFilter = + let isOnlyProjectSelected = + match tests with + | [| single |] -> single.id = (TestItem.constructProjectRootId project.Project) + | _ -> false + + (Option.isSome runRequest.``include``) && (not isOnlyProjectSelected) + { ProjectPath = projectPath TargetFramework = project.Info.TargetFramework ShouldDebug = shouldDebug - HasIncludeFilter = Option.isSome runRequest.``include`` + HasIncludeFilter = hasIncludeFilter Tests = replaceProjectRootIfPresent tests }) let runHandler @@ -1542,7 +1566,12 @@ module Interactions = logger.Error(message, buildFailures |> List.map ProjectPath.fromProject) else - let librariesCapableOfListOnlyDiscovery = set [ "Expecto"; "xunit.abstractions" ] + let detectablePackageToFramework = + dict + [ "Expecto", TestFrameworkId.Expecto + "xunit.abstractions", TestFrameworkId.XUnit ] + + let librariesCapableOfListOnlyDiscovery = set detectablePackageToFramework.Keys let listDiscoveryProjects, trxDiscoveryProjects = builtTestProjects @@ -1557,6 +1586,17 @@ module Interactions = let! testNames = DotnetCli.listTests project.Project project.Info.TargetFramework false cancellationToken + let detectedTestFramework = + let getPackageName (pr: PackageReference) = pr.Name + + project.PackageReferences + |> Array.tryPick (getPackageName >> Dict.tryGet detectablePackageToFramework) + + let testItemFactory (testItemBuilder: TestItem.TestItemBuilder) = + testItemFactory + { testItemBuilder with + testFramework = detectedTestFramework } + let testHierarchy = testNames |> Array.map (fun n -> {| FullName = n; Data = () |}) @@ -1600,6 +1640,7 @@ module Interactions = let trxDiscoveredTests = TestDiscovery.discoverFromTrx testItemFactory tryGetLocation makeTrxPath trxDiscoveryProjects + let listDiscoveredTests = listDiscoveredPerProject |> Array.map snd let newTests = Array.concat [ listDiscoveredTests; trxDiscoveredTests ] From 1305ea6c47706e3fd9ebc628151c829e1234ec2a Mon Sep 17 00:00:00 2001 From: Spencer Farley <2847259+farlee2121@users.noreply.github.com> Date: Wed, 4 Oct 2023 19:07:13 -0500 Subject: [PATCH 2/2] Support running single cases in MSTest DataTestMethods Doesn't appear to be possible to filter to only one case for parameterized tests in MSTest. Instead, we truncate the filter string to run all the cases for that method. This looks close enough to the real thing unless you debug. --- src/Components/TestExplorer.fs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/Components/TestExplorer.fs b/src/Components/TestExplorer.fs index 40dc8632..0683d50a 100644 --- a/src/Components/TestExplorer.fs +++ b/src/Components/TestExplorer.fs @@ -1202,6 +1202,17 @@ module Interactions = // NOTE: using DisplayName allows single theory cases to be run for xUnit let operator = if test.children.size = 0 then "=" else "~" $"(DisplayName{operator}{escapedTestName})" + else if test.TestFramework = TestFrameworkId.MsTest && String.endWith ")" fullTestName then + // NOTE: MSTest can't filter to parameterized test cases + // Truncating before the case parameters will run all the theory cases + // example parameterized test name -> `MsTestTests.TestClass.theoryTest (2,3,5)` + let truncateOnLast (separator: string) (toSplit: string) = + match toSplit.LastIndexOf(separator) with + | -1 -> toSplit + | index -> toSplit.Substring(0, index) + + let truncatedTestName = truncateOnLast @" \(" escapedTestName + $"(FullyQualifiedName~{truncatedTestName})" else if test.children.size = 0 then $"(FullyQualifiedName={escapedTestName})" else