Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
80 commits
Select commit Hold shift + click to select a range
6536cc6
Add Test.Runner.fromTestV2
lydell Jul 22, 2026
6aae883
Notify runners of each run of a fuzz test
lydell Jul 22, 2026
732c1a7
Make it possible to tag tests
lydell Jul 22, 2026
a87ad90
Simplify away intermediate type
lydell Jul 23, 2026
b9f1a90
Fewer impossible states
lydell Jul 26, 2026
ad13a50
Return the RandomRun and a function for rerunning the failure
lydell Jul 26, 2026
56528cf
Remove old NotifyRunStarted
lydell Jul 26, 2026
1fb8d30
Make it possible to run a fuzz test from a previous RandomRun
lydell Jul 26, 2026
4b52fa9
Fix public vs internal types
lydell Jul 26, 2026
a80567f
The old implemenation already does this, no need to worry
lydell Jul 26, 2026
a5d8226
Description
lydell Jul 26, 2026
00fd131
Call it fuzzerInts consistently
lydell Jul 26, 2026
0f93b19
Expose fuzzReproduce
lydell Jul 26, 2026
7e6d8b3
More docs
lydell Jul 26, 2026
8be1c2a
Expose tagTest
lydell Jul 26, 2026
54d75cf
Remove fuzzReproduce – let the runner automatically provide a saved r…
lydell Jul 28, 2026
50b44a3
Continue with a regular fuzz run after a saved RandomRun
lydell Jul 28, 2026
b6d30f2
Add parametrize and fuzzWithExamples
lydell Jul 28, 2026
5ece70b
Actually expose stuff
lydell Jul 29, 2026
d49d9d5
Catch exceptions earlier for fuzz tests
lydell Aug 2, 2026
0123966
Optimize away one layer of lambdas
lydell Aug 2, 2026
2129942
Docs
lydell Aug 2, 2026
2f69e71
fromTestV2Helper -> toTestsHelper
lydell Aug 5, 2026
c72d811
Use arrays instead of lists – maybe faster append, node-test-runner n…
lydell Aug 5, 2026
b28a21f
Remove parametrized and fuzzWithExamples from this branch
lydell Aug 16, 2026
6cbc795
RunnerV2 module
lydell Aug 16, 2026
ebb7451
Opaque
lydell Aug 16, 2026
b2c7f4c
RunnerTests
lydell Aug 16, 2026
ee0f805
Add missing docs
lydell Aug 16, 2026
f07729a
Improve docs
lydell Aug 16, 2026
ef858b8
Add test for tagTest
lydell Aug 16, 2026
6890f3b
Rename to fromFuzzTestExpectation
lydell Aug 17, 2026
77e33e2
Avoid creating a new record by not destructuring
lydell Aug 17, 2026
cebd96d
Flatten to `FuzzTestPass DistributionReport`
lydell Aug 17, 2026
646ba7a
Apply doc suggestions from code review
lydell Aug 17, 2026
530c4aa
Extract code block to tryReproduceFailureFromFuzzerInts function
lydell Aug 17, 2026
3d83b98
Add comment explaining reproducing fuzz test failures a bit more
lydell Aug 17, 2026
9116894
Optimize toTestsHelper
lydell Aug 17, 2026
e48084e
Add `Test.RunnerV2.identifyTest`
lydell Aug 22, 2026
e523882
Make running tests also measure how long they take
lydell Aug 22, 2026
c384b2d
Capture Debug.log
lydell Aug 22, 2026
a2e470e
Massively simplify Debug.log collection
lydell Aug 22, 2026
43f1d19
Add DebugLogs.fromString
lydell Aug 22, 2026
9bbde9a
Renames
lydell Aug 22, 2026
4577f5f
Document stderr
lydell Aug 22, 2026
6cd9456
Tweaks
lydell Aug 22, 2026
b63cc63
Simplify by storing debug logs as a string
lydell Aug 22, 2026
c049d6f
Document debug log string format
lydell Aug 22, 2026
8a19106
Remove thrown exception test for now
lydell Aug 22, 2026
ba02a3a
Update comment
lydell Aug 22, 2026
5c572dc
Rename leftover old name for param
lydell Aug 22, 2026
87336e8
Make it easier to use Debug.log to debug a runner
lydell Aug 22, 2026
c5f7f91
Simplify links
lydell Aug 22, 2026
61ce974
Count number of skipped tests
lydell Aug 22, 2026
2cde6a1
Optimize Only record update
lydell Aug 22, 2026
d9af4fd
Revert "Count number of skipped tests"
lydell Aug 22, 2026
34073a0
Fix compile error after revert
lydell Aug 22, 2026
206d315
Merge branch 'master' into fromTestV2
lydell Aug 24, 2026
b9d6196
Merge branch 'master' into fromTestV2
lydell Sep 27, 2026
0fd56c3
Fix compilation errors in tests after merge
lydell Sep 27, 2026
1d4b68b
Count tests excluded due to `skip` and `only`
lydell Sep 27, 2026
5f3c0a7
Remove comment referring to deprecated module
lydell Sep 27, 2026
2173f95
Don’t use Test.Runner.getFailureReason internally
lydell Sep 27, 2026
3add818
Merge branch 'master' into fromTestV2
lydell Sep 28, 2026
ca106cf
Remove unnecessary in-let
lydell Sep 28, 2026
77ea3e7
Suppress “unused” `Mode` (Kernel-backed)
lydell Sep 28, 2026
0e25523
Merge branch 'master' into fromTestV2
lydell Sep 29, 2026
8751da7
Fix swapIfOutOfOrder building a record with unrewritten field names
gampleman Sep 29, 2026
0791168
Report how many values a failing fuzz test generated
gampleman Sep 29, 2026
d0b5565
Add an F-metric harness for measuring bug-finding ability
gampleman Sep 29, 2026
8966def
Add the combined small/large domain cases, and markdown output
gampleman Sep 29, 2026
8c214fa
Fix benchmarks/, and make it measure passing-test throughput
gampleman Sep 30, 2026
01bd7b6
Compare F-metric time ratios over a fixed set of cases
gampleman Sep 30, 2026
bcea362
Track which inputs have been covered, and decline them
gampleman Sep 30, 2026
e1e6506
Make generator density explicit, and bound the coverage bookkeeping
gampleman Sep 30, 2026
e6071e3
Walk the run by index when recording coverage, and explain the residu…
gampleman Sep 30, 2026
70f2364
Replace the tracked-node budget with a depth bound
gampleman Sep 30, 2026
6d0895b
Add benchmarks with a real test body
gampleman Oct 1, 2026
35c569a
Experiment: scale trackability with --fuzz instead of a fixed width cap
gampleman Oct 1, 2026
4ccccc1
Bound the cumulative path product, not just each node
gampleman Oct 1, 2026
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 benchmarks/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
elm-stuff/
elm_home/
elm.js
index.html
results/
308 changes: 234 additions & 74 deletions benchmarks/Main.elm
Original file line number Diff line number Diff line change
@@ -1,80 +1,240 @@
module Main exposing (main)
port module Main exposing (main)

import Benchmark exposing (..)
import Benchmark.Runner as Runner
import Expect exposing (Expectation)
{-| Throughput benchmarks for elm-test.

Where `f-metric/` asks "is elm-test good at finding bugs", this asks "how fast
does it get through a test". The two answer different questions, and the F-metric
harness structurally cannot answer this one: its whole corpus is failing tests,
so it never measures the case that dominates a real suite -- a fuzz test that
passes and therefore runs its full budget.

That's also the case where exhaustive checking should pay off by stopping early:
`Fuzz.bool` given `--fuzz 100` has two possible inputs and ought to cost two
runs, not a hundred.

Headless rather than browser-based, so it runs in CI and so numbers can be
produced without a human clicking anything. That means it no longer uses
elm-explorations/benchmark, whose runner is a browser program; the statistics are
simple enough to do in `runner.mjs`.

Uses the public `Test.Runner` API. Its `run : () -> List Expectation` is a plain
function, which is what makes it benchmarkable -- `Test.RunnerV2.runFuzzTest`
returns a `Task`, and a Task can't be timed from inside Elm.

-}

import Array exposing (Array)
import Json.Decode as Decode
import Json.Encode as Encode
import Platform
import Random
import Snippets
import Test.Internal exposing (Test(..))
import Test exposing (Test)
import Test.Runner


main : Runner.BenchmarkProgram
port toJs : Encode.Value -> Cmd msg


port fromJs : (Decode.Value -> msg) -> Sub msg


main : Program () () Msg
main =
Runner.program suite


suite : Benchmark
suite =
describe "Fuzz"
[ describe "int"
[ benchmark "generating" (benchTest Snippets.intPass)
, benchmark "simplifying" (benchTest Snippets.intFail)
]
, describe "intRange"
[ benchmark "generating" (benchTest Snippets.intRangePass)
, benchmark "simplifying" (benchTest Snippets.intRangeFail)
]
, describe "string"
[ benchmark "generating" (benchTest Snippets.stringPass)
, benchmark "simplifying" (benchTest Snippets.stringFail)
]
, describe "float"
[ benchmark "generating" (benchTest Snippets.floatPass)
, benchmark "simplifying" (benchTest Snippets.floatFail)
]
, describe "bool"
[ benchmark "generating" (benchTest Snippets.boolPass)
, benchmark "simplifying" (benchTest Snippets.boolFail)
]
, describe "char"
[ benchmark "generating" (benchTest Snippets.charPass)
, benchmark "simplifying" (benchTest Snippets.charFail)
]
, describe "list of int"
[ benchmark "generating" (benchTest Snippets.listIntPass)
, benchmark "simplifying" (benchTest Snippets.listIntFail)
]
, describe "maybe of int"
[ benchmark "generating" (benchTest Snippets.maybeIntPass)
, benchmark "simplifying" (benchTest Snippets.maybeIntFail)
]
, describe "result of string and int"
[ benchmark "generating" (benchTest Snippets.resultPass)
, benchmark "simplifying" (benchTest Snippets.resultFail)
]
, describe "map"
[ benchmark "generating" (benchTest Snippets.mapPass)
, benchmark "simplifying" (benchTest Snippets.mapFail)
]
, describe "andMap"
[ benchmark "generating" (benchTest Snippets.andMapPass)
, benchmark "simplifying" (benchTest Snippets.andMapFail)
]
, describe "map5"
[ benchmark "generating" (benchTest Snippets.map5Pass)
, benchmark "simplifying" (benchTest Snippets.map5Fail)
]
]


benchTest : Test -> (() -> List Expectation)
benchTest test =
case test of
ElmTestVariant__FuzzTest fn ->
\_ -> fn (Random.initialSeed 0) 10

ElmTestVariant__Labeled _ test_ ->
benchTest test_

test_ ->
Debug.todo <| "No support for benchmarking this type of test: " ++ Debug.toString test_
Platform.worker
{ init = \_ -> ( (), Cmd.none )
, update = update
, subscriptions = \_ -> fromJs FromJs
}


type Msg
= FromJs Decode.Value


type Request
= Manifest
| Run RunParams


type alias RunParams =
{ index : Int
, runs : Int
, seed : Int
, iterations : Int
}


update : Msg -> () -> ( (), Cmd Msg )
update (FromJs raw) model =
case Decode.decodeValue requestDecoder raw of
Err error ->
( model
, toJs
(Encode.object
[ ( "tag", Encode.string "error" )
, ( "error", Encode.string (Decode.errorToString error) )
]
)
)

Ok Manifest ->
( model
, toJs
(Encode.object
[ ( "tag", Encode.string "manifest" )
, ( "benchmarks"
, Encode.list
(\( name, passes, _ ) ->
Encode.object
[ ( "name", Encode.string name )

{- runs/sec is only meaningful for a test that
actually executes its whole budget. A failing
test stops at the first value, so dividing by
`runs` would overstate it by orders of
magnitude.
-}
, ( "passes", Encode.bool passes )
]
)
benchmarks
)
]
)
)

Ok (Run params) ->
case Array.get params.index (Array.fromList benchmarks) of
Nothing ->
( model
, toJs
(Encode.object
[ ( "tag", Encode.string "error" )
, ( "error", Encode.string "no benchmark at that index" )
]
)
)

Just ( _, _, test ) ->
( model
, toJs
(Encode.object
[ ( "tag", Encode.string "done" )

{- The count exists to force the work. Without
consuming the expectations there'd be nothing
stopping the whole loop being optimized away.
-}
, ( "checksum", Encode.int (repeat params test) )
]
)
)


requestDecoder : Decode.Decoder Request
requestDecoder =
Decode.field "tag" Decode.string
|> Decode.andThen
(\tag ->
case tag of
"manifest" ->
Decode.succeed Manifest

"run" ->
Decode.map4 RunParams
(Decode.field "index" Decode.int)
(Decode.field "runs" Decode.int)
(Decode.field "seed" Decode.int)
(Decode.field "iterations" Decode.int)
|> Decode.map Run

_ ->
Decode.fail ("unknown tag: " ++ tag)
)


{-| Run the whole test `iterations` times, returning the number of expectations
produced so the work can't be elided.

Each iteration rebuilds the runners from scratch, which is what a real runner
does per test, and keeps one iteration from warming state for the next.

-}
repeat : RunParams -> Test -> Int
repeat params test =
repeatHelp params test params.iterations 0


repeatHelp : RunParams -> Test -> Int -> Int -> Int
repeatHelp params test remaining acc =
if remaining <= 0 then
acc

else
let
expectations : Int
expectations =
case Test.Runner.fromTest params.runs (Random.initialSeed params.seed) test of
Test.Runner.Plain runners ->
List.foldl (\runner sum -> sum + List.length (runner.run ())) 0 runners

_ ->
0
in
repeatHelp params test (remaining - 1) (acc + expectations)



-- BENCHMARKS


{-| Name, and the test to run.

The `(passes)` cases are the interesting ones for throughput: they run the full
`runs` budget. The `simplify/` ones stop at the first value and then simplify, so
they measure simplification cost instead.

-}
benchmarks : List ( String, Bool, Test )
benchmarks =
[ -- Small enough to enumerate exhaustively. If exhaustive checking lands,
-- these should collapse to a couple of runs regardless of the budget.
( "small/unit", True, Snippets.unitPass )
, ( "small/bool", True, Snippets.boolPass )
, ( "small/order", True, Snippets.orderPass )
, ( "small/intRange-0-20", True, Snippets.intRange0To20Pass )
, ( "small/pair-bool", True, Snippets.pairBoolPass )
, ( "small/maybe-bool", True, Snippets.maybeBoolPass )
, ( "small/oneOfValues", True, Snippets.oneOfValuesPass )

-- Mid-size: finite, but not trivially so. These decide where a budget
-- boundary should sit.
, ( "mid/intRange-0-1000", True, Snippets.intRange0To1000Pass )
, ( "mid/pair-intRange-0-30", True, Snippets.pairIntRange0To30Pass )

-- Unbounded. These must not regress: they're where an enumeration probe or
-- deduplication bookkeeping is pure overhead.
, ( "large/int", True, Snippets.intPass )
, ( "large/float", True, Snippets.floatPass )
, ( "large/string", True, Snippets.stringPass )
, ( "large/list-int", True, Snippets.listIntPass )
, ( "large/record-map5", True, Snippets.map5Pass )

{- Same fuzzers, but with a test body that does real work. Coverage tracking's
overhead is mostly per-run and fixed, so a heavier body dilutes it; early
termination skips the body too, so a heavier body makes that win larger.
-}
, ( "heavy/bool", True, Snippets.boolHeavyPass )
, ( "heavy/pair-bool", True, Snippets.pairBoolHeavyPass )
, ( "heavy/string", True, Snippets.stringHeavyPass )
, ( "heavy/list-int", True, Snippets.listIntHeavyPass )

-- Rejection path.
, ( "filter/even", True, Snippets.filterPass )

-- Simplification cost, for contrast with the passing cases.
, ( "simplify/int", False, Snippets.intFail )
, ( "simplify/string", False, Snippets.stringFail )
, ( "simplify/list-int", False, Snippets.listIntFail )
]
Loading
Loading