Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
icy-arctic-fox committed Jun 21, 2024
1 parent afa1b34 commit 3a5c89a
Show file tree
Hide file tree
Showing 9 changed files with 201 additions and 113 deletions.
6 changes: 6 additions & 0 deletions spec/spectator/core/example_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
require "../../spec_helper"

describe Spectator::Core::Example do
describe "#description" do
end
end
18 changes: 17 additions & 1 deletion src/spectator/core/context.cr
Original file line number Diff line number Diff line change
@@ -1,4 +1,20 @@
require "./hooks"
require "./item"

module Spectator::Core
abstract class Context
module Context
include Hooks

def describe(description, &)
context(description) { with self yield self }
end

abstract def context(description, &)

def it(description, &block : Example ->) : Example
Example.new(description, &block).tap do |example|
example.parent = self
end
end
end
end
33 changes: 33 additions & 0 deletions src/spectator/core/context_hook.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
require "./location_range"

module Spectator::Core
class ContextHook
getter! location : LocationRange

getter! exception : Exception

@called = Atomic::Flag.new

def initialize(@location = nil, &@block : ->)
end

def initialize(@block : ->)
end

def call
# Ensure the hook is called once.
called = @called.test_and_set
# Re-raise previous error if there was one.
@exception.try { |ex| raise ex }
# Only call hook if it hasn't been called yet.
return unless called

begin
@block.call
rescue ex
@exception = ex
raise ex
end
end
end
end
46 changes: 35 additions & 11 deletions src/spectator/core/example.cr
Original file line number Diff line number Diff line change
@@ -1,21 +1,30 @@
require "./item"
require "./location_range"
require "./result"

module Spectator::Core
# Information about a test case and functionality for running it.
class Example
# Name of the example.
# This may be nil if the example does not have a name.
# In that case, the description of the first matcher executed in the example should be used.
property! name : String

class Example < Item
# Creates a new example.
# The *name* may be nil if the example has no name.
def initialize(@name = nil, &@block : Example -> Nil)
#
# The *description* can be a string, nil, or any other object.
# When it is a string or nil, it will be stored as-is.
# Any other types will be converted to a string by calling `#inspect` on it.
def initialize(description = nil, location : LocationRange? = nil, &@block : Example -> Nil)
super(description, location)
end

# Runs the example.
def run : Result
@block.call(self)
Result.capture do
if context = parent?
context.with_hooks(self) do
@block.call(self)
end
else
@block.call(self)
end
end
end

# Constructs a string representation of the example.
Expand All @@ -28,8 +37,23 @@ module Spectator::Core
end
end

def to_proc
Procsy.new(self)
def inspect(io : IO) : Nil
io << "#<" << self.class << ' '
if description = @description
io << '"' << description << '"'
else
io << "Anonymous Example"
end
if location = @location
io << " @ " << location
end
io << " 0x"
object_id.to_s(io, 16)
io << '>'
end

def to_proc(&block : Example ->)
Procsy.new(self, &block)
end

struct Procsy
Expand Down
40 changes: 31 additions & 9 deletions src/spectator/core/example_group.cr
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
require "./context"
require "./item"

module Spectator::Core
# Information about a group of examples and functionality for running them.
# The group can be nested.
class ExampleGroup
# Name of the group.
# This may be nil if the group does not have a name.
property! name : String
class ExampleGroup < Item
include Context

getter examples = [] of Example
def context(description, &)
self.class.new(description).tap do |child|
child.parent = self
with child yield
end
end

# Creates a new group.
# The *name* may be nil if the group has no name.
def initialize(@name = nil)
def self.new(description = nil, location : LocationRange? = nil, &)
group = new(description, location)
with group yield group
group
end

# Constructs a string representation of the group.
Expand All @@ -19,8 +26,23 @@ module Spectator::Core
if name = @name
io << name
else
io << "<Anonymous Context>"
io << "<Anonymous ExampleGroup>"
end
end

def inspect(io : IO) : Nil
io << "#<" << self.class << ' '
if description = @description
io << '"' << description << '"'
else
io << "Anonymous Example Group"
end
if location = @location
io << " @ " << location
end
io << " 0x"
object_id.to_s(io, 16)
io << '>'
end
end
end
28 changes: 28 additions & 0 deletions src/spectator/core/example_hook.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
require "./example"
require "./location_range"

module Spectator::Core
class ExampleHook(T)
getter! location : LocationRange

getter! exception : Exception

def initialize(@location = nil, &@block : T ->)
end

def initialize(@block : T ->)
end

def call(example : T)
# Re-raise previous error if there was one.
@exception.try { |ex| raise ex }

begin
@block.call(example)
rescue ex
@exception = ex
raise ex
end
end
end
end
111 changes: 20 additions & 91 deletions src/spectator/core/hooks.cr
Original file line number Diff line number Diff line change
@@ -1,119 +1,48 @@
module Spectator::Core
class ExampleHook
getter! location : LocationRange

getter! exception : Exception

def initialize(@location = nil, &@block : Example ->)
end

def initialize(@block : Example ->)
end

def call(example : Example)
# Re-raise previous error if there was one.
@exception.try { |ex| raise ex }

begin
@block.call(example)
rescue ex
@exception = ex
raise ex
end
end
end

class ContextHook
getter! location : LocationRange

getter! exception : Exception

@called = Atomic::Flag.new

def initialize(@location = nil, &@block : ->)
end

def initialize(@block : ->)
end

def call
# Ensure the hook is called once.
called = @called.test_and_set
# Re-raise previous error if there was one.
@exception.try { |ex| raise ex }
# Only call hook if it hasn't been called yet.
return unless called

begin
@block.call
rescue ex
@exception = ex
raise ex
end
end
end

class ExampleProcsyHook
getter! location : LocationRange

getter! exception : Exception

def initialize(@location = nil, &@block : Example::Procsy ->)
end

def initialize(@block : Example::Procsy ->)
end

def call(example : Example)
# Re-raise previous error if there was one.
@exception.try { |ex| raise ex }

procsy = example.to_proc
begin
@block.call(procsy)
rescue ex
@exception = ex
raise ex
end
end
end
require "./example_hook"

module Spectator::Core
module Hooks
def before_each(& : Example ->) : Nil
hooks = @before_each ||= [] of ExampleHook
def before_each(&block : Example ->) : Nil
hooks = @before_each ||= [] of ExampleHook(Example)
hooks << ExampleHook(Example).new(&block)
end

def after_each(& : Example ->)
hooks = @after_each ||= [] of ExampleHook
def after_each(&block : Example ->)
hooks = @after_each ||= [] of ExampleHook(Example)
hooks << ExampleHook(Example).new(&block)
end

def before_all(& : ->)
def before_all(&block : ->)
hooks = @before_all ||= [] of ContextHook
hooks << ContextHook.new(&block)
end

def after_all(& : ->)
def after_all(&block : ->)
hooks = @after_all ||= [] of ContextHook
hooks << ContextHook.new(&block)
end

def around_each(& : Example ->)
hooks = @around_each ||= [] of ExampleHook(Example::Procsy)
hooks << ExampleHook(Example::Procsy).new(&block)
end

def around_all(& : ->)
end

def run_with_hooks(example : Example, & : ->) : Nil
def with_hooks(example : Example, &block : ->) : Nil
@before_all.try &.each &.call
@before_each.try &.each &.call(example)
proc = wrap_with_around_each_hooks(example) { yield }
proc.run
# proc = wrap_with_around_each_hooks(example, &block)
# proc.run
block.call
@after_each.try &.each &.call(example)
@after_all.try &.each &.call
end

private def wrap_with_around_each_hooks(example : Example, & : ->)
proc = Example::Procsy.new(example) { yield }
private def wrap_with_around_each_hooks(example : Example, &block : ->)
proc = example.to_proc(&block)
@around_each.try do |hooks|
proc = Example::Procsy.new(example, &proc)
end
proc
end
Expand Down
21 changes: 21 additions & 0 deletions src/spectator/core/item.cr
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,32 @@ module Spectator::Core
# This may be nil if the item does not have a description.
getter! description : String

# The full description of the item.
# This is a combination of all parent descriptions and this item's description.
def full_description : String
String.build do |io|
build_full_description(io)
end
end

protected def build_full_description(io : IO) : Nil
parent?.try &.build_full_description(io)
if description = description?
io << ' ' << description
end
end

# The location of the item in the source code.
# This may be nil if the item does not have a location,
# such as if it was dynamically generated.
getter! location : LocationRange

# Context (the example group) the item belongs to.
getter! parent : Context

def parent=(@parent : Context)
end

# Creates a new item.
# The *description* can be a string, nil, or any other object.
# When it is a string or nil, it will be stored as-is.
Expand Down
Loading

0 comments on commit 3a5c89a

Please sign in to comment.