From c3b95275b7763c89b5b9d367e98c42b444af357b Mon Sep 17 00:00:00 2001
From: Rafael Fourquet <fourquet.rafael@gmail.com>
Date: Sun, 1 Aug 2021 09:47:03 +0200
Subject: [PATCH] dryrun: keep track of loop variables from parent testsets

Partially fixes #27.

The full fix might be to make `resolve!` also keep track of these
loop variables; but this would involve storing lists of collected
iterators for each nested testset: once we evaluate an iterator
for filtering in `resolve!`, we have to store it in case
it has some random behavior (e.g. `rand(1:9, i)` where `i` is
a loop variable from a parent testset), in which case re-evaling
it at execution time would lead to inconsistency.
The testset tree would also have to be traversed depth-first by also
unfolding testset-for. This could be a nice improvement if the perfs
don't suffer (instead of storing parent strings subjects and looping
through them, we would store iterators and recurse into them).
---
 src/ReTest.jl    | 60 +++++++++++++++++++++++++++++++++++++-----------
 test/runtests.jl | 27 ++++++++++++++++++++++
 2 files changed, 73 insertions(+), 14 deletions(-)

diff --git a/src/ReTest.jl b/src/ReTest.jl
index 1504eea..986da2a 100644
--- a/src/ReTest.jl
+++ b/src/ReTest.jl
@@ -243,7 +243,13 @@ function resolve!(mod::Module, ts::TestsetExpr, pat::Pattern;
     strings = empty!(ts.strings)
     desc = ts.desc
     ts.loopvalues = nothing # unnecessary ?
-    ts.loopiters = nothing
+    loopiters = ts.loopiters =
+        if ts.loops === nothing
+            nothing
+        else
+            Expr(:tuple, (arg.args[1] for arg in ts.loops)...)
+        end
+
     if 0 != ts.id != id && !warned[] && has(pat, Integer)
         # this can happen when nested testsets are added and Revise is active
         @warn "testset IDs have changed since last run"
@@ -337,7 +343,6 @@ function resolve!(mod::Module, ts::TestsetExpr, pat::Pattern;
     else # we have a testset-for with description which needs interpolation, or
          # the iterator must be computed to get an iterator counter
         xs = ()
-        loopiters = Expr(:tuple, (arg.args[1] for arg in loops)...)
 
         try
             # we need to evaluate roughly the following:
@@ -359,7 +364,6 @@ function resolve!(mod::Module, ts::TestsetExpr, pat::Pattern;
             xs = Core.eval(mod, xsgen)
             @assert xs isa Vector
             ts.loopvalues = xs
-            ts.loopiters = loopiters
         catch
             @assert xs == ()
             ts.descwidth = shown ? descwidth(missing) : 0
@@ -409,15 +413,20 @@ function resolve!(mod::Module, ts::TestsetExpr, pat::Pattern;
     run, id
 end
 
-eval_desc(mod, ts, x) =
+eval_desc(mod, ts, x; stack=false) = # stack => x == iterstack in dryrun
     if ts.desc isa String
         ts.desc
     else
         try
-            Core.eval(mod, quote
-                      let $(ts.loopiters) = $x
-                          $(ts.desc)
-                      end
+            Core.eval(mod,
+                      if stack
+                          Expr(:let, x, ts.desc)
+                      else
+                          quote
+                              let $(ts.loopiters) = $x
+                                  $(ts.desc)
+                              end
+                          end
                       end)::String
         catch
             missing
@@ -442,7 +451,7 @@ function make_ts(ts::TestsetExpr, pat::Pattern, stats, chan)
         end
     else
         c = count(x -> x === nothing, (ts.loopvalues, ts.loopiters))
-        @assert c == 0 || c == 2
+        @assert c == 0 || c == 1
         if c == 0
             loops = [Expr(:(=), ts.loopiters, ts.loopvalues)]
         else
@@ -1448,7 +1457,7 @@ function dryrun(mod::Module, ts::TestsetExpr, pat::Pattern, align::Int=0,
                 # external calls:
                 ; maxidw::Int, marks::Bool, tag::Vector, clear::Bool,
                 # only recursive calls:
-                evaldesc=true, repeated=nothing, show::Bool=true)
+                evaldesc=true, repeated=nothing, show::Bool=true, iterstack=Expr(:block))
     @assert ts.run
     desc = ts.desc
 
@@ -1518,7 +1527,8 @@ function dryrun(mod::Module, ts::TestsetExpr, pat::Pattern, align::Int=0,
             for tsc in ts.children
                 tsc.run || continue
                 dryrun(mod, tsc, pat, align + 2, subject,
-                       maxidw=maxidw, marks=marks, tag=tag, clear=clear, show=true)
+                       maxidw=maxidw, marks=marks, tag=tag, clear=clear, show=true,
+                       iterstack=iterstack)
             end
             false, false, false # meaningless unused triple
         elseif marks
@@ -1536,7 +1546,7 @@ function dryrun(mod::Module, ts::TestsetExpr, pat::Pattern, align::Int=0,
                 tsc.run || continue
                 cp, cf, cu = dryrun(mod, tsc, pat, align + 2, subject,
                                     maxidw=maxidw, marks=marks, tag=tag, clear=clear,
-                                    show=false)
+                                    show=false, iterstack=iterstack)
                 passes |= cp
                 fails |= cf
                 unrun |= cu
@@ -1585,10 +1595,30 @@ function dryrun(mod::Module, ts::TestsetExpr, pat::Pattern, align::Int=0,
             ts.iter = iter # necessary when reachable is used
             dryrun(mod, beginend, pat, align, parentsubj; evaldesc=false,
                    repeated=repeated, maxidw=maxidw, marks=marks, tag=tag,
-                   clear=clear, show=show)
+                   clear=clear, show=show, iterstack=iterstack)
         end
 
         loopvalues = ts.loopvalues
+        if loopvalues === nothing
+            # we check whether we can now evaluate loopvalues via iterstack
+            try
+                # cf. resolve!
+                xssym = gensym()
+                xsgen = quote
+                    let $xssym = []
+                        $(Expr(:for, Expr(:block, ts.loops...),
+                               Expr(:call, Expr(:., :Base, QuoteNode(:push!)),
+                                    xssym, ts.loopiters)))
+                        $xssym
+                    end
+                end
+                loopvalues = Core.eval(mod, Expr(:let, iterstack, xsgen))
+                @assert loopvalues isa Vector
+            catch
+                @assert loopvalues == nothing
+            end
+        end
+
         if loopvalues === nothing
             # ts.desc is probably a String (cf. resolve!); if so, don't print repeated
             # identitical lines (caveat: if subjects of children would change randomly)
@@ -1615,7 +1645,8 @@ function dryrun(mod::Module, ts::TestsetExpr, pat::Pattern, align::Int=0,
         else
             passes, fails, unrun = false, false, false
             for (i, x) in enumerate(loopvalues)
-                descx = eval_desc(mod, ts, x)
+                push!(iterstack.args, Expr(:(=), ts.loopiters, x))
+                descx = eval_desc(mod, ts, iterstack, stack=true)
                 if descx === missing
                     # we would usually have `i == 1`, but not in some rare cases;
                     # once we find an uninterpolated description, we still assume
@@ -1628,6 +1659,7 @@ function dryrun(mod::Module, ts::TestsetExpr, pat::Pattern, align::Int=0,
                 else
                     lp, lf, lu = dryrun_beginend(descx, iter=i)
                 end
+                pop!(iterstack.args)
                 passes |= lp
                 fails |= lf
                 unrun |= lu
diff --git a/test/runtests.jl b/test/runtests.jl
index aeb801c..89a6103 100644
--- a/test/runtests.jl
+++ b/test/runtests.jl
@@ -635,6 +635,33 @@ end # MultiLoops
     check(MultiLoops, "1 1", [(1, 1)])
 end
 
+module LoopsVariablesDryrun
+# check that even with for-iterators which depend on previous loop variables,
+# dryrun mode is able to compute them and corresponding descriptions,
+# and filter accordingly
+
+using ReTest
+
+@testset "a$i" for i=1:2
+    @testset "b$j" for j=1:i
+        @test true
+    end
+end
+end
+
+@chapter Loops begin
+    # no match, so this it at least filtered for final testsets
+    check(LoopsVariablesDryrun, "a1/b3", dry=true, verbose=9, [], output="""
+1| a1
+1| a2
+""")
+    check(LoopsVariablesDryrun, "a2/b1", dry=true, verbose=9, [], output="""
+1| a1
+1| a2
+2|   b1
+""")
+end
+
 # * Anonym ...................................................................
 
 module Anonym