Skip to content

Commit

Permalink
Add support for keyword arguments for lanes in Ruby 3 (#21587)
Browse files Browse the repository at this point in the history
* Add tests for lanes using keyword parameters

* Support keyword parameters in lanes in Ruby 3

While that previously worked implicitly in Ruby 2.x (with our internal implementation always passing a `Hash` in `lane.call(…)` but Ruby converting it as keywords dynamically if the block expected keywords), this has to be explicit in Ruby 3.x now.

See https://www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-keyword-arguments-in-ruby-3-0/

* Support Windows using a different error message format

* Fix CI Linux failures and rubocop violations

* Slightly improve code comment

* Slightly tweak unit tests and fix typos

* Improve test description

Co-authored-by: Roger Oba <rogerluan.oba@gmail.com>

* Add test cases for passing nil to keyword parameters

---------

Co-authored-by: Roger Oba <rogerluan.oba@gmail.com>
  • Loading branch information
AliSoftware and rogerluan committed Oct 19, 2023
1 parent 5c63bd5 commit 2c4f29f
Show file tree
Hide file tree
Showing 4 changed files with 113 additions and 2 deletions.
10 changes: 9 additions & 1 deletion fastlane/lib/fastlane/lane.rb
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion fastlane/lib/fastlane/runner.rb
Expand Up @@ -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)
Expand Down
13 changes: 13 additions & 0 deletions 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
90 changes: 90 additions & 0 deletions fastlane/spec/runner_spec.rb
Expand Up @@ -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')
Expand All @@ -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

0 comments on commit 2c4f29f

Please sign in to comment.