blob: 72a7ef01c373469963d4e52367fadce75b632d1d [file] [log] [blame]
// Copyright 2016 The Vanadium 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:convert';
import 'dart:io';
import '../base/common.dart';
import '../mobile/device.dart';
import '../mobile/device_spec.dart';
import '../mobile/android.dart';
import '../test/coverage_collector.dart';
import '../test/reporter.dart';
import '../globals.dart';
import '../util.dart';
class MDTestRunner {
List<Process> appProcesses;
MDTestRunner() {
appProcesses = <Process>[];
}
/// Invoke runApp function for each device spec to device mapping in parallel
Future<int> runAllApps(Map<DeviceSpec, Device> deviceMapping) async {
List<Future<int>> runAppList = <Future<int>>[];
for (DeviceSpec deviceSpec in deviceMapping.keys) {
Device device = deviceMapping[deviceSpec];
runAppList.add(runApp(deviceSpec, device));
}
int res = 0;
List<int> results = await Future.wait(runAppList);
for (int result in results)
res += result;
return res == 0 ? 0 : 1;
}
/// Create a process that runs 'flutter run ...' command which installs and
/// starts the app on the device. The function finds a observatory port
/// through the process output. If no observatory port is found, then report
/// error.
Future<int> runApp(DeviceSpec deviceSpec, Device device) async {
// Currently, unlocking iOS device is not supported.
if (device.isAndroidDevice() && await unlockDevice(device) != 0) {
printError('Device ${device.id} fails to wake up.');
return 1;
}
printInfo('Start application ${deviceSpec.appPath} on device ${device.id}');
Process process = await Process.start(
'flutter',
['run', '-d', '${device.id}', '--target=${deviceSpec.appPath}'],
workingDirectory: deviceSpec.appRootPath
);
appProcesses.add(process);
Stream lineStream = process.stdout
.transform(new Utf8Decoder())
.transform(new LineSplitter());
RegExp portPattern = new RegExp(r'Observatory listening on (http.*)');
await for (var line in lineStream) {
Match portMatch = portPattern.firstMatch(line.toString());
if (portMatch != null) {
printInfo('${line.toString().trim()} (${deviceSpec.nickName}: ${device.id})');
deviceSpec.observatoryUrl = portMatch.group(1);
break;
}
printTrace(line.toString().trim());
}
process.stderr.drain();
if (deviceSpec.observatoryUrl == null) {
printError('No observatory url is found.');
return 1;
}
return 0;
}
/// Run all tests without test output reformatting
Future<int> runAllTests(Iterable<String> testPaths) async {
int result = 0;
for (String testPath in testPaths) {
result += await runTest(testPath);
}
return result == 0 ? 0 : 1;
}
/// Run all tests with test output in TAP format
Future<int> runAllTestsToTAP(
Iterable<String> testPaths,
TAPReporter reporter
) async {
int diffFrom = beginOfDiff(testPaths);
int result = 0;
reporter.printHeader();
for (String testPath in testPaths) {
result += await runTestToTAP(testPath, diffFrom, reporter);
}
reporter.printSummary();
return result == 0 ? 0 : 1;
}
/// Create a process and invoke 'dart [testPath]' to run the test script.
/// After test result is returned (either pass or fail), return the current
/// process exit code (0 if success, otherwise failure)
Future<int> runTest(String testPath) async {
Process process = await Process.start(
'dart',
['$testPath']
);
RegExp testStopPattern = new RegExp(r'All tests passed|Some tests failed');
Stream stdoutStream = process.stdout
.transform(new Utf8Decoder())
.transform(new LineSplitter());
await for (var line in stdoutStream) {
print(line.toString().trim());
if (testStopPattern.hasMatch(line.toString()))
break;
}
process.stderr.drain();
process.kill();
return 0;
}
/// Create a process and invoke 'pub run test --reporter json [testPath]' to
/// run the test script and convert json output into tap output on the fly.
/// After test result is returned (either pass or fail), return the current
/// process exit code (0 if success, otherwise failure)
Future<int> runTestToTAP(
String testPath,
int diffFrom,
TAPReporter reporter
) async {
Process process = await Process.start(
'pub',
['run', 'test', '--reporter', 'json', '$testPath']
);
bool hasTestOutput = await reporter.report(
testPath.substring(diffFrom),
process.stdout
.transform(new Utf8Decoder())
.transform(new LineSplitter())
);
if (hasTestOutput) {
process.stderr.drain();
return await process.exitCode;
}
Stream stderrStream = process.stderr
.transform(new Utf8Decoder())
.transform(new LineSplitter());
await for (var line in stderrStream) {
print(line.toString().trim());
}
return 1;
}
/// Kill all app processes
Future<Null> killAppProcesses() async {
for (Process process in appProcesses) {
process.kill();
}
}
}
/// Create a coverage collector for each application and assign a coverage
/// collection task for the coverage collector
void buildCoverageCollectionTasks(
Map<DeviceSpec, Device> deviceMapping,
Map<String, CoverageCollector> collectorPool
) {
assert(collectorPool != null);
// Build app path to coverage collector mapping and add collection tasks
deviceMapping.keys.forEach((DeviceSpec spec) {
collectorPool.putIfAbsent(
spec.appRootPath,
() => new CoverageCollector()
).collectCoverage(spec.observatoryUrl);
});
}
/// Run coverage collection tasks for each application
Future<Null> runCoverageCollectionTasks(
Map<String, CoverageCollector> collectorPool
) async {
assert(collectorPool.isNotEmpty);
// Collect coverage for every application
for (CoverageCollector collector in collectorPool.values) {
await collector.finishPendingJobs();
}
}
/// Compute application code coverage and write coverage info in lcov format
Future<int> computeAppsCoverage(
Map<String, CoverageCollector> collectorPool,
String commandName
) async {
if (collectorPool.isEmpty)
return 1;
// Write coverage info to coverage/code_coverage folder under each
// application folder
for (String appRootPath in collectorPool.keys) {
CoverageCollector collector = collectorPool[appRootPath];
String coverageData = await collector.finalizeCoverage(appRootPath);
if (coverageData == null)
return 1;
String codeCoverageDirPath = normalizePath(
appRootPath,
'$defaultCodeCoverageDirectoryPath'
);
File codeCoverageReport = getUniqueFile(
new Directory(codeCoverageDirPath),
'cov_$commandName',
'lcov'
);
try {
// Write coverage info to code_coverage folder
codeCoverageReport
..createSync(recursive: true)
..writeAsStringSync(coverageData, flush: true);
printTrace('Writing code coverage to ${codeCoverageReport.path}');
} on FileSystemException {
printError('Cannot write code coverage info to ${codeCoverageReport.path}');
return 1;
}
}
return 0;
}