Skip to content

Commit

Permalink
Xunit theory filter (#1943)
Browse files Browse the repository at this point in the history
* Support running single theory cases in xunit in test explorer

* 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.
  • Loading branch information
farlee2121 authored Oct 6, 2023
1 parent 94dc2c0 commit 81cb6b5
Showing 1 changed file with 63 additions and 11 deletions.
74 changes: 63 additions & 11 deletions src/Components/TestExplorer.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -227,6 +231,12 @@ module TestFrameworkId =
[<Literal>]
let MsTest = "MSTest"

[<Literal>]
let XUnit = "XUnit"

[<Literal>]
let Expecto = "Expecto"

type TestResult =
{ FullTestName: string
Outcome: TestResultOutcome
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -1067,7 +1081,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<TrxParser.UnitTest>) =
// NOTE: A project could have multiple test frameworks, but we only track NUnit for now to work around a defect
Expand All @@ -1088,11 +1102,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 =
Expand Down Expand Up @@ -1180,17 +1193,32 @@ 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.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={fullName})"
$"(FullyQualifiedName={escapedTestName})"
else
$"(FullyQualifiedName~{fullName})"
$"(FullyQualifiedName~{escapedTestName})"

let filterExpression =
tests |> Array.map testToFilterExpression |> String.concat "|"
Expand Down Expand Up @@ -1282,7 +1310,6 @@ module Interactions =

tryFind "Expected:", tryFind "But was:"


{ FullTestName = trxResult.UnitTest.FullName
Outcome = !!trxResult.UnitTestResult.Outcome
ErrorMessage = trxResult.UnitTestResult.Output.ErrorInfo.Message
Expand Down Expand Up @@ -1412,10 +1439,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
Expand Down Expand Up @@ -1544,7 +1579,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
Expand All @@ -1559,6 +1599,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 = () |})
Expand Down Expand Up @@ -1602,6 +1653,7 @@ module Interactions =
let trxDiscoveredTests =
TestDiscovery.discoverFromTrx testItemFactory tryGetLocation makeTrxPath trxDiscoveryProjects


let listDiscoveredTests = listDiscoveredPerProject |> Array.map snd
let newTests = Array.concat [ listDiscoveredTests; trxDiscoveredTests ]

Expand Down

0 comments on commit 81cb6b5

Please sign in to comment.