blob: f3525520b525b850c7dc8288795b78d66b000758 [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 'package:fxtest/fxtest.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'] ?? [])
// ignore: unnecessary_lambdas
.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') ?? false) {
return TestType.component;
// .cm tests are considered suites
} else if (packageUrl!.fullComponentName?.endsWith('.cm') ?? false) {
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!);
}
/// Create an execution handle using the test definition, and using any overrides
/// specified by the invoker.
ExecutionHandle createExecutionHandle({
String? parallelOverride,
bool useRunTestSuiteForV2 = false,
}) {
switch (testType) {
case TestType.component:
return ExecutionHandle.component(decoratedPackageUrl.toString(), os);
case TestType.suite:
List<String> flags = [];
if (parallelOverride != null) {
flags.addAll(['--parallel', parallelOverride]);
} else if (parallel != null) {
flags.addAll(['--parallel', parallel!]);
}
if (useRunTestSuiteForV2) {
return ExecutionHandle.suiteFallbackRunTestSuite(
decoratedPackageUrl.toString(), os,
flags: flags);
}
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.toLowerCase() != 'fuchsia' && containsE2eEnvironments;
bool get containsE2eEnvironments => 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.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" />';
}