| // 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 ==(dynamic 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" />'; |
| } |