Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve support for interactive experiences #482

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
14 changes: 14 additions & 0 deletions Expecto.Tests/Bug_RepeatedColourSetting.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module Bug_RepeatedColourSetting

open Expecto
open Expecto.Logging


[<Tests>]
let tests =
ftest "Colour can be set repeatedly" {
let colours = [|Colour0; Colour256|]
colours |> Array.iter ANSIOutputWriter.setColourLevel

Expect.equal (ANSIOutputWriter.getColour ()) (Array.last colours) "Colour should be the last set value"
}
1 change: 1 addition & 0 deletions Expecto.Tests/Expecto.Tests.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
<Compile Include="FsCheckTests.fs" />
<Compile Include="PerformanceTests.fs" />
<Compile Include="Bug341.fs" />
<Compile Include="Bug_RepeatedColourSetting.fs" />
<Compile Include="Main.fs" />
<None Include="paket.references" />
<ProjectReference Include="..\Expecto.Hopac\Expecto.Hopac.fsproj" />
Expand Down
67 changes: 67 additions & 0 deletions Expecto.Tests/Tests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -1821,4 +1821,71 @@ let theory =
testTheoryTask "task odd numbers" [1; 3; 5;] <| fun x -> task {
Expect.isTrue (x % 2 = 1) "should be odd"
}
]

[<Tests>]
let interactiveRunTests =
testList "running for interactive flows" [
testList "logging config" [
testCase "set loggingConfig via CLIArguments" <| fun () ->
let tests = testList "Test me" [
testCase "I am a case" (fun () -> ())
]

let globalLoggerOutput = new Text.StringBuilder()
let globalWriter = new StringWriter(globalLoggerOutput)
Global.initialise {
Global.defaultConfig with
getLogger = (fun name ->
TextWriterTarget(name, LogLevel.Info, globalWriter)
)
}

(runTestsWithCLIArgs [] [|"--help"|] tests) |> ignore

let cliArgLoggerOutput = new Text.StringBuilder()
let cliArgWriter = new StringWriter(cliArgLoggerOutput)
let loggingConfigFactory _ =
{
Global.defaultConfig with
getLogger = (fun name ->
TextWriterTarget(name, LogLevel.Info, cliArgWriter)
)
}

(runTestsWithCLIArgs [LoggingConfigFactory (FromVerbosity loggingConfigFactory)] [|"--help"|] tests) |> ignore

Expect.equal (string cliArgLoggerOutput) (string globalLoggerOutput) "Caputred output should be the same with Global.initialize or CLIArguments.LoggingConfigFactory"
]

testList "runTestsReturnLogs" [
testCase "can print help" <| fun () ->
let tests = (testList "hi" [])
let output = runTestsReturnLogs [] [|"--help"|] tests
Expect.stringContains output "Options:" ""
Expect.stringContains output "--help Show this help message." "help message should contain the --help description"


testCase "can list tests" <| fun () ->
let tests = (testList "hi" [
testCase "case1" (fun () -> ())
testCase "case2" (fun () -> ())
])
let expectedTestNames = ["hi.case1"; "hi.case2"]
let testListText = String.Join(Environment.NewLine, expectedTestNames)

let output = runTestsReturnLogs [] [|"--list-tests"|] tests
Expect.stringContains output testListText ""

testCase "can run tests and show basic status counts" <| fun () ->
let tests = testList "Interactive Tests" [
testCase "I pass" (fun () -> ())
test "I fail" {
Expect.equal true false "Should fail"
}
]

let output = runTestsReturnLogs [] [|"--colours"; "0"|] tests
Expect.stringContains output "1 passed, 0 ignored, 1 failed, 0 errored" ""
]
]
4 changes: 4 additions & 0 deletions Expecto/Expecto.Impl.fs
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,9 @@ module Impl =
colour: ColourLevel
/// Split test names by `.` or `/`
joinWith: JoinWith
/// A factory method taking the configured min log level and returning a logging config.
/// Can be used to swap out log targets like the LiterateConsoleTarget, TextWriterTarget, and OutputWindowTarget
loggingConfigFactory: (LogLevel -> LoggingConfig) option
}
static member defaultConfig =
{ runInParallel = true
Expand All @@ -546,6 +549,7 @@ module Impl =
noSpinner = false
colour = Colour8
joinWith = JoinWith.Dot
loggingConfigFactory = None
}

member x.appendSummaryHandler handleSummary =
Expand Down
55 changes: 48 additions & 7 deletions Expecto/Expecto.fs
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@
member inline __.TryWith(p, cf) = task.TryWith(p, cf)
member __.Run f =
let a = task {
do! task.Run f

Check warning on line 239 in Expecto/Expecto.fs

View workflow job for this annotation

GitHub Actions / build

This state machine is not statically compilable. The resumable code value(s) 'f' does not have a definition. An alternative dynamic implementation will be used, which may be slower. Consider adjusting your code to ensure this state machine is statically compilable, or else suppress this warning.

Check warning on line 239 in Expecto/Expecto.fs

View workflow job for this annotation

GitHub Actions / build

This state machine is not statically compilable. The resumable code value(s) 'f' does not have a definition. An alternative dynamic implementation will be used, which may be slower. Consider adjusting your code to ensure this state machine is statically compilable, or else suppress this warning.
}
match focusState with
| Normal -> testCaseTask name a
Expand Down Expand Up @@ -365,6 +365,10 @@
type SummaryHandler =
| SummaryHandler of (TestRunSummary -> unit)

[<ReferenceEquality>]
type LoggingConfigFactory =
| FromVerbosity of (LogLevel -> LoggingConfig)

/// The CLI arguments are the parameters that are possible to send to Expecto
/// and change the runner's behaviour.
type CLIArguments =
Expand Down Expand Up @@ -426,6 +430,9 @@
| Append_Summary_Handler of SummaryHandler
/// Specify test names join character.
| JoinWith of split: string
/// A factory method taking the configured min log level and returning a logging config.
/// Can be used to swap out log targets like the LiterateConsoleTarget, TextWriterTarget, and OutputWindowTarget
| LoggingConfigFactory of LoggingConfigFactory

let options = [
"--sequenced", "Don't run the tests in parallel.", Args.none Sequenced
Expand Down Expand Up @@ -513,6 +520,7 @@
| Printer p -> fun o -> { o with printer = p }
| Verbosity l -> fun o -> { o with verbosity = l }
| Append_Summary_Handler (SummaryHandler h) -> fun o -> o.appendSummaryHandler h
| LoggingConfigFactory (FromVerbosity f) -> fun o -> { o with loggingConfigFactory = Some f}

[<CompilationRepresentation (CompilationRepresentationFlags.ModuleSuffix)>]
module ExpectoConfig =
Expand Down Expand Up @@ -587,13 +595,18 @@
let runTestsWithCLIArgsAndCancel (ct:CancellationToken) cliArgs args tests =
let runTestsWithCancel (ct:CancellationToken) config (tests:Test) =
ANSIOutputWriter.setColourLevel config.colour
Global.initialiseIfDefault
{ Global.defaultConfig with
getLogger = fun name ->
LiterateConsoleTarget(
name, config.verbosity,
consoleSemaphore = Global.semaphore()) :> Logger
}

let loggingConfig =
match config.loggingConfigFactory with
| Some factory -> factory config.verbosity |> Global.initialise
| None ->
{ Global.defaultConfig with
getLogger = fun name ->
LiterateConsoleTarget(
name, config.verbosity,
consoleSemaphore = Global.semaphore()) :> Logger
} |> Global.initialiseIfDefault

config.logName |> Option.iter setLogName
if config.failOnFocusedTests && passesFocusTestCheck config tests |> not then
1
Expand Down Expand Up @@ -648,3 +661,31 @@
/// Returns 0 if all tests passed, otherwise 1
let runTestsInAssemblyWithCLIArgs cliArgs args =
runTestsInAssemblyWithCLIArgsAndCancel CancellationToken.None cliArgs args

/// Runs all given tests with the supplied typed command-line options.
/// Returns the console output as a string (with ANSI coloring by default)
/// Useful for interactive environments like F# interactive or notebooks
let runTestsReturnLogs cliArgs args tests =
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could use feedback on this method name.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe runTestsForInteractive

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about runTestsInteractively?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought of a similar name, but I feel like it denotes the test run itself is interactive, as in there will be decision points for the user during the test run.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, runTestsForInteractive seems reasonable to me, then.


let literateOutputWriter (outputBuilder: Text.StringBuilder) (text: (string*ConsoleColor) list) : unit =
let colorizeLine (text, color) = ColourText.colouriseText color text
let sbAppend (builder: Text.StringBuilder) (text: string) =
builder.Append(text)

text
|> List.iter (colorizeLine >> (sbAppend outputBuilder) >> ignore)

let outputBuilder = System.Text.StringBuilder("")

let loggingConfigFactory verbosity =
{ Global.defaultConfig with
getLogger = fun name ->
Expecto.Logging.LiterateConsoleTarget(
name,
minLevel = verbosity,
outputWriter = (literateOutputWriter outputBuilder)) :> Expecto.Logging.Logger
}

let cliArgs = (LoggingConfigFactory (FromVerbosity loggingConfigFactory)) :: cliArgs
runTestsWithCLIArgs cliArgs args tests |> ignore
outputBuilder.ToString()
2 changes: 1 addition & 1 deletion Expecto/Logging.fs
Original file line number Diff line number Diff line change
Expand Up @@ -879,7 +879,7 @@ module internal ANSIOutputWriter =
override __.WriteLine() = write "\n"

let mutable internal colours = None
let internal setColourLevel c = if colours.IsNone then colours <- Some c
let internal setColourLevel c = colours <- Some c
Copy link
Collaborator Author

@farlee2121 farlee2121 Nov 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I couldn't discern a reason this needed to be settable only once, and it prevents the colour setting from working as expected in interactive environments

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let internal getColour() = Option.defaultValue Colour8 colours

let colourReset = "\u001b[0m"
Expand Down
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ What follows is the Table of Contents for this README, which also serves as the
- [`runTestsWithCLIArgsAndCancel`](#runtestswithcliargsandcancel)
- [`runTestsInAssemblyWithCLIArgs`](#runtestsinassemblywithcliargs)
- [`runTestsInAssemblyWithCLIArgsAndCancel`](#runtestsinassemblywithcliargsandcancel)
- [`runTestsReturnLogs`](#runtestsreturnlogs)
- [Filtering with `filter`](#filtering-with-filter)
- [Shuffling with `shuffle`](#shuffling-with-shuffle)
- [Stress testing](#stress-testing)
Expand Down Expand Up @@ -220,6 +221,8 @@ runTestsWithCLIArgs [] [||] simpleTest
which returns 1 if any tests failed, otherwise 0. Useful for returning to the
operating system as error code.

For interactive environments, you can alternatively call [`runTestsReturnLogs`](#runtestsreturnlogs), which returns the console output as a string.

It's worth noting that `<|` is just a way to change the associativity of the
language parser. In other words; it's equivalent to:

Expand Down Expand Up @@ -250,6 +253,22 @@ Signature `CancellationToken -> CLIArguments seq -> string[] -> int`. Runs the t
assembly and also overrides the passed `CLIArguments` with the command line
parameters. All tests need to be marked with the `[<Tests>]` attribute.

### `runTestsReturnLogs`

Signature `CLIArguments seq -> string[] -> Test -> string`.
Accepts the same arguments as `runTestsWithCLIArgs`, but returns the console output as a string.

This is useful for interactive environments like [F# interactive](https://learn.microsoft.com/en-us/dotnet/fsharp/tools/fsharp-interactive/) and [Polyglot Notebooks](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.dotnet-interactive-vscode), where console output is not available but the returned string can be displayed instead.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I plan to add a screen shot once I can show the proper package reference.
Something like #r "nuget: Expecto, 10.2.0".

Note that ANSI colors are used by default, but can be turned off using `--colours 0`.
```fsharp
runTestsReturnLogs [] [|"--colours";"0"|] tests
```

Any valid CLI arguments work, including `--help` and `--list-tests`.



### Filtering with `filter`

You can single out tests by filtering them by name (e.g. in the
Expand Down
Loading