| // Copyright 2014 The Flutter Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| // This test builds an integration test from the list of samples in the | |
| // examples/api/lib directory, and then runs it. The tests are just smoke tests, | |
| // designed to start up each example and run it for a couple of frames to make | |
| // sure it doesn't throw an exception or fail to compile. | |
| import 'dart:async'; | |
| import 'dart:convert'; | |
| import 'dart:io' show Process, ProcessException, exitCode, stderr, stdout; | |
| import 'package:file/file.dart'; | |
| import 'package:file/local.dart'; | |
| import 'package:path/path.dart' as path; | |
| import 'package:platform/platform.dart'; | |
| import 'package:process/process.dart'; | |
| FileSystem filesystem = const LocalFileSystem(); | |
| ProcessManager processManager = const LocalProcessManager(); | |
| Platform platform = const LocalPlatform(); | |
| FutureOr<dynamic> main() async { | |
| if (!platform.isLinux && !platform.isWindows && !platform.isMacOS) { | |
| stderr.writeln('Example smoke tests are only designed to run on desktop platforms'); | |
| exitCode = 4; | |
| return; | |
| } | |
| final Directory flutterDir = filesystem.directory( | |
| path.absolute( | |
| path.dirname( | |
| path.dirname( | |
| path.dirname(platform.script.toFilePath()), | |
| ), | |
| ), | |
| ), | |
| ); | |
| final Directory apiDir = flutterDir.childDirectory('examples').childDirectory('api'); | |
| final File integrationTest = await generateTest(apiDir); | |
| try { | |
| await runSmokeTests(flutterDir: flutterDir, integrationTest: integrationTest, apiDir: apiDir); | |
| } finally { | |
| await cleanUp(integrationTest); | |
| } | |
| } | |
| Future<void> cleanUp(File integrationTest) async { | |
| try { | |
| await integrationTest.delete(); | |
| // Delete the integration_test directory if it is empty. | |
| await integrationTest.parent.delete(); | |
| } on FileSystemException { | |
| // Ignore, there might be other files in there preventing it from | |
| // being removed, or it might not exist. | |
| } | |
| } | |
| // Executes the generated smoke test. | |
| Future<void> runSmokeTests({ | |
| required Directory flutterDir, | |
| required File integrationTest, | |
| required Directory apiDir, | |
| }) async { | |
| final File flutterExe = | |
| flutterDir.childDirectory('bin').childFile(platform.isWindows ? 'flutter.bat' : 'flutter'); | |
| final List<String> cmd = <String>[ | |
| // If we're in a container with no X display, then use the virtual framebuffer. | |
| if (platform.isLinux && | |
| (platform.environment['DISPLAY'] == null || | |
| platform.environment['DISPLAY']!.isEmpty)) '/usr/bin/xvfb-run', | |
| flutterExe.absolute.path, | |
| 'test', | |
| '--reporter=expanded', | |
| '--device-id=${platform.operatingSystem}', | |
| integrationTest.absolute.path, | |
| ]; | |
| await runCommand(cmd, workingDirectory: apiDir); | |
| } | |
| // A class to hold information related to an example, used to generate names | |
| // from for the tests. | |
| class ExampleInfo { | |
| ExampleInfo(File file, Directory examplesLibDir) | |
| : importPath = _getImportPath(file, examplesLibDir), | |
| importName = '' { | |
| importName = importPath.replaceAll(RegExp(r'\.dart$'), '').replaceAll(RegExp(r'\W'), '_'); | |
| } | |
| final String importPath; | |
| String importName; | |
| static String _getImportPath(File example, Directory examplesLibDir) { | |
| final String relativePath = | |
| path.relative(example.absolute.path, from: examplesLibDir.absolute.path); | |
| // So that Windows paths are proper URIs in the import statements. | |
| return path.toUri(relativePath).toFilePath(windows: false); | |
| } | |
| } | |
| // Generates the combined smoke test. | |
| Future<File> generateTest(Directory apiDir) async { | |
| final Directory examplesLibDir = apiDir.childDirectory('lib'); | |
| // Get files from git, to avoid any non-repo files that might be in someone's | |
| // workspace. | |
| final List<String> gitFiles = (await runCommand( | |
| <String>['git', 'ls-files', '**/*.dart'], | |
| workingDirectory: examplesLibDir, | |
| quiet: true, | |
| )).replaceAll(r'\', '/') | |
| .trim() | |
| .split('\n'); | |
| final Iterable<File> examples = gitFiles.map<File>((String examplePath) { | |
| return filesystem.file(path.join(examplesLibDir.absolute.path, examplePath)); | |
| }); | |
| // Collect the examples, and import them all as separate symbols. | |
| final List<String> imports = <String>[]; | |
| imports.add('''import 'package:flutter/widgets.dart';'''); | |
| imports.add('''import 'package:flutter/scheduler.dart';'''); | |
| imports.add('''import 'package:flutter_test/flutter_test.dart';'''); | |
| imports.add('''import 'package:integration_test/integration_test.dart';'''); | |
| final List<ExampleInfo> infoList = <ExampleInfo>[]; | |
| for (final File example in examples) { | |
| final ExampleInfo info = ExampleInfo(example, examplesLibDir); | |
| infoList.add(info); | |
| imports.add('''import 'package:flutter_api_samples/${info.importPath}' as ${info.importName};'''); | |
| } | |
| imports.sort(); | |
| infoList.sort((ExampleInfo a, ExampleInfo b) => a.importPath.compareTo(b.importPath)); | |
| final StringBuffer buffer = StringBuffer(); | |
| buffer.writeln('// Temporary generated file. Do not commit.'); | |
| buffer.writeln("import 'dart:io';"); | |
| buffer.writeAll(imports, '\n'); | |
| buffer.writeln(r''' | |
| import '../../../dev/manual_tests/test/mock_image_http.dart'; | |
| void main() { | |
| IntegrationTestWidgetsFlutterBinding? binding; | |
| try { | |
| binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized() as IntegrationTestWidgetsFlutterBinding; | |
| } catch (e) { | |
| stderr.writeln('Unable to initialize binding${binding == null ? '' : ' $binding'}: $e'); | |
| exitCode = 128; | |
| return; | |
| } | |
| '''); | |
| for (final ExampleInfo info in infoList) { | |
| buffer.writeln(''' | |
| testWidgets( | |
| 'Smoke test ${info.importPath}', | |
| (WidgetTester tester) async { | |
| final ErrorWidgetBuilder originalBuilder = ErrorWidget.builder; | |
| try { | |
| HttpOverrides.runZoned(() { | |
| ${info.importName}.main(); | |
| }, createHttpClient: (SecurityContext? context) => FakeHttpClient(context)); | |
| await tester.pump(); | |
| await tester.pump(); | |
| expect(find.byType(WidgetsApp), findsOneWidget); | |
| } finally { | |
| ErrorWidget.builder = originalBuilder; | |
| timeDilation = 1.0; | |
| } | |
| }, | |
| ); | |
| '''); | |
| } | |
| buffer.writeln('}'); | |
| final File integrationTest = | |
| apiDir.childDirectory('integration_test').childFile('smoke_integration_test.dart'); | |
| integrationTest.createSync(recursive: true); | |
| integrationTest.writeAsStringSync(buffer.toString()); | |
| return integrationTest; | |
| } | |
| // Run a command, and optionally stream the output as it runs, returning the | |
| // stdout. | |
| Future<String> runCommand( | |
| List<String> cmd, { | |
| required Directory workingDirectory, | |
| bool quiet = false, | |
| List<String>? output, | |
| Map<String, String>? environment, | |
| }) async { | |
| final List<int> stdoutOutput = <int>[]; | |
| final List<int> combinedOutput = <int>[]; | |
| final Completer<void> stdoutComplete = Completer<void>(); | |
| final Completer<void> stderrComplete = Completer<void>(); | |
| late Process process; | |
| Future<int> allComplete() async { | |
| await stderrComplete.future; | |
| await stdoutComplete.future; | |
| return process.exitCode; | |
| } | |
| try { | |
| process = await processManager.start( | |
| cmd, | |
| workingDirectory: workingDirectory.absolute.path, | |
| environment: environment, | |
| ); | |
| process.stdout.listen( | |
| (List<int> event) { | |
| stdoutOutput.addAll(event); | |
| combinedOutput.addAll(event); | |
| if (!quiet) { | |
| stdout.add(event); | |
| } | |
| }, | |
| onDone: () async => stdoutComplete.complete(), | |
| ); | |
| process.stderr.listen( | |
| (List<int> event) { | |
| combinedOutput.addAll(event); | |
| if (!quiet) { | |
| stderr.add(event); | |
| } | |
| }, | |
| onDone: () async => stderrComplete.complete(), | |
| ); | |
| } on ProcessException catch (e) { | |
| stderr.writeln('Running "${cmd.join(' ')}" in ${workingDirectory.path} ' | |
| 'failed with:\n$e'); | |
| exitCode = 2; | |
| return utf8.decode(stdoutOutput); | |
| } on ArgumentError catch (e) { | |
| stderr.writeln('Running "${cmd.join(' ')}" in ${workingDirectory.path} ' | |
| 'failed with:\n$e'); | |
| exitCode = 3; | |
| return utf8.decode(stdoutOutput); | |
| } | |
| final int processExitCode = await allComplete(); | |
| if (processExitCode != 0) { | |
| stderr.writeln('Running "${cmd.join(' ')}" in ${workingDirectory.path} exited with code $processExitCode'); | |
| exitCode = processExitCode; | |
| } | |
| if (output != null) { | |
| output.addAll(utf8.decode(combinedOutput).split('\n')); | |
| } | |
| return utf8.decode(stdoutOutput); | |
| } |