Skip to content

Commit

Permalink
get something working with new Value base class.
Browse files Browse the repository at this point in the history
  • Loading branch information
botandrose-machine committed Oct 5, 2023
1 parent 19d43dd commit 4f181d3
Show file tree
Hide file tree
Showing 5 changed files with 144 additions and 50 deletions.
3 changes: 2 additions & 1 deletion lib/cucumber/core/compiler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,9 @@ def create_test_case(pickle)
uri = pickle.uri
test_steps = pickle.steps.map { |step| create_test_step(step, uri) }
lines = source_lines_for_pickle(pickle).sort.reverse
location = Test::Location.new(uri, lines)
tags = source_lines_for_all_pickle_tags(pickle, uri)
test_case = Test::Case.new(id_generator.new_id, pickle.name, test_steps, Test::Location.new(uri, lines), tags, pickle.language)
test_case = Test::Case.new(id: id_generator.new_id, name: pickle.name, test_steps: test_steps, location: location, tags: tags, language: pickle.language)
@event_bus&.test_case_created(test_case, pickle)
test_case
end
Expand Down
19 changes: 6 additions & 13 deletions lib/cucumber/core/test/case.rb
Original file line number Diff line number Diff line change
@@ -1,23 +1,16 @@
# frozen_string_literal: true

require 'cucumber/core/value'
require 'cucumber/core/test/result'
require 'cucumber/tag_expressions'

module Cucumber
module Core
module Test
class Case
attr_reader :id, :name, :test_steps, :location, :tags, :language, :around_hooks

def initialize(id, name, test_steps, location, tags, language, around_hooks = [])
Case = Value.define(:id, :name, :test_steps, :location, :tags, :language, around_hooks: []) do
def initialize(*)
super
raise ArgumentError.new("test_steps should be an Array but is a #{test_steps.class}") unless test_steps.is_a?(Array)
@id = id
@name = name
@test_steps = test_steps
@location = location
@tags = tags
@language = language
@around_hooks = around_hooks
end

def step_count
Expand All @@ -36,11 +29,11 @@ def describe_to(visitor, *args)
end

def with_steps(test_steps)
self.class.new(id, name, test_steps, location, tags, language, around_hooks)
with(test_steps: test_steps)
end

def with_around_hooks(around_hooks)
self.class.new(id, name, test_steps, location, tags, language, around_hooks)
with(around_hooks: around_hooks)
end

def match_tags?(*expressions)
Expand Down
95 changes: 95 additions & 0 deletions lib/cucumber/core/value.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# frozen_string_literal: true

module Cucumber
module Core
class Value
class << self
def define(*args, **kwargs, &block)
Builder.new(args, kwargs, block).build
end
end

Builder = Struct.new(:args, :kwargs, :block) do
def build
validate_definition!

klass = ::Class.new(Value)

klass.instance_variable_set(:@members, members)

members[:all].each do |arg|
klass.define_method(arg) do
@attributes[arg]
end
end

klass.class_eval(&block) if block

klass
end

private

def validate_definition!
raise ArgumentError if args.any?(/=/)

dup_arg = members[:all].detect { |a| members[:all].count(a) > 1 }
raise ArgumentError, "duplicate member #{dup_arg}" if dup_arg
end

def members
{
all: args + kwargs.keys,
required: args,
optional: kwargs
}
end
end

def members
self.class.instance_variable_get :@members
end

def initialize(**kwargs)
validate_kwargs!(kwargs)

@attributes = {}
members[:required].each do |arg|
@attributes[arg] = kwargs.fetch(arg)
end
members[:optional].each do |arg, default|
@attributes[arg] = kwargs.fetch(arg, default)
end

freeze
end

def inspect
attribute_markers = @attributes.map do |key, value|
"#{key}=#{value}"
end.join(', ')

display = ['value', self.class.name, attribute_markers].compact.join(' ')

"#<#{display}>"
end
alias to_s inspect

def with(**kwargs)
return self if kwargs.empty?

self.class.new(**@attributes.merge(kwargs))
end

private

def validate_kwargs!(kwargs)
extras = kwargs.keys - members[:all]
raise ArgumentError, "unknown arguments #{extras.join(', ')}" if extras.any?

missing = members[:required] - kwargs.keys
raise ArgumentError, "missing arguments #{missing.map(&:inspect).join(', ')}" if missing.any?
end
end
end
end
16 changes: 12 additions & 4 deletions spec/cucumber/core/test/case_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,21 @@
include Cucumber::Core
include Cucumber::Core::Gherkin::Writer

let(:id) { double }
let(:name) { double }
let(:test_steps) { [double, double] }
let(:location) { double }
let(:tags) { double }
let(:language) { double }
let(:test_case) { described_class.new(id, name, test_steps, location, tags, language) }
let(:test_steps) { [double, double] }
let(:test_case) do
described_class.new(
id: double,
name: name,
test_steps: test_steps,
location: location,
tags: tags,
language: language
)
end

context 'describing itself' do
let(:visitor) { double }
Expand All @@ -42,7 +50,7 @@
expect(first_hook).to receive(:describe_to).ordered.and_yield
expect(second_hook).to receive(:describe_to).ordered.and_yield
around_hooks = [first_hook, second_hook]
described_class.new(id, name, [], location, tags, language, around_hooks).describe_to(visitor, double)
test_case.with(test_steps: [], around_hooks: around_hooks).describe_to(visitor, double)
end
end

Expand Down
61 changes: 29 additions & 32 deletions spec/cucumber/core/test/runner_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,19 @@
require 'cucumber/core/test/duration_matcher'

describe Cucumber::Core::Test::Runner do
let(:step_id) { double }
let(:test_id) { double }
let(:name) { double }
let(:location) { double }
let(:tags) { double }
let(:language) { double }
let(:test_case) { Cucumber::Core::Test::Case.new(test_id, name, test_steps, location, tags, language) }
let(:text) { double }
let(:runner) { described_class.new(event_bus) }
let(:event_bus) { double.as_null_object }
let(:passing) { Cucumber::Core::Test::Step.new(step_id, text, location, location).with_action { :no_op } }
let(:failing) { Cucumber::Core::Test::Step.new(step_id, text, location, location).with_action { raise exception } }
let(:pending) { Cucumber::Core::Test::Step.new(step_id, text, location, location).with_action { raise Cucumber::Core::Test::Result::Pending.new('TODO') } }
let(:skipping) { Cucumber::Core::Test::Step.new(step_id, text, location, location).with_action { raise Cucumber::Core::Test::Result::Skipped.new } }
let(:undefined) { Cucumber::Core::Test::Step.new(step_id, text, location, location) }
let(:exception) { StandardError.new('test error') }
let(:test_steps) { [] }
let(:test_case) { Cucumber::Core::Test::Case.new(id: double, name: double, test_steps: test_steps, location: double, tags: double, language: double) }
let(:step_id) { double }
let(:text) { double }
let(:location) { double }
let(:runner) { described_class.new(event_bus) }
let(:event_bus) { double.as_null_object }
let(:passing) { Cucumber::Core::Test::Step.new(step_id, text, location, location).with_action { :no_op } }
let(:failing) { Cucumber::Core::Test::Step.new(step_id, text, location, location).with_action { raise exception } }
let(:pending) { Cucumber::Core::Test::Step.new(step_id, text, location, location).with_action { raise Cucumber::Core::Test::Result::Pending.new('TODO') } }
let(:skipping) { Cucumber::Core::Test::Step.new(step_id, text, location, location).with_action { raise Cucumber::Core::Test::Result::Skipped.new } }
let(:undefined) { Cucumber::Core::Test::Step.new(step_id, text, location, location) }
let(:exception) { StandardError.new('test error') }

before do
allow(event_bus).to receive(:test_case_started)
Expand Down Expand Up @@ -226,8 +223,8 @@

context 'with multiple test cases' do
context 'when the first test case fails' do
let(:first_test_case) { Cucumber::Core::Test::Case.new(test_id, name, [failing], location, tags, language) }
let(:last_test_case) { Cucumber::Core::Test::Case.new(test_id, name, [passing], location, tags, language) }
let(:first_test_case) { test_case.with(test_steps: [failing]) }
let(:last_test_case) { test_case.with(test_steps: [passing]) }
let(:test_cases) { [first_test_case, last_test_case] }

it 'reports the results correctly for the following test case' do
Expand Down Expand Up @@ -260,47 +257,47 @@

it "passes normally when around hooks don't fail" do
around_hook = Cucumber::Core::Test::AroundHook.new { |block| block.call }
test_case = Cucumber::Core::Test::Case.new(test_id, name, [passing_step], location, tags, language, [around_hook])
expect(event_bus).to receive(:test_case_finished).with(test_case, anything) do |_reported_test_case, result|
passing_test_case = test_case.with(test_steps: [passing_step], around_hooks: [around_hook])
expect(event_bus).to receive(:test_case_finished).with(passing_test_case, anything) do |_reported_test_case, result|
expect(result).to be_passed
end
test_case.describe_to runner
passing_test_case.describe_to runner
end

it 'gets a failed result if the Around hook fails before the test case is run' do
around_hook = Cucumber::Core::Test::AroundHook.new { |_block| raise exception }
test_case = Cucumber::Core::Test::Case.new(test_id, name, [passing_step], location, tags, language, [around_hook])
expect(event_bus).to receive(:test_case_finished).with(test_case, anything) do |_reported_test_case, result|
failing_test_case = test_case.with(test_steps: [passing_step], around_hooks: [around_hook])
expect(event_bus).to receive(:test_case_finished).with(failing_test_case, anything) do |_reported_test_case, result|
expect(result).to be_failed
expect(result.exception).to eq exception
end
test_case.describe_to runner
failing_test_case.describe_to runner
end

it 'gets a failed result if the Around hook fails after the test case is run' do
around_hook = Cucumber::Core::Test::AroundHook.new { |block| block.call; raise exception }
test_case = Cucumber::Core::Test::Case.new(test_id, name, [passing_step], location, tags, language, [around_hook])
expect(event_bus).to receive(:test_case_finished).with(test_case, anything) do |_reported_test_case, result|
failing_test_case = test_case.with(test_steps: [passing_step], around_hooks: [around_hook])
expect(event_bus).to receive(:test_case_finished).with(failing_test_case, anything) do |_reported_test_case, result|
expect(result).to be_failed
expect(result.exception).to eq exception
end
test_case.describe_to runner
failing_test_case.describe_to runner
end

it 'fails when a step fails if the around hook works' do
around_hook = Cucumber::Core::Test::AroundHook.new { |block| block.call }
failing_step = Cucumber::Core::Test::Step.new(step_id, text, location, location).with_action { raise exception }
test_case = Cucumber::Core::Test::Case.new(test_id, name, [failing_step], location, tags, language, [around_hook])
expect(event_bus).to receive(:test_case_finished).with(test_case, anything) do |_reported_test_case, result|
failing_test_case = test_case.with(test_steps: [failing_step], around_hooks: [around_hook])
expect(event_bus).to receive(:test_case_finished).with(failing_test_case, anything) do |_reported_test_case, result|
expect(result).to be_failed
expect(result.exception).to eq exception
end
test_case.describe_to runner
failing_test_case.describe_to runner
end

it 'sends after_test_step for a step interrupted by (a timeout in) the around hook' do
around_hook = Cucumber::Core::Test::AroundHook.new { |block| block.call; raise exception }
test_case = Cucumber::Core::Test::Case.new(test_id, name, [], location, tags, language, [around_hook])
failing_test_case = test_case.with(test_steps: [], around_hooks: [around_hook])
allow(runner).to receive(:running_test_step).and_return(passing_step)
expect(event_bus).to receive(:test_step_finished).with(passing_step, anything) do |_reported_test_case, result|
expect(result).to be_failed
Expand All @@ -310,7 +307,7 @@
expect(result).to be_failed
expect(result.exception).to eq(exception)
end
test_case.describe_to(runner)
failing_test_case.describe_to(runner)
end
end
end

0 comments on commit 4f181d3

Please sign in to comment.