screengrab/lib/screengrab/runner.rb in fastlane_hotfix-2.165.1 vs screengrab/lib/screengrab/runner.rb in fastlane_hotfix-2.187.0
- old
+ new
@@ -4,16 +4,10 @@
require_relative 'reports_generator'
require_relative 'module'
module Screengrab
class Runner
- NEEDED_PERMISSIONS = [
- 'android.permission.READ_EXTERNAL_STORAGE',
- 'android.permission.WRITE_EXTERNAL_STORAGE',
- 'android.permission.CHANGE_CONFIGURATION'
- ].freeze
-
attr_accessor :number_of_retries
def initialize(executor = FastlaneCore::CommandExecutor,
config = Screengrab.config,
android_env = Screengrab.android_environment)
@@ -22,10 +16,11 @@
@config = config
@android_env = android_env
end
def run
+ # Standardize the locales
FastlaneCore::PrintTable.print_values(config: @config, hide_keys: [], title: "Summary for screengrab #{Fastlane::VERSION}")
app_apk_path = @config.fetch(:app_apk_path, ask: false)
tests_apk_path = @config.fetch(:tests_apk_path, ask: false)
discovered_apk_paths = Dir[File.join('**', '*.apk')]
@@ -55,29 +50,27 @@
clear_local_previous_screenshots(device_type_dir_name)
device_serial = select_device
device_screenshots_paths = [
- determine_external_screenshots_path(device_serial, @config[:locales]),
- determine_internal_screenshots_paths(@config[:app_package_name], @config[:locales])
- ].flatten
+ determine_external_screenshots_path(device_serial, @config[:app_package_name], @config[:locales]),
+ determine_internal_screenshots_paths(device_serial, @config[:app_package_name], @config[:locales])
+ ].flatten(1)
# Root is needed to access device paths at /data
if @config[:use_adb_root]
- run_adb_command("-s #{device_serial} root", print_all: false, print_command: true)
- run_adb_command("-s #{device_serial} wait-for-device", print_all: false, print_command: true)
+ run_adb_command("-s #{device_serial.shellescape} root", print_all: false, print_command: true)
+ run_adb_command("-s #{device_serial.shellescape} wait-for-device", print_all: false, print_command: true)
end
clear_device_previous_screenshots(@config[:app_package_name], device_serial, device_screenshots_paths)
app_apk_path ||= select_app_apk(discovered_apk_paths)
tests_apk_path ||= select_tests_apk(discovered_apk_paths)
- validate_apk(app_apk_path)
+ number_of_screenshots = run_tests(device_type_dir_name, device_serial, app_apk_path, tests_apk_path, test_classes_to_use, test_packages_to_use, @config[:launch_arguments])
- number_of_screenshots = run_tests(device_type_dir_name, device_screenshots_paths, device_serial, app_apk_path, tests_apk_path, test_classes_to_use, test_packages_to_use, @config[:launch_arguments])
-
ReportsGenerator.new.generate
UI.success("Captured #{number_of_screenshots} new screenshots! 📷✨")
end
@@ -129,149 +122,162 @@
def screenshot_file_names_in(output_directory, device_type)
Dir.glob(File.join(output_directory, '**', device_type, '*.png'), File::FNM_CASEFOLD)
end
- def determine_external_screenshots_path(device_serial, locales)
+ def get_device_environment_variable(device_serial, variable_name)
# macOS evaluates $foo in `echo $foo` before executing the command,
# Windows doesn't - hence the double backslash vs. single backslash
- command = Helper.windows? ? "shell echo \$EXTERNAL_STORAGE " : "shell echo \\$EXTERNAL_STORAGE"
- device_ext_storage = run_adb_command("-s #{device_serial} #{command}",
- print_all: true,
- print_command: true)
- device_ext_storage = device_ext_storage.strip
+ command = Helper.windows? ? "shell echo \$#{variable_name.shellescape.shellescape}" : "shell echo \\$#{variable_name.shellescape.shellescape}"
+ value = run_adb_command("-s #{device_serial.shellescape} #{command}",
+ print_all: true,
+ print_command: true)
+ return value.strip
+ end
+
+ # Don't need to use to use run-as if external
+ def use_adb_run_as?(path, device_serial)
+ device_ext_storage = get_device_environment_variable(device_serial, "EXTERNAL_STORAGE")
+ return !path.start_with?(device_ext_storage)
+ end
+
+ def determine_external_screenshots_path(device_serial, app_package_name, locales)
+ device_ext_storage = get_device_environment_variable(device_serial, "EXTERNAL_STORAGE")
return locales.map do |locale|
- File.join(device_ext_storage, @config[:app_package_name], 'screengrab', locale, "images", "screenshots")
- end.flatten
+ [
+ File.join(device_ext_storage, app_package_name, 'screengrab', locale, "images", "screenshots"),
+ File.join(device_ext_storage, "Android", "data", app_package_name, 'files', 'screengrab', locale, "images", "screenshots")
+ ]
+ end.flatten.map { |path| [path, false] }
end
- def determine_internal_screenshots_paths(app_package_name, locales)
+ def determine_internal_screenshots_paths(device_serial, app_package_name, locales)
+ device_data = get_device_environment_variable(device_serial, "ANDROID_DATA")
return locales.map do |locale|
[
- "/data/user/0/#{app_package_name}/files/#{app_package_name}/screengrab/#{locale}/images/screenshots",
+ "#{device_data}/user/0/#{app_package_name}/files/#{app_package_name}/screengrab/#{locale}/images/screenshots",
# https://github.com/fastlane/fastlane/issues/15653#issuecomment-578541663
- "/data/data/#{app_package_name}/files/#{app_package_name}/screengrab/#{locale}/images/screenshots",
+ "#{device_data}/data/#{app_package_name}/files/#{app_package_name}/screengrab/#{locale}/images/screenshots",
- "/data/data/#{app_package_name}/app_screengrab/#{locale}/images/screenshots",
- "/data/data/#{app_package_name}/screengrab/#{locale}/images/screenshots"
+ "#{device_data}/data/#{app_package_name}/app_screengrab/#{locale}/images/screenshots",
+ "#{device_data}/data/#{app_package_name}/screengrab/#{locale}/images/screenshots"
]
- end.flatten
+ end.flatten.map { |path| [path, true] }
end
def clear_device_previous_screenshots(app_package_name, device_serial, device_screenshots_paths)
UI.message('Cleaning screenshots on device')
- device_screenshots_paths.each do |device_path|
- if_device_path_exists(app_package_name, device_serial, device_path) do |path|
- run_adb_command("-s #{device_serial} shell run-as #{app_package_name} rm -rf #{path}",
+ device_screenshots_paths.each do |(device_path, needs_run_as)|
+ if_device_path_exists(app_package_name, device_serial, device_path, needs_run_as) do |path|
+ # Determine if path needs the run-as permission
+ run_as = needs_run_as ? " run-as #{app_package_name.shellescape.shellescape}" : ""
+
+ run_adb_command("-s #{device_serial.shellescape} shell#{run_as} rm -rf #{path.shellescape.shellescape}",
print_all: true,
print_command: true)
end
end
end
- def validate_apk(app_apk_path)
- unless @android_env.aapt_path
- UI.important("The `aapt` command could not be found on your system, so your app APK could not be validated")
- return
- end
-
- UI.message('Validating app APK')
- apk_permissions = @executor.execute(command: "#{@android_env.aapt_path} dump permissions #{app_apk_path}",
- print_all: true,
- print_command: true)
-
- missing_permissions = NEEDED_PERMISSIONS.reject { |needed| apk_permissions.include?(needed) }
-
- if missing_permissions.any?
- UI.user_error!("The needed permission(s) #{missing_permissions.join(', ')} could not be found in your app APK")
- end
- end
-
def install_apks(device_serial, app_apk_path, tests_apk_path)
UI.message('Installing app APK')
- apk_install_output = run_adb_command("-s #{device_serial} install -t -r #{app_apk_path.shellescape}",
+ apk_install_output = run_adb_command("-s #{device_serial.shellescape} install -t -r #{app_apk_path.shellescape}",
print_all: true,
print_command: true)
UI.user_error!("App APK could not be installed") if apk_install_output.include?("Failure [")
UI.message('Installing tests APK')
- apk_install_output = run_adb_command("-s #{device_serial} install -t -r #{tests_apk_path.shellescape}",
+ apk_install_output = run_adb_command("-s #{device_serial.shellescape} install -t -r #{tests_apk_path.shellescape}",
print_all: true,
print_command: true)
UI.user_error!("Tests APK could not be installed") if apk_install_output.include?("Failure [")
end
def uninstall_apks(device_serial, app_package_name, tests_package_name)
packages = installed_packages(device_serial)
if packages.include?(app_package_name.to_s)
UI.message('Uninstalling app APK')
- run_adb_command("-s #{device_serial} uninstall #{app_package_name}",
+ run_adb_command("-s #{device_serial.shellescape} uninstall #{app_package_name.shellescape}",
print_all: true,
print_command: true)
end
if packages.include?(tests_package_name.to_s)
UI.message('Uninstalling tests APK')
- run_adb_command("-s #{device_serial} uninstall #{tests_package_name}",
+ run_adb_command("-s #{device_serial.shellescape} uninstall #{tests_package_name.shellescape}",
print_all: true,
print_command: true)
end
end
def grant_permissions(device_serial)
UI.message('Granting the permission necessary to change locales on the device')
- run_adb_command("-s #{device_serial} shell pm grant #{@config[:app_package_name]} android.permission.CHANGE_CONFIGURATION",
+ run_adb_command("-s #{device_serial.shellescape} shell pm grant #{@config[:app_package_name].shellescape.shellescape} android.permission.CHANGE_CONFIGURATION",
print_all: true,
- print_command: true)
+ print_command: true,
+ raise_errors: false)
- if device_api_version(device_serial) >= 23
- UI.message('Granting the permissions necessary to access device external storage')
- run_adb_command("-s #{device_serial} shell pm grant #{@config[:app_package_name]} android.permission.WRITE_EXTERNAL_STORAGE",
- print_all: true,
- print_command: true)
- run_adb_command("-s #{device_serial} shell pm grant #{@config[:app_package_name]} android.permission.READ_EXTERNAL_STORAGE",
- print_all: true,
- print_command: true)
- end
+ UI.message('Granting the permissions necessary to access device external storage')
+ run_adb_command("-s #{device_serial.shellescape} shell pm grant #{@config[:app_package_name].shellescape.shellescape} android.permission.WRITE_EXTERNAL_STORAGE",
+ print_all: true,
+ print_command: true,
+ raise_errors: false)
+ run_adb_command("-s #{device_serial.shellescape} shell pm grant #{@config[:app_package_name].shellescape.shellescape} android.permission.READ_EXTERNAL_STORAGE",
+ print_all: true,
+ print_command: true,
+ raise_errors: false)
end
- def run_tests(device_type_dir_name, device_screenshots_paths, device_serial, app_apk_path, tests_apk_path, test_classes_to_use, test_packages_to_use, launch_arguments)
+ def kill_app(device_serial, package_name)
+ run_adb_command("-s #{device_serial.shellescape} shell am force-stop #{package_name.shellescape.shellescape}.test",
+ print_all: true,
+ print_command: true)
+ run_adb_command("-s #{device_serial.shellescape} shell am force-stop #{package_name.shellescape.shellescape}",
+ print_all: true,
+ print_command: true)
+ end
+
+ def run_tests(device_type_dir_name, device_serial, app_apk_path, tests_apk_path, test_classes_to_use, test_packages_to_use, launch_arguments)
+ sdk_version = device_api_version(device_serial)
unless @config[:reinstall_app]
install_apks(device_serial, app_apk_path, tests_apk_path)
grant_permissions(device_serial)
- enable_clean_status_bar(device_serial, app_apk_path)
+ enable_clean_status_bar(device_serial, sdk_version)
end
number_of_screenshots = 0
@config[:locales].each do |locale|
if @config[:reinstall_app]
uninstall_apks(device_serial, @config[:app_package_name], @config[:tests_package_name])
install_apks(device_serial, app_apk_path, tests_apk_path)
grant_permissions(device_serial)
- enable_clean_status_bar(device_serial, app_apk_path)
+ else
+ kill_app(device_serial, @config[:app_package_name])
end
- number_of_screenshots += run_tests_for_locale(device_type_dir_name, device_screenshots_paths, locale, device_serial, test_classes_to_use, test_packages_to_use, launch_arguments)
+ number_of_screenshots += run_tests_for_locale(device_type_dir_name, locale, device_serial, test_classes_to_use, test_packages_to_use, launch_arguments, sdk_version)
end
number_of_screenshots
end
- def run_tests_for_locale(device_type_dir_name, device_screenshots_paths, locale, device_serial, test_classes_to_use, test_packages_to_use, launch_arguments)
+ def run_tests_for_locale(device_type_dir_name, locale, device_serial, test_classes_to_use, test_packages_to_use, launch_arguments, sdk_version)
UI.message("Running tests for locale: #{locale}")
- instrument_command = ["-s #{device_serial} shell am instrument --no-window-animation -w",
- "-e testLocale #{locale.tr('-', '_')}",
- "-e endingLocale #{@config[:ending_locale].tr('-', '_')}"]
+ instrument_command = ["-s #{device_serial.shellescape} shell am instrument --no-window-animation -w",
+ "-e testLocale #{locale.shellescape.shellescape}"]
+ if sdk_version >= 28
+ instrument_command << "--no-hidden-api-checks"
+ end
instrument_command << "-e appendTimestamp #{@config[:use_timestamp_suffix]}"
- instrument_command << "-e class #{test_classes_to_use.join(',')}" if test_classes_to_use
- instrument_command << "-e package #{test_packages_to_use.join(',')}" if test_packages_to_use
+ instrument_command << "-e class #{test_classes_to_use.join(',').shellescape.shellescape}" if test_classes_to_use
+ instrument_command << "-e package #{test_packages_to_use.join(',').shellescape.shellescape}" if test_packages_to_use
instrument_command << launch_arguments.map { |item| '-e ' + item }.join(' ') if launch_arguments
- instrument_command << "#{@config[:tests_package_name]}/#{@config[:test_instrumentation_runner]}"
+ instrument_command << "#{@config[:tests_package_name].shellescape.shellescape}/#{@config[:test_instrumentation_runner].shellescape.shellescape}"
test_output = run_adb_command(instrument_command.join(" \\\n"),
print_all: true,
print_command: true)
@@ -281,34 +287,44 @@
else
UI.error("Tests failed")
end
end
- pull_screenshots_from_device(locale, device_serial, device_screenshots_paths, device_type_dir_name)
+ pull_screenshots_from_device(locale, device_serial, device_type_dir_name)
end
- def pull_screenshots_from_device(locale, device_serial, device_screenshots_paths, device_type_dir_name)
+ def pull_screenshots_from_device(locale, device_serial, device_type_dir_name)
UI.message("Pulling captured screenshots for locale #{locale} from the device")
starting_screenshot_count = screenshot_file_names_in(@config[:output_directory], device_type_dir_name).length
UI.verbose("Starting screenshot count is: #{starting_screenshot_count}")
+ device_screenshots_paths = [
+ determine_external_screenshots_path(device_serial, @config[:app_package_name], [locale]),
+ determine_internal_screenshots_paths(device_serial, @config[:app_package_name], [locale])
+ ].flatten(1)
+
# Make a temp directory into which to pull the screenshots before they are moved to their final location.
# This makes directory cleanup easier, as the temp directory will be removed when the block completes.
Dir.mktmpdir do |tempdir|
- device_screenshots_paths.each do |device_path|
- if_device_path_exists(@config[:app_package_name], device_serial, device_path) do |path|
+ device_screenshots_paths.each do |(device_path, needs_run_as)|
+ if_device_path_exists(@config[:app_package_name], device_serial, device_path, needs_run_as) do |path|
+ UI.message(path)
next unless path.include?(locale)
- out = run_adb_command("-s #{device_serial} pull #{path} #{tempdir}",
+ out = run_adb_command("-s #{device_serial.shellescape} pull #{path.shellescape} #{tempdir.shellescape}",
print_all: false,
print_command: true,
raise_errors: false)
if out =~ /Permission denied/
dir = File.dirname(path)
base = File.basename(path)
- run_adb_command("-s #{device_serial} shell run-as #{@config[:app_package_name]} 'tar -cC #{dir} #{base}' | tar -xvC #{tempdir}",
+
+ # Determine if path needs the run-as permission
+ run_as = needs_run_as ? " run-as #{@config[:app_package_name].shellescape.shellescape}" : ""
+
+ run_adb_command("-s #{device_serial.shellescape} shell#{run_as} \"tar -cC #{dir} #{base}\" | tar -xv -f- -C #{tempdir}",
print_all: false,
print_command: true)
end
end
end
@@ -368,12 +384,15 @@
end
end
# Some device commands fail if executed against a device path that does not exist, so this helper method
# provides a way to conditionally execute a block only if the provided path exists on the device.
- def if_device_path_exists(app_package_name, device_serial, device_path)
- return if run_adb_command("-s #{device_serial} shell run-as #{app_package_name} ls #{device_path}",
+ def if_device_path_exists(app_package_name, device_serial, device_path, needs_run_as)
+ # Determine if path needs the run-as permission
+ run_as = needs_run_as ? " run-as #{app_package_name.shellescape.shellescape}" : ""
+
+ return if run_adb_command("-s #{device_serial.shellescape} shell#{run_as} ls #{device_path.shellescape.shellescape}",
print_all: false,
print_command: false).include?('No such file')
yield(device_path)
rescue
@@ -381,24 +400,23 @@
# We can safely ignore that and treat it as if it returned 'No such file'
end
# Return an array of packages that are installed on the device
def installed_packages(device_serial)
- packages = run_adb_command("-s #{device_serial} shell pm list packages",
+ packages = run_adb_command("-s #{device_serial.shellescape} shell pm list packages",
print_all: true,
print_command: true)
packages.split("\n").map { |package| package.gsub("package:", "") }
end
def run_adb_command(command, print_all: false, print_command: false, raise_errors: true)
- adb_path = @android_env.adb_path.chomp("adb")
adb_host = @config[:adb_host]
- host = adb_host.nil? ? '' : "-H #{adb_host} "
+ host = adb_host.nil? ? '' : "-H #{adb_host.shellescape} "
output = ''
begin
errout = nil
- cmdout = @executor.execute(command: adb_path + "adb " + host + command,
+ cmdout = @executor.execute(command: @android_env.adb_path + " " + host + command,
print_all: print_all,
print_command: print_command,
error: raise_errors ? nil : proc { |out, status| errout = out }) || ''
output = errout || cmdout
rescue => ex
@@ -411,39 +429,24 @@
line.start_with?('adb: ') && !line.start_with?('adb: error: ')
end.join('') # Lines retain their newline chars
end
def device_api_version(device_serial)
- run_adb_command("-s #{device_serial} shell getprop ro.build.version.sdk",
+ run_adb_command("-s #{device_serial.shellescape} shell getprop ro.build.version.sdk",
print_all: true, print_command: true).to_i
end
- def enable_clean_status_bar(device_serial, app_apk_path)
- return unless device_api_version(device_serial) >= 23
+ def enable_clean_status_bar(device_serial, sdk_version)
+ return unless sdk_version >= 23
- unless @android_env.aapt_path
- UI.error("The `aapt` command could not be found, so status bar could not be cleaned. Make sure android_home is configured for screengrab or ANDROID_HOME is set in the environment")
- return
- end
-
- # Check if the app wants to use the clean status bar feature
- badging_dump = @executor.execute(command: "#{@android_env.aapt_path} dump badging #{app_apk_path}",
- print_all: true, print_command: true)
- return unless badging_dump.include?('uses-feature: name=\'tools.fastlane.screengrab.cleanstatusbar\'')
-
UI.message('Enabling clean status bar')
- # Make sure the app requests the DUMP permission
- unless badging_dump.include?('uses-permission: name=\'android.permission.DUMP\'')
- UI.user_error!("The clean status bar feature requires the android.permission.DUMP permission but it could not be found in your app APK")
- end
-
# Grant the DUMP permission
- run_adb_command("-s #{device_serial} shell pm grant #{@config[:app_package_name]} android.permission.DUMP",
- print_all: true, print_command: true)
+ run_adb_command("-s #{device_serial.shellescape} shell pm grant #{@config[:app_package_name].shellescape.shellescape} android.permission.DUMP",
+ print_all: true, print_command: true, raise_errors: false)
# Enable the SystemUI demo mode
- run_adb_command("-s #{device_serial} shell settings put global sysui_demo_allowed 1",
+ run_adb_command("-s #{device_serial.shellescape} shell settings put global sysui_demo_allowed 1",
print_all: true, print_command: true)
end
end
end