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