| // Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file | 
 | // for details. All rights reserved. Use of this source code is governed by a | 
 | // BSD-style license that can be found in the LICENSE.md file. | 
 |  | 
 | // @dart = 2.9 | 
 | import 'dart:async' show StreamSubscription, Timer; | 
 | import 'dart:convert' show jsonEncode; | 
 | import 'dart:io' show File, exitCode; | 
 | import 'dart:isolate' show Isolate, ReceivePort, SendPort; | 
 |  | 
 | import 'package:args/args.dart' show ArgParser; | 
 |  | 
 | import "frontend_server_flutter.dart" show Logger, compileTests; | 
 |  | 
 | const suiteNamePrefix = "flutter_frontend"; | 
 |  | 
 | class Options { | 
 |   final String configurationName; | 
 |   final bool verbose; | 
 |   final bool printFailureLog; | 
 |   final Uri outputDirectory; | 
 |   final String testFilter; | 
 |   final String flutterDir; | 
 |   final String flutterPlatformDir; | 
 |  | 
 |   Options( | 
 |       this.configurationName, | 
 |       this.verbose, | 
 |       this.printFailureLog, | 
 |       this.outputDirectory, | 
 |       this.testFilter, | 
 |       this.flutterDir, | 
 |       this.flutterPlatformDir); | 
 |  | 
 |   static Options parse(List<String> args) { | 
 |     var parser = new ArgParser() | 
 |       ..addOption("named-configuration", | 
 |           abbr: "n", | 
 |           defaultsTo: suiteNamePrefix, | 
 |           help: "configuration name to use for emitting json result files") | 
 |       ..addOption("output-directory", | 
 |           help: "directory to which results.json and logs.json are written") | 
 |       ..addFlag("verbose", | 
 |           abbr: "v", help: "print additional information", defaultsTo: false) | 
 |       ..addFlag("print", | 
 |           abbr: "p", help: "print failure logs", defaultsTo: false) | 
 |       ..addOption("flutterDir") | 
 |       ..addOption("flutterPlatformDir"); | 
 |     var parsedArguments = parser.parse(args); | 
 |     String outputPath = parsedArguments["output-directory"] ?? "."; | 
 |     Uri outputDirectory = Uri.base.resolveUri(Uri.directory(outputPath)); | 
 |     String filter; | 
 |     if (parsedArguments.rest.length == 1) { | 
 |       filter = parsedArguments.rest.single; | 
 |       if (filter.startsWith("$suiteNamePrefix/")) { | 
 |         filter = filter.substring(suiteNamePrefix.length + 1); | 
 |       } | 
 |     } | 
 |     return Options( | 
 |       parsedArguments["named-configuration"], | 
 |       parsedArguments["verbose"], | 
 |       parsedArguments["print"], | 
 |       outputDirectory, | 
 |       filter, | 
 |       parsedArguments["flutterDir"], | 
 |       parsedArguments["flutterPlatformDir"], | 
 |     ); | 
 |   } | 
 | } | 
 |  | 
 | class ResultLogger extends Logger { | 
 |   final SuiteConfiguration suiteConfiguration; | 
 |   final Map<String, Stopwatch> stopwatches = {}; | 
 |   List<String> _log = <String>[]; | 
 |  | 
 |   ResultLogger(this.suiteConfiguration); | 
 |  | 
 |   handleTestResult(String testName, bool matchedExpectations) { | 
 |     String fullTestName = "$suiteNamePrefix/$testName"; | 
 |     suiteConfiguration.resultsPort.send(jsonEncode({ | 
 |       "name": fullTestName, | 
 |       "configuration": suiteConfiguration.configurationName, | 
 |       "suite": suiteNamePrefix, | 
 |       "test_name": testName, | 
 |       "time_ms": stopwatches[testName].elapsedMilliseconds, | 
 |       "expected": "Pass", | 
 |       "result": matchedExpectations ? "Pass" : "Fail", | 
 |       "matches": matchedExpectations, | 
 |     })); | 
 |     if (!matchedExpectations) { | 
 |       String failureLog = _log.join("\n"); | 
 |       failureLog = "$failureLog\n\nRe-run this test: dart --enable-asserts " | 
 |           "pkg/frontend_server/test/test.dart " | 
 |           "--flutterDir=${suiteConfiguration.flutterDir} " | 
 |           "--flutterPlatformDir=${suiteConfiguration.flutterPlatformDir} " | 
 |           "-p $testName"; | 
 |       suiteConfiguration.logsPort.send(jsonEncode({ | 
 |         "name": fullTestName, | 
 |         "configuration": suiteConfiguration.configurationName, | 
 |         "result": matchedExpectations ? "OK" : "Failure", | 
 |         "log": failureLog, | 
 |       })); | 
 |       if (suiteConfiguration.printFailureLog) { | 
 |         print('FAILED: $fullTestName'); | 
 |         print(failureLog); | 
 |       } | 
 |     } | 
 |     if (suiteConfiguration.verbose) { | 
 |       String result = matchedExpectations ? "PASS" : "FAIL"; | 
 |       print("${fullTestName}: ${result}"); | 
 |     } | 
 |   } | 
 |  | 
 |   void logTestStart(String testName) { | 
 |     stopwatches[testName] = Stopwatch()..start(); | 
 |     _log.clear(); | 
 |   } | 
 |  | 
 |   @override | 
 |   void log(String s) { | 
 |     _log.add(s); | 
 |   } | 
 |  | 
 |   @override | 
 |   void notice(String s) { | 
 |     // ignored. | 
 |   } | 
 |  | 
 |   @override | 
 |   void logExpectedResult(String testName) { | 
 |     handleTestResult(testName, true); | 
 |   } | 
 |  | 
 |   @override | 
 |   void logUnexpectedResult(String testName) { | 
 |     handleTestResult(testName, false); | 
 |   } | 
 | } | 
 |  | 
 | const Duration timeoutDuration = Duration(minutes: 45); | 
 |  | 
 | class SuiteConfiguration { | 
 |   final SendPort resultsPort; | 
 |   final SendPort logsPort; | 
 |   final bool verbose; | 
 |   final bool printFailureLog; | 
 |   final String configurationName; | 
 |   final String testFilter; | 
 |  | 
 |   final int shard; | 
 |   final int shards; | 
 |  | 
 |   final String flutterDir; | 
 |   final String flutterPlatformDir; | 
 |  | 
 |   const SuiteConfiguration( | 
 |       this.resultsPort, | 
 |       this.logsPort, | 
 |       this.verbose, | 
 |       this.printFailureLog, | 
 |       this.configurationName, | 
 |       this.testFilter, | 
 |       this.flutterDir, | 
 |       this.flutterPlatformDir, | 
 |       this.shard, | 
 |       this.shards); | 
 | } | 
 |  | 
 | void runSuite(SuiteConfiguration configuration) async { | 
 |   ResultLogger logger = ResultLogger(configuration); | 
 |   try { | 
 |     await compileTests( | 
 |       configuration.flutterDir, | 
 |       configuration.flutterPlatformDir, | 
 |       logger, | 
 |       filter: configuration.testFilter, | 
 |       shard: configuration.shard, | 
 |       shards: configuration.shards, | 
 |     ); | 
 |   } catch (e) { | 
 |     logger.logUnexpectedResult("startup"); | 
 |   } | 
 | } | 
 |  | 
 | void writeLinesToFile(Uri uri, List<String> lines) async { | 
 |   await File.fromUri(uri).writeAsString(lines.map((line) => "$line\n").join()); | 
 | } | 
 |  | 
 | main([List<String> arguments = const <String>[]]) async { | 
 |   List<String> results = []; | 
 |   List<String> logs = []; | 
 |   Options options = Options.parse(arguments); | 
 |   ReceivePort resultsPort = new ReceivePort() | 
 |     ..listen((resultEntry) => results.add(resultEntry)); | 
 |   ReceivePort logsPort = new ReceivePort() | 
 |     ..listen((logEntry) => logs.add(logEntry)); | 
 |   String filter = options.testFilter; | 
 |  | 
 |   const int shards = 4; | 
 |   List<Future<bool>> futures = []; | 
 |   for (int shard = 0; shard < shards; shard++) { | 
 |     // Start the test suite in a new isolate. | 
 |     ReceivePort exitPort = new ReceivePort(); | 
 |     ReceivePort errorPort = new ReceivePort(); | 
 |     SuiteConfiguration configuration = new SuiteConfiguration( | 
 |       resultsPort.sendPort, | 
 |       logsPort.sendPort, | 
 |       options.verbose, | 
 |       options.printFailureLog, | 
 |       options.configurationName, | 
 |       filter, | 
 |       options.flutterDir, | 
 |       options.flutterPlatformDir, | 
 |       shard, | 
 |       shards, | 
 |     ); | 
 |     Future<bool> future = Future<bool>(() async { | 
 |       Stopwatch stopwatch = Stopwatch()..start(); | 
 |       print("Running suite shard $shard of $shards"); | 
 |       Isolate isolate = await Isolate.spawn<SuiteConfiguration>( | 
 |           runSuite, configuration, | 
 |           onExit: exitPort.sendPort, onError: errorPort.sendPort); | 
 |       bool gotError = false; | 
 |       StreamSubscription errorSubscription = errorPort.listen((message) { | 
 |         print("Got error: $message!"); | 
 |         gotError = true; | 
 |         logs.add("$message"); | 
 |       }); | 
 |       bool timedOut = false; | 
 |       Timer timer = Timer(timeoutDuration, () { | 
 |         timedOut = true; | 
 |         print("Suite timed out after " | 
 |             "${timeoutDuration.inMilliseconds}ms"); | 
 |         isolate.kill(priority: Isolate.immediate); | 
 |       }); | 
 |       await exitPort.first; | 
 |       errorSubscription.cancel(); | 
 |       timer.cancel(); | 
 |       if (!timedOut && !gotError) { | 
 |         int seconds = stopwatch.elapsedMilliseconds ~/ 1000; | 
 |         print("Suite finished (shard #$shard) (took ${seconds} seconds)"); | 
 |       } | 
 |       return timedOut || gotError; | 
 |     }); | 
 |     futures.add(future); | 
 |   } | 
 |  | 
 |   // Wait for isolate to terminate and clean up. | 
 |   Iterable<bool> timeoutsOrCrashes = await Future.wait(futures); | 
 |   bool timeoutOrCrash = timeoutsOrCrashes.any((timeout) => timeout); | 
 |   resultsPort.close(); | 
 |   logsPort.close(); | 
 |  | 
 |   // Write results.json and logs.json. | 
 |   Uri resultJsonUri = options.outputDirectory.resolve("results.json"); | 
 |   Uri logsJsonUri = options.outputDirectory.resolve("logs.json"); | 
 |   await writeLinesToFile(resultJsonUri, results); | 
 |   await writeLinesToFile(logsJsonUri, logs); | 
 |   print("Log files written to ${resultJsonUri.toFilePath()} and" | 
 |       " ${logsJsonUri.toFilePath()}"); | 
 |  | 
 |   exitCode = timeoutOrCrash ? 1 : 0; | 
 | } |