Skip to content

Commit

Permalink
Add ability to test asynchronous actions.
Browse files Browse the repository at this point in the history
Resolves #5.

Now, if you need to test that something asynchronous happens,
you can call `@ctx.wait_for_action("my action")` to let the test
runner know that the test isn't over until the action completes.

After the asynchronous action is finished, you can mark it as such
by calling `@ctx.finished_action("my action")` to let the test
runner know that the action is complete.

You can add new actions to wait for at any time during a test,
but the names must be unique in order for them to be differentiated.

Once all awaited actions are marked as finished, the asynchronous
test example is done running.

If one or more actions don't get marked as finished in a timely manner,
an informational message will be printed every 5 seconds for all open
actions that are still being awaited.

If the whole test suite will time out after 30 seconds.

These time intervals will be able to be configured at runtime
using the constant override mechanism mentioned in this ticket:
savi-lang/savi#161
  • Loading branch information
jemc committed Mar 24, 2022
1 parent d0bc03b commit 26d4278
Show file tree
Hide file tree
Showing 10 changed files with 291 additions and 60 deletions.
7 changes: 7 additions & 0 deletions manifest.savi
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@
:dependency Map v0
:from "github:savi-lang/Map"

:dependency Time v0
:from "github:savi-lang/Time"

:dependency Timer v0
:from "github:savi-lang/Timer"
:depends on Time

:manifest bin "spec"
:copies Spec
:sources "spec/*.savi"
22 changes: 19 additions & 3 deletions spec/Spec.Assert.Spec.savi
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,27 @@

assert error: _MaybeError.yes!

:it "can be called from another type that holds the spec context"
_OtherTypeThatCanAssert.new(@ctx)
:it "can be called from a class that holds the spec context"
class = _ClassThatCanAssert.new(@ctx)
class.assert_something

:class _OtherTypeThatCanAssert
:it "can be called asynchronously from another actor with waiting"
@ctx.wait_for_action("actor finished 1")
@ctx.wait_for_action("actor finished 2")
actor = _ActorThatCanAssert.new(@ctx)
actor.assert_something
actor.finished_action("actor finished 1")
actor.finished_action("actor finished 2")

:class _ClassThatCanAssert
:is Spec.Context.Holder
:let ctx Spec.Context
:new (@ctx)
:fun assert_something: assert: True

:actor _ActorThatCanAssert
:is Spec.Context.Holder
:let ctx Spec.Context
:new (@ctx)
:be assert_something: assert: True
:be finished_action(action String): @ctx.finished_action(action)
22 changes: 18 additions & 4 deletions src/Spec.Assert.savi
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
success
pos
)
ctx.process.enqueue(assert)
ctx._process.enqueue(assert)

:fun non relation(
caller Spec.Context.Holder
Expand All @@ -40,7 +40,7 @@
Inspect[lhs]
Inspect[rhs]
)
ctx.process.enqueue(assert)
ctx._process.enqueue(assert)

:fun non type_relation(
caller Spec.Context.Holder
Expand All @@ -60,7 +60,7 @@
Inspect[lhs]
rhs
)
ctx.process.enqueue(assert)
ctx._process.enqueue(assert)

:fun non has_error(
caller Spec.Context.Holder
Expand All @@ -76,7 +76,7 @@
pos
expects_error
)
ctx.process.enqueue(assert)
ctx._process.enqueue(assert)

:fun file_and_line String
line = Inspect[@pos.row + 1]
Expand Down Expand Up @@ -178,3 +178,17 @@
], "\n"))

None

:class val Spec.Assert.WaitForAction
:is Spec.Assert
:let action String
:new val (@spec, @example, @action, @pos): @success = True
:fun print_failure(env Env)
None

:class val Spec.Assert.FinishedAction
:is Spec.Assert
:let action String
:new val (@spec, @example, @action, @pos): @success = True
:fun print_failure(env Env)
None
28 changes: 24 additions & 4 deletions src/Spec.Context.savi
Original file line number Diff line number Diff line change
@@ -1,9 +1,29 @@
:trait box Spec.Context.Holder
:fun ctx Spec.Context

:class val Spec.Context
:let env Env
:let process Spec.Process
:let spec String
:let example String
:new (@env, @process, @spec, @example)
:let _process Spec.Process
:let _runner _Runner.Any
:new (@env, @spec, @example, @_process, @_runner)

:trait box Spec.Context.Holder
:fun ctx Spec.Context
:fun wait_for_action(
action String
pos = source_code_position_of_argument action
)
@_process.enqueue(
Spec.Assert.WaitForAction.new(@spec, @example, action, pos)
)

:fun finished_action(
action String
pos = source_code_position_of_argument action
)
@_process.enqueue(
Spec.Assert.FinishedAction.new(@spec, @example, action, pos)
)

:fun dispose_when_done(actor)
@_runner.dispose_when_done(actor)
73 changes: 61 additions & 12 deletions src/Spec.Process.savi
Original file line number Diff line number Diff line change
Expand Up @@ -4,59 +4,106 @@
:var spec_count USize
:let statuses: Map.Ordered(String, Spec.Status).new

:const timed_ping_seconds U64: 5
:const timeout_seconds U64: 30
:let _timer: _Timer.new(@)

:new (@env, @spec_count)
@reporter = Spec.Reporter.Linear.new(@env, @statuses, Spec.Reporter.Full.new(@env))
@reporter = Spec.Reporter.Linear.new(@env, @statuses
Spec.Reporter.Full.new(@env, @statuses)
)

:fun non run(env, runners Array(Spec.Run.Any)) // TODO: shouldn't need this indirection
process = @new(env, runners.size)
runners.each -> (runner | runner.run(process))

:be _timed_ping
@statuses.each -> (spec, spec_status |
spec_status.examples.each -> (example, example_status |
next unless example_status.is_waiting_to_end

@reporter.example_timed_ping(spec, example)
)
)

:be _timed_out
@statuses.each -> (spec, spec_status |
spec_status.examples.each -> (example, example_status |
next unless example_status.is_waiting_to_end

@reporter.example_timed_out(spec, example)
example_status._runner.dispose
_Log.fail(@env)
)
)
@overall_ended

:be spec_began(spec String, expected_examples USize)
// TODO: complain if spec is empty or duplicate
status = Spec.Status.new(spec)
status.expected_examples = expected_examples
@statuses[spec] = status
@reporter.spec_began(spec)

:fun ref overall_began
@reporter.overall_began

:fun ref overall_ended
@reporter.overall_ended
@_timer.dispose

:fun ref spec_ended(spec String)
try (
status = @statuses[spec]!
status.ended = True
status.is_ended = True
status.examples.each -> (example, example_status |
if (example_status.ended.not) (
if example_status.is_ended.not (
_Log.bug(@env, "spec_ended before all of its examples ended")
)
)
@reporter.spec_ended(spec)

// If now all specs have ended, then we know we've ended overall.
if (@statuses.has_all -> (_, s | s.is_ended)) @overall_ended
|
_Log.bug(@env, "spec_ended before the spec_began")
)

:be example_began(spec String, example String):
:be example_began(spec String, example String, runner _Runner.Any):
// TODO: complain if example is empty or duplicate
try (
@statuses[spec]!.examples[example] = Spec.Status.ForExample.new(example)
@statuses[spec]!.examples[example] =
Spec.Status.ForExample.new(example, runner)
@reporter.example_began(spec, example)
|
_Log.bug(@env, "example_began before the spec_began")
)

:be example_close(spec String, example String): @example_ended(spec, example)
:be example_close(spec String, example String)
try (
status = @statuses[spec]!.examples[example]!
status.is_synchronous_portion_ended = True
if status.is_ready_to_end @example_ended(spec, example)
|
_Log.bug(@env, "example_close before the example_began")
)

:fun ref example_ended(spec String, example String)
try (
status = @statuses[spec]!
example_status = status.examples[example]!
example_status.ended = True
example_status.is_ended = True
example_status._runner.dispose

skip = example_status.events.size == 0
@reporter.example_ended(spec, example, skip)

if (
status.examples.size >= status.expected_examples
&& status.examples.has_all -> (_, es | es.ended)
&& status.examples.has_all -> (_, es | es.is_ended)
) (
@spec_ended(spec)
)

skip = example_status.events.size == 0
@reporter.example_ended(spec, example, skip)
|
_Log.bug(@env, "example_ended before the example_began")
)
Expand All @@ -69,8 +116,10 @@
if !event.success _Log.fail(@env)

try (
@statuses[spec]!.examples[example]!.events << event
status = @statuses[spec]!.examples[example]!
status.events << event
@reporter.event(spec, example, event)
if status.is_ready_to_end @example_ended(spec, example)
|
_Log.bug(@env, "assert before the example_began and/or spec_began")
)
Loading

0 comments on commit 26d4278

Please sign in to comment.