Skip to content

Commit c80a34e

Browse files
committed
filtering: add iter(::Integer) pattern constructor
1 parent 3f3c205 commit c80a34e

File tree

4 files changed

+73
-23
lines changed

4 files changed

+73
-23
lines changed

src/ReTest.jl

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module ReTest
22

3-
export retest, @testset, @testset_macro, not, interpolated, reachable, depth, pass, fail
3+
export retest, @testset, @testset_macro, not, interpolated, reachable, depth, pass, fail, iter
44

55
using Distributed
66
using Base.Threads: nthreads
@@ -80,6 +80,7 @@ mutable struct TestsetExpr
8080
loopiters::Maybe{Expr}
8181
hasbroken::Bool
8282
hasbrokenrec::Bool # recursive hasbroken, transiently
83+
iter::Union{Int,UnitRange{Int}} # transient loop iteration counter
8384
run::Bool
8485
descwidth::Int # max width of self and children shown descriptions
8586
body::Expr
@@ -343,7 +344,8 @@ function resolve!(mod::Module, ts::TestsetExpr, pat::Pattern;
343344
for str in parentstrs
344345
!strict && ts.run && break
345346
new = str * "/" * desc
346-
hasmissing && new === missing ||
347+
# string[end] == new can happen with loops when has(pat, Iter) and desc isa String
348+
hasmissing && new === missing || !isempty(strings) && strings[end] === new ||
347349
push!(strings, new)
348350
hasmissing |= new === missing # comes either from desc or str
349351
ts.run = ts.run || decide(new)
@@ -352,7 +354,7 @@ function resolve!(mod::Module, ts::TestsetExpr, pat::Pattern;
352354
end
353355

354356
loops = ts.loops
355-
if loops === nothing || desc isa String
357+
if loops === nothing || desc isa String && !has(pat, Iter)
356358
# TODO: maybe, for testset-for and !(desc isa String), still try this branch
357359
# in case the the interpolation can be resolved thanks to a global binding
358360
# (i.e. the description doesn't depend on loop variables)
@@ -368,9 +370,11 @@ function resolve!(mod::Module, ts::TestsetExpr, pat::Pattern;
368370
if shown
369371
ts.descwidth = descwidth(desc)
370372
end
373+
ts.iter = 1
371374
decide_testset!(desc, false)
372375

373-
else # we have a testset-for with description which needs interpolation
376+
else # we have a testset-for with description which needs interpolation, or
377+
# the iterator must be computed to get an iterator counter
374378
xs = ()
375379
loopiters = Expr(:tuple, (arg.args[1] for arg in loops)...)
376380

@@ -398,17 +402,20 @@ function resolve!(mod::Module, ts::TestsetExpr, pat::Pattern;
398402
catch
399403
@assert xs == ()
400404
ts.descwidth = shown ? descwidth(missing) : 0
405+
ts.iter = typemax(Int)
401406
ts.run = ts.run || decide(missing)
402407
push!(strings, missing)
403408
end
404409
hasmissing = false
405-
for x in xs # empty loop if eval above threw
410+
for (iter, x) in enumerate(xs) # empty loop if eval above threw
406411
descx = eval_desc(mod, ts, x)
412+
ts.iter = iter
407413
if shown
408414
ts.descwidth = max(ts.descwidth, descwidth(descx))
409415
end
410-
if !strict && ts.run
411-
if !shown # no need to compute subsequent descx to update ts.descwidth
416+
if ts.run && (!strict || ts.desc isa String)
417+
if ts.desc isa String || !shown # no need to compute subsequent descx to update ts.descwidth
418+
iter = length(xs)
412419
break
413420
else
414421
continue
@@ -420,6 +427,7 @@ function resolve!(mod::Module, ts::TestsetExpr, pat::Pattern;
420427

421428
run = ts.run
422429
ts.hasbrokenrec = ts.hasbroken
430+
ts.iter = 1:ts.iter # for children, when reachable is used, set the possible range
423431

424432
for tsc in ts.children
425433
runc, id = resolve!(mod, tsc, pat, force = !strict && ts.run,
@@ -465,7 +473,7 @@ function make_ts(ts::TestsetExpr, pat::Pattern, stats, chan)
465473
body = make_ts(ts.body, pat, stats, chan)
466474
end
467475

468-
if ts.loops === nothing
476+
if !isfor(ts)
469477
quote
470478
@testset $(ts.mod) $(isfinal(ts)) $pat $(ts.id) $(ts.desc) $(ts.options) #=
471479
=# $(ts.marks) $stats $chan $body
@@ -1448,7 +1456,7 @@ function dryrun(mod::Module, ts::TestsetExpr, pat::Pattern, align::Int=0, parent
14481456
@assert ts.run
14491457
desc = ts.desc
14501458

1451-
if ts.loops === nothing
1459+
if !isfor(ts)
14521460
if evaldesc && !(desc isa String)
14531461
try
14541462
desc = Core.eval(mod, desc)
@@ -1532,8 +1540,8 @@ function dryrun(mod::Module, ts::TestsetExpr, pat::Pattern, align::Int=0, parent
15321540
end
15331541
false, false, false
15341542
end
1535-
else
1536-
function dryrun_beginend(descx, repeated=nothing)
1543+
else # isfor(ts)
1544+
function dryrun_beginend(descx; iter, repeated=nothing)
15371545
# avoid repeating ourselves, transform this iteration into a "begin/end" testset
15381546
if descx isa Expr
15391547
@assert descx.head == :string
@@ -1553,6 +1561,8 @@ function dryrun(mod::Module, ts::TestsetExpr, pat::Pattern, align::Int=0, parent
15531561
ts.parent, ts.children)
15541562
beginend.run = true
15551563
beginend.id = ts.id
1564+
beginend.iter = iter
1565+
ts.iter = iter # necessary when reachable is used
15561566
beginend.marks = ts.marks
15571567
dryrun(mod, beginend, pat, align, parentsubj; evaldesc=false,
15581568
repeated=repeated, maxidw=maxidw, marks=marks, clear=clear, show=show)
@@ -1564,7 +1574,11 @@ function dryrun(mod::Module, ts::TestsetExpr, pat::Pattern, align::Int=0, parent
15641574
# identitical lines (caveat: if subjects of children would change randomly)
15651575
# but still try simply to evaluate the length of the iterator
15661576
repeated = -1
1567-
if ts.desc isa String
1577+
if ts.desc isa String && !has(pat, Iter)
1578+
# when has(pat, Iter), we probably weren't able to eval the forloop-iterator
1579+
# in resolve!, so no need to re-try here; plus, we wouldn't be able in this
1580+
# branch to tell how many iterations are matching, without going thru the
1581+
# dryrun_beginend function in an enumerate'd loop
15681582
local iterlen
15691583
try
15701584
iterlen = 1
@@ -1575,7 +1589,9 @@ function dryrun(mod::Module, ts::TestsetExpr, pat::Pattern, align::Int=0, parent
15751589
catch
15761590
end
15771591
end
1578-
dryrun_beginend(ts.desc, repeated)
1592+
# if iterlen was computed, then !has(pat, Iter) so the value of the iter
1593+
# keyword below doesn't matter
1594+
dryrun_beginend(ts.desc, repeated=repeated, iter=1:typemax(Int))
15791595
else
15801596
passes, fails, unrun = false, false, false
15811597
for (i, x) in enumerate(loopvalues)
@@ -1587,9 +1603,10 @@ function dryrun(mod::Module, ts::TestsetExpr, pat::Pattern, align::Int=0, parent
15871603
# so we add the "repeated" annotation
15881604
# (it's certainly not worth it to bother being more precise about
15891605
# exactly which iterations are uninterpolated)
1590-
lp, lf, lu = dryrun_beginend(ts.desc, length(loopvalues)-i+1)
1606+
lp, lf, lu = dryrun_beginend(ts.desc, repeated=length(loopvalues)-i+1,
1607+
iter=i:length(loopvalues))
15911608
else
1592-
lp, lf, lu = dryrun_beginend(descx)
1609+
lp, lf, lu = dryrun_beginend(descx, iter=i)
15931610
end
15941611
passes |= lp
15951612
fails |= lf

src/patterns.jl

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,13 @@ struct Fail <: Pattern end
6565
const fail = Fail()
6666

6767

68+
### iter
69+
70+
struct Iter <: Pattern
71+
i::Int
72+
end
73+
74+
6875
## alwaysmatches
6976

7077
alwaysmatches(pat::And, d) = all(p -> alwaysmatches(p, d), pat.xs)
@@ -86,6 +93,7 @@ alwaysmatches(pat::Reachable, d) = alwaysmatches(pat.x, d)
8693
alwaysmatches(dep::Depth, d) = dep.d == d
8794
alwaysmatches(::Pass, _) = false
8895
alwaysmatches(::Fail, _) = false
96+
alwaysmatches(::Iter, _) = false
8997

9098

9199
## matches
@@ -136,6 +144,13 @@ matches(::Fail, subj::AbstractString, ts) = !something(pastresult(ts.marks, subj
136144
matches(::Union{Pass,Fail}, ::Missing, ts) =
137145
isempty(ts.marks) ? false : missing
138146

147+
matches(i::Iter, subj, ts) =
148+
if ts.iter isa Int
149+
i.i == ts.iter
150+
else
151+
i.i ts.iter
152+
end
153+
139154

140155
## has
141156

@@ -505,3 +520,15 @@ The pattern `[pass, fail]` therefore matches any testset
505520
which already ran.
506521
"""
507522
pass, fail
523+
524+
"""
525+
iter(i::Integer)
526+
527+
Filtering pattern which matches only the `i`-th iteration of a testset-for.
528+
A non-for testset is considered to have a unique iteration.
529+
530+
!!! warning
531+
532+
This is very experimental, not tested, and likely to be removed in a future version.
533+
"""
534+
iter(i::Integer) = Iter(Int(i))

src/testset.jl

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ mutable struct ReTestSet <: AbstractTestSet
6666
id::Int64
6767
overall::Bool # TODO: could be conveyed by having self.mod == ""
6868
marks::Dict{String, Union{Symbol, Vector{Symbol}}} # used in matches()
69+
iter::Int # used in matches()
6970
results::Vector
7071
n_passed::Int
7172
anynonpass::Bool
@@ -76,10 +77,11 @@ end
7677

7778
function ReTestSet(mod, desc::String, id::Integer=0;
7879
overall=false, verbose=true, parent=nothing,
79-
marks=Dict{String, Union{Symbol, Vector{Symbol}}}())
80+
marks=Dict{String, Union{Symbol, Vector{Symbol}}}(),
81+
iter=1)
8082
parentsubj = parent === nothing ? "" : parent.subject
8183
subject = string(parentsubj, '/', desc)
82-
ReTestSet(mod, parent, desc, subject, id, overall, marks, [],
84+
ReTestSet(mod, parent, desc, subject, id, overall, marks, iter, [],
8385
0, false, verbose, NamedTuple(), nothing)
8486
end
8587

@@ -447,11 +449,11 @@ default_rng() = isdefined(Random, :default_rng) ?
447449
Random.default_rng() :
448450
Random.GLOBAL_RNG
449451

450-
function make_retestset(mod, desc, id, verbose, marks, remove_last=false)
452+
function make_retestset(mod, desc, id, verbose, marks, remove_last=false, iter=1)
451453
_testsets = get(task_local_storage(), :__BASETESTNEXT__, Test.AbstractTestSet[])
452454
@assert !(remove_last && isempty(_testsets))
453455
testsets = @view _testsets[1:end-remove_last]
454-
ReTestSet(mod, desc, id; verbose=verbose, marks=marks,
456+
ReTestSet(mod, desc, id; verbose=verbose, marks=marks, iter=iter,
455457
parent = isempty(testsets) ? nothing : testsets[end])
456458
end
457459

@@ -551,8 +553,9 @@ function testset_forloop(mod::Module, isfinal::Bool, pat::Pattern, id::Int64,
551553

552554
desc = esc(desc)
553555
blk = quote
556+
iter += 1
554557
local ts0 = make_retestset($mod, $desc, $id, $(options.transient_verbose),
555-
$marks, !first_iteration)
558+
$marks, !first_iteration, iter)
556559

557560
if !$isfinal || matches($pat, ts0.subject, ts0)
558561
# Trick to handle `break` and `continue` in the test code before
@@ -586,6 +589,7 @@ function testset_forloop(mod::Module, isfinal::Bool, pat::Pattern, id::Int64,
586589
quote
587590
local arr = Vector{Any}()
588591
local first_iteration = true
592+
local iter = 0
589593
local ts
590594
local RNG = default_rng()
591595
local oldrng = copy(RNG)

test/test_patterns.jl

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
11
module TestPatterns
22
using Test
33

4-
using ReTest: and, or, not, interpolated, reachable, depth, pass, fail
4+
using ReTest: and, or, not, interpolated, reachable, depth, pass, fail, iter
55
import ReTest
66

77
struct MockTestset
88
id
99
marks
1010
parent
11+
iter
1112

12-
MockTestset() = new(rand(1:typemax(Int)), Dict(), nothing)
13+
MockTestset() = new(rand(1:typemax(Int)), Dict(), nothing, 1)
1314
end
1415

1516
ReTest.tsdepth(::MockTestset) = 1
1617

17-
const basic_patterns = [and(), or(), not(0), interpolated, 0, r"", depth(2), pass, fail]
18+
const basic_patterns = [and(), or(), not(0), interpolated, 0, r"", depth(2), pass, fail, iter(1)]
1819
VERSION >= v"1.3" && push!(basic_patterns, reachable(1))
1920

2021
@testset "patterns: ==" begin
@@ -60,6 +61,7 @@ end
6061
@test ReTest.matches(a, missing, MockTestset()) isa Union{Missing, Bool}
6162
@test ReTest.alwaysmatches(a, 1) isa Bool
6263
@test ReTest.has(a, Integer) isa Bool
64+
@test ReTest.has(a, ReTest.Iter) isa Bool
6365
end
6466
end
6567

0 commit comments

Comments
 (0)