blob: 427d7c0a5a69e73c9ad53f33915b0af2c3762b0f [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:fxutils/fxutils.dart';
/// Helper which pairs positional arguments with any trailing `-a` arguments,
/// or, similarly, `-p` arguments with any trailing `-c` arguments.
///
/// Also translates any test name value of "." to the current working directory,
/// since we can safely assume "." will never be an actual test name.
///
/// There is some redudancy in the values passed to [rawArgs] and [testNames]
/// due to details of the `ArgParser`'s API. This stems from the fact that args
/// are all parsed into their respective buckets, but original ordering is lost.
/// Thus, we know all the floating arguments passed (testNames), and which values
/// were passed under a `-a` flag, but not which [testNames] those additional
/// `-a` filters immediately followed. Because of this, we take both the
/// extracted [testNames] and the original raw, unparsed series of CLI arguments,
/// and reverse-engineer what we need.
class TestNamesCollector {
/// All arguments passed to `fx test`. For example, if the user executed:
///
/// > `fx test test_one -a filter_one test_two --flag_one --flag_two`
///
/// this value would be:
///
/// > `[test_one, -a, filter_one, test_two, --flag_one, --flag_two,]`
///
/// If the user executed:
///
/// > `fx test -p pkg-name -c comp-name`
///
/// this value would unsurprisingly be:
///
/// > `[-p, pkg-name, -c, comp-name]`
final List<String> rawArgs;
/// Extracted testNames from the set of arguments passed to `fx test`. For
/// example, if the user executed:
///
/// > `fx test test_one -a filter_one test_two --flag_one --flag_two`
///
/// this value would be:
///
/// > `[test_one, test_two]`
///
/// If the user executed:
///
/// > `fx test test_one -p pkg-name -c comp-name`
///
/// this value would be:
///
/// > `[test_one]`
final List<String> testNames;
TestNamesCollector({
/// The entire string of arguments passed to `fx test`.
required List<String> rawArgs,
/// The extracted test names from all arguments passed to `fx test`.
required List<String> rawTestNames,
required String relativeCwd,
}) : testNames =
TestNamesCollector._processTestList(rawTestNames, relativeCwd),
rawArgs = TestNamesCollector._processRawArgs(rawArgs, relativeCwd);
/// Loops over a combination of the original, raw list of arguments, and the
/// parsed arguments, to determine which positional arguments are modified by
/// a trailing `--a|-a` flag. For example,
///
/// ```
/// test_one -a filter_one test_two -a filter_two -a filter_two_b test_three
/// ```
///
/// Should be parsed into:
///
/// ```
/// [
/// [<MatchableArgument "test_one">, <MatchableArgument "filter_one">],
/// [<MatchableArgument "test_two">, <MatchableArgument "filter_two">, <MatchableArgument "filter_two_b">],
/// [<MatchableArgument "test_three">]
/// ]
/// ```
List<List<MatchableArgument>> collect() {
List<List<MatchableArgument>> groupedTestFilters = [];
var seenRootNames = <String>{};
var args = _ListEmitter<String>(rawArgs);
while (args.hasMore()) {
var arg = args.take();
if (seenRootNames.contains(arg)) {
continue;
}
_ArgumentsProgress? argsProgress;
if (testNames.contains(arg)) {
seenRootNames.add(arg!);
groupedTestFilters.add([MatchableArgument.unrestricted(arg)]);
argsProgress = _takeAndFilters(
args,
testNames,
);
} else if (['--package', '-p'].contains(arg)) {
// Break out if we're at the end and had a dangling `-p` flag.
if (!args.hasMore()) break;
var nextArg = args.take();
groupedTestFilters.add([MatchableArgument.packageName(nextArg!)]);
argsProgress = _takeComponentFilters(
args,
testNames,
);
} else if (['--component', '-c'].contains(arg)) {
// Break out if we're at the end and had a dangling `-c` flag.
if (!args.hasMore()) break;
var nextArg = args.take();
groupedTestFilters.add([MatchableArgument.componentName(nextArg!)]);
}
if (argsProgress != null) {
if (argsProgress.additionalFilters.isNotEmpty) {
groupedTestFilters[groupedTestFilters.length - 1]
.addAll(argsProgress.additionalFilters);
}
args = argsProgress.args;
}
}
if (groupedTestFilters.isEmpty) {
groupedTestFilters.add([MatchableArgument.empty()]);
}
return groupedTestFilters;
}
_ArgumentsProgress _takeComponentFilters(
_ListEmitter<String> args,
List<String> testNames,
) {
final additionalFilters = <MatchableArgument>[];
// Stop 1 item early because a trailing `-c` flag is definitionally
// not followed by another token
while (args.hasNMore(2)) {
var arg = args.peek();
if (['--component', '-c'].contains(arg)) {
// Confirm our peek
args.take();
var nextArg = args.take();
additionalFilters.add(MatchableArgument.componentName(nextArg!));
} else {
break;
}
}
return _ArgumentsProgress(args, additionalFilters);
}
_ArgumentsProgress _takeAndFilters(
_ListEmitter<String> args,
List<String> testNames,
) {
final additionalFilters = <MatchableArgument>[];
// Stop 1 item early because a trailing `-a` flag is definitionally
// not followed by another token
while (args.hasNMore(2)) {
var arg = args.peek();
if (['--all', '-a'].contains(arg)) {
// Confirm our peek
args.take();
var nextFilter = args.take();
additionalFilters.addAll(
nextFilter!
.split(',')
// ignore: unnecessary_lambdas
.map((testName) => MatchableArgument.unrestricted(testName))
.toList(),
);
} else {
break;
}
}
return _ArgumentsProgress(args, additionalFilters);
}
static List<String> _processTestList(
List<String> testList,
String relativeCwd,
) {
// Replace a test name that is merely "." with the actual current directory
// Use a set-to-list maneuver to remove duplicates
return {
for (var testName in testList)
testName == cwdToken ? relativeCwd : testName
}.toList();
}
static List<String> _processRawArgs(
List<String> rawArgs,
String relativeCwd,
) {
// Replace a test name that is merely "." with the actual current directory
// Use a set-to-list maneuver to remove duplicates
return [for (var arg in rawArgs) arg == cwdToken ? relativeCwd : arg];
}
}
/// Helper that tracks progress through the positionally-aware parsing of
/// raw arguments.
class _ArgumentsProgress {
final _ListEmitter<String> args;
final List<MatchableArgument> additionalFilters;
_ArgumentsProgress(this.args, this.additionalFilters);
}
class _ListEmitter<T> {
final List<T> _list;
int _counter = 0;
_ListEmitter(this._list);
/// Look at the next item in the list without advancing the marker.
T? peek() => _list.length > _counter ? _list[_counter] : null;
/// Look at the next item in the list and advance the marker.
T? take() {
if (_counter == _list.length) return null;
_counter += 1;
return _list[_counter - 1];
}
bool hasMore() => hasNMore(1);
bool hasNMore(int numberMore) => _list.length - _counter >= numberMore;
}
enum MatchType {
/// Default rule which can match against any portion of a [TestDefinition].
unrestricted,
/// Specialized rule which indicates only matching against package names.
packageName,
/// Specialized rule which indicates only matching against component names.
componentName,
}
/// Container for a [String] used to match against different parts of test names
/// with an additional [MatchType] attribute which optionally specifies
/// limitations on parts of a [TestDefinition] eligible for matching.
class MatchableArgument {
final String? arg;
final MatchType matchType;
/// Generic constructor which accepts all paramters. Not preferred for
/// external use.
MatchableArgument._(this.arg, this.matchType);
/// Helper constructor for unrestricted matchers.
factory MatchableArgument.unrestricted(String arg) =>
MatchableArgument._(arg, MatchType.unrestricted);
/// Helper constructor for package-name matchers.
factory MatchableArgument.packageName(String arg) =>
MatchableArgument._(arg, MatchType.packageName);
/// Helper constructor for component-name matchers.
factory MatchableArgument.componentName(String arg) =>
MatchableArgument._(arg, MatchType.componentName);
/// Helper constructor for when there are zero test name tokens whatsoever.
factory MatchableArgument.empty() =>
MatchableArgument._(null, MatchType.unrestricted);
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other is MatchableArgument &&
runtimeType == other.runtimeType &&
arg == other.arg &&
matchType == other.matchType);
}
@override
int get hashCode => arg.hashCode ^ matchType.hashCode;
@override
String toString() =>
'<MatchableArgument ${matchType.toString().split('.')[1]}::"$arg" />';
}