blob: 119a3e8b7c3853ebb350d597afdb8324b82a2291 [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 'package:path/path.dart' as path;
import '../base/common.dart';
import '../mobile/device.dart';
import '../mobile/device_spec.dart';
import '../mobile/device_util.dart';
import '../globals.dart';
import '../runner/mdtest_command.dart';
class RunCommand extends MDTestCommand {
@override
final String name = 'run';
@override
final String description = 'Run multi-device driver tests';
dynamic _specs;
List<Device> _devices;
@override
Future<int> runCore() async {
print('Running "mdtest run command" ...');
this._specs = await loadSpecs(argResults['specs']);
print(_specs);
this._devices = await getDevices();
if (_devices.isEmpty) {
printError('No device found.');
return 1;
}
List<DeviceSpec> allDeviceSpecs
= await constructAllDeviceSpecs(_specs['devices']);
Map<DeviceSpec, Set<Device>> individualMatches
= findIndividualMatches(allDeviceSpecs, _devices);
Map<DeviceSpec, Device> deviceMapping
= findMatchingDeviceMapping(allDeviceSpecs, individualMatches);
if(deviceMapping == null) {
printError('No device specs to devices mapping is found.');
return 1;
}
if (await runAllApps(deviceMapping) != 0) {
printError('Error when running applications');
return 1;
}
await storeMatches(deviceMapping);
if (await runTest(_specs['test-path']) != 0) {
printError('Test execution exit with error.');
return 1;
}
return 0;
}
RunCommand() {
usesSpecsOption();
}
}
Future<dynamic> loadSpecs(String specsPath) async {
try {
// Read specs file into json format
dynamic newSpecs = JSON.decode(await new File(specsPath).readAsString());
// Get the parent directory of the specs file
String rootPath = new File(specsPath).parent.absolute.path;
// Normalize the 'test-path' in the specs file
newSpecs['test-path'] = normalizePath(rootPath, newSpecs['test-path']);
// Normalize the 'app-path' in the specs file
newSpecs['devices'].forEach((String name, Map<String, String> map) {
map['app-path'] = normalizePath(rootPath, map['app-path']);
map['app-root'] = normalizePath(rootPath, map['app-root']);
});
return newSpecs;
} on FileSystemException {
printError('File $specsPath does not exist.');
exit(1);
} on FormatException {
printError('File $specsPath is not in JSON format.');
exit(1);
} catch (e) {
print('Unknown Exception details:\n $e');
exit(1);
}
}
String normalizePath(String rootPath, String relativePath) {
return path.normalize(path.join(rootPath, relativePath));
}
/// Build a list of device specs from mappings loaded from JSON .spec file
Future<List<DeviceSpec>> constructAllDeviceSpecs(dynamic allSpecs) async {
List<DeviceSpec> deviceSpecs = <DeviceSpec>[];
for(String name in allSpecs.keys) {
Map<String, String> specs = allSpecs[name];
deviceSpecs.add(
new DeviceSpec(
nickName: name,
deviceID: specs['device-id'],
deviceModelName: specs['model-name'],
appRootPath: specs['app-root'],
appPath: specs['app-path']
)
);
}
return deviceSpecs;
}
/// Find all matched devices for each device spec
Map<DeviceSpec, Set<Device>> findIndividualMatches(
List<DeviceSpec> deviceSpecs,
List<Device> devices) {
Map<DeviceSpec, Set<Device>> individualMatches
= new Map<DeviceSpec, Set<Device>>();
for(DeviceSpec deviceSpecs in deviceSpecs) {
Set<Device> matchedDevices = new Set<Device>();
for(Device device in devices) {
if(deviceSpecs.matches(device))
matchedDevices.add(device);
}
individualMatches[deviceSpecs] = matchedDevices;
}
return individualMatches;
}
/// Return the first device spec to device matching, null if no such matching
Map<DeviceSpec, Device> findMatchingDeviceMapping(
List<DeviceSpec> deviceSpecs,
Map<DeviceSpec, Set<Device>> individualMatches) {
Map<DeviceSpec, Device> deviceMapping = <DeviceSpec, Device>{};
Set<Device> visited = new Set<Device>();
if (!_findMatchingDeviceMapping(0, deviceSpecs, individualMatches,
visited, deviceMapping)) {
return null;
}
return deviceMapping;
}
/// Find a mapping that matches every device spec to a device. If such
/// mapping is not found, return false, otherwise return true.
bool _findMatchingDeviceMapping(
int order,
List<DeviceSpec> deviceSpecs,
Map<DeviceSpec, Set<Device>> individualMatches,
Set<Device> visited,
Map<DeviceSpec, Device> deviceMapping
) {
if(order == deviceSpecs.length) return true;
DeviceSpec deviceSpec = deviceSpecs[order];
Set<Device> matchedDevices = individualMatches[deviceSpec];
for(Device candidate in matchedDevices) {
if(visited.add(candidate)) {
deviceMapping[deviceSpec] = candidate;
if(_findMatchingDeviceMapping(order + 1, deviceSpecs, individualMatches,
visited, deviceMapping))
return true;
else {
visited.remove(candidate);
deviceMapping.remove(deviceSpec);
}
}
}
return false;
}
List<Process> appProcesses = <Process>[];
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 {
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) {
print(line.toString().trim());
Match portMatch = portPattern.firstMatch(line.toString());
if (portMatch != null) {
deviceSpec.observatoryUrl = portMatch.group(1);
break;
}
}
process.stderr.drain();
if (deviceSpec.observatoryUrl == null) {
printError('No observatory url is found.');
return 1;
}
return 0;
}
/// Store the specs to device mapping as a system temporary file. The file
/// stores device nickname as well as device id and observatory port for
/// each device
Future<Null> storeMatches(Map<DeviceSpec, Device> deviceMapping) async {
Map<String, dynamic> matchesData = new Map<String, dynamic>();
deviceMapping.forEach((DeviceSpec specs, Device device) {
matchesData[specs.nickName] =
{
'device-id': device.id,
'observatory-url': specs.observatoryUrl
};
});
Directory systemTempDir = Directory.systemTemp;
File tempFile = new File('${systemTempDir.path}/$defaultTempSpecsName');
if(await tempFile.exists())
await tempFile.delete();
File file = await tempFile.create();
await file.writeAsString(JSON.encode(matchesData));
}
/// Create a process and invoke 'dart testPath' to run the test script. After
/// test result is returned (either pass or fail), kill all app processes and
/// return the current process exit code
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())) {
process.stderr.drain();
killAllProcesses(appProcesses);
break;
}
}
return await process.exitCode;
}
/// Kill all given processes
Future<Null> killAllProcesses(List<Process> processes) async {
for (Process process in processes) {
process.kill();
}
}