Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Spec: add support for focus #8125

Merged
merged 8 commits into from
Sep 9, 2019
Prev Previous commit
Next Next commit
Spec: refactor to make it easier to add more filter kinds
Before this commit an `it` block ran immediately. This makes it hard to
implement other filtering mechanisms. In particular to implement
something like RSpec's `focus` we need to first know whether there's any
example marked with `focus: true`. Without collecting all examples
first we can't do this.

So, this commit first collects all describes, contexts and examples and
then filters then. The code is now much cleaner is easier to extend.
  • Loading branch information
asterite committed Aug 29, 2019
commit 4235af5d67000e5a77c7b050e2f575ec39ae1faa
137 changes: 48 additions & 89 deletions src/spec/context.cr
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
require "./item"

module Spec
# :nodoc:
#
# A context represents a `describe` or `context`.
abstract class Context
# All the children, which can be `describe`/`context` or `it`
getter children = [] of NestedContext | Example
end

# :nodoc:
Expand All @@ -12,8 +18,17 @@ module Spec
elapsed : Time::Span?,
exception : Exception?

def self.root_context
RootContext.instance
end

# :nodoc:
#
# The root context is the main interface that the spec DSL interacts with.
class RootContext < Context
class_getter instance = RootContext.new
@@current_context : Context = @@instance

def initialize
@results = {
success: [] of Result,
Expand All @@ -23,35 +38,20 @@ module Spec
}
end

def parent
nil
def run
children.each &.run
end

def succeeded
@results[:fail].empty? && @results[:error].empty?
end

def self.report(kind, full_description, file, line, elapsed = nil, ex = nil)
def report(kind, full_description, file, line, elapsed = nil, ex = nil)
result = Result.new(kind, full_description, file, line, elapsed, ex)
@@contexts_stack.last.report(result)
end

def report(result)
Spec.formatters.each(&.report(result))

@results[result.kind] << result
end

def self.print_results(elapsed_time, aborted = false)
@@instance.print_results(elapsed_time, aborted)
end

def self.succeeded
@@instance.succeeded
end

def self.finish(elapsed_time, aborted = false)
@@instance.finish(elapsed_time, aborted)
def succeeded
@results[:fail].empty? && @results[:error].empty?
end

def finish(elapsed_time, aborted = false)
Expand Down Expand Up @@ -147,69 +147,36 @@ module Spec
end
end

@@instance = RootContext.new
@@contexts_stack = [@@instance] of Context
def describe(description, file, line, end_line, &block)
context = Spec::NestedContext.new(@@current_context, description, file, line, end_line)
@@current_context.children << context

def self.describe(description, file, line, &block)
describe = Spec::NestedContext.new(description, file, line, @@contexts_stack.last)
@@contexts_stack.push describe
Spec.formatters.each(&.push(describe))
block.call
Spec.formatters.each(&.pop)
@@contexts_stack.pop
end

def self.it(description, file, line, end_line, &block)
Spec::RootContext.check_nesting_spec(file, line) do
return unless Spec.split_filter_matches
return unless Spec.matches?(description, file, line, end_line)

Spec.formatters.each(&.before_example(description))

start = Time.monotonic
begin
Spec.run_before_each_hooks
block.call
Spec::RootContext.report(:success, description, file, line, Time.monotonic - start)
rescue ex : Spec::AssertionFailed
Spec::RootContext.report(:fail, description, file, line, Time.monotonic - start, ex)
Spec.abort! if Spec.fail_fast?
rescue ex
Spec::RootContext.report(:error, description, file, line, Time.monotonic - start, ex)
Spec.abort! if Spec.fail_fast?
ensure
Spec.run_after_each_hooks

# We do this to give a chance for signals (like CTRL+C) to be handled,
# which currently are only handled when there's a fiber switch
# (IO stuff, sleep, etc.). Without it the user might wait more than needed
# after pressing CTRL+C to quit the tests.
Fiber.yield
end
old_context = @@current_context
@@current_context = context
begin
block.call
ensure
@@current_context = old_context
end
end

def self.pending(description, file, line, end_line, &block)
Spec::RootContext.check_nesting_spec(file, line) do
return unless Spec.matches?(description, file, line, end_line)

Spec.formatters.each(&.before_example(description))

Spec::RootContext.report(:pending, description, file, line)
end
def it(description, file, line, end_line, &block)
add_example(description, file, line, end_line, block, pending: false)
end

def self.matches?(description, pattern, line, locations)
@@contexts_stack.any?(&.matches?(pattern, line, locations)) || description =~ pattern
def pending(description, file, line, end_line, &block)
add_example(description, file, line, end_line, block, pending: true)
end

def matches?(pattern, line, locations)
false
private def add_example(description, file, line, end_line, block, pending)
check_nesting_spec(file, line) do
@@current_context.children << Example.new(@@current_context, description, file, line, end_line, block, pending)
end
end

@@spec_nesting = false

def self.check_nesting_spec(file, line, &block)
def check_nesting_spec(file, line, &block)
raise NestingSpecError.new("can't nest `it` or `pending`", file, line) if @@spec_nesting

@@spec_nesting = true
Expand All @@ -223,29 +190,21 @@ module Spec

# :nodoc:
class NestedContext < Context
getter parent : Context
getter description : String
getter file : String
getter line : Int32
include Item

def initialize(@description : String, @file, @line, @parent)
end
getter! parent : Context

def report(result)
@parent.report Result.new(result.kind, "#{@description} #{result.description}", result.file, result.line, result.elapsed, result.exception)
def initialize(@parent : Context, @description : String, @file : String, @line : Int32, @end_line : Int32)
end

def matches?(pattern, line, locations)
return true if @description =~ pattern
return true if @line == line

if locations
lines = locations[@file]?
return true unless lines
return lines.includes?(@line)
end
def run
Spec.formatters.each(&.push(self))
children.each &.run
Spec.formatters.each(&.pop)
end

false
def report(result)
parent.report Result.new(result.kind, "#{@description} #{result.description}", result.file, result.line, result.elapsed, result.exception)
end
end
end
74 changes: 29 additions & 45 deletions src/spec/dsl.cr
Original file line number Diff line number Diff line change
Expand Up @@ -109,58 +109,19 @@ module Spec
lines << line
end

@@split_filter : NamedTuple(remainder: Int32, quotient: Int32)? = nil
record SplitFilter, remainder : Int32, quotient : Int32

@@split_filter : SplitFilter? = nil

def self.add_split_filter(filter)
if filter
r, m = filter.split('%').map &.to_i
@@split_filter = {remainder: r, quotient: m}
@@split_filter = SplitFilter.new(remainder: r, quotient: m)
else
@@split_filter = nil
end
end

@@spec_counter = -1

def self.split_filter_matches
split_filter = @@split_filter

if split_filter
@@spec_counter += 1
@@spec_counter % split_filter[:quotient] == split_filter[:remainder]
else
true
end
end

# :nodoc:
def self.matches?(description, file, line, end_line = line)
spec_pattern = @@pattern
spec_line = @@line
locations = @@locations

# When a method invokes `it` and only forwards line information,
# not end_line information (this can happen in code before we
# introduced the end_line feature) then running a spec by giving
# a line won't work because end_line might be located before line.
# So, we also check `line == spec_line` to somehow preserve
# backwards compatibility.
if spec_line && (line == spec_line || line <= spec_line <= end_line)
return true
end

if locations
lines = locations[file]?
return true if lines && lines.any? { |l| line == l || line <= l <= end_line }
end

if spec_pattern || spec_line || locations
Spec::RootContext.matches?(description, spec_pattern, spec_line, locations)
else
true
end
end

@@fail_fast = false

# :nodoc:
Expand Down Expand Up @@ -199,10 +160,33 @@ module Spec
# :nodoc:
def self.run
start_time = Time.monotonic

at_exit do
run_filters
root_context.run
ensure
elapsed_time = Time.monotonic - start_time
Spec::RootContext.finish(elapsed_time, @@aborted)
exit 1 unless Spec::RootContext.succeeded && !@@aborted
root_context.finish(elapsed_time, @@aborted)
exit 1 unless root_context.succeeded && !@@aborted
end
end

# :nodoc:
def self.run_filters
if pattern = @@pattern
root_context.filter_by_pattern(pattern)
end

if line = @@line
root_context.filter_by_line(line)
end

if locations = @@locations
root_context.filter_by_locations(locations)
end

if split_filter = @@split_filter
root_context.filter_by_split(split_filter)
end
end
end
Expand Down
48 changes: 48 additions & 0 deletions src/spec/example.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
require "./item"

module Spec
class Example
include Item

getter parent : Context
getter block : ->
getter? pending : Bool

def initialize(@parent : Context, @description : String,
@file : String, @line : Int32, @end_line : Int32,
@block : ->, @pending : Bool)
end

def run
Spec.root_context.check_nesting_spec(file, line) do
Spec.formatters.each(&.before_example(description))

if pending?
Spec.root_context.report(:pending, description, file, line)
return
end

start = Time.monotonic
begin
Spec.run_before_each_hooks
block.call
Spec.root_context.report(:success, description, file, line, Time.monotonic - start)
rescue ex : Spec::AssertionFailed
Spec.root_context.report(:fail, description, file, line, Time.monotonic - start, ex)
Spec.abort! if Spec.fail_fast?
rescue ex
Spec.root_context.report(:error, description, file, line, Time.monotonic - start, ex)
Spec.abort! if Spec.fail_fast?
ensure
Spec.run_after_each_hooks

# We do this to give a chance for signals (like CTRL+C) to be handled,
# which currently are only handled when there's a fiber switch
# (IO stuff, sleep, etc.). Without it the user might wait more than needed
# after pressing CTRL+C to quit the tests.
Fiber.yield
end
end
end
end
end
Loading