Skip to content

Use Test.RunnerV2 - #686

Draft
lydell wants to merge 85 commits into
masterfrom
hash
Draft

Use Test.RunnerV2#686
lydell wants to merge 85 commits into
masterfrom
hash

Conversation

@lydell

@lydell lydell commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator

Use Test.RunnerV2 from elm-explorations/test#260, and implement everything that unlocks.

More info coming soon.

@jfmengels jfmengels left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Great work I'm not sure I reviewed everything, it was a lot of changes, but here are a few suggestions I have.

I think at this point it would be better for me to review by trying it out. If you have some instructions on how to use the 2 branches together, I would love to see them.

Comment thread elm/src/Test/Reporter/Console.elm Outdated


sendResult : Int -> Bool -> String -> List String -> Maybe String -> Decode.Value -> Decode.Value -> Cmd msg
sendResult testId isFuzzTest jsDefinitionName labels expectationElmCode debugLogs report =

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Maaaybe worth using a record to make function calls easier to read?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

The call sites look like this:

Ports.sendResult testId False jsDefinitionName labels expectationElmCode debugLogs report
Ports.sendResult testId True jsDefinitionName labels expectationElmCode debugLogs report

So a record would mostly be 🦜 (but give a name to the boolean).

I wonder what’s faster, passing a big record or many parameters. My guess would be many parameters (up to a point).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Alright that's fair.

I wonder what’s faster, passing a big record or many parameters.

Haven't checked it, but I'm pretty sure many parameters is faster. (will try to add a benchmark for that in my benchmark repo 👀 )

Comment thread elm/src/Test/Runner/Node.elm Outdated
outcome =
outcomeFromExpectations (config.run ())
-- The unnecessary-looking tuple here ensures that `getAndClearDebugLogs`
-- runs _after_ the thunk.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

You could also do

let
  ( expectation, duration ) =
    runWithDuration (\() -> Runner.runUnitTest unitTest)
  -- ...
in
sendUnitTestResult testId unitTest cachedExpectation duration (getAndClearDebugLogs False) model.testReporter

or

let
  ( expectation, duration ) =
    runWithDuration (\() -> Runner.runUnitTest unitTest)
in
let
  debugLogs =
    getAndClearDebugLogs False
  -- ...
in
-- ...

In both cases, this is a clearer sign that getAndClearDebugLogs will be called after runWithDuration. The order of computation of a tuple seems too incidental to rely on comfortably I'd say. (Also, one less tuple allocation)

I think a comment indicating that getAndClearDebugLogs has to be computed afterwards because it relies on side-effects from runWithDuration would be welcome still.

cachedExpectation =
case expectation of
UnitTestPass ->
CachedUnitTestPass

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I feel like the conversion a custom type is unnecessary, especially as it gets reconverted to something else right away in sendUnitTestResult

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

It is this way because sendUnitTestResult is also called in a different place – where we read test expectations from cache instead of running tests.

Nothing

else
Just (Debug.toString expectation)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What is the output of this? Would it start with CachedUnitTestFail ...? If so, that feels a bit odd.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Yes, for example:

CachedUnitTestFail { description = "Expect.equal", reason = Equality "\" blah\"" "\"blah\"" }

It can also be CachedUnitTestPass if there are debug logs.

Why does it feel a bit odd?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Maybe I should mention that it is used for codegen:ing the cache file.

Comment thread lib/Generate.js
)
.replace(
getAndClearDebugLogsDefinition,
'$1 = paused => { var logs = _Json_wrap(_Debug_logs); _Debug_logs = []; _Debug_logPaused = paused; return logs; }'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Use function(paused) instead of => to keep compiling to ES5?

Comment on lines +281 to +282
getAndClearDebugLogs True
|> (\_ ->

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Seems like there's a number of places where we ignre the returned logs. Should there be a variant of getAndClearDebugLogs that simply clears Debug.logs?

Comment thread lib/Hash.js
* @returns { void }
*/
function makeAcyclicStep(graph) {
const scc = Tarjan.stronglyConnectedComponents({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do you know if these chains are reduced to their smallest possible cycles? (Similar to the Elm import cycle sometimes including unrelated modules)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I don’t. All I know is that I naively “solved” recursive function chains myself, and kept finding cases where it broke. Then I read up on it and learned that it is a well-known computer science graph problem, and that Tarjan’s algorithm can solve it. Then I found an implementation of it on npm with lots of dependents that looked good. Since I started using it I haven’t run into issues.

Comment thread README.md Outdated
@lydell

lydell commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator Author

If you have some instructions on how to use the 2 branches together, I would love to see them.

In elm-explorations/test, run node --run install-local && rm -f ~/.elm/0.19.2/packages/registry.dat to copy the local version of the package into ~/.elm/.

In node-test-runner:

  1. cd example-application
  2. rm -rf elm-stuff
  3. ../bin/elm-test

To test it on some other project’s test suite:

  1. Make sure that the project uses Elm 0.19.2.
  2. Make sure that the project uses "elm-explorations/test": "2.2.1".
  3. rm -rf elm-stuff
  4. ~/path/to/node-test-runner/bin/elm-test

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants