mirror of
https://github.com/RevenueCat/purchases-ios
synced 2026-11-09 22:35:56 +05:45
Add Maestro SDK update tests verifying persisted state (#7900)
* Add Maestro SDK update tests verifying persisted state
Builds a barebones test app against the latest released SDK and against the local SDK, installs one over the other in the same simulator, and asserts with screenshots that the app user ID (anonymous and logged in) and entitlements are preserved after the update.
Co-authored-by: Cursor <cursoragent@cursor.com>
* [TEMP] Only run SDK update tests on CI
Co-authored-by: Cursor <cursoragent@cursor.com>
* Keep SDK update tester labels from shifting when messages appear
Co-authored-by: Cursor <cursoragent@cursor.com>
* Revert "[TEMP] Only run SDK update tests on CI"
This reverts commit d65904de54.
* Use the shared plugin actions for the SDK update tests
Temporarily pins fastlane-plugin-revenuecat_internal to the commit of RevenueCat/fastlane-plugin-revenuecat_internal#161 until it's merged.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Build and run the SDK update test cases in a single CI job
Setup takes longer than each test case, so running them on separate machines sharing the apps through the workspace isn't worth it.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Purchase a fixed offering and package in the SDK update tester
The current offering and its first package can change in the dashboard.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Purchase from an offering not tied to Paywalls V1 in the SDK update tester
Co-authored-by: Cursor <cursoragent@cursor.com>
* Allow triggering the SDK update tests via @RCGitBot please test
Co-authored-by: Cursor <cursoragent@cursor.com>
* Remove redundant SDK update tester customer info stream
* Wait for entitlement label before asserting SDK update state
* Read the previous SDK release from purchases-ios GitHub releases
The shared plugin action now reads GitHub releases, which the purchases-ios-spm mirror doesn't have.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Pin shared SDK update actions to merged plugin commit
* Keep shared plugin revision only in lockfile
* Bump fastlane-plugin-revenuecat_internal to 4c49d48
Co-authored-by: Cursor <cursoragent@cursor.com>
* Run SDK update tests only in release CI
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
974db087f8
commit
091ca1b152
16 files changed
+606
No files matched your search
@@ -1695,6 +1695,52 @@ jobs:
|
||||
- store_artifacts:
|
||||
path: fastlane/test_output
|
||||
|
||||
# Builds the SDKUpdateTester app against the latest released SDK and against the local SDK, then runs each SDK
|
||||
# update test case with them.
|
||||
run-sdk-update-test:
|
||||
executor:
|
||||
name: macos-executor
|
||||
xcode_version: "27.0.0"
|
||||
steps:
|
||||
- macos/preboot-simulator:
|
||||
version: "27.0"
|
||||
platform: "iOS"
|
||||
device: "iPhone 17"
|
||||
- checkout
|
||||
- install-dependencies:
|
||||
install_xcbeautify: false
|
||||
- revenuecat/install-mise-tools:
|
||||
tools: tuist
|
||||
- revenuecat/install-maestro
|
||||
- run:
|
||||
name: Build SDK update test apps
|
||||
command: |
|
||||
REVENUECAT_API_KEY="${RC_E2E_TEST_API_KEY_PRODUCTION_TEST_STORE}" bundle exec fastlane build_sdk_update_test_apps
|
||||
no_output_timeout: 20m
|
||||
- run:
|
||||
name: Run SDK update test (anonymous_user)
|
||||
command: bundle exec fastlane run_sdk_update_test test_case:anonymous_user
|
||||
no_output_timeout: 15m
|
||||
- run:
|
||||
name: Run SDK update test (logged_in_user)
|
||||
command: bundle exec fastlane run_sdk_update_test test_case:logged_in_user
|
||||
no_output_timeout: 15m
|
||||
when: always
|
||||
# Not storing the apps as artifacts, since they contain the API key.
|
||||
- store_artifacts:
|
||||
path: build/sdk_update_tests/release/xcodebuild.log
|
||||
destination: release/xcodebuild.log
|
||||
- store_artifacts:
|
||||
path: build/sdk_update_tests/local/xcodebuild.log
|
||||
destination: local/xcodebuild.log
|
||||
# One path per test case, since their output also contains the reports of retried attempts.
|
||||
- store_test_results:
|
||||
path: fastlane/test_output/sdk_update_tests/anonymous_user/junit
|
||||
- store_test_results:
|
||||
path: fastlane/test_output/sdk_update_tests/logged_in_user/junit
|
||||
- store_artifacts:
|
||||
path: fastlane/test_output/sdk_update_tests
|
||||
|
||||
release-checks:
|
||||
executor:
|
||||
name: macos-executor
|
||||
@@ -2843,6 +2889,10 @@ workflows:
|
||||
context:
|
||||
- e2e-tests
|
||||
- slack-secrets
|
||||
- run-sdk-update-test:
|
||||
<<: *release-branches
|
||||
context:
|
||||
- e2e-tests
|
||||
- backend-integration-tests-SK1:
|
||||
context:
|
||||
- slack-secrets
|
||||
@@ -2947,6 +2997,7 @@ workflows:
|
||||
- check-api-changes
|
||||
- run-all-maestro-e2e-tests
|
||||
- run-workflow-maestro-tests
|
||||
- run-sdk-update-test
|
||||
- lint
|
||||
- run-test-ios-26
|
||||
- run-test-ios-27
|
||||
|
||||
@@ -39,6 +39,7 @@ const JOBS = {
|
||||
"run-revenuecat-ui-ios-18-and-17": ["slack-secrets"],
|
||||
"run-revenuecat-ui-ios-26": ["slack-secrets"],
|
||||
"run-revenuecat-ui-ios-27": ["slack-secrets"],
|
||||
"run-sdk-update-test": ["e2e-tests"],
|
||||
"run-test-ios-16-and-15": ["slack-secrets"],
|
||||
"run-test-ios-18-and-17": ["slack-secrets"],
|
||||
"run-test-ios-26": ["slack-secrets"],
|
||||
|
||||
@@ -91,6 +91,7 @@ CI.xcconfig
|
||||
# Tuist workspaces
|
||||
RevenueCat-Tuist.xcworkspace/
|
||||
Projects/**/*.xcodeproj
|
||||
Projects/SDKUpdateTester/*.xcworkspace
|
||||
Examples/**/*.xcodeproj
|
||||
|
||||
Projects/**/xcshareddata/
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import ProjectDescription
|
||||
import ProjectDescriptionHelpers
|
||||
|
||||
/// The SDK update tests build this app twice: once against the latest released SDK and once against
|
||||
/// the local SDK sources. Both builds share the bundle ID so that installing the second one over the first
|
||||
/// one behaves like an app update, preserving the app's persisted data.
|
||||
///
|
||||
/// Set `TUIST_SDK_UPDATE_TESTER_RELEASE_VERSION` (e.g. `5.92.0`) to depend on that released version.
|
||||
/// Leave it unset to depend on the local SDK sources.
|
||||
///
|
||||
/// Each variant generates a differently named Xcode project, so both can coexist and be built in parallel.
|
||||
enum SDKUpdateTesterVariant {
|
||||
case local
|
||||
case release(version: Version)
|
||||
|
||||
var projectName: String {
|
||||
switch self {
|
||||
case .local:
|
||||
return "SDKUpdateTester-Local"
|
||||
case .release:
|
||||
return "SDKUpdateTester-Release"
|
||||
}
|
||||
}
|
||||
|
||||
var package: ProjectDescription.Package {
|
||||
switch self {
|
||||
case .local:
|
||||
return .package(path: .relativeToRoot("."))
|
||||
case let .release(version):
|
||||
return .package(url: "https://github.com/RevenueCat/purchases-ios-spm.git", .exact(version))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let variant: SDKUpdateTesterVariant = {
|
||||
let releaseVersion = Environment.sdkUpdateTesterReleaseVersion.getString(default: "")
|
||||
guard !releaseVersion.isEmpty else {
|
||||
return .local
|
||||
}
|
||||
guard let version = Version(string: releaseVersion) else {
|
||||
preconditionFailure("Invalid TUIST_SDK_UPDATE_TESTER_RELEASE_VERSION '\(releaseVersion)'.")
|
||||
}
|
||||
return .release(version: version)
|
||||
}()
|
||||
|
||||
let project = Project(
|
||||
name: variant.projectName,
|
||||
organizationName: .revenueCatOrgName,
|
||||
packages: [variant.package],
|
||||
settings: .appProject,
|
||||
targets: [
|
||||
.target(
|
||||
name: "SDKUpdateTester",
|
||||
destinations: .iOS,
|
||||
product: .app,
|
||||
bundleId: "com.revenuecat.SDKUpdateTester",
|
||||
deploymentTargets: .iOS("17.0"),
|
||||
infoPlist: .extendingDefault(
|
||||
with: [
|
||||
"UILaunchScreen": [
|
||||
"UIColorName": "",
|
||||
"UIImageName": ""
|
||||
],
|
||||
"REVENUECAT_API_KEY": "$(REVENUECAT_API_KEY)"
|
||||
]
|
||||
),
|
||||
sources: ["Sources/**/*.swift"],
|
||||
dependencies: [
|
||||
.package(product: "RevenueCat", type: .runtime)
|
||||
],
|
||||
settings: .appTarget
|
||||
)
|
||||
],
|
||||
schemes: [
|
||||
.scheme(
|
||||
name: "SDKUpdateTester",
|
||||
shared: true,
|
||||
buildAction: .buildAction(targets: ["SDKUpdateTester"]),
|
||||
runAction: .runAction(configuration: "Debug")
|
||||
)
|
||||
]
|
||||
)
|
||||
@@ -0,0 +1,78 @@
|
||||
//
|
||||
// HomeView.swift
|
||||
// SDKUpdateTester
|
||||
//
|
||||
// Created by Antonio Pallares on 2/10/26.
|
||||
// Copyright © 2026 RevenueCat, Inc. All rights reserved.
|
||||
//
|
||||
|
||||
import RevenueCat
|
||||
import SwiftUI
|
||||
|
||||
struct HomeView: View {
|
||||
|
||||
@State private var appUserID: String = Purchases.shared.appUserID
|
||||
@State private var isLoggingIn = false
|
||||
@State private var errorMessage: String?
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
VStack(spacing: 32) {
|
||||
// Differs between the release and the local builds, so keep it out of the screenshots' crop areas.
|
||||
Text("RevenueCat SDK \(Purchases.frameworkVersion)")
|
||||
.font(.footnote)
|
||||
.accessibilityIdentifier("sdk_version")
|
||||
|
||||
VStack(spacing: 8) {
|
||||
Text("App User ID")
|
||||
.font(.headline)
|
||||
Text(appUserID)
|
||||
.font(.system(.body, design: .monospaced))
|
||||
.multilineTextAlignment(.center)
|
||||
.padding()
|
||||
.accessibilityIdentifier("app_user_id")
|
||||
}
|
||||
|
||||
Button("Log in") {
|
||||
logIn()
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.disabled(Constants.appUserIDToLogIn == nil || isLoggingIn)
|
||||
.accessibilityIdentifier("log_in_button")
|
||||
|
||||
NavigationLink("Go to purchase screen") {
|
||||
PurchaseView()
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.accessibilityIdentifier("purchase_screen_button")
|
||||
|
||||
if let errorMessage {
|
||||
Text(errorMessage)
|
||||
.foregroundStyle(.red)
|
||||
.accessibilityIdentifier("error_message")
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
// Top-aligned so messages appearing below don't shift the screenshotted labels.
|
||||
.frame(maxHeight: .infinity, alignment: .top)
|
||||
.navigationTitle("SDK Update Tester")
|
||||
}
|
||||
}
|
||||
|
||||
private func logIn() {
|
||||
guard let appUserIDToLogIn = Constants.appUserIDToLogIn else { return }
|
||||
|
||||
isLoggingIn = true
|
||||
errorMessage = nil
|
||||
Task {
|
||||
do {
|
||||
_ = try await Purchases.shared.logIn(appUserIDToLogIn)
|
||||
} catch {
|
||||
errorMessage = "Log in failed: \(error.localizedDescription)"
|
||||
}
|
||||
appUserID = Purchases.shared.appUserID
|
||||
isLoggingIn = false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
//
|
||||
// PurchaseView.swift
|
||||
// SDKUpdateTester
|
||||
//
|
||||
// Created by Antonio Pallares on 2/10/26.
|
||||
// Copyright © 2026 RevenueCat, Inc. All rights reserved.
|
||||
//
|
||||
|
||||
import RevenueCat
|
||||
import SwiftUI
|
||||
|
||||
struct PurchaseView: View {
|
||||
|
||||
// An offering also used by rc-maestro's E2E tests, which only require it to have no paywall attached.
|
||||
private static let offeringIdentifier = "no_paywall"
|
||||
|
||||
@State private var activeEntitlements: String = "Loading..."
|
||||
@State private var isPurchasing = false
|
||||
@State private var statusMessage: String?
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 32) {
|
||||
VStack(spacing: 8) {
|
||||
Text("Active entitlements")
|
||||
.font(.headline)
|
||||
Text(activeEntitlements)
|
||||
.font(.system(.body, design: .monospaced))
|
||||
.multilineTextAlignment(.center)
|
||||
.padding()
|
||||
.accessibilityIdentifier("active_entitlements")
|
||||
}
|
||||
|
||||
Button("Purchase subscription") {
|
||||
purchase()
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.disabled(isPurchasing)
|
||||
.accessibilityIdentifier("purchase_button")
|
||||
|
||||
if let statusMessage {
|
||||
Text(statusMessage)
|
||||
.multilineTextAlignment(.center)
|
||||
.accessibilityIdentifier("status_message")
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
// Top-aligned so the status message appearing below doesn't shift the screenshotted label.
|
||||
.frame(maxHeight: .infinity, alignment: .top)
|
||||
.navigationTitle("Purchase")
|
||||
.task {
|
||||
do {
|
||||
update(with: try await Purchases.shared.customerInfo())
|
||||
} catch {
|
||||
activeEntitlements = "Error: \(error.localizedDescription)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func update(with customerInfo: CustomerInfo) {
|
||||
let identifiers = customerInfo.entitlements.active.keys.sorted()
|
||||
activeEntitlements = identifiers.isEmpty ? "None" : identifiers.joined(separator: ", ")
|
||||
}
|
||||
|
||||
private func purchase() {
|
||||
isPurchasing = true
|
||||
statusMessage = nil
|
||||
Task {
|
||||
defer { isPurchasing = false }
|
||||
do {
|
||||
let offering = try await Purchases.shared.offerings().offering(identifier: Self.offeringIdentifier)
|
||||
guard let package = offering?.monthly else {
|
||||
statusMessage = "No monthly package found in the '\(Self.offeringIdentifier)' offering"
|
||||
return
|
||||
}
|
||||
let result = try await Purchases.shared.purchase(package: package)
|
||||
if result.userCancelled {
|
||||
statusMessage = "Purchase cancelled"
|
||||
} else {
|
||||
update(with: result.customerInfo)
|
||||
statusMessage = "Purchased \(package.storeProduct.productIdentifier)"
|
||||
}
|
||||
} catch {
|
||||
statusMessage = "Purchase failed: \(error.localizedDescription)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
//
|
||||
// SDKUpdateTesterApp.swift
|
||||
// SDKUpdateTester
|
||||
//
|
||||
// Created by Antonio Pallares on 2/10/26.
|
||||
// Copyright © 2026 RevenueCat, Inc. All rights reserved.
|
||||
//
|
||||
|
||||
import RevenueCat
|
||||
import SwiftUI
|
||||
|
||||
/// Barebones app used by the SDK update Maestro tests.
|
||||
///
|
||||
/// It must only use long-standing public APIs, since it's compiled against both the latest released SDK
|
||||
/// and the local SDK sources.
|
||||
@main
|
||||
struct SDKUpdateTesterApp: App {
|
||||
|
||||
init() {
|
||||
Purchases.logLevel = .verbose
|
||||
Purchases.configure(withAPIKey: Constants.apiKey)
|
||||
}
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
HomeView()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
enum Constants {
|
||||
|
||||
static let apiKey: String = {
|
||||
Bundle.main.object(forInfoDictionaryKey: "REVENUECAT_API_KEY") as? String ?? ""
|
||||
}()
|
||||
|
||||
/// The app user ID used by the "Log in" button, passed by Maestro as a launch argument.
|
||||
static var appUserIDToLogIn: String? {
|
||||
UserDefaults.standard.string(forKey: "app_user_id_to_log_in").flatMap { $0.isEmpty ? nil : $0 }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
# Run with the app built against the local SDK, installed over the app used by before_update.yaml.
|
||||
# Requires the same environment variables as before_update.yaml.
|
||||
|
||||
appId: com.revenuecat.SDKUpdateTester
|
||||
name: Anonymous user - after SDK update
|
||||
---
|
||||
- runFlow: ../utils/assert_state_preserved.yaml
|
||||
@@ -0,0 +1,19 @@
|
||||
# Run with the app built against the latest released SDK, before updating it.
|
||||
# Requires the following environment variables:
|
||||
# SCREENSHOTS_DIR: absolute path of the directory where the reference screenshots are stored.
|
||||
# EXPECTED_SDK_VERSION: version of the SDK the installed app was built against.
|
||||
|
||||
appId: com.revenuecat.SDKUpdateTester
|
||||
name: Anonymous user - before SDK update
|
||||
---
|
||||
- clearState
|
||||
- launchApp
|
||||
- runFlow: ../utils/assert_sdk_version.yaml
|
||||
- assertVisible:
|
||||
id: app_user_id
|
||||
text: "\\$RCAnonymousID:.+"
|
||||
- takeScreenshot:
|
||||
path: ${SCREENSHOTS_DIR}/app_user_id
|
||||
cropOn:
|
||||
id: app_user_id
|
||||
- runFlow: ../utils/purchase_subscription.yaml
|
||||
@@ -0,0 +1,8 @@
|
||||
# Run with the app built against the local SDK, installed over the app used by before_update.yaml.
|
||||
# Launched without the app user ID argument, so the user can't be logged in again: it must be the persisted one.
|
||||
# Requires the same environment variables as before_update.yaml.
|
||||
|
||||
appId: com.revenuecat.SDKUpdateTester
|
||||
name: Logged in user - after SDK update
|
||||
---
|
||||
- runFlow: ../utils/assert_state_preserved.yaml
|
||||
@@ -0,0 +1,29 @@
|
||||
# Run with the app built against the latest released SDK, before updating it.
|
||||
# Requires the following environment variables:
|
||||
# SCREENSHOTS_DIR: absolute path of the directory where the reference screenshots are stored.
|
||||
# EXPECTED_SDK_VERSION: version of the SDK the installed app was built against.
|
||||
# APP_USER_ID: app user ID to log in with. Must be unique per test run.
|
||||
|
||||
appId: com.revenuecat.SDKUpdateTester
|
||||
name: Logged in user - before SDK update
|
||||
---
|
||||
- clearState
|
||||
- launchApp:
|
||||
arguments:
|
||||
app_user_id_to_log_in: ${APP_USER_ID}
|
||||
- runFlow: ../utils/assert_sdk_version.yaml
|
||||
- assertVisible:
|
||||
id: app_user_id
|
||||
text: "\\$RCAnonymousID:.+"
|
||||
- tapOn:
|
||||
id: log_in_button
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
id: app_user_id
|
||||
text: ${APP_USER_ID}
|
||||
timeout: 30000
|
||||
- takeScreenshot:
|
||||
path: ${SCREENSHOTS_DIR}/app_user_id
|
||||
cropOn:
|
||||
id: app_user_id
|
||||
- runFlow: ../utils/purchase_subscription.yaml
|
||||
@@ -0,0 +1,11 @@
|
||||
# Asserts that the running app was built against the expected SDK version.
|
||||
# Requires the EXPECTED_SDK_VERSION environment variable.
|
||||
|
||||
appId: com.revenuecat.SDKUpdateTester
|
||||
name: Assert SDK version
|
||||
---
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
id: sdk_version
|
||||
text: "RevenueCat SDK ${EXPECTED_SDK_VERSION}"
|
||||
timeout: 30000
|
||||
@@ -0,0 +1,31 @@
|
||||
# Run after updating the SDK. Asserts that the app user ID and the active entitlements match the reference
|
||||
# screenshots taken before the update. Does not log in nor purchase anything.
|
||||
# Requires the SCREENSHOTS_DIR and EXPECTED_SDK_VERSION environment variables.
|
||||
|
||||
appId: com.revenuecat.SDKUpdateTester
|
||||
name: Assert state preserved after SDK update
|
||||
---
|
||||
# Not clearing the state: the data persisted by the previous SDK version must be kept.
|
||||
- launchApp
|
||||
- runFlow: assert_sdk_version.yaml
|
||||
- assertScreenshot:
|
||||
path: ${SCREENSHOTS_DIR}/app_user_id.png
|
||||
cropOn:
|
||||
id: app_user_id
|
||||
thresholdPercentage: 99
|
||||
- tapOn:
|
||||
id: purchase_screen_button
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
id: active_entitlements
|
||||
timeout: 30000
|
||||
- extendedWaitUntil:
|
||||
notVisible:
|
||||
id: active_entitlements
|
||||
text: "Loading\\.\\.\\."
|
||||
timeout: 30000
|
||||
- assertScreenshot:
|
||||
path: ${SCREENSHOTS_DIR}/active_entitlements.png
|
||||
cropOn:
|
||||
id: active_entitlements
|
||||
thresholdPercentage: 99
|
||||
@@ -0,0 +1,31 @@
|
||||
# Starting from the home screen, navigates to the purchase screen, purchases a subscription through the
|
||||
# Test Store and takes the reference screenshot of the active entitlements.
|
||||
# Requires the SCREENSHOTS_DIR environment variable.
|
||||
|
||||
appId: com.revenuecat.SDKUpdateTester
|
||||
name: Purchase subscription
|
||||
env:
|
||||
EXPECTED_ENTITLEMENT: ${EXPECTED_ENTITLEMENT || 'pro'}
|
||||
---
|
||||
- tapOn:
|
||||
id: purchase_screen_button
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
id: active_entitlements
|
||||
text: "None"
|
||||
timeout: 30000
|
||||
- tapOn:
|
||||
id: purchase_button
|
||||
- extendedWaitUntil:
|
||||
visible: "Test Store Purchase"
|
||||
timeout: 30000
|
||||
- tapOn: "Test valid purchase"
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
id: active_entitlements
|
||||
text: ${EXPECTED_ENTITLEMENT}
|
||||
timeout: 30000
|
||||
- takeScreenshot:
|
||||
path: ${SCREENSHOTS_DIR}/active_entitlements
|
||||
cropOn:
|
||||
id: active_entitlements
|
||||
@@ -1436,6 +1436,116 @@ platform :ios do
|
||||
end
|
||||
end
|
||||
|
||||
# Builds the SDKUpdateTester app twice, in parallel: against the latest released SDK and against the local SDK.
|
||||
# Each app is copied to <output_dir>/<release|local>/SDKUpdateTester.app, next to a version.txt file with the
|
||||
# SDK version it was built against.
|
||||
# Parameters:
|
||||
# - release_version: released SDK version to build against. Defaults to the latest release before the current version.
|
||||
# - output_dir: defaults to build/sdk_update_tests, relative to the repository root.
|
||||
desc "Build the SDK update test apps"
|
||||
lane :build_sdk_update_test_apps do |options|
|
||||
base_path = File.expand_path("..", Dir.pwd)
|
||||
output_dir = File.expand_path(options[:output_dir] || "build/sdk_update_tests", base_path)
|
||||
project_dir = "#{base_path}/Projects/SDKUpdateTester"
|
||||
# The release app depends on the purchases-ios-spm mirror, which only has tags, not GitHub releases.
|
||||
release_version = options[:release_version] || get_latest_release_before_version(
|
||||
repo_name: "purchases-ios",
|
||||
version: current_version_number
|
||||
)
|
||||
|
||||
api_key = ENV["REVENUECAT_API_KEY"]
|
||||
UI.user_error!("REVENUECAT_API_KEY environment variable must be set") unless api_key
|
||||
setup_xcconfig(api_key: api_key)
|
||||
|
||||
builds = {
|
||||
"release" => {
|
||||
project: "SDKUpdateTester-Release",
|
||||
sdk_version: release_version,
|
||||
tuist_env: { "TUIST_SDK_UPDATE_TESTER_RELEASE_VERSION" => release_version }
|
||||
},
|
||||
"local" => {
|
||||
project: "SDKUpdateTester-Local",
|
||||
sdk_version: current_version_number,
|
||||
tuist_env: {}
|
||||
}
|
||||
}
|
||||
UI.message("Building SDK update test apps with SDK versions: #{builds.transform_values { |build| build[:sdk_version] }}")
|
||||
|
||||
# Generating sequentially because both projects share the same Derived folder.
|
||||
builds.each_value do |build|
|
||||
sh(build[:tuist_env], "tuist", "generate", "--path", project_dir, "--no-open")
|
||||
end
|
||||
|
||||
FileUtils.rm_rf(output_dir)
|
||||
build_processes = builds.map do |name, build|
|
||||
FileUtils.mkdir_p("#{output_dir}/#{name}")
|
||||
log_path = "#{output_dir}/#{name}/xcodebuild.log"
|
||||
UI.message("Building #{name} app. Logs: #{log_path}")
|
||||
pid = Process.spawn(
|
||||
"xcodebuild", "build",
|
||||
"-project", "#{project_dir}/#{build[:project]}.xcodeproj",
|
||||
"-scheme", "SDKUpdateTester",
|
||||
"-configuration", "Debug",
|
||||
"-destination", "generic/platform=iOS Simulator",
|
||||
"-derivedDataPath", "#{base_path}/build/sdk_update_tests_derived_data/#{name}",
|
||||
"ARCHS=arm64",
|
||||
out: log_path,
|
||||
err: [:child, :out]
|
||||
)
|
||||
[name, pid, log_path]
|
||||
end
|
||||
|
||||
failed_builds = build_processes.reject do |name, pid, log_path|
|
||||
_, status = Process.wait2(pid)
|
||||
UI.message("Finished building #{name} app (#{status.success? ? 'succeeded' : 'failed'})")
|
||||
status.success?
|
||||
end
|
||||
failed_builds.each do |name, _, log_path|
|
||||
UI.error("Build of #{name} app failed. Last lines of #{log_path}:")
|
||||
UI.error(File.readlines(log_path).last(50).join)
|
||||
end
|
||||
UI.user_error!("Failed to build SDK update test apps: #{failed_builds.map(&:first).join(', ')}") unless failed_builds.empty?
|
||||
|
||||
builds.each do |name, build|
|
||||
app_path = "#{base_path}/build/sdk_update_tests_derived_data/#{name}/Build/Products/Debug-iphonesimulator/SDKUpdateTester.app"
|
||||
FileUtils.cp_r(app_path, "#{output_dir}/#{name}/")
|
||||
File.write("#{output_dir}/#{name}/version.txt", build[:sdk_version])
|
||||
end
|
||||
UI.success("✅ SDK update test apps available in #{output_dir}")
|
||||
end
|
||||
|
||||
# Runs an SDK update test case: installs the app built against the released SDK and runs the test case's
|
||||
# before_update.yaml flow, then installs the app built against the local SDK over it (keeping its data, like
|
||||
# an app update) and runs the after_update.yaml flow.
|
||||
# Parameters:
|
||||
# - test_case: name of the flows directory in Projects/SDKUpdateTester/maestro (e.g. "anonymous_user").
|
||||
# - apps_dir: output_dir of build_sdk_update_test_apps. Defaults to build/sdk_update_tests.
|
||||
# - output_dir: defaults to fastlane/test_output/sdk_update_tests/<test_case>.
|
||||
# - max_attempts: defaults to 3.
|
||||
desc "Run an SDK update Maestro test case"
|
||||
lane :run_sdk_update_test do |options|
|
||||
test_case = options[:test_case] || UI.user_error!("test_case is required")
|
||||
base_path = File.expand_path("..", Dir.pwd)
|
||||
apps_dir = File.expand_path(options[:apps_dir] || "build/sdk_update_tests", base_path)
|
||||
flows_dir = "#{base_path}/Projects/SDKUpdateTester/maestro/#{test_case}"
|
||||
app = lambda do |name|
|
||||
version_path = "#{apps_dir}/#{name}/version.txt"
|
||||
UI.user_error!("#{version_path} not found. Run build_sdk_update_test_apps first.") unless File.exist?(version_path)
|
||||
{ app_path: "#{apps_dir}/#{name}/SDKUpdateTester.app", sdk_version: File.read(version_path).strip }
|
||||
end
|
||||
|
||||
run_sdk_update_maestro_test(
|
||||
platform: "ios",
|
||||
app_id: "com.revenuecat.SDKUpdateTester",
|
||||
steps: [
|
||||
app.call("release").merge(flow: "#{flows_dir}/before_update.yaml"),
|
||||
app.call("local").merge(flow: "#{flows_dir}/after_update.yaml")
|
||||
],
|
||||
output_dir: File.expand_path(options[:output_dir] || "fastlane/test_output/sdk_update_tests/#{test_case}", base_path),
|
||||
max_attempts: Integer(options.fetch(:max_attempts, 3))
|
||||
)
|
||||
end
|
||||
|
||||
desc "replace API KEY for installation and integration tests"
|
||||
lane :replace_api_key_integration_tests do
|
||||
replace_text_in_files(
|
||||
|
||||
@@ -258,6 +258,22 @@ run API Tests
|
||||
|
||||
run_maestro_e2e_tests
|
||||
|
||||
### ios build_sdk_update_test_apps
|
||||
|
||||
```sh
|
||||
[bundle exec] fastlane ios build_sdk_update_test_apps
|
||||
```
|
||||
|
||||
Build the SDK update test apps
|
||||
|
||||
### ios run_sdk_update_test
|
||||
|
||||
```sh
|
||||
[bundle exec] fastlane ios run_sdk_update_test
|
||||
```
|
||||
|
||||
Run an SDK update Maestro test case
|
||||
|
||||
### ios replace_api_key_integration_tests
|
||||
|
||||
```sh
|
||||
|
||||
Reference in new issue
Block a user