-
Notifications
You must be signed in to change notification settings - Fork 9
Add Firebase Test Lab Support #355
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 8 commits
bd7d602
f866e66
848c0ae
928613f
c52bcff
e49b930
66f3e65
372ad20
0deed47
77e180f
5ac5225
c659f02
23870c0
7ac5002
9dc580e
ff89bd0
d7b73c5
c6a8f2c
8e16c7b
14eed21
bd7395e
3267537
b99126a
1c1a8bc
8ab2031
9ba5d34
5d15b3a
85b0df0
5b46778
fdb177c
839be18
f1e3128
d9536de
08bc2c0
7826e23
1b789ce
46e3825
913578a
20ae178
febebef
75a8d41
8e7c274
fb136d0
6db3296
a660c60
495b038
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -37,6 +37,10 @@ Gem::Specification.new do |spec| | |
| spec.add_dependency 'parallel', '~> 1.14' | ||
| spec.add_dependency 'chroma', '0.2.0' | ||
| spec.add_dependency 'activesupport', '~> 5' | ||
|
|
||
| # `google-cloud-storage` is required by fastlane, but we pin it in case it's not in the future | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
| spec.add_dependency 'google-cloud-storage', '~> 1' | ||
AliSoftware marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| # Some of the upstream code uses `BigDecimal.new` which version 2.0 of the | ||
| # `bigdecimal` gem removed. Until we'll find the time to identify the | ||
| # dependencies and see if we can move them to something compatible with | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,186 @@ | ||
| require 'securerandom' | ||
|
|
||
| module Fastlane | ||
| module Actions | ||
| module SharedValues | ||
| FIREBASE_TEST_RESULT = :FIREBASE_TEST_LOG_FILE | ||
| FIREBASE_TEST_LOG_FILE_PATH = :FIREBASE_TEST_LOG_FILE_PATH | ||
| end | ||
|
|
||
| class AndroidFirebaseTestAction < Action | ||
| def self.run(params) | ||
| # Preflight – ensure the system is set up correctly | ||
| Fastlane::FirebaseTestRunner.verify_has_gcloud_binary | ||
|
|
||
| # Log in to Firebase (and validate credentials) | ||
| test_runner = Fastlane::FirebaseTestRunner.new(key_file: params[:key_file]) | ||
AliSoftware marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| # Set up the log file and output directory | ||
| FileUtils.mkdir_p(params[:results_output_dir]) | ||
| Fastlane::Actions.lane_context[:FIREBASE_TEST_LOG_FILE_PATH] = File.join(params[:results_output_dir], 'output.log') | ||
|
|
||
| device = Fastlane::FirebaseDevice.new( | ||
| model: params[:model], | ||
| version: params[:version], | ||
| locale: params[:locale], | ||
| orientation: params[:orientation] | ||
| ) | ||
|
|
||
| result = test_runner.run_tests( | ||
| apk_path: params[:apk_path], | ||
| test_apk_path: params[:test_apk_path], | ||
| device: device, | ||
| type: params[:type] | ||
| ) | ||
|
|
||
| # Download all of the outputs from the job to the local machine | ||
| test_runner.download_result_files( | ||
| result: result, | ||
| destination: params[:results_output_dir], | ||
| project_id: params[:project_id], | ||
| key_file_path: params[:key_file] | ||
| ) | ||
|
|
||
| FastlaneCore::UI.test_failure! "Firebase Tests failed – more information can be found at #{result.more_details_url}" unless result.success? | ||
|
|
||
| UI.success 'Firebase Tests Complete' | ||
| end | ||
|
|
||
| ##################################################### | ||
| # @!group Documentation | ||
| ##################################################### | ||
|
|
||
| def self.description | ||
| 'Runs the specified tests in Firebase Test Lab' | ||
| end | ||
|
|
||
| def self.details | ||
| description | ||
| end | ||
|
|
||
| def self.available_options | ||
| run_uuid = SecureRandom.uuid | ||
|
|
||
| [ | ||
| FastlaneCore::ConfigItem.new( | ||
| key: :project_id, | ||
| env_name: 'GCP_PROJECT', | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm assuming there is a rationale behind choosing that specific If that's the rationale (this env var name being some standard used by other stuff as well), then you should probably add a If it's not (i.e. it's just that the client app you want to test this action with first happens to have that env var set, but for legacy reasons with some old tooling which won't be relevant anymore, and thus no hard reason to keep that name), you might as well change this to try to follow our usual naming conventions of the env var (even if that means that you'll explicitly pass
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Addressed in d9536de |
||
| description: 'The Project ID to test in', | ||
| type: String | ||
| ), | ||
| FastlaneCore::ConfigItem.new( | ||
| key: :key_file, | ||
| env_name: 'GOOGLE_APPLICATION_CREDENTIALS', | ||
|
||
| description: 'The key file used to authorize with Google Cloud', | ||
| type: String, | ||
| verify_block: proc do |value| | ||
| next if File.file? value | ||
|
|
||
| UI.user_error!("Invalid key file path: #{value}") | ||
jkmassel marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| end | ||
| ), | ||
| FastlaneCore::ConfigItem.new( | ||
| key: :apk_path, | ||
| description: 'The application APK', | ||
|
||
| type: String, | ||
| verify_block: proc do |value| | ||
| next if File.file? value | ||
|
|
||
| UI.user_error!("Invalid application APK: #{value}") | ||
jkmassel marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| end | ||
| ), | ||
| FastlaneCore::ConfigItem.new( | ||
| key: :test_apk_path, | ||
| description: 'The test APK', | ||
jkmassel marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| type: String, | ||
| verify_block: proc do |value| | ||
| next if File.file? value | ||
|
|
||
| UI.user_error!("Invalid test APK: #{value}") | ||
jkmassel marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| end | ||
| ), | ||
| FastlaneCore::ConfigItem.new( | ||
| key: :model, | ||
| description: 'The device model to run', | ||
jkmassel marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| type: String, | ||
| verify_block: proc do |value| | ||
| model_names = Fastlane::FirebaseDevice.valid_model_names | ||
| next if model_names.include? value | ||
|
|
||
| UI.user_error!("Invalid Model Name: #{value}. Valid Model Names: #{model_names}") | ||
jkmassel marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| end | ||
| ), | ||
| FastlaneCore::ConfigItem.new( | ||
| key: :version, | ||
| description: 'The device version to run', | ||
jkmassel marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| type: Integer, | ||
| verify_block: proc do |value| | ||
| version_numbers = Fastlane::FirebaseDevice.valid_version_numbers | ||
| next if version_numbers.include? value | ||
|
|
||
| UI.user_error!("Invalid Version Number: #{value}. Valid Verison Numbers: #{version_numbers}") | ||
jkmassel marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| end | ||
| ), | ||
| FastlaneCore::ConfigItem.new( | ||
| key: :locale, | ||
| description: 'The locale code to run in', | ||
jkmassel marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| type: String, | ||
| default_value: 'en', | ||
| verify_block: proc do |value| | ||
| locale_codes = Fastlane::FirebaseDevice.valid_locales | ||
| next if locale_codes.include? value | ||
|
|
||
| UI.user_error!("Invalid Locale: #{value}. Valid Locales: #{locale_codes}") | ||
jkmassel marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| end | ||
| ), | ||
| FastlaneCore::ConfigItem.new( | ||
| key: :orientation, | ||
| description: 'Which orientation to run the device in', | ||
| type: String, | ||
| default_value: 'portrait', | ||
| verify_block: proc do |value| | ||
| orientations = Fastlane::FirebaseDevice.valid_orientations | ||
| next if orientations.include? value | ||
|
|
||
| UI.user_error!("Invalid Orientation: #{value}. Valid Orientations: #{orientations}") | ||
jkmassel marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| end | ||
| ), | ||
| FastlaneCore::ConfigItem.new( | ||
| key: :type, | ||
| description: 'Which type of test are we running?', | ||
jkmassel marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| type: String, | ||
| default_value: 'instrumentation', | ||
| verify_block: proc do |value| | ||
| types = Fastlane::FirebaseTestRunner::VALID_TEST_TYPES | ||
| next if types.include? value | ||
|
|
||
| UI.user_error!("Invalid Test Type: #{value}. Valid Types: #{types}") | ||
jkmassel marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| end | ||
| ), | ||
| FastlaneCore::ConfigItem.new( | ||
| key: :test_run_id, | ||
| description: 'A unique ID used to identify this test run', | ||
| type: String, | ||
| default_value: run_uuid, | ||
|
||
| default_value_dynamic: true | ||
| ), | ||
| FastlaneCore::ConfigItem.new( | ||
| key: :results_output_dir, | ||
| description: 'Where should we store the results of this test run?', | ||
jkmassel marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| type: String, | ||
| default_value: File.join(Dir.tmpdir(), run_uuid), | ||
| default_value_dynamic: true | ||
| ), | ||
| ] | ||
| end | ||
|
|
||
| def self.authors | ||
| ['Automattic'] | ||
| end | ||
|
|
||
| def self.is_supported?(platform) | ||
| platform == :android | ||
| end | ||
| end | ||
| end | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| module Fastlane | ||
| class FirebaseDevice | ||
| attr_reader :model, :version, :locale, :orientation | ||
|
Comment on lines
+1
to
+3
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 I really like that this was represented as a model class. We don't have many model classes in our I'm not a fan of this being defined directly at the root of the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, I worried about this for a moment as well – IMHO if that happens at some point we can address it then? I didn't want to bury it too deeply since it's a pain to reference a deeply-nested model object?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed. I think we could also keep the idea in the back of our head for later to introduce a I worried about it for this old WIP as well, which might be even more at risk of clashing with something provided by fastlane given its more generic name… so maybe at that point create that
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yeah exactly – this was what had me thinking it'd make sense to leave it for the moment 🙂
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One thing we could consider at least doing though: put all the Firebase-related models under a Since those are models that all go together hand in hand that would make it clear and neat, and would make us ready for if we add more, unrelated model files for other unrelated things in the future, avoinding to put everything in the same bowl.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note that:
|
||
|
|
||
| @@locale_data = nil | ||
jkmassel marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| @@model_data = nil | ||
jkmassel marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| @@version_data = nil | ||
jkmassel marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| def initialize(model:, version:, orientation:, locale: 'en') | ||
| raise 'Invalid Model' unless FirebaseDevice.valid_model_names.include? model | ||
| raise 'Invalid Version' unless FirebaseDevice.valid_version_numbers.include? version | ||
| raise 'Invalid Locale' unless FirebaseDevice.valid_locales.include? locale | ||
| raise 'Invalid Orientation' unless FirebaseDevice.valid_orientations.include? orientation | ||
|
|
||
| @model = model | ||
| @version = version | ||
| @locale = locale | ||
| @orientation = orientation | ||
| end | ||
|
|
||
| def to_s | ||
| "model=#{@model},version=#{@version},locale=#{@locale},orientation=#{@orientation}" | ||
| end | ||
|
|
||
| def self.valid_model_names | ||
| JSON.parse(model_data).map { |device| device['codename'] } | ||
| end | ||
|
|
||
| def self.valid_version_numbers | ||
| JSON.parse(version_data).map { |version| version['apiLevel'].to_i } | ||
| end | ||
|
|
||
| def self.valid_locales | ||
| JSON.parse(locale_data).map { |locale| locale['id'] } | ||
| end | ||
|
|
||
| def self.valid_orientations | ||
| %w[portrait landscape] | ||
| end | ||
|
|
||
| def self.locale_data | ||
| @@locale_data || Fastlane::Actions.sh('gcloud firebase test android locales list --format="json"', log: false) | ||
| end | ||
|
|
||
| def self.model_data | ||
| @@model_data || Fastlane::Actions.sh('gcloud firebase test android models list --format="json"', log: false) | ||
| end | ||
|
|
||
| def self.version_data | ||
| @@version_data || Fastlane::Actions.sh('gcloud firebase test android versions list --format="json"', log: false) | ||
| end | ||
| end | ||
| end | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,42 @@ | ||||||
| module Fastlane | ||||||
| class FirebaseTestLabResult | ||||||
| def initialize(log_file_path:) | ||||||
| raise "No log file found at path #{log_file_path}" unless File.file? log_file_path | ||||||
|
|
||||||
| @path = log_file_path | ||||||
| end | ||||||
|
|
||||||
| # Scan the log file to for indications that the Test Run failed | ||||||
jkmassel marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||
| def success? | ||||||
| File.readlines(@path).none? { |line| !line.include? 'Failed' } | ||||||
|
||||||
| File.readlines(@path).none? { |line| !line.include? 'Failed' } | |
| File.readlines(@path).none? { |line| line.include? 'Failed' } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cc @oguzkocer FYI as you're testing this PR with a real-world usage in a client app, and this code/issue might lead to you seeing results opposite to what you'd expect
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Excellent catch there – this is addressed in ff89bd0.
jkmassel marked this conversation as resolved.
Show resolved
Hide resolved
jkmassel marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm also surprised here that rubocop doesn't complain about that return being useless (Ruby has implicit return)…
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Definitely weird, agreed!
Uh oh!
There was an error while loading. Please reload this page.