Error occurs only when testing/debugging

As mentioned more briefly in Tests Not Giving Good Error Info, I’m encountering a strange phantom error which only occurs when running tests or stepping thru via the debugger – when I just run the code normally, no error arises. This strikes me as something that should basically never happen, so I’m quite baffled.

I haven’t been able to construct toy examples which replicate the issue, so a decent amount of my code will be copied below to enable reproducing it. (This is almost the same code that I eventually copied in the other thread as well.)

The functions at the very end are the relevant ones. no_tests(50) runs fine, while with_tests(50) has an error… despite the only difference between them being the @test macros.

Although with my actual code I was also seeing the phantom error when stepping through via the debugger, with the code below I’m not able to replicate that for some reason – now I’m seeing the error when using @test, but stepping into no_tests(50) and then telling the debugger to continue appears to hang (whereas the function runs in a reasonable amount of time when not debugging).

As I mentioned in the other thread, I’m in the process of porting this code from an older version of Julia, so if something looks weird that’s my excuse. Any advice about the code itself is appreciated, but mainly I’m worried about this really weird phantom-error behavior.

Thanks for any insights!

using Test
using Random

srand(x) = Random.seed!(x)

using JuMP
using Clp

import Base.==

abstract type Split end

struct LinSplit <: Split
  coefs::Vector{Float64}
  con::Float64
end

function ==(l1::LinSplit,l2::LinSplit)
  if l1.con!=l2.con
    return false
  else
    for i in 1:max(length(l1.coefs), length(l2.coefs))
      if get(l1.coefs, i, 0.0) != get(l2.coefs, i, 0.0)
        return false
      end
    end
    return true
  end
end

LinSplit(coef,con) =
  LinSplit(convert(Vector{Float64}, coef), convert(Float64, con))

abstract type BSPNode end

struct None <: BSPNode
end

struct SplitNode <: BSPNode
  split::Split # split definition
  neg::BSPNode # negative side
  pos::BSPNode # positive side
end

==(s1::SplitNode, s2::SplitNode) =
  s1.split == s2.split && s1.neg == s2.neg && s1.pos == s2.pos

struct LeafNode <: BSPNode
  value::Any
end

==(l1::LeafNode,l2::LeafNode) =
  l1.value == l2.value

abstract type Region end

struct LinBound
  split::LinSplit
  positive::Bool
end

==(l1::LinBound,l2::LinBound) =
  l1.split == l2.split && l1.positive == l2.positive

import Base.isless

function isless(l1::LinBound, l2::LinBound) # defining so they can be sorted
  if l1.positive < l2.positive
    return true
  elseif l2.positive < l1.positive
    return false
  elseif l1.split.con < l2.split.con
    return true
  elseif l2.split.con < l1.split.con
    return false
  else
    for i in 1:length(l1.split.coefs)
      if l1.split.coefs[i] < l2.split.coefs[i]
        return true
      elseif l2.split.coefs[i] < l1.split.coefs[i]
        return false
      end
    end
    return false
  end
end

struct LinRegion <: Region
  bounds::Vector{LinBound}
  dimensionality::Int
end

function regularize_bound(b::LinBound)
  z = sqrt(sum(b.split.coefs .^ 2))
  if z == 0.0
    return b
  else
    return LinBound(LinSplit(b.split.coefs / z, b.split.con / z), b.positive)
  end
end

function ==(l1::LinRegion,l2::LinRegion)
  b1 = map(regularize_bound, trim_region(l1).bounds)
  b2 = map(regularize_bound, trim_region(l2).bounds)
  return sort(b1) == sort(b2)
end

boundless_reg(d::Int) = LinRegion(Vector{LinBound}(),d)

function rect_split(dim,loc,numdims)
  z = zeros(numdims)
  z[dim]=1.0
  return LinSplit(z,loc)
end

function rect_region(mins, maxs)
  max_d = length(mins)
  s = Vector{LinBound}()
  for d in 1:max_d
    z = zeros(max_d)
    z[d] = 1.0
    s = union(s,[LinBound(LinSplit(z,mins[d]), true)])
  end
  for d in 1:max_d
    z = zeros(max_d)
    z[d] = 1.0
    s = union(s,[LinBound(LinSplit(z,maxs[d]), false)])
  end
  return LinRegion(s,max_d)
end

import Base.isequal

isequal(t1::SplitNode, t2::SplitNode) =
  isequal(t1.split, t2.split) &&
  isequal(t1.pos, t2.pos) &&
  isequal(t1.neg, t2.neg)

isequal(t1::LinSplit, t2::LinSplit) =
  t1.coefs == t2.coefs &&
  t1.con == t2.con

struct BSPTree
  boundary::Region
  root::BSPNode
end

==(t1::BSPTree, t2::BSPTree) = (t1.boundary == t2.boundary) && isequal(t1.root, t2.root)
isequal(t1::BSPTree, t2::BSPTree) = t1 == t2

function randomrect(n)
  mins = rand(n)
  maxs = rand(n)+mins
  return rect_region(mins,maxs)
end

function randomrect()
  randomrect(2)
end

function randomsplit(n)
  LinSplit(rand(n),rand())
end

function random_rect_split(n)
  coefs = zeros(n)
  coefs[rand(1:n)] = 1.0
  LinSplit(coefs,rand())
end

# creates a balanced tree of the given depth
function randomtree(dims, depth)
  if depth == 0
    return LeafNode(rand())
  else
    return SplitNode(randomsplit(dims),
                     randomtree(dims,depth-1),
                     randomtree(dims,depth-1))
  end
end

function random_rect_tree(dims, depth)
  if depth == 0
    return LeafNode(rand())
  else
    return SplitNode(random_rect_split(dims),
                     random_rect_tree(dims,depth-1),
                     random_rect_tree(dims,depth-1))
  end
end

function epsilonequal(t1::BSPTree, t2::BSPTree, epsilon::Float64=0.0000001)
  if !(regvalid(intersection(t1.boundary, t2.boundary), epsilon))
    return true # If they have no intersection, they trivially agree about it!
  elseif typeof(t1.root) == SplitNode
    return epsilonequal(takepos(t1), t2, epsilon) && epsilonequal(takeneg(t1), t2, epsilon)
  elseif typeof(t2.root) == SplitNode
    return epsilonequal(takepos(t2), t1, epsilon) && epsilonequal(takeneg(t2), t1, epsilon)
  elseif t1.root == None() && t2.root == None()
    return true
  elseif t1.root == None() || t2.root == None()
    return false
  else
    return epsilonequal(t1.root.value, t2.root.value, epsilon)
  end
end

epsilonequal(a::Float64, b::Float64, epsilon::Float64=0.0000001) = (a == b) || (abs(a - b) < epsilon)

epsilonequal(a::Number, b::Number, epsilon::Float64=0.0000001) = epsilonequal(convert(Float64,a), convert(Float64,b), epsilon)

epsilonequal(a::LinBound, b::LinBound, epsilon::Float64=0.0000001) =
  a.positive == b.positive && epsilonequal(a.split, b.split, epsilon)

epsilonequal(a::LinSplit, b::LinSplit, epsilon::Float64=0.0000001) =
  epsilonequal(a.con, b.con, epsilon) && all(map(((x,y) -> epsilonequal(x,y, epsilon)), a.coefs, b.coefs)) # shouldn't really be assuming the vectors have same length here

function intersection(r1::LinRegion, r2::LinRegion)
  return LinRegion(union(r1.bounds, r2.bounds), max(r1.dimensionality, r2.dimensionality))
end

function rect_valid(r::Region)
  max_d = r.dimensionality
  maxs = fill(Inf, max_d)
  maxs_closed = trues(max_d) # whether the maximum is closed
  mins = fill(-Inf, max_d)
  mins_closed = trues(max_d) # whether the minimum is closed
  for bound in r.bounds # Go through bounds checking number of nonzero coefs; if >1, return 0; if 1, record the new min/max implied.
    bound_coefs = bound.split.coefs
    bound_length = length(bound_coefs)
    if bound_length > max_d
      throw("rect_valid handed a region with more coefficients than dimensions: $r")
    end
    nonzero_count = 0
    nonzero_loc = 0
    for dim in 1:bound_length
      if !(bound_coefs[dim] == 0.0)
        nonzero_count = nonzero_count + 1
        if nonzero_count > 1
          return 0
        end
        nonzero_loc = dim
      end
    end
    if nonzero_loc > 0 #if the bound is doing anything at all
      # Now that we know the bound is uni-dimensional, add it to the list of dimension bounds.
      adjusted_con = bound.split.con/bound_coefs[nonzero_loc] # The actual location for this bound.
      if bound.positive # closed bound
        if bound_coefs[nonzero_loc] > 0.0 # positive coef, so bound.positive=true means the bound is expressing a minimum
          if adjusted_con > mins[nonzero_loc] # lower bound would be increased
            mins[nonzero_loc] = adjusted_con
            mins_closed[nonzero_loc] = true # inherits the closed status, since we increased past whatever the bound previously might have been
          end # if the bound is less than or equal to one already established, we have no need to update the open or closed status in this case, since it could only be loosening the constraint, which we don't want to do
        elseif bound_coefs[nonzero_loc] < 0.0 # negative coef, so bound is actually a maximum, since -x>=c means x<=c
          if adjusted_con < maxs[nonzero_loc] # existing upper bound would be decreased
            maxs[nonzero_loc] = adjusted_con
            maxs_closed[nonzero_loc] = true # inherits the open status, since we decreased past whatever the previous bound would have been
          end # no need to update open/closed status otherwise
        end
      else # open bound
        if bound_coefs[nonzero_loc] > 0.0 # positive coef, so bound is saying x<c, IE giving a max
          if adjusted_con < maxs[nonzero_loc] # new max would be lower than old max
            maxs[nonzero_loc] = adjusted_con # lower the max
            maxs_closed[nonzero_loc] = false # inherit the open status
          elseif adjusted_con == maxs[nonzero_loc] # if they're equal, we don't need to update the max, but we may need to switch it from closed to open since an open constraint is stricter
            maxs_closed[nonzero_loc] = false # let's just assign; no point checking
          end # otherwise, nothing to update; we were already maximally strict
        elseif bound_coefs[nonzero_loc] < 0.0 && adjusted_con >= mins[nonzero_loc] # negative coef, so we're saying -x<c, IE x>c, so we are giving a min
          if adjusted_con > mins[nonzero_loc] # new min would be higher
            mins[nonzero_loc] = adjusted_con
            mins_closed[nonzero_loc] = false # inherit the open status
          elseif adjusted_con == mins[nonzero_loc] # if they're equal, we still need to enforce openness
            mins_closed[nonzero_loc] = false
          end
        end
      end
    else # no nonzero locs in this bound, because we already checked whether there's 1, and returned for the case where there's more than one
      if bound.positive && bound.split.con > 0 # no way to get > 0 with no coefficients; this region is invalid
        return -1
      elseif !bound.positive && bound.split.con <= 0 # if the constant is 0 or below, there's no way we can get strictly under it
        return -1
      end
    end
  end
  # Now that we've added all the bounds, check for validity.
  for dim in 1:max_d
    if maxs[dim] < mins[dim] || (maxs[dim]==mins[dim] && !(maxs_closed[dim] && mins_closed[dim]))
      return -1
    end
  end
  return +1
end

function zero_bound_check(b::LinBound) # using the same idea as the zero-coef bound case in rect_valid, returns +1 if the bound rules out nothing, -1 if everything, otherwise zero
  if all((x->x==0.0), b.split.coefs)
    if b.positive
      if b.split.con > 0
        return -1
      else
        return +1
      end
    else
      if b.split.con <= 0
        return -1
      else
        return +1
      end
    end
  else
    return 0
  end
end


function regvalid(r::LinRegion, epsilon::Float64=0.0000001)
  r_v = rect_valid(r)
  if r_v==1
    return true
  elseif r_v==-1
    return false
  end
  m = makemodel(r)
  for bound in r.bounds
    if !(bound.positive)
      d = -bound.split.coefs
      @objective(m[1], Max, sum(d[i]*(m[2])[i] for i=1:length(d))) # optimize away from each face
      optimize!(m[1])
      s = termination_status(m[1])
      if s == :Infeasible # not even the region's closure is valid
        return false
      elseif s == :Optimal
        optimize_away = getobjectivevalue(m[1])
        difference = optimize_away + bound.split.con # Since things are negative, I want the optimized value minus the **negative of** the constant; so really I want to add them.
        if difference < epsilon
          return false
        end
      end
    end
  end
  return true
end

regvalid(r::BSPTree) =
  regvalid(r.boundary)


takepos(reg::LinRegion, split::LinSplit) = LinRegion(union(reg.bounds, [LinBound(split, true)]), reg.dimensionality)
takeneg(reg::LinRegion, split::LinSplit) = LinRegion(union(reg.bounds, [LinBound(split, false)]), reg.dimensionality)

takepos(tree::BSPTree) =
  BSPTree(takepos(tree.boundary, tree.root.split), tree.root.pos)

takeneg(tree::BSPTree) =
  BSPTree(takeneg(tree.boundary, tree.root.split), tree.root.neg)


function makemodel(r::LinRegion)
  m = Model(optimizer_with_attributes(Clp.Optimizer, "InfeasibleReturn" => 1, "LogLevel" => 0))
  d = r.dimensionality
  @variable(m,x[1:d])
  for bound in r.bounds
    if bound.positive
      @constraint(m, sum(x[i]*bound.split.coefs[i] for i=1:length(bound.split.coefs)) >= bound.split.con)
    else
      @constraint(m, sum(x[i]*bound.split.coefs[i] for i=1:length(bound.split.coefs)) <= bound.split.con)
    end
  end
  return (m, x)
end

function cheap_treearithmetic(operator, base_type)
  eval(
    quote

      function $operator(n1::BSPNode, n2::$base_type) # Only work on raw nodes if we're combining with a number; otherwise we want to track regions to avoid computing things which would be trimmed, since that can make an exponential difference.
        if typeof(n1) == LeafNode
          return LeafNode( $operator(n1.value, n2))
        elseif n1 == None()
          # return None()
          return LeafNode(n2) # None() combined with anything else acts like an identity element.
        elseif typeof(n1) == SplitNode
          return SplitNode(n1.split, $operator(n1.neg, n2),
                                     $operator(n1.pos, n2))
        end
      end

      function $operator(n1::$base_type, n2::BSPNode)
        if typeof(n2) == LeafNode
          return LeafNode( $operator(n1, n2.value))
        elseif n2 == None()
          #return None()
          return LeafNode(n1) # None() combined with anything else acts like an identity element.
        elseif typeof(n2) == SplitNode
          return SplitNode(n2.split, $operator(n1, n2.neg),
                                     $operator(n1, n2.pos))
        end
      end

      function $operator(t1::BSPTree, t2::BSPTree)
        i = intersection(t1.boundary, t2.boundary)
        if typeof(t1.root) == LeafNode
          if !regvalid(i)
            return BSPTree(i, None())
          end
          return merge_bottom_up(BSPTree(i, ($operator(t1.root.value, cheap_trim(t2, i).root))))
        elseif typeof(t1.root) == SplitNode
          pos = takepos(t1)
          neg = takeneg(t1)
          return BSPTree(i,
                         SplitNode(t1.root.split,
                                   identity($operator(neg, t2)).root, # I have to use "identity" here to prevent the order of operations from being changed when $operator is expanded.
                                   identity($operator(pos, t2)).root)) #
        elseif  t1.root == None()
          return t2
        elseif t2.root == None()
          return t1
        end
      end

      $operator(t1::BSPTree, t2::$base_type) = BSPTree(t1.boundary, $operator(t1.root, t2))
      $operator(t1::$base_type, t2::BSPTree) = BSPTree(t2.boundary, $operator(t1, t2.root))
    end
    )
end

cheap_treearithmetic(op) = cheap_treearithmetic(op, Number)


import Base.+
cheap_treearithmetic(:+)
import Base.*
cheap_treearithmetic(:*)
import Base.max
cheap_treearithmetic(:max)
import Base.min
cheap_treearithmetic(:min)
import Base./
cheap_treearithmetic(:/)
import Base.-
cheap_treearithmetic(:-)
import Base.^
cheap_treearithmetic(:^,Integer) # adding the integer definitions to eliminate some compiler warnings about ambiguous matches
cheap_treearithmetic(:^)


function with_tests(n)
  for i=1:n
    srand(i)
    dims = rand(1:5)
    a = BSPTree(randomrect(dims), randomtree(dims, rand(1:4)))
    @test epsilonequal(a,a)
    @test epsilonequal(a, a + 0.000001, 0.01)
    @test !epsilonequal(a, a + 0.1, 0.01)
  end
end

function no_tests(n)
  for i=1:n
    srand(i)
    dims = rand(1:5)
    a = BSPTree(randomrect(dims), randomtree(dims, rand(1:4)))
    epsilonequal(a,a)
    epsilonequal(a, a + 0.000001, 0.01)
    !epsilonequal(a, a + 0.1, 0.01)
  end
end

no_tests(50)

with_tests(50)

After trying out the behavior of @test in Julia 1.5 more, I’m thinking that this “phantom error” behavior isn’t a result of the tested code throwing an error at all. Rather, when it tells me “there was an error during testing”, without telling me anything about the error, what it means is just that the test failed.

My confusion was due to the behavior of Julia 0.4.6 / the corresponding Juno, wherein I would get an explicit message in-line telling me that a test failed. But, EG, @test 1==2 now displays the “phantom error” behavior.

As for the error I was seeing when debugging, that must have been something that happened in my original code after the point where the test failed.

Hmm, that doesn’t seem to be a complete explanation of what I’m seeing – when I enter into my code with the @test statements, I see a different error before I see a failed test. But surprisingly that’s not the case for with_tests from the code copied here. Not sure what the difference is, since this is just me copying the relevant functions from my own code.