Unverified Commit 8712f63d authored by Jenn Magder's avatar Jenn Magder Committed by GitHub

Split tools_tests subshards into separate shards to support sub-sub-sharding (#75033)

parent 13fe079b
...@@ -62,6 +62,9 @@ final bool useFlutterTestFormatter = Platform.environment['FLUTTER_TEST_FORMATTE ...@@ -62,6 +62,9 @@ final bool useFlutterTestFormatter = Platform.environment['FLUTTER_TEST_FORMATTE
/// and make sure it runs _all_ shards. /// and make sure it runs _all_ shards.
const int kBuildTestShardCount = 2; const int kBuildTestShardCount = 2;
const String kShardKey = 'SHARD';
const String kSubshardKey = 'SUBSHARD';
/// The number of Cirrus jobs that run Web tests in parallel. /// The number of Cirrus jobs that run Web tests in parallel.
/// ///
/// The default is 8 shards. Typically .cirrus.yml would define the /// The default is 8 shards. Typically .cirrus.yml would define the
...@@ -88,6 +91,8 @@ const List<String> kWebTestFileKnownFailures = <String>[ ...@@ -88,6 +91,8 @@ const List<String> kWebTestFileKnownFailures = <String>[
'test/examples/sector_layout_test.dart', 'test/examples/sector_layout_test.dart',
]; ];
const String kSmokeTestShardName = 'smoke_tests';
/// When you call this, you can pass additional arguments to pass custom /// When you call this, you can pass additional arguments to pass custom
/// arguments to flutter test. For example, you might want to call this /// arguments to flutter test. For example, you might want to call this
/// script with the parameter --local-engine=host_debug_unopt to /// script with the parameter --local-engine=host_debug_unopt to
...@@ -107,18 +112,22 @@ Future<void> main(List<String> args) async { ...@@ -107,18 +112,22 @@ Future<void> main(List<String> args) async {
print('═' * 80); print('═' * 80);
await _runSmokeTests(); await _runSmokeTests();
print('═' * 80); print('═' * 80);
await selectShard(const <String, ShardRunner>{ await selectShard(<String, ShardRunner>{
'add_to_app_life_cycle_tests': _runAddToAppLifeCycleTests, 'add_to_app_life_cycle_tests': _runAddToAppLifeCycleTests,
'build_tests': _runBuildTests, 'build_tests': _runBuildTests,
'framework_coverage': _runFrameworkCoverage, 'framework_coverage': _runFrameworkCoverage,
'framework_tests': _runFrameworkTests, 'framework_tests': _runFrameworkTests,
'tool_coverage': _runToolCoverage, 'tool_coverage': _runToolCoverage,
'tool_tests': _runToolTests, 'tool_tests': _runToolTests,
'tool_general_tests': _runGeneralToolTests,
'tool_command_tests': _runCommandsToolTests,
'tool_integration_tests': _runIntegrationToolTests,
'web_tool_tests': _runWebToolTests, 'web_tool_tests': _runWebToolTests,
'web_tests': _runWebUnitTests, 'web_tests': _runWebUnitTests,
'web_integration_tests': _runWebIntegrationTests, 'web_integration_tests': _runWebIntegrationTests,
'web_long_running_tests': _runWebLongRunningTests, 'web_long_running_tests': _runWebLongRunningTests,
'flutter_plugins': _runFlutterPluginsTests, 'flutter_plugins': _runFlutterPluginsTests,
kSmokeTestShardName: () async {}, // No-op, the smoke tests already ran. Used for testing this script.
}); });
} on ExitException catch (error) { } on ExitException catch (error) {
error.apply(); error.apply();
...@@ -183,68 +192,96 @@ Future<void> _runSmokeTests() async { ...@@ -183,68 +192,96 @@ Future<void> _runSmokeTests() async {
// We run the "pass" and "fail" smoke tests first, and alone, because those // We run the "pass" and "fail" smoke tests first, and alone, because those
// are particularly critical and sensitive. If one of these fails, there's no // are particularly critical and sensitive. If one of these fails, there's no
// point even trying the others. // point even trying the others.
await _runFlutterTest(automatedTests, final List<ShardRunner> tests = <ShardRunner>[
script: path.join('test_smoke_test', 'pass_test.dart'), () => _runFlutterTest(
printOutput: false, automatedTests,
); script: path.join('test_smoke_test', 'pass_test.dart'),
await _runFlutterTest(automatedTests, printOutput: false,
script: path.join('test_smoke_test', 'fail_test.dart'), ),
expectFailure: true, () => _runFlutterTest(
printOutput: false, automatedTests,
); script: path.join('test_smoke_test', 'fail_test.dart'),
// We run the timeout tests individually because they are timing-sensitive. expectFailure: true,
await _runFlutterTest(automatedTests, printOutput: false,
script: path.join('test_smoke_test', 'timeout_pass_test.dart'), ),
expectFailure: false, // We run the timeout tests individually because they are timing-sensitive.
printOutput: false, () => _runFlutterTest(
); automatedTests,
await _runFlutterTest(automatedTests, script: path.join('test_smoke_test', 'timeout_pass_test.dart'),
script: path.join('test_smoke_test', 'timeout_fail_test.dart'), expectFailure: false,
expectFailure: true, printOutput: false,
printOutput: false, ),
); () => _runFlutterTest(
await _runFlutterTest(automatedTests, automatedTests,
script: path.join('test_smoke_test', 'pending_timer_fail_test.dart'), script: path.join('test_smoke_test', 'timeout_fail_test.dart'),
expectFailure: true, expectFailure: true,
printOutput: false, printOutput: false,
outputChecker: (CommandResult result) { ),
return result.flattenedStdout.contains('failingPendingTimerTest') () => _runFlutterTest(automatedTests,
? null script:
: 'Failed to find the stack trace for the pending Timer.'; path.join('test_smoke_test', 'pending_timer_fail_test.dart'),
} expectFailure: true,
); printOutput: false, outputChecker: (CommandResult result) {
// We run the remaining smoketests in parallel, because they each take some return result.flattenedStdout.contains('failingPendingTimerTest')
// time to run (e.g. compiling), so we don't want to run them in series, ? null
// especially on 20-core machines... : 'Failed to find the stack trace for the pending Timer.';
await Future.wait<void>( }),
<Future<void>>[ // We run the remaining smoketests in parallel, because they each take some
_runFlutterTest(automatedTests, // time to run (e.g. compiling), so we don't want to run them in series,
script: path.join('test_smoke_test', 'crash1_test.dart'), // especially on 20-core machines...
expectFailure: true, () => Future.wait<void>(
printOutput: false, <Future<void>>[
), _runFlutterTest(
_runFlutterTest(automatedTests, automatedTests,
script: path.join('test_smoke_test', 'crash2_test.dart'), script: path.join('test_smoke_test', 'crash1_test.dart'),
expectFailure: true, expectFailure: true,
printOutput: false, printOutput: false,
), ),
_runFlutterTest(automatedTests, _runFlutterTest(
script: path.join('test_smoke_test', 'syntax_error_test.broken_dart'), automatedTests,
expectFailure: true, script: path.join('test_smoke_test', 'crash2_test.dart'),
printOutput: false, expectFailure: true,
), printOutput: false,
_runFlutterTest(automatedTests, ),
script: path.join('test_smoke_test', 'missing_import_test.broken_dart'), _runFlutterTest(
expectFailure: true, automatedTests,
printOutput: false, script:
), path.join('test_smoke_test', 'syntax_error_test.broken_dart'),
_runFlutterTest(automatedTests, expectFailure: true,
script: path.join('test_smoke_test', 'disallow_error_reporter_modification_test.dart'), printOutput: false,
expectFailure: true, ),
printOutput: false, _runFlutterTest(
), automatedTests,
], script: path.join(
); 'test_smoke_test', 'missing_import_test.broken_dart'),
expectFailure: true,
printOutput: false,
),
_runFlutterTest(
automatedTests,
script: path.join('test_smoke_test',
'disallow_error_reporter_modification_test.dart'),
expectFailure: true,
printOutput: false,
),
],
),
];
List<ShardRunner> testsToRun;
// Smoke tests are special and run first for all test shards.
// Run all smoke tests for other shards.
// Only shard smoke tests when explicitly specified.
final String shardName = Platform.environment[kShardKey];
if (shardName == kSmokeTestShardName) {
testsToRun = _selectIndexOfTotalSubshard<ShardRunner>(tests);
} else {
testsToRun = tests;
}
for (final ShardRunner test in testsToRun) {
await test();
}
// Verify that we correctly generated the version file. // Verify that we correctly generated the version file.
final String versionError = await verifyVersion(File(path.join(flutterRoot, 'version'))); final String versionError = await verifyVersion(File(path.join(flutterRoot, 'version')));
...@@ -276,6 +313,42 @@ Future<void> _runToolCoverage() async { ...@@ -276,6 +313,42 @@ Future<void> _runToolCoverage() async {
); );
} }
Future<void> _runGeneralToolTests() async {
await _pubRunTest(
path.join(flutterRoot, 'packages', 'flutter_tools'),
testPaths: <String>[path.join('test', 'general.shard')],
enableFlutterToolAsserts: false,
// Detect unit test time regressions (poor time delay handling, etc).
perTestTimeout: const Duration(seconds: 2),
);
}
Future<void> _runCommandsToolTests() async {
// Due to https://github.com/flutter/flutter/issues/46180, skip the hermetic directory
// on Windows.
final String suffix = Platform.isWindows ? 'permeable' : '';
await _pubRunTest(
path.join(flutterRoot, 'packages', 'flutter_tools'),
forceSingleCore: true,
testPaths: <String>[path.join('test', 'commands.shard', suffix)],
);
}
Future<void> _runIntegrationToolTests() async {
final String toolsPath = path.join(flutterRoot, 'packages', 'flutter_tools');
final List<String> allTests = Directory(path.join(toolsPath, 'test', 'integration.shard'))
.listSync(recursive: true).whereType<File>()
.map<String>((FileSystemEntity entry) => path.relative(entry.path, from: toolsPath))
.where((String testPath) => path.basename(testPath).endsWith('_test.dart')).toList();
await _pubRunTest(
toolsPath,
forceSingleCore: true,
testPaths: _selectIndexOfTotalSubshard<String>(allTests),
);
}
// TODO(jmagman): Remove once LUCI configs are migrated to tool_tests_general, tool_tests_command, and tool_tests_integration.
Future<void> _runToolTests() async { Future<void> _runToolTests() async {
const String kDotShard = '.shard'; const String kDotShard = '.shard';
const String kWeb = 'web'; const String kWeb = 'web';
...@@ -810,7 +883,10 @@ Future<void> _runWebLongRunningTests() async { ...@@ -810,7 +883,10 @@ Future<void> _runWebLongRunningTests() async {
() => _runGalleryE2eWebTest('release', canvasKit: true), () => _runGalleryE2eWebTest('release', canvasKit: true),
]; ];
await _ensureChromeDriverIsRunning(); await _ensureChromeDriverIsRunning();
await _selectIndexedSubshard(tests, kWebLongRunningTestShardCount); if (!await _runShardRunnerIndexOfTotalSubshard(tests)) {
// TODO(jmagman): Remove fallback once LUCI configs are migrated to d+_d+ subshard format.
await _selectIndexedSubshard(tests, kWebLongRunningTestShardCount);
}
await _stopChromeDriver(); await _stopChromeDriver();
} }
...@@ -1457,6 +1533,60 @@ Future<void> _selectIndexedSubshard(List<ShardRunner> tests, int numberOfShards) ...@@ -1457,6 +1533,60 @@ Future<void> _selectIndexedSubshard(List<ShardRunner> tests, int numberOfShards)
await selectSubshard(subshards); await selectSubshard(subshards);
} }
/// Parse (one-)index/total-named subshards from environment variable SUBSHARD
/// and equally distribute [tests] between them.
/// Subshard format is "{index}_{total number of shards}".
/// The scheduler can change the number of total shards without needing an additional
/// commit in this repository.
///
/// Examples:
/// 1_3
/// 2_3
/// 3_3
List<T> _selectIndexOfTotalSubshard<T>(List<T> tests, {String subshardKey = kSubshardKey}) {
// Example: "1_3" means the first (one-indexed) shard of three total shards.
final String subshardName = Platform.environment[subshardKey];
if (subshardName == null) {
print('$kSubshardKey environment variable is missing, skipping sharding');
return tests;
}
print('$bold$subshardKey=$subshardName$reset');
final RegExp pattern = RegExp(r'^(\d+)_(\d+)$');
final Match match = pattern.firstMatch(subshardName);
if (match == null || match.groupCount != 2) {
print('${red}Invalid subshard name "$subshardName". Expected format "[int]_[int]" ex. "1_3"');
// TODO(jmagman): exit(1) here instead once LUCI configs are migrated to d+_d+ subshard format.
return null;
}
// One-indexed.
final int index = int.parse(match.group(1));
final int total = int.parse(match.group(2));
if (index > total) {
print('${red}Invalid subshard name "$subshardName". Index number must be greater or equal to total.');
exit(1);
}
final int testsPerShard = tests.length ~/ total;
final int start = (index - 1) * testsPerShard;
final int end = index * testsPerShard;
print('Selecting subshard $index of $total (range ${start + 1}-$end of ${tests.length})');
return tests.sublist(start, end);
}
Future<bool> _runShardRunnerIndexOfTotalSubshard(List<ShardRunner> tests) async {
final List<ShardRunner> sublist = _selectIndexOfTotalSubshard<ShardRunner>(tests);
// TODO(jmagman): Remove the boolean return to indicate fallback to unsharded variant once LUCI configs are migrated to d+_d+ subshard format.
if (sublist == null) {
return false;
}
for (final ShardRunner test in sublist) {
await test();
}
return true;
}
/// If the CIRRUS_TASK_NAME environment variable exists, we use that to determine /// If the CIRRUS_TASK_NAME environment variable exists, we use that to determine
/// the shard and sub-shard (parsing it in the form shard-subshard-platform, ignoring /// the shard and sub-shard (parsing it in the form shard-subshard-platform, ignoring
/// the platform). /// the platform).
...@@ -1465,8 +1595,8 @@ Future<void> _selectIndexedSubshard(List<ShardRunner> tests, int numberOfShards) ...@@ -1465,8 +1595,8 @@ Future<void> _selectIndexedSubshard(List<ShardRunner> tests, int numberOfShards)
/// environment variables. For example, to run all the framework tests you can /// environment variables. For example, to run all the framework tests you can
/// just set SHARD=framework_tests. To run specifically the third subshard of /// just set SHARD=framework_tests. To run specifically the third subshard of
/// the Web tests you can set SHARD=web_tests SUBSHARD=2 (it's zero-based). /// the Web tests you can set SHARD=web_tests SUBSHARD=2 (it's zero-based).
Future<void> selectShard(Map<String, ShardRunner> shards) => _runFromList(shards, 'SHARD', 'shard', 0); Future<void> selectShard(Map<String, ShardRunner> shards) => _runFromList(shards, kShardKey, 'shard', 0);
Future<void> selectSubshard(Map<String, ShardRunner> subshards) => _runFromList(subshards, 'SUBSHARD', 'subshard', 1); Future<void> selectSubshard(Map<String, ShardRunner> subshards) => _runFromList(subshards, kSubshardKey, 'subshard', 1);
const String CIRRUS_TASK_NAME = 'CIRRUS_TASK_NAME'; const String CIRRUS_TASK_NAME = 'CIRRUS_TASK_NAME';
......
...@@ -8,6 +8,7 @@ import 'package:file/file.dart' as fs; ...@@ -8,6 +8,7 @@ import 'package:file/file.dart' as fs;
import 'package:file/memory.dart'; import 'package:file/memory.dart';
import 'package:mockito/mockito.dart'; import 'package:mockito/mockito.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import 'package:process/process.dart';
import '../test.dart'; import '../test.dart';
import 'common.dart'; import 'common.dart';
...@@ -80,4 +81,44 @@ void main() { ...@@ -80,4 +81,44 @@ void main() {
expect(actualHash, kSampleHash); expect(actualHash, kSampleHash);
}); });
}); });
group('test.dart script', () {
const ProcessManager processManager = LocalProcessManager();
Future<ProcessResult> runScript(
[Map<String, String> environment, List<String> otherArgs = const <String>[]]) async {
final String dart = path.absolute(
path.join('..', '..', 'bin', 'cache', 'dart-sdk', 'bin', 'dart'));
final ProcessResult scriptProcess = processManager.runSync(<String>[
dart,
'test.dart',
...otherArgs,
], environment: environment);
return scriptProcess;
}
test('subshards tests correctly', () async {
ProcessResult result = await runScript(
<String, String>{'SHARD': 'smoke_tests', 'SUBSHARD': '1_3'},
);
expect(result.exitCode, 0);
// There are currently 6 smoke tests. This shard should contain test 1 and 2.
expect(result.stdout, contains('Selecting subshard 1 of 3 (range 1-2 of 6)'));
result = await runScript(
<String, String>{'SHARD': 'smoke_tests', 'SUBSHARD': '5_6'},
);
expect(result.exitCode, 0);
// This shard should contain only test 5.
expect(result.stdout, contains('Selecting subshard 5 of 6 (range 5-5 of 6)'));
});
test('exits with code 1 when SUBSHARD index greater than total', () async {
final ProcessResult result = await runScript(
<String, String>{'SHARD': 'smoke_tests', 'SUBSHARD': '100_99'},
);
expect(result.exitCode, 1);
expect(result.stdout, contains('Invalid subshard name'));
});
});
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment