From 9f5f22c6449c984fd1cbc299b86ba47f2ee63e8b Mon Sep 17 00:00:00 2001 From: Patrick Stevens <3138005+Smaug123@users.noreply.github.com> Date: Sun, 23 Jun 2024 11:44:53 +0100 Subject: [PATCH] Pull report generation into lib (#90) --- .../AssemblyLevelAttributes.fs | 58 ++++ .../CreateTrxReport.fs | 243 ++++++++++++++++ .../SurfaceBaseline.txt | 10 + .../WoofWare.NUnitTestRunner.Lib.fsproj | 4 +- WoofWare.NUnitTestRunner.Lib/version.json | 4 +- WoofWare.NUnitTestRunner/Program.fs | 273 +----------------- 6 files changed, 321 insertions(+), 271 deletions(-) create mode 100644 WoofWare.NUnitTestRunner.Lib/AssemblyLevelAttributes.fs create mode 100644 WoofWare.NUnitTestRunner.Lib/CreateTrxReport.fs diff --git a/WoofWare.NUnitTestRunner.Lib/AssemblyLevelAttributes.fs b/WoofWare.NUnitTestRunner.Lib/AssemblyLevelAttributes.fs new file mode 100644 index 0000000..18c95a6 --- /dev/null +++ b/WoofWare.NUnitTestRunner.Lib/AssemblyLevelAttributes.fs @@ -0,0 +1,58 @@ +namespace WoofWare.NUnitTestRunner + +open System.Reflection + +/// Attributes at the assembly level which control the behaviour of NUnit. +type AssemblyLevelAttributes = + { + /// How many tests can be running at once, if anything's running in parallel. + Parallelism : int option + /// Whether the tests in this assembly can be parallelised at all. + Parallelizable : Parallelizable option + } + +[] +module AssemblyLevelAttributes = + + /// Reflectively obtain the values of any relevant assembly attributes. + let get (assy : Assembly) : AssemblyLevelAttributes = + ((None, None), assy.CustomAttributes) + ||> Seq.fold (fun (levelPar, par) attr -> + match attr.AttributeType.FullName with + | "NUnit.Framework.LevelOfParallelismAttribute" -> + let arg = attr.ConstructorArguments |> Seq.exactlyOne |> _.Value |> unbox + + match levelPar with + | None -> (Some arg, par) + | Some existing -> + failwith $"Assembly %s{assy.Location} declares parallelism %i{arg} and also %i{existing}" + | "NUnit.Framework.NonParallelizableAttribute" -> + match levelPar with + | None -> (Some 1, par) + | Some existing -> + failwith + $"Assembly %s{assy.Location} declares non-parallelizable and also parallelism %i{existing}" + | "NUnit.Framework.ParallelizableAttribute" -> + match par with + | Some _ -> failwith "Got multiple Parallelize attributes in assembly" + | None -> + match attr.ConstructorArguments |> Seq.toList with + | [] -> levelPar, Some (Parallelizable.Yes AssemblyParallelScope.Fixtures) + | [ v ] -> + match v.Value with + | :? int as v -> + match v with + | 512 -> levelPar, Some (Parallelizable.Yes AssemblyParallelScope.Fixtures) + | 256 -> levelPar, Some (Parallelizable.Yes AssemblyParallelScope.Children) + | 257 -> failwith "ParallelScope.All is invalid on assemblies; only Fixtures or Children" + | 1 -> failwith "ParallelScope.Self is invalid on assemblies; only Fixtures or Children" + | v -> failwith $"Could not recognise value %i{v} of parallel scope on assembly" + | v -> failwith $"Unexpectedly non-int value %O{v} of parallel scope on assembly" + | _ -> failwith "unexpectedly got multiple args to Parallelizable on assembly" + | _ -> levelPar, par + ) + |> fun (par, canPar) -> + { + Parallelizable = canPar + Parallelism = par + } diff --git a/WoofWare.NUnitTestRunner.Lib/CreateTrxReport.fs b/WoofWare.NUnitTestRunner.Lib/CreateTrxReport.fs new file mode 100644 index 0000000..2e4113d --- /dev/null +++ b/WoofWare.NUnitTestRunner.Lib/CreateTrxReport.fs @@ -0,0 +1,243 @@ +namespace WoofWare.NUnitTestRunner + +open System +open System.Reflection + +/// Methods for constructing TRX reports. +[] +module BuildTrxReport = + + /// Build a TRX report from the given results. + let build + (assy : Assembly) + (creationTime : DateTimeOffset) + (startTime : DateTimeOffset) + (results : FixtureRunResults list) + : TrxReport + = + let finishTime = DateTimeOffset.Now + let finishTimeHumanReadable = finishTime.ToString @"yyyy-MM-dd HH:mm:ss" + let nowMachine = finishTime.ToString @"yyyy-MM-dd_HH_mm_ss" + + let testListId = Guid.NewGuid () + + let testDefinitions, testEntries = + results + |> List.collect (fun results -> results.IndividualTestRunMetadata) + |> List.map (fun (data, _) -> + let defn = + { + Name = data.TestName + Storage = assy.Location.ToLowerInvariant () + Id = data.TestId + Execution = + { + Id = data.ExecutionId + } + TestMethod = + { + CodeBase = assy.Location + AdapterTypeName = Uri "executor://woofware/" + ClassName = data.ClassName + Name = data.TestName + } + } + + let entry : TrxTestEntry = + { + TestListId = testListId + ExecutionId = data.ExecutionId + TestId = data.TestId + + } + + defn, entry + ) + |> List.unzip + + let hostname = Environment.MachineName + + let settings = + { + Name = "default" + Id = Guid.NewGuid () + Deployment = + { + RunDeploymentRoot = $"_%s{hostname}_%s{nowMachine}" + } + } + + let testList : TrxTestListEntry = + { + Id = testListId + Name = "All" + } + + let counters = + (TrxCounters.Zero, results) + // TODO: this is woefully inefficient + ||> List.fold (fun counters results -> + let counters = + (counters, results.Failed) + ||> List.fold (fun counters (_, _) -> + // TODO: the counters can be more specific about the failure mode + counters.AddFailed () + ) + + let counters = + (counters, results.OtherFailures) + ||> List.fold (fun counters _ -> + // TODO: the counters can be more specific about the failure mode + counters.AddFailed () + ) + + (counters, results.Success) + ||> List.fold (fun counters (_, success, _) -> + match success with + | TestMemberSuccess.Ok -> counters.AddPassed () + | TestMemberSuccess.Ignored _ + | TestMemberSuccess.Explicit _ -> counters.AddNotExecuted () + | TestMemberSuccess.Inconclusive _ -> counters.AddInconclusive () + ) + ) + + // TODO: I'm sure we can do better than this; there's a whole range of possible + // states! + let outcome = + if counters.Failed > 0u then + TrxOutcome.Failed + else + TrxOutcome.Completed + + let resultSummary : TrxResultsSummary = + { + Outcome = outcome + Counters = counters + Output = + { + StdOut = None + StdErr = None + ErrorInfo = None + } + RunInfos = + [ + // TODO: capture stdout + ] + } + + let times : TrxReportTimes = + { + Creation = creationTime + Queuing = startTime + Start = startTime + Finish = finishTime + + } + + let magicGuid = Guid.Parse "13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" + + let results = + results + |> List.collect (fun results -> results.IndividualTestRunMetadata) + |> List.map (fun (i, cause) -> + let exc = + match cause with + | Choice2Of3 _ -> None + | Choice1Of3 (TestMemberFailure.Malformed reasons) -> + { + StackTrace = None + Message = reasons |> String.concat "\n" |> Some + } + |> Some + | Choice1Of3 (TestMemberFailure.Failed fail) + | Choice1Of3 (TestMemberFailure.Failed fail) + | Choice1Of3 (TestMemberFailure.Failed fail) -> + ((None, None), fail) + ||> List.fold (fun (stackTrace, message) tf -> + match tf with + | TestFailure.TestFailed (UserMethodFailure.Threw (_, exc)) + | TestFailure.SetUpFailed (UserMethodFailure.Threw (_, exc)) + | TestFailure.TearDownFailed (UserMethodFailure.Threw (_, exc)) -> + let stackTrace = + match stackTrace with + | None -> (exc : Exception).ToString () + | Some s -> s + + (Some stackTrace, message) + | TestFailure.TestFailed (UserMethodFailure.ReturnedNonUnit (_, ret)) + | TestFailure.SetUpFailed (UserMethodFailure.ReturnedNonUnit (_, ret)) + | TestFailure.TearDownFailed (UserMethodFailure.ReturnedNonUnit (_, ret)) -> + let newMessage = $"returned non-unit value %O{ret}" + + let message = + match message with + | None -> newMessage + | Some message -> $"%s{message}\n%s{newMessage}" + + (stackTrace, Some message) + ) + |> fun (stackTrace, message) -> + { + StackTrace = stackTrace + Message = message + } + |> Some + | Choice3Of3 (UserMethodFailure.Threw (_, exc)) -> + { + StackTrace = (exc : Exception).ToString () |> Some + Message = None + } + |> Some + | Choice3Of3 (UserMethodFailure.ReturnedNonUnit (_, ret)) -> + { + Message = $"returned non-unit value %O{ret}" |> Some + StackTrace = None + } + |> Some + + let outcome = + match cause with + | Choice1Of3 _ -> TrxTestOutcome.Failed + | Choice2Of3 TestMemberSuccess.Ok -> TrxTestOutcome.Passed + | Choice2Of3 (TestMemberSuccess.Inconclusive _) -> TrxTestOutcome.Inconclusive + | Choice2Of3 (TestMemberSuccess.Ignored _) + | Choice2Of3 (TestMemberSuccess.Explicit _) -> TrxTestOutcome.NotExecuted + // TODO: we can totally do better here, more fine-grained classification + | Choice3Of3 _ -> TrxTestOutcome.Failed + + { + ExecutionId = i.ExecutionId + TestId = i.TestId + TestName = i.TestName + ComputerName = i.ComputerName + Duration = i.End - i.Start + StartTime = i.Start + EndTime = i.End + TestType = magicGuid + Outcome = outcome + TestListId = testListId + RelativeResultsDirectory = i.ExecutionId.ToString () // for some reason + Output = + match i.StdOut, i.StdErr, exc with + | None, None, None -> None + | stdout, stderr, exc -> + Some + { + TrxOutput.StdOut = stdout + StdErr = stderr + ErrorInfo = exc + } + } + ) + + { + Id = Guid.NewGuid () + Name = $"@%s{hostname} %s{finishTimeHumanReadable}" + Times = times + Settings = settings + Results = results + TestDefinitions = testDefinitions + TestEntries = testEntries + TestLists = [ testList ] + ResultsSummary = resultSummary + } diff --git a/WoofWare.NUnitTestRunner.Lib/SurfaceBaseline.txt b/WoofWare.NUnitTestRunner.Lib/SurfaceBaseline.txt index a608830..84f5186 100644 --- a/WoofWare.NUnitTestRunner.Lib/SurfaceBaseline.txt +++ b/WoofWare.NUnitTestRunner.Lib/SurfaceBaseline.txt @@ -1,3 +1,11 @@ +WoofWare.NUnitTestRunner.AssemblyLevelAttributes inherit obj, implements WoofWare.NUnitTestRunner.AssemblyLevelAttributes System.IEquatable, System.Collections.IStructuralEquatable, WoofWare.NUnitTestRunner.AssemblyLevelAttributes System.IComparable, System.IComparable, System.Collections.IStructuralComparable +WoofWare.NUnitTestRunner.AssemblyLevelAttributes..ctor [constructor]: (int option, WoofWare.NUnitTestRunner.AssemblyParallelScope WoofWare.NUnitTestRunner.Parallelizable option) +WoofWare.NUnitTestRunner.AssemblyLevelAttributes.get_Parallelism [method]: unit -> int option +WoofWare.NUnitTestRunner.AssemblyLevelAttributes.get_Parallelizable [method]: unit -> WoofWare.NUnitTestRunner.AssemblyParallelScope WoofWare.NUnitTestRunner.Parallelizable option +WoofWare.NUnitTestRunner.AssemblyLevelAttributes.Parallelism [property]: [read-only] int option +WoofWare.NUnitTestRunner.AssemblyLevelAttributes.Parallelizable [property]: [read-only] WoofWare.NUnitTestRunner.AssemblyParallelScope WoofWare.NUnitTestRunner.Parallelizable option +WoofWare.NUnitTestRunner.AssemblyLevelAttributesModule inherit obj +WoofWare.NUnitTestRunner.AssemblyLevelAttributesModule.get [static method]: System.Reflection.Assembly -> WoofWare.NUnitTestRunner.AssemblyLevelAttributes WoofWare.NUnitTestRunner.AssemblyParallelScope inherit obj, implements WoofWare.NUnitTestRunner.AssemblyParallelScope System.IEquatable, System.Collections.IStructuralEquatable, WoofWare.NUnitTestRunner.AssemblyParallelScope System.IComparable, System.IComparable, System.Collections.IStructuralComparable - union type with 2 cases WoofWare.NUnitTestRunner.AssemblyParallelScope+Tags inherit obj WoofWare.NUnitTestRunner.AssemblyParallelScope+Tags.Children [static field]: int = 0 @@ -12,6 +20,8 @@ WoofWare.NUnitTestRunner.AssemblyParallelScope.get_Tag [method]: unit -> int WoofWare.NUnitTestRunner.AssemblyParallelScope.IsChildren [property]: [read-only] bool WoofWare.NUnitTestRunner.AssemblyParallelScope.IsFixtures [property]: [read-only] bool WoofWare.NUnitTestRunner.AssemblyParallelScope.Tag [property]: [read-only] int +WoofWare.NUnitTestRunner.BuildTrxReport inherit obj +WoofWare.NUnitTestRunner.BuildTrxReport.build [static method]: System.Reflection.Assembly -> System.DateTimeOffset -> System.DateTimeOffset -> WoofWare.NUnitTestRunner.FixtureRunResults list -> WoofWare.NUnitTestRunner.TrxReport WoofWare.NUnitTestRunner.ClassParallelScope inherit obj, implements WoofWare.NUnitTestRunner.ClassParallelScope System.IEquatable, System.Collections.IStructuralEquatable, WoofWare.NUnitTestRunner.ClassParallelScope System.IComparable, System.IComparable, System.Collections.IStructuralComparable - union type with 4 cases WoofWare.NUnitTestRunner.ClassParallelScope+Tags inherit obj WoofWare.NUnitTestRunner.ClassParallelScope+Tags.All [static field]: int = 3 diff --git a/WoofWare.NUnitTestRunner.Lib/WoofWare.NUnitTestRunner.Lib.fsproj b/WoofWare.NUnitTestRunner.Lib/WoofWare.NUnitTestRunner.Lib.fsproj index 75eea5a..9e51930 100644 --- a/WoofWare.NUnitTestRunner.Lib/WoofWare.NUnitTestRunner.Lib.fsproj +++ b/WoofWare.NUnitTestRunner.Lib/WoofWare.NUnitTestRunner.Lib.fsproj @@ -36,6 +36,8 @@ + + True \ @@ -51,7 +53,7 @@ - + diff --git a/WoofWare.NUnitTestRunner.Lib/version.json b/WoofWare.NUnitTestRunner.Lib/version.json index 56b562e..b45ff4c 100644 --- a/WoofWare.NUnitTestRunner.Lib/version.json +++ b/WoofWare.NUnitTestRunner.Lib/version.json @@ -1,5 +1,5 @@ { - "version": "0.13", + "version": "0.14", "publicReleaseRefSpec": [ "^refs/heads/main$" ], @@ -8,4 +8,4 @@ ":/Directory.Build.props", ":/README.md" ] -} +} \ No newline at end of file diff --git a/WoofWare.NUnitTestRunner/Program.fs b/WoofWare.NUnitTestRunner/Program.fs index 82163e8..8e7d51a 100644 --- a/WoofWare.NUnitTestRunner/Program.fs +++ b/WoofWare.NUnitTestRunner/Program.fs @@ -132,47 +132,10 @@ module Program = let ctx = LoadContext (args.Dll, runtime, contexts) let assy = ctx.LoadFromAssemblyPath args.Dll.FullName - let levelOfParallelism, par = - ((None, None), assy.CustomAttributes) - ||> Seq.fold (fun (levelPar, par) attr -> - match attr.AttributeType.FullName with - | "NUnit.Framework.LevelOfParallelismAttribute" -> - let arg = attr.ConstructorArguments |> Seq.exactlyOne |> _.Value |> unbox - - match levelPar with - | None -> (Some arg, par) - | Some existing -> - failwith $"Assembly %s{assy.Location} declares parallelism %i{arg} and also %i{existing}" - | "NUnit.Framework.NonParallelizableAttribute" -> - match levelPar with - | None -> (Some 1, par) - | Some existing -> - failwith - $"Assembly %s{assy.Location} declares non-parallelizable and also parallelism %i{existing}" - | "NUnit.Framework.ParallelizableAttribute" -> - match par with - | Some _ -> failwith "Got multiple Parallelize attributes in assembly" - | None -> - match attr.ConstructorArguments |> Seq.toList with - | [] -> levelPar, Some (Parallelizable.Yes AssemblyParallelScope.Fixtures) - | [ v ] -> - match v.Value with - | :? int as v -> - match v with - | 512 -> levelPar, Some (Parallelizable.Yes AssemblyParallelScope.Fixtures) - | 256 -> levelPar, Some (Parallelizable.Yes AssemblyParallelScope.Children) - | 257 -> - failwith "ParallelScope.All is invalid on assemblies; only Fixtures or Children" - | 1 -> - failwith "ParallelScope.Self is invalid on assemblies; only Fixtures or Children" - | v -> failwith $"Could not recognise value %i{v} of parallel scope on assembly" - | v -> failwith $"Unexpectedly non-int value %O{v} of parallel scope on assembly" - | _ -> failwith "unexpectedly got multiple args to Parallelizable on assembly" - | _ -> levelPar, par - ) + let attrs = AssemblyLevelAttributes.get assy let levelOfParallelism = - match args.LevelOfParallelism, levelOfParallelism with + match args.LevelOfParallelism, attrs.Parallelism with | None, None -> None | Some taken, Some ignored -> match args.Logging with @@ -187,7 +150,7 @@ module Program = let testFixtures = assy.ExportedTypes |> Seq.map TestFixture.parse |> Seq.toList - use par = new ParallelQueue (levelOfParallelism, par) + use par = new ParallelQueue (levelOfParallelism, attrs.Parallelizable) let creationTime = DateTimeOffset.Now @@ -206,233 +169,7 @@ module Program = let results = results.Result |> Seq.concat |> List.ofSeq - let finishTime = DateTimeOffset.Now - let finishTimeHumanReadable = finishTime.ToString @"yyyy-MM-dd HH:mm:ss" - let nowMachine = finishTime.ToString @"yyyy-MM-dd_HH_mm_ss" - - let testListId = Guid.NewGuid () - - let testDefinitions, testEntries = - results - |> List.collect (fun results -> results.IndividualTestRunMetadata) - |> List.map (fun (data, _) -> - let defn = - { - Name = data.TestName - Storage = assy.Location.ToLowerInvariant () - Id = data.TestId - Execution = - { - Id = data.ExecutionId - } - TestMethod = - { - CodeBase = assy.Location - AdapterTypeName = Uri "executor://woofware/" - ClassName = data.ClassName - Name = data.TestName - } - } - - let entry : TrxTestEntry = - { - TestListId = testListId - ExecutionId = data.ExecutionId - TestId = data.TestId - - } - - defn, entry - ) - |> List.unzip - - let hostname = Environment.MachineName - - let settings = - { - Name = "default" - Id = Guid.NewGuid () - Deployment = - { - RunDeploymentRoot = $"_%s{hostname}_%s{nowMachine}" - } - } - - let testList : TrxTestListEntry = - { - Id = testListId - Name = "All" - } - - let counters = - (TrxCounters.Zero, results) - // TODO: this is woefully inefficient - ||> List.fold (fun counters results -> - let counters = - (counters, results.Failed) - ||> List.fold (fun counters (_, _) -> - // TODO: the counters can be more specific about the failure mode - counters.AddFailed () - ) - - let counters = - (counters, results.OtherFailures) - ||> List.fold (fun counters _ -> - // TODO: the counters can be more specific about the failure mode - counters.AddFailed () - ) - - (counters, results.Success) - ||> List.fold (fun counters (_, success, _) -> - match success with - | TestMemberSuccess.Ok -> counters.AddPassed () - | TestMemberSuccess.Ignored _ - | TestMemberSuccess.Explicit _ -> counters.AddNotExecuted () - | TestMemberSuccess.Inconclusive _ -> counters.AddInconclusive () - ) - ) - - // TODO: I'm sure we can do better than this; there's a whole range of possible - // states! - let outcome = - if counters.Failed > 0u then - TrxOutcome.Failed - else - TrxOutcome.Completed - - let resultSummary : TrxResultsSummary = - { - Outcome = outcome - Counters = counters - Output = - { - StdOut = None - StdErr = None - ErrorInfo = None - } - RunInfos = - [ - // TODO: capture stdout - ] - } - - let times : TrxReportTimes = - { - Creation = creationTime - Queuing = startTime - Start = startTime - Finish = finishTime - - } - - let magicGuid = Guid.Parse "13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" - - let results = - results - |> List.collect (fun results -> results.IndividualTestRunMetadata) - |> List.map (fun (i, cause) -> - let exc = - match cause with - | Choice2Of3 _ -> None - | Choice1Of3 (TestMemberFailure.Malformed reasons) -> - { - StackTrace = None - Message = reasons |> String.concat "\n" |> Some - } - |> Some - | Choice1Of3 (TestMemberFailure.Failed fail) - | Choice1Of3 (TestMemberFailure.Failed fail) - | Choice1Of3 (TestMemberFailure.Failed fail) -> - ((None, None), fail) - ||> List.fold (fun (stackTrace, message) tf -> - match tf with - | TestFailure.TestFailed (UserMethodFailure.Threw (_, exc)) - | TestFailure.SetUpFailed (UserMethodFailure.Threw (_, exc)) - | TestFailure.TearDownFailed (UserMethodFailure.Threw (_, exc)) -> - let stackTrace = - match stackTrace with - | None -> (exc : Exception).ToString () - | Some s -> s - - (Some stackTrace, message) - | TestFailure.TestFailed (UserMethodFailure.ReturnedNonUnit (_, ret)) - | TestFailure.SetUpFailed (UserMethodFailure.ReturnedNonUnit (_, ret)) - | TestFailure.TearDownFailed (UserMethodFailure.ReturnedNonUnit (_, ret)) -> - let newMessage = $"returned non-unit value %O{ret}" - - let message = - match message with - | None -> newMessage - | Some message -> $"%s{message}\n%s{newMessage}" - - (stackTrace, Some message) - ) - |> fun (stackTrace, message) -> - { - StackTrace = stackTrace - Message = message - } - |> Some - | Choice3Of3 (UserMethodFailure.Threw (_, exc)) -> - { - StackTrace = (exc : Exception).ToString () |> Some - Message = None - } - |> Some - | Choice3Of3 (UserMethodFailure.ReturnedNonUnit (_, ret)) -> - { - Message = $"returned non-unit value %O{ret}" |> Some - StackTrace = None - } - |> Some - - let outcome = - match cause with - | Choice1Of3 _ -> TrxTestOutcome.Failed - | Choice2Of3 TestMemberSuccess.Ok -> TrxTestOutcome.Passed - | Choice2Of3 (TestMemberSuccess.Inconclusive _) -> TrxTestOutcome.Inconclusive - | Choice2Of3 (TestMemberSuccess.Ignored _) - | Choice2Of3 (TestMemberSuccess.Explicit _) -> TrxTestOutcome.NotExecuted - // TODO: we can totally do better here, more fine-grained classification - | Choice3Of3 _ -> TrxTestOutcome.Failed - - { - ExecutionId = i.ExecutionId - TestId = i.TestId - TestName = i.TestName - ComputerName = i.ComputerName - Duration = i.End - i.Start - StartTime = i.Start - EndTime = i.End - TestType = magicGuid - Outcome = outcome - TestListId = testListId - RelativeResultsDirectory = i.ExecutionId.ToString () // for some reason - Output = - match i.StdOut, i.StdErr, exc with - | None, None, None -> None - | stdout, stderr, exc -> - Some - { - TrxOutput.StdOut = stdout - StdErr = stderr - ErrorInfo = exc - } - } - ) - - let report : TrxReport = - { - Id = Guid.NewGuid () - Name = $"@%s{hostname} %s{finishTimeHumanReadable}" - Times = times - Settings = settings - Results = results - TestDefinitions = testDefinitions - TestEntries = testEntries - TestLists = [ testList ] - ResultsSummary = resultSummary - } + let report = BuildTrxReport.build assy creationTime startTime results match args.Trx with | Some trxPath -> @@ -442,7 +179,7 @@ module Program = Console.Error.WriteLine $"Written TRX file: %s{trxPath.FullName}" | None -> () - match outcome with + match report.ResultsSummary.Outcome with | TrxOutcome.Completed -> 0 | _ -> 1