@@ -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 ('''
123128app_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