Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions .github/scripts/run-ios-integration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#!/usr/bin/env python3
"""Bound Flutter's simulator VM discovery hang; retry only that startup race.

Remove when the pinned Flutter includes the fix for flutter/flutter#181771.
"""

import os
import queue
import signal
import subprocess
import sys
import threading
import time


def run(command, discovery_timeout=120):
for attempt in range(2):
process = subprocess.Popen(
command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
text=True, start_new_session=True,
)
output = queue.Queue()

def read_output():
for line in process.stdout:
output.put(line)
output.put(None)

reader = threading.Thread(target=read_output, daemon=True)
reader.start()
deadline = None
stalled = False
while True:
try:
line = output.get(timeout=0.2)
except queue.Empty:
line = ""
if line is None:
break
if line:
print(line, end="", flush=True)
if "Waiting for VM Service port to be available..." in line:
deadline = time.monotonic() + discovery_timeout
elif "VM Service URL on device:" in line:
deadline = None
if deadline is not None and time.monotonic() >= deadline:
stalled = True
print("VM Service discovery timed out.", flush=True)
try:
os.killpg(process.pid, signal.SIGTERM)
except ProcessLookupError:
pass
try:
process.wait(timeout=10)
except subprocess.TimeoutExpired:
pass
# Also stop descendants if the parent exited before them.
try:
os.killpg(process.pid, signal.SIGKILL)
except ProcessLookupError:
pass
break
result = process.wait()
reader.join(timeout=2)
if not stalled:
return result
if attempt == 0:
print("Retrying simulator launch once (flutter/flutter#181771).", flush=True)
return 1


if __name__ == "__main__":
if len(sys.argv) < 2:
sys.exit("Usage: run-ios-integration.py <command> [arguments...]")
sys.exit(run(sys.argv[1:]))
47 changes: 17 additions & 30 deletions .github/workflows/android-test.yaml
Original file line number Diff line number Diff line change
@@ -1,43 +1,30 @@
name: Test-Android
name: Android integration
on:
push:
branches:
- 'main'
branches: [main]
pull_request:
branches:
- '*'
jobs:
android-test:
test:
runs-on: ubuntu-latest

env:
FLUTTER_CHANNEL: stable
FLUTTER_VERSION: 3.27.3

timeout-minutes: 30
steps:
- name: Checkout code
uses: actions/checkout@v6

- run: echo API_KEY=${{ secrets.TEST_API_KEY }} > example/.env

- name: Set up Flutter
uses: subosito/flutter-action@v2
- uses: actions/checkout@v6
- uses: actions/setup-java@v5
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
channel: ${{ env.FLUTTER_CHANNEL }}

# This step enables KVM (Kernel-based Virtual Machine).
# KVM is a virtualization module in the Linux kernel that allows the
# kernel to function as a hypervisor. This is necessary for running
# virtual machines on the host system.
java-version: '17'
distribution: zulu
- uses: subosito/flutter-action@v2
with:
flutter-version: '3.44.2'
- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm

- name: run android tests
uses: reactivecircus/android-emulator-runner@v2
- run: flutter pub get
working-directory: example
- uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 29
script: cd example && flutter drive --driver=test_drive/integration_test.dart --target=test/widget_test.dart
api-level: 36
arch: x86_64
script: cd example && flutter test integration_test/mobile_test.dart --verbose
109 changes: 24 additions & 85 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -1,94 +1,33 @@
name: CI
name: Mobile builds
on:
push:
branches:
- 'main'
branches: [main]
pull_request:
branches:
- '*'
jobs:
build:
runs-on: macos-latest

strategy:
matrix:
include:
- flutter-version: '3.27.3'
build: cocoapods
- flutter-version: '3.44.0'
build: android
- flutter-version: '3.44.0'
build: spm

env:
FLUTTER_CHANNEL: stable

platform: [android, ios-spm, ios-cocoapods]
steps:
- name: Checkout code
uses: actions/checkout@v6

- name: Fetch submodules
run: git submodule update --init --recursive

- name: Set up JDK 17
if: matrix.build != 'spm'
uses: actions/setup-java@v5
with:
java-version: 17
distribution: 'zulu'

- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: ${{ matrix.flutter-version }}
channel: ${{ env.FLUTTER_CHANNEL }}

- name: Copy Confidence sources for CocoaPods
if: matrix.build == 'cocoapods'
working-directory: ios/Classes
run: cp -r confidence-sdk/Sources/Confidence .

- name: Remove the submodule
if: matrix.build == 'cocoapods'
working-directory: ios/Classes
run: rm -rf confidence-sdk

- name: Remove git submodule
if: matrix.build == 'cocoapods'
working-directory: ios/Classes
run: git rm confidence-sdk

- name: Install dependencies
if: matrix.build != 'spm'
working-directory: example
run: flutter pub get

- run: echo API_KEY=${{ secrets.TEST_API_KEY }} > example/.env
if: matrix.build != 'spm'

- name: Build Android
if: matrix.build != 'spm'
working-directory: example
run: flutter build apk --release

- name: Build iOS (CocoaPods)
if: matrix.build == 'cocoapods'
working-directory: example
run: flutter build ios --release --no-codesign

- name: Copy Confidence sources for SPM
if: matrix.build == 'spm'
run: cp -r ios/Classes/confidence-sdk/Sources/Confidence ios/confidence_flutter_sdk/Sources/confidence_flutter_sdk/

- name: Prepare plugin without git context
if: matrix.build == 'spm'
run: |
mkdir -p /tmp/confidence_flutter_sdk
rsync -a --exclude='.git' --exclude='ios/Classes/confidence-sdk' . /tmp/confidence_flutter_sdk/

- run: echo API_KEY=dummy > /tmp/confidence_flutter_sdk/example/.env
if: matrix.build == 'spm'
- name: Build iOS (SPM)
if: matrix.build == 'spm'
working-directory: /tmp/confidence_flutter_sdk/example
run: flutter build ios --no-codesign
- uses: actions/checkout@v6
- name: Stage package without Git identity overrides
run: python3 tool/stage_package.py "$RUNNER_TEMP/confidence_openfeature_provider"
- uses: actions/setup-java@v5
with:
java-version: '17'
distribution: zulu
- uses: subosito/flutter-action@v2
with:
flutter-version: '3.44.2'
channel: stable
- if: matrix.platform == 'ios-cocoapods'
run: flutter config --no-enable-swift-package-manager
- run: flutter pub get
working-directory: ${{ runner.temp }}/confidence_openfeature_provider/example
- if: matrix.platform == 'android'
run: flutter build apk --debug
working-directory: ${{ runner.temp }}/confidence_openfeature_provider/example
- if: matrix.platform != 'android'
run: flutter build ios --simulator --debug
working-directory: ${{ runner.temp }}/confidence_openfeature_provider/example
18 changes: 18 additions & 0 deletions .github/workflows/dart-provider.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: Dart provider
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: subosito/flutter-action@v2
with:
flutter-version: '3.44.2'
channel: stable
- run: flutter pub get
- run: dart format --output=none --set-exit-if-changed lib test tool example/lib example/integration_test
- run: flutter analyze --fatal-infos
- run: flutter test
102 changes: 20 additions & 82 deletions .github/workflows/ios-test.yaml
Original file line number Diff line number Diff line change
@@ -1,90 +1,28 @@
name: Test-iOS
name: iOS integration
on:
push:
branches:
- 'main'
branches: [main]
pull_request:
branches:
- '*'
jobs:
ios-test:
test:
runs-on: macos-latest
timeout-minutes: 45

env:
FLUTTER_CHANNEL: stable
FLUTTER_VERSION: 3.44.0

timeout-minutes: 30
steps:
- name: Checkout code
uses: actions/checkout@v6

- name: fetch submodules
run: git submodule update --init --recursive

- name: Copy iOS
working-directory: ios/Classes
run: cp -r confidence-sdk/Sources/Confidence .

- name: Remove the submodule
working-directory: ios/Classes
run: rm -rf confidence-sdk

- name: Remove git submodule
working-directory: ios/Classes
run: git rm confidence-sdk

- name: Select iOS simulator
id: simulator
run: |
SIMULATOR=$(xcrun simctl list devices available -j | python3 .github/scripts/find-simulator.py iOS)
echo "udid=$SIMULATOR" >> "$GITHUB_OUTPUT"
echo "Using simulator: $SIMULATOR"

- name: Set up Flutter
uses: subosito/flutter-action@v2
- uses: actions/checkout@v6
- name: Stage package without Git identity overrides
run: python3 tool/stage_package.py "$RUNNER_TEMP/confidence_openfeature_provider"
- uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
channel: ${{ env.FLUTTER_CHANNEL }}

- run: echo API_KEY=${{ secrets.TEST_API_KEY }} > example/.env
- name: Boot iOS simulator and wait until it is ready
flutter-version: '3.44.2'
- name: Select simulator
id: simulator
run: |
# `simctl boot` returns as soon as boot is *initiated*, not when the
# device is usable, so booting inline let `flutter test` race the Dart
# VM debug connection against a still-booting simulator. When it lost
# that race the app built, installed and launched but the harness
# never attached, so the job sat silent until the 30 minute timeout
# with "No tests ran." and "Error waiting for a debug connection".
#
# Measured locally: `boot` returned after 2s while the device needed
# 3s more warm and 17s more cold, most of it "Waiting on Data
# Migration". `bootstatus -b` boots if needed and blocks until the
# device reports Finished. It also exits non-zero for an invalid
# device, so a boot failure fails here instead of masquerading as a
# test timeout further down.
xcrun simctl bootstatus "${{ steps.simulator.outputs.udid }}" -b

- name: Configure Flutter to use CocoaPods
run: flutter config --no-enable-swift-package-manager

# `flutter test` intermittently fails to attach to the Dart VM on a cold
# runner: the app builds, installs and launches, but the harness never
# connects and the step sits silent until it is killed, reporting
# "No tests ran." and "Error waiting for a debug connection: The log
# reader failed unexpectedly". Booting the simulator to readiness first
# (see the step above) did not stop it, so this is an attach race rather
# than a boot race.
#
# It is intermittent, not deterministic — the same commit passes on some
# runs and stalls on others — and a second attempt, with the app already
# built and the simulator already warm, has consistently succeeded. Two
# bounded attempts fit inside the job's 45 minute budget, whereas the
# previous single 30 minute attempt burned almost the whole budget before
# failing.
- name: Run iOS integration tests
uses: nick-fields/retry@v3
with:
timeout_minutes: 13
max_attempts: 2
command: cd example && flutter test -d "${{ steps.simulator.outputs.udid }}" integration_test/widget_test.dart
simulator=$(xcrun simctl list devices available -j | python3 .github/scripts/find-simulator.py iOS)
echo "udid=$simulator" >> "$GITHUB_OUTPUT"
xcrun simctl bootstatus "$simulator" -b
- run: flutter pub get
working-directory: ${{ runner.temp }}/confidence_openfeature_provider/example
# Flutter can miss the simulator's VM Service startup log (#181771).
# Retry only discovery hangs, never test failures.
- run: python3 "$GITHUB_WORKSPACE/.github/scripts/run-ios-integration.py" flutter test integration_test/mobile_test.dart -d '${{ steps.simulator.outputs.udid }}' --verbose
working-directory: ${{ runner.temp }}/confidence_openfeature_provider/example
Loading
Loading