diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..b912060 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,8 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + time: "08:00" + timezone: "America/New_York" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 56cf580..f3366dd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,8 +7,8 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/cache@v1 + - uses: actions/checkout@v4 + - uses: actions/cache@v2 with: path: vendor/bundle key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile') }} @@ -17,13 +17,13 @@ jobs: - name: Set up Ruby uses: ruby/setup-ruby@v1 with: - ruby-version: 2.5 + ruby-version: 2.7 - name: Install dependencies run: bundle check || bundle install --jobs=4 --retry=3 --path vendor/bundle - name: Run tests run: bundle exec rake - name: Upload artifact - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: test-results path: test-results diff --git a/Gemfile b/Gemfile index d27ba53..5c170af 100644 --- a/Gemfile +++ b/Gemfile @@ -1,27 +1,17 @@ source('https://rubygems.org') -# Provides a consistent environment for Ruby projects by tracking and installing exact gem versions. -gem 'bundler' -# Automation tool for mobile developers. +gemspec + +# Add development dependencies here gem 'fastlane', '>= 2.225.0' -# Provides an interactive debugging environment for Ruby. gem 'pry' -# A simple task automation tool. gem 'rake' -# Behavior-driven testing tool for Ruby. gem 'rspec' -# Formatter for RSpec to generate JUnit compatible reports. gem 'rspec_junit_formatter' -# A Ruby static code analyzer and formatter. gem 'rubocop', '1.50.2' -# A collection of RuboCop cops for performance optimizations. gem 'rubocop-performance' -# A RuboCop extension focused on enforcing tools. gem 'rubocop-require_tools' -# SimpleCov is a code coverage analysis tool for Ruby. gem 'simplecov' -gemspec - plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') eval_gemfile(plugins_path) if File.exist?(plugins_path) diff --git a/fastlane-plugin-maestro_orchestration.gemspec b/fastlane-plugin-maestro_orchestration.gemspec index 8e450a6..37932f3 100644 --- a/fastlane-plugin-maestro_orchestration.gemspec +++ b/fastlane-plugin-maestro_orchestration.gemspec @@ -19,6 +19,6 @@ Gem::Specification.new do |spec| # Don't add a dependency to fastlane or fastlane_re # since this would cause a circular dependency - # spec.add_dependency 'your-dependency', '~> 1.0.0' + spec.add_dependency('fastlane-plugin-android_emulator', '~> 1.2', '>= 1.2.1') end diff --git a/fastlane/Pluginfile b/fastlane/Pluginfile deleted file mode 100644 index e0576b0..0000000 --- a/fastlane/Pluginfile +++ /dev/null @@ -1 +0,0 @@ -# Autogenerated by fastlane diff --git a/lib/fastlane/plugin/maestro_orchestration/actions/maestro_orchestration_action.rb b/lib/fastlane/plugin/maestro_orchestration/actions/maestro_orchestration_action.rb deleted file mode 100644 index cd23b39..0000000 --- a/lib/fastlane/plugin/maestro_orchestration/actions/maestro_orchestration_action.rb +++ /dev/null @@ -1,47 +0,0 @@ -require 'fastlane/action' -require_relative '../helper/maestro_orchestration_helper' - -module Fastlane - module Actions - class MaestroOrchestrationAction < Action - def self.run(params) - UI.message("The maestro_orchestration plugin is working!") - end - - def self.description - "Plugin for maestro testing framework." - end - - def self.authors - ["Nemanja Risteski"] - end - - def self.return_value - # If your method provides a return value, you can describe here what it does - end - - def self.details - # Optional: - "" - end - - def self.available_options - [ - # FastlaneCore::ConfigItem.new(key: :your_option, - # env_name: "MAESTRO_ORCHESTRATION_YOUR_OPTION", - # description: "A description of your option", - # optional: false, - # type: String) - ] - end - - def self.is_supported?(platform) - # Adjust this if your plugin only works for a particular platform (iOS vs. Android, for example) - # See: https://docs.fastlane.tools/advanced/#control-configuration-by-lane-and-by-platform - # - # [:ios, :mac, :android].include?(platform) - true - end - end - end -end diff --git a/lib/fastlane/plugin/maestro_orchestration/actions/maestro_orchestration_android_action.rb b/lib/fastlane/plugin/maestro_orchestration/actions/maestro_orchestration_android_action.rb new file mode 100644 index 0000000..7c4a6e0 --- /dev/null +++ b/lib/fastlane/plugin/maestro_orchestration/actions/maestro_orchestration_android_action.rb @@ -0,0 +1,140 @@ +require 'fastlane/action' +require 'fastlane_core/configuration/config_item' +require 'fastlane/plugin/android_emulator' +require_relative '../helper/maestro_orchestration_helper' + +module Fastlane + module Actions + class MaestroOrchestrationAndroidAction < Action + def self.run(params) + required_params = [:emulator_package, :emulator_device, :maestro_flow_file] + missing_params = required_params.select { |param| params[param].nil? } + + if missing_params.any? + missing_params.each do |param| + UI.error("Missing parameter: #{param}") + end + raise "Missing required parameters: #{missing_params.join(', ')}" + end + + Fastlane::Actions::AndroidEmulatorAction.run( + name: params[:emulator_name], + sdk_dir: params[:sdk_dir], + package: params[:emulator_package], + device: params[:emulator_device], + port: params[:emulator_port], + demo_mode: false, + cold_boot: true, + additional_options: [] + ) + sleep(5) + demo_mode(params) + build_and_install_android_app(params) + + UI.message("Running Maestro tests on Android...") + sh("maestro test #{params[:maestro_flow_file]}") + UI.success("Finished Maestro tests on Android.") + + UI.message("Exit demo mode and kill Android emulator...") + adb = "#{params[:sdk_dir]}/platform-tools/adb" + system("#{adb} shell am broadcast -a com.android.systemui.demo -e command exit") + sleep(3) + system("#{adb} emu kill") + UI.success("Android emulator killed. Process finished.") + end + + def self.demo_mode(params) + UI.message("Checking and allowing demo mode on Android emulator...") + sh("#{params[:sdk_dir]}/platform-tools/adb shell settings put global sysui_demo_allowed 1") + sh("#{params[:sdk_dir]}/platform-tools/adb shell settings get global sysui_demo_allowed") + + UI.message("Setting demo mode commands...") + sh("#{params[:sdk_dir]}/platform-tools/adb shell am broadcast -a com.android.systemui.demo -e command enter") + sh("#{params[:sdk_dir]}/platform-tools/adb shell am broadcast -a com.android.systemui.demo -e command clock -e hhmm 1200") + sh("#{params[:sdk_dir]}/platform-tools/adb shell am broadcast -a com.android.systemui.demo -e command battery -e level 100") + sh("#{params[:sdk_dir]}/platform-tools/adb shell am broadcast -a com.android.systemui.demo -e command network -e wifi show -e level 4") + sh("#{params[:sdk_dir]}/platform-tools/adb shell am broadcast -a com.android.systemui.demo -e command network -e mobile show -e datatype none -e level 4") + end + + def self.build_and_install_android_app(params) + UI.message("Building Android app...") + other_action.gradle(task: "assembleDebug") + + apk_path = Dir["app/build/outputs/apk/debug/app-debug.apk"].first + + if apk_path.nil? + UI.user_error!("Error: APK file not found in build outputs.") + end + + UI.message("Found APK file at: #{apk_path}") + sh("adb install -r '#{apk_path}'") + UI.success("APK installed on Android emulator.") + end + + def self.description + "Boots an Android emulator, builds the app, installs it, and runs Maestro tests" + end + + def self.available_options + [ + FastlaneCore::ConfigItem.new( + key: :sdk_dir, + env_name: "MAESTRO_ANDROID_SDK_DIR", + description: "Path to the Android SDK DIR", + default_value: "~/Library/Android/sdk", + optional: true, + verify_block: proc do |value| + UI.user_error!("No ANDROID_SDK_DIR given, pass using `sdk_dir: 'sdk_dir'`") unless value && !value.empty? + end + ), + FastlaneCore::ConfigItem.new( + key: :emulator_name, + env_name: "MAESTRO_AVD_NAME", + description: "Name of the AVD", + default_value: "Maestro_Android_Emulator", + optional: true + ), + FastlaneCore::ConfigItem.new( + key: :emulator_package, + env_name: "MAESTRO_AVD_PACKAGE", + description: "The selected system image of the emulator", + default_value: "system-images;android-35;google_apis_playstore;arm64-v8a", + optional: true + ), + FastlaneCore::ConfigItem.new( + key: :emulator_device, + env_name: "MAESTRO_AVD_DEVICE", + description: "Device", + default_value: "pixel_8_pro", + optional: true + ), + FastlaneCore::ConfigItem.new( + key: :location, + env_name: "MAESTRO_AVD_LOCATION", + description: "Set location of the emulator ' '", + default_value: "28.0362979, -82.4930012", + optional: true + ), + FastlaneCore::ConfigItem.new( + key: :emulator_port, + env_name: "MAESTRO_AVD_PORT", + description: "Port of the emulator", + default_value: "5554", + optional: true + ), + FastlaneCore::ConfigItem.new( + key: :maestro_flow_file, + env_name: "MAESTRO_ANDROID_FLOW_FILE", + description: "The path to the Maestro flow YAML file", + optional: false, + type: String + ) + ] + end + + def self.is_supported?(platform) + platform == :android + end + end + end +end diff --git a/lib/fastlane/plugin/maestro_orchestration/actions/maestro_orchestration_ios_action.rb b/lib/fastlane/plugin/maestro_orchestration/actions/maestro_orchestration_ios_action.rb new file mode 100644 index 0000000..c004ce8 --- /dev/null +++ b/lib/fastlane/plugin/maestro_orchestration/actions/maestro_orchestration_ios_action.rb @@ -0,0 +1,119 @@ +require 'fastlane/action' +require 'fastlane_core/configuration/config_item' +require_relative '../helper/maestro_orchestration_helper' + +module Fastlane + module Actions + class MaestroOrchestrationIosAction < Action + def self.run(params) + required_params = [:simulator_device, :scheme, :workspace, :maestro_flow_file] + missing_params = required_params.select { |param| params[param].nil? } + + if missing_params.any? + missing_params.each do |param| + UI.error("Missing parameter: #{param}") + end + raise "Missing required parameters: #{missing_params.join(', ')}" + end + + device_name = params[:simulator_device] + boot_ios_simulator(device_name) + build_and_install_ios_app(params) + + UI.message("Running Maestro tests on iOS...") + `maestro test #{params[:maestro_flow_file]}` + UI.success("Finished Maestro tests on iOS.") + end + + def self.boot_ios_simulator(device_name) + simulators_list = `xcrun simctl list devices`.strip + + unless simulators_list.include?(device_name) + UI.error("Simulator '#{device_name}' not found.") + return + end + + device_status = simulators_list.match(/#{Regexp.quote(device_name)}.*\((.*?)\)/) + if device_status && device_status[1].casecmp('Booted').zero? + UI.success("#{device_name} is already booted.") + else + UI.message("#{device_name} is not booted. Booting now...") + system("xcrun simctl boot '#{device_name}'") + UI.message("Waiting for the simulator to boot...") + sleep(5) + UI.success("Simulator '#{device_name}' is booted.") + end + end + + def self.build_and_install_ios_app(params) + UI.message("Building iOS app with scheme: #{params[:scheme]}") + other_action.gym( + workspace: params[:workspace], + scheme: params[:scheme], + destination: "platform=iOS Simulator,name=#{params[:simulator_device]}", + configuration: "Debug", + clean: true, + sdk: "iphonesimulator", + build_path: "./build", + skip_archive: true, + skip_package_ipa: true, + include_symbols: false, + include_bitcode: false, + xcargs: "-UseModernBuildSystem=YES" + ) + + derived_data_path = File.expand_path("~/Library/Developer/Xcode/DerivedData") + app_path = Dir["#{derived_data_path}/**/#{params[:scheme]}.app"].first + + if app_path.nil? + UI.user_error!("Error: .app file not found in DerivedData.") + end + + UI.message("Found .app file at: #{app_path}") + sh("xcrun simctl install booted '#{app_path}'") + UI.success("App installed on iOS simulator.") + end + + def self.description + "Boots an iOS simulator, builds the app, installs it, and runs Maestro tests" + end + + def self.available_options + [ + FastlaneCore::ConfigItem.new( + key: :simulator_device, + env_name: "MAESTRO_IOS_DEVICE", + description: "The iOS simulator device to boot", + optional: false, + type: String + ), + FastlaneCore::ConfigItem.new( + key: :scheme, + env_name: "MAESTRO_IOS_SCHEME", + description: "The iOS app scheme to build", + optional: false, + type: String + ), + FastlaneCore::ConfigItem.new( + key: :workspace, + env_name: "MAESTRO_IOS_WORKSPACE", + description: "The Xcode workspace", + optional: false, + type: String + ), + FastlaneCore::ConfigItem.new( + key: :maestro_flow_file, + env_name: "MAESTRO_IOS_FLOW_FILE", + description: "The path to the Maestro flows YAML file", + optional: false, + type: String + ) + ] + end + + def self.is_supported?(platform) + platform == :ios + end + end + end +end diff --git a/spec/maestro_orchestration_action_spec.rb b/spec/maestro_orchestration_action_spec.rb index a86ab30..fa00d6e 100644 --- a/spec/maestro_orchestration_action_spec.rb +++ b/spec/maestro_orchestration_action_spec.rb @@ -1,9 +1,100 @@ -describe Fastlane::Actions::MaestroOrchestrationAction do - describe '#run' do - it 'prints a message' do - expect(Fastlane::UI).to receive(:message).with("The maestro_orchestration plugin is working!") +describe Fastlane::Actions::MaestroOrchestrationIosAction do + describe 'Parameter Passing' do + it 'makes sure that all the parameters are passed' do + valid_params = { + simulator_device: "iPhone 14", + scheme: "MyAppScheme", + workspace: "MyApp.xcworkspace", + maestro_flow_file: "flows.yaml" + } + %i[simulator_device scheme workspace maestro_flow_file].each do |key| + expect(valid_params[key]).not_to be_nil + end + end + + it 'throws error if maestro_flow_file is not provided' do + invalid_params = { + simulator_device: "iPhone 14", + scheme: "MyAppScheme", + workspace: "MyApp.xcworkspace" + } + expect { Fastlane::Actions::MaestroOrchestrationIosAction.run(invalid_params) }.to raise_error("Missing required parameters: maestro_flow_file") + end + + it 'throws error if simulator_device is not provided' do + invalid_params = { + maestro_flow_file: "flows.yaml", + scheme: "MyAppScheme", + workspace: "MyApp.xcworkspace" + } + expect { Fastlane::Actions::MaestroOrchestrationIosAction.run(invalid_params) }.to raise_error("Missing required parameters: simulator_device") + end + + it 'throws error if scheme is not provided' do + invalid_params = { + simulator_device: "iPhone 14", + maestro_flow_file: "flows.yaml", + workspace: "MyApp.xcworkspace" + } + expect { Fastlane::Actions::MaestroOrchestrationIosAction.run(invalid_params) }.to raise_error("Missing required parameters: scheme") + end + + it 'throws error if workspace is not provided' do + invalid_params = { + simulator_device: "iPhone 14", + scheme: "MyAppScheme", + maestro_flow_file: "flows.yaml" + } + expect { Fastlane::Actions::MaestroOrchestrationIosAction.run(invalid_params) }.to raise_error("Missing required parameters: workspace") + end + end +end + +describe Fastlane::Actions::MaestroOrchestrationAndroidAction do + describe 'Parameter Passing' do + it "makes sure that all the parameters are passed" do + valid_params = { + package: "system-images;android-29;google_apis;x86", + device: "Nexus 5X", + flow_file: "flows.yaml" + } + + %i[package device flow_file].each do |key| + expect(valid_params[key]).not_to be_nil + end + end + + it 'throws error if emulator_device is not provided' do + invalid_params = { + emulator_name: "Pixel_3_API_29", + sdk_dir: "/Users/username/Library/Android/sdk", + emulator_package: "system-images;android-29;google_apis;x86", + emulator_port: "5554", + maestro_flow_file: "flows.yaml" + } + expect { Fastlane::Actions::MaestroOrchestrationAndroidAction.run(invalid_params) }.to raise_error("Missing required parameters: emulator_device") + end + + it 'throws error if emulator_package is not provided' do + invalid_params = { + emulator_name: "Pixel_3_API_29", + sdk_dir: "/Users/username/Library/Android/sdk", + emulator_device: "Nexus 5X", + emulator_port: "5554", + maestro_flow_file: "flows.yaml" + } + expect { Fastlane::Actions::MaestroOrchestrationAndroidAction.run(invalid_params) }.to raise_error("Missing required parameters: emulator_package") + end - Fastlane::Actions::MaestroOrchestrationAction.run(nil) + it 'throws error if maestro_flow_file is not provided' do + invalid_params = { + emulator_name: "Pixel_3_API_29", + sdk_dir: "/Users/username/Library/Android/sdk", + emulator_package: "system-images;android-29;google_apis;x86", + emulator_device: "Nexus 5X", + emulator_port: "5554" + } + expect { Fastlane::Actions::MaestroOrchestrationAndroidAction.run(invalid_params) }.to raise_error("Missing required parameters: maestro_flow_file") end end end