blob: 88b2eb12b5b8da53d2de42e1fe294d69dfb935e6 [file] [log] [blame]
// 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.
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;
// TODO(karlklose): this field is unnecessary with extended results and
// should be removed.
final bool flaked;
final bool isFlaky;
final String 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["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
"python", [gsutilPy]..addAll(arguments),
runInShell: Platform.isWindows);
var stderr = processResult.stderr as String;
if (processResult.exitCode != 0) {
if (processResult.exitCode == 1 && stderr.contains("No URLs matched") ||
stderr.contains("One or more URLs matched no objects")) {
return null;
var error = "Failed to run: python $gsutilPy $arguments\n"
"exitCode: ${processResult.exitCode}\n"
if (processResult.exitCode == 1 &&
stderr.contains("401 Anonymous caller")) {
error =
"\n\nYou need to authenticate by running:\npython $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 == null) {
return null;
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) =>
/// 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)
.cast<Map<String, dynamic>>();
Future<List<Map<String, dynamic>>> loadResults(String path) async {
var results = <Map<String, dynamic>>[];
var lines = File(path)
.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) =>
Future<Map<String, Map<String, dynamic>>> loadResultsMap(String path) async =>
createResultsMap(await loadResults(path));