Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
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
9 changes: 9 additions & 0 deletions src/SnapshotTesting.jl
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,14 @@ import DeepDiffs
using Test

include("snapshots.jl")
include("suite.jl")

export SnapshotTestSuite,
snapshot_tests,
snapshot_expected_dir,
snapshot_produce,
snapshot_test_extras,
snapshot_allow_additions,
run_snapshot_tests

end
107 changes: 107 additions & 0 deletions src/suite.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
"""
SnapshotTestSuite

Abstract type for dispatch-based snapshot test suites. Packages define a concrete
subtype and overload the interface functions to get automatic test iteration,
filtering, and plumbing.

# Required methods
- `snapshot_tests(suite)` — return a `Vector{Pair{String,String}}` of `(name, path)` pairs
- `snapshot_expected_dir(suite)` — return the path to the expected snapshots directory
- `snapshot_produce(suite, name, path, dir)` — produce output files in `dir`

# Optional methods (have defaults)
- `snapshot_test_extras(suite, name, path)` — run assertions before the snapshot comparison (default: no-op)
- `snapshot_allow_additions(suite)` — whether to allow new files in snapshots (default: `true`)

# Example

```julia
struct MySnapshots <: SnapshotTestSuite end

SnapshotTesting.snapshot_tests(::MySnapshots) = [("test1" => "path/to/test1"), ...]
SnapshotTesting.snapshot_expected_dir(::MySnapshots) = joinpath(@__DIR__, "expected")
function SnapshotTesting.snapshot_produce(::MySnapshots, name, path, dir)
write(joinpath(dir, "out.txt"), run_my_code(path))
end

run_snapshot_tests(MySnapshots())
run_snapshot_tests(MySnapshots(); filter="test1") # run single test
```
"""
abstract type SnapshotTestSuite end

"""
snapshot_tests(suite::SnapshotTestSuite) -> Vector{Pair{String,String}}

Return the list of test cases as `name => path` pairs.
"""
function snapshot_tests end

"""
snapshot_expected_dir(suite::SnapshotTestSuite) -> String

Return the path to the directory containing expected snapshot outputs.
"""
function snapshot_expected_dir end

"""
snapshot_produce(suite::SnapshotTestSuite, name::String, path::String, dir::String)

Produce snapshot output files in `dir`.
"""
function snapshot_produce end

"""
snapshot_test_extras(suite::SnapshotTestSuite, name::String, path::String)

Run additional assertions before the snapshot comparison. Defaults to no-op.
"""
snapshot_test_extras(::SnapshotTestSuite, name, path) = nothing

"""
snapshot_allow_additions(suite::SnapshotTestSuite) -> Bool

Whether to allow new files in snapshot output that aren't in the expected directory.
Defaults to `true`.
"""
snapshot_allow_additions(::SnapshotTestSuite) = true

"""
run_snapshot_tests(suite::SnapshotTestSuite; filter=nothing, skip=String[])

Run all snapshot tests defined by `suite`, with optional filtering and skipping.

- `filter=nothing`: run all tests
- `filter="name"`: run only the test with exactly this name
- `filter=f::Function`: run tests where `f(name)` returns `true`
- `skip=["name1", ...]`: skip tests with these names
"""
function run_snapshot_tests(suite::SnapshotTestSuite; filter=nothing, skip=String[])
cases = snapshot_tests(suite)
expected = snapshot_expected_dir(suite)
allow = snapshot_allow_additions(suite)
pred = _make_filter(filter)
Comment on lines +80 to +84
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.

Could we maybe also just have a run_snapshot_test syntactic sugar function that runs a single snapshot test

skip_set = Set{String}(skip)

matched = false
for (name, path) in cases
pred(name) || continue
name in skip_set && continue
matched = true
@testset "$name" begin
snapshot_test_extras(suite, name, path)
test_snapshot(expected, name; allow_additions=allow) do dir
snapshot_produce(suite, name, path, dir)
end
end
end

if !matched && filter !== nothing
@warn "No snapshot tests matched the filter" filter
end
end

_make_filter(::Nothing) = _ -> true
_make_filter(name::AbstractString) = n -> n == name
_make_filter(f::Function) = f
3 changes: 3 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,7 @@ using Test
@testset "update modes" begin
include("test_update_modes.jl")
end
@testset "suite" begin
include("test_suite.jl")
end
end
159 changes: 159 additions & 0 deletions test/test_suite.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
using SnapshotTesting
using Test

# --- Test suite types ---

struct BasicSuite <: SnapshotTestSuite
cases::Vector{Pair{String,String}}
expected_dir::String
end
SnapshotTesting.snapshot_tests(s::BasicSuite) = s.cases
SnapshotTesting.snapshot_expected_dir(s::BasicSuite) = s.expected_dir
function SnapshotTesting.snapshot_produce(::BasicSuite, name, path, dir)
write(joinpath(dir, "out.txt"), "output for $name from $path")
end

mutable struct ExtrasSuite <: SnapshotTestSuite
cases::Vector{Pair{String,String}}
expected_dir::String
extras_called::Vector{String}
ExtrasSuite(cases, dir) = new(cases, dir, String[])
end
SnapshotTesting.snapshot_tests(s::ExtrasSuite) = s.cases
SnapshotTesting.snapshot_expected_dir(s::ExtrasSuite) = s.expected_dir
function SnapshotTesting.snapshot_test_extras(s::ExtrasSuite, name, _path)
push!(s.extras_called, name)
end
function SnapshotTesting.snapshot_produce(::ExtrasSuite, name, _path, dir)
write(joinpath(dir, "out.txt"), "extras output for $name")
end

struct StrictSuite <: SnapshotTestSuite
cases::Vector{Pair{String,String}}
expected_dir::String
end
SnapshotTesting.snapshot_tests(s::StrictSuite) = s.cases
SnapshotTesting.snapshot_expected_dir(s::StrictSuite) = s.expected_dir
SnapshotTesting.snapshot_allow_additions(::StrictSuite) = false
function SnapshotTesting.snapshot_produce(::StrictSuite, name, _path, dir)
write(joinpath(dir, "out.txt"), "output for $name")
end

# --- Helper to pre-create expected snapshot directories ---

function setup_expected!(expected_dir, name, content)
d = joinpath(expected_dir, name)
mkpath(d)
write(joinpath(d, "out.txt"), content)
end

# --- Tests ---

@testset "SnapshotTestSuite" begin

@testset "basic suite runs all tests" begin
mktempdir() do tmpdir
expected = joinpath(tmpdir, "expected")
mkpath(expected)
setup_expected!(expected, "test_a", "output for test_a from /path/test_a")
setup_expected!(expected, "test_b", "output for test_b from /path/test_b")

cases = ["test_a" => "/path/test_a", "test_b" => "/path/test_b"]
suite = BasicSuite(cases, expected)
run_snapshot_tests(suite)
end
end

@testset "filter by exact string" begin
mktempdir() do tmpdir
expected = joinpath(tmpdir, "expected")
mkpath(expected)
setup_expected!(expected, "test_a", "output for test_a from /path/test_a")

cases = ["test_a" => "/path/test_a", "test_b" => "/path/test_b"]
suite = BasicSuite(cases, expected)
run_snapshot_tests(suite; filter="test_a")
# test_b should not have been run (no directory created)
@test !isdir(joinpath(expected, "test_b"))
end
end

@testset "filter by predicate function" begin
mktempdir() do tmpdir
expected = joinpath(tmpdir, "expected")
mkpath(expected)
setup_expected!(expected, "csv_one", "output for csv_one from /path/csv_one")
setup_expected!(expected, "csv_two", "output for csv_two from /path/csv_two")

cases = [
"csv_one" => "/path/csv_one",
"csv_two" => "/path/csv_two",
"bin_one" => "/path/bin_one",
]
suite = BasicSuite(cases, expected)
run_snapshot_tests(suite; filter=n -> startswith(n, "csv_"))
@test !isdir(joinpath(expected, "bin_one"))
end
end

@testset "skip list" begin
mktempdir() do tmpdir
expected = joinpath(tmpdir, "expected")
mkpath(expected)
setup_expected!(expected, "test_b", "output for test_b from /path/test_b")

cases = ["test_a" => "/path/test_a", "test_b" => "/path/test_b"]
suite = BasicSuite(cases, expected)
run_snapshot_tests(suite; skip=["test_a"])
@test !isdir(joinpath(expected, "test_a"))
end
end

@testset "empty suite does not error" begin
mktempdir() do tmpdir
expected = joinpath(tmpdir, "expected")
mkpath(expected)
suite = BasicSuite(Pair{String,String}[], expected)
run_snapshot_tests(suite)
end
end

@testset "filter matching nothing emits warning" begin
mktempdir() do tmpdir
expected = joinpath(tmpdir, "expected")
mkpath(expected)
cases = ["test_a" => "/path/test_a"]
suite = BasicSuite(cases, expected)
@test_logs (:warn, "No snapshot tests matched the filter") run_snapshot_tests(
suite; filter="nonexistent"
)
end
end

@testset "snapshot_test_extras is called before produce" begin
mktempdir() do tmpdir
expected = joinpath(tmpdir, "expected")
mkpath(expected)
setup_expected!(expected, "test_a", "extras output for test_a")
setup_expected!(expected, "test_b", "extras output for test_b")

cases = ["test_a" => "/path/test_a", "test_b" => "/path/test_b"]
suite = ExtrasSuite(cases, expected)
run_snapshot_tests(suite)
@test suite.extras_called == ["test_a", "test_b"]
end
end

@testset "snapshot_allow_additions=false is respected" begin
mktempdir() do tmpdir
expected = joinpath(tmpdir, "expected")
mkpath(expected)
setup_expected!(expected, "test_a", "output for test_a")

cases = ["test_a" => "/path/test_a"]
suite = StrictSuite(cases, expected)
run_snapshot_tests(suite)
end
end

end
Loading