blob: d005ef1120c664c84f074addcd4ebc54f40dacdc [file] [log] [blame]
// Copyright (c) 2017, 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.
import 'dart:async';
import 'package:args/command_runner.dart';
import 'package:args/args.dart';
import 'package:gardening/src/luci.dart';
import 'package:gardening/src/luci_api.dart';
import 'package:gardening/src/results/status_expectations.dart';
import 'package:gardening/src/results/test_result_service.dart';
import 'package:gardening/src/util.dart';
import 'package:gardening/src/console_table.dart';
import 'package:gardening/src/results/result_models.dart' as models;
import 'package:gardening/src/results/util.dart';
import 'package:gardening/src/logdog_new.dart';
import 'package:gardening/src/logdog_rpc.dart';
import 'package:gardening/src/buildbucket.dart';
import 'package:gardening/src/extended_printer.dart';
/// Build standard arguments for input.
void buildArgs(ArgParser argParser) {
argParser.addFlag('scripting',
defaultsTo: false,
abbr: 's',
negatable: false,
help: "Use flag to remove templated output.");
}
/// Get output table based on arguments passed in [argResults].
OutputTable getOutputTable(ArgResults argResults) {
if (argResults["scripting"]) {
return new ScriptTable();
}
return new ConsoleTable(template: rows);
}
/// Determine if arguments is a CQ url or commit-number + patchset.
bool isCqInput(ArgResults argResults) {
if (argResults.rest.length == 1) {
return isSwarmingTaskUrl(argResults.rest.first);
}
if (argResults.rest.length == 2) {
return areNumbers(argResults.rest);
}
return false;
}
String howToUse = "Use by calling one of the following:\n\n"
"\tget <command> <file> : where file is a local path.\n"
"\tget <command> <uri_to_result_log> : for direct links to result.logs.\n"
"\tget <command> <uri_try_bot> : for links to try bot builders.\n"
"\tget <command> <commit_number> <patchset> : for links to try bot builders.\n"
"\tget <command> <builder> : for a builder name.\n"
"\tget <command> <builder> <build> : for a builder and build number.\n"
"\tget <command> <builder_group> : for a builder group.\n";
/// Utility method to get a single test-result no matter what has been passed in
/// as arguments. The test-result can either be from a builder-group, a single
/// build on a builder or from a log.
Future<models.TestResult> getTestResult(ArgResults argResults) async {
if (argResults.rest.length == 0) {
print("No result.log file given as argument.");
print(howToUse);
return null;
}
var logger = createLogger();
var cache = createCacheFunction(logger);
var testResultService = new TestResultService(logger, cache);
String firstArgument = argResults.rest.first;
var luciApi = new LuciApi();
bool isBuilderGroup = (await getBuilderGroups(luciApi, DART_CLIENT, cache()))
.any((builder) => builder == firstArgument);
bool isBuilder = (await getAllBuilders(luciApi, DART_CLIENT, cache()))
.any((builder) => builder == firstArgument);
if (argResults.rest.length == 1) {
if (argResults.rest.first.startsWith("http")) {
return testResultService.getTestResult(firstArgument);
} else if (isBuilderGroup) {
return testResultService.forBuilderGroup(firstArgument);
} else if (isBuilder) {
return testResultService.latestForBuilder(BUILDER_PROJECT, firstArgument);
}
}
if (argResults.rest.length == 2 &&
isBuilder &&
isNumber(argResults.rest[1])) {
var buildNumber = int.parse(argResults.rest[1]);
return testResultService.forBuild(
BUILDER_PROJECT, argResults.rest[0], buildNumber);
}
print("Too many arguments passed to command or arguments were incorrect.");
print(howToUse);
return null;
}
/// Utility method to get test results from the CQ.
Future<Iterable<BuildBucketTestResult>> getTestResultsFromCq(
ArgResults argResults) async {
if (argResults.rest.length == 0) {
print("No result.log file given as argument.");
print(howToUse);
return null;
}
var logger = createLogger();
var createCache = createCacheFunction(logger);
var testResultService = new TestResultService(logger, createCache);
String firstArgument = argResults.rest.first;
if (argResults.rest.length == 1) {
if (!isSwarmingTaskUrl(firstArgument)) {
print("URI does not match "
"`https://ci.chromium.org/swarming/task/<taskid>?server...`.");
print(howToUse);
return null;
}
String swarmingTaskId = getSwarmingTaskId(firstArgument);
return await testResultService.getFromSwarmingTaskId(swarmingTaskId);
}
if (argResults.rest.length == 2 && areNumbers(argResults.rest)) {
int changeNumber = int.parse(firstArgument);
int patchset = int.parse(argResults.rest.last);
return await testResultService.fromGerrit(changeNumber, patchset);
}
print("Too many arguments passed to command or arguments were incorrect.");
print(howToUse);
return null;
}
/// [GetCommand] handles when given command 'get' and expect a sub-command.
class GetCommand extends Command {
@override
String get description => "Get results for files and test-suites.";
@override
String get name => "get";
GetCommand() {
addSubcommand(new GetTestsWithResultCommand());
addSubcommand(new GetTestsWithResultAndExpectationCommand());
addSubcommand(new GetTestFailuresCommand());
addSubcommand(new GetTestMatrix());
}
}
/// [GetTestsWithResultCommand] handles when given the sub-command 'tests' and
/// returns a list of tests with their respective results.
class GetTestsWithResultCommand extends Command {
@override
String get description => "Get results for tests.";
@override
String get name => "tests";
GetTestsWithResultCommand() {
buildArgs(argParser);
}
Future run() async {
models.TestResult testResults = await getTestResult(argResults);
if (testResults == null) {
return;
}
var outputTable = getOutputTable(argResults)
..addHeader(new Column("Test", width: 60), (item) {
return item.name;
})
..addHeader(new Column("Result"), (item) {
return item.result;
});
outputTable.print(testResults.results);
}
}
/// [GetTestsWithResultAndExpectationCommand] handles when given the sub-command
/// 'result' and returns a list of tests with their result and expectations.
class GetTestsWithResultAndExpectationCommand extends Command {
@override
String get description => "Get results and expectations for tests.";
@override
String get name => "results";
GetTestsWithResultAndExpectationCommand() {
buildArgs(argParser);
}
Future run() async {
models.TestResult testResult = null;
if (isCqInput(argResults)) {
Iterable<BuildBucketTestResult> buildBucketTestResults =
await getTestResultsFromCq(argResults);
Iterable<models.TestResult> testResults =
buildBucketTestResults.map((build) => build.testResult);
testResult = new models.TestResult()..combineWith(testResults);
} else {
testResult = await getTestResult(argResults);
}
if (testResult == null) {
return;
}
List<TestExpectationResult> withExpectations =
await getTestResultsWithExpectation(testResult);
var outputTable = getOutputTable(argResults)
..addHeader(new Column("Test", width: 38), (TestExpectationResult item) {
return item.result.name;
})
..addHeader(new Column("Result", width: 18), (item) {
return item.result.result;
})
..addHeader(new Column("Expected"), (item) {
return item.expectations.toString();
})
..addHeader(new Column("Success", width: 4), (item) {
return item.isSuccess() ? "OK" : "FAIL";
});
outputTable.print(withExpectations);
}
}
/// [GetTestFailuresCommand] handles when given the sub-command 'failures' and
/// returns only the failing tests.
class GetTestFailuresCommand extends Command {
@override
String get description => "Get failures of tests.";
@override
String get name => "failures";
GetTestFailuresCommand() {
buildArgs(argParser);
}
Future run() {
if (isCqInput(argResults)) {
return handleCqInput(argResults);
} else {
return handleBuildbotInput(argResults);
}
}
Future handleCqInput(ArgResults argResults) async {
Iterable<BuildBucketTestResult> buildBucketTestResults =
await getTestResultsFromCq(argResults);
if (buildBucketTestResults == null) {
return;
}
print("All result logs fetched.");
print("Calling test.py to find statuses for each test.");
print("");
for (var buildResult in buildBucketTestResults) {
printBuild(buildResult.build);
List<TestExpectationResult> results =
await getTestResultsWithExpectation(buildResult.testResult);
printFailingTestExpectationResults(results);
print("");
}
}
Future handleBuildbotInput(ArgResults argResults) async {
models.TestResult testResult = await getTestResult(argResults);
if (testResult == null) {
return;
}
print("All result logs fetched.");
var estimatedTime =
new Duration(milliseconds: testResult.results.length * 100 ~/ 1000);
print("Calling test.py to find status files for the configuration and "
"the expectation for ${testResult.results.length} tests. "
"Estimated time remaining is ${estimatedTime.inSeconds} seconds...");
List<TestExpectationResult> withExpectations =
await getTestResultsWithExpectation(testResult);
printFailingTestExpectationResults(withExpectations);
print("");
}
}
/// [GetTestMatrix] responds to 'test-matrix' and returns all configurations for
/// a client.
class GetTestMatrix extends Command {
@override
String get description => "Gets all invokations of test.py for each builder "
"and output the result in csv form.";
@override
String get name => "test-matrix";
GetTestMatrix() {
argParser.addFlag('scripting',
defaultsTo: false, abbr: 's', negatable: false);
}
Future run() async {
// We first get all the last builds for all the bots. That will give us the
// name as well.
var logger = createLogger();
var createCache = createCacheFunction(logger);
var cache = createCache(duration: new Duration(days: 1));
var shardRegExp = new RegExp(r"(.*)-(\d)-(\d)-");
var stepRegExp =
new RegExp(r"read_results_of_(.*)\/0\/logs\/result\.log\/0");
print("builder;step;mode;arch;compiler;runtime;checked;strong;hostChecked;"
"minified;csp;system;vmOptions;useSdk;builderTag;fastStartup;"
"dart2JsWithKernel;enableAsserts;hotReload;"
"hotReloadRollback;previewDart2;selectors");
var logdog = new LogdogRpc();
var testResultService = new TestResultService(logger, createCache);
return latestBuildNumbers(cache).then((buildNumbers) {
// Get steps for this builder and build number.
// Shards run in the same configuration.
return buildNumbers.keys.where((builder) {
var shardMatch = shardRegExp.firstMatch(builder);
return shardMatch == null || shardMatch.group(2) == 1;
}).map((builder) {
int buildNumber = buildNumbers[builder];
return logdog
.query(
BUILDER_PROJECT,
"bb/client.dart/$builder/${buildNumber}/+"
"/recipes/steps/**/result.log/0",
cache)
.then((builderStreams) {
return Future.wait(builderStreams.map((stream) {
return testResultService.fromLogdog(stream.path).then((testResult) {
var configuration = testResult.configurations["conf1"];
var shardMatch = shardRegExp.firstMatch(builder);
String builderName =
shardMatch != null ? shardMatch.group(1) : builder;
String step = stepRegExp
.firstMatch(stream.path)
.group(1)
.replaceAll("_", " ");
print("$builderName;$step;${configuration.toCsvString()}");
}).catchError(
errorLogger(logger, "Could not get log from $stream", null));
}));
}).catchError(errorLogger(
logger,
"Could not download steps for $builder with "
"build number $buildNumber",
new List<LogdogStream>()));
});
}).then(Future.wait);
}
}
/// Prints a test result.
void printFailingTestExpectationResults(List<TestExpectationResult> results) {
List<TestExpectationResult> failing =
results.where((x) => !x.isSuccess()).toList();
failing.sort((a, b) => a.result.name.compareTo(b.result.name));
int index = 0;
failing.forEach((fail) => printFailingTest(fail, index++));
if (index == 0) {
print("\tNo failures found.");
}
}
/// Prints a builder to stdout.
void printBuild(BuildBucketBuild build) {
new ExtendedPrinter()
..println("${build.builder}")
..printLinePattern("=");
}
/// Prints a failing test to stdout.
void printFailingTest(TestExpectationResult result, int index) {
var conf = result.configuration
.toArgs(includeSelectors: false)
.map((arg) => arg.replaceAll("--", ""));
var extPrint = new ExtendedPrinter(preceding: "\t");
if (index > 0) {
extPrint.printLinePattern("-");
}
extPrint
..println("FAILED: ${getQualifiedNameForTest(result.result.name)}")
..println("Result: ${result.result.result}")
..println("Expected: ${result.expectations}")
..println("Configuration: ${conf.join(', ')}")
..println("")
..println(
"To run locally (if you have the right architecture and runtime):")
..println(getReproductionCommand(result.configuration, result.result.name));
}