diff --git a/Expecto.Tests/Tests.fs b/Expecto.Tests/Tests.fs index fcee2e44..28e512dc 100644 --- a/Expecto.Tests/Tests.fs +++ b/Expecto.Tests/Tests.fs @@ -1300,7 +1300,7 @@ let cancel = use ct = new CancellationTokenSource() let! _ = Async.StartChild(async { do! Async.Sleep 50 ct.Cancel() }) - let! results = evalTestsWithCancel ct.Token config t + let! results = evalTestsWithCancel ct.Token config t false results |> List.iter (fun (_,r) -> let d = int r.duration.TotalMilliseconds Expect.isLessThan d 1000 "cancel length" diff --git a/Expecto/Expect.fs b/Expecto/Expect.fs index 9ab8b998..90d3488f 100644 --- a/Expecto/Expect.fs +++ b/Expecto/Expect.fs @@ -587,7 +587,7 @@ let streamsEqual (s1 : IO.Stream) (s2 : IO.Stream) message = /// subset of the functions. Statistical test to 99.99% confidence level. let isFasterThanSub (f1:Performance.Measurer<_,_>->'a) (f2:Performance.Measurer<_,_>->'a) format = let toString (s:SampleStatistics) = - sprintf "%.4f \u00B1 %.4f ms" s.mean s.meanStandardError + sprintf "%.4f ± %.4f ms" s.mean s.meanStandardError match Performance.timeCompare f1 f2 with | Performance.ResultNotTheSame (r1, r2)-> diff --git a/Expecto/Expecto.fs b/Expecto/Expecto.fs index 17eb6716..57cf0ede 100644 --- a/Expecto/Expecto.fs +++ b/Expecto/Expecto.fs @@ -619,33 +619,29 @@ module Impl = >> setField "reason" m) failed = fun n m d -> - logger.logWithAck LogLevel.Error ( - eventX "{testName} failed in {duration}. {message}" - >> setField "testName" n - >> setField "duration" d - >> setField "message" m) + async { + do! logger.logWithAck LogLevel.Error ( + eventX "{testName} failed in {duration}. {message}" + >> setField "testName" n + >> setField "duration" d + >> setField "message" m) + ANSIOutputWriter.Flush() + } exn = fun n e d -> - logger.logWithAck LogLevel.Error ( - eventX "{testName} errored in {duration}" - >> setField "testName" n - >> setField "duration" d - >> addExn e) + async { + do! logger.logWithAck LogLevel.Error ( + eventX "{testName} errored in {duration}" + >> setField "testName" n + >> setField "duration" d + >> addExn e) + ANSIOutputWriter.Flush() + } + - summary = fun config summary -> + summary = fun _config summary -> let spirit = - if summary.successful then - if Console.OutputEncoding.WebName = "utf-8" - && not config.mySpiritIsWeak then - "ᕙ໒( ˵ ಠ ╭͜ʖ╮ ಠೃ ˵ )७ᕗ" - else - "Success!" - else - if Console.OutputEncoding.WebName = "utf-8" - && not config.mySpiritIsWeak then - "( ರ Ĺ̯ ರೃ )" - else - "" + if summary.successful then "Success!" else String.Empty let commonAncestor = let rec loop ancestor (descendants : string list) = match descendants with @@ -852,7 +848,7 @@ module Impl = /// FsCheck end size (default: 100 for testing and 10,000 for /// stress testing). fsCheckEndSize: int option - /// Turn off spirits. + /// Depricated. Will be removed on next major release. mySpiritIsWeak: bool /// Allows duplicate test names. allowDuplicateNames: bool @@ -866,10 +862,11 @@ module Impl = filter = id failOnFocusedTests = false printer = - if Environment.GetEnvironmentVariable "TEAMCITY_PROJECT_NAME" <> null then - TestPrinters.teamCityPrinter TestPrinters.defaultPrinter - else + let tc = Environment.GetEnvironmentVariable "TEAMCITY_PROJECT_NAME" + if isNull tc then TestPrinters.defaultPrinter + else + TestPrinters.teamCityPrinter TestPrinters.defaultPrinter verbosity = Info logName = None locate = fun _ -> SourceLocation.empty @@ -953,9 +950,14 @@ module Impl = config.parallelWorkers /// Evaluates tests. - let evalTestsWithCancel (ct:CancellationToken) config test = + let evalTestsWithCancel (ct:CancellationToken) config test progressStarted = async { + let tests = Test.toTestCodeList test + let testLength = List.length tests + + let testsCompleted = ref 0 + let evalTestAsync (test:FlatTest) = let beforeEach (test:FlatTest) = @@ -966,10 +968,14 @@ module Impl = let! result = execTestAsync ct config test do! beforeAsync do! TestPrinters.printResult config test result + + if progressStarted then + Fraction (Interlocked.Increment testsCompleted, testLength) + |> ProgressIndicator.update + return test,result } - let tests = Test.toTestCodeList test let inline cons xs x = x::xs if not config.``parallel`` || @@ -1015,7 +1021,7 @@ module Impl = /// Evaluates tests. let evalTests config test = - evalTestsWithCancel CancellationToken.None config test + evalTestsWithCancel CancellationToken.None config test false let evalTestsSilent test = let config = @@ -1031,16 +1037,26 @@ module Impl = async { do! config.printer.beforeRun test + ProgressIndicator.text "Expecto Running... " + let progressStarted = ProgressIndicator.start() + + let w = Stopwatch.StartNew() - let! results = evalTests config test + let! results = evalTestsWithCancel ct config test progressStarted w.Stop() - let testSummary = { results = results - duration = w.Elapsed - maxMemory = 0L - memoryLimit = 0L - timedOut = [] } + let testSummary = { + results = results + duration = w.Elapsed + maxMemory = 0L + memoryLimit = 0L + timedOut = [] + } do! config.printer.summary config testSummary + if progressStarted then + ProgressIndicator.stop() + ANSIOutputWriter.Close() + return testSummary.errorCode } @@ -1052,6 +1068,9 @@ module Impl = async { do! config.printer.beforeRun test + ProgressIndicator.text "Expecto Running... " + let progressStarted = ProgressIndicator.start() + let tests = Test.toTestCodeList test |> List.filter (fun t -> Option.isNone t.shouldSkipEvaluation) @@ -1068,11 +1087,13 @@ module Impl = let next = List.length tests |> rand.Next List.item next tests - let finishTimestamp = - lazy + let totalTicks = config.stress.Value.TotalSeconds * float Stopwatch.Frequency |> int64 - |> (+) (Stopwatch.GetTimestamp()) + + let finishTime = + lazy + totalTicks |> (+) (Stopwatch.GetTimestamp()) let asyncRun foldRunner (runningTests:ResizeArray<_>, results, @@ -1101,7 +1122,7 @@ module Impl = Async.Start(async { let finishMilliseconds = - max (finishTimestamp.Value - Stopwatch.GetTimestamp()) 0L + max (finishTime.Value - Stopwatch.GetTimestamp()) 0L * 1000L / Stopwatch.Frequency let timeout = int finishMilliseconds + int config.stressTimeout.TotalMilliseconds @@ -1110,7 +1131,14 @@ module Impl = }, cancel.Token) Seq.takeWhile (fun test -> - if Stopwatch.GetTimestamp() < finishTimestamp.Value + let now = Stopwatch.GetTimestamp() + + if progressStarted then + 100 - int((finishTime.Value - now + totalTicks / 200L) / totalTicks) + |> Percent + |> ProgressIndicator.update + + if now < finishTime.Value && not ct.IsCancellationRequested then runningTests.Add test true @@ -1136,7 +1164,7 @@ module Impl = |> asyncRun Async.foldSequentiallyWithCancel initial |> Async.bind (fun (runningTests,results,maxMemory) -> if maxMemory > memoryLimit || - Stopwatch.GetTimestamp() > finishTimestamp.Value then + Stopwatch.GetTimestamp() > finishTime.Value then async.Return (runningTests,results,maxMemory) else let parallel = @@ -1165,6 +1193,10 @@ module Impl = do! config.printer.summary config testSummary + if progressStarted then + ProgressIndicator.stop() + ANSIOutputWriter.Close() + return testSummary.errorCode } @@ -1325,6 +1357,7 @@ module Impl = eventX "It was requested that no focused tests exist, but yet there are {count} focused tests found." >> setField "count" focused.Length) |> Async.StartImmediate + ANSIOutputWriter.Flush() false [] @@ -1580,7 +1613,7 @@ module Tests = | FsCheck_Max_Tests n -> fun o -> {o with fsCheckMaxTests = n } | FsCheck_Start_Size n -> fun o -> {o with fsCheckStartSize = n } | FsCheck_End_Size n -> fun o -> {o with fsCheckEndSize = Some n } - | My_Spirit_Is_Weak -> fun o -> { o with mySpiritIsWeak = true } + | My_Spirit_Is_Weak -> id | Allow_Duplicate_Names -> fun o -> { o with allowDuplicateNames = true } let parsed = @@ -1634,13 +1667,15 @@ module Tests = | _ -> None ) - /// Runs tests with the supplied config. /// Returns 0 if all tests passed, otherwise 1 let runTestsWithCancel (ct:CancellationToken) config (tests:Test) = Global.initialiseIfDefault { Global.defaultConfig with - getLogger = fun name -> LiterateConsoleTarget(name, config.verbosity, consoleSemaphore = Global.semaphore()) :> Logger } + getLogger = fun name -> + LiterateConsoleTarget(name, config.verbosity, + outputWriter = ANSIOutputWriter.TextToOutput, + consoleSemaphore = Global.semaphore()) :> Logger } config.logName |> Option.iter setLogName if config.failOnFocusedTests && passesFocusTestCheck config tests |> not then 1 @@ -1656,6 +1691,7 @@ module Tests = eventX "Found duplicated test names, these names are: {duplicates}" >> setField "duplicates" duplicates.Value ) |> Async.RunSynchronously + ANSIOutputWriter.Flush() 1 /// Runs tests with the supplied config. /// Returns 0 if all tests passed, otherwise 1 diff --git a/Expecto/Expecto.fsproj b/Expecto/Expecto.fsproj index fa7bf7cf..4da25247 100644 --- a/Expecto/Expecto.fsproj +++ b/Expecto/Expecto.fsproj @@ -15,6 +15,7 @@ + diff --git a/Expecto/Logging.fs b/Expecto/Logging.fs index 0d896e1f..85e3bde7 100644 --- a/Expecto/Logging.fs +++ b/Expecto/Logging.fs @@ -10,12 +10,10 @@ /// Original Source: /// https://github.com/logary/logary/blob/996bdf92713f406b17c6cd7284e4d674f49e3ff6/src/Logary.Facade/Facade.fs /// -/// Changes: -/// Changed namespace to Expecto.Logging and file name to Logging.fs - Anthony Lloyd - 11 Jun 2018 -/// namespace Expecto.Logging open System +open System.Text /// The log level denotes how 'important' the gauge or event message is. [] diff --git a/Expecto/Progress.fs b/Expecto/Progress.fs new file mode 100644 index 00000000..02e60bfa --- /dev/null +++ b/Expecto/Progress.fs @@ -0,0 +1,189 @@ +namespace Expecto + +open System +open System.Threading +open System.IO +open System.Text +open System.Runtime.InteropServices + +#nowarn "9" + +type private FuncTextWriter(encoding:Encoding, write:string->unit) = + inherit TextWriter() + override __.Encoding = encoding + override __.Write (s:string) = write s + override __.WriteLine (s:string) = s + "\n" |> write + override __.WriteLine() = write "\n" + +type Progress = + | Percent of int + | Fraction of int * int + +module internal ProgressIndicator = + let originalStdout = stdout + let originalStderr = stderr + let private hideCursor = "\x1B[?25l" + let private showCursor = "\x1B[?25h" + let private animation = @"|/-\" + + let private color = "\x1b[30;1m" + let private colorReset = "\x1B[0m" + + let mutable private textValue = String.Empty + let private progressValue = Percent 0 |> ref + let private isRunning = ref false + let private isEnabled = not Console.IsOutputRedirected + + let text s = + textValue <- s + + let mutable private currentLength = 0 + + let update progress = + progressValue := + match progress with + | Percent p -> max p 0 |> min 100 |> Percent + | f -> f + + let start() = + lock isRunning (fun () -> + if !isRunning then false + else + isRunning := true + if isEnabled then + hideCursor |> stdout.Write + Thread(fun () -> + let start = DateTime.UtcNow + while !isRunning do + lock isRunning (fun () -> + if !isRunning then + let t = (DateTime.UtcNow - start).TotalSeconds + let a = animation.[int t % animation.Length] + let progress = + match !progressValue with + | Percent p -> + if p=100 then [|'1';'0';'0';'%';' ';a|] + elif p<10 then [|' ';' ';char(48+p);'%';' ';a|] + else [|' ';char(48+p/10);char(48+p%10);'%';' ';a|] + |> String + | Fraction (n,d) -> + let ns, ds = string n, string d + String(' ',ds.Length-ns.Length) + ns + "/" + ds + + " " + string a + currentLength <- textValue.Length + progress.Length + color + textValue + progress + + String('\b', currentLength) + colorReset + |> originalStdout.Write + originalStdout.Flush() + ) + Thread.Sleep 1000 + ).Start() + true + ) + + let stop() = + lock isRunning (fun() -> + if !isRunning then + isRunning := false + if isEnabled then + String(' ', currentLength) + String('\b', currentLength) + showCursor + |> originalStdout.Write + originalStdout.Flush() + ) + + let pause f = + lock isRunning (fun () -> + if !isRunning then + stop(); f(); start() |> ignore + else f() + ) + +module internal ANSIOutputWriter = + let private colorForWhite = + if Console.BackgroundColor = ConsoleColor.White then "\x1B[30m" + else "\x1B[37;1m" + let private colorReset = "\x1B[0m" + let private colorANSI = function + | ConsoleColor.Black -> "\x1b[30m" + | ConsoleColor.DarkBlue -> "\x1b[34m" + | ConsoleColor.DarkGreen -> "\x1b[32m" + | ConsoleColor.DarkCyan -> "\x1b[36m" + | ConsoleColor.DarkRed -> "\x1b[31m" + | ConsoleColor.DarkMagenta -> "\x1b[35m" + | ConsoleColor.DarkYellow -> "\x1b[33m" + | ConsoleColor.Gray -> "\x1b[37m" + | ConsoleColor.DarkGray -> "\x1b[30;1m" + | ConsoleColor.Blue -> "\x1b[34;1m" + | ConsoleColor.Green -> "\x1b[32;1m" + | ConsoleColor.Cyan -> "\x1b[36;1m" + | ConsoleColor.Red -> "\x1b[31;1m" + | ConsoleColor.Magenta -> "\x1b[35;1m" + | ConsoleColor.Yellow -> "\x1b[33;1m" + | ConsoleColor.White -> colorForWhite + | _ -> "" + + let private foregroundColor = Console.ForegroundColor + + let private buffer = StringBuilder() + let private textToOutput (parts: (string * ConsoleColor) list) = + lock buffer <| fun _ -> + let mutable currentColour = foregroundColor + parts |> List.iter (fun (text, colour) -> + if currentColour <> colour then + colorANSI colour |> buffer.Append |> ignore + currentColour <- colour + buffer.Append text |> ignore + ) + buffer.Append colorReset |> ignore + + let Flush() = + lock buffer <| fun _ -> + ProgressIndicator.pause <| fun _ -> + buffer.ToString() |> ProgressIndicator.originalStdout.Write + buffer.Clear() |> ignore + let Close() = + Flush() + Console.SetOut ProgressIndicator.originalStdout + Console.SetError ProgressIndicator.originalStderr + + module WindowsConsole = + open Microsoft.FSharp.NativeInterop + [] + extern void* private GetStdHandle(int _nStdHandle) + [] + extern bool private GetConsoleMode(void* _hConsoleHandle, int* _lpMode) + [] + extern bool private SetConsoleMode(void* _hConsoleHandle, int _lpMode) + let enableVTMode() = + let INVALID_HANDLE_VALUE = nativeint -1 + let STD_OUTPUT_HANDLE = -11 + let ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004 + let handle = GetStdHandle(STD_OUTPUT_HANDLE) + if handle <> INVALID_HANDLE_VALUE then + let mode = NativePtr.stackalloc 1 + if GetConsoleMode(handle, mode) then + let value = NativePtr.read mode + let value = value ||| ENABLE_VIRTUAL_TERMINAL_PROCESSING + SetConsoleMode(handle, value) |> ignore + do +#if NETSTANDARD2_0 + if RuntimeInformation.IsOSPlatform OSPlatform.Windows then + WindowsConsole.enableVTMode() +#else + if Environment.OSVersion.Platform = PlatformID.Win32NT then + WindowsConsole.enableVTMode() +#endif + ProgressIndicator.originalStdout.Flush() + let encoding = ProgressIndicator.originalStdout.Encoding + let std s = textToOutput [s, foregroundColor] + new FuncTextWriter(encoding, std) + |> Console.SetOut + let errorEncoding = ProgressIndicator.originalStderr.Encoding + let errorToOutput s = + textToOutput [s, ConsoleColor.Red] + Flush() + new FuncTextWriter(errorEncoding, errorToOutput) + |> Console.SetError + let TextToOutput (sem:obj) (parts: (string * ConsoleColor) list) = + lock sem <| fun _ -> + textToOutput parts \ No newline at end of file diff --git a/README.md b/README.md index 979cb5d3..10444287 100644 --- a/README.md +++ b/README.md @@ -1085,8 +1085,6 @@ ExpectoConfig record, that looks like: /// FsCheck end size (default: 100 for testing and 10,000 for /// stress testing). fsCheckEndSize: int option - /// Turn off spirits. - mySpiritIsWeak: bool /// Allows duplicate test names. allowDuplicateNames: bool } @@ -1094,12 +1092,6 @@ ExpectoConfig record, that looks like: By doing a `let config = { defaultConfig with parallel = true }`, for example. -If you [don't](https://github.com/haf/expecto/pull/43) -[like](https://github.com/haf/expecto/issues/145#issuecomment-297032723) the -spirits appearing in the output, you can turn them off by setting -`mySpiritIsWeak = true` when you run Expecto, or by running with -`--my-spirit-is-weak` from the command line ( ರ Ĺ̯ ರೃ ). - ## Contributing Please review the [guidelines for contributing](./CONTRIBUTING.md) to Expecto. diff --git a/build.fsx b/build.fsx index 17ef9e18..713cc3f7 100644 --- a/build.fsx +++ b/build.fsx @@ -63,8 +63,15 @@ let build project = ToolPath = dotnetExePath Configuration = configuration Project = project + AdditionalArgs = ["--no-dependencies"] }) +Target "BuildExpecto" (fun _ -> + build "Expecto/Expecto.fsproj" + build "Expecto.Hopac/Expecto.Hopac.fsproj" + build "Expecto.FsCheck/Expecto.FsCheck.fsproj" +) + Target "BuildBenchmarkDotNet" (fun _ -> build "Expecto.BenchmarkDotNet/Expecto.BenchmarkDotNet.fsproj" ) @@ -150,6 +157,7 @@ Target "All" ignore ==> "InstallDotNetCore" ==> "AssemblyInfo" ==> "ProjectVersion" +==> "BuildExpecto" ==> "BuildBenchmarkDotNet" ==> "BuildTest" ==> "RunTest"