From 4f181d388080b70c1bfc7c61ed3224443f0ba4b5 Mon Sep 17 00:00:00 2001 From: Micah Geisel Date: Thu, 5 Oct 2023 12:43:02 +0100 Subject: [PATCH] get something working with new Value base class. --- lib/cucumber/core/compiler.rb | 3 +- lib/cucumber/core/test/case.rb | 19 ++---- lib/cucumber/core/value.rb | 95 ++++++++++++++++++++++++++ spec/cucumber/core/test/case_spec.rb | 16 +++-- spec/cucumber/core/test/runner_spec.rb | 61 ++++++++--------- 5 files changed, 144 insertions(+), 50 deletions(-) create mode 100644 lib/cucumber/core/value.rb diff --git a/lib/cucumber/core/compiler.rb b/lib/cucumber/core/compiler.rb index 0a00b420..cacb962d 100644 --- a/lib/cucumber/core/compiler.rb +++ b/lib/cucumber/core/compiler.rb @@ -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 diff --git a/lib/cucumber/core/test/case.rb b/lib/cucumber/core/test/case.rb index 0270f24b..da7d1020 100644 --- a/lib/cucumber/core/test/case.rb +++ b/lib/cucumber/core/test/case.rb @@ -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 @@ -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) 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 ab01f17c..f06137f6 100644 --- a/spec/cucumber/core/test/case_spec.rb +++ b/spec/cucumber/core/test/case_spec.rb @@ -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 } @@ -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 diff --git a/spec/cucumber/core/test/runner_spec.rb b/spec/cucumber/core/test/runner_spec.rb index 1503a9cc..e9fda727 100644 --- a/spec/cucumber/core/test/runner_spec.rb +++ b/spec/cucumber/core/test/runner_spec.rb @@ -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) @@ -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 @@ -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 @@ -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