Skip to content

Commit a1d4df6

Browse files
authored
perf: speed up shorebird_tests with template project and Gradle cache (#108)
- Create a template Flutter project once in setUpAll and copy it per test, avoiding repeated `flutter create` calls - Run a warm-up `flutter build apk` in setUpAll (outside per-test timeout) to prime the Gradle cache - Add actions/cache for ~/.gradle so subsequent CI runs start warm - Add VERBOSE env var and failure output logging from #107
1 parent 8f6ff05 commit a1d4df6

File tree

5 files changed

+100
-25
lines changed

5 files changed

+100
-25
lines changed

.github/workflows/shorebird_ci.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,17 @@ jobs:
3838
distribution: "zulu"
3939
java-version: "17"
4040

41+
- name: 📦 Cache Gradle
42+
if: ${{ matrix.os == 'ubuntu-latest' || matrix.os == 'macos-latest' }}
43+
uses: actions/cache@v4
44+
with:
45+
path: |
46+
~/.gradle/caches
47+
~/.gradle/wrapper
48+
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
49+
restore-keys: |
50+
${{ runner.os }}-gradle-
51+
4152
- name: 🐦 Run Flutter Tools Tests
4253
# TODO(eseidel): Find a nice way to run this on windows.
4354
if: ${{ matrix.os == 'ubuntu-latest' || matrix.os == 'macos-latest' }}

packages/shorebird_tests/test/android_test.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import 'package:test/test.dart';
33
import 'shorebird_tests.dart';
44

55
void main() {
6+
setUpAll(warmUpTemplateProject);
7+
68
group('shorebird android projects', () {
79
testWithShorebirdProject('can build an apk', (projectDirectory) async {
810
await projectDirectory.runFlutterBuildApk();

packages/shorebird_tests/test/base_test.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import 'package:test/test.dart';
33
import 'shorebird_tests.dart';
44

55
void main() {
6+
setUpAll(warmUpTemplateProject);
7+
68
group('shorebird helpers', () {
79
testWithShorebirdProject('can build a base project',
810
(projectDirectory) async {

packages/shorebird_tests/test/ios_test.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import 'package:test/test.dart';
33
import 'shorebird_tests.dart';
44

55
void main() {
6+
setUpAll(warmUpTemplateProject);
7+
68
group(
79
'shorebird ios projects',
810
() {

packages/shorebird_tests/test/shorebird_tests.dart

Lines changed: 83 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -91,46 +91,104 @@ Future<void> _createFlutterProject(Directory projectDirectory) async {
9191
}
9292
}
9393

94-
@isTest
95-
Future<void> testWithShorebirdProject(String name,
96-
FutureOr<void> Function(Directory projectDirectory) testFn) async {
97-
test(
98-
name,
99-
() async {
100-
final parentDirectory = Directory.systemTemp.createTempSync();
101-
final projectDirectory = Directory(
102-
path.join(
103-
parentDirectory.path,
104-
'shorebird_test',
105-
),
106-
)..createSync();
94+
/// Cached template project directory, created once and reused across tests.
95+
///
96+
/// This avoids running `flutter create` for every test, which saves
97+
/// significant time (especially the first Gradle/SDK download).
98+
Directory? _templateProject;
10799

108-
try {
109-
await _createFlutterProject(projectDirectory);
100+
/// Creates (or returns the cached) template Flutter project with
101+
/// shorebird.yaml configured. The first call runs `flutter create` and
102+
/// `flutter build apk` to warm up Gradle caches.
103+
///
104+
/// Call this from `setUpAll` so the expensive setup runs outside per-test
105+
/// timeouts.
106+
Future<void> warmUpTemplateProject() => _getTemplateProject();
107+
108+
Future<Directory> _getTemplateProject() async {
109+
if (_templateProject != null) {
110+
return _templateProject!;
111+
}
112+
113+
final Directory templateDir = Directory(
114+
path.join(Directory.systemTemp.createTempSync().path, 'shorebird_template'),
115+
)..createSync();
110116

111-
projectDirectory.pubspecFile.writeAsStringSync('''
112-
${projectDirectory.pubspecFile.readAsStringSync()}
117+
await _createFlutterProject(templateDir);
118+
119+
templateDir.pubspecFile.writeAsStringSync('''
120+
${templateDir.pubspecFile.readAsStringSync()}
113121
assets:
114122
- shorebird.yaml
115123
''');
116124

117-
File(
118-
path.join(
119-
projectDirectory.path,
120-
'shorebird.yaml',
121-
),
122-
).writeAsStringSync('''
125+
File(
126+
path.join(templateDir.path, 'shorebird.yaml'),
127+
).writeAsStringSync('''
123128
app_id: "123"
124129
''');
125130

131+
// Warm up the Gradle cache with a throwaway build so subsequent
132+
// per-test builds are fast and don't hit the per-test timeout.
133+
// Skip if Gradle cache is already populated (e.g., from GHA cache restore).
134+
final Directory gradleCache = Directory(
135+
path.join(Platform.environment['HOME'] ?? '', '.gradle', 'caches'),
136+
);
137+
final bool hasGradleCache =
138+
gradleCache.existsSync() && gradleCache.listSync().isNotEmpty;
139+
if (hasGradleCache) {
140+
print('[warmup] Gradle cache exists, skipping warm-up build');
141+
} else {
142+
await _runFlutterCommand(
143+
['build', 'apk'],
144+
workingDirectory: templateDir,
145+
);
146+
}
147+
148+
_templateProject = templateDir;
149+
return templateDir;
150+
}
151+
152+
/// Copies the template project to a fresh directory for test isolation.
153+
Future<Directory> _copyTemplateProject() async {
154+
final Directory template = await _getTemplateProject();
155+
final Directory testDir = Directory(
156+
path.join(Directory.systemTemp.createTempSync().path, 'shorebird_test'),
157+
);
158+
159+
// Use platform copy to preserve the full directory tree efficiently.
160+
if (Platform.isWindows) {
161+
await Process.run('xcopy', [
162+
template.path,
163+
testDir.path,
164+
'/E',
165+
'/I',
166+
'/Q',
167+
]);
168+
} else {
169+
await Process.run('cp', ['-R', template.path, testDir.path]);
170+
}
171+
172+
return testDir;
173+
}
174+
175+
@isTest
176+
Future<void> testWithShorebirdProject(String name,
177+
FutureOr<void> Function(Directory projectDirectory) testFn) async {
178+
test(
179+
name,
180+
() async {
181+
final Directory projectDirectory = await _copyTemplateProject();
182+
183+
try {
126184
await testFn(projectDirectory);
127185
} finally {
128186
projectDirectory.deleteSync(recursive: true);
129187
}
130188
},
131189
timeout: Timeout(
132-
// These tests usually run flutter create, flutter build, etc, which can take a while,
133-
// specially in CI, so setting from the default of 30 seconds to 6 minutes.
190+
// Per-test timeout can be shorter now since the template project
191+
// creation and Gradle warm-up happen outside the test timeout.
134192
Duration(minutes: 6),
135193
),
136194
);

0 commit comments

Comments
 (0)