blob: 52a4d7a6afab71202036398aa8b560131d3256cf [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:args/args.dart';
import 'package:pub_semver/pub_semver.dart';
import 'device.dart';
import 'key_provider.dart';
import '../globals.dart';
import '../util.dart';
class DeviceSpec implements GroupKeyProvider {
DeviceSpec(String nickname, { this.specProperties }) {
specProperties['nickname'] = nickname;
}
Map<String, String> specProperties;
String get platform => specProperties['platform'];
String get nickName => specProperties['nickname'];
String get deviceID => specProperties['device-id'];
String get deviceModelName => specProperties['model-name'];
String get deviceOSVersion => specProperties['os-version'];
String get deviceScreenSize => specProperties['screen-size'];
String get appRootPath => specProperties['app-root'];
String get appPath => specProperties['app-path'];
String get observatoryUrl => specProperties['observatory-url'];
void set observatoryUrl(String url) {
specProperties['observatory-url'] = url;
}
/// Match if property names are not specified or equal to the device property.
/// Checked property names includes: device-id, model-name, screen-size
bool matches(Device device) {
List<String> checkedProperties = [
'platform',
'device-id',
'model-name',
'screen-size'
];
return checkedProperties.every(
(String propertyName) => isNullOrEqual(propertyName, device)
)
&&
osVersionIsNullOrMatches(device);
}
bool isNullOrEqual(String propertyName, Device device) {
return specProperties[propertyName] == null
||
specProperties[propertyName] == device.properties[propertyName];
}
bool osVersionIsNullOrMatches(Device device) {
String osVersion = specProperties['os-version'];
if (osVersion == null) {
return true;
}
VersionConstraint versionConstraint
= new VersionConstraint.parse(osVersion);
return versionConstraint.allows(
new Version.parse(device.properties['os-version'])
);
}
@override
String groupKey() {
return appPath;
}
@override
String toString() => '<nickname: $nickName, '
'platform: $platform, '
'id: $deviceID, '
'model name: $deviceModelName, '
'os version: $deviceOSVersion, '
'screen size: $deviceScreenSize, '
'observatory url: $observatoryUrl, '
'app path: $appPath>';
}
dynamic loadSpecs(ArgResults argResults) {
String specsPath = argResults['spec'];
try {
File specsFile = new File(specsPath);
// Read specs file into json object
dynamic newSpecs = JSON.decode(specsFile.readAsStringSync());
// Get the parent directory of the specs file
String rootPath = specsFile.parent.absolute.path;
// Normalize the 'test-path' specified from the command line argument
List<String> testPathsFromCommandLine
= listFilePathsFromGlobPatterns(Directory.current.path, argResults.rest);
printTrace('Test paths from command line: $testPathsFromCommandLine');
newSpecs['test-paths'] = testPathsFromCommandLine;
// Normalize the 'app-path' in the specs file
newSpecs['devices']?.forEach((String name, Map<String, String> map) {
map['app-root'] = normalizePath(rootPath, map['app-root']);
map['app-path'] = normalizePath(map['app-root'], map['app-path']);
});
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) {
printError('Unknown Exception details:\n $e');
exit(1);
}
}
/// Check if test spec meets the requirements. If user does not specify any
/// valid test paths neither from the test spec nor from the command line,
/// report error. If 'devices' property is not specified, report error. If
/// no device spec is specified, report error. If platform property is not
/// one of 'ios' or 'android', report error. If os-version is specified, but
/// platform is not specified, report error. If os-version does not match
/// semantic version, report error. If screen size property is not one of
/// 'small', 'normal', 'large' and 'xlarge', report error. If app-root
/// is not specified or is not a directory, report error. If appPath is not
/// specified or is not a file, report error.
///
/// Note: If a test path does not exist, it will be ignored and thus does not
/// count as a valid test path. If device nickname is not unique, json decoder
/// will overwrite the previous device spec associated with the same nickname,
/// thus only the last nickname to device spec pair is used.
int sanityCheckSpecs(dynamic spec, String specsPath) {
if (spec['test-paths'].isEmpty) {
printError(
'No test paths found. '
'You must specify at least one test path.'
);
return 1;
}
dynamic deviceSpecs = spec['devices'];
if (deviceSpecs == null) {
printError('"devices" property is not specified in $specsPath.');
return 1;
}
if (deviceSpecs.isEmpty) {
printError('No device spec is found in $specsPath.');
return 1;
}
for (String nickname in deviceSpecs.keys) {
dynamic individualDeviceSpec = deviceSpecs[nickname];
List<String> platforms = <String>['ios', 'android'];
String platform = individualDeviceSpec['platform'];
if (platform != null && !platforms.contains(platform)) {
printError('Platform must be one of $platforms.');
return 1;
}
String osVersion = individualDeviceSpec['os-version'];
if (osVersion != null) {
if (platform == null) {
printError(
'You must also specify platform type if you specify os-version.'
);
return 1;
}
try {
new VersionConstraint.parse(osVersion);
} on FormatException {
printError(
'The os-version you specified does not meet the requirement of '
'semantic version.'
);
return 1;
} catch (e) {
printError('Unknown Exception when parsing os-vesion, details:\n $e.');
return 1;
}
}
List<String> screenSizes = <String>['small', 'normal', 'large', 'xlarge'];
String screenSize = individualDeviceSpec['screen-size'];
if (screenSize != null && !screenSizes.contains(screenSize)) {
printError('Screen size must be one of $screenSizes.');
return 1;
}
String appRootPath = individualDeviceSpec['app-root'];
if (appRootPath == null) {
printError('Application root path is not specified.');
return 1;
}
if (!FileSystemEntity.isDirectorySync(appRootPath)) {
printError('Application root path is not a directory.');
return 1;
}
String appPath = individualDeviceSpec['app-path'];
if (appPath == null) {
printError('Application path is not specified.');
return 1;
}
if (!FileSystemEntity.isFileSync(appPath)) {
printError('Application path is not a file.');
return 1;
}
}
return 0;
}
/// 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> spec = allSpecs[name];
deviceSpecs.add(
new DeviceSpec(
name,
specProperties: spec
)
);
}
return deviceSpecs;
}