Skip to content

Commit

Permalink
Merge pull request #38 from mmcc007/#30_config_ios_build
Browse files Browse the repository at this point in the history
Added support for configurable iOS build
  • Loading branch information
mmcc007 authored Aug 7, 2019
2 parents 0b10a09 + 92e89d5 commit cc97251
Show file tree
Hide file tree
Showing 15 changed files with 205 additions and 60 deletions.
38 changes: 33 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
[![pub package](https://img.shields.io/pub/v/sylph.svg)](https://pub.dartlang.org/packages/sylph)
[![Build Status](https://travis-ci.com/mmcc007/sylph.svg?branch=master)](https://travis-ci.com/mmcc007/sylph)

<img src="art/sylph_logo.png" width="30%" title="Sylph" alt="Sylph">

_A sylph is a mythological invisible being of the air._
[Wikipedia](https://en.wikipedia.org/wiki/Sylph)

# _Sylph_
_Sylph_ is a command line utility for running Flutter integration and end-to-end tests on pools of real iOS and Android devices in the cloud. _Sylph_ runs on a developer mac or in a CI environment.

Expand Down Expand Up @@ -59,14 +62,15 @@ For alternative configuration options see:
https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html

# Configuration
All configuration information is passed to _Sylph_ using a configuration file. The default config file is called `sylph.yaml`:
Configuration information is passed to _Sylph_ using a configuration file. The default config file is called `sylph.yaml`:
```yaml
# Config file for Flutter tests on real device pools.
# Auto-creates projects and device pools if needed.
# Configures android and ios test runs.
# Builds app, uploads and runs tests.
# Then monitors tests, returns final pass/fail result and downloads artifacts.
# Note: assumes the 'aws' command line utility is logged-in.
# Note: to build the debug iOS app, certain environment variables are required.

# sylph config
tmp_dir: /tmp/sylph
Expand Down Expand Up @@ -112,6 +116,15 @@ Multiple test suites, consisting of multiple tests, can be run on each device in

Device pools can consist of multiple devices. Devices in a device pool must be of the same type, iOS or Android.

## Building an iOS debug app
To build a testable iOS app locally, that can run on any real device in the cloud, the following environment variables must be present:
- APP_IDENTIFIER
This is the bundle identifier of your iOS app. For example, com.mycompany.myapp.
- TEAM_ID
This is the Developer Portal Team ID. It is of the form 'ABCDEFGHIJ'.

A check is made before the start of a run to confirm these environment variables are present.

## Populating a device pool
To add devices to a device pool, pick devices from the list provided by
```
Expand All @@ -122,7 +135,7 @@ sylph -d ios
and add to the appropriate pool type in sylph.yaml. The listed devices are devices currently available on Device Farm.

## Configuration Validation
The sylph.yaml is validated to confirm the devices are available on Device Farm and tests are present before starting a run.
The sylph.yaml is validated to confirm the devices are available on Device Farm and tests are present before starting a run. Presence of the required environment variables for the iOS build are also confirmed.

# Configuring a CI Environment for _Sylph_

Expand All @@ -144,6 +157,12 @@ This is used to configure the CI's ssh client to find the match host. For exampl
- MATCH_PORT
This is used to configure the CI's ssh client to find the match host's ssh port. For example, 22.

The following environment variables are also required by _Sylph_ in a CI environment to configure the Fastlane Appfile and exportOptions.plist correctly:
- APP_IDENTIFIER
This is the bundle identifier of your iOS app. For example, com.mycompany.myapp.
- TEAM_ID
This is the Developer Portal Team ID. It is of the form 'ABCDEFGHIJ'.

## AWS CLI Credentials for CI
The following AWS CLI credentials are required:
- AWS_ACCESS_KEY_ID
Expand All @@ -153,11 +172,20 @@ For details on other credentials see:
https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html

Note: the Travis-CI build uses pre-configured AWS CLI values in [.aws/config](.aws/config)
## Example secrets for Travis-CI
_Sylph_ runs on Travis-CI and expects the following environment variables:
## Sample environment variables for Travis-CI
For example, when _Sylph_ is run on Travis-CI the following environment variables are used:

![secret variables](art/travis_env_vars.png)


# Upgrade
To upgrade, simply re-issue the install command
````bash
$ pub global activate sylph
````
To check the version of _Sylph_ currently installed:
```
pub global list
```
# Live demo
To see _Sylph_ in action in a CI environment, a demo of the [example](example) app is available.

Expand Down
31 changes: 16 additions & 15 deletions analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
analyzer:
# exclude:
# - path/to/excluded/files/**

# Lint rules and documentation, see http://dart-lang.github.io/linter/lints
linter:
rules:
- cancel_subscriptions
- hash_and_equals
- iterable_contains_unrelated_type
- list_remove_unrelated_type
- test_types_in_equals
- unrelated_type_equality_checks
- valid_regexps
- curly_braces_in_flow_control_structures
include: package:pedantic/analysis_options.yaml
#analyzer:
## exclude:
## - path/to/excluded/files/**
#
## Lint rules and documentation, see http://dart-lang.github.io/linter/lints
#linter:
# rules:
# - cancel_subscriptions
# - hash_and_equals
# - iterable_contains_unrelated_type
# - list_remove_unrelated_type
# - test_types_in_equals
# - unrelated_type_equality_checks
# - valid_regexps
# - curly_braces_in_flow_control_structures
Binary file added art/sylph_logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified art/travis_env_vars.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 4 additions & 3 deletions bin/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ main(List<String> arguments) async {
final configArg = 'config';
final devicesArg = 'devices';
final helpArg = 'help';
final ArgParser argParser = new ArgParser(allowTrailingOptions: false)
final ArgParser argParser = ArgParser(allowTrailingOptions: false)
..addOption(configArg,
abbr: 'c',
defaultsTo: 'sylph.yaml',
Expand Down Expand Up @@ -50,12 +50,13 @@ main(List<String> arguments) async {
}
break;
case 'android':
for (final sylphDevice in getDevices(DeviceType.android)) {
for (final sylphDevice
in getDeviceFarmDevicesByType(DeviceType.android)) {
print(sylphDevice);
}
break;
case 'ios':
for (final sylphDevice in getDevices(DeviceType.ios)) {
for (final sylphDevice in getDeviceFarmDevicesByType(DeviceType.ios)) {
print(sylphDevice);
}
break;
Expand Down
2 changes: 1 addition & 1 deletion example/test_driver/main_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ void main() {
});

tearDownAll(() async {
if (driver != null) driver.close();
if (driver != null) await driver.close();
});

test('tap on the floating action button; verify counter', () async {
Expand Down
8 changes: 4 additions & 4 deletions lib/resources/exportOptions.plist
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
<string>development</string>
<key>provisioningProfiles</key>
<dict>
<key>com.orbsoft.counter</key>
<string>match Development com.orbsoft.counter</string>
<!--<string>match AppStore com.orbsoft.counter</string>-->
<key>$APP_IDENTIFIER</key>
<string>match Development $APP_IDENTIFIER</string>
<!--<string>match AppStore com.mycompany.myapp</string>-->
</dict>
<key>signingCertificate</key>
<!--<string>iPhone Distribution</string>-->
Expand All @@ -19,7 +19,7 @@
<key>stripSwiftSymbols</key>
<true/>
<key>teamID</key>
<string>ET2VMHJPVM</string>
<string>$TEAM_ID</string>
<key>uploadSymbols</key>
<false/>
</dict>
Expand Down
8 changes: 4 additions & 4 deletions lib/resources/fastlane/Appfile
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
app_identifier("com.orbsoft.counter") # The bundle identifier of your app
apple_id("[email protected]") # Your Apple email address
app_identifier("$APP_IDENTIFIER") # The bundle identifier of your app
#apple_id("$APPLE_ID") # Your Apple email address

itc_team_id("118607454") # App Store Connect Team ID
team_id("ET2VMHJPVM") # Developer Portal Team ID
#itc_team_id("$ITC_TEAM_ID") # App Store Connect Team ID
#team_id("$TEAM_ID") # Developer Portal Team ID

# For more information about the Appfile, see:
# https://docs.fastlane.tools/advanced/#appfile
44 changes: 36 additions & 8 deletions lib/src/bundle.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'package:resource/resource.dart';

import 'utils.dart';

// resource consts
const kResourcesUri = 'package:sylph/resources';
const kAppiumTemplateName = 'appium_bundle.zip';
const kAppiumTestSpecName = 'test_spec.yaml';
Expand All @@ -13,6 +14,24 @@ const kTestBundleName = '$kTestBundleDir.zip';
const kDefaultFlutterAppName = 'flutter_app';
const kBuildToOsMapFileName = 'build_to_os.txt';

// env consts
const kCIEnvVar = 'CI';
const kExportOptionsPlistEnvVars = ['APP_IDENTIFIER', 'TEAM_ID'];
const kAppfileEnvVars = [
'APP_IDENTIFIER',
// 'APPLE_ID',
// 'ITC_TEAM_ID',
// 'TEAM_ID'
]; // order dependent
const kCIBuildEnvVars = [
'PUBLISHING_MATCH_CERTIFICATE_REPO',
'MATCH_PASSWORD',
'MATCH_HOST',
'MATCH_PORT',
'AWS_ACCESS_KEY_ID',
'AWS_SECRET_ACCESS_KEY'
];

/// Bundles Flutter tests using appium template found in staging area.
/// Resulting bundle is saved on disk in temporary location
/// for later upload.
Expand Down Expand Up @@ -85,15 +104,15 @@ Future<void> unpackResources(String tmpDir) async {
await unpackFile(kBuildToOsMapFileName, tmpDir);

// unpack export options
// todo: configure exportOptions.plist for provisioning profile, team, etc...
await unpackFile('exportOptions.plist', 'ios');
await unpackFile('exportOptions.plist', 'ios',
envVars: kExportOptionsPlistEnvVars);

// unpack components used in a CI environment
final envVars = Platform.environment;
if (envVars['CI'] == 'true') {
print('CI environment detected. Unpacking related resources');
if (envVars[kCIEnvVar] == 'true') {
print('CI environment detected. Unpacking related resources.');
// unpack fastlane
await unpackFile('fastlane/Appfile', 'ios');
await unpackFile('fastlane/Appfile', 'ios', envVars: kAppfileEnvVars);
await unpackFile('fastlane/Fastfile', 'ios');
await unpackFile('GemFile', 'ios');
await unpackFile('GemFile.lock', 'ios');
Expand Down Expand Up @@ -134,9 +153,18 @@ Future<void> unpackScript(String srcPath, String dstDir) async {
cmd('chmod', ['u+x', '$dstDir/$srcPath']);
}

Future unpackFile(String srcPath, String dstDir) async {
/// Unpack file from resources while optionally applying env vars.
Future unpackFile(String srcPath, String dstDir, {List<String> envVars}) async {
final resource = Resource('$kResourcesUri/$srcPath');
final String script = await resource.readAsString();
String resourceStr = await resource.readAsString();

if (envVars != null) {
final env = Platform.environment;
for (final envVar in envVars) {
resourceStr = resourceStr.replaceAll('\$$envVar', env[envVar]);
}
}

final file = await File('$dstDir/$srcPath').create(recursive: true);
await file.writeAsString(script, flush: true);
await file.writeAsString(resourceStr, flush: true);
}
4 changes: 2 additions & 2 deletions lib/src/device_farm.dart
Original file line number Diff line number Diff line change
Expand Up @@ -241,8 +241,8 @@ void downloadJobArtifacts(String runArn, String runArtifactDir) {
final List jobs = deviceFarmCmd(['list-jobs', '--arn', runArn])['jobs'];

for (final job in jobs) {
// get job device
final jobDevice = getDeviceFarmDevice(job['device']);
// load job device
final jobDevice = loadDeviceFarmDevice(job['device']);

// generate job artifacts dir and download job artifacts
downloadArtifacts(
Expand Down
30 changes: 23 additions & 7 deletions lib/src/devices.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,26 @@ import 'utils.dart';

enum DeviceType { ios, android }

List<DeviceFarmDevice> getDevices(DeviceType deviceType) {
/// Get device farm devices filtered by type.
List<DeviceFarmDevice> getDeviceFarmDevicesByType(DeviceType deviceType) {
return getDeviceFarmDevices()
.where((device) => device.deviceType == deviceType)
.toList();
}

/// Get current device farm devices using device farm API.
List<DeviceFarmDevice> getDeviceFarmDevices() {
final _deviceFarmDevices = deviceFarmCmd(['list-devices'])['devices'];
final List<DeviceFarmDevice> deviceFarmDevices = [];
for (final _deviceFarmDevice in _deviceFarmDevices) {
deviceFarmDevices.add(getDeviceFarmDevice(_deviceFarmDevice));
deviceFarmDevices.add(loadDeviceFarmDevice(_deviceFarmDevice));
}
deviceFarmDevices.sort();
return deviceFarmDevices;
}

SylphDevice getDeviceFarmDevice(Map device) {
/// Load a device farm device from a [Map] of device.
DeviceFarmDevice loadDeviceFarmDevice(Map device) {
return DeviceFarmDevice(
device['name'],
device['modelId'],
Expand All @@ -30,26 +33,35 @@ SylphDevice getDeviceFarmDevice(Map device) {
device['arn']);
}

/// Get current sylph devices from [Map] of device pool info.
List getSylphDevices(Map devicePoolInfo) {
final _sylphDevices = devicePoolInfo['devices'];
final sylphDevices = [];
for (final _sylphDevice in _sylphDevices) {
sylphDevices.add(getSylphDevice(_sylphDevice, devicePoolInfo['pool_type']));
sylphDevices
.add(loadSylphDevice(_sylphDevice, devicePoolInfo['pool_type']));
}
sylphDevices.sort();
return sylphDevices;
}

SylphDevice getSylphDevice(Map device, String poolType) {
/// Load a sylph device from [Map] of device and pool type.
SylphDevice loadSylphDevice(Map device, String poolType) {
return SylphDevice(
device['name'],
device['model'],
Version.parse(device['os'].toString()),
stringToEnum(DeviceType.values, poolType));
}

/// Describe a sylph device that can be compared and sorted.
class SylphDevice implements Comparable {
SylphDevice(this.name, this.model, this.os, this.deviceType);
SylphDevice(this.name, this.model, this.os, this.deviceType)
: assert(name != null),
assert(model != null),
assert(os != null),
assert(deviceType != null);

final String name, model;
final Version os;
final DeviceType deviceType;
Expand Down Expand Up @@ -88,10 +100,13 @@ class SylphDevice implements Comparable {
name.hashCode ^ model.hashCode ^ os.hashCode ^ deviceType.hashCode;
}

/// Describe a device farm device that can be compared and sorted.
class DeviceFarmDevice extends SylphDevice {
DeviceFarmDevice(String name, String modelId, Version os,
DeviceType deviceType, this.availability, this.arn)
: super(name, modelId, os, deviceType);
: assert(availability != null),
assert(arn != null),
super(name, modelId, os, deviceType);

final String availability, arn;

Expand All @@ -103,6 +118,7 @@ class DeviceFarmDevice extends SylphDevice {
@override
bool operator ==(other) {
if (other is SylphDevice) {
// allow comparison with a sylph device.
return super == (other);
} else {
return other is DeviceFarmDevice &&
Expand Down
3 changes: 2 additions & 1 deletion lib/src/sylph_run.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ Future<bool> sylphRun(String configFilePath, String sylphRunName,

// Validate config file
if (!isValidConfig(config)) {
stderr.writeln('Error: invalid config file.');
stderr.writeln(
'Sylph run was terminated due to invalid config file or environment settings.');
exit(1);
}

Expand Down
Loading

0 comments on commit cc97251

Please sign in to comment.