| 1 | // Copyright 2014 The Flutter Authors. All rights reserved. |
| 2 | // Use of this source code is governed by a BSD-style license that can be |
| 3 | // found in the LICENSE file. |
| 4 | |
| 5 | import 'dart:io'; |
| 6 | |
| 7 | import 'package:args/args.dart' ; |
| 8 | import 'package:file/local.dart' ; |
| 9 | import 'package:glob/glob.dart' ; |
| 10 | import 'package:path/path.dart' as path; |
| 11 | |
| 12 | import 'lib/runner.dart'; |
| 13 | |
| 14 | Future<void> main(List<String> arguments) async { |
| 15 | exit(await run(arguments) ? 0 : 1); |
| 16 | } |
| 17 | |
| 18 | // Return true if successful, false if failed. |
| 19 | Future<bool> run(List<String> arguments) async { |
| 20 | final ArgParser argParser = ArgParser(allowTrailingOptions: false, usageLineLength: 72) |
| 21 | ..addOption( |
| 22 | 'repeat' , |
| 23 | defaultsTo: '1' , |
| 24 | help: |
| 25 | 'How many times to run each test. Set to a high value to look for flakes. If a test specifies a number of iterations, the lower of the two values is used.' , |
| 26 | valueHelp: 'count' , |
| 27 | ) |
| 28 | ..addOption( |
| 29 | 'shards' , |
| 30 | defaultsTo: '1' , |
| 31 | help: 'How many shards to split the tests into. Used in continuous integration.' , |
| 32 | valueHelp: 'count' , |
| 33 | ) |
| 34 | ..addOption( |
| 35 | 'shard-index' , |
| 36 | defaultsTo: '0' , |
| 37 | help: |
| 38 | 'The current shard to run the tests with the range [0 .. shards - 1]. Used in continuous integration.' , |
| 39 | valueHelp: 'count' , |
| 40 | ) |
| 41 | ..addFlag('skip-on-fetch-failure' , help: 'Whether to skip tests that we fail to download.' ) |
| 42 | ..addFlag('skip-template' , help: 'Whether to skip tests named "template.test".' ) |
| 43 | ..addFlag('verbose' , help: 'Describe what is happening in detail.' ) |
| 44 | ..addFlag('help' , negatable: false, help: 'Print this help message.' ); |
| 45 | |
| 46 | void printHelp() { |
| 47 | print('run_tests.dart [options...] path/to/file1.test path/to/file2.test...' ); |
| 48 | print('For details on the test registry format, see:' ); |
| 49 | print(' https://github.com/flutter/tests/blob/main/registry/template.test'); |
| 50 | print('' ); |
| 51 | print(argParser.usage); |
| 52 | print('' ); |
| 53 | } |
| 54 | |
| 55 | ArgResults parsedArguments; |
| 56 | try { |
| 57 | parsedArguments = argParser.parse(arguments); |
| 58 | } on ArgParserException catch (error) { |
| 59 | printHelp(); |
| 60 | print('Error: ${error.message} Use --help for usage information.' ); |
| 61 | exit(1); |
| 62 | } |
| 63 | |
| 64 | final int? repeat = int.tryParse(parsedArguments['repeat' ] as String); |
| 65 | final bool skipOnFetchFailure = parsedArguments['skip-on-fetch-failure' ] as bool; |
| 66 | final bool skipTemplate = parsedArguments['skip-template' ] as bool; |
| 67 | final bool verbose = parsedArguments['verbose' ] as bool; |
| 68 | final bool help = parsedArguments['help' ] as bool; |
| 69 | final int? numberShards = int.tryParse(parsedArguments['shards' ] as String); |
| 70 | final int? shardIndex = int.tryParse(parsedArguments['shard-index' ] as String); |
| 71 | final List<File> files = parsedArguments.rest |
| 72 | .expand((String path) => Glob(path).listFileSystemSync(const LocalFileSystem())) |
| 73 | .whereType<File>() |
| 74 | .where((File file) => !skipTemplate || path.basename(file.path) != 'template.test' ) |
| 75 | .toList(); |
| 76 | |
| 77 | if (files.isEmpty && parsedArguments.rest.isNotEmpty) { |
| 78 | print('No files resolved from glob(s): ${parsedArguments.rest}' ); |
| 79 | } |
| 80 | |
| 81 | if (help || |
| 82 | repeat == null || |
| 83 | files.isEmpty || |
| 84 | numberShards == null || |
| 85 | numberShards <= 0 || |
| 86 | shardIndex == null || |
| 87 | shardIndex < 0) { |
| 88 | printHelp(); |
| 89 | if (verbose) { |
| 90 | if (repeat == null) { |
| 91 | print('Error: Could not parse repeat count (" ${parsedArguments['repeat' ]}")' ); |
| 92 | } |
| 93 | if (numberShards == null) { |
| 94 | print('Error: Could not parse shards count (" ${parsedArguments['shards' ]}")' ); |
| 95 | } else if (numberShards < 1) { |
| 96 | print( |
| 97 | 'Error: The specified shards count ( $numberShards) is less than 1. It must be greater than zero.' , |
| 98 | ); |
| 99 | } |
| 100 | if (shardIndex == null) { |
| 101 | print('Error: Could not parse shard index (" ${parsedArguments['shard-index' ]}")' ); |
| 102 | } else if (shardIndex < 0) { |
| 103 | print( |
| 104 | 'Error: The specified shard index ( $shardIndex) is negative. It must be in the range [0 .. shards - 1].' , |
| 105 | ); |
| 106 | } |
| 107 | if (parsedArguments.rest.isEmpty) { |
| 108 | print('Error: No file arguments specified.' ); |
| 109 | } else if (files.isEmpty) { |
| 110 | print( |
| 111 | 'Error: File arguments (" ${parsedArguments.rest.join('", "' )}") did not identify any real files.' , |
| 112 | ); |
| 113 | } |
| 114 | } |
| 115 | return help; |
| 116 | } |
| 117 | |
| 118 | if (shardIndex > numberShards - 1) { |
| 119 | print( |
| 120 | 'Error: The specified shard index ( $shardIndex) is more than the specified number of shards ( $numberShards). ' |
| 121 | 'It must be in the range [0 .. shards - 1].' , |
| 122 | ); |
| 123 | return false; |
| 124 | } |
| 125 | |
| 126 | if (files.length < numberShards) { |
| 127 | print('Warning: There are more shards than tests. Some shards will not run any tests.' ); |
| 128 | } |
| 129 | |
| 130 | return runTests( |
| 131 | repeat: repeat, |
| 132 | skipOnFetchFailure: skipOnFetchFailure, |
| 133 | verbose: verbose, |
| 134 | numberShards: numberShards, |
| 135 | shardIndex: shardIndex, |
| 136 | files: files, |
| 137 | ); |
| 138 | } |
| 139 | |