| // 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 'package:fxtest/fxtest.dart'; |
| import 'package:meta/meta.dart'; |
| import 'package:path/path.dart' as p; |
| |
| /// Structured representation of a single entry from `//out/default/tests.json`. |
| /// |
| /// Contains all the relevant (e.g., we care about here) data from a single |
| /// test. Also contains an instance of [ExecutionHandle] which is used at |
| /// test-execution time to reduce ambiguity around low level invocation details. |
| class TestDefinition { |
| final String buildDir; |
| final List<String> command; |
| final String cpu; |
| final String runtimeDeps; |
| final String path; |
| final String label; |
| final String packageLabel; |
| final String name; |
| final String os; |
| final PackageUrl packageUrl; |
| final String maxLogSeverity; |
| final String parallel; |
| |
| String hash; |
| |
| final List<TestEnvironment> testEnvironments; |
| |
| TestDefinition({ |
| @required this.buildDir, |
| @required this.name, |
| @required this.os, |
| this.packageUrl, |
| this.cpu, |
| this.command, |
| this.runtimeDeps, |
| this.label, |
| this.packageLabel, |
| this.path, |
| this.maxLogSeverity, |
| this.parallel, |
| this.testEnvironments = const [], |
| }); |
| |
| factory TestDefinition.fromJson( |
| Map<String, dynamic> data, { |
| @required String buildDir, |
| }) { |
| Map<String, dynamic> testDetails = data['test'] ?? {}; |
| List<TestEnvironment> testEnvironments = (data['environments'] ?? []) |
| .map((dynamic _data) => TestEnvironment.fromJson(_data)) |
| .cast<TestEnvironment>() |
| .toList() ?? |
| []; |
| Map<dynamic, dynamic> logSettings = testDetails['log_settings'] ?? {}; |
| return TestDefinition( |
| buildDir: buildDir, |
| command: List<String>.from(testDetails['command'] ?? []), |
| cpu: testDetails['cpu'] ?? '', |
| runtimeDeps: testDetails['runtime_deps'] ?? '', |
| label: testDetails['label'] ?? '', |
| packageLabel: testDetails['package_label'] ?? '', |
| name: testDetails['name'] ?? '', |
| os: testDetails['os'] ?? '', |
| packageUrl: testDetails['package_url'] == null |
| ? null |
| : PackageUrl.fromString(testDetails['package_url']), |
| path: testDetails['path'] ?? '', |
| maxLogSeverity: logSettings['max_severity'], |
| parallel: testDetails['parallel']?.toString(), |
| testEnvironments: testEnvironments, |
| ); |
| } |
| |
| @override |
| String toString() => '''<TestDefinition |
| cpu: $cpu |
| command: ${(command ?? []).join(" ")} |
| deps_file: $runtimeDeps |
| label: $label |
| package_label: ${packageLabel ?? ''} |
| package_url: ${packageUrl ?? ''} |
| path: $path |
| name: $name |
| os: $os |
| max_log_severity: $maxLogSeverity |
| parallel: $parallel |
| />'''; |
| |
| TestType get testType { |
| if (isE2E) { |
| return TestType.e2e; |
| } else if (command != null && command.isNotEmpty) { |
| // `command` must be checked before `host`, because `command` is a subset |
| // of all `host` tests |
| return TestType.command; |
| } else if (packageUrl != null) { |
| // The order of `component` / `suite` does not currently matter |
| |
| // .cmx tests are considered components |
| if (packageUrl.fullComponentName.endsWith('.cmx')) { |
| return TestType.component; |
| // .cm tests are considered suites |
| } else if (packageUrl.fullComponentName.endsWith('.cm')) { |
| return TestType.suite; |
| } |
| // Package Urls must end with either ".cmx" or ".cm" |
| throw MalformedFuchsiaUrlException(packageUrl.toString()); |
| } else if (path != null && path.isNotEmpty) { |
| // As per above, `host` must be checked after `command` |
| |
| // Tests with a path must be host tests. All Fuchsia tests *must* be |
| // component tests, which means these are a legacy configuration which is |
| // unsupported by `fx test`. |
| if (os == 'fuchsia') { |
| return TestType.unsupportedDeviceTest; |
| } else { |
| return TestType.host; |
| } |
| } |
| return TestType.unsupported; |
| } |
| |
| bool get isUnsupported => unsupportedTestTypes.contains(testType); |
| |
| PackageUrl get decoratedPackageUrl { |
| if (packageUrl == null || hash == null) { |
| return packageUrl; |
| } |
| return PackageUrl.copyWithHash(other: packageUrl, hash: hash); |
| } |
| |
| ExecutionHandle createExecutionHandle() { |
| switch (testType) { |
| case TestType.component: |
| return ExecutionHandle.component(decoratedPackageUrl.toString(), os); |
| case TestType.suite: |
| List<String> flags = []; |
| if (parallel != null) { |
| flags.addAll(['--parallel', parallel]); |
| } |
| return ExecutionHandle.suite(decoratedPackageUrl.toString(), os, |
| flags: flags); |
| case TestType.command: |
| return ExecutionHandle.command(command.join(' '), os); |
| case TestType.host: |
| return ExecutionHandle.host(fullPath, os); |
| case TestType.e2e: |
| return ExecutionHandle.e2e([path, ...command].join(' '), os); |
| case TestType.unsupported: |
| return ExecutionHandle.unsupported(); |
| default: |
| return ExecutionHandle.unsupportedDeviceTest(path); |
| } |
| } |
| |
| /// End-to-end tests start on the host machine (designated by an [os] value |
| /// of `linux`), but also require interaction with a physical device. |
| bool get isE2E => |
| (os == null || os.toLowerCase() != 'fuchsia') && containsE2eEnvironments; |
| |
| bool get containsE2eEnvironments => |
| testEnvironments != null && testEnvironments.any((env) => env.isE2E); |
| |
| String get fullPath => p.join(buildDir, path); |
| } |
| |
| /// Structured representation of an optionally populated `environments` key |
| /// inside individual test blocks within `tests.json`. |
| /// |
| /// `TestEnvironment` separates the task of making sense of that data from the |
| /// core task of parsing a test block's simpler key/value pairs. |
| /// |
| /// `TestEnvironment` is a work-in-progress and does not exhaustively contain |
| /// every field that can exist in the `environments` list. In the future, if new |
| /// feature requests require the use of a field not yet handled, this is not an |
| /// unexpected shortcoming and you should feel confident adding it. |
| class TestEnvironment { |
| static const hostOsValues = <String>{'linux', 'mac'}; |
| |
| final bool isDefined; |
| final String deviceDimension; |
| final String os; |
| TestEnvironment._({ |
| @required this.isDefined, |
| @required this.deviceDimension, |
| @required this.os, |
| }); |
| |
| factory TestEnvironment.fromJson(Map<String, dynamic> data) { |
| if (data == null || data.isEmpty) return TestEnvironment.empty(); |
| return TestEnvironment._( |
| isDefined: true, |
| deviceDimension: getMapPath(data, ['dimensions', 'device_type']), |
| os: getMapPath(data, ['dimensions', 'os'])); |
| } |
| |
| factory TestEnvironment.empty() => TestEnvironment._( |
| isDefined: false, |
| deviceDimension: null, |
| os: null, |
| ); |
| |
| bool get requiresDevice => deviceDimension != null; |
| bool get nonHostOs => os == null || !hostOsValues.contains(os.toLowerCase()); |
| |
| bool get isE2E => isDefined == true && (requiresDevice || nonHostOs); |
| |
| @override |
| String toString() => |
| '<TestEnvironment os: "$os", deviceType: "$deviceDimension" />'; |
| } |