blob: 660ca0d87f0b0af441c54ee0432c2d85bb96d725 [file] [log] [blame]
// Copyright 2019 The Fuchsia Authors. 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 'dart:io';
import 'package:fxtest/fxtest.dart';
import 'package:meta/meta.dart';
/// Main entry-point for all Fuchsia tests, both host and on-device.
///
/// [FuchsiaTestCommand] receives a combination of test names and feature flags,
/// and due to the nature of Fuchsia tests, needs to run each passed test name
/// against the full set of feature flags. For example, if the way to invoke
/// this command is `fx test <...args>`, and the developer executes
/// `... test1 test2 -flag1 -flag2`, the desired behavior for our user lies
/// behind pretending the user entered `... test1 -flag1 -flag2` and
/// `... test2 -flag1 flag2` separately. Note that the individual tests indicated
/// by these two imaginary commands are then rolled back into one singular test
/// suite, so end users will be none the wiser.
///
/// The following is a high level road map of how [FuchsiaTestCommand] works:
///
/// - Parse commands into testNames and flags
/// - Produce a list of each individual `testName` paired with all provided flags
/// - loop over this list `testName` and flag combinations, matching tests
/// out of `out/default/tests.json`
/// - Load each matching test's raw JSON into a [TestDefinition] instance which
/// handles determining all the runtime invocation details for a given test
/// - Loop over the list of [TestDefinition] instances, listening to emitted
/// [TestEvent] instances with a [FormattedOutput] class
/// - Every time a [TestEvent] instance is emitted, our [FormattedOutput]
/// class captures it and flushes appropriate text to [stdout].
/// - Once the [TestDefinition] list has been completely processed, flush any
/// captured stderr from individual test processes to show errors / stacktraces
class FuchsiaTestCommand {
// ignore: close_sinks
final _eventStreamController = StreamController<TestEvent>();
final AnalyticsReporter analyticsReporter;
/// Bundle of configuration options for this invocation.
final TestsConfig testsConfig;
/// Absolute path of the active Fuchsia build.
final FuchsiaLocator fuchsiaLocator;
/// Translator between [TestEvent] instances and output for the user.
final OutputFormatter outputFormatter;
/// Function that yields disposable wrappers around tests and [Process]
/// instances.
final TestRunner Function(TestsConfig) testRunnerBuilder;
/// Helper which verifies that we actually want to run tests (not a dry run,
/// for example) and are set up to even be successful (device and package
/// server are available, for example).
final Checklist checklist;
int _numberOfTests;
FuchsiaTestCommand({
@required this.analyticsReporter,
@required this.fuchsiaLocator,
@required this.outputFormatter,
@required this.checklist,
@required this.testsConfig,
@required this.testRunnerBuilder,
}) : _numberOfTests = 0 {
if (outputFormatter == null) {
throw AssertionError('`outputFormatter` must not be null');
}
stream.listen(outputFormatter.update);
}
factory FuchsiaTestCommand.fromConfig(
TestsConfig testsConfig, {
@required TestRunner Function(TestsConfig) testRunnerBuilder,
FuchsiaLocator fuchsiaLocator,
OutputFormatter outputFormatter,
}) {
fuchsiaLocator = fuchsiaLocator ?? FuchsiaLocator.shared;
var _outputFormatter =
outputFormatter ?? OutputFormatter.fromConfig(testsConfig);
return FuchsiaTestCommand(
analyticsReporter: testsConfig.flags.dryRun
? AnalyticsReporter.noop()
: AnalyticsReporter(
fuchsiaLocator: fuchsiaLocator,
),
checklist: PreChecker.fromConfig(
testsConfig,
eventSink: _outputFormatter.update,
),
fuchsiaLocator: fuchsiaLocator,
outputFormatter: _outputFormatter,
testRunnerBuilder: testRunnerBuilder,
testsConfig: testsConfig,
);
}
Stream<TestEvent> get stream => _eventStreamController.stream;
void emitEvent(TestEvent event) {
if (!_eventStreamController.isClosed) {
_eventStreamController.sink.add(event);
}
}
void dispose() {
_eventStreamController.close();
}
Future<void> runTestSuite(TestsManifestReader manifestReader) async {
var parsedManifest = await readManifest(manifestReader);
manifestReader.reportOnTestBundles(
userFriendlyBuildDir: fuchsiaLocator.userFriendlyBuildDir,
eventEmitter: emitEvent,
parsedManifest: parsedManifest,
testsConfig: testsConfig,
);
try {
// Let the output formatter know that we're done parsing and
// emitting preliminary events
emitEvent(BeginningTests());
await runTests(parsedManifest.testBundles);
} on FailFastException catch (_) {
// Non-zero exit code indicates generic but fatal failure
exitCode = failureExitCode;
}
emitEvent(AllTestsCompleted());
}
Future<ParsedManifest> readManifest(
TestsManifestReader manifestReader,
) async {
List<TestDefinition> testDefinitions = await manifestReader.loadTestsJson(
buildDir: fuchsiaLocator.buildDir,
fxLocation: fuchsiaLocator.fx,
manifestFileName: 'tests.json',
);
return manifestReader.aggregateTests(
eventEmitter: emitEvent,
exactMatching: testsConfig.flags.exactMatches,
testBundleBuilder: testBundleBuilder,
testDefinitions: testDefinitions,
testsConfig: testsConfig,
);
}
TestBundle testBundleBuilder(TestDefinition testDefinition) =>
TestBundle.build(
fxSuffix: fuchsiaLocator.fx,
realtimeOutputSink: (String val) => emitEvent(TestOutputEvent(val)),
timeElapsedSink: (Duration duration, String cmd, String output) =>
emitEvent(TimeElapsedEvent(duration, cmd, output)),
testRunnerBuilder: testRunnerBuilder,
testDefinition: testDefinition,
testsConfig: testsConfig,
workingDirectory: fuchsiaLocator.buildDir,
);
Future<void> runTests(List<TestBundle> testBundles) async {
if (!await checklist.isDeviceReady(testBundles)) {
emitEvent(FatalError('Device is not ready for running device tests'));
return;
}
var count = 0;
for (TestBundle testBundle in testBundles) {
await testBundle.run().forEach((TestEvent event) {
emitEvent(event);
if (event is FatalError) {
exitCode = failureExitCode;
} else if (event is TestResult && !event.isSuccess) {
exitCode = event.exitCode ?? failureExitCode;
}
});
count += 1;
_numberOfTests = count;
if (testsConfig.flags.limit > 0 && count >= testsConfig.flags.limit) {
break;
}
}
}
/// Function guaranteed to be called at the end of execution, whether that is
/// natural or the result of a SIGINT.
Future<void> cleanUp() async {
await _reportAnalytics();
}
Future<void> _reportAnalytics() async {
final bool _actuallyRanTests = _numberOfTests != null && _numberOfTests > 0;
if (!testsConfig.flags.dryRun && _actuallyRanTests) {
await analyticsReporter.report(
subcommand: 'test',
action: 'number',
label: '$_numberOfTests',
);
}
}
}