blob: 732eb31c84461d560d66e55962256763060252dd [file] [log] [blame]
Greg Spencerab2b0852021-09-28 09:32:06 -07001// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5// This test builds an integration test from the list of samples in the
6// examples/api/lib directory, and then runs it. The tests are just smoke tests,
7// designed to start up each example and run it for a couple of frames to make
8// sure it doesn't throw an exception or fail to compile.
9
10import 'dart:async';
11import 'dart:convert';
Alexandre Ardhuinccd33632022-07-19 00:04:07 +020012import 'dart:io' show Process, ProcessException, exitCode, stderr, stdout;
Greg Spencerab2b0852021-09-28 09:32:06 -070013
14import 'package:file/file.dart';
15import 'package:file/local.dart';
16import 'package:path/path.dart' as path;
17import 'package:platform/platform.dart';
18import 'package:process/process.dart';
19
Greg Spencerab2b0852021-09-28 09:32:06 -070020FileSystem filesystem = const LocalFileSystem();
21ProcessManager processManager = const LocalProcessManager();
22Platform platform = const LocalPlatform();
23
24FutureOr<dynamic> main() async {
25 if (!platform.isLinux && !platform.isWindows && !platform.isMacOS) {
26 stderr.writeln('Example smoke tests are only designed to run on desktop platforms');
27 exitCode = 4;
28 return;
29 }
30 final Directory flutterDir = filesystem.directory(
31 path.absolute(
32 path.dirname(
33 path.dirname(
34 path.dirname(platform.script.toFilePath()),
35 ),
36 ),
37 ),
38 );
39 final Directory apiDir = flutterDir.childDirectory('examples').childDirectory('api');
40 final File integrationTest = await generateTest(apiDir);
41 try {
42 await runSmokeTests(flutterDir: flutterDir, integrationTest: integrationTest, apiDir: apiDir);
43 } finally {
44 await cleanUp(integrationTest);
45 }
46}
47
48Future<void> cleanUp(File integrationTest) async {
49 try {
50 await integrationTest.delete();
51 // Delete the integration_test directory if it is empty.
Ian Hickson61a0add2021-10-08 09:25:14 -070052 await integrationTest.parent.delete();
Greg Spencerab2b0852021-09-28 09:32:06 -070053 } on FileSystemException {
54 // Ignore, there might be other files in there preventing it from
55 // being removed, or it might not exist.
56 }
57}
58
59// Executes the generated smoke test.
60Future<void> runSmokeTests({
61 required Directory flutterDir,
62 required File integrationTest,
63 required Directory apiDir,
64}) async {
65 final File flutterExe =
66 flutterDir.childDirectory('bin').childFile(platform.isWindows ? 'flutter.bat' : 'flutter');
67 final List<String> cmd = <String>[
68 // If we're in a container with no X display, then use the virtual framebuffer.
69 if (platform.isLinux &&
70 (platform.environment['DISPLAY'] == null ||
71 platform.environment['DISPLAY']!.isEmpty)) '/usr/bin/xvfb-run',
72 flutterExe.absolute.path,
73 'test',
74 '--reporter=expanded',
75 '--device-id=${platform.operatingSystem}',
76 integrationTest.absolute.path,
77 ];
78 await runCommand(cmd, workingDirectory: apiDir);
79}
80
81// A class to hold information related to an example, used to generate names
82// from for the tests.
83class ExampleInfo {
84 ExampleInfo(this.file, Directory examplesLibDir)
85 : importPath = _getImportPath(file, examplesLibDir),
86 importName = '' {
87 importName = importPath.replaceAll(RegExp(r'\.dart$'), '').replaceAll(RegExp(r'\W'), '_');
88 }
89
90 final File file;
91 final String importPath;
92 String importName;
93
94 static String _getImportPath(File example, Directory examplesLibDir) {
95 final String relativePath =
96 path.relative(example.absolute.path, from: examplesLibDir.absolute.path);
97 // So that Windows paths are proper URIs in the import statements.
98 return path.toUri(relativePath).toFilePath(windows: false);
99 }
100}
101
102// Generates the combined smoke test.
103Future<File> generateTest(Directory apiDir) async {
104 final Directory examplesLibDir = apiDir.childDirectory('lib');
105
106 // Get files from git, to avoid any non-repo files that might be in someone's
107 // workspace.
108 final List<String> gitFiles = (await runCommand(
109 <String>['git', 'ls-files', '**/*.dart'],
110 workingDirectory: examplesLibDir,
111 quiet: true,
112 )).replaceAll(r'\', '/')
113 .trim()
114 .split('\n');
115 final Iterable<File> examples = gitFiles.map<File>((String examplePath) {
116 return filesystem.file(path.join(examplesLibDir.absolute.path, examplePath));
117 });
118
119 // Collect the examples, and import them all as separate symbols.
120 final List<String> imports = <String>[];
121 imports.add('''import 'package:flutter/widgets.dart';''');
fzyzcjy61deaef2022-11-02 06:50:00 +0800122 imports.add('''import 'package:flutter/scheduler.dart';''');
Greg Spencerab2b0852021-09-28 09:32:06 -0700123 imports.add('''import 'package:flutter_test/flutter_test.dart';''');
124 imports.add('''import 'package:integration_test/integration_test.dart';''');
125 final List<ExampleInfo> infoList = <ExampleInfo>[];
126 for (final File example in examples) {
127 final ExampleInfo info = ExampleInfo(example, examplesLibDir);
128 infoList.add(info);
129 imports.add('''import 'package:flutter_api_samples/${info.importPath}' as ${info.importName};''');
130 }
131 imports.sort();
132 infoList.sort((ExampleInfo a, ExampleInfo b) => a.importPath.compareTo(b.importPath));
133
134 final StringBuffer buffer = StringBuffer();
135 buffer.writeln('// Temporary generated file. Do not commit.');
136 buffer.writeln("import 'dart:io';");
137 buffer.writeAll(imports, '\n');
138 buffer.writeln(r'''
139
140
141import '../../../dev/manual_tests/test/mock_image_http.dart';
142
143void main() {
144 IntegrationTestWidgetsFlutterBinding? binding;
145 try {
146 binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized() as IntegrationTestWidgetsFlutterBinding;
147 } catch (e) {
148 stderr.writeln('Unable to initialize binding${binding == null ? '' : ' $binding'}: $e');
149 exitCode = 128;
150 return;
151 }
152
153''');
154 for (final ExampleInfo info in infoList) {
155 buffer.writeln('''
156 testWidgets(
157 'Smoke test ${info.importPath}',
158 (WidgetTester tester) async {
159 final ErrorWidgetBuilder originalBuilder = ErrorWidget.builder;
160 try {
161 HttpOverrides.runZoned(() {
162 ${info.importName}.main();
163 }, createHttpClient: (SecurityContext? context) => FakeHttpClient(context));
164 await tester.pump();
165 await tester.pump();
166 expect(find.byType(WidgetsApp), findsOneWidget);
167 } finally {
168 ErrorWidget.builder = originalBuilder;
fzyzcjy61deaef2022-11-02 06:50:00 +0800169 timeDilation = 1.0;
Greg Spencerab2b0852021-09-28 09:32:06 -0700170 }
171 },
172 );
173''');
174 }
175 buffer.writeln('}');
176
177 final File integrationTest =
178 apiDir.childDirectory('integration_test').childFile('smoke_integration_test.dart');
179 integrationTest.createSync(recursive: true);
180 integrationTest.writeAsStringSync(buffer.toString());
181 return integrationTest;
182}
183
184// Run a command, and optionally stream the output as it runs, returning the
185// stdout.
186Future<String> runCommand(
187 List<String> cmd, {
188 required Directory workingDirectory,
189 bool quiet = false,
190 List<String>? output,
191 Map<String, String>? environment,
192}) async {
193 final List<int> stdoutOutput = <int>[];
194 final List<int> combinedOutput = <int>[];
195 final Completer<void> stdoutComplete = Completer<void>();
196 final Completer<void> stderrComplete = Completer<void>();
197
198 late Process process;
199 Future<int> allComplete() async {
200 await stderrComplete.future;
201 await stdoutComplete.future;
202 return process.exitCode;
203 }
204
205 try {
206 process = await processManager.start(
207 cmd,
208 workingDirectory: workingDirectory.absolute.path,
Greg Spencerab2b0852021-09-28 09:32:06 -0700209 environment: environment,
210 );
211 process.stdout.listen(
212 (List<int> event) {
213 stdoutOutput.addAll(event);
214 combinedOutput.addAll(event);
215 if (!quiet) {
216 stdout.add(event);
217 }
218 },
219 onDone: () async => stdoutComplete.complete(),
220 );
221 process.stderr.listen(
222 (List<int> event) {
223 combinedOutput.addAll(event);
224 if (!quiet) {
225 stderr.add(event);
226 }
227 },
228 onDone: () async => stderrComplete.complete(),
229 );
230 } on ProcessException catch (e) {
231 stderr.writeln('Running "${cmd.join(' ')}" in ${workingDirectory.path} '
Alexandre Ardhuind40ee212022-05-07 01:04:13 +0200232 'failed with:\n$e');
Greg Spencerab2b0852021-09-28 09:32:06 -0700233 exitCode = 2;
234 return utf8.decode(stdoutOutput);
235 } on ArgumentError catch (e) {
236 stderr.writeln('Running "${cmd.join(' ')}" in ${workingDirectory.path} '
Alexandre Ardhuind40ee212022-05-07 01:04:13 +0200237 'failed with:\n$e');
Greg Spencerab2b0852021-09-28 09:32:06 -0700238 exitCode = 3;
239 return utf8.decode(stdoutOutput);
240 }
241
242 final int processExitCode = await allComplete();
243 if (processExitCode != 0) {
244 stderr.writeln('Running "${cmd.join(' ')}" in ${workingDirectory.path} exited with code $processExitCode');
245 exitCode = processExitCode;
246 }
247
248 if (output != null) {
249 output.addAll(utf8.decode(combinedOutput).split('\n'));
250 }
251
252 return utf8.decode(stdoutOutput);
253}