Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions release/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,11 @@
"description": "EXPERIMENTAL. Enables F# analyzers for custom code diagnostics. Requires restart.",
"type": "boolean"
},
"FSharp.enableTestingPlatform": {
"default": false,
"description": "EXPERIMENTAL. Runs tests through the Microsoft.Testing.Platform server protocol for projects that set IsTestingPlatformApplication. Requires restart.",
"type": "boolean"
},
"FSharp.enableMSBuildProjectGraph": {
"default": false,
"description": "EXPERIMENTAL. Enables support for loading workspaces with MsBuild\u0027s ProjectGraph. This can improve load times. Requires restart.",
Expand Down
114 changes: 68 additions & 46 deletions src/Components/TestExplorer.fs
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,10 @@ type TestItem with

member this.TestFramework: string = this?testFramework

/// Addresses this test to Microsoft.Testing.Platform. A grouping node and a test run under
/// VSTest carry `None`.
member this.PlatformUid: string option = this?platformUid |> Option.ofObj

[<RequireQualifiedAccess; StringEnum(CaseRules.None)>]
type TestResultOutcome =
| NotExecuted
Expand Down Expand Up @@ -269,26 +273,6 @@ module TestFrameworkId =
else
None

module TestItemDTO =
let getFullname_withNestedParamTests (dto: TestItemDTO) =
match dto.ExecutorUri |> TestFrameworkId.tryFromExecutorUri with
// NOTE: XUnit and MSTest don't include the theory case parameters in the FullyQualifiedName, but do include them in the DisplayName.
// Thus we need to append the DisplayName to differentiate the test cases
| Some TestFrameworkId.MsTest ->
if dto.FullName.EndsWith(dto.DisplayName) then
dto.FullName
else
dto.FullName + "." + dto.DisplayName
| Some TestFrameworkId.XUnit ->
// NOTE: XUnit includes the FullyQualifiedName in the DisplayName.
// But it doesn't nest theory cases, just appends the case parameters
if dto.DisplayName <> dto.FullName then
let theoryCaseFragment = dto.DisplayName.Split('.') |> Array.last
dto.FullName + "." + theoryCaseFragment
else
dto.FullName
| _ -> dto.FullName

type TestResult =
{ FullTestName: string
Outcome: TestResultOutcome
Expand Down Expand Up @@ -323,7 +307,7 @@ module TestResult =
let ofTestResultDTO (testResultDto: TestResultDTO) : TestResult =
let expected, actual = tryExtractExpectedAndActual testResultDto.ErrorMessage

{ FullTestName = testResultDto.TestItem |> TestItemDTO.getFullname_withNestedParamTests
{ FullTestName = testResultDto.TestItem.FullName
Outcome = testResultDto.Outcome |> TestResultOutcome.ofOutcomeDto
Output = testResultDto.AdditionalOutput
ErrorMessage = testResultDto.ErrorMessage
Expand Down Expand Up @@ -820,13 +804,18 @@ module TestItem =
recurse root

type TestItemBuilder =
{ id: TestId
label: string
uri: Uri option
range: Vscode.Range option
children: TestItem array
// i.e. NUnit. Used for an Nunit-specific workaround
testFramework: TestFrameworkId option }
{
id: TestId
label: string
uri: Uri option
range: Vscode.Range option
children: TestItem array
// i.e. NUnit. Used for an Nunit-specific workaround
testFramework: TestFrameworkId option
/// Addresses the test to Microsoft.Testing.Platform, which names a test to run by uid
/// rather than by a filter expression.
platformUid: string option
}

type TestItemFactory = TestItemBuilder -> TestItem

Expand All @@ -844,6 +833,10 @@ module TestItem =
| Some frameworkId -> testItem?testFramework <- frameworkId
| None -> ()

match builder.platformUid with
| Some uid -> testItem?platformUid <- uid
| None -> ()

testItem

factory
Expand All @@ -865,7 +858,8 @@ module TestItem =
uri = location |> LocationRecord.tryGetUri
range = location |> LocationRecord.tryGetRange
children = namedNode.Children |> Array.map recurse
testFramework = None }
testFramework = None
platformUid = None }

recurse hierarchy

Expand Down Expand Up @@ -901,7 +895,8 @@ module TestItem =
uri = Some uri
range = range
children = t.childs |> Array.map (fun n -> recurse fullName (Some t.moduleType) n)
testFramework = t?``type`` }
testFramework = t?``type``
platformUid = None }

ti

Expand All @@ -919,7 +914,8 @@ module TestItem =
uri = None
range = None
children = children
testFramework = None }
testFramework = None
platformUid = None }


let ofTestDTOs testItemFactory tryGetLocation (flatTests: TestItemDTO array) =
Expand Down Expand Up @@ -970,17 +966,28 @@ module TestItem =
children = namedNode.Children |> Array.map recurse
testFramework =
namedNode.Data
|> Option.bind (fun t -> t.ExecutorUri |> TestFrameworkId.tryFromExecutorUri) }
|> Option.bind (fun t -> t.ExecutorUri |> TestFrameworkId.tryFromExecutorUri)
platformUid = namedNode.Data |> Option.bind (fun dto -> dto.PlatformUid) }

recurse hierarchy

let mapDtosForProject ((projectPath, targetFramework), flatTests) =
let testDtoToNamedItem (dto: TestItemDTO) =
{| Data = dto
FullName = dto |> TestItemDTO.getFullname_withNestedParamTests |}
/// The tree the server reported, linked through `ParentId`.
let hierarchyOfDtos (flatTests: TestItemDTO array) : TestName.NameHierarchy<TestItemDTO> array =
let byParent = flatTests |> Array.groupBy (fun dto -> dto.ParentId) |> Map.ofArray

let childrenOf parentId =
byParent |> Map.tryFind parentId |> Option.defaultValue [||]

let namedHierarchies =
flatTests |> Array.map testDtoToNamedItem |> TestName.inferHierarchy
let rec build (dto: TestItemDTO) : TestName.NameHierarchy<TestItemDTO> =
{ TestName.NameHierarchy.Data = (if dto.IsLeaf then Some dto else None)
TestName.NameHierarchy.FullName = dto.FullName
TestName.NameHierarchy.Name = (TestName.splitSegments dto.FullName |> List.last).Text
TestName.NameHierarchy.Children = childrenOf (Some dto.Id) |> Array.map build }

childrenOf None |> Array.map build

let mapDtosForProject ((projectPath, targetFramework), flatTests) =
let namedHierarchies = hierarchyOfDtos flatTests

let projectChildTestItems =
namedHierarchies
Expand Down Expand Up @@ -1081,7 +1088,8 @@ module TestItem =
uri = maybeLocation |> LocationRecord.tryGetUri
range = maybeLocation |> LocationRecord.tryGetRange
children = [||]
testFramework = None }
testFramework = None
platformUid = None }

collection.add (testItem)

Expand Down Expand Up @@ -1156,7 +1164,8 @@ module TestDiscovery =
uri = withUri.uri
range = withUri.range
children = target.children.TestItems()
testFramework = withUri?testFramework }
testFramework = withUri?testFramework
platformUid = target.PlatformUid }

(replacementItem, withUri)

Expand Down Expand Up @@ -1308,7 +1317,8 @@ module TestDiscovery =
let testItemFactory (testItemBuilder: TestItem.TestItemBuilder) =
testItemFactory
{ testItemBuilder with
testFramework = detectedTestFramework }
testFramework = detectedTestFramework
platformUid = None }

let testHierarchy =
testNames
Expand Down Expand Up @@ -1542,7 +1552,8 @@ module Interactions =
let testItemFactory (ti: TestItem.TestItemBuilder) =
testItemFactory
{ ti with
testFramework = testResult.TestFramework }
testFramework = testResult.TestFramework
platformUid = None }

TestItem.getOrMakeHierarchyPath
rootTestCollection
Expand Down Expand Up @@ -1883,10 +1894,20 @@ module Interactions =
let onAttachDebugger (processId: int) =
VSCodeActions.launchDebugger (string processId)

let filterExpression, projectSubset =
/// Names the selected tests to Microsoft.Testing.Platform, which runs a test by
/// uid rather than by a filter expression. A grouping node holds no uid of its
/// own, so a selection is read as the runnable tests under it.
let testUids (selectedCases: TestItem seq) =
selectedCases
|> Array.ofSeq
|> TestItem.runnableFromArray
|> Array.choose (fun test -> test.PlatformUid)
|> Array.distinct

let filterExpression, projectSubset, uids =
match req.``include`` with
| None -> None, None
| Some selectedCases when Seq.isEmpty selectedCases -> None, None
| None -> None, None, None
| Some selectedCases when Seq.isEmpty selectedCases -> None, None, None
| Some selectedCases ->
let filter =
selectedCases
Expand All @@ -1902,7 +1923,7 @@ module Interactions =
|> Array.ofSeq
|> Some

filter, projectSubset
filter, projectSubset, Some(testUids selectedCases)

logger.Debug($"Test Filter Expression: {filterExpression}")

Expand All @@ -1914,6 +1935,7 @@ module Interactions =
onAttachDebugger
projectSubset
filterExpression
uids
shouldDebug

mergeResults TrimMissing.Trim runResult.Data
Expand Down
9 changes: 9 additions & 0 deletions src/Core/DTO.fs
Original file line number Diff line number Diff line change
Expand Up @@ -372,12 +372,21 @@ module DTO =

type TestItemDTO =
{
/// Distinguishes this node from every other node reported by the server.
Id: string
/// The `Id` of the node one level up, or `None` at the root of a project.
ParentId: string option
/// A runnable test. `false` marks a grouping node.
IsLeaf: bool
FullName: string
DisplayName: string
/// Identifies the test adapter that ran the tests
/// Example: executor://xunit/VsTestRunner2/netcoreapp
/// Used for determining the test library, which effects how tests names are broken down
ExecutorUri: string
/// Addresses a runnable test to Microsoft.Testing.Platform, which identifies tests by
/// opaque uid. A grouping node and a test run under VSTest carry `None`.
PlatformUid: string option
ProjectFilePath: string
TargetFramework: string
CodeFilePath: string option
Expand Down
13 changes: 10 additions & 3 deletions src/Core/LanguageService.fs
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,14 @@ module LanguageService =
``end``: Fable.Import.VSCode.Vscode.Position }

type TestRunRequest =
{ LimitToProjects: string array option
TestCaseFilter: string option
AttachDebugger: bool }
{
LimitToProjects: string array option
TestCaseFilter: string option
/// Names the tests to run on Microsoft.Testing.Platform, by the uid each was
/// discovered under. `None` runs every test of the projects being run.
TestUids: string array option
AttachDebugger: bool
}

type Uri with

Expand Down Expand Up @@ -622,6 +627,7 @@ Consider:
(onAttachDebugger: ProcessId -> JS.Promise<bool>)
(projectSubset: string array option)
(testCaseFilter: string option)
(testUids: string array option)
(attachDebugger: bool)
=
match client with
Expand All @@ -646,6 +652,7 @@ Consider:
let request: Types.TestRunRequest =
{ LimitToProjects = projectSubset
TestCaseFilter = testCaseFilter
TestUids = testUids
AttachDebugger = attachDebugger }

cl.sendRequest ("test/runTests", request)
Expand Down
Loading