diff --git a/.github/workflows/ui_tests.yml b/.github/workflows/ui_tests.yml index 7b51ceb7f..9187b714e 100644 --- a/.github/workflows/ui_tests.yml +++ b/.github/workflows/ui_tests.yml @@ -28,28 +28,16 @@ jobs: steps: - uses: nschloe/action-cached-lfs-checkout@f46300cd8952454b9f0a21a3d133d4bd5684cfc2 #v1.2.3 - - uses: actions/cache@v5 - with: - path: vendor/bundle - key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }} - restore-keys: | - ${{ runner.os }}-gems- - - name: Setup environment run: source ci_scripts/ci_common.sh && setup_github_actions_environment - name: Run tests run: | - if [[ -z "${{ github.event.inputs.test_name }}" ]]; then - bundle exec fastlane ui_tests device:${{ matrix.device }} - else - bundle exec fastlane ui_tests device:${{ matrix.device }} test_name:${{ github.event.inputs.test_name }} + args=(--device-type "${{ matrix.device }}") + if [[ -n "${{ github.event.inputs.test_name }}" ]]; then + args+=( --test-name "${{ github.event.inputs.test_name }}") fi - - - name: Zip results # Faster uploads - if: failure() - working-directory: fastlane/test_output - run: zip -r UITests.xcresult.zip UITests.xcresult + swift run tools ci ui-tests "${args[@]}" - name: Archive artifacts uses: actions/upload-artifact@v7 @@ -57,32 +45,22 @@ jobs: if: failure() with: name: ${{ matrix.device }} - path: fastlane/test_output/UITests.xcresult.zip + path: test_output/UITests.xcresult.zip retention-days: 7 if-no-files-found: ignore - - name: Collect coverage - # Skip if not successful and in forks - if: ${{ success() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} - run: xcresultparser -q -o cobertura -t ElementX -p $(pwd) fastlane/test_output/UITests.xcresult > fastlane/test_output/ui-cobertura.xml - - name: Upload coverage to Codecov # Skip if not successful and in forks if: ${{ success() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2 with: report_type: coverage - files: fastlane/test_output/ui-cobertura.xml + files: test_output/ui-cobertura.xml disable_search: true fail_ci_if_error: true token: ${{ secrets.CODECOV_TOKEN }} flags: uitests - - name: Collect test results - # Skip if cancelled and in forks - if: ${{ !cancelled() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} - run: xcresultparser -q -o junit -p $(pwd) fastlane/test_output/UITests.xcresult > fastlane/test_output/ui-junit.xml - - name: Upload test results to Codecov # Skip if cancelled and in forks if: ${{ !cancelled() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} @@ -90,7 +68,7 @@ jobs: continue-on-error: true with: report_type: test_results - files: fastlane/test_output/ui-junit.xml + files: test_output/ui-junit.xml disable_search: true fail_ci_if_error: false token: ${{ secrets.CODECOV_TOKEN }} diff --git a/Tools/Sources/Commands/CI/CI.swift b/Tools/Sources/Commands/CI/CI.swift index 3c139c6f7..2c388553e 100644 --- a/Tools/Sources/Commands/CI/CI.swift +++ b/Tools/Sources/Commands/CI/CI.swift @@ -8,6 +8,7 @@ struct CI: ParsableCommand { subcommands: [ AccessibilityTests.self, UnitTests.self, + UITests.self, RunTests.self, ConfigureNightly.self ]) diff --git a/Tools/Sources/Commands/CI/UITests.swift b/Tools/Sources/Commands/CI/UITests.swift new file mode 100644 index 000000000..25c8dad6f --- /dev/null +++ b/Tools/Sources/Commands/CI/UITests.swift @@ -0,0 +1,79 @@ +import ArgumentParser +import CommandLineTools +import Foundation + +struct UITests: AsyncParsableCommand { + static let configuration = CommandConfiguration(commandName: "ui-tests", + abstract: "Runs the UI test CI workflow for a specific device type.", + discussion: """ + Examples: + swift run tools ci ui-tests --device-type iPhone + swift run tools ci ui-tests --device-type iPad + swift run tools ci ui-tests --device-type iPhone --test-name "ClassName/testName" + """) + + enum DeviceType: String, CaseIterable, ExpressibleByArgument { + case iPhone + case iPad + } + + @Option(help: "The device type to test (iPhone or iPad).") + var deviceType: DeviceType + + @Option(help: "iOS version for the simulator.") + var osVersion = "26.1" + + @Option(help: "Run only a specific test (format: 'ClassName/testName').") + var testName: String? + + private var simulatorName: String { + switch deviceType { + case .iPhone: "iPhone-\(osVersion)" + case .iPad: "iPad-\(osVersion)" + } + } + + private var simulatorType: String { + switch deviceType { + case .iPhone: "com.apple.CoreSimulator.SimDeviceType.iPhone-17" + case .iPad: "com.apple.CoreSimulator.SimDeviceType.iPad-A16" + } + } + + /// We used to run these simultaneously on iPhone and iPad but it is *really* slow on GitHub runners. + /// Presumably because launching 2 simulators uses more memory than the runner has available. + func run() async throws { + var args = [ + "--scheme", "UITests", + "--device", simulatorName, + "--os-version", osVersion, + "--create-simulator-name", simulatorName, + "--create-simulator-type", simulatorType + ] + + if let testName { + args += ["--test-name", testName] + } + + var testsFailed = false + do { + print("\n🧪 Running UI tests (\(deviceType.rawValue))…\n") + try await RunTests.parse(args).run() + } catch { + testsFailed = true + print("\n❌ UI tests (\(deviceType.rawValue)) failed.\n") + } + + await CI.zipResults(bundles: ["UITests.xcresult"], + outputName: "UITests.xcresult.zip") + + await CI.collectCoverage(resultBundle: "UITests.xcresult", outputName: "ui-cobertura.xml") + await CI.collectTestResults(resultBundle: "UITests.xcresult", outputName: "ui-junit.xml") + + if testsFailed { + throw ExitCode.failure + } + + print("\n✅ UI tests (\(deviceType.rawValue)) passed.\n") + } +} diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 07a29bec6..c4301b50b 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -45,49 +45,6 @@ lane :unit_tests do |options| # We use xcresultparser in the workflow to collect coverage from both result bundles. end -lane :ui_tests do |options| - # We used to run these simultaneously on iPhone and iPad but it is *really* slow on GitHub runners. - # Presumably because launching 2 simulators uses more memory than the runner has available. - - if options[:device] == "iPhone" - device = "iPhone-#{simulator_version}" - - create_simulator_if_necessary( - name: "iPhone-#{simulator_version}", - type: "com.apple.CoreSimulator.SimDeviceType.iPhone-17" - ) - elsif options[:device] == "iPad" - device = "iPad-#{simulator_version}" - - create_simulator_if_necessary( - name: "iPad-#{simulator_version}", - type: "com.apple.CoreSimulator.SimDeviceType.iPad-A16" - ) - else - UI.user_error!("Please supply a device argument as device:iPhone or device:iPad") - end - - if options[:test_name] - test_to_run = ["UITests/#{options[:test_name]}"] - else - test_to_run = nil - end - - reset_simulator = ENV.key?('CI') - - run_tests( - scheme: "UITests", - device: "#{device} (#{simulator_version})", - ensure_devices_found: true, - prelaunch_simulator: false, - result_bundle: true, - only_testing: test_to_run, - number_of_retries: 3, - reset_simulator: reset_simulator, - xcodebuild_formatter: "xcbeautify --quiet --is-ci --renderer github-actions" - ) -end - lane :integration_tests do clear_derived_data()