---
hide:
  - toc
search:
  exclude: true
---
```````pony linenums="1"
use "debug"
use "collections"

class val _Shrink is Equatable[_Round]
  """
  An execution of a property during the shrinking process.
  """
  let _round: USize

  new val create(round': USize) =>
    _round = round'

  fun round(): USize => _round

  fun inc(): _Round =>
    _Shrink.create(this._round + 1)

  fun eq(other: box->_Round): Bool =>
    match other
    | let s: _Shrink => s._round == this._round
    else
      false
    end

  fun string(): String iso^ =>
    ("shrink(" + this._round.string() + ")").string()

class val _Run is Equatable[_Round]
  """
  An execution of a property during test mode. i.e. normal execution
  to find a sample for which the property does not hold.
  """
  let _round: USize

  new val create(round': USize) =>
    _round = round'

  fun round(): USize => _round

  fun inc(): _Round =>
    _Run.create(this._round + 1)

  fun eq(other: box->_Round): Bool =>
    match other
    | let s: _Run => s._round == this._round
    else
      false
    end

  fun string(): String iso^ =>
    ("run(" + this._round.string() + ")").string()

type _Round is (_Shrink | _Run)
  """Represents a single execution of a property."""


interface val PropertyLogger
  fun log(msg: String, verbose: Bool = false)

interface val PropertyResultNotify
  fun fail(msg: String)
    """
    Called when a Property has failed (did not hold for a sample)
    or when execution raised an error.

    Does not necessarily denote completeness of the property execution,
    see `complete(success: Bool)` for that purpose.
    """

  fun complete(success: Bool)
    """
    Called when the Property execution is complete
    signalling whether it was successful or not.
    """

actor PropertyRunner[T]
  """
  Actor executing a Property1 implementation
  in a way that allows garbage collection between single
  property executions, because it uses recursive behaviours
  for looping.
  """
  let _prop1: Property1[T]
  let _params: PropertyParams
  let _rnd: Randomness
  let _notify: PropertyResultNotify
  let _gen: Generator[T]
  let _logger: PropertyLogger
  let _env: Env

  // state changed during runtime
  var _current_round: _Round = _Run.create(0)
    """
    The number and kind of the round currently executed.
    Keep track of which runs/shrinks we expect to be notified about.
    """
  let _expected_actions: Set[String] = Set[String]
    """
    List of expected actions for this round.
    Will be cleared after each round.
    """
  let _disposables: Array[DisposableActor] = Array[DisposableActor]
    """
    Disosable actors that are disposed of after every round.
    """
  var _shrinker: Iterator[T^] = _EmptyIterator[T^]
  var _sample_repr: String = ""
  var _pass: Bool = true

  new create(
    p1: Property1[T] iso,
    params: PropertyParams,
    notify: PropertyResultNotify,
    logger: PropertyLogger,
    env: Env
  ) =>
    _env = env
    _prop1 = consume p1
    _params = params
    _logger = logger
    _notify = notify
    _rnd = Randomness(_params.seed)
    _gen = _prop1.gen()


// RUNNING PROPERTIES //

  be complete_run(round: _Round, success: Bool) =>
    """
    Complete a property run.

    This behaviour is called from the PropertyHelper
    or from the actor itself.
    """

    // verify that this is an expected call
    if this._current_round != round then
      _logger.log("unexpected " + if success then "finish" else "fail" end + " msg for " + round.string() +
        ". expecting " + this._current_round.string(), true)
      return
    end

    _pass = success // in case of sync property - signal failure

    if not success then
      // found a bad example, try to shrink it
      if not _shrinker.has_next() then
        _logger.log("no shrinks available")
        fail(_sample_repr, 0)
      else
        // prepare next round
        _prepare_next_round()
        // set rounds to shrinking
        this._current_round = _Shrink.create(0) // reset rounds for shrinking
        // start shrinking process
        do_shrink(_sample_repr)
      end
    else
      // property holds, recurse
      _prepare_next_round()
      run()
    end

  fun ref _prepare_next_round() =>
    this._current_round = this._current_round.inc()
    this._expected_actions.clear()
    for disposable in Poperator[DisposableActor](this._disposables) do
      disposable.dispose()
    end

  fun ref _generate_with_retry(max_retries: USize): ValueAndShrink[T] ? =>
    var tries: USize = 0
    repeat
      try
        return _gen.generate_and_shrink(_rnd)?
      else
        tries = tries + 1
      end
    until (tries > max_retries) end

    error

  be run() =>
    if this._current_round.round() >= _params.num_samples then
      complete() // all samples have been successful
      return
    end

    // prepare property run
    (var sample, _shrinker) =
      try
        _generate_with_retry(_params.max_generator_retries)?
      else
        // break out if we were not able to generate a sample
        _notify.fail(
          "Unable to generate samples from the given iterator, tried " +
          _params.max_generator_retries.string() + " times." +
          " (round: " + this._current_round.string() + ")")
        _notify.complete(false)
        return
      end


    // create a string representation before consuming ``sample`` with property
    (sample, _sample_repr) = _Stringify.apply[T](consume sample)
    let run_notify = recover val this~complete_run() end
    let helper = PropertyHelper(_env, this, run_notify, this._current_round, _params.string())
    _pass = true // will be set to false by fail calls

    try
      _prop1.property(consume sample, helper)?
    else
      fail(_sample_repr, 0 where err=true)
      return
    end
    // dispatch to another behavior
    // as complete_run might have set _pass already through a call to
    // complete_run
    _run_finished(this._current_round)

  be _run_finished(round: _Round) =>
    if not _params.async and _pass then
      // otherwise complete_run has already been called
      complete_run(round, true)
    end

// SHRINKING //

  be complete_shrink(failed_repr: String, last_repr: String, shrink_round: _Round, success: Bool) =>

    // verify that this is an expected call
    if this._current_round != shrink_round then
      _logger.log("unexpected " + if success then "complete" else "fail" end + " msg for " + shrink_round.string() +
        ". Currently at " + _current_round.string(), true)
      return
    end

    _pass = success // in case of sync property - signal failure

    if success then
      // we have a sample that did not fail and thus can stop shrinking
      fail(failed_repr, shrink_round.round())

    else
      // we have a failing shrink sample, recurse
      _prepare_next_round()
      do_shrink(last_repr)
    end

  be do_shrink(failed_repr: String) =>

    // shrink iters can be infinite, so we need to limit
    // the examples we consider during shrinking
    let round_num = this._current_round.round()
    if round_num == _params.max_shrink_rounds then
      fail(failed_repr, round_num)
      return
    end

    (let shrink, let current_repr) =
      try
        _Stringify.apply[T](_shrinker.next()?)
      else
        // no more shrink samples, report previous failed example
        fail(failed_repr, round_num)
        return
      end
    // callback for asynchronous shrinking or aborting on error case
    let run_notify =
      recover val
        this~complete_shrink(failed_repr, current_repr)
      end
    let helper = PropertyHelper(
      _env,
      this,
      run_notify,
      this._current_round,
      _params.string()
    )
    _pass = true // will be set to false by fail calls

    try
      _prop1.property(consume shrink, helper)?
    else
      fail(current_repr, round_num where err=true)
      return
    end
    // dispatch to another behaviour
    // to ensure _complete_shrink has been called already
    _shrink_finished(failed_repr, current_repr, this._current_round)

  be _shrink_finished(
    failed_repr: String,
    current_repr: String,
    shrink_round: _Round)
  =>
    if not _params.async and _pass then
      // directly complete the shrink run
      complete_shrink(failed_repr, current_repr, shrink_round, true)
    end

// interface towards PropertyHelper

  be expect_action(name: String, round: _Round) =>
    if round != this._current_round then
      _logger.log("unexpected expect action \"" + name + "\" call for " + round.string() +
        ". Currently at " + this._current_round.string(), true)
      return
    end
    _logger.log("Action expected: " + name)
    _expected_actions.set(name)

  be complete_action(name: String, round: _Round, ph: PropertyHelper) =>
    if round != this._current_round then
      _logger.log("unexpected complete action \"" + name + "\" msg for " + round.string() +
        ". Currently at " + this._current_round.string(), true)
      return
    end
    _logger.log("Action completed: " + name)
    _finish_action(name, true, round, ph)

  be fail_action(name: String, round: _Round, ph: PropertyHelper) =>
    if round != this._current_round then
      _logger.log("unexpected fail action \"" + name + "\" msg for " + round.string() +
        ". Currently at " + this._current_round.string(), true)
      return
    end
    _logger.log("Action failed: " + name)
    _finish_action(name, false, round, ph)

  fun ref _finish_action(name: String, success: Bool, round: _Round, ph: PropertyHelper) =>
    try
      _expected_actions.extract(name)?

      // call back into the helper to invoke the current run_notify
      // that we don't have access to otherwise
      if not success then
        ph.complete(false)
      elseif _expected_actions.size() == 0 then
        ph.complete(true)
      end
    else
      _logger.log("Action '" + name + "' finished unexpectedly at " + round.string() + ". ignoring.")
    end

  be dispose_when_done(disposable: DisposableActor, round: _Round) =>
    """
    Let us not have older rounds interfere with newer ones, 
    thus dispose directly.
    """
    if round != this._current_round then
      _logger.log("Unexpected dispose_when_done for " + round.string() + 
        ". Currently at " + this._current_round.string(), true)
      _logger.log("Disposing right now...", true)
      disposable.dispose()
      return
    end
    _disposables.push(disposable)

  be dispose() =>
    _dispose()

  fun ref _dispose() =>
    for disposable in Poperator[DisposableActor](_disposables) do
      disposable.dispose()
    end

  be log(msg: String, verbose: Bool = false) =>
    _logger.log(msg, verbose)

  // end interface towards PropertyHelper

  fun ref complete() =>
    """
    Complete the Property execution successfully.
    """
    _notify.complete(true)

  fun ref fail(repr: String, rounds: USize = 0, err: Bool = false) =>
    """
    Complete the Property execution
    while signalling failure to the `PropertyResultNotify`.
    """
    if err then
      _report_error(repr, rounds)
    else
      _report_failed(repr, rounds)
    end
    _notify.complete(false)

  fun _report_error(sample_repr: String,
    shrink_rounds: USize = 0,
    loc: SourceLoc = __loc) =>
    """
    Report an error that happened during property evaluation
    and signal failure to the `PropertyResultNotify`.
    """
    _notify.fail(
      "Property errored for sample "
        + sample_repr
        + " (after "
        + shrink_rounds.string()
        + " shrinks)"
    )

  fun _report_failed(sample_repr: String,
    shrink_rounds: USize = 0,
    loc: SourceLoc = __loc) =>
    """
    Report a failed property and signal failure to the `PropertyResultNotify`.
    """
    _notify.fail(
      "Property failed for sample "
        + sample_repr
        + " (after "
        + shrink_rounds.string()
        + " shrinks)"
    )


class _EmptyIterator[T]
  fun ref has_next(): Bool => false
  fun ref next(): T^ ? => error

primitive _Stringify
  fun apply[T](t: T): (T^, String) =>
    """turn anything into a string"""
    let digest = (digestof t)
    let s =
      match t
      | let str: Stringable =>
        str.string()
      | let rs: ReadSeq[Stringable] =>
        "[" + " ".join(rs.values()) + "]"
      | (let s1: Stringable, let s2: Stringable) =>
        "(" + s1.string() + ", " + s2.string() + ")"
      | (let s1: Stringable, let s2: ReadSeq[Stringable]) =>
        "(" + s1.string() + ", [" + " ".join(s2.values()) + "])"
      | (let s1: ReadSeq[Stringable], let s2: Stringable) =>
        "([" + " ".join(s1.values()) + "], " + s2.string() + ")"
      | (let s1: ReadSeq[Stringable], let s2: ReadSeq[Stringable]) =>
        "([" + " ".join(s1.values()) + "], [" + " ".join(s2.values()) + "])"
      | (let s1: Stringable, let s2: Stringable, let s3: Stringable) =>
        "(" + s1.string() + ", " + s2.string() + ", " + s3.string() + ")"
      | ((let s1: Stringable, let s2: Stringable), let s3: Stringable) =>
        "((" + s1.string() + ", " + s2.string() + "), " + s3.string() + ")"
      | (let s1: Stringable, (let s2: Stringable, let s3: Stringable)) =>
        "(" + s1.string() + ", (" + s2.string() + ", " + s3.string() + "))"
      else
        "<identity:" + digest.string() + ">"
      end
    (consume t, consume s)


```````