Skip to content

feat: adding shorebird specific tests #45

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
May 9, 2024
Merged
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
41 changes: 41 additions & 0 deletions .github/workflows/shorebird_ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: shorebird_ci

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

on:
pull_request:
push:
branches:
- main

jobs:
test:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]

runs-on: ${{ matrix.os }}

name: 🐦 Shorebird Test

env:
FLUTTER_STORAGE_BASE_URL: https://download.shorebird.dev

steps:
- name: 📚 Git Checkout
uses: actions/checkout@v4

- name: 🎯 Setup Dart
uses: dart-lang/setup-dart@v1

- uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '11'

- name: 🐦 Run Shorebird Tests
run: dart test
working-directory: packages/shorebird_tests
3 changes: 3 additions & 0 deletions packages/shorebird_tests/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# https://dart.dev/guides/libraries/private-files
# Created by `dart pub`
.dart_tool/
1 change: 1 addition & 0 deletions packages/shorebird_tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
A dart project that includes tests that perfoma asserts in the modifications made on the Flutter framework by the Shorebird team.
2 changes: 2 additions & 0 deletions packages/shorebird_tests/analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# This file configures the static analysis results for your project (errors,
include: package:lints/recommended.yaml
15 changes: 15 additions & 0 deletions packages/shorebird_tests/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
name: shorebird_tests
description: Shorebird's Flutter customizations tests
version: 1.0.0

environment:
sdk: ^3.3.4

dependencies:
archive: ^3.5.1
path: ^1.9.0
Comment on lines +8 to +10
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need these dependencies?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


dev_dependencies:
lints: ^3.0.0
meta: ^1.15.0
test: ^1.24.0
74 changes: 74 additions & 0 deletions packages/shorebird_tests/test/android_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import 'package:test/test.dart';

import 'shorebird_tests.dart';

void main() {
group('shorebird android projects', () {
testWithShorebirdProject('can build an apk', (projectDirectory) async {
await projectDirectory.runFlutterBuildApk();

expect(projectDirectory.apkFile().existsSync(), isTrue);
expect(projectDirectory.shorebirdFile.existsSync(), isTrue);
});

testWithShorebirdProject(
'adds the public key when passed through the environment variable',
(projectDirectory) async {
const base64PublicKey = 'public_123';
await projectDirectory.runFlutterBuildApk(
environment: {
'SHOREBIRD_PUBLIC_KEY': base64PublicKey,
},
);

final generatedYaml =
await projectDirectory.getGeneratedShorebirdYaml();

expect(generatedYaml, contains('patch_public_key: $base64PublicKey'));
},
);

group('when building with a flavor', () {
testWithShorebirdProject(
'correctly changes the app id',
(projectDirectory) async {
projectDirectory.addAndroidFlavors();
projectDirectory.addShorebirdFlavors();

await projectDirectory.runFlutterBuildApk(flavor: 'internal');

final generatedYaml =
await projectDirectory.getGeneratedShorebirdYaml(
flavor: 'internal',
);

expect(generatedYaml, contains('app_id: internal_123'));
},
);

testWithShorebirdProject(
'correctly changes the app id and adds the public key when passed through the environment variable',
(projectDirectory) async {
const base64PublicKey = 'public_123';
projectDirectory.addAndroidFlavors();
projectDirectory.addShorebirdFlavors();

await projectDirectory.runFlutterBuildApk(
flavor: 'internal',
environment: {
'SHOREBIRD_PUBLIC_KEY': base64PublicKey,
},
);

final generatedYaml =
await projectDirectory.getGeneratedShorebirdYaml(
flavor: 'internal',
);

expect(generatedYaml, contains('app_id: internal_123'));
expect(generatedYaml, contains('patch_public_key: $base64PublicKey'));
},
);
});
});
}
15 changes: 15 additions & 0 deletions packages/shorebird_tests/test/base_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import 'package:test/test.dart';

import 'shorebird_tests.dart';

void main() {
group('shorebird helpers', () {
testWithShorebirdProject('can build a base project',
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we even need this test?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, in theory we don't, but I kind like having basic ones like that cause it makes it easier to track down a test that broke in an "earlier" layer.

Happy to remove them if you think they just "fill up space"

(projectDirectory) async {
expect(projectDirectory.existsSync(), isTrue);

expect(projectDirectory.pubspecFile.existsSync(), isTrue);
expect(projectDirectory.shorebirdFile.existsSync(), isTrue);
});
});
}
204 changes: 204 additions & 0 deletions packages/shorebird_tests/test/shorebird_tests.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
import 'dart:async';
import 'dart:io';

import 'package:archive/archive_io.dart';
import 'package:path/path.dart' as path;

import 'package:meta/meta.dart';
import 'package:test/test.dart';

/// This will be the path to the flutter binary housed in this flutter repository.
///
/// Which since we are running the tests from this inner package , we need to go up two directories
/// in order to find the flutter binary in the bin folder.
File get _flutterBinaryFile => File(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doc comments throughout this file would be useful. Here: "this assumes we're running from (path)" or similar

path.join(
Directory.current.path,
'..',
'..',
'bin',
'flutter${Platform.isWindows ? '.bat' : ''}',
),
);

/// Runs a flutter command using the correct binary ([_flutterBinaryFile]) with the given arguments.
Future<ProcessResult> _runFlutterCommand(
List<String> arguments, {
required Directory workingDirectory,
Map<String, String>? environment,
}) async {
return Process.run(
_flutterBinaryFile.absolute.path,
arguments,
workingDirectory: workingDirectory.path,
environment: {
'FLUTTER_STORAGE_BASE_URL': 'https://download.shorebird.dev',
if (environment != null) ...environment,
},
);
}

Future<void> _createFlutterProject(Directory projectDirectory) async {
final result = await _runFlutterCommand(
['create', '--empty', '.'],
workingDirectory: projectDirectory,
);
if (result.exitCode != 0) {
throw Exception('Failed to create Flutter project: ${result.stderr}');
}
}

@isTest
Future<void> testWithShorebirdProject(String name,
FutureOr<void> Function(Directory projectDirectory) testFn) async {
test(
name,
() async {
final parentDirectory = Directory.systemTemp.createTempSync();
final projectDirectory = Directory(
path.join(
parentDirectory.path,
'shorebird_test',
),
)..createSync();

File(
path.join(
projectDirectory.path,
'shorebird.yaml',
),
).writeAsStringSync('''
app_id: 123
''');

try {
await _createFlutterProject(projectDirectory);

projectDirectory.pubspecFile.writeAsStringSync('''
${projectDirectory.pubspecFile.readAsStringSync()}

assets:
- shorebird.yaml
''');
await testFn(projectDirectory);
} finally {
projectDirectory.deleteSync(recursive: true);
}
},
timeout: Timeout(
// These tests usually run flutter create, flutter build, etc, which can take a while,
// specially in CI, so setting from the default of 30 seconds to 6 minutes.
Duration(minutes: 6),
),
);
}

extension ShorebirdProjectDirectoryOnDirectory on Directory {
File get pubspecFile => File(
path.join(this.path, 'pubspec.yaml'),
);

File get shorebirdFile => File(
path.join(this.path, 'shorebird.yaml'),
);

File get appGradleFile => File(
path.join(this.path, 'android', 'app', 'build.gradle'),
);

void addAndroidFlavors() {
// TODO(erickzanardo): Maybe in the future make this more dynamic
// and allow the user to pass the flavors, but it is good for now.
const flavors = '''
flavorDimensions "track"
productFlavors {
playStore {
dimension "track"
applicationIdSuffix ".ps"
}
internal {
dimension "track"
applicationIdSuffix ".internal"
}
global {
dimension "track"
applicationIdSuffix ".global"
}
}
''';

final currentGradleContent = appGradleFile.readAsStringSync();
appGradleFile.writeAsStringSync(
'''
${currentGradleContent.replaceFirst(
' buildTypes {',
' $flavors\n buildTypes {',
)}
''',
);
}

void addShorebirdFlavors() {
const flavors = '''
flavors:
global: global_123
internal: internal_123
playStore: playStore_123
''';

final currentShorebirdContent = shorebirdFile.readAsStringSync();
shorebirdFile.writeAsStringSync(
'''
$currentShorebirdContent
$flavors
''',
);
}

Future<void> runFlutterBuildApk({
String? flavor,
Map<String, String>? environment,
}) async {
final result = await _runFlutterCommand(
[
'build',
'apk',
if (flavor != null) '--flavor=$flavor',
],
workingDirectory: this,
environment: environment,
);
if (result.exitCode != 0) {
throw Exception('Failed to run `flutter build apk`: ${result.stderr}');
}
}

File apkFile({String? flavor}) => File(
path.join(
this.path,
'build',
'app',
'outputs',
'flutter-apk',
'app-${flavor != null ? '$flavor-' : ''}release.apk',
),
);

Future<String> getGeneratedShorebirdYaml({String? flavor}) async {
final decodedBytes =
ZipDecoder().decodeBytes(apkFile(flavor: flavor).readAsBytesSync());

await extractArchiveToDisk(
decodedBytes, path.join(this.path, 'apk-extracted'));

return File(
path.join(
this.path,
'apk-extracted',
'assets',
'flutter_assets',
'shorebird.yaml',
),
).readAsStringSync();
}
}
Loading