| // 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 file. |
| |
| /// Parses results.json and flaky.json. |
| |
| import 'dart:async'; |
| import 'dart:convert'; |
| import 'dart:io'; |
| import 'dart:math' as math; |
| |
| import 'package:pool/pool.dart'; |
| |
| /// The path to the gsutil script. |
| late String gsutilPy; |
| |
| // TODO(karlklose): Update this class with all fields that |
| // are used in pkg/test_runner and the tools/bots scripts and include |
| // validation (in particular for fields from extend_results.dart, that are |
| // optional but expected in some contexts and should always be all or nothing). |
| class Result { |
| final String configuration; |
| final String expectation; |
| final bool matches; |
| final String name; |
| final String outcome; |
| final bool? changed; |
| final String? commitHash; |
| final bool flaked; // From optional flakiness_data argument to constructor. |
| final bool? isFlaky; // From results.json after it is extended. |
| final String? previousOutcome; |
| |
| Result( |
| this.configuration, |
| this.name, |
| this.outcome, |
| this.expectation, |
| this.matches, |
| this.changed, |
| this.commitHash, |
| this.isFlaky, |
| this.previousOutcome, { |
| this.flaked = false, |
| }); |
| |
| Result.fromMap( |
| Map<String, dynamic> map, [ |
| Map<String, dynamic>? flakinessData, |
| ]) : configuration = map["configuration"] as String, |
| name = map["name"] as String, |
| outcome = map["result"] as String, |
| expectation = map["expected"] as String, |
| matches = map["matches"] as bool, |
| changed = map["changed"] as bool?, |
| commitHash = map["commit_hash"] as String?, |
| isFlaky = map["flaky"] as bool?, |
| previousOutcome = map["previous_result"] as String?, |
| flaked = flakinessData != null && |
| (flakinessData["active"] ?? true) == true && |
| (flakinessData["outcomes"] as List).contains(map["result"]); |
| |
| String get key => "$configuration:$name"; |
| } |
| |
| /// Cloud storage location containing results. |
| const testResultsStoragePath = "gs://dart-test-results/builders"; |
| |
| /// Limit the number of concurrent subprocesses by half the number of cores. |
| final gsutilPool = Pool(math.max(1, Platform.numberOfProcessors ~/ 2)); |
| |
| /// Runs gsutil with the provided [arguments] and returns the standard output. |
| /// |
| /// Returns null if the requested URL didn't exist. |
| Future<String> runGsutil(List<String> arguments) async { |
| return gsutilPool.withResource(() async { |
| var processResult = await Process.run( |
| "python3", [gsutilPy]..addAll(arguments), |
| runInShell: Platform.isWindows); |
| if (processResult.exitCode != 0) { |
| var stderr = processResult.stderr as String; |
| if (processResult.exitCode == 1 && stderr.contains("No URLs matched") || |
| stderr.contains("One or more URLs matched no objects")) { |
| return ""; |
| } |
| var error = "Failed to run: python3 $gsutilPy $arguments\n" |
| "exitCode: ${processResult.exitCode}\n" |
| "stdout:\n${processResult.stdout}\n" |
| "stderr:\n${processResult.stderr}"; |
| if (processResult.exitCode == 1 && |
| stderr.contains("401 Anonymous caller")) { |
| error = |
| "\n\nYou need to authenticate by running:\npython3 $gsutilPy config\n"; |
| } |
| throw Exception(error); |
| } |
| return processResult.stdout as String; |
| }); |
| } |
| |
| /// Returns the contents of the provided cloud storage [path], or null if it |
| /// didn't exist. |
| Future<String> catGsutil(String path) => runGsutil(["cat", path]); |
| |
| /// Returns the files and directories in the provided cloud storage [directory], |
| /// or null if it didn't exist. |
| Future<Iterable<String>> lsGsutil(String directory) async { |
| var contents = await runGsutil(["ls", directory]); |
| if (contents.isEmpty) { |
| return const []; |
| } |
| return LineSplitter.split(contents).map((String path) { |
| var elements = path.split("/"); |
| if (elements[elements.length - 1].isEmpty) { |
| return elements[elements.length - 2]; |
| } else { |
| return elements[elements.length - 1]; |
| } |
| }); |
| } |
| |
| /// Copies a file to or from cloud storage. |
| Future cpGsutil(String source, String destination) => |
| runGsutil(["cp", source, destination]); |
| |
| /// Copies a directory recursively to or from cloud strorage. |
| Future cpRecursiveGsutil(String source, String destination) => |
| runGsutil(["-m", "cp", "-r", "-Z", source, destination]); |
| |
| /// Lists the bots in cloud storage. |
| Future<Iterable<String>> listBots() => lsGsutil("$testResultsStoragePath"); |
| |
| /// Returns the cloud storage path for the [bot]. |
| String botCloudPath(String bot) => "$testResultsStoragePath/$bot"; |
| |
| /// Returns the cloud storage path to the [build] on the [bot]. |
| String buildCloudPath(String bot, String build) => |
| "${botCloudPath(bot)}/$build"; |
| |
| /// Returns the cloud storage path to the [file] inside the [bot]'s directory. |
| String fileCloudPath(String bot, String file) => "${botCloudPath(bot)}/$file"; |
| |
| /// Reads the contents of the [file] inside the [bot]'s cloud storage. |
| Future<String> readFile(String bot, String file) => |
| catGsutil(fileCloudPath(bot, file)); |
| |
| /// Returns the cloud storage path to the [file] inside the [build] on the |
| /// [bot]. |
| String buildFileCloudPath(String bot, String build, String file) => |
| "${buildCloudPath(bot, build)}/$file"; |
| |
| /// Reads the contents of the [file] inside the [build] in the [bot]'s cloud |
| /// storage. |
| Future<String> readBuildFile(String bot, String build, String file) => |
| catGsutil(buildFileCloudPath(bot, build, file)); |
| |
| List<Map<String, dynamic>> parseResults(String contents) { |
| return LineSplitter.split(contents) |
| .map(jsonDecode) |
| .toList() |
| .cast<Map<String, dynamic>>(); |
| } |
| |
| Future<List<Map<String, dynamic>>> loadResults(String path) async { |
| var results = <Map<String, dynamic>>[]; |
| var lines = File(path) |
| .openRead() |
| .cast<List<int>>() |
| .transform(utf8.decoder) |
| .transform(const LineSplitter()); |
| await for (var line in lines) { |
| results.add(jsonDecode(line) as Map<String, dynamic>); |
| } |
| return results; |
| } |
| |
| Map<String, Map<String, dynamic>> createResultsMap( |
| List<Map<String, dynamic>> results) { |
| var result = <String, Map<String, dynamic>>{}; |
| for (var map in results) { |
| var key = "${map["configuration"]}:${map["name"]}"; |
| result.putIfAbsent(key, () => map); |
| } |
| return result; |
| } |
| |
| Map<String, Map<String, dynamic>> parseResultsMap(String contents) => |
| createResultsMap(parseResults(contents)); |
| |
| Future<Map<String, Map<String, dynamic>>> loadResultsMap(String path) async => |
| createResultsMap(await loadResults(path)); |