property_helper.pony


interface val _PropertyRunNotify
  """
  simple callback for notifying the runner
  that a run completed
  """
  fun apply(success: Bool)

interface tag _IPropertyRunner
  """
  interface for a PropertyRunner without the generic type parameter

  and only with the behaviours we are interested in.
  """

  be expect_action(name: String)

  be complete_action(name: String, ph: PropertyHelper)

  be fail_action(name: String, ph: PropertyHelper)

  be dispose_when_done(disposable: DisposableActor)

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


class val PropertyHelper
  """
  Helper for ponycheck properties.

  Mirrors the [TestHelper](ponytest-TestHelper.md) API as close as possible.

  Contains assertion functions and functions for completing asynchronous
  properties, for expecting and completing or failing actions.

  Internally a new PropertyHelper will be created for each call to
  a property with a new sample and also for every shrink run.
  So don't assume anything about the identity of the PropertyHelper inside of
  your Properties.

  This class is `val` by default so it can be safely passed around to other
  actors.

  It exposes the process [Env](builtin-Env.md) as public `env` field in order to
  give access to the root authority and other stuff.
  """
  let _runner: _IPropertyRunner
  let _run_notify: _PropertyRunNotify
  let _run_context: String

  let env: Env

  new val create(env': Env,
                 runner: _IPropertyRunner,
                 run_notify: _PropertyRunNotify,
                 run_context: String) =>
    env = env'
    _runner = runner
    _run_notify = run_notify
    _run_context = run_context

/****** START DUPLICATION FROM TESTHELPER ********/

  fun log(msg: String, verbose: Bool = false) =>
    """
    Log the given message.

    The verbose parameter allows messages to be printed only when the --verbose
    command line option is used. For example, by default assert failures are
    logged, but passes are not. With --verbose both passes and fails are
    reported.

    Logs are printed one test at a time to avoid interleaving log lines from
    concurrent tests.
    """
    _runner.log(msg, verbose)

  fun fail(msg: String = "Test failed") =>
    """
    Flag the test as having failed.
    """
    _fail(msg)

  fun assert_false(
    predicate: Bool,
    msg: String val = "",
    loc: SourceLoc val = __loc)
    : Bool val
  =>
    """
    Assert that the given expression is false.
    """
    if predicate then
      _fail(_fmt_msg(loc, "Assert false failed. " + msg))
      return false
    end
    _runner.log(_fmt_msg(loc, "Assert false passed. " + msg))
    true

  fun assert_true(
    predicate: Bool,
    msg: String val = "",
    loc: SourceLoc val = __loc)
    : Bool val
  =>
    """
    Assert that the given expression is true.
    """
    if not predicate then
      _fail(_fmt_msg(loc, "Assert true failed. " + msg))
      return false
    end
    _runner.log(_fmt_msg(loc, "Assert true passed. " + msg))
    true

  fun assert_error(
    test: {(): None ?} box,
    msg: String = "",
    loc: SourceLoc = __loc)
    : Bool
  =>
    """
    Assert that the given test function throws an error when run.
    """
    try
      test()?
      _fail(_fmt_msg(loc, "Assert error failed. " + msg))
      false
    else
      _runner.log(_fmt_msg(loc, "Assert error passed. " + msg), true)
      true
    end

  fun assert_no_error(
    test: {(): None ?} box,
    msg: String = "",
    loc: SourceLoc = __loc)
    : Bool
  =>
    """
    Assert that the given test function does not throw an error when run.
    """
    try
      test()?
      _runner.log(_fmt_msg(loc, "Assert no error passed. " + msg), true)
      true
    else
      _fail(_fmt_msg(loc, "Assert no error failed. " + msg))
      false
    end

  fun assert_is[A](
    expect: A,
    actual: A,
    msg: String = "",
    loc: SourceLoc = __loc)
    : Bool
  =>
    """
    Assert that the 2 given expressions resolve to the same instance
    """
    if expect isnt actual then
      _fail(_fmt_msg(loc, "Assert is failed. " + msg
        + " Expected (" + (digestof expect).string() + ") is ("
        + (digestof actual).string() + ")"))
      return false
    end

    _runner.log(
      _fmt_msg(loc, "Assert is passed. " + msg
        + " Got (" + (digestof expect).string() + ") is ("
        + (digestof actual).string() + ")"),
      true)
    true

  fun assert_isnt[A](
    not_expect: A,
    actual: A,
    msg: String = "",
    loc: SourceLoc = __loc)
    : Bool
  =>
    """
    Assert that the 2 given expressions resolve to different instances.
    """
    if not_expect is actual then
      _fail(_fmt_msg(loc, "Assert isn't failed. " + msg
        + " Expected (" + (digestof not_expect).string() + ") isnt ("
        + (digestof actual).string() + ")"))
      return false
    end

    _runner.log(
      _fmt_msg(loc, "Assert isn't passed. " + msg
        + " Got (" + (digestof not_expect).string() + ") isnt ("
        + (digestof actual).string() + ")"),
      true)
    true

  fun assert_eq[A: (Equatable[A] #read & Stringable #read)](
    expect: A,
    actual: A,
    msg: String = "",
    loc: SourceLoc = __loc)
    : Bool
  =>
    """
    Assert that the 2 given expressions are equal.
    """
    if expect != actual then
      _fail(_fmt_msg(loc,  "Assert eq failed. " + msg
        + " Expected (" + expect.string() + ") == (" + actual.string() + ")"))
      return false
    end

    _runner.log(_fmt_msg(loc, "Assert eq passed. " + msg
      + " Got (" + expect.string() + ") == (" + actual.string() + ")"),
      true)
    true

  fun assert_ne[A: (Equatable[A] #read & Stringable #read)](
    not_expect: A,
    actual: A,
    msg: String = "",
    loc: SourceLoc = __loc)
    : Bool
  =>
    """
    Assert that the 2 given expressions are not equal.
    """
    if not_expect == actual then
      _fail(_fmt_msg(loc, "Assert ne failed. " + msg
        + " Expected (" + not_expect.string() + ") != (" + actual.string()
        + ")"))
      return false
    end

    _runner.log(
      _fmt_msg(loc, "Assert ne passed. " + msg
        + " Got (" + not_expect.string() + ") != (" + actual.string() + ")"),
      true)
    true

  fun assert_array_eq[A: (Equatable[A] #read & Stringable #read)](
    expect: ReadSeq[A],
    actual: ReadSeq[A],
    msg: String = "",
    loc: SourceLoc = __loc)
    : Bool
  =>
    """
    Assert that the contents of the 2 given ReadSeqs are equal.
    """
    var ok = true

    if expect.size() != actual.size() then
      ok = false
    else
      try
        var i: USize = 0
        while i < expect.size() do
          if expect(i)? != actual(i)? then
            ok = false
            break
          end

          i = i + 1
        end
      else
        ok = false
      end
    end

    if not ok then
      _fail(_fmt_msg(loc, "Assert EQ failed. " + msg + " Expected ("
        + _print_array[A](expect) + ") == (" + _print_array[A](actual) + ")"))
      return false
    end

    _runner.log(
      _fmt_msg(loc, "Assert EQ passed. " + msg + " Got ("
        + _print_array[A](expect) + ") == (" + _print_array[A](actual) + ")"),
      true)
    true

  fun assert_array_eq_unordered[A: (Equatable[A] #read & Stringable #read)](
    expect: ReadSeq[A],
    actual: ReadSeq[A],
    msg: String = "",
    loc: SourceLoc = __loc)
    : Bool
  =>
    """
    Assert that the contents of the 2 given ReadSeqs are equal ignoring order.
    """
    try
      let missing = Array[box->A]
      let consumed = Array[Bool].init(false, actual.size())
      for e in expect.values() do
        var found = false
        var i: USize = -1
        for a in actual.values() do
          i = i + 1
          if consumed(i)? then continue end
          if e == a then
            consumed.update(i, true)?
            found = true
            break
          end
        end
        if not found then
          missing.push(e)
        end
      end

      let extra = Array[box->A]
      for (i, c) in consumed.pairs() do
        if not c then extra.push(actual(i)?) end
      end

      if (extra.size() != 0) or (missing.size() != 0) then
        _fail(
          _fmt_msg(loc, "Assert EQ_UNORDERED failed. " + msg
            + " Expected (" + _print_array[A](expect) + ") == ("
            + _print_array[A](actual) + "):"
            + "\nMissing: " + _print_array[box->A](missing)
            + "\nExtra: " + _print_array[box->A](extra)
          )
        )
        return false
      end
      _runner.log(
        _fmt_msg(
          loc,
          "Assert EQ_UNORDERED passed. "
          + msg
          + " Got ("
          + _print_array[A](expect)
          + ") == ("
          + _print_array[A](actual)
          + ")"
        ),
        true
      )
      true
    else
      _fail("Assert EQ_UNORDERED failed from an internal error.")
      false
    end

  fun _print_array[A: Stringable #read](array: ReadSeq[A]): String =>
    """
    Generate a printable string of the contents of the given readseq to use in
    error messages.
    """
    "[len=" + array.size().string() + ": " + ", ".join(array.values()) + "]"


/****** END DUPLICATION FROM TESTHELPER *********/

  fun expect_action(name: String) =>
    """
    expect some action of the given name to complete
    for the property to hold.

    If all expected actions are completed successfully,
    the property is considered successful.

    If 1 action fails, the property is considered failing.

    Call `complete_action(name)` or `fail_action(name)`
    to mark some action as completed.

    Example:

    ```pony
      actor AsyncActor

        let _ph: PropertyHelper

        new create(ph: PropertyHelper) =>
          _ph = ph

        be complete(s: String) =>
          if (s.size() % 2) == 0 then
            _ph.complete_action("is_even")
          else
            _ph.fail_action("is_even")

      class EvenStringProperty is Property1[String]
        fun name(): String => "even_string"

        fun gen(): Generator[String] =>
          Generators.ascii()

      fun property(arg1: String, ph: PropertyHelper) =>
        ph.expect_action("is_even")
        AsyncActor(ph).check(arg1)
    ```

    """
    _runner.expect_action(name)

  fun val complete_action(name: String) =>
    """
    Complete an expected action successfully.

    If all expected actions are completed successfully,
    the property is considered successful.

    If 1 action fails, the property is considered failing.

    If the action `name` was not expected, i.e. was not registered using
    `expect_action`, nothing happens.
    """
    _runner.complete_action(name, this)

  fun val fail_action(name: String) =>
    """
    Mark an expected action as failed.

    If all expected actions are completed successfully,
    the property is considered successful.

    If 1 action fails, the property is considered failing.
    """
    _runner.fail_action(name, this)

  fun complete(success: Bool) =>
    """
    Complete an asynchronous property successfully.

    Once this method is called the property
    is considered successful or failing
    depending on the value of the parameter `success`.

    For more fine grained control over completing or failing
    a property that consists of many steps, consider using
    `expect_action`, `complete_action` and `fail_action`.
    """
    _run_notify.apply(success)

  fun dispose_when_done(disposable: DisposableActor) =>
    """
    Dispose the actor after a property run / a shrink is done.
    """
    _runner.dispose_when_done(disposable)

  fun _fail(msg: String) =>
    _runner.log(msg)
    _run_notify.apply(false)

  fun _fmt_msg(loc: SourceLoc, msg: String): String =>
    let msg_prefix = _run_context + " " + _format_loc(loc)
    if msg.size() > 0 then
      msg_prefix + ": " + msg
    else
      msg_prefix
    end

  fun _format_loc(loc: SourceLoc): String =>
    loc.file() + ":" + loc.line().string()