diff --git a/fastlane/lib/fastlane/lane.rb b/fastlane/lib/fastlane/lane.rb index 9389733def8..4552c303f1a 100644 --- a/fastlane/lib/fastlane/lane.rb +++ b/fastlane/lib/fastlane/lane.rb @@ -24,11 +24,19 @@ def initialize(platform: nil, name: nil, description: nil, block: nil, is_privat self.platform = platform self.name = name self.description = description - self.block = block + # We want to support _both_ lanes expecting a `Hash` (like `lane :foo do |options|`), and lanes expecting + # keyword parameters (like `lane :foo do |param1:, param2:, param3: 'default value'|`) + block_expects_keywords = !block.nil? && block.parameters.any? { |type, _| [:key, :keyreq].include?(type) } + # Conversion of the `Hash` parameters (passed by `Lane#call`) into keywords has to be explicit in Ruby 3 + # https://www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-keyword-arguments-in-ruby-3-0/ + self.block = block_expects_keywords ? proc { |options| block.call(**options) } : block self.is_private = is_private end # Execute this lane + # + # @param [Hash] parameters The Hash of parameters to pass to the lane + # def call(parameters) block.call(parameters || {}) end diff --git a/fastlane/lib/fastlane/runner.rb b/fastlane/lib/fastlane/runner.rb index 42bbc121c5e..bb0ea7d3f8d 100644 --- a/fastlane/lib/fastlane/runner.rb +++ b/fastlane/lib/fastlane/runner.rb @@ -15,7 +15,7 @@ def full_lane_name # This will take care of executing **one** lane. That's when the user triggers a lane from the CLI for example # This method is **not** executed when switching a lane - # @param lane_name The name of the lane to execute + # @param lane The name of the lane to execute # @param platform The name of the platform to execute # @param parameters [Hash] The parameters passed from the command line to the lane def execute(lane, platform = nil, parameters = nil) diff --git a/fastlane/spec/fixtures/fastfiles/FastfileLaneKeywordParams b/fastlane/spec/fixtures/fastfiles/FastfileLaneKeywordParams new file mode 100644 index 00000000000..d46bf14eb37 --- /dev/null +++ b/fastlane/spec/fixtures/fastfiles/FastfileLaneKeywordParams @@ -0,0 +1,13 @@ +platform :ios do + lane :lane_no_param do + 'No parameter' + end + + lane :lane_hash_param do |options| + "name: #{options[:name].inspect}; version: #{options[:version].inspect}; interactive: #{options[:interactive].inspect}" + end + + lane :lane_kw_params do |name:, version:, interactive: true| + "name: #{name.inspect}; version: #{version.inspect}; interactive: #{interactive.inspect}" + end +end diff --git a/fastlane/spec/runner_spec.rb b/fastlane/spec/runner_spec.rb index a03c511d279..0cd68c25093 100644 --- a/fastlane/spec/runner_spec.rb +++ b/fastlane/spec/runner_spec.rb @@ -20,6 +20,7 @@ it "doesn't show private lanes" do expect(@ff.runner.available_lanes).to_not(include('android such_private')) end + describe "step_name override" do it "handle overriding of step_name" do allow(Fastlane::Actions).to receive(:execute_action).with('Let it Frame') @@ -32,5 +33,94 @@ end end end + + describe "#execute" do + before do + @ff = Fastlane::FastFile.new('./fastlane/spec/fixtures/fastfiles/FastfileLaneKeywordParams') + end + + context 'when a lane does not expect any parameter' do + it 'accepts calling the lane with no parameter' do + result = @ff.runner.execute(:lane_no_param, :ios) + expect(result).to eq('No parameter') + end + + it 'accepts calling the lane with arbitrary (unused) parameter' do + result = @ff.runner.execute(:lane_no_param, :ios, { unused1: 42, unused2: true }) + expect(result).to eq('No parameter') + end + end + + context 'when a lane expects its parameters as a Hash' do + it 'accepts calling the lane with no parameter at all' do + result = @ff.runner.execute(:lane_hash_param, :ios) + expect(result).to eq('name: nil; version: nil; interactive: nil') + end + + it 'accepts calling the lane with less parameters than used by the lane' do + result = @ff.runner.execute(:lane_hash_param, :ios, { version: '12.3' }) + expect(result).to eq('name: nil; version: "12.3"; interactive: nil') + end + + it 'accepts calling the lane with more parameters than used by the lane' do + result = @ff.runner.execute(:lane_hash_param, :ios, { name: 'test', version: '12.3', interactive: true, unused: 42 }) + expect(result).to eq('name: "test"; version: "12.3"; interactive: true') + end + end + + context 'when a lane expects its parameters as keywords' do + def keywords_list(list) + # The way keyword lists appear in error messages is different in Windows & Linux vs macOS + if FastlaneCore::Helper.windows? || FastlaneCore::Helper.linux? + list.map(&:to_s).join(', ') # On Windows and Linux, keyword names don't show the `:` prefix in error messages + else + list.map(&:inspect).join(', ') # On other platforms, they do have `:` in front or keyword names + end + end + + it 'fails when calling the lane with required parameters not being passed' do + expect do + @ff.runner.execute(:lane_kw_params, :ios) + end.to raise_error(ArgumentError, "missing keywords: #{keywords_list(%i[name version])}") + end + + it 'fails when calling the lane with some missing parameters' do + expect do + @ff.runner.execute(:lane_kw_params, :ios, { name: 'test', interactive: true }) + end.to raise_error(ArgumentError, "missing keyword: #{keywords_list(%i[version])}") + end + + it 'fails when calling the lane with extra parameters' do + expect do + @ff.runner.execute(:lane_kw_params, :ios, { name: 'test', version: '12.3', interactive: true, unexpected: 42 }) + end.to raise_error(ArgumentError, "unknown keyword: #{keywords_list(%i[unexpected])}") + end + + it 'takes all parameters into account when all are passed explicitly' do + result = @ff.runner.execute(:lane_kw_params, :ios, { name: 'test', version: "12.3", interactive: false }) + expect(result).to eq('name: "test"; version: "12.3"; interactive: false') + end + + it 'uses default values of parameters not provided explicitly' do + result = @ff.runner.execute(:lane_kw_params, :ios, { name: 'test', version: "12.3" }) + expect(result).to eq('name: "test"; version: "12.3"; interactive: true') + end + + it 'allows parameters to be provided in arbitrary order' do + result = @ff.runner.execute(:lane_kw_params, :ios, { version: "12.3", interactive: true, name: 'test' }) + expect(result).to eq('name: "test"; version: "12.3"; interactive: true') + end + + it 'allows a required parameter to receive a nil value' do + result = @ff.runner.execute(:lane_kw_params, :ios, { name: nil, version: "12.3", interactive: true }) + expect(result).to eq('name: nil; version: "12.3"; interactive: true') + end + + it 'allows a default value to be overridden with a nil value' do + result = @ff.runner.execute(:lane_kw_params, :ios, { name: 'test', version: "12.3", interactive: nil }) + expect(result).to eq('name: "test"; version: "12.3"; interactive: nil') + end + end + end end end