diff --git a/lib/cucumber/core/compiler.rb b/lib/cucumber/core/compiler.rb index 2e8af89a..32c39feb 100644 --- a/lib/cucumber/core/compiler.rb +++ b/lib/cucumber/core/compiler.rb @@ -35,12 +35,15 @@ def done private def create_test_case(pickle) - uri = pickle.uri - test_steps = pickle.steps.map { |step| create_test_step(step, uri) } - location = location_from_pickle(pickle) - parent_locations = parent_locations_from_pickle(pickle) - tags = tags_from_pickle(pickle, uri) - test_case = Test::Case.new(id_generator.new_id, pickle.name, test_steps, location, parent_locations, tags, pickle.language) + test_case = Test::Case.new( + id: id_generator.new_id, + name: pickle.name, + test_steps: pickle.steps.map { |step| create_test_step(step, pickle.uri) }, + location: location_from_pickle(pickle), + parent_locations: parent_locations_from_pickle(pickle), + tags: tags_from_pickle(pickle), + language: pickle.language + ) @event_bus&.test_case_created(test_case, pickle) test_case end @@ -80,9 +83,9 @@ def location_from_pickle_step(pickle_step, uri) Test::Location.new(uri, lines.sort.reverse) end - def tags_from_pickle(pickle, uri) + def tags_from_pickle(pickle) pickle.tags.map do |tag| - location = Test::Location.new(uri, source_line(tag.ast_node_id)) + location = Test::Location.new(pickle.uri, source_line(tag.ast_node_id)) Test::Tag.new(location, tag.name) end end diff --git a/lib/cucumber/core/test/case.rb b/lib/cucumber/core/test/case.rb index 132073df..0940a40c 100644 --- a/lib/cucumber/core/test/case.rb +++ b/lib/cucumber/core/test/case.rb @@ -1,26 +1,13 @@ # 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, :parent_locations, :tags, :language, :around_hooks - - def initialize(id, name, test_steps, location, parent_locations, tags, language, around_hooks = []) - 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 - @parent_locations = parent_locations - @tags = tags - @language = language - @around_hooks = around_hooks - end - + Case = Value.define(:id, :name, :test_steps, :location, :parent_locations, :tags, :language, around_hooks: []) do def step_count test_steps.count end @@ -37,11 +24,11 @@ def describe_to(visitor, *args) end def with_steps(test_steps) - self.class.new(id, name, test_steps, location, parent_locations, tags, language, around_hooks) + with(test_steps: test_steps) end def with_around_hooks(around_hooks) - self.class.new(id, name, test_steps, location, parent_locations, tags, language, around_hooks) + with(around_hooks: around_hooks) end def match_tags?(*expressions) diff --git a/lib/cucumber/core/value.rb b/lib/cucumber/core/value.rb new file mode 100644 index 00000000..9673b6b0 --- /dev/null +++ b/lib/cucumber/core/value.rb @@ -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 diff --git a/spec/cucumber/core/test/case_spec.rb b/spec/cucumber/core/test/case_spec.rb index bb5c40b6..8e5cd394 100644 --- a/spec/cucumber/core/test/case_spec.rb +++ b/spec/cucumber/core/test/case_spec.rb @@ -9,14 +9,22 @@ include Cucumber::Core include Cucumber::Core::Gherkin::Writer - let(:id) { double } let(:name) { double } + let(:test_steps) { [double, double] } let(:location) { double } - let(:parent_locations) { double } let(:tags) { double } let(:language) { double } - let(:test_case) { described_class.new(id, name, test_steps, location, parent_locations, tags, language) } - let(:test_steps) { [double, double] } + let(:test_case) do + described_class.new( + id: double, + name: name, + test_steps: test_steps, + location: location, + parent_locations: double, + tags: tags, + language: language + ) + end describe '#describe_to' do let(:visitor) { double } @@ -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, parent_locations, tags, language, around_hooks).describe_to(visitor, double) + test_case.with(test_steps: [], around_hooks: around_hooks).describe_to(visitor, double) end end diff --git a/spec/cucumber/core/test/runner_spec.rb b/spec/cucumber/core/test/runner_spec.rb index 2a4864d0..d0a45e98 100644 --- a/spec/cucumber/core/test/runner_spec.rb +++ b/spec/cucumber/core/test/runner_spec.rb @@ -7,8 +7,10 @@ require 'cucumber/core/test/duration_matcher' describe Cucumber::Core::Test::Runner do - let(:test_case) { Cucumber::Core::Test::Case.new(double, double, test_steps, double, double, double, double) } + let(:test_steps) { double } + let(:test_case) { Cucumber::Core::Test::Case.new(id: double, name: double, test_steps: test_steps, location: double, parent_locations: double, tags: double, language: 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(double, text, double, double).with_action { :no_op } } @@ -221,9 +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(double, double, [failing], double, double, double, double) } - let(:last_test_case) { Cucumber::Core::Test::Case.new(double, double, [passing], double, double, double, double) } - let(:test_cases) { [first_test_case, last_test_case] } + let(:first_test_case) { test_case.with(test_steps: [failing]) } + let(:last_test_case) { test_case.with(test_steps: [passing]) } it 'reports the results correctly for the following test case' do expect(event_bus).to receive(:test_case_finished) { |reported_test_case, result| @@ -231,16 +232,20 @@ expect(result).to be_passed if reported_test_case.equal?(last_test_case) }.twice - test_cases.each { |test_case| test_case.describe_to(runner) } + [first_test_case, last_test_case].each do |test_case| + test_case.describe_to(runner) + end end end end context 'when passing the latest result to a mapping' do - let(:hook_mapping) { Cucumber::Core::Test::UnskippableAction.new { |last_result| @result_spy = last_result } } - let(:after_hook) { Cucumber::Core::Test::HookStep.new(double, text, double, hook_mapping) } - let(:failing_step) { Cucumber::Core::Test::Step.new(double, text, double).with_action { fail } } - let(:test_steps) { [failing_step, after_hook] } + let(:test_steps) do + hook_mapping = Cucumber::Core::Test::UnskippableAction.new { |last_result| @result_spy = last_result } + after_hook = Cucumber::Core::Test::HookStep.new(double, text, double, hook_mapping) + failing_step = Cucumber::Core::Test::Step.new(double, text, double).with_action { fail } + [failing_step, after_hook] + end it 'passes a Failed result when the scenario is failing' do test_case.describe_to(runner) @@ -254,47 +259,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(double, double, [passing_step], double, double, double, double, [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(double, double, [passing_step], double, double, double, double, [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(double, double, [passing_step], double, double, double, double, [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(double, text, double, double).with_action { raise exception } - test_case = Cucumber::Core::Test::Case.new(double, double, [failing_step], double, double, double, double, [around_hook]) - expect(event_bus).to receive(:test_case_finished).with(test_case, anything) do |_reported_test_case, result| + failing_step = Cucumber::Core::Test::Step.new(double, text, location, location).with_action { raise exception } + 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(double, double, [], double, double, double, double, [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 @@ -304,7 +309,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