// 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 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 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 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 cmd = [ // 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(this.file, Directory examplesLibDir) : importPath = _getImportPath(file, examplesLibDir), importName = '' { importName = importPath.replaceAll(RegExp(r'\.dart$'), '').replaceAll(RegExp(r'\W'), '_'); } final File file; 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 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 gitFiles = (await runCommand( ['git', 'ls-files', '**/*.dart'], workingDirectory: examplesLibDir, quiet: true, )).replaceAll(r'\', '/') .trim() .split('\n'); final Iterable examples = gitFiles.map((String examplePath) { return filesystem.file(path.join(examplesLibDir.absolute.path, examplePath)); }); // Collect the examples, and import them all as separate symbols. final List imports = []; 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 infoList = []; 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 runCommand( List cmd, { required Directory workingDirectory, bool quiet = false, List? output, Map? environment, }) async { final List stdoutOutput = []; final List combinedOutput = []; final Completer stdoutComplete = Completer(); final Completer stderrComplete = Completer(); late Process process; Future 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 event) { stdoutOutput.addAll(event); combinedOutput.addAll(event); if (!quiet) { stdout.add(event); } }, onDone: () async => stdoutComplete.complete(), ); process.stderr.listen( (List 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); }