From 64326bda57b22c5e1b2aa51f33fab8cdd76d35f6 Mon Sep 17 00:00:00 2001 From: Peter Trost Date: Fri, 28 Aug 2026 17:31:00 +0200 Subject: [PATCH 1/2] feat(test): support sharding tests across CI runners MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add `--shard-index` and `--total-shards` to `very_good test` and `very_good dart test`, so a test suite can be split across multiple CI runners with a `strategy.matrix`. The test optimizer already discovers every test file, so sharding is a partition of that list rather than new machinery. Files are sorted and dealt out round-robin, which keeps shards balanced by file count and makes the partition deterministic across machines — `Directory.listSync` order is filesystem dependent, so without sorting two runners could disagree and either skip or duplicate tests. Tests tagged `skip_very_good_optimization` are sharded as well. They run as standalone files alongside the optimizer entrypoint, so leaving them unsharded would re-run all of them on every runner. Each runner generates its own `.test_optimizer.dart` containing only its slice, so no shard-specific filenames are needed. A shard with no test files succeeds instead of failing with "No tests were found", so an oversized matrix does not break the build. Sharding is rejected with a usage error when combined with `--min-coverage`, since each shard only exercises a subset of the codebase and its coverage is not representative of the whole suite, and when the optimizer is disabled, which sharding depends on. Closes #1538 Co-Authored-By: Claude Opus 5 --- bricks/test_optimizer/brick.yaml | 8 + bricks/test_optimizer/hooks/lib/pre_gen.dart | 77 ++++++-- .../hooks/test/pre_gen_test.dart | 139 +++++++++++++++ lib/src/cli/dart_cli.dart | 4 + lib/src/cli/flutter_cli.dart | 4 + .../cli/templates/test_optimizer_bundle.dart | 14 +- lib/src/cli/test_cli_runner.dart | 22 ++- .../dart/commands/dart_test_command.dart | 50 ++++++ lib/src/commands/test/test.dart | 105 +++++++++++ site/docs/commands/test.md | 44 +++++ test/src/cli/test_cli_runner_test.dart | 105 +++++++++++ .../dart/commands/dart_test_test.dart | 93 ++++++++++ test/src/commands/test/test_test.dart | 166 ++++++++++++++++++ 13 files changed, 812 insertions(+), 19 deletions(-) diff --git a/bricks/test_optimizer/brick.yaml b/bricks/test_optimizer/brick.yaml index 07aa58c3d..3c2b80b62 100644 --- a/bricks/test_optimizer/brick.yaml +++ b/bricks/test_optimizer/brick.yaml @@ -12,3 +12,11 @@ vars: default: "." description: The path to the package root. prompt: Please enter the path to the package root. + shard-index: + type: number + description: The 1-based index of the shard to generate tests for. + prompt: Please enter the shard index. + total-shards: + type: number + description: The total number of shards the test suite is split into. + prompt: Please enter the total number of shards. diff --git a/bricks/test_optimizer/hooks/lib/pre_gen.dart b/bricks/test_optimizer/hooks/lib/pre_gen.dart index c0348ef02..bc789d217 100644 --- a/bricks/test_optimizer/hooks/lib/pre_gen.dart +++ b/bricks/test_optimizer/hooks/lib/pre_gen.dart @@ -33,35 +33,80 @@ Future run(HookContext context) async { final flutterSdkRegExp = RegExp(r'sdk:\s*flutter$', multiLine: true); final isFlutter = flutterSdkRegExp.hasMatch(pubspecContents); - final identifierGenerator = DartIdentifierGenerator(); - final testIdentifierTable = >[]; + final shardIndex = context.vars['shard-index'] as int?; + final totalShards = context.vars['total-shards'] as int?; + final tests = testDir .listSync(recursive: true) - .where((entity) => entity.isTest); + .where( + (entity) => entity.isTest, + ); final notOptimizedTests = await getNotOptimizedTests(tests, testDir.path); - for (final entity in tests) { - final relativePath = path - .relative(entity.path, from: testDir.path) - .replaceAll(r'\', '/'); - testIdentifierTable.add({ - 'path': relativePath, - 'identifier': identifierGenerator.next(), - }); - } + // Sorting guarantees a deterministic order across machines, which is what + // makes sharding reproducible: `Directory.listSync` order is filesystem + // dependent, so without this two runners could disagree on the partition + // and either skip or duplicate tests. + final testPaths = + tests + .map( + (entity) => path + .relative(entity.path, from: testDir.path) + .replaceAll(r'\', '/'), + ) + .toList() + ..sort(); + + final optimizedTestPaths = shardOf( + testPaths.where((p) => !notOptimizedTests.contains(p)).toList(), + shardIndex: shardIndex, + totalShards: totalShards, + ); + + // Non optimized tests run as standalone files alongside the optimizer + // entrypoint, so they must be sharded too. Otherwise every runner would + // re-run all of them, defeating the purpose of sharding. + final shardedNotOptimizedTests = shardOf( + notOptimizedTests..sort(), + shardIndex: shardIndex, + totalShards: totalShards, + ); - final optimizedTestsIdentifierTable = testIdentifierTable - .where((e) => !notOptimizedTests.contains(e['path'])) - .toList(); + final identifierGenerator = DartIdentifierGenerator(); + final optimizedTestsIdentifierTable = [ + for (final relativePath in optimizedTestPaths) + {'path': relativePath, 'identifier': identifierGenerator.next()}, + ]; context.vars = { 'tests': optimizedTestsIdentifierTable, 'isFlutter': isFlutter, - 'notOptimizedTests': notOptimizedTests, + 'notOptimizedTests': shardedNotOptimizedTests, }; } +/// Returns the subset of [paths] that belongs to the shard [shardIndex] out of +/// [totalShards]. +/// +/// Returns [paths] unchanged when sharding is not enabled (either value is +/// `null`). +/// +/// Files are dealt out round-robin (index modulo [totalShards]) over the +/// already sorted [paths], which keeps shards balanced in file count and makes +/// the partition stable for a given test suite. +List shardOf( + List paths, { + required int? shardIndex, + required int? totalShards, +}) { + if (shardIndex == null || totalShards == null) return paths; + + return [ + for (var i = shardIndex - 1; i < paths.length; i += totalShards) paths[i], + ]; +} + extension on FileSystemEntity { bool get isTest { return this is File && path.basename(this.path).endsWith('_test.dart'); diff --git a/bricks/test_optimizer/hooks/test/pre_gen_test.dart b/bricks/test_optimizer/hooks/test/pre_gen_test.dart index 2e96591cf..29958ca59 100644 --- a/bricks/test_optimizer/hooks/test/pre_gen_test.dart +++ b/bricks/test_optimizer/hooks/test/pre_gen_test.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'dart:math'; import 'package:hooks/pre_gen.dart' as pre_gen; import 'package:mason/mason.dart'; @@ -286,5 +287,143 @@ dependencies: }, ); }); + group('Sharding', () { + /// Creates a package with [count] optimizable test files, plus any + /// [notOptimized] files carrying the skip optimization tag. + Directory createPackage(int count, {int notOptimized = 0}) { + File(path.join(tempDirectory.path, 'pubspec.yaml')).createSync(); + final testDir = Directory(path.join(tempDirectory.path, 'test')) + ..createSync(); + for (var i = 0; i < count; i++) { + File(path.join(testDir.path, 'test${i}_test.dart')).createSync(); + } + for (var i = 0; i < notOptimized; i++) { + File( + path.join(testDir.path, 'skip${i}_test.dart'), + ).writeAsStringSync(notOptimizedTestContent); + } + return testDir; + } + + List pathsOf(HookContext context) { + final tests = context.vars['tests'] as List>; + return tests.map((e) => e['path']!).toList(); + } + + Future> runShard(int index, int total) async { + final context = _FakeContext() + ..vars['package-root'] = tempDirectory.absolute.path + ..vars['shard-index'] = index + ..vars['total-shards'] = total; + await pre_gen.run(context); + return [ + ...pathsOf(context), + ...(context.vars['notOptimizedTests']! as List).cast(), + ]; + } + + test('runs every test exactly once across all shards', () async { + createPackage(7, notOptimized: 2); + + final shards = [ + for (var i = 1; i <= 3; i++) await runShard(i, 3), + ]; + final union = shards.expand((shard) => shard).toList(); + + expect( + union..sort(), + [ + for (var i = 0; i < 7; i++) 'test${i}_test.dart', + for (var i = 0; i < 2; i++) 'skip${i}_test.dart', + ]..sort(), + reason: 'Shards must be a complete and disjoint partition', + ); + }); + + test('shards non optimized tests as well', () async { + createPackage(0, notOptimized: 4); + + final first = await runShard(1, 2); + final second = await runShard(2, 2); + + expect(first, ['skip0_test.dart', 'skip2_test.dart']); + expect(second, ['skip1_test.dart', 'skip3_test.dart']); + }); + + test('is deterministic across runs', () async { + createPackage(9); + + expect(await runShard(2, 4), await runShard(2, 4)); + }); + + test('balances shards within one file of each other', () async { + createPackage(10); + + final sizes = [ + for (var i = 1; i <= 4; i++) (await runShard(i, 4)).length, + ]; + + expect(sizes.reduce(max) - sizes.reduce(min), lessThanOrEqualTo(1)); + }); + + test( + 'yields an empty shard when there are more shards than tests', + () async { + createPackage(2); + + expect(await runShard(3, 3), isEmpty); + }, + ); + + test('includes every test when sharding is not requested', () async { + createPackage(3); + + final context = _FakeContext() + ..vars['package-root'] = tempDirectory.absolute.path; + await pre_gen.run(context); + + expect(pathsOf(context), hasLength(3)); + }); + }); + + group('shardOf', () { + const paths = ['a.dart', 'b.dart', 'c.dart', 'd.dart', 'e.dart']; + + test('returns paths untouched when shard index is null', () { + expect( + pre_gen.shardOf(paths, shardIndex: null, totalShards: 3), + paths, + ); + }); + + test('returns paths untouched when total shards is null', () { + expect( + pre_gen.shardOf(paths, shardIndex: 1, totalShards: null), + paths, + ); + }); + + test('deals paths out round robin', () { + expect( + pre_gen.shardOf(paths, shardIndex: 1, totalShards: 2), + ['a.dart', 'c.dart', 'e.dart'], + ); + expect( + pre_gen.shardOf(paths, shardIndex: 2, totalShards: 2), + ['b.dart', 'd.dart'], + ); + }); + + test('returns everything for a single shard', () { + expect(pre_gen.shardOf(paths, shardIndex: 1, totalShards: 1), paths); + }); + + test('returns empty for an out of range shard', () { + expect( + pre_gen.shardOf(paths, shardIndex: 9, totalShards: 9), + isEmpty, + ); + }); + }); }); } diff --git a/lib/src/cli/dart_cli.dart b/lib/src/cli/dart_cli.dart index b6ee3baa7..78fe52e76 100644 --- a/lib/src/cli/dart_cli.dart +++ b/lib/src/cli/dart_cli.dart @@ -136,6 +136,8 @@ class Dart { void Function(String)? stderr, GeneratorBuilder buildGenerator = MasonGenerator.fromBundle, List? reportOn, + int? shardIndex, + int? totalShards, }) async { return TestCLIRunner.test( logger: logger, @@ -157,6 +159,8 @@ class Dart { stderr: stderr, reportOn: reportOn, buildGenerator: buildGenerator, + shardIndex: shardIndex, + totalShards: totalShards, ); } } diff --git a/lib/src/cli/flutter_cli.dart b/lib/src/cli/flutter_cli.dart index 494d15015..1935f1f77 100644 --- a/lib/src/cli/flutter_cli.dart +++ b/lib/src/cli/flutter_cli.dart @@ -218,6 +218,8 @@ class Flutter { void Function(String)? stderr, GeneratorBuilder buildGenerator = MasonGenerator.fromBundle, List? reportOn, + int? shardIndex, + int? totalShards, }) async { return TestCLIRunner.test( logger: logger, @@ -238,6 +240,8 @@ class Flutter { stderr: stderr, buildGenerator: buildGenerator, reportOn: reportOn, + shardIndex: shardIndex, + totalShards: totalShards, ); } } diff --git a/lib/src/cli/templates/test_optimizer_bundle.dart b/lib/src/cli/templates/test_optimizer_bundle.dart index af7015c76..3a4a02f3e 100644 --- a/lib/src/cli/templates/test_optimizer_bundle.dart +++ b/lib/src/cli/templates/test_optimizer_bundle.dart @@ -20,7 +20,7 @@ final testOptimizerBundle = MasonBundle.fromJson({ }, { "path": "lib/pre_gen.dart", - "data": "aW1wb3J0ICdkYXJ0OmlvJzsKCmltcG9ydCAncGFja2FnZTpob29rcy9kYXJ0X2lkZW50aWZpZXJfZ2VuZXJhdG9yLmRhcnQnOwppbXBvcnQgJ3BhY2thZ2U6bWFzb24vbWFzb24uZGFydCc7CmltcG9ydCAncGFja2FnZTpwYXRoL3BhdGguZGFydCcgYXMgcGF0aDsKCnR5cGVkZWYgRXhpdEZuID0gTmV2ZXIgRnVuY3Rpb24oaW50IGNvZGUpOwoKRXhpdEZuIGV4aXRGbiA9IGV4aXQ7CgpTdHJpbmcgc2tpcFZlcnlHb29kT3B0aW1pemF0aW9uVGFnID0gJ3NraXBfdmVyeV9nb29kX29wdGltaXphdGlvbic7ClJlZ0V4cCBza2lwVmVyeUdvb2RPcHRpbWl6YXRpb25SZWdFeHAgPSBSZWdFeHAoCiAgIkBUYWdzXFxzKlxcKFxccypcXFtbXFxzXFxTXSo/W1wiJ10kc2tpcFZlcnlHb29kT3B0aW1pemF0aW9uVGFnW1wiJ11bXFxzXFxTXSo/XFxdXFxzKlxcKSIsCiAgbXVsdGlMaW5lOiB0cnVlLAopOwoKRnV0dXJlPHZvaWQ+IHJ1bihIb29rQ29udGV4dCBjb250ZXh0KSBhc3luYyB7CiAgZmluYWwgcGFja2FnZVJvb3QgPSBjb250ZXh0LnZhcnNbJ3BhY2thZ2Utcm9vdCddIGFzIFN0cmluZzsKICBmaW5hbCB0ZXN0RGlyID0gRGlyZWN0b3J5KHBhdGguam9pbihwYWNrYWdlUm9vdCwgJ3Rlc3QnKSk7CgogIGlmICghdGVzdERpci5leGlzdHNTeW5jKCkpIHsKICAgIGNvbnRleHQubG9nZ2VyLmVycignQ291bGQgbm90IGZpbmQgZGlyZWN0b3J5ICR7dGVzdERpci5wYXRofScpOwogICAgZXhpdEZuKDEpOwogIH0KCiAgZmluYWwgcHVic3BlYyA9IEZpbGUocGF0aC5qb2luKHBhY2thZ2VSb290LCAncHVic3BlYy55YW1sJykpOwogIGlmICghcHVic3BlYy5leGlzdHNTeW5jKCkpIHsKICAgIGNvbnRleHQubG9nZ2VyLmVycignQ291bGQgbm90IGZpbmQgcHVic3BlYy55YW1sIGF0ICR7dGVzdERpci5wYXRofScpOwogICAgZXhpdEZuKDEpOwogIH0KCiAgZmluYWwgcHVic3BlY0NvbnRlbnRzID0gYXdhaXQgcHVic3BlYy5yZWFkQXNTdHJpbmcoKTsKICBmaW5hbCBmbHV0dGVyU2RrUmVnRXhwID0gUmVnRXhwKHInc2RrOlxzKmZsdXR0ZXIkJywgbXVsdGlMaW5lOiB0cnVlKTsKICBmaW5hbCBpc0ZsdXR0ZXIgPSBmbHV0dGVyU2RrUmVnRXhwLmhhc01hdGNoKHB1YnNwZWNDb250ZW50cyk7CgogIGZpbmFsIGlkZW50aWZpZXJHZW5lcmF0b3IgPSBEYXJ0SWRlbnRpZmllckdlbmVyYXRvcigpOwogIGZpbmFsIHRlc3RJZGVudGlmaWVyVGFibGUgPSA8TWFwPFN0cmluZywgU3RyaW5nPj5bXTsKICBmaW5hbCB0ZXN0cyA9IHRlc3REaXIKICAgICAgLmxpc3RTeW5jKHJlY3Vyc2l2ZTogdHJ1ZSkKICAgICAgLndoZXJlKChlbnRpdHkpID0+IGVudGl0eS5pc1Rlc3QpOwoKICBmaW5hbCBub3RPcHRpbWl6ZWRUZXN0cyA9IGF3YWl0IGdldE5vdE9wdGltaXplZFRlc3RzKHRlc3RzLCB0ZXN0RGlyLnBhdGgpOwoKICBmb3IgKGZpbmFsIGVudGl0eSBpbiB0ZXN0cykgewogICAgZmluYWwgcmVsYXRpdmVQYXRoID0gcGF0aAogICAgICAgIC5yZWxhdGl2ZShlbnRpdHkucGF0aCwgZnJvbTogdGVzdERpci5wYXRoKQogICAgICAgIC5yZXBsYWNlQWxsKHInXCcsICcvJyk7CiAgICB0ZXN0SWRlbnRpZmllclRhYmxlLmFkZCh7CiAgICAgICdwYXRoJzogcmVsYXRpdmVQYXRoLAogICAgICAnaWRlbnRpZmllcic6IGlkZW50aWZpZXJHZW5lcmF0b3IubmV4dCgpLAogICAgfSk7CiAgfQoKICBmaW5hbCBvcHRpbWl6ZWRUZXN0c0lkZW50aWZpZXJUYWJsZSA9IHRlc3RJZGVudGlmaWVyVGFibGUKICAgICAgLndoZXJlKChlKSA9PiAhbm90T3B0aW1pemVkVGVzdHMuY29udGFpbnMoZVsncGF0aCddKSkKICAgICAgLnRvTGlzdCgpOwoKICBjb250ZXh0LnZhcnMgPSB7CiAgICAndGVzdHMnOiBvcHRpbWl6ZWRUZXN0c0lkZW50aWZpZXJUYWJsZSwKICAgICdpc0ZsdXR0ZXInOiBpc0ZsdXR0ZXIsCiAgICAnbm90T3B0aW1pemVkVGVzdHMnOiBub3RPcHRpbWl6ZWRUZXN0cywKICB9Owp9CgpleHRlbnNpb24gb24gRmlsZVN5c3RlbUVudGl0eSB7CiAgYm9vbCBnZXQgaXNUZXN0IHsKICAgIHJldHVybiB0aGlzIGlzIEZpbGUgJiYgcGF0aC5iYXNlbmFtZSh0aGlzLnBhdGgpLmVuZHNXaXRoKCdfdGVzdC5kYXJ0Jyk7CiAgfQp9CgpGdXR1cmU8TGlzdDxTdHJpbmc+PiBnZXROb3RPcHRpbWl6ZWRUZXN0cygKICBJdGVyYWJsZTxGaWxlU3lzdGVtRW50aXR5PiB0ZXN0cywKICBTdHJpbmcgdGVzdERpciwKKSBhc3luYyB7CiAgZmluYWwgcGF0aHMgPSB0ZXN0cy5tYXAoKGUpID0+IGUucGF0aCkudG9MaXN0KCk7CiAgZmluYWwgZm9ybWF0dGVkUGF0aHMgPSBwYXRocy5tYXAoKGUpID0+IGUucmVwbGFjZUFsbCgnLy4vJywgJy8nKSkudG9MaXN0KCk7CgogIGZpbmFsIGZpbGVGdXR1cmVzID0gZm9ybWF0dGVkUGF0aHMubWFwKF9jaGVja0ZpbGVGb3JTa2lwVmVyeUdvb2RPcHRpbWl6YXRpb24pOwogIGZpbmFsIGZpbGVSZXN1bHRzID0gYXdhaXQgRnV0dXJlLndhaXQoZmlsZUZ1dHVyZXMpOwoKICBmaW5hbCB0ZXN0V2l0aFZlcnlHb29kVGVzdCA9IDxTdHJpbmc+W107CiAgZm9yICh2YXIgaSA9IDA7IGkgPCBmb3JtYXR0ZWRQYXRocy5sZW5ndGg7IGkrKykgewogICAgaWYgKGZpbGVSZXN1bHRzW2ldKSB7CiAgICAgIHRlc3RXaXRoVmVyeUdvb2RUZXN0LmFkZChmb3JtYXR0ZWRQYXRoc1tpXSk7CiAgICB9CiAgfQoKICAvLy8gRm9ybWF0IHRvIHJlbGF0aXZlIHBhdGgKICBmaW5hbCByZWxhdGl2ZVBhdGhzID0gdGVzdFdpdGhWZXJ5R29vZFRlc3QKICAgICAgLm1hcCgoZSkgPT4gcGF0aC5yZWxhdGl2ZShlLCBmcm9tOiB0ZXN0RGlyKSkKICAgICAgLnRvTGlzdCgpOwoKICByZXR1cm4gcmVsYXRpdmVQYXRoczsKfQoKLy8vIENoZWNrIGlmIGEgc2luZ2xlIGZpbGUgY29udGFpbnMgc2tpcF92ZXJ5X2dvb2Rfb3B0aW1pemF0aW9uIHRhZwpGdXR1cmU8Ym9vbD4gX2NoZWNrRmlsZUZvclNraXBWZXJ5R29vZE9wdGltaXphdGlvbihTdHJpbmcgcGF0aCkgYXN5bmMgewogIGZpbmFsIGZpbGUgPSBGaWxlKHBhdGgpOwogIGlmICghZmlsZS5leGlzdHNTeW5jKCkpIHJldHVybiBmYWxzZTsKICBmaW5hbCBjb250ZW50ID0gYXdhaXQgZmlsZS5yZWFkQXNTdHJpbmcoKTsKICByZXR1cm4gc2tpcFZlcnlHb29kT3B0aW1pemF0aW9uUmVnRXhwLmhhc01hdGNoKGNvbnRlbnQpOwp9Cg==", + "data": "aW1wb3J0ICdkYXJ0OmlvJzsKCmltcG9ydCAncGFja2FnZTpob29rcy9kYXJ0X2lkZW50aWZpZXJfZ2VuZXJhdG9yLmRhcnQnOwppbXBvcnQgJ3BhY2thZ2U6bWFzb24vbWFzb24uZGFydCc7CmltcG9ydCAncGFja2FnZTpwYXRoL3BhdGguZGFydCcgYXMgcGF0aDsKCnR5cGVkZWYgRXhpdEZuID0gTmV2ZXIgRnVuY3Rpb24oaW50IGNvZGUpOwoKRXhpdEZuIGV4aXRGbiA9IGV4aXQ7CgpTdHJpbmcgc2tpcFZlcnlHb29kT3B0aW1pemF0aW9uVGFnID0gJ3NraXBfdmVyeV9nb29kX29wdGltaXphdGlvbic7ClJlZ0V4cCBza2lwVmVyeUdvb2RPcHRpbWl6YXRpb25SZWdFeHAgPSBSZWdFeHAoCiAgIkBUYWdzXFxzKlxcKFxccypcXFtbXFxzXFxTXSo/W1wiJ10kc2tpcFZlcnlHb29kT3B0aW1pemF0aW9uVGFnW1wiJ11bXFxzXFxTXSo/XFxdXFxzKlxcKSIsCiAgbXVsdGlMaW5lOiB0cnVlLAopOwoKRnV0dXJlPHZvaWQ+IHJ1bihIb29rQ29udGV4dCBjb250ZXh0KSBhc3luYyB7CiAgZmluYWwgcGFja2FnZVJvb3QgPSBjb250ZXh0LnZhcnNbJ3BhY2thZ2Utcm9vdCddIGFzIFN0cmluZzsKICBmaW5hbCB0ZXN0RGlyID0gRGlyZWN0b3J5KHBhdGguam9pbihwYWNrYWdlUm9vdCwgJ3Rlc3QnKSk7CgogIGlmICghdGVzdERpci5leGlzdHNTeW5jKCkpIHsKICAgIGNvbnRleHQubG9nZ2VyLmVycignQ291bGQgbm90IGZpbmQgZGlyZWN0b3J5ICR7dGVzdERpci5wYXRofScpOwogICAgZXhpdEZuKDEpOwogIH0KCiAgZmluYWwgcHVic3BlYyA9IEZpbGUocGF0aC5qb2luKHBhY2thZ2VSb290LCAncHVic3BlYy55YW1sJykpOwogIGlmICghcHVic3BlYy5leGlzdHNTeW5jKCkpIHsKICAgIGNvbnRleHQubG9nZ2VyLmVycignQ291bGQgbm90IGZpbmQgcHVic3BlYy55YW1sIGF0ICR7dGVzdERpci5wYXRofScpOwogICAgZXhpdEZuKDEpOwogIH0KCiAgZmluYWwgcHVic3BlY0NvbnRlbnRzID0gYXdhaXQgcHVic3BlYy5yZWFkQXNTdHJpbmcoKTsKICBmaW5hbCBmbHV0dGVyU2RrUmVnRXhwID0gUmVnRXhwKHInc2RrOlxzKmZsdXR0ZXIkJywgbXVsdGlMaW5lOiB0cnVlKTsKICBmaW5hbCBpc0ZsdXR0ZXIgPSBmbHV0dGVyU2RrUmVnRXhwLmhhc01hdGNoKHB1YnNwZWNDb250ZW50cyk7CgogIGZpbmFsIHNoYXJkSW5kZXggPSBjb250ZXh0LnZhcnNbJ3NoYXJkLWluZGV4J10gYXMgaW50PzsKICBmaW5hbCB0b3RhbFNoYXJkcyA9IGNvbnRleHQudmFyc1sndG90YWwtc2hhcmRzJ10gYXMgaW50PzsKCiAgZmluYWwgdGVzdHMgPSB0ZXN0RGlyCiAgICAgIC5saXN0U3luYyhyZWN1cnNpdmU6IHRydWUpCiAgICAgIC53aGVyZSgKICAgICAgICAoZW50aXR5KSA9PiBlbnRpdHkuaXNUZXN0LAogICAgICApOwoKICBmaW5hbCBub3RPcHRpbWl6ZWRUZXN0cyA9IGF3YWl0IGdldE5vdE9wdGltaXplZFRlc3RzKHRlc3RzLCB0ZXN0RGlyLnBhdGgpOwoKICAvLyBTb3J0aW5nIGd1YXJhbnRlZXMgYSBkZXRlcm1pbmlzdGljIG9yZGVyIGFjcm9zcyBtYWNoaW5lcywgd2hpY2ggaXMgd2hhdAogIC8vIG1ha2VzIHNoYXJkaW5nIHJlcHJvZHVjaWJsZTogYERpcmVjdG9yeS5saXN0U3luY2Agb3JkZXIgaXMgZmlsZXN5c3RlbQogIC8vIGRlcGVuZGVudCwgc28gd2l0aG91dCB0aGlzIHR3byBydW5uZXJzIGNvdWxkIGRpc2FncmVlIG9uIHRoZSBwYXJ0aXRpb24KICAvLyBhbmQgZWl0aGVyIHNraXAgb3IgZHVwbGljYXRlIHRlc3RzLgogIGZpbmFsIHRlc3RQYXRocyA9CiAgICAgIHRlc3RzCiAgICAgICAgICAubWFwKAogICAgICAgICAgICAoZW50aXR5KSA9PiBwYXRoCiAgICAgICAgICAgICAgICAucmVsYXRpdmUoZW50aXR5LnBhdGgsIGZyb206IHRlc3REaXIucGF0aCkKICAgICAgICAgICAgICAgIC5yZXBsYWNlQWxsKHInXCcsICcvJyksCiAgICAgICAgICApCiAgICAgICAgICAudG9MaXN0KCkKICAgICAgICAuLnNvcnQoKTsKCiAgZmluYWwgb3B0aW1pemVkVGVzdFBhdGhzID0gc2hhcmRPZigKICAgIHRlc3RQYXRocy53aGVyZSgocCkgPT4gIW5vdE9wdGltaXplZFRlc3RzLmNvbnRhaW5zKHApKS50b0xpc3QoKSwKICAgIHNoYXJkSW5kZXg6IHNoYXJkSW5kZXgsCiAgICB0b3RhbFNoYXJkczogdG90YWxTaGFyZHMsCiAgKTsKCiAgLy8gTm9uIG9wdGltaXplZCB0ZXN0cyBydW4gYXMgc3RhbmRhbG9uZSBmaWxlcyBhbG9uZ3NpZGUgdGhlIG9wdGltaXplcgogIC8vIGVudHJ5cG9pbnQsIHNvIHRoZXkgbXVzdCBiZSBzaGFyZGVkIHRvby4gT3RoZXJ3aXNlIGV2ZXJ5IHJ1bm5lciB3b3VsZAogIC8vIHJlLXJ1biBhbGwgb2YgdGhlbSwgZGVmZWF0aW5nIHRoZSBwdXJwb3NlIG9mIHNoYXJkaW5nLgogIGZpbmFsIHNoYXJkZWROb3RPcHRpbWl6ZWRUZXN0cyA9IHNoYXJkT2YoCiAgICBub3RPcHRpbWl6ZWRUZXN0cy4uc29ydCgpLAogICAgc2hhcmRJbmRleDogc2hhcmRJbmRleCwKICAgIHRvdGFsU2hhcmRzOiB0b3RhbFNoYXJkcywKICApOwoKICBmaW5hbCBpZGVudGlmaWVyR2VuZXJhdG9yID0gRGFydElkZW50aWZpZXJHZW5lcmF0b3IoKTsKICBmaW5hbCBvcHRpbWl6ZWRUZXN0c0lkZW50aWZpZXJUYWJsZSA9IFsKICAgIGZvciAoZmluYWwgcmVsYXRpdmVQYXRoIGluIG9wdGltaXplZFRlc3RQYXRocykKICAgICAgeydwYXRoJzogcmVsYXRpdmVQYXRoLCAnaWRlbnRpZmllcic6IGlkZW50aWZpZXJHZW5lcmF0b3IubmV4dCgpfSwKICBdOwoKICBjb250ZXh0LnZhcnMgPSB7CiAgICAndGVzdHMnOiBvcHRpbWl6ZWRUZXN0c0lkZW50aWZpZXJUYWJsZSwKICAgICdpc0ZsdXR0ZXInOiBpc0ZsdXR0ZXIsCiAgICAnbm90T3B0aW1pemVkVGVzdHMnOiBzaGFyZGVkTm90T3B0aW1pemVkVGVzdHMsCiAgfTsKfQoKLy8vIFJldHVybnMgdGhlIHN1YnNldCBvZiBbcGF0aHNdIHRoYXQgYmVsb25ncyB0byB0aGUgc2hhcmQgW3NoYXJkSW5kZXhdIG91dCBvZgovLy8gW3RvdGFsU2hhcmRzXS4KLy8vCi8vLyBSZXR1cm5zIFtwYXRoc10gdW5jaGFuZ2VkIHdoZW4gc2hhcmRpbmcgaXMgbm90IGVuYWJsZWQgKGVpdGhlciB2YWx1ZSBpcwovLy8gYG51bGxgKS4KLy8vCi8vLyBGaWxlcyBhcmUgZGVhbHQgb3V0IHJvdW5kLXJvYmluIChpbmRleCBtb2R1bG8gW3RvdGFsU2hhcmRzXSkgb3ZlciB0aGUKLy8vIGFscmVhZHkgc29ydGVkIFtwYXRoc10sIHdoaWNoIGtlZXBzIHNoYXJkcyBiYWxhbmNlZCBpbiBmaWxlIGNvdW50IGFuZCBtYWtlcwovLy8gdGhlIHBhcnRpdGlvbiBzdGFibGUgZm9yIGEgZ2l2ZW4gdGVzdCBzdWl0ZS4KTGlzdDxTdHJpbmc+IHNoYXJkT2YoCiAgTGlzdDxTdHJpbmc+IHBhdGhzLCB7CiAgcmVxdWlyZWQgaW50PyBzaGFyZEluZGV4LAogIHJlcXVpcmVkIGludD8gdG90YWxTaGFyZHMsCn0pIHsKICBpZiAoc2hhcmRJbmRleCA9PSBudWxsIHx8IHRvdGFsU2hhcmRzID09IG51bGwpIHJldHVybiBwYXRoczsKCiAgcmV0dXJuIFsKICAgIGZvciAodmFyIGkgPSBzaGFyZEluZGV4IC0gMTsgaSA8IHBhdGhzLmxlbmd0aDsgaSArPSB0b3RhbFNoYXJkcykgcGF0aHNbaV0sCiAgXTsKfQoKZXh0ZW5zaW9uIG9uIEZpbGVTeXN0ZW1FbnRpdHkgewogIGJvb2wgZ2V0IGlzVGVzdCB7CiAgICByZXR1cm4gdGhpcyBpcyBGaWxlICYmIHBhdGguYmFzZW5hbWUodGhpcy5wYXRoKS5lbmRzV2l0aCgnX3Rlc3QuZGFydCcpOwogIH0KfQoKRnV0dXJlPExpc3Q8U3RyaW5nPj4gZ2V0Tm90T3B0aW1pemVkVGVzdHMoCiAgSXRlcmFibGU8RmlsZVN5c3RlbUVudGl0eT4gdGVzdHMsCiAgU3RyaW5nIHRlc3REaXIsCikgYXN5bmMgewogIGZpbmFsIHBhdGhzID0gdGVzdHMubWFwKChlKSA9PiBlLnBhdGgpLnRvTGlzdCgpOwogIGZpbmFsIGZvcm1hdHRlZFBhdGhzID0gcGF0aHMubWFwKChlKSA9PiBlLnJlcGxhY2VBbGwoJy8uLycsICcvJykpLnRvTGlzdCgpOwoKICBmaW5hbCBmaWxlRnV0dXJlcyA9IGZvcm1hdHRlZFBhdGhzLm1hcChfY2hlY2tGaWxlRm9yU2tpcFZlcnlHb29kT3B0aW1pemF0aW9uKTsKICBmaW5hbCBmaWxlUmVzdWx0cyA9IGF3YWl0IEZ1dHVyZS53YWl0KGZpbGVGdXR1cmVzKTsKCiAgZmluYWwgdGVzdFdpdGhWZXJ5R29vZFRlc3QgPSA8U3RyaW5nPltdOwogIGZvciAodmFyIGkgPSAwOyBpIDwgZm9ybWF0dGVkUGF0aHMubGVuZ3RoOyBpKyspIHsKICAgIGlmIChmaWxlUmVzdWx0c1tpXSkgewogICAgICB0ZXN0V2l0aFZlcnlHb29kVGVzdC5hZGQoZm9ybWF0dGVkUGF0aHNbaV0pOwogICAgfQogIH0KCiAgLy8vIEZvcm1hdCB0byByZWxhdGl2ZSBwYXRoCiAgZmluYWwgcmVsYXRpdmVQYXRocyA9IHRlc3RXaXRoVmVyeUdvb2RUZXN0CiAgICAgIC5tYXAoKGUpID0+IHBhdGgucmVsYXRpdmUoZSwgZnJvbTogdGVzdERpcikpCiAgICAgIC50b0xpc3QoKTsKCiAgcmV0dXJuIHJlbGF0aXZlUGF0aHM7Cn0KCi8vLyBDaGVjayBpZiBhIHNpbmdsZSBmaWxlIGNvbnRhaW5zIHNraXBfdmVyeV9nb29kX29wdGltaXphdGlvbiB0YWcKRnV0dXJlPGJvb2w+IF9jaGVja0ZpbGVGb3JTa2lwVmVyeUdvb2RPcHRpbWl6YXRpb24oU3RyaW5nIHBhdGgpIGFzeW5jIHsKICBmaW5hbCBmaWxlID0gRmlsZShwYXRoKTsKICBpZiAoIWZpbGUuZXhpc3RzU3luYygpKSByZXR1cm4gZmFsc2U7CiAgZmluYWwgY29udGVudCA9IGF3YWl0IGZpbGUucmVhZEFzU3RyaW5nKCk7CiAgcmV0dXJuIHNraXBWZXJ5R29vZE9wdGltaXphdGlvblJlZ0V4cC5oYXNNYXRjaChjb250ZW50KTsKfQo=", "type": "text", }, { @@ -40,7 +40,7 @@ final testOptimizerBundle = MasonBundle.fromJson({ }, { "path": "test/pre_gen_test.dart", - "data": "aW1wb3J0ICdkYXJ0OmlvJzsKCmltcG9ydCAncGFja2FnZTpob29rcy9wcmVfZ2VuLmRhcnQnIGFzIHByZV9nZW47CmltcG9ydCAncGFja2FnZTptYXNvbi9tYXNvbi5kYXJ0JzsKaW1wb3J0ICdwYWNrYWdlOm1vY2t0YWlsL21vY2t0YWlsLmRhcnQnOwppbXBvcnQgJ3BhY2thZ2U6cGF0aC9wYXRoLmRhcnQnIGFzIHBhdGg7CmltcG9ydCAncGFja2FnZTp0ZXN0L3Rlc3QuZGFydCc7CgpjbGFzcyBfTW9ja0xvZ2dlciBleHRlbmRzIE1vY2sgaW1wbGVtZW50cyBMb2dnZXIge30KCmNsYXNzIF9GYWtlQ29udGV4dCBleHRlbmRzIEZha2UgaW1wbGVtZW50cyBIb29rQ29udGV4dCB7CiAgQG92ZXJyaWRlCiAgZmluYWwgbG9nZ2VyID0gX01vY2tMb2dnZXIoKTsKCiAgQG92ZXJyaWRlCiAgTWFwPFN0cmluZywgT2JqZWN0Pz4gdmFycyA9IHt9Owp9CgpmaW5hbCBub3RPcHRpbWl6ZWRUZXN0Q29udGVudCA9CiAgICAnJycKQFRhZ3MoWycke3ByZV9nZW4uc2tpcFZlcnlHb29kT3B0aW1pemF0aW9uVGFnfSddKQp2b2lkIG1haW4oKSB7CiAgdGVzdCgndGVzdCcsICgpIHsKICAgIGV4cGVjdCgxLCAxKTsKICB9KTsKfQonJyc7CgpmaW5hbCBhbm90aGVyTm90T3B0aW1pemVkVGVzdENvbnRlbnQgPQogICAgJycnCkBUYWdzKFsnJHtwcmVfZ2VuLnNraXBWZXJ5R29vZE9wdGltaXphdGlvblRhZ30nLCAnYW5vdGhlcl90YWcnXSkKdm9pZCBtYWluKCkgewogIHRlc3QoJ2Fub3RoZXIgdGVzdCcsICgpIHsKICAgIGV4cGVjdCgxLCAxKTsKICB9KTsKfQonJyc7Cgp2b2lkIG1haW4oKSB7CiAgbGF0ZSBEaXJlY3RvcnkgdGVtcERpcmVjdG9yeTsKCiAgc2V0VXAoKCkgewogICAgdGVtcERpcmVjdG9yeSA9IERpcmVjdG9yeS5zeXN0ZW1UZW1wLmNyZWF0ZVRlbXBTeW5jKCd0ZXN0X29wdGltaXplcicpOwogIH0pOwoKICB0ZWFyRG93bigoKSB7CiAgICB0ZW1wRGlyZWN0b3J5LmRlbGV0ZVN5bmMocmVjdXJzaXZlOiB0cnVlKTsKICB9KTsKCiAgZ3JvdXAoJ1ByZSBnZW4gaG9vaycsICgpIHsKICAgIGxhdGUgSG9va0NvbnRleHQgY29udGV4dDsKCiAgICBzZXRVcCgoKSB7CiAgICAgIGNvbnRleHQgPSBfRmFrZUNvbnRleHQoKTsKICAgIH0pOwoKICAgIGdyb3VwKCdDb21wbGV0ZXMnLCAoKSB7CiAgICAgIHRlc3QoJ3dpdGggdGVzdCBmaWxlcyBsaXN0JywgKCkgYXN5bmMgewogICAgICAgIEZpbGUocGF0aC5qb2luKHRlbXBEaXJlY3RvcnkucGF0aCwgJ3B1YnNwZWMueWFtbCcpKS5jcmVhdGVTeW5jKCk7CgogICAgICAgIGZpbmFsIHRlc3REaXIgPSBEaXJlY3RvcnkocGF0aC5qb2luKHRlbXBEaXJlY3RvcnkucGF0aCwgJ3Rlc3QnKSkKICAgICAgICAgIC4uY3JlYXRlU3luYygpOwogICAgICAgIEZpbGUocGF0aC5qb2luKHRlc3REaXIucGF0aCwgJ3Rlc3QxX3Rlc3QuZGFydCcpKS5jcmVhdGVTeW5jKCk7CiAgICAgICAgRmlsZShwYXRoLmpvaW4odGVzdERpci5wYXRoLCAndGVzdDJfdGVzdC5kYXJ0JykpLmNyZWF0ZVN5bmMoKTsKICAgICAgICBGaWxlKHBhdGguam9pbih0ZXN0RGlyLnBhdGgsICdub190ZXN0X2hlcmUuZGFydCcpKS5jcmVhdGVTeW5jKCk7CgogICAgICAgIGNvbnRleHQudmFyc1sncGFja2FnZS1yb290J10gPSB0ZW1wRGlyZWN0b3J5LmFic29sdXRlLnBhdGg7CgogICAgICAgIGF3YWl0IHByZV9nZW4ucnVuKGNvbnRleHQpOwoKICAgICAgICBmaW5hbCB0ZXN0cyA9IGNvbnRleHQudmFyc1sndGVzdHMnXSBhcyBMaXN0PE1hcDxTdHJpbmcsIFN0cmluZz4+OwogICAgICAgIGZpbmFsIHRlc3RzTWFwID0gPFN0cmluZywgU3RyaW5nPnt9OwogICAgICAgIGZvciAoZmluYWwgdGVzdCBpbiB0ZXN0cykgewogICAgICAgICAgZmluYWwgcGF0aCA9IHRlc3RbJ3BhdGgnXSE7CiAgICAgICAgICBmaW5hbCBpZGVudGlmaWVyID0gdGVzdFsnaWRlbnRpZmllciddITsKICAgICAgICAgIHRlc3RzTWFwW3BhdGhdID0gaWRlbnRpZmllcjsKICAgICAgICB9CgogICAgICAgIGZpbmFsIHBhdGhzID0gdGVzdHNNYXAua2V5czsKICAgICAgICBleHBlY3QocGF0aHMsIGNvbnRhaW5zKCd0ZXN0MV90ZXN0LmRhcnQnKSk7CiAgICAgICAgZXhwZWN0KHBhdGhzLCBjb250YWlucygndGVzdDJfdGVzdC5kYXJ0JykpOwogICAgICAgIGV4cGVjdChwYXRocywgaXNOb3QoY29udGFpbnMoJ25vX3Rlc3RfaGVyZS5kYXJ0JykpKTsKCiAgICAgICAgZXhwZWN0KAogICAgICAgICAgdGVzdHNNYXAudmFsdWVzLnRvU2V0KCkubGVuZ3RoLAogICAgICAgICAgZXF1YWxzKHRlc3RzLmxlbmd0aCksCiAgICAgICAgICByZWFzb246ICdBbGwgdGVzdHMgZmlsZXMgc2hvdWxkIGhhdmUgdW5pcXVlIGlkZW50aWZpZXJzJywKICAgICAgICApOwoKICAgICAgICBleHBlY3QoY29udGV4dC52YXJzWydpc0ZsdXR0ZXInXSwgZmFsc2UpOwogICAgICB9KTsKCiAgICAgIHRlc3QoJ3dpdGggcHJvcGVyIGlzRmx1dHRlciBpZGVudGlmaWNhdGlvbicsICgpIGFzeW5jIHsKICAgICAgICBGaWxlKHBhdGguam9pbih0ZW1wRGlyZWN0b3J5LnBhdGgsICdwdWJzcGVjLnlhbWwnKSkKICAgICAgICAgIC4uY3JlYXRlU3luYygpCiAgICAgICAgICAuLndyaXRlQXNTdHJpbmdTeW5jKCcnJwpkZXBlbmRlbmNpZXM6CiAgZmx1dHRlcjoKICAgIHNkazogZmx1dHRlcicnJyk7CgogICAgICAgIERpcmVjdG9yeShwYXRoLmpvaW4odGVtcERpcmVjdG9yeS5wYXRoLCAndGVzdCcpKS5jcmVhdGVTeW5jKCk7CgogICAgICAgIGNvbnRleHQudmFyc1sncGFja2FnZS1yb290J10gPSB0ZW1wRGlyZWN0b3J5LmFic29sdXRlLnBhdGg7CgogICAgICAgIGF3YWl0IHByZV9nZW4ucnVuKGNvbnRleHQpOwoKICAgICAgICBleHBlY3QoY29udGV4dC52YXJzWydpc0ZsdXR0ZXInXSwgdHJ1ZSk7CiAgICAgIH0pOwoKICAgICAgdGVzdCgnd2l0aCBwcm9wZXIgbm90IG9wdGltaXplZCB0ZXN0cyBpZGVudGlmaWNhdGlvbicsICgpIGFzeW5jIHsKICAgICAgICBGaWxlKHBhdGguam9pbih0ZW1wRGlyZWN0b3J5LnBhdGgsICdwdWJzcGVjLnlhbWwnKSkuY3JlYXRlU3luYygpOwoKICAgICAgICBmaW5hbCB0ZXN0RGlyID0gRGlyZWN0b3J5KHBhdGguam9pbih0ZW1wRGlyZWN0b3J5LnBhdGgsICd0ZXN0JykpCiAgICAgICAgICAuLmNyZWF0ZVN5bmMoKTsKICAgICAgICBGaWxlKHBhdGguam9pbih0ZXN0RGlyLnBhdGgsICd0ZXN0MV90ZXN0LmRhcnQnKSkuY3JlYXRlU3luYygpOwogICAgICAgIEZpbGUocGF0aC5qb2luKHRlc3REaXIucGF0aCwgJ3Rlc3QyX3Rlc3QuZGFydCcpKS5jcmVhdGVTeW5jKCk7CiAgICAgICAgRmlsZShwYXRoLmpvaW4odGVzdERpci5wYXRoLCAnbm9fdGVzdF9oZXJlLmRhcnQnKSkuY3JlYXRlU3luYygpOwogICAgICAgIEZpbGUoCiAgICAgICAgICBwYXRoLmpvaW4odGVzdERpci5wYXRoLCAnbm90X29wdGltaXplZF90ZXN0LmRhcnQnKSwKICAgICAgICApLndyaXRlQXNTdHJpbmdTeW5jKG5vdE9wdGltaXplZFRlc3RDb250ZW50KTsKICAgICAgICBGaWxlKAogICAgICAgICAgcGF0aC5qb2luKHRlc3REaXIucGF0aCwgJ2Fub3RoZXJfbm90X29wdGltaXplZF90ZXN0LmRhcnQnKSwKICAgICAgICApLndyaXRlQXNTdHJpbmdTeW5jKGFub3RoZXJOb3RPcHRpbWl6ZWRUZXN0Q29udGVudCk7CgogICAgICAgIGNvbnRleHQudmFyc1sncGFja2FnZS1yb290J10gPSB0ZW1wRGlyZWN0b3J5LmFic29sdXRlLnBhdGg7CgogICAgICAgIGF3YWl0IHByZV9nZW4ucnVuKGNvbnRleHQpOwoKICAgICAgICBmaW5hbCB0ZXN0cyA9IGNvbnRleHQudmFyc1sndGVzdHMnXSBhcyBMaXN0PE1hcDxTdHJpbmcsIFN0cmluZz4+OwogICAgICAgIGZpbmFsIHRlc3RzTWFwID0gPFN0cmluZywgU3RyaW5nPnt9OwogICAgICAgIGZvciAoZmluYWwgdGVzdCBpbiB0ZXN0cykgewogICAgICAgICAgZmluYWwgcGF0aCA9IHRlc3RbJ3BhdGgnXSE7CiAgICAgICAgICBmaW5hbCBpZGVudGlmaWVyID0gdGVzdFsnaWRlbnRpZmllciddITsKICAgICAgICAgIHRlc3RzTWFwW3BhdGhdID0gaWRlbnRpZmllcjsKICAgICAgICB9CgogICAgICAgIGZpbmFsIHBhdGhzID0gdGVzdHNNYXAua2V5czsKICAgICAgICBleHBlY3QocGF0aHMsIGNvbnRhaW5zKCd0ZXN0MV90ZXN0LmRhcnQnKSk7CiAgICAgICAgZXhwZWN0KHBhdGhzLCBjb250YWlucygndGVzdDJfdGVzdC5kYXJ0JykpOwogICAgICAgIGV4cGVjdChwYXRocywgaXNOb3QoY29udGFpbnMoJ25vX3Rlc3RfaGVyZS5kYXJ0JykpKTsKICAgICAgICBleHBlY3QocGF0aHMsIGlzTm90KGNvbnRhaW5zKCdub3Rfb3B0aW1pemVkX3Rlc3QuZGFydCcpKSk7CiAgICAgICAgZXhwZWN0KHBhdGhzLCBpc05vdChjb250YWlucygnYW5vdGhlcl9ub3Rfb3B0aW1pemVkX3Rlc3QuZGFydCcpKSk7CgogICAgICAgIGV4cGVjdCgKICAgICAgICAgIHRlc3RzTWFwLnZhbHVlcy50b1NldCgpLmxlbmd0aCwKICAgICAgICAgIGVxdWFscyh0ZXN0cy5sZW5ndGgpLAogICAgICAgICAgcmVhc29uOiAnQWxsIHRlc3RzIGZpbGVzIHNob3VsZCBoYXZlIHVuaXF1ZSBpZGVudGlmaWVycycsCiAgICAgICAgKTsKICAgICAgICBmaW5hbCBub3RPcHRpbWl6ZWRUZXN0cyA9CiAgICAgICAgICAgIGNvbnRleHQudmFyc1snbm90T3B0aW1pemVkVGVzdHMnXSBhcyBMaXN0PFN0cmluZz47CiAgICAgICAgZXhwZWN0KAogICAgICAgICAgbm90T3B0aW1pemVkVGVzdHMsCiAgICAgICAgICBjb250YWlucygnbm90X29wdGltaXplZF90ZXN0LmRhcnQnKSwKICAgICAgICApOwogICAgICAgIGV4cGVjdCgKICAgICAgICAgIG5vdE9wdGltaXplZFRlc3RzLAogICAgICAgICAgY29udGFpbnMoJ2Fub3RoZXJfbm90X29wdGltaXplZF90ZXN0LmRhcnQnKSwKICAgICAgICApOwogICAgICB9KTsKICAgIH0pOwoKICAgIGdyb3VwKCdGYWlscycsICgpIHsKICAgICAgc2V0VXAoKCkgewogICAgICAgIHByZV9nZW4uZXhpdEZuID0gKGNvZGUpIHsKICAgICAgICAgIHRocm93IFByb2Nlc3NFeGNlcHRpb24oJ2V4aXQnLCBbY29kZS50b1N0cmluZygpXSk7CiAgICAgICAgfTsKICAgICAgfSk7CgogICAgICB0ZWFyRG93bigoKSB7CiAgICAgICAgcHJlX2dlbi5leGl0Rm4gPSBleGl0OwogICAgICB9KTsKCiAgICAgIHRlc3QoJ3doZW4gdGFyZ2V0IHRlc3QgZGlyIGRvZXMgbm90IGV4aXN0JywgKCkgYXN5bmMgewogICAgICAgIEZpbGUocGF0aC5qb2luKHRlbXBEaXJlY3RvcnkucGF0aCwgJ3B1YnNwZWMueWFtbCcpKS5jcmVhdGVTeW5jKCk7CgogICAgICAgIGZpbmFsIHRlc3REaXIgPSBEaXJlY3RvcnkocGF0aC5qb2luKHRlbXBEaXJlY3RvcnkucGF0aCwgJ3Rlc3QnKSk7CgogICAgICAgIGNvbnRleHQudmFyc1sncGFja2FnZS1yb290J10gPSB0ZW1wRGlyZWN0b3J5LmFic29sdXRlLnBhdGg7CgogICAgICAgIGF3YWl0IGV4cGVjdExhdGVyKAogICAgICAgICAgKCkgPT4gcHJlX2dlbi5ydW4oY29udGV4dCksCiAgICAgICAgICB0aHJvd3NBKAogICAgICAgICAgICBpc0E8UHJvY2Vzc0V4Y2VwdGlvbj4oKS5oYXZpbmcoCiAgICAgICAgICAgICAgKGV4KSA9PiBleC5hcmd1bWVudHMuZmlyc3QsCiAgICAgICAgICAgICAgJ2Vycm9yIGNvZGUnLAogICAgICAgICAgICAgIGVxdWFscygnMScpLAogICAgICAgICAgICApLAogICAgICAgICAgKSwKICAgICAgICApOwoKICAgICAgICB2ZXJpZnkoCiAgICAgICAgICAoKSA9PiBjb250ZXh0LmxvZ2dlci5lcnIoJ0NvdWxkIG5vdCBmaW5kIGRpcmVjdG9yeSAke3Rlc3REaXIucGF0aH0nKSwKICAgICAgICApLmNhbGxlZCgxKTsKCiAgICAgICAgZXhwZWN0KGNvbnRleHQudmFyc1sndGVzdHMnXSwgaXNOdWxsKTsKICAgICAgICBleHBlY3QoY29udGV4dC52YXJzWydpc0ZsdXR0ZXInXSwgaXNOdWxsKTsKICAgICAgfSk7CgogICAgICB0ZXN0KCd3aGVuIHRhcmdldCBkaXIgZG9lcyBub3QgY29udGFpbiBhIHB1YnNwZWMueWFtbCcsICgpIGFzeW5jIHsKICAgICAgICBmaW5hbCB0ZXN0RGlyID0gRGlyZWN0b3J5KHBhdGguam9pbih0ZW1wRGlyZWN0b3J5LnBhdGgsICd0ZXN0JykpCiAgICAgICAgICAuLmNyZWF0ZVN5bmMoKTsKICAgICAgICBGaWxlKHBhdGguam9pbih0ZXN0RGlyLnBhdGgsICd0ZXN0MV90ZXN0LmRhcnQnKSkuY3JlYXRlU3luYygpOwogICAgICAgIEZpbGUocGF0aC5qb2luKHRlc3REaXIucGF0aCwgJ3Rlc3QyX3Rlc3QuZGFydCcpKS5jcmVhdGVTeW5jKCk7CiAgICAgICAgRmlsZShwYXRoLmpvaW4odGVzdERpci5wYXRoLCAnbm9fdGVzdF9oZXJlLmRhcnQnKSkuY3JlYXRlU3luYygpOwoKICAgICAgICBjb250ZXh0LnZhcnNbJ3BhY2thZ2Utcm9vdCddID0gdGVtcERpcmVjdG9yeS5hYnNvbHV0ZS5wYXRoOwoKICAgICAgICBhd2FpdCBleHBlY3RMYXRlcigKICAgICAgICAgICgpID0+IHByZV9nZW4ucnVuKGNvbnRleHQpLAogICAgICAgICAgdGhyb3dzQSgKICAgICAgICAgICAgaXNBPFByb2Nlc3NFeGNlcHRpb24+KCkuaGF2aW5nKAogICAgICAgICAgICAgIChleCkgPT4gZXguYXJndW1lbnRzLmZpcnN0LAogICAgICAgICAgICAgICdlcnJvciBjb2RlJywKICAgICAgICAgICAgICBlcXVhbHMoJzEnKSwKICAgICAgICAgICAgKSwKICAgICAgICAgICksCiAgICAgICAgKTsKCiAgICAgICAgdmVyaWZ5KAogICAgICAgICAgKCkgPT4gY29udGV4dC5sb2dnZXIuZXJyKAogICAgICAgICAgICAnQ291bGQgbm90IGZpbmQgcHVic3BlYy55YW1sIGF0ICR7dGVzdERpci5wYXRofScsCiAgICAgICAgICApLAogICAgICAgICkuY2FsbGVkKDEpOwoKICAgICAgICBleHBlY3QoY29udGV4dC52YXJzWyd0ZXN0cyddLCBpc051bGwpOwogICAgICAgIGV4cGVjdChjb250ZXh0LnZhcnNbJ2lzRmx1dHRlciddLCBpc051bGwpOwogICAgICB9KTsKICAgIH0pOwoKICAgIGdyb3VwKCdza2lwVmVyeUdvb2RPcHRpbWl6YXRpb25SZWdFeHAgcmVnZXgnLCAoKSB7CiAgICAgIGZpbmFsIHJlZ2V4ID0gcHJlX2dlbi5za2lwVmVyeUdvb2RPcHRpbWl6YXRpb25SZWdFeHA7CiAgICAgIHRlc3QoJ21hdGNoZXMgc2luZ2xlLWxpbmUgdGFnJywgKCkgewogICAgICAgIGZpbmFsIGNvbnRlbnQgPSAiQFRhZ3MoWycke3ByZV9nZW4uc2tpcFZlcnlHb29kT3B0aW1pemF0aW9uVGFnfSddKSI7CiAgICAgICAgZXhwZWN0KHJlZ2V4Lmhhc01hdGNoKGNvbnRlbnQpLCBpc1RydWUpOwogICAgICB9KTsKCiAgICAgIHRlc3QoJ21hdGNoZXMgc2luZ2xlLWxpbmUgd2l0aCBtdWx0aXBsZSB0YWdzJywgKCkgewogICAgICAgIGZpbmFsIGNvbnRlbnQgPQogICAgICAgICAgICAiQFRhZ3MoWycke3ByZV9nZW4uc2tpcFZlcnlHb29kT3B0aW1pemF0aW9uVGFnfScsICdjaHJvbWUnXSkiOwogICAgICAgIGV4cGVjdChyZWdleC5oYXNNYXRjaChjb250ZW50KSwgaXNUcnVlKTsKICAgICAgfSk7CgogICAgICB0ZXN0KCdtYXRjaGVzIG11bHRpLWxpbmUgdGFnIGxpc3QnLCAoKSB7CiAgICAgICAgZmluYWwgY29udGVudCA9CiAgICAgICAgICAgICcnJwogICAgICBAVGFncyhbCiAgICAgICAgJyR7cHJlX2dlbi5za2lwVmVyeUdvb2RPcHRpbWl6YXRpb25UYWd9JywKICAgICAgICAnY2hyb21lJywKICAgICAgICAndGVzdCcsCiAgICAgIF0pCiAgICAgICcnJzsKICAgICAgICBleHBlY3QocmVnZXguaGFzTWF0Y2goY29udGVudCksIGlzVHJ1ZSk7CiAgICAgIH0pOwoKICAgICAgdGVzdCgnbWF0Y2hlcyBtdWx0aS1saW5lIHdoZXJlIHRhZyBpcyBub3QgdGhlIGZpcnN0JywgKCkgewogICAgICAgIGZpbmFsIGNvbnRlbnQgPQogICAgICAgICAgICAnJycKICAgICAgQFRhZ3MoWwogICAgICAgICdjaHJvbWUnLAogICAgICAgICcke3ByZV9nZW4uc2tpcFZlcnlHb29kT3B0aW1pemF0aW9uVGFnfScsCiAgICAgICAgJ3Rlc3QnLAogICAgICBdKQogICAgICAnJyc7CiAgICAgICAgZXhwZWN0KHJlZ2V4Lmhhc01hdGNoKGNvbnRlbnQpLCBpc1RydWUpOwogICAgICB9KTsKCiAgICAgIHRlc3QoJ2RvZXMgbm90IG1hdGNoIHdoZW4gdGFnIG1pc3NpbmcnLCAoKSB7CiAgICAgICAgY29uc3QgY29udGVudCA9ICJAVGFncyhbJ2Nocm9tZScsICd0ZXN0J10pIjsKICAgICAgICBleHBlY3QocmVnZXguaGFzTWF0Y2goY29udGVudCksIGlzRmFsc2UpOwogICAgICB9KTsKCiAgICAgIHRlc3QoCiAgICAgICAgJ2RvZXMgbm90IG1hdGNoIHN1YnN0cmluZyBvbmx5IChlLmcuIHNraXBfdmVyeV9nb29kX29wdGltaXphdGlvbix0ZXN0KScsCiAgICAgICAgKCkgewogICAgICAgICAgZmluYWwgY29udGVudCA9CiAgICAgICAgICAgICAgJycnCiAgICAgIEBUYWdzKFsKICAgICAgICAnJHtwcmVfZ2VuLnNraXBWZXJ5R29vZE9wdGltaXphdGlvblRhZ30sdGVzdCcsCiAgICAgICAgJ2Nocm9tZScsCiAgICAgIF0pCiAgICAgICcnJzsKICAgICAgICAgIGV4cGVjdCgKICAgICAgICAgICAgcmVnZXguaGFzTWF0Y2goY29udGVudCksCiAgICAgICAgICAgIGlzRmFsc2UsCiAgICAgICAgICApOyAvLyBvbmx5IGV4YWN0IHRhZyBzaG91bGQgbWF0Y2gKICAgICAgICB9LAogICAgICApOwogICAgfSk7CiAgfSk7Cn0K", + "data": "aW1wb3J0ICdkYXJ0OmlvJzsKaW1wb3J0ICdkYXJ0Om1hdGgnOwoKaW1wb3J0ICdwYWNrYWdlOmhvb2tzL3ByZV9nZW4uZGFydCcgYXMgcHJlX2dlbjsKaW1wb3J0ICdwYWNrYWdlOm1hc29uL21hc29uLmRhcnQnOwppbXBvcnQgJ3BhY2thZ2U6bW9ja3RhaWwvbW9ja3RhaWwuZGFydCc7CmltcG9ydCAncGFja2FnZTpwYXRoL3BhdGguZGFydCcgYXMgcGF0aDsKaW1wb3J0ICdwYWNrYWdlOnRlc3QvdGVzdC5kYXJ0JzsKCmNsYXNzIF9Nb2NrTG9nZ2VyIGV4dGVuZHMgTW9jayBpbXBsZW1lbnRzIExvZ2dlciB7fQoKY2xhc3MgX0Zha2VDb250ZXh0IGV4dGVuZHMgRmFrZSBpbXBsZW1lbnRzIEhvb2tDb250ZXh0IHsKICBAb3ZlcnJpZGUKICBmaW5hbCBsb2dnZXIgPSBfTW9ja0xvZ2dlcigpOwoKICBAb3ZlcnJpZGUKICBNYXA8U3RyaW5nLCBPYmplY3Q/PiB2YXJzID0ge307Cn0KCmZpbmFsIG5vdE9wdGltaXplZFRlc3RDb250ZW50ID0KICAgICcnJwpAVGFncyhbJyR7cHJlX2dlbi5za2lwVmVyeUdvb2RPcHRpbWl6YXRpb25UYWd9J10pCnZvaWQgbWFpbigpIHsKICB0ZXN0KCd0ZXN0JywgKCkgewogICAgZXhwZWN0KDEsIDEpOwogIH0pOwp9CicnJzsKCmZpbmFsIGFub3RoZXJOb3RPcHRpbWl6ZWRUZXN0Q29udGVudCA9CiAgICAnJycKQFRhZ3MoWycke3ByZV9nZW4uc2tpcFZlcnlHb29kT3B0aW1pemF0aW9uVGFnfScsICdhbm90aGVyX3RhZyddKQp2b2lkIG1haW4oKSB7CiAgdGVzdCgnYW5vdGhlciB0ZXN0JywgKCkgewogICAgZXhwZWN0KDEsIDEpOwogIH0pOwp9CicnJzsKCnZvaWQgbWFpbigpIHsKICBsYXRlIERpcmVjdG9yeSB0ZW1wRGlyZWN0b3J5OwoKICBzZXRVcCgoKSB7CiAgICB0ZW1wRGlyZWN0b3J5ID0gRGlyZWN0b3J5LnN5c3RlbVRlbXAuY3JlYXRlVGVtcFN5bmMoJ3Rlc3Rfb3B0aW1pemVyJyk7CiAgfSk7CgogIHRlYXJEb3duKCgpIHsKICAgIHRlbXBEaXJlY3RvcnkuZGVsZXRlU3luYyhyZWN1cnNpdmU6IHRydWUpOwogIH0pOwoKICBncm91cCgnUHJlIGdlbiBob29rJywgKCkgewogICAgbGF0ZSBIb29rQ29udGV4dCBjb250ZXh0OwoKICAgIHNldFVwKCgpIHsKICAgICAgY29udGV4dCA9IF9GYWtlQ29udGV4dCgpOwogICAgfSk7CgogICAgZ3JvdXAoJ0NvbXBsZXRlcycsICgpIHsKICAgICAgdGVzdCgnd2l0aCB0ZXN0IGZpbGVzIGxpc3QnLCAoKSBhc3luYyB7CiAgICAgICAgRmlsZShwYXRoLmpvaW4odGVtcERpcmVjdG9yeS5wYXRoLCAncHVic3BlYy55YW1sJykpLmNyZWF0ZVN5bmMoKTsKCiAgICAgICAgZmluYWwgdGVzdERpciA9IERpcmVjdG9yeShwYXRoLmpvaW4odGVtcERpcmVjdG9yeS5wYXRoLCAndGVzdCcpKQogICAgICAgICAgLi5jcmVhdGVTeW5jKCk7CiAgICAgICAgRmlsZShwYXRoLmpvaW4odGVzdERpci5wYXRoLCAndGVzdDFfdGVzdC5kYXJ0JykpLmNyZWF0ZVN5bmMoKTsKICAgICAgICBGaWxlKHBhdGguam9pbih0ZXN0RGlyLnBhdGgsICd0ZXN0Ml90ZXN0LmRhcnQnKSkuY3JlYXRlU3luYygpOwogICAgICAgIEZpbGUocGF0aC5qb2luKHRlc3REaXIucGF0aCwgJ25vX3Rlc3RfaGVyZS5kYXJ0JykpLmNyZWF0ZVN5bmMoKTsKCiAgICAgICAgY29udGV4dC52YXJzWydwYWNrYWdlLXJvb3QnXSA9IHRlbXBEaXJlY3RvcnkuYWJzb2x1dGUucGF0aDsKCiAgICAgICAgYXdhaXQgcHJlX2dlbi5ydW4oY29udGV4dCk7CgogICAgICAgIGZpbmFsIHRlc3RzID0gY29udGV4dC52YXJzWyd0ZXN0cyddIGFzIExpc3Q8TWFwPFN0cmluZywgU3RyaW5nPj47CiAgICAgICAgZmluYWwgdGVzdHNNYXAgPSA8U3RyaW5nLCBTdHJpbmc+e307CiAgICAgICAgZm9yIChmaW5hbCB0ZXN0IGluIHRlc3RzKSB7CiAgICAgICAgICBmaW5hbCBwYXRoID0gdGVzdFsncGF0aCddITsKICAgICAgICAgIGZpbmFsIGlkZW50aWZpZXIgPSB0ZXN0WydpZGVudGlmaWVyJ10hOwogICAgICAgICAgdGVzdHNNYXBbcGF0aF0gPSBpZGVudGlmaWVyOwogICAgICAgIH0KCiAgICAgICAgZmluYWwgcGF0aHMgPSB0ZXN0c01hcC5rZXlzOwogICAgICAgIGV4cGVjdChwYXRocywgY29udGFpbnMoJ3Rlc3QxX3Rlc3QuZGFydCcpKTsKICAgICAgICBleHBlY3QocGF0aHMsIGNvbnRhaW5zKCd0ZXN0Ml90ZXN0LmRhcnQnKSk7CiAgICAgICAgZXhwZWN0KHBhdGhzLCBpc05vdChjb250YWlucygnbm9fdGVzdF9oZXJlLmRhcnQnKSkpOwoKICAgICAgICBleHBlY3QoCiAgICAgICAgICB0ZXN0c01hcC52YWx1ZXMudG9TZXQoKS5sZW5ndGgsCiAgICAgICAgICBlcXVhbHModGVzdHMubGVuZ3RoKSwKICAgICAgICAgIHJlYXNvbjogJ0FsbCB0ZXN0cyBmaWxlcyBzaG91bGQgaGF2ZSB1bmlxdWUgaWRlbnRpZmllcnMnLAogICAgICAgICk7CgogICAgICAgIGV4cGVjdChjb250ZXh0LnZhcnNbJ2lzRmx1dHRlciddLCBmYWxzZSk7CiAgICAgIH0pOwoKICAgICAgdGVzdCgnd2l0aCBwcm9wZXIgaXNGbHV0dGVyIGlkZW50aWZpY2F0aW9uJywgKCkgYXN5bmMgewogICAgICAgIEZpbGUocGF0aC5qb2luKHRlbXBEaXJlY3RvcnkucGF0aCwgJ3B1YnNwZWMueWFtbCcpKQogICAgICAgICAgLi5jcmVhdGVTeW5jKCkKICAgICAgICAgIC4ud3JpdGVBc1N0cmluZ1N5bmMoJycnCmRlcGVuZGVuY2llczoKICBmbHV0dGVyOgogICAgc2RrOiBmbHV0dGVyJycnKTsKCiAgICAgICAgRGlyZWN0b3J5KHBhdGguam9pbih0ZW1wRGlyZWN0b3J5LnBhdGgsICd0ZXN0JykpLmNyZWF0ZVN5bmMoKTsKCiAgICAgICAgY29udGV4dC52YXJzWydwYWNrYWdlLXJvb3QnXSA9IHRlbXBEaXJlY3RvcnkuYWJzb2x1dGUucGF0aDsKCiAgICAgICAgYXdhaXQgcHJlX2dlbi5ydW4oY29udGV4dCk7CgogICAgICAgIGV4cGVjdChjb250ZXh0LnZhcnNbJ2lzRmx1dHRlciddLCB0cnVlKTsKICAgICAgfSk7CgogICAgICB0ZXN0KCd3aXRoIHByb3BlciBub3Qgb3B0aW1pemVkIHRlc3RzIGlkZW50aWZpY2F0aW9uJywgKCkgYXN5bmMgewogICAgICAgIEZpbGUocGF0aC5qb2luKHRlbXBEaXJlY3RvcnkucGF0aCwgJ3B1YnNwZWMueWFtbCcpKS5jcmVhdGVTeW5jKCk7CgogICAgICAgIGZpbmFsIHRlc3REaXIgPSBEaXJlY3RvcnkocGF0aC5qb2luKHRlbXBEaXJlY3RvcnkucGF0aCwgJ3Rlc3QnKSkKICAgICAgICAgIC4uY3JlYXRlU3luYygpOwogICAgICAgIEZpbGUocGF0aC5qb2luKHRlc3REaXIucGF0aCwgJ3Rlc3QxX3Rlc3QuZGFydCcpKS5jcmVhdGVTeW5jKCk7CiAgICAgICAgRmlsZShwYXRoLmpvaW4odGVzdERpci5wYXRoLCAndGVzdDJfdGVzdC5kYXJ0JykpLmNyZWF0ZVN5bmMoKTsKICAgICAgICBGaWxlKHBhdGguam9pbih0ZXN0RGlyLnBhdGgsICdub190ZXN0X2hlcmUuZGFydCcpKS5jcmVhdGVTeW5jKCk7CiAgICAgICAgRmlsZSgKICAgICAgICAgIHBhdGguam9pbih0ZXN0RGlyLnBhdGgsICdub3Rfb3B0aW1pemVkX3Rlc3QuZGFydCcpLAogICAgICAgICkud3JpdGVBc1N0cmluZ1N5bmMobm90T3B0aW1pemVkVGVzdENvbnRlbnQpOwogICAgICAgIEZpbGUoCiAgICAgICAgICBwYXRoLmpvaW4odGVzdERpci5wYXRoLCAnYW5vdGhlcl9ub3Rfb3B0aW1pemVkX3Rlc3QuZGFydCcpLAogICAgICAgICkud3JpdGVBc1N0cmluZ1N5bmMoYW5vdGhlck5vdE9wdGltaXplZFRlc3RDb250ZW50KTsKCiAgICAgICAgY29udGV4dC52YXJzWydwYWNrYWdlLXJvb3QnXSA9IHRlbXBEaXJlY3RvcnkuYWJzb2x1dGUucGF0aDsKCiAgICAgICAgYXdhaXQgcHJlX2dlbi5ydW4oY29udGV4dCk7CgogICAgICAgIGZpbmFsIHRlc3RzID0gY29udGV4dC52YXJzWyd0ZXN0cyddIGFzIExpc3Q8TWFwPFN0cmluZywgU3RyaW5nPj47CiAgICAgICAgZmluYWwgdGVzdHNNYXAgPSA8U3RyaW5nLCBTdHJpbmc+e307CiAgICAgICAgZm9yIChmaW5hbCB0ZXN0IGluIHRlc3RzKSB7CiAgICAgICAgICBmaW5hbCBwYXRoID0gdGVzdFsncGF0aCddITsKICAgICAgICAgIGZpbmFsIGlkZW50aWZpZXIgPSB0ZXN0WydpZGVudGlmaWVyJ10hOwogICAgICAgICAgdGVzdHNNYXBbcGF0aF0gPSBpZGVudGlmaWVyOwogICAgICAgIH0KCiAgICAgICAgZmluYWwgcGF0aHMgPSB0ZXN0c01hcC5rZXlzOwogICAgICAgIGV4cGVjdChwYXRocywgY29udGFpbnMoJ3Rlc3QxX3Rlc3QuZGFydCcpKTsKICAgICAgICBleHBlY3QocGF0aHMsIGNvbnRhaW5zKCd0ZXN0Ml90ZXN0LmRhcnQnKSk7CiAgICAgICAgZXhwZWN0KHBhdGhzLCBpc05vdChjb250YWlucygnbm9fdGVzdF9oZXJlLmRhcnQnKSkpOwogICAgICAgIGV4cGVjdChwYXRocywgaXNOb3QoY29udGFpbnMoJ25vdF9vcHRpbWl6ZWRfdGVzdC5kYXJ0JykpKTsKICAgICAgICBleHBlY3QocGF0aHMsIGlzTm90KGNvbnRhaW5zKCdhbm90aGVyX25vdF9vcHRpbWl6ZWRfdGVzdC5kYXJ0JykpKTsKCiAgICAgICAgZXhwZWN0KAogICAgICAgICAgdGVzdHNNYXAudmFsdWVzLnRvU2V0KCkubGVuZ3RoLAogICAgICAgICAgZXF1YWxzKHRlc3RzLmxlbmd0aCksCiAgICAgICAgICByZWFzb246ICdBbGwgdGVzdHMgZmlsZXMgc2hvdWxkIGhhdmUgdW5pcXVlIGlkZW50aWZpZXJzJywKICAgICAgICApOwogICAgICAgIGZpbmFsIG5vdE9wdGltaXplZFRlc3RzID0KICAgICAgICAgICAgY29udGV4dC52YXJzWydub3RPcHRpbWl6ZWRUZXN0cyddIGFzIExpc3Q8U3RyaW5nPjsKICAgICAgICBleHBlY3QoCiAgICAgICAgICBub3RPcHRpbWl6ZWRUZXN0cywKICAgICAgICAgIGNvbnRhaW5zKCdub3Rfb3B0aW1pemVkX3Rlc3QuZGFydCcpLAogICAgICAgICk7CiAgICAgICAgZXhwZWN0KAogICAgICAgICAgbm90T3B0aW1pemVkVGVzdHMsCiAgICAgICAgICBjb250YWlucygnYW5vdGhlcl9ub3Rfb3B0aW1pemVkX3Rlc3QuZGFydCcpLAogICAgICAgICk7CiAgICAgIH0pOwogICAgfSk7CgogICAgZ3JvdXAoJ0ZhaWxzJywgKCkgewogICAgICBzZXRVcCgoKSB7CiAgICAgICAgcHJlX2dlbi5leGl0Rm4gPSAoY29kZSkgewogICAgICAgICAgdGhyb3cgUHJvY2Vzc0V4Y2VwdGlvbignZXhpdCcsIFtjb2RlLnRvU3RyaW5nKCldKTsKICAgICAgICB9OwogICAgICB9KTsKCiAgICAgIHRlYXJEb3duKCgpIHsKICAgICAgICBwcmVfZ2VuLmV4aXRGbiA9IGV4aXQ7CiAgICAgIH0pOwoKICAgICAgdGVzdCgnd2hlbiB0YXJnZXQgdGVzdCBkaXIgZG9lcyBub3QgZXhpc3QnLCAoKSBhc3luYyB7CiAgICAgICAgRmlsZShwYXRoLmpvaW4odGVtcERpcmVjdG9yeS5wYXRoLCAncHVic3BlYy55YW1sJykpLmNyZWF0ZVN5bmMoKTsKCiAgICAgICAgZmluYWwgdGVzdERpciA9IERpcmVjdG9yeShwYXRoLmpvaW4odGVtcERpcmVjdG9yeS5wYXRoLCAndGVzdCcpKTsKCiAgICAgICAgY29udGV4dC52YXJzWydwYWNrYWdlLXJvb3QnXSA9IHRlbXBEaXJlY3RvcnkuYWJzb2x1dGUucGF0aDsKCiAgICAgICAgYXdhaXQgZXhwZWN0TGF0ZXIoCiAgICAgICAgICAoKSA9PiBwcmVfZ2VuLnJ1bihjb250ZXh0KSwKICAgICAgICAgIHRocm93c0EoCiAgICAgICAgICAgIGlzQTxQcm9jZXNzRXhjZXB0aW9uPigpLmhhdmluZygKICAgICAgICAgICAgICAoZXgpID0+IGV4LmFyZ3VtZW50cy5maXJzdCwKICAgICAgICAgICAgICAnZXJyb3IgY29kZScsCiAgICAgICAgICAgICAgZXF1YWxzKCcxJyksCiAgICAgICAgICAgICksCiAgICAgICAgICApLAogICAgICAgICk7CgogICAgICAgIHZlcmlmeSgKICAgICAgICAgICgpID0+IGNvbnRleHQubG9nZ2VyLmVycignQ291bGQgbm90IGZpbmQgZGlyZWN0b3J5ICR7dGVzdERpci5wYXRofScpLAogICAgICAgICkuY2FsbGVkKDEpOwoKICAgICAgICBleHBlY3QoY29udGV4dC52YXJzWyd0ZXN0cyddLCBpc051bGwpOwogICAgICAgIGV4cGVjdChjb250ZXh0LnZhcnNbJ2lzRmx1dHRlciddLCBpc051bGwpOwogICAgICB9KTsKCiAgICAgIHRlc3QoJ3doZW4gdGFyZ2V0IGRpciBkb2VzIG5vdCBjb250YWluIGEgcHVic3BlYy55YW1sJywgKCkgYXN5bmMgewogICAgICAgIGZpbmFsIHRlc3REaXIgPSBEaXJlY3RvcnkocGF0aC5qb2luKHRlbXBEaXJlY3RvcnkucGF0aCwgJ3Rlc3QnKSkKICAgICAgICAgIC4uY3JlYXRlU3luYygpOwogICAgICAgIEZpbGUocGF0aC5qb2luKHRlc3REaXIucGF0aCwgJ3Rlc3QxX3Rlc3QuZGFydCcpKS5jcmVhdGVTeW5jKCk7CiAgICAgICAgRmlsZShwYXRoLmpvaW4odGVzdERpci5wYXRoLCAndGVzdDJfdGVzdC5kYXJ0JykpLmNyZWF0ZVN5bmMoKTsKICAgICAgICBGaWxlKHBhdGguam9pbih0ZXN0RGlyLnBhdGgsICdub190ZXN0X2hlcmUuZGFydCcpKS5jcmVhdGVTeW5jKCk7CgogICAgICAgIGNvbnRleHQudmFyc1sncGFja2FnZS1yb290J10gPSB0ZW1wRGlyZWN0b3J5LmFic29sdXRlLnBhdGg7CgogICAgICAgIGF3YWl0IGV4cGVjdExhdGVyKAogICAgICAgICAgKCkgPT4gcHJlX2dlbi5ydW4oY29udGV4dCksCiAgICAgICAgICB0aHJvd3NBKAogICAgICAgICAgICBpc0E8UHJvY2Vzc0V4Y2VwdGlvbj4oKS5oYXZpbmcoCiAgICAgICAgICAgICAgKGV4KSA9PiBleC5hcmd1bWVudHMuZmlyc3QsCiAgICAgICAgICAgICAgJ2Vycm9yIGNvZGUnLAogICAgICAgICAgICAgIGVxdWFscygnMScpLAogICAgICAgICAgICApLAogICAgICAgICAgKSwKICAgICAgICApOwoKICAgICAgICB2ZXJpZnkoCiAgICAgICAgICAoKSA9PiBjb250ZXh0LmxvZ2dlci5lcnIoCiAgICAgICAgICAgICdDb3VsZCBub3QgZmluZCBwdWJzcGVjLnlhbWwgYXQgJHt0ZXN0RGlyLnBhdGh9JywKICAgICAgICAgICksCiAgICAgICAgKS5jYWxsZWQoMSk7CgogICAgICAgIGV4cGVjdChjb250ZXh0LnZhcnNbJ3Rlc3RzJ10sIGlzTnVsbCk7CiAgICAgICAgZXhwZWN0KGNvbnRleHQudmFyc1snaXNGbHV0dGVyJ10sIGlzTnVsbCk7CiAgICAgIH0pOwogICAgfSk7CgogICAgZ3JvdXAoJ3NraXBWZXJ5R29vZE9wdGltaXphdGlvblJlZ0V4cCByZWdleCcsICgpIHsKICAgICAgZmluYWwgcmVnZXggPSBwcmVfZ2VuLnNraXBWZXJ5R29vZE9wdGltaXphdGlvblJlZ0V4cDsKICAgICAgdGVzdCgnbWF0Y2hlcyBzaW5nbGUtbGluZSB0YWcnLCAoKSB7CiAgICAgICAgZmluYWwgY29udGVudCA9ICJAVGFncyhbJyR7cHJlX2dlbi5za2lwVmVyeUdvb2RPcHRpbWl6YXRpb25UYWd9J10pIjsKICAgICAgICBleHBlY3QocmVnZXguaGFzTWF0Y2goY29udGVudCksIGlzVHJ1ZSk7CiAgICAgIH0pOwoKICAgICAgdGVzdCgnbWF0Y2hlcyBzaW5nbGUtbGluZSB3aXRoIG11bHRpcGxlIHRhZ3MnLCAoKSB7CiAgICAgICAgZmluYWwgY29udGVudCA9CiAgICAgICAgICAgICJAVGFncyhbJyR7cHJlX2dlbi5za2lwVmVyeUdvb2RPcHRpbWl6YXRpb25UYWd9JywgJ2Nocm9tZSddKSI7CiAgICAgICAgZXhwZWN0KHJlZ2V4Lmhhc01hdGNoKGNvbnRlbnQpLCBpc1RydWUpOwogICAgICB9KTsKCiAgICAgIHRlc3QoJ21hdGNoZXMgbXVsdGktbGluZSB0YWcgbGlzdCcsICgpIHsKICAgICAgICBmaW5hbCBjb250ZW50ID0KICAgICAgICAgICAgJycnCiAgICAgIEBUYWdzKFsKICAgICAgICAnJHtwcmVfZ2VuLnNraXBWZXJ5R29vZE9wdGltaXphdGlvblRhZ30nLAogICAgICAgICdjaHJvbWUnLAogICAgICAgICd0ZXN0JywKICAgICAgXSkKICAgICAgJycnOwogICAgICAgIGV4cGVjdChyZWdleC5oYXNNYXRjaChjb250ZW50KSwgaXNUcnVlKTsKICAgICAgfSk7CgogICAgICB0ZXN0KCdtYXRjaGVzIG11bHRpLWxpbmUgd2hlcmUgdGFnIGlzIG5vdCB0aGUgZmlyc3QnLCAoKSB7CiAgICAgICAgZmluYWwgY29udGVudCA9CiAgICAgICAgICAgICcnJwogICAgICBAVGFncyhbCiAgICAgICAgJ2Nocm9tZScsCiAgICAgICAgJyR7cHJlX2dlbi5za2lwVmVyeUdvb2RPcHRpbWl6YXRpb25UYWd9JywKICAgICAgICAndGVzdCcsCiAgICAgIF0pCiAgICAgICcnJzsKICAgICAgICBleHBlY3QocmVnZXguaGFzTWF0Y2goY29udGVudCksIGlzVHJ1ZSk7CiAgICAgIH0pOwoKICAgICAgdGVzdCgnZG9lcyBub3QgbWF0Y2ggd2hlbiB0YWcgbWlzc2luZycsICgpIHsKICAgICAgICBjb25zdCBjb250ZW50ID0gIkBUYWdzKFsnY2hyb21lJywgJ3Rlc3QnXSkiOwogICAgICAgIGV4cGVjdChyZWdleC5oYXNNYXRjaChjb250ZW50KSwgaXNGYWxzZSk7CiAgICAgIH0pOwoKICAgICAgdGVzdCgKICAgICAgICAnZG9lcyBub3QgbWF0Y2ggc3Vic3RyaW5nIG9ubHkgKGUuZy4gc2tpcF92ZXJ5X2dvb2Rfb3B0aW1pemF0aW9uLHRlc3QpJywKICAgICAgICAoKSB7CiAgICAgICAgICBmaW5hbCBjb250ZW50ID0KICAgICAgICAgICAgICAnJycKICAgICAgQFRhZ3MoWwogICAgICAgICcke3ByZV9nZW4uc2tpcFZlcnlHb29kT3B0aW1pemF0aW9uVGFnfSx0ZXN0JywKICAgICAgICAnY2hyb21lJywKICAgICAgXSkKICAgICAgJycnOwogICAgICAgICAgZXhwZWN0KAogICAgICAgICAgICByZWdleC5oYXNNYXRjaChjb250ZW50KSwKICAgICAgICAgICAgaXNGYWxzZSwKICAgICAgICAgICk7IC8vIG9ubHkgZXhhY3QgdGFnIHNob3VsZCBtYXRjaAogICAgICAgIH0sCiAgICAgICk7CiAgICB9KTsKICAgIGdyb3VwKCdTaGFyZGluZycsICgpIHsKICAgICAgLy8vIENyZWF0ZXMgYSBwYWNrYWdlIHdpdGggW2NvdW50XSBvcHRpbWl6YWJsZSB0ZXN0IGZpbGVzLCBwbHVzIGFueQogICAgICAvLy8gW25vdE9wdGltaXplZF0gZmlsZXMgY2FycnlpbmcgdGhlIHNraXAgb3B0aW1pemF0aW9uIHRhZy4KICAgICAgRGlyZWN0b3J5IGNyZWF0ZVBhY2thZ2UoaW50IGNvdW50LCB7aW50IG5vdE9wdGltaXplZCA9IDB9KSB7CiAgICAgICAgRmlsZShwYXRoLmpvaW4odGVtcERpcmVjdG9yeS5wYXRoLCAncHVic3BlYy55YW1sJykpLmNyZWF0ZVN5bmMoKTsKICAgICAgICBmaW5hbCB0ZXN0RGlyID0gRGlyZWN0b3J5KHBhdGguam9pbih0ZW1wRGlyZWN0b3J5LnBhdGgsICd0ZXN0JykpCiAgICAgICAgICAuLmNyZWF0ZVN5bmMoKTsKICAgICAgICBmb3IgKHZhciBpID0gMDsgaSA8IGNvdW50OyBpKyspIHsKICAgICAgICAgIEZpbGUocGF0aC5qb2luKHRlc3REaXIucGF0aCwgJ3Rlc3Qke2l9X3Rlc3QuZGFydCcpKS5jcmVhdGVTeW5jKCk7CiAgICAgICAgfQogICAgICAgIGZvciAodmFyIGkgPSAwOyBpIDwgbm90T3B0aW1pemVkOyBpKyspIHsKICAgICAgICAgIEZpbGUoCiAgICAgICAgICAgIHBhdGguam9pbih0ZXN0RGlyLnBhdGgsICdza2lwJHtpfV90ZXN0LmRhcnQnKSwKICAgICAgICAgICkud3JpdGVBc1N0cmluZ1N5bmMobm90T3B0aW1pemVkVGVzdENvbnRlbnQpOwogICAgICAgIH0KICAgICAgICByZXR1cm4gdGVzdERpcjsKICAgICAgfQoKICAgICAgTGlzdDxTdHJpbmc+IHBhdGhzT2YoSG9va0NvbnRleHQgY29udGV4dCkgewogICAgICAgIGZpbmFsIHRlc3RzID0gY29udGV4dC52YXJzWyd0ZXN0cyddIGFzIExpc3Q8TWFwPFN0cmluZywgU3RyaW5nPj47CiAgICAgICAgcmV0dXJuIHRlc3RzLm1hcCgoZSkgPT4gZVsncGF0aCddISkudG9MaXN0KCk7CiAgICAgIH0KCiAgICAgIEZ1dHVyZTxMaXN0PFN0cmluZz4+IHJ1blNoYXJkKGludCBpbmRleCwgaW50IHRvdGFsKSBhc3luYyB7CiAgICAgICAgZmluYWwgY29udGV4dCA9IF9GYWtlQ29udGV4dCgpCiAgICAgICAgICAuLnZhcnNbJ3BhY2thZ2Utcm9vdCddID0gdGVtcERpcmVjdG9yeS5hYnNvbHV0ZS5wYXRoCiAgICAgICAgICAuLnZhcnNbJ3NoYXJkLWluZGV4J10gPSBpbmRleAogICAgICAgICAgLi52YXJzWyd0b3RhbC1zaGFyZHMnXSA9IHRvdGFsOwogICAgICAgIGF3YWl0IHByZV9nZW4ucnVuKGNvbnRleHQpOwogICAgICAgIHJldHVybiBbCiAgICAgICAgICAuLi5wYXRoc09mKGNvbnRleHQpLAogICAgICAgICAgLi4uKGNvbnRleHQudmFyc1snbm90T3B0aW1pemVkVGVzdHMnXSEgYXMgTGlzdCkuY2FzdDxTdHJpbmc+KCksCiAgICAgICAgXTsKICAgICAgfQoKICAgICAgdGVzdCgncnVucyBldmVyeSB0ZXN0IGV4YWN0bHkgb25jZSBhY3Jvc3MgYWxsIHNoYXJkcycsICgpIGFzeW5jIHsKICAgICAgICBjcmVhdGVQYWNrYWdlKDcsIG5vdE9wdGltaXplZDogMik7CgogICAgICAgIGZpbmFsIHNoYXJkcyA9IFsKICAgICAgICAgIGZvciAodmFyIGkgPSAxOyBpIDw9IDM7IGkrKykgYXdhaXQgcnVuU2hhcmQoaSwgMyksCiAgICAgICAgXTsKICAgICAgICBmaW5hbCB1bmlvbiA9IHNoYXJkcy5leHBhbmQoKHNoYXJkKSA9PiBzaGFyZCkudG9MaXN0KCk7CgogICAgICAgIGV4cGVjdCgKICAgICAgICAgIHVuaW9uLi5zb3J0KCksCiAgICAgICAgICBbCiAgICAgICAgICAgIGZvciAodmFyIGkgPSAwOyBpIDwgNzsgaSsrKSAndGVzdCR7aX1fdGVzdC5kYXJ0JywKICAgICAgICAgICAgZm9yICh2YXIgaSA9IDA7IGkgPCAyOyBpKyspICdza2lwJHtpfV90ZXN0LmRhcnQnLAogICAgICAgICAgXS4uc29ydCgpLAogICAgICAgICAgcmVhc29uOiAnU2hhcmRzIG11c3QgYmUgYSBjb21wbGV0ZSBhbmQgZGlzam9pbnQgcGFydGl0aW9uJywKICAgICAgICApOwogICAgICB9KTsKCiAgICAgIHRlc3QoJ3NoYXJkcyBub24gb3B0aW1pemVkIHRlc3RzIGFzIHdlbGwnLCAoKSBhc3luYyB7CiAgICAgICAgY3JlYXRlUGFja2FnZSgwLCBub3RPcHRpbWl6ZWQ6IDQpOwoKICAgICAgICBmaW5hbCBmaXJzdCA9IGF3YWl0IHJ1blNoYXJkKDEsIDIpOwogICAgICAgIGZpbmFsIHNlY29uZCA9IGF3YWl0IHJ1blNoYXJkKDIsIDIpOwoKICAgICAgICBleHBlY3QoZmlyc3QsIFsnc2tpcDBfdGVzdC5kYXJ0JywgJ3NraXAyX3Rlc3QuZGFydCddKTsKICAgICAgICBleHBlY3Qoc2Vjb25kLCBbJ3NraXAxX3Rlc3QuZGFydCcsICdza2lwM190ZXN0LmRhcnQnXSk7CiAgICAgIH0pOwoKICAgICAgdGVzdCgnaXMgZGV0ZXJtaW5pc3RpYyBhY3Jvc3MgcnVucycsICgpIGFzeW5jIHsKICAgICAgICBjcmVhdGVQYWNrYWdlKDkpOwoKICAgICAgICBleHBlY3QoYXdhaXQgcnVuU2hhcmQoMiwgNCksIGF3YWl0IHJ1blNoYXJkKDIsIDQpKTsKICAgICAgfSk7CgogICAgICB0ZXN0KCdiYWxhbmNlcyBzaGFyZHMgd2l0aGluIG9uZSBmaWxlIG9mIGVhY2ggb3RoZXInLCAoKSBhc3luYyB7CiAgICAgICAgY3JlYXRlUGFja2FnZSgxMCk7CgogICAgICAgIGZpbmFsIHNpemVzID0gWwogICAgICAgICAgZm9yICh2YXIgaSA9IDE7IGkgPD0gNDsgaSsrKSAoYXdhaXQgcnVuU2hhcmQoaSwgNCkpLmxlbmd0aCwKICAgICAgICBdOwoKICAgICAgICBleHBlY3Qoc2l6ZXMucmVkdWNlKG1heCkgLSBzaXplcy5yZWR1Y2UobWluKSwgbGVzc1RoYW5PckVxdWFsVG8oMSkpOwogICAgICB9KTsKCiAgICAgIHRlc3QoCiAgICAgICAgJ3lpZWxkcyBhbiBlbXB0eSBzaGFyZCB3aGVuIHRoZXJlIGFyZSBtb3JlIHNoYXJkcyB0aGFuIHRlc3RzJywKICAgICAgICAoKSBhc3luYyB7CiAgICAgICAgICBjcmVhdGVQYWNrYWdlKDIpOwoKICAgICAgICAgIGV4cGVjdChhd2FpdCBydW5TaGFyZCgzLCAzKSwgaXNFbXB0eSk7CiAgICAgICAgfSwKICAgICAgKTsKCiAgICAgIHRlc3QoJ2luY2x1ZGVzIGV2ZXJ5IHRlc3Qgd2hlbiBzaGFyZGluZyBpcyBub3QgcmVxdWVzdGVkJywgKCkgYXN5bmMgewogICAgICAgIGNyZWF0ZVBhY2thZ2UoMyk7CgogICAgICAgIGZpbmFsIGNvbnRleHQgPSBfRmFrZUNvbnRleHQoKQogICAgICAgICAgLi52YXJzWydwYWNrYWdlLXJvb3QnXSA9IHRlbXBEaXJlY3RvcnkuYWJzb2x1dGUucGF0aDsKICAgICAgICBhd2FpdCBwcmVfZ2VuLnJ1bihjb250ZXh0KTsKCiAgICAgICAgZXhwZWN0KHBhdGhzT2YoY29udGV4dCksIGhhc0xlbmd0aCgzKSk7CiAgICAgIH0pOwogICAgfSk7CgogICAgZ3JvdXAoJ3NoYXJkT2YnLCAoKSB7CiAgICAgIGNvbnN0IHBhdGhzID0gWydhLmRhcnQnLCAnYi5kYXJ0JywgJ2MuZGFydCcsICdkLmRhcnQnLCAnZS5kYXJ0J107CgogICAgICB0ZXN0KCdyZXR1cm5zIHBhdGhzIHVudG91Y2hlZCB3aGVuIHNoYXJkIGluZGV4IGlzIG51bGwnLCAoKSB7CiAgICAgICAgZXhwZWN0KAogICAgICAgICAgcHJlX2dlbi5zaGFyZE9mKHBhdGhzLCBzaGFyZEluZGV4OiBudWxsLCB0b3RhbFNoYXJkczogMyksCiAgICAgICAgICBwYXRocywKICAgICAgICApOwogICAgICB9KTsKCiAgICAgIHRlc3QoJ3JldHVybnMgcGF0aHMgdW50b3VjaGVkIHdoZW4gdG90YWwgc2hhcmRzIGlzIG51bGwnLCAoKSB7CiAgICAgICAgZXhwZWN0KAogICAgICAgICAgcHJlX2dlbi5zaGFyZE9mKHBhdGhzLCBzaGFyZEluZGV4OiAxLCB0b3RhbFNoYXJkczogbnVsbCksCiAgICAgICAgICBwYXRocywKICAgICAgICApOwogICAgICB9KTsKCiAgICAgIHRlc3QoJ2RlYWxzIHBhdGhzIG91dCByb3VuZCByb2JpbicsICgpIHsKICAgICAgICBleHBlY3QoCiAgICAgICAgICBwcmVfZ2VuLnNoYXJkT2YocGF0aHMsIHNoYXJkSW5kZXg6IDEsIHRvdGFsU2hhcmRzOiAyKSwKICAgICAgICAgIFsnYS5kYXJ0JywgJ2MuZGFydCcsICdlLmRhcnQnXSwKICAgICAgICApOwogICAgICAgIGV4cGVjdCgKICAgICAgICAgIHByZV9nZW4uc2hhcmRPZihwYXRocywgc2hhcmRJbmRleDogMiwgdG90YWxTaGFyZHM6IDIpLAogICAgICAgICAgWydiLmRhcnQnLCAnZC5kYXJ0J10sCiAgICAgICAgKTsKICAgICAgfSk7CgogICAgICB0ZXN0KCdyZXR1cm5zIGV2ZXJ5dGhpbmcgZm9yIGEgc2luZ2xlIHNoYXJkJywgKCkgewogICAgICAgIGV4cGVjdChwcmVfZ2VuLnNoYXJkT2YocGF0aHMsIHNoYXJkSW5kZXg6IDEsIHRvdGFsU2hhcmRzOiAxKSwgcGF0aHMpOwogICAgICB9KTsKCiAgICAgIHRlc3QoJ3JldHVybnMgZW1wdHkgZm9yIGFuIG91dCBvZiByYW5nZSBzaGFyZCcsICgpIHsKICAgICAgICBleHBlY3QoCiAgICAgICAgICBwcmVfZ2VuLnNoYXJkT2YocGF0aHMsIHNoYXJkSW5kZXg6IDksIHRvdGFsU2hhcmRzOiA5KSwKICAgICAgICAgIGlzRW1wdHksCiAgICAgICAgKTsKICAgICAgfSk7CiAgICB9KTsKICB9KTsKfQo=", "type": "text", }, ], @@ -61,5 +61,15 @@ final testOptimizerBundle = MasonBundle.fromJson({ "default": ".", "prompt": "Please enter the path to the package root.", }, + "shard-index": { + "type": "number", + "description": "The 1-based index of the shard to generate tests for.", + "prompt": "Please enter the shard index.", + }, + "total-shards": { + "type": "number", + "description": "The total number of shards the test suite is split into.", + "prompt": "Please enter the total number of shards.", + }, }, }); diff --git a/lib/src/cli/test_cli_runner.dart b/lib/src/cli/test_cli_runner.dart index 3d6c42c06..d734f88d3 100644 --- a/lib/src/cli/test_cli_runner.dart +++ b/lib/src/cli/test_cli_runner.dart @@ -103,6 +103,8 @@ class TestCLIRunner { GeneratorBuilder buildGenerator = MasonGenerator.fromBundle, List? reportOn, bool checkIgnore = false, + int? shardIndex, + int? totalShards, @visibleForTesting VeryGoodTestRunner? overrideTestRunner, }) async { final initialCwd = cwd; @@ -140,7 +142,11 @@ class TestCLIRunner { '''Shuffling test order with --test-randomize-ordering-seed=$randomSeed\n''', ); } - var vars = {'package-root': workingDirectory}; + var vars = { + 'package-root': workingDirectory, + 'shard-index': ?shardIndex, + 'total-shards': ?totalShards, + }; if (optimizePerformance) { final optimizationProgress = logger.progress('Optimizing tests'); try { @@ -162,6 +168,20 @@ class TestCLIRunner { final notOptimizedTests = vars['notOptimizedTests'] as List? ?? []; + + // A shard can legitimately end up empty when there are more shards + // than test files. That is not a failure: the shard has nothing to do, + // so report success instead of letting the test runner exit with + // "No tests were found", which would fail the CI job. + if (shardIndex != null && + optimizePerformance && + (vars['tests'] as List? ?? []).isEmpty && + notOptimizedTests.isEmpty) { + stdout?.call('No tests found for shard $shardIndex in $path\n'); + await _cleanupOptimizerFile(cwd); + return ExitCode.success.code; + } + return _overrideAnsiOutput( forceAnsi, () => diff --git a/lib/src/commands/dart/commands/dart_test_command.dart b/lib/src/commands/dart/commands/dart_test_command.dart index b5dabf9ae..61451c3cd 100644 --- a/lib/src/commands/dart/commands/dart_test_command.dart +++ b/lib/src/commands/dart/commands/dart_test_command.dart @@ -7,6 +7,7 @@ import 'package:mason/mason.dart'; import 'package:meta/meta.dart'; import 'package:path/path.dart' as path; import 'package:very_good_cli/src/cli/cli.dart'; +import 'package:very_good_cli/src/commands/test/test.dart'; import 'package:very_good_cli/src/very_good_config/very_good_config.dart'; /// Options for configuring the Dart test command. @@ -30,6 +31,8 @@ class DartTestOptions { required this.runSkipped, required this.checkIgnore, required this.fileReporter, + required this.shardIndex, + required this.totalShards, }); /// Parses [ArgResults] into a [DartTestOptions] instance. @@ -102,6 +105,12 @@ class DartTestOptions { 'file-reporter', testConfig.fileReporter, ); + final shardIndex = int.tryParse( + argResults['shard-index'] as String? ?? '', + ); + final totalShards = int.tryParse( + argResults['total-shards'] as String? ?? '', + ); final rest = argResults.rest; return DartTestOptions._( @@ -122,6 +131,8 @@ class DartTestOptions { runSkipped: runSkipped, checkIgnore: checkIgnore, fileReporter: fileReporter, + shardIndex: shardIndex, + totalShards: totalShards, rest: rest, ); } @@ -179,6 +190,12 @@ class DartTestOptions { /// `:` (e.g. `json:reports/tests.json`). final String? fileReporter; + /// The 1-based index of the shard to run, when sharding is enabled. + final int? shardIndex; + + /// The total number of shards the test suite is split into. + final int? totalShards; + /// The remaining arguments passed to the `dart test` command. final List rest; } @@ -204,6 +221,8 @@ typedef DartTestCommandCall = Future> Function({ void Function(String)? stderr, List? reportOn, bool checkIgnore, + int? shardIndex, + int? totalShards, }); /// {@template dart_test_command} @@ -335,6 +354,22 @@ class DartTestCommand extends Command { 'Enable an additional reporter writing test results to a file. ' 'Should be in the form : (e.g. "json:reports/tests.json").', valueHelp: 'name:path', + ) + ..addOption( + 'shard-index', + help: + 'The 1-based index of the shard to run. ' + 'Must be used together with --total-shards. ' + 'Requires optimization to be enabled.', + valueHelp: '1', + ) + ..addOption( + 'total-shards', + help: + 'Split the test suite into this many shards and run only the one ' + 'selected by --shard-index. Useful to parallelize tests across ' + 'multiple CI runners.', + valueHelp: '3', ); } @@ -374,6 +409,19 @@ This command should be run from the root of your Dart project.'''); final options = DartTestOptions.parse(_argResults, config: config); + final shardingError = validateSharding( + shardIndex: options.shardIndex, + totalShards: options.totalShards, + rawShardIndex: _argResults['shard-index'] as String?, + rawTotalShards: _argResults['total-shards'] as String?, + optimizePerformance: options.optimizePerformance, + minCoverage: options.minCoverage, + ); + if (shardingError != null) { + _logger.err(shardingError); + return ExitCode.usage.code; + } + if (isDartInstalled) { try { final results = await _dartTest( @@ -410,6 +458,8 @@ This command should be run from the root of your Dart project.'''); ], reportOn: options.reportOn.isEmpty ? null : options.reportOn, checkIgnore: options.checkIgnore, + shardIndex: options.shardIndex, + totalShards: options.totalShards, ); if (results.any((code) => code != ExitCode.success.code)) { return ExitCode.unavailable.code; diff --git a/lib/src/commands/test/test.dart b/lib/src/commands/test/test.dart index ca565a4f9..a06507286 100644 --- a/lib/src/commands/test/test.dart +++ b/lib/src/commands/test/test.dart @@ -33,6 +33,8 @@ class FlutterTestOptions { required this.flavor, required this.timeout, required this.fileReporter, + required this.shardIndex, + required this.totalShards, required this.rest, }); @@ -124,6 +126,12 @@ class FlutterTestOptions { 'file-reporter', testConfig.fileReporter, ); + final shardIndex = int.tryParse( + argResults['shard-index'] as String? ?? '', + ); + final totalShards = int.tryParse( + argResults['total-shards'] as String? ?? '', + ); final rest = argResults.rest; return FlutterTestOptions._( @@ -148,6 +156,8 @@ class FlutterTestOptions { flavor: flavor, timeout: effectiveTimeout, fileReporter: fileReporter, + shardIndex: shardIndex, + totalShards: totalShards, rest: rest, ); } @@ -218,10 +228,72 @@ class FlutterTestOptions { /// `:` (e.g. `json:reports/tests.json`). final String? fileReporter; + /// The 1-based index of the shard to run, when sharding is enabled. + final int? shardIndex; + + /// The total number of shards the test suite is split into. + final int? totalShards; + /// The remaining arguments passed to the test command. final List rest; } +/// Validates the `--shard-index` / `--total-shards` combination. +/// +/// Returns an error message describing the problem, or `null` when the +/// configuration is valid. +/// +/// [rawShardIndex] and [rawTotalShards] are the unparsed command line values, +/// needed to tell "not provided" apart from "provided but not a number". +String? validateSharding({ + required int? shardIndex, + required int? totalShards, + required String? rawShardIndex, + required String? rawTotalShards, + required bool optimizePerformance, + required double? minCoverage, +}) { + if (rawShardIndex == null && rawTotalShards == null) return null; + + if (rawShardIndex == null || rawTotalShards == null) { + return '--shard-index and --total-shards must be used together.'; + } + + if (totalShards == null || totalShards < 1) { + return '--total-shards must be a positive integer, ' + 'but got "$rawTotalShards".'; + } + + if (shardIndex == null || shardIndex < 1) { + return '--shard-index must be a positive integer, ' + 'but got "$rawShardIndex".'; + } + + if (shardIndex > totalShards) { + return '--shard-index ($shardIndex) must be less than or equal to ' + '--total-shards ($totalShards).'; + } + + // Sharding partitions the test files consolidated by the optimizer, so it + // cannot work when the optimization is turned off. + if (!optimizePerformance) { + return 'Sharding requires optimization to be enabled. ' + 'Remove --no-optimization or --platform to use sharding.'; + } + + // Each shard only exercises a fraction of the codebase, so its coverage is + // not representative of the whole suite. Merge the lcov files from every + // shard and enforce the threshold in a separate job instead. + if (minCoverage != null) { + return 'Sharding cannot be combined with --min-coverage, because each ' + 'shard only covers a subset of the tests. Collect coverage per shard ' + 'with --coverage, merge the lcov reports, and check the threshold ' + 'once all shards have completed.'; + } + + return null; +} + /// Signature for the [Flutter.installed] method. typedef FlutterInstalledCommand = Future Function({ required Logger logger, @@ -244,6 +316,8 @@ typedef FlutterTestCommand = Future> Function({ void Function(String)? stdout, void Function(String)? stderr, List? reportOn, + int? shardIndex, + int? totalShards, }); /// {@template test_command} @@ -411,6 +485,22 @@ class TestCommand extends Command { 'Enable an additional reporter writing test results to a file. ' 'Should be in the form : (e.g. "json:reports/tests.json").', valueHelp: 'name:path', + ) + ..addOption( + 'shard-index', + help: + 'The 1-based index of the shard to run. ' + 'Must be used together with --total-shards. ' + 'Requires optimization to be enabled.', + valueHelp: '1', + ) + ..addOption( + 'total-shards', + help: + 'Split the test suite into this many shards and run only the one ' + 'selected by --shard-index. Useful to parallelize tests across ' + 'multiple CI runners.', + valueHelp: '3', ); } @@ -452,6 +542,19 @@ This command should be run from the root of your Flutter project.'''); final options = FlutterTestOptions.parse(_argResults, config: config); + final shardingError = validateSharding( + shardIndex: options.shardIndex, + totalShards: options.totalShards, + rawShardIndex: _argResults['shard-index'] as String?, + rawTotalShards: _argResults['total-shards'] as String?, + optimizePerformance: options.optimizePerformance, + minCoverage: options.minCoverage, + ); + if (shardingError != null) { + _logger.err(shardingError); + return ExitCode.usage.code; + } + if (isFlutterInstalled) { try { final results = await _flutterTest( @@ -477,6 +580,8 @@ This command should be run from the root of your Flutter project.'''); randomSeed: options.randomSeed, forceAnsi: options.forceAnsi, reportOn: options.reportOn.isEmpty ? null : options.reportOn, + shardIndex: options.shardIndex, + totalShards: options.totalShards, arguments: [ if (options.excludeTags != null) ...['-x', options.excludeTags!], if (options.tags != null) ...['-t', options.tags!], diff --git a/site/docs/commands/test.md b/site/docs/commands/test.md index 505fc3d8d..5ad659f06 100644 --- a/site/docs/commands/test.md +++ b/site/docs/commands/test.md @@ -51,6 +51,12 @@ very_good test [arguments] Useful when tests hang due to an unbounded pumpAndSettle() call. --file-reporter= Enable an additional reporter writing test results to a file. Should be in the form : (e.g. "json:reports/tests.json"). + --shard-index=<1> The 1-based index of the shard to run. + Must be used together with --total-shards. + Requires optimization to be enabled. + --total-shards=<3> Split the test suite into this many shards and run only the one + selected by --shard-index. + Useful to parallelize tests across multiple CI runners. Run "very_good help" to see global options. ``` @@ -74,6 +80,44 @@ For **Dart** projects, use **`very_good dart test`** instead. All test parameters and options work identically with this command. ::: +### Sharding tests across CI runners + +When the test phase is your CI bottleneck, split the suite across several +runners with `--shard-index` and `--total-shards`. Each runner generates its own +optimized entrypoint containing only its slice of the test files, so the shards +together run every test exactly once. + +```yaml +jobs: + test: + strategy: + matrix: + shard: [1, 2, 3] + steps: + - run: very_good test --shard-index ${{ matrix.shard }} --total-shards 3 +``` + +Test files are sorted and then dealt out round-robin, which keeps the partition +deterministic: the same suite always produces the same shards, on every machine. +Files tagged with `skip_very_good_optimization` are sharded as well, so they do +not run on every runner. + +A shard with no test files (more shards than test files) succeeds without +running anything, so an oversized matrix will not fail your build. + +:::caution +Sharding cannot be combined with `--min-coverage`. Each shard only exercises a +subset of the codebase, so its coverage is not representative of the whole +suite. Collect coverage per shard with `--coverage`, merge the resulting lcov +reports once every shard has finished, and enforce the threshold on the merged +report in a separate job. +::: + +:::info +Sharding requires the test optimizer, so it cannot be used with +`--no-optimization` or with `--platform` (which disables the optimizer). +::: + ### Passing Flutter specific arguments The `dart test` and `flutter test` commands expose more arguments than those available through `very_good dart test` or `very_good test`. However, you can use the argument terminator `--` to pass additional arguments directly to the underlying test commands. Any arguments after `--` will be forwarded to `dart test` or `flutter test`, making all their options available. diff --git a/test/src/cli/test_cli_runner_test.dart b/test/src/cli/test_cli_runner_test.dart index 0ad1e1744..d2a1be34d 100644 --- a/test/src/cli/test_cli_runner_test.dart +++ b/test/src/cli/test_cli_runner_test.dart @@ -1083,6 +1083,111 @@ void main() { }, ); + test('passes shard vars to the generator', () async { + final tempDirectory = Directory.systemTemp.createTempSync(); + addTearDown(() => tempDirectory.deleteSync(recursive: true)); + + File(p.join(tempDirectory.path, 'pubspec.yaml')).createSync(); + Directory(p.join(tempDirectory.path, 'test')).createSync(); + when( + () => hooks.preGen( + vars: any(named: 'vars'), + onVarsChanged: any(named: 'onVarsChanged'), + workingDirectory: any(named: 'workingDirectory'), + ), + ).thenAnswer((invocation) async { + (invocation.namedArguments[#onVarsChanged] + as void Function(Map vars)) + .call({ + 'package-root': tempDirectory.path, + 'tests': [ + {'path': 'a_test.dart', 'identifier': 'a'}, + ], + }); + }); + + await expectLater( + TestCLIRunner.test( + testType: TestRunType.flutter, + cwd: tempDirectory.path, + logger: logger, + stdout: stdoutLogs.add, + stderr: stderrLogs.add, + buildGenerator: generatorBuilder(), + optimizePerformance: true, + shardIndex: 2, + totalShards: 3, + overrideTestRunner: testRunner( + Stream.fromIterable([ + const DoneTestEvent(success: true, time: 0), + const ExitTestEvent(exitCode: 0, time: 0), + ]), + ), + ), + completion(equals([ExitCode.success.code])), + ); + + verify( + () => hooks.preGen( + vars: { + 'package-root': tempDirectory.path, + 'shard-index': 2, + 'total-shards': 3, + }, + onVarsChanged: any(named: 'onVarsChanged'), + workingDirectory: tempDirectory.path, + ), + ).called(1); + }); + + test('succeeds without running tests when the shard is empty', () async { + final tempDirectory = Directory.systemTemp.createTempSync(); + addTearDown(() => tempDirectory.deleteSync(recursive: true)); + + File(p.join(tempDirectory.path, 'pubspec.yaml')).createSync(); + Directory(p.join(tempDirectory.path, 'test')).createSync(); + when( + () => hooks.preGen( + vars: any(named: 'vars'), + onVarsChanged: any(named: 'onVarsChanged'), + workingDirectory: any(named: 'workingDirectory'), + ), + ).thenAnswer((invocation) async { + (invocation.namedArguments[#onVarsChanged] + as void Function(Map vars)) + .call({ + 'package-root': tempDirectory.path, + 'tests': >[], + 'notOptimizedTests': [], + }); + }); + + await expectLater( + TestCLIRunner.test( + testType: TestRunType.flutter, + cwd: tempDirectory.path, + logger: logger, + stdout: stdoutLogs.add, + stderr: stderrLogs.add, + buildGenerator: generatorBuilder(), + optimizePerformance: true, + shardIndex: 9, + totalShards: 9, + overrideTestRunner: testRunner(const Stream.empty()), + ), + completion(equals([ExitCode.success.code])), + ); + + expect( + stdoutLogs, + equals([ + 'Running "flutter test" in . ...\n', + 'No tests found for shard 9 in .\n', + ]), + ); + expect(testRunnerArgs, isEmpty); + }); + test('runs tests w/optimizations (passing)', () async { final tempDirectory = Directory.systemTemp.createTempSync(); addTearDown(() => tempDirectory.deleteSync(recursive: true)); diff --git a/test/src/commands/dart/commands/dart_test_test.dart b/test/src/commands/dart/commands/dart_test_test.dart index 589183c29..c9f920d77 100644 --- a/test/src/commands/dart/commands/dart_test_test.dart +++ b/test/src/commands/dart/commands/dart_test_test.dart @@ -50,6 +50,8 @@ const expectedTestUsage = [ ' --[no-]check-ignore Whether to check for and respect coverage ignore comments (e.g. // coverage:ignore-line).\n' ' (defaults to on)\n' ' --file-reporter= Enable an additional reporter writing test results to a file. Should be in the form : (e.g. "json:reports/tests.json").\n' + ' --shard-index=<1> The 1-based index of the shard to run. Must be used together with --total-shards. Requires optimization to be enabled.\n' + ' --total-shards=<3> Split the test suite into this many shards and run only the one selected by --shard-index. Useful to parallelize tests across multiple CI runners.\n' '\n' 'Run "very_good help" to see global options.', ]; @@ -75,6 +77,8 @@ abstract class DartTestCommandCall { void Function(String)? stderr, bool? forceAnsi, List? reportOn, + int? shardIndex, + int? totalShards, }); } @@ -122,6 +126,8 @@ void main() { stderr: any(named: 'stderr'), forceAnsi: any(named: 'forceAnsi'), reportOn: any(named: 'reportOn'), + shardIndex: any(named: 'shardIndex'), + totalShards: any(named: 'totalShards'), ), ).thenAnswer((_) async => [0]); when(() => argResults['concurrency']).thenReturn(concurrency); @@ -133,6 +139,8 @@ void main() { when(() => argResults['run-skipped']).thenReturn(false); when(() => argResults['optimization']).thenReturn(true); when(() => argResults['platform']).thenReturn(null); + when(() => argResults['shard-index']).thenReturn(null); + when(() => argResults['total-shards']).thenReturn(null); when( () => argResults['collect-coverage-from'], ).thenReturn('imports'); @@ -831,6 +839,91 @@ void main() { ).called(1); }); + group('sharding', () { + void withShards(String? index, String? total) { + when(() => argResults['shard-index']).thenReturn(index); + when(() => argResults['total-shards']).thenReturn(total); + } + + test('forwards shard values to the test runner', () async { + withShards('2', '3'); + + final result = await testCommand.run(); + + expect(result, equals(ExitCode.success.code)); + verify( + () => dartTest( + shardIndex: 2, + totalShards: 3, + optimizePerformance: true, + arguments: defaultArguments, + logger: logger, + stdout: logger.write, + stderr: logger.err, + ), + ).called(1); + }); + + test('fails when only one shard option is provided', () async { + withShards('1', null); + + final result = await testCommand.run(); + + expect(result, equals(ExitCode.usage.code)); + verify( + () => logger.err( + '--shard-index and --total-shards must be used together.', + ), + ).called(1); + }); + + test('fails when --shard-index exceeds --total-shards', () async { + withShards('4', '3'); + + final result = await testCommand.run(); + + expect(result, equals(ExitCode.usage.code)); + verify( + () => logger.err( + '--shard-index (4) must be less than or equal to ' + '--total-shards (3).', + ), + ).called(1); + }); + + test('fails when sharding with optimization disabled', () async { + withShards('1', '3'); + when(() => argResults['optimization']).thenReturn(false); + + final result = await testCommand.run(); + + expect(result, equals(ExitCode.usage.code)); + verify( + () => logger.err( + 'Sharding requires optimization to be enabled. ' + 'Remove --no-optimization or --platform to use sharding.', + ), + ).called(1); + }); + + test('fails when sharding is combined with --min-coverage', () async { + withShards('1', '3'); + when(() => argResults['min-coverage']).thenReturn('100'); + + final result = await testCommand.run(); + + expect(result, equals(ExitCode.usage.code)); + verify( + () => logger.err( + 'Sharding cannot be combined with --min-coverage, because each ' + 'shard only covers a subset of the tests. Collect coverage per ' + 'shard with --coverage, merge the lcov reports, and check the ' + 'threshold once all shards have completed.', + ), + ).called(1); + }); + }); + group('very_good.yaml configuration', () { test( 'fails with exit code ${ExitCode.config.code} ' diff --git a/test/src/commands/test/test_test.dart b/test/src/commands/test/test_test.dart index 4dd2070ac..e3aa75664 100644 --- a/test/src/commands/test/test_test.dart +++ b/test/src/commands/test/test_test.dart @@ -53,6 +53,8 @@ const expectedTestUsage = [ ' --flavor Build a custom app flavor as defined by platform-specific build setup. Supports the use of product flavors in Android Gradle scripts, and the use of custom Xcode schemes.\n' ' --timeout= Maximum seconds to let tests run before killing the process. Useful when tests hang due to an unbounded pumpAndSettle() call.\n' ' --file-reporter= Enable an additional reporter writing test results to a file. Should be in the form : (e.g. "json:reports/tests.json").\n' + ' --shard-index=<1> The 1-based index of the shard to run. Must be used together with --total-shards. Requires optimization to be enabled.\n' + ' --total-shards=<3> Split the test suite into this many shards and run only the one selected by --shard-index. Useful to parallelize tests across multiple CI runners.\n' '\n' 'Run "very_good help" to see global options.', ]; @@ -77,6 +79,8 @@ abstract class FlutterTestCommand { void Function(String)? stderr, bool? forceAnsi, List? reportOn, + int? shardIndex, + int? totalShards, }); } @@ -123,6 +127,8 @@ void main() { stderr: any(named: 'stderr'), forceAnsi: any(named: 'forceAnsi'), reportOn: any(named: 'reportOn'), + shardIndex: any(named: 'shardIndex'), + totalShards: any(named: 'totalShards'), ), ).thenAnswer((_) async => [0]); when(() => argResults['concurrency']).thenReturn(concurrency); @@ -133,6 +139,8 @@ void main() { when(() => argResults['fail-fast']).thenReturn(false); when(() => argResults['optimization']).thenReturn(true); when(() => argResults['platform']).thenReturn(null); + when(() => argResults['shard-index']).thenReturn(null); + when(() => argResults['total-shards']).thenReturn(null); when(() => argResults['report-on']).thenReturn([]); when(() => argResults['run-skipped']).thenReturn(false); when(() => argResults['flavor']).thenReturn(null); @@ -937,6 +945,164 @@ void main() { ); }); + group('sharding', () { + void withShards(String? index, String? total) { + when(() => argResults['shard-index']).thenReturn(index); + when(() => argResults['total-shards']).thenReturn(total); + } + + test('forwards shard values to the test runner', () async { + withShards('2', '3'); + + final result = await testCommand.run(); + + expect(result, equals(ExitCode.success.code)); + verify( + () => flutterTest( + shardIndex: 2, + totalShards: 3, + optimizePerformance: true, + arguments: defaultArguments, + logger: logger, + stdout: logger.write, + stderr: logger.err, + ), + ).called(1); + }); + + test('does not forward shard values when not sharding', () async { + final result = await testCommand.run(); + + expect(result, equals(ExitCode.success.code)); + verify( + () => flutterTest( + optimizePerformance: true, + arguments: defaultArguments, + logger: logger, + stdout: logger.write, + stderr: logger.err, + ), + ).called(1); + }); + + test('fails when --shard-index is used without --total-shards', () async { + withShards('1', null); + + final result = await testCommand.run(); + + expect(result, equals(ExitCode.usage.code)); + verify( + () => logger.err( + '--shard-index and --total-shards must be used together.', + ), + ).called(1); + verifyNever( + () => flutterTest( + shardIndex: any(named: 'shardIndex'), + totalShards: any(named: 'totalShards'), + ), + ); + }); + + test('fails when --total-shards is used without --shard-index', () async { + withShards(null, '3'); + + final result = await testCommand.run(); + + expect(result, equals(ExitCode.usage.code)); + verify( + () => logger.err( + '--shard-index and --total-shards must be used together.', + ), + ).called(1); + }); + + test('fails when --shard-index exceeds --total-shards', () async { + withShards('4', '3'); + + final result = await testCommand.run(); + + expect(result, equals(ExitCode.usage.code)); + verify( + () => logger.err( + '--shard-index (4) must be less than or equal to ' + '--total-shards (3).', + ), + ).called(1); + }); + + test('fails when --shard-index is not a positive integer', () async { + withShards('0', '3'); + + final result = await testCommand.run(); + + expect(result, equals(ExitCode.usage.code)); + verify( + () => logger.err( + '--shard-index must be a positive integer, but got "0".', + ), + ).called(1); + }); + + test('fails when --shard-index is not a number', () async { + withShards('abc', '3'); + + final result = await testCommand.run(); + + expect(result, equals(ExitCode.usage.code)); + verify( + () => logger.err( + '--shard-index must be a positive integer, but got "abc".', + ), + ).called(1); + }); + + test('fails when --total-shards is not a positive integer', () async { + withShards('1', '0'); + + final result = await testCommand.run(); + + expect(result, equals(ExitCode.usage.code)); + verify( + () => logger.err( + '--total-shards must be a positive integer, but got "0".', + ), + ).called(1); + }); + + test('fails when sharding with optimization disabled', () async { + withShards('1', '3'); + when(() => argResults['optimization']).thenReturn(false); + + final result = await testCommand.run(); + + expect(result, equals(ExitCode.usage.code)); + verify( + () => logger.err( + 'Sharding requires optimization to be enabled. ' + 'Remove --no-optimization or --platform to use sharding.', + ), + ).called(1); + }); + + test('fails when sharding is combined with --min-coverage', () async { + withShards('1', '3'); + when(() => argResults['min-coverage']).thenReturn('100'); + + final result = await testCommand.run(); + + expect(result, equals(ExitCode.usage.code)); + verify( + () => logger.err( + 'Sharding cannot be combined with --min-coverage, because each ' + 'shard only covers a subset of the tests. Collect coverage per ' + 'shard with --coverage, merge the lcov reports, and check the ' + 'threshold once all shards have completed.', + ), + ).called(1); + }); + }); + group('very_good.yaml configuration', () { test( 'fails with exit code ${ExitCode.config.code} ' From 5e1db47584a81622823a5706d7391410ddccdea0 Mon Sep 17 00:00:00 2001 From: Peter Trost Date: Sat, 29 Aug 2026 14:55:04 +0200 Subject: [PATCH 2/2] fix(test): address sharding review feedback Normalize path separators in getNotOptimizedTests so tagged tests in subdirectories are excluded from the optimized set on Windows. Without this the relative paths compare unequal to the forward-slash normalized paths built in run(), causing those tests to be both inlined into the optimizer entrypoint and run standalone. Parse the shard values inside validateSharding instead of passing both the raw and parsed forms, and shorten the --min-coverage error. Also cover the shard that only contains non optimized tests, and document how sharding interacts with --recursive. Co-Authored-By: Claude Opus 5 --- bricks/test_optimizer/hooks/lib/pre_gen.dart | 9 ++-- .../hooks/test/pre_gen_test.dart | 30 +++++++++++ .../cli/templates/test_optimizer_bundle.dart | 4 +- .../dart/commands/dart_test_command.dart | 2 - lib/src/commands/test/test.dart | 15 +++--- site/docs/commands/test.md | 4 ++ test/src/cli/test_cli_runner_test.dart | 52 +++++++++++++++++++ .../dart/commands/dart_test_test.dart | 7 ++- test/src/commands/test/test_test.dart | 7 ++- 9 files changed, 104 insertions(+), 26 deletions(-) diff --git a/bricks/test_optimizer/hooks/lib/pre_gen.dart b/bricks/test_optimizer/hooks/lib/pre_gen.dart index bc789d217..5562b7f66 100644 --- a/bricks/test_optimizer/hooks/lib/pre_gen.dart +++ b/bricks/test_optimizer/hooks/lib/pre_gen.dart @@ -38,9 +38,7 @@ Future run(HookContext context) async { final tests = testDir .listSync(recursive: true) - .where( - (entity) => entity.isTest, - ); + .where((entity) => entity.isTest); final notOptimizedTests = await getNotOptimizedTests(tests, testDir.path); @@ -130,9 +128,10 @@ Future> getNotOptimizedTests( } } - /// Format to relative path + /// Format to relative path, normalizing separators so the paths compare + /// equal to the ones built in [run] on Windows too. final relativePaths = testWithVeryGoodTest - .map((e) => path.relative(e, from: testDir)) + .map((e) => path.relative(e, from: testDir).replaceAll(r'\', '/')) .toList(); return relativePaths; diff --git a/bricks/test_optimizer/hooks/test/pre_gen_test.dart b/bricks/test_optimizer/hooks/test/pre_gen_test.dart index 29958ca59..989f28e03 100644 --- a/bricks/test_optimizer/hooks/test/pre_gen_test.dart +++ b/bricks/test_optimizer/hooks/test/pre_gen_test.dart @@ -375,6 +375,36 @@ dependencies: }, ); + test( + 'excludes nested non optimized tests from the optimized set', + () async { + File(path.join(tempDirectory.path, 'pubspec.yaml')).createSync(); + final testDir = Directory(path.join(tempDirectory.path, 'test')) + ..createSync(); + final nested = Directory(path.join(testDir.path, 'sub')) + ..createSync(); + File( + path.join(nested.path, 'skip_test.dart'), + ).writeAsStringSync(notOptimizedTestContent); + + final context = _FakeContext() + ..vars['package-root'] = tempDirectory.absolute.path; + await pre_gen.run(context); + + expect( + pathsOf(context), + isEmpty, + reason: + 'A tagged test in a subdirectory must not be optimized, ' + 'otherwise it runs both inlined and standalone', + ); + expect( + context.vars['notOptimizedTests'], + ['sub/skip_test.dart'], + ); + }, + ); + test('includes every test when sharding is not requested', () async { createPackage(3); diff --git a/lib/src/cli/templates/test_optimizer_bundle.dart b/lib/src/cli/templates/test_optimizer_bundle.dart index 3a4a02f3e..e1bf653d0 100644 --- a/lib/src/cli/templates/test_optimizer_bundle.dart +++ b/lib/src/cli/templates/test_optimizer_bundle.dart @@ -20,7 +20,7 @@ final testOptimizerBundle = MasonBundle.fromJson({ }, { "path": "lib/pre_gen.dart", - "data": "aW1wb3J0ICdkYXJ0OmlvJzsKCmltcG9ydCAncGFja2FnZTpob29rcy9kYXJ0X2lkZW50aWZpZXJfZ2VuZXJhdG9yLmRhcnQnOwppbXBvcnQgJ3BhY2thZ2U6bWFzb24vbWFzb24uZGFydCc7CmltcG9ydCAncGFja2FnZTpwYXRoL3BhdGguZGFydCcgYXMgcGF0aDsKCnR5cGVkZWYgRXhpdEZuID0gTmV2ZXIgRnVuY3Rpb24oaW50IGNvZGUpOwoKRXhpdEZuIGV4aXRGbiA9IGV4aXQ7CgpTdHJpbmcgc2tpcFZlcnlHb29kT3B0aW1pemF0aW9uVGFnID0gJ3NraXBfdmVyeV9nb29kX29wdGltaXphdGlvbic7ClJlZ0V4cCBza2lwVmVyeUdvb2RPcHRpbWl6YXRpb25SZWdFeHAgPSBSZWdFeHAoCiAgIkBUYWdzXFxzKlxcKFxccypcXFtbXFxzXFxTXSo/W1wiJ10kc2tpcFZlcnlHb29kT3B0aW1pemF0aW9uVGFnW1wiJ11bXFxzXFxTXSo/XFxdXFxzKlxcKSIsCiAgbXVsdGlMaW5lOiB0cnVlLAopOwoKRnV0dXJlPHZvaWQ+IHJ1bihIb29rQ29udGV4dCBjb250ZXh0KSBhc3luYyB7CiAgZmluYWwgcGFja2FnZVJvb3QgPSBjb250ZXh0LnZhcnNbJ3BhY2thZ2Utcm9vdCddIGFzIFN0cmluZzsKICBmaW5hbCB0ZXN0RGlyID0gRGlyZWN0b3J5KHBhdGguam9pbihwYWNrYWdlUm9vdCwgJ3Rlc3QnKSk7CgogIGlmICghdGVzdERpci5leGlzdHNTeW5jKCkpIHsKICAgIGNvbnRleHQubG9nZ2VyLmVycignQ291bGQgbm90IGZpbmQgZGlyZWN0b3J5ICR7dGVzdERpci5wYXRofScpOwogICAgZXhpdEZuKDEpOwogIH0KCiAgZmluYWwgcHVic3BlYyA9IEZpbGUocGF0aC5qb2luKHBhY2thZ2VSb290LCAncHVic3BlYy55YW1sJykpOwogIGlmICghcHVic3BlYy5leGlzdHNTeW5jKCkpIHsKICAgIGNvbnRleHQubG9nZ2VyLmVycignQ291bGQgbm90IGZpbmQgcHVic3BlYy55YW1sIGF0ICR7dGVzdERpci5wYXRofScpOwogICAgZXhpdEZuKDEpOwogIH0KCiAgZmluYWwgcHVic3BlY0NvbnRlbnRzID0gYXdhaXQgcHVic3BlYy5yZWFkQXNTdHJpbmcoKTsKICBmaW5hbCBmbHV0dGVyU2RrUmVnRXhwID0gUmVnRXhwKHInc2RrOlxzKmZsdXR0ZXIkJywgbXVsdGlMaW5lOiB0cnVlKTsKICBmaW5hbCBpc0ZsdXR0ZXIgPSBmbHV0dGVyU2RrUmVnRXhwLmhhc01hdGNoKHB1YnNwZWNDb250ZW50cyk7CgogIGZpbmFsIHNoYXJkSW5kZXggPSBjb250ZXh0LnZhcnNbJ3NoYXJkLWluZGV4J10gYXMgaW50PzsKICBmaW5hbCB0b3RhbFNoYXJkcyA9IGNvbnRleHQudmFyc1sndG90YWwtc2hhcmRzJ10gYXMgaW50PzsKCiAgZmluYWwgdGVzdHMgPSB0ZXN0RGlyCiAgICAgIC5saXN0U3luYyhyZWN1cnNpdmU6IHRydWUpCiAgICAgIC53aGVyZSgKICAgICAgICAoZW50aXR5KSA9PiBlbnRpdHkuaXNUZXN0LAogICAgICApOwoKICBmaW5hbCBub3RPcHRpbWl6ZWRUZXN0cyA9IGF3YWl0IGdldE5vdE9wdGltaXplZFRlc3RzKHRlc3RzLCB0ZXN0RGlyLnBhdGgpOwoKICAvLyBTb3J0aW5nIGd1YXJhbnRlZXMgYSBkZXRlcm1pbmlzdGljIG9yZGVyIGFjcm9zcyBtYWNoaW5lcywgd2hpY2ggaXMgd2hhdAogIC8vIG1ha2VzIHNoYXJkaW5nIHJlcHJvZHVjaWJsZTogYERpcmVjdG9yeS5saXN0U3luY2Agb3JkZXIgaXMgZmlsZXN5c3RlbQogIC8vIGRlcGVuZGVudCwgc28gd2l0aG91dCB0aGlzIHR3byBydW5uZXJzIGNvdWxkIGRpc2FncmVlIG9uIHRoZSBwYXJ0aXRpb24KICAvLyBhbmQgZWl0aGVyIHNraXAgb3IgZHVwbGljYXRlIHRlc3RzLgogIGZpbmFsIHRlc3RQYXRocyA9CiAgICAgIHRlc3RzCiAgICAgICAgICAubWFwKAogICAgICAgICAgICAoZW50aXR5KSA9PiBwYXRoCiAgICAgICAgICAgICAgICAucmVsYXRpdmUoZW50aXR5LnBhdGgsIGZyb206IHRlc3REaXIucGF0aCkKICAgICAgICAgICAgICAgIC5yZXBsYWNlQWxsKHInXCcsICcvJyksCiAgICAgICAgICApCiAgICAgICAgICAudG9MaXN0KCkKICAgICAgICAuLnNvcnQoKTsKCiAgZmluYWwgb3B0aW1pemVkVGVzdFBhdGhzID0gc2hhcmRPZigKICAgIHRlc3RQYXRocy53aGVyZSgocCkgPT4gIW5vdE9wdGltaXplZFRlc3RzLmNvbnRhaW5zKHApKS50b0xpc3QoKSwKICAgIHNoYXJkSW5kZXg6IHNoYXJkSW5kZXgsCiAgICB0b3RhbFNoYXJkczogdG90YWxTaGFyZHMsCiAgKTsKCiAgLy8gTm9uIG9wdGltaXplZCB0ZXN0cyBydW4gYXMgc3RhbmRhbG9uZSBmaWxlcyBhbG9uZ3NpZGUgdGhlIG9wdGltaXplcgogIC8vIGVudHJ5cG9pbnQsIHNvIHRoZXkgbXVzdCBiZSBzaGFyZGVkIHRvby4gT3RoZXJ3aXNlIGV2ZXJ5IHJ1bm5lciB3b3VsZAogIC8vIHJlLXJ1biBhbGwgb2YgdGhlbSwgZGVmZWF0aW5nIHRoZSBwdXJwb3NlIG9mIHNoYXJkaW5nLgogIGZpbmFsIHNoYXJkZWROb3RPcHRpbWl6ZWRUZXN0cyA9IHNoYXJkT2YoCiAgICBub3RPcHRpbWl6ZWRUZXN0cy4uc29ydCgpLAogICAgc2hhcmRJbmRleDogc2hhcmRJbmRleCwKICAgIHRvdGFsU2hhcmRzOiB0b3RhbFNoYXJkcywKICApOwoKICBmaW5hbCBpZGVudGlmaWVyR2VuZXJhdG9yID0gRGFydElkZW50aWZpZXJHZW5lcmF0b3IoKTsKICBmaW5hbCBvcHRpbWl6ZWRUZXN0c0lkZW50aWZpZXJUYWJsZSA9IFsKICAgIGZvciAoZmluYWwgcmVsYXRpdmVQYXRoIGluIG9wdGltaXplZFRlc3RQYXRocykKICAgICAgeydwYXRoJzogcmVsYXRpdmVQYXRoLCAnaWRlbnRpZmllcic6IGlkZW50aWZpZXJHZW5lcmF0b3IubmV4dCgpfSwKICBdOwoKICBjb250ZXh0LnZhcnMgPSB7CiAgICAndGVzdHMnOiBvcHRpbWl6ZWRUZXN0c0lkZW50aWZpZXJUYWJsZSwKICAgICdpc0ZsdXR0ZXInOiBpc0ZsdXR0ZXIsCiAgICAnbm90T3B0aW1pemVkVGVzdHMnOiBzaGFyZGVkTm90T3B0aW1pemVkVGVzdHMsCiAgfTsKfQoKLy8vIFJldHVybnMgdGhlIHN1YnNldCBvZiBbcGF0aHNdIHRoYXQgYmVsb25ncyB0byB0aGUgc2hhcmQgW3NoYXJkSW5kZXhdIG91dCBvZgovLy8gW3RvdGFsU2hhcmRzXS4KLy8vCi8vLyBSZXR1cm5zIFtwYXRoc10gdW5jaGFuZ2VkIHdoZW4gc2hhcmRpbmcgaXMgbm90IGVuYWJsZWQgKGVpdGhlciB2YWx1ZSBpcwovLy8gYG51bGxgKS4KLy8vCi8vLyBGaWxlcyBhcmUgZGVhbHQgb3V0IHJvdW5kLXJvYmluIChpbmRleCBtb2R1bG8gW3RvdGFsU2hhcmRzXSkgb3ZlciB0aGUKLy8vIGFscmVhZHkgc29ydGVkIFtwYXRoc10sIHdoaWNoIGtlZXBzIHNoYXJkcyBiYWxhbmNlZCBpbiBmaWxlIGNvdW50IGFuZCBtYWtlcwovLy8gdGhlIHBhcnRpdGlvbiBzdGFibGUgZm9yIGEgZ2l2ZW4gdGVzdCBzdWl0ZS4KTGlzdDxTdHJpbmc+IHNoYXJkT2YoCiAgTGlzdDxTdHJpbmc+IHBhdGhzLCB7CiAgcmVxdWlyZWQgaW50PyBzaGFyZEluZGV4LAogIHJlcXVpcmVkIGludD8gdG90YWxTaGFyZHMsCn0pIHsKICBpZiAoc2hhcmRJbmRleCA9PSBudWxsIHx8IHRvdGFsU2hhcmRzID09IG51bGwpIHJldHVybiBwYXRoczsKCiAgcmV0dXJuIFsKICAgIGZvciAodmFyIGkgPSBzaGFyZEluZGV4IC0gMTsgaSA8IHBhdGhzLmxlbmd0aDsgaSArPSB0b3RhbFNoYXJkcykgcGF0aHNbaV0sCiAgXTsKfQoKZXh0ZW5zaW9uIG9uIEZpbGVTeXN0ZW1FbnRpdHkgewogIGJvb2wgZ2V0IGlzVGVzdCB7CiAgICByZXR1cm4gdGhpcyBpcyBGaWxlICYmIHBhdGguYmFzZW5hbWUodGhpcy5wYXRoKS5lbmRzV2l0aCgnX3Rlc3QuZGFydCcpOwogIH0KfQoKRnV0dXJlPExpc3Q8U3RyaW5nPj4gZ2V0Tm90T3B0aW1pemVkVGVzdHMoCiAgSXRlcmFibGU8RmlsZVN5c3RlbUVudGl0eT4gdGVzdHMsCiAgU3RyaW5nIHRlc3REaXIsCikgYXN5bmMgewogIGZpbmFsIHBhdGhzID0gdGVzdHMubWFwKChlKSA9PiBlLnBhdGgpLnRvTGlzdCgpOwogIGZpbmFsIGZvcm1hdHRlZFBhdGhzID0gcGF0aHMubWFwKChlKSA9PiBlLnJlcGxhY2VBbGwoJy8uLycsICcvJykpLnRvTGlzdCgpOwoKICBmaW5hbCBmaWxlRnV0dXJlcyA9IGZvcm1hdHRlZFBhdGhzLm1hcChfY2hlY2tGaWxlRm9yU2tpcFZlcnlHb29kT3B0aW1pemF0aW9uKTsKICBmaW5hbCBmaWxlUmVzdWx0cyA9IGF3YWl0IEZ1dHVyZS53YWl0KGZpbGVGdXR1cmVzKTsKCiAgZmluYWwgdGVzdFdpdGhWZXJ5R29vZFRlc3QgPSA8U3RyaW5nPltdOwogIGZvciAodmFyIGkgPSAwOyBpIDwgZm9ybWF0dGVkUGF0aHMubGVuZ3RoOyBpKyspIHsKICAgIGlmIChmaWxlUmVzdWx0c1tpXSkgewogICAgICB0ZXN0V2l0aFZlcnlHb29kVGVzdC5hZGQoZm9ybWF0dGVkUGF0aHNbaV0pOwogICAgfQogIH0KCiAgLy8vIEZvcm1hdCB0byByZWxhdGl2ZSBwYXRoCiAgZmluYWwgcmVsYXRpdmVQYXRocyA9IHRlc3RXaXRoVmVyeUdvb2RUZXN0CiAgICAgIC5tYXAoKGUpID0+IHBhdGgucmVsYXRpdmUoZSwgZnJvbTogdGVzdERpcikpCiAgICAgIC50b0xpc3QoKTsKCiAgcmV0dXJuIHJlbGF0aXZlUGF0aHM7Cn0KCi8vLyBDaGVjayBpZiBhIHNpbmdsZSBmaWxlIGNvbnRhaW5zIHNraXBfdmVyeV9nb29kX29wdGltaXphdGlvbiB0YWcKRnV0dXJlPGJvb2w+IF9jaGVja0ZpbGVGb3JTa2lwVmVyeUdvb2RPcHRpbWl6YXRpb24oU3RyaW5nIHBhdGgpIGFzeW5jIHsKICBmaW5hbCBmaWxlID0gRmlsZShwYXRoKTsKICBpZiAoIWZpbGUuZXhpc3RzU3luYygpKSByZXR1cm4gZmFsc2U7CiAgZmluYWwgY29udGVudCA9IGF3YWl0IGZpbGUucmVhZEFzU3RyaW5nKCk7CiAgcmV0dXJuIHNraXBWZXJ5R29vZE9wdGltaXphdGlvblJlZ0V4cC5oYXNNYXRjaChjb250ZW50KTsKfQo=", + "data": "aW1wb3J0ICdkYXJ0OmlvJzsKCmltcG9ydCAncGFja2FnZTpob29rcy9kYXJ0X2lkZW50aWZpZXJfZ2VuZXJhdG9yLmRhcnQnOwppbXBvcnQgJ3BhY2thZ2U6bWFzb24vbWFzb24uZGFydCc7CmltcG9ydCAncGFja2FnZTpwYXRoL3BhdGguZGFydCcgYXMgcGF0aDsKCnR5cGVkZWYgRXhpdEZuID0gTmV2ZXIgRnVuY3Rpb24oaW50IGNvZGUpOwoKRXhpdEZuIGV4aXRGbiA9IGV4aXQ7CgpTdHJpbmcgc2tpcFZlcnlHb29kT3B0aW1pemF0aW9uVGFnID0gJ3NraXBfdmVyeV9nb29kX29wdGltaXphdGlvbic7ClJlZ0V4cCBza2lwVmVyeUdvb2RPcHRpbWl6YXRpb25SZWdFeHAgPSBSZWdFeHAoCiAgIkBUYWdzXFxzKlxcKFxccypcXFtbXFxzXFxTXSo/W1wiJ10kc2tpcFZlcnlHb29kT3B0aW1pemF0aW9uVGFnW1wiJ11bXFxzXFxTXSo/XFxdXFxzKlxcKSIsCiAgbXVsdGlMaW5lOiB0cnVlLAopOwoKRnV0dXJlPHZvaWQ+IHJ1bihIb29rQ29udGV4dCBjb250ZXh0KSBhc3luYyB7CiAgZmluYWwgcGFja2FnZVJvb3QgPSBjb250ZXh0LnZhcnNbJ3BhY2thZ2Utcm9vdCddIGFzIFN0cmluZzsKICBmaW5hbCB0ZXN0RGlyID0gRGlyZWN0b3J5KHBhdGguam9pbihwYWNrYWdlUm9vdCwgJ3Rlc3QnKSk7CgogIGlmICghdGVzdERpci5leGlzdHNTeW5jKCkpIHsKICAgIGNvbnRleHQubG9nZ2VyLmVycignQ291bGQgbm90IGZpbmQgZGlyZWN0b3J5ICR7dGVzdERpci5wYXRofScpOwogICAgZXhpdEZuKDEpOwogIH0KCiAgZmluYWwgcHVic3BlYyA9IEZpbGUocGF0aC5qb2luKHBhY2thZ2VSb290LCAncHVic3BlYy55YW1sJykpOwogIGlmICghcHVic3BlYy5leGlzdHNTeW5jKCkpIHsKICAgIGNvbnRleHQubG9nZ2VyLmVycignQ291bGQgbm90IGZpbmQgcHVic3BlYy55YW1sIGF0ICR7dGVzdERpci5wYXRofScpOwogICAgZXhpdEZuKDEpOwogIH0KCiAgZmluYWwgcHVic3BlY0NvbnRlbnRzID0gYXdhaXQgcHVic3BlYy5yZWFkQXNTdHJpbmcoKTsKICBmaW5hbCBmbHV0dGVyU2RrUmVnRXhwID0gUmVnRXhwKHInc2RrOlxzKmZsdXR0ZXIkJywgbXVsdGlMaW5lOiB0cnVlKTsKICBmaW5hbCBpc0ZsdXR0ZXIgPSBmbHV0dGVyU2RrUmVnRXhwLmhhc01hdGNoKHB1YnNwZWNDb250ZW50cyk7CgogIGZpbmFsIHNoYXJkSW5kZXggPSBjb250ZXh0LnZhcnNbJ3NoYXJkLWluZGV4J10gYXMgaW50PzsKICBmaW5hbCB0b3RhbFNoYXJkcyA9IGNvbnRleHQudmFyc1sndG90YWwtc2hhcmRzJ10gYXMgaW50PzsKCiAgZmluYWwgdGVzdHMgPSB0ZXN0RGlyCiAgICAgIC5saXN0U3luYyhyZWN1cnNpdmU6IHRydWUpCiAgICAgIC53aGVyZSgoZW50aXR5KSA9PiBlbnRpdHkuaXNUZXN0KTsKCiAgZmluYWwgbm90T3B0aW1pemVkVGVzdHMgPSBhd2FpdCBnZXROb3RPcHRpbWl6ZWRUZXN0cyh0ZXN0cywgdGVzdERpci5wYXRoKTsKCiAgLy8gU29ydGluZyBndWFyYW50ZWVzIGEgZGV0ZXJtaW5pc3RpYyBvcmRlciBhY3Jvc3MgbWFjaGluZXMsIHdoaWNoIGlzIHdoYXQKICAvLyBtYWtlcyBzaGFyZGluZyByZXByb2R1Y2libGU6IGBEaXJlY3RvcnkubGlzdFN5bmNgIG9yZGVyIGlzIGZpbGVzeXN0ZW0KICAvLyBkZXBlbmRlbnQsIHNvIHdpdGhvdXQgdGhpcyB0d28gcnVubmVycyBjb3VsZCBkaXNhZ3JlZSBvbiB0aGUgcGFydGl0aW9uCiAgLy8gYW5kIGVpdGhlciBza2lwIG9yIGR1cGxpY2F0ZSB0ZXN0cy4KICBmaW5hbCB0ZXN0UGF0aHMgPQogICAgICB0ZXN0cwogICAgICAgICAgLm1hcCgKICAgICAgICAgICAgKGVudGl0eSkgPT4gcGF0aAogICAgICAgICAgICAgICAgLnJlbGF0aXZlKGVudGl0eS5wYXRoLCBmcm9tOiB0ZXN0RGlyLnBhdGgpCiAgICAgICAgICAgICAgICAucmVwbGFjZUFsbChyJ1wnLCAnLycpLAogICAgICAgICAgKQogICAgICAgICAgLnRvTGlzdCgpCiAgICAgICAgLi5zb3J0KCk7CgogIGZpbmFsIG9wdGltaXplZFRlc3RQYXRocyA9IHNoYXJkT2YoCiAgICB0ZXN0UGF0aHMud2hlcmUoKHApID0+ICFub3RPcHRpbWl6ZWRUZXN0cy5jb250YWlucyhwKSkudG9MaXN0KCksCiAgICBzaGFyZEluZGV4OiBzaGFyZEluZGV4LAogICAgdG90YWxTaGFyZHM6IHRvdGFsU2hhcmRzLAogICk7CgogIC8vIE5vbiBvcHRpbWl6ZWQgdGVzdHMgcnVuIGFzIHN0YW5kYWxvbmUgZmlsZXMgYWxvbmdzaWRlIHRoZSBvcHRpbWl6ZXIKICAvLyBlbnRyeXBvaW50LCBzbyB0aGV5IG11c3QgYmUgc2hhcmRlZCB0b28uIE90aGVyd2lzZSBldmVyeSBydW5uZXIgd291bGQKICAvLyByZS1ydW4gYWxsIG9mIHRoZW0sIGRlZmVhdGluZyB0aGUgcHVycG9zZSBvZiBzaGFyZGluZy4KICBmaW5hbCBzaGFyZGVkTm90T3B0aW1pemVkVGVzdHMgPSBzaGFyZE9mKAogICAgbm90T3B0aW1pemVkVGVzdHMuLnNvcnQoKSwKICAgIHNoYXJkSW5kZXg6IHNoYXJkSW5kZXgsCiAgICB0b3RhbFNoYXJkczogdG90YWxTaGFyZHMsCiAgKTsKCiAgZmluYWwgaWRlbnRpZmllckdlbmVyYXRvciA9IERhcnRJZGVudGlmaWVyR2VuZXJhdG9yKCk7CiAgZmluYWwgb3B0aW1pemVkVGVzdHNJZGVudGlmaWVyVGFibGUgPSBbCiAgICBmb3IgKGZpbmFsIHJlbGF0aXZlUGF0aCBpbiBvcHRpbWl6ZWRUZXN0UGF0aHMpCiAgICAgIHsncGF0aCc6IHJlbGF0aXZlUGF0aCwgJ2lkZW50aWZpZXInOiBpZGVudGlmaWVyR2VuZXJhdG9yLm5leHQoKX0sCiAgXTsKCiAgY29udGV4dC52YXJzID0gewogICAgJ3Rlc3RzJzogb3B0aW1pemVkVGVzdHNJZGVudGlmaWVyVGFibGUsCiAgICAnaXNGbHV0dGVyJzogaXNGbHV0dGVyLAogICAgJ25vdE9wdGltaXplZFRlc3RzJzogc2hhcmRlZE5vdE9wdGltaXplZFRlc3RzLAogIH07Cn0KCi8vLyBSZXR1cm5zIHRoZSBzdWJzZXQgb2YgW3BhdGhzXSB0aGF0IGJlbG9uZ3MgdG8gdGhlIHNoYXJkIFtzaGFyZEluZGV4XSBvdXQgb2YKLy8vIFt0b3RhbFNoYXJkc10uCi8vLwovLy8gUmV0dXJucyBbcGF0aHNdIHVuY2hhbmdlZCB3aGVuIHNoYXJkaW5nIGlzIG5vdCBlbmFibGVkIChlaXRoZXIgdmFsdWUgaXMKLy8vIGBudWxsYCkuCi8vLwovLy8gRmlsZXMgYXJlIGRlYWx0IG91dCByb3VuZC1yb2JpbiAoaW5kZXggbW9kdWxvIFt0b3RhbFNoYXJkc10pIG92ZXIgdGhlCi8vLyBhbHJlYWR5IHNvcnRlZCBbcGF0aHNdLCB3aGljaCBrZWVwcyBzaGFyZHMgYmFsYW5jZWQgaW4gZmlsZSBjb3VudCBhbmQgbWFrZXMKLy8vIHRoZSBwYXJ0aXRpb24gc3RhYmxlIGZvciBhIGdpdmVuIHRlc3Qgc3VpdGUuCkxpc3Q8U3RyaW5nPiBzaGFyZE9mKAogIExpc3Q8U3RyaW5nPiBwYXRocywgewogIHJlcXVpcmVkIGludD8gc2hhcmRJbmRleCwKICByZXF1aXJlZCBpbnQ/IHRvdGFsU2hhcmRzLAp9KSB7CiAgaWYgKHNoYXJkSW5kZXggPT0gbnVsbCB8fCB0b3RhbFNoYXJkcyA9PSBudWxsKSByZXR1cm4gcGF0aHM7CgogIHJldHVybiBbCiAgICBmb3IgKHZhciBpID0gc2hhcmRJbmRleCAtIDE7IGkgPCBwYXRocy5sZW5ndGg7IGkgKz0gdG90YWxTaGFyZHMpIHBhdGhzW2ldLAogIF07Cn0KCmV4dGVuc2lvbiBvbiBGaWxlU3lzdGVtRW50aXR5IHsKICBib29sIGdldCBpc1Rlc3QgewogICAgcmV0dXJuIHRoaXMgaXMgRmlsZSAmJiBwYXRoLmJhc2VuYW1lKHRoaXMucGF0aCkuZW5kc1dpdGgoJ190ZXN0LmRhcnQnKTsKICB9Cn0KCkZ1dHVyZTxMaXN0PFN0cmluZz4+IGdldE5vdE9wdGltaXplZFRlc3RzKAogIEl0ZXJhYmxlPEZpbGVTeXN0ZW1FbnRpdHk+IHRlc3RzLAogIFN0cmluZyB0ZXN0RGlyLAopIGFzeW5jIHsKICBmaW5hbCBwYXRocyA9IHRlc3RzLm1hcCgoZSkgPT4gZS5wYXRoKS50b0xpc3QoKTsKICBmaW5hbCBmb3JtYXR0ZWRQYXRocyA9IHBhdGhzLm1hcCgoZSkgPT4gZS5yZXBsYWNlQWxsKCcvLi8nLCAnLycpKS50b0xpc3QoKTsKCiAgZmluYWwgZmlsZUZ1dHVyZXMgPSBmb3JtYXR0ZWRQYXRocy5tYXAoX2NoZWNrRmlsZUZvclNraXBWZXJ5R29vZE9wdGltaXphdGlvbik7CiAgZmluYWwgZmlsZVJlc3VsdHMgPSBhd2FpdCBGdXR1cmUud2FpdChmaWxlRnV0dXJlcyk7CgogIGZpbmFsIHRlc3RXaXRoVmVyeUdvb2RUZXN0ID0gPFN0cmluZz5bXTsKICBmb3IgKHZhciBpID0gMDsgaSA8IGZvcm1hdHRlZFBhdGhzLmxlbmd0aDsgaSsrKSB7CiAgICBpZiAoZmlsZVJlc3VsdHNbaV0pIHsKICAgICAgdGVzdFdpdGhWZXJ5R29vZFRlc3QuYWRkKGZvcm1hdHRlZFBhdGhzW2ldKTsKICAgIH0KICB9CgogIC8vLyBGb3JtYXQgdG8gcmVsYXRpdmUgcGF0aCwgbm9ybWFsaXppbmcgc2VwYXJhdG9ycyBzbyB0aGUgcGF0aHMgY29tcGFyZQogIC8vLyBlcXVhbCB0byB0aGUgb25lcyBidWlsdCBpbiBbcnVuXSBvbiBXaW5kb3dzIHRvby4KICBmaW5hbCByZWxhdGl2ZVBhdGhzID0gdGVzdFdpdGhWZXJ5R29vZFRlc3QKICAgICAgLm1hcCgoZSkgPT4gcGF0aC5yZWxhdGl2ZShlLCBmcm9tOiB0ZXN0RGlyKS5yZXBsYWNlQWxsKHInXCcsICcvJykpCiAgICAgIC50b0xpc3QoKTsKCiAgcmV0dXJuIHJlbGF0aXZlUGF0aHM7Cn0KCi8vLyBDaGVjayBpZiBhIHNpbmdsZSBmaWxlIGNvbnRhaW5zIHNraXBfdmVyeV9nb29kX29wdGltaXphdGlvbiB0YWcKRnV0dXJlPGJvb2w+IF9jaGVja0ZpbGVGb3JTa2lwVmVyeUdvb2RPcHRpbWl6YXRpb24oU3RyaW5nIHBhdGgpIGFzeW5jIHsKICBmaW5hbCBmaWxlID0gRmlsZShwYXRoKTsKICBpZiAoIWZpbGUuZXhpc3RzU3luYygpKSByZXR1cm4gZmFsc2U7CiAgZmluYWwgY29udGVudCA9IGF3YWl0IGZpbGUucmVhZEFzU3RyaW5nKCk7CiAgcmV0dXJuIHNraXBWZXJ5R29vZE9wdGltaXphdGlvblJlZ0V4cC5oYXNNYXRjaChjb250ZW50KTsKfQo=", "type": "text", }, { @@ -40,7 +40,7 @@ final testOptimizerBundle = MasonBundle.fromJson({ }, { "path": "test/pre_gen_test.dart", - "data": "aW1wb3J0ICdkYXJ0OmlvJzsKaW1wb3J0ICdkYXJ0Om1hdGgnOwoKaW1wb3J0ICdwYWNrYWdlOmhvb2tzL3ByZV9nZW4uZGFydCcgYXMgcHJlX2dlbjsKaW1wb3J0ICdwYWNrYWdlOm1hc29uL21hc29uLmRhcnQnOwppbXBvcnQgJ3BhY2thZ2U6bW9ja3RhaWwvbW9ja3RhaWwuZGFydCc7CmltcG9ydCAncGFja2FnZTpwYXRoL3BhdGguZGFydCcgYXMgcGF0aDsKaW1wb3J0ICdwYWNrYWdlOnRlc3QvdGVzdC5kYXJ0JzsKCmNsYXNzIF9Nb2NrTG9nZ2VyIGV4dGVuZHMgTW9jayBpbXBsZW1lbnRzIExvZ2dlciB7fQoKY2xhc3MgX0Zha2VDb250ZXh0IGV4dGVuZHMgRmFrZSBpbXBsZW1lbnRzIEhvb2tDb250ZXh0IHsKICBAb3ZlcnJpZGUKICBmaW5hbCBsb2dnZXIgPSBfTW9ja0xvZ2dlcigpOwoKICBAb3ZlcnJpZGUKICBNYXA8U3RyaW5nLCBPYmplY3Q/PiB2YXJzID0ge307Cn0KCmZpbmFsIG5vdE9wdGltaXplZFRlc3RDb250ZW50ID0KICAgICcnJwpAVGFncyhbJyR7cHJlX2dlbi5za2lwVmVyeUdvb2RPcHRpbWl6YXRpb25UYWd9J10pCnZvaWQgbWFpbigpIHsKICB0ZXN0KCd0ZXN0JywgKCkgewogICAgZXhwZWN0KDEsIDEpOwogIH0pOwp9CicnJzsKCmZpbmFsIGFub3RoZXJOb3RPcHRpbWl6ZWRUZXN0Q29udGVudCA9CiAgICAnJycKQFRhZ3MoWycke3ByZV9nZW4uc2tpcFZlcnlHb29kT3B0aW1pemF0aW9uVGFnfScsICdhbm90aGVyX3RhZyddKQp2b2lkIG1haW4oKSB7CiAgdGVzdCgnYW5vdGhlciB0ZXN0JywgKCkgewogICAgZXhwZWN0KDEsIDEpOwogIH0pOwp9CicnJzsKCnZvaWQgbWFpbigpIHsKICBsYXRlIERpcmVjdG9yeSB0ZW1wRGlyZWN0b3J5OwoKICBzZXRVcCgoKSB7CiAgICB0ZW1wRGlyZWN0b3J5ID0gRGlyZWN0b3J5LnN5c3RlbVRlbXAuY3JlYXRlVGVtcFN5bmMoJ3Rlc3Rfb3B0aW1pemVyJyk7CiAgfSk7CgogIHRlYXJEb3duKCgpIHsKICAgIHRlbXBEaXJlY3RvcnkuZGVsZXRlU3luYyhyZWN1cnNpdmU6IHRydWUpOwogIH0pOwoKICBncm91cCgnUHJlIGdlbiBob29rJywgKCkgewogICAgbGF0ZSBIb29rQ29udGV4dCBjb250ZXh0OwoKICAgIHNldFVwKCgpIHsKICAgICAgY29udGV4dCA9IF9GYWtlQ29udGV4dCgpOwogICAgfSk7CgogICAgZ3JvdXAoJ0NvbXBsZXRlcycsICgpIHsKICAgICAgdGVzdCgnd2l0aCB0ZXN0IGZpbGVzIGxpc3QnLCAoKSBhc3luYyB7CiAgICAgICAgRmlsZShwYXRoLmpvaW4odGVtcERpcmVjdG9yeS5wYXRoLCAncHVic3BlYy55YW1sJykpLmNyZWF0ZVN5bmMoKTsKCiAgICAgICAgZmluYWwgdGVzdERpciA9IERpcmVjdG9yeShwYXRoLmpvaW4odGVtcERpcmVjdG9yeS5wYXRoLCAndGVzdCcpKQogICAgICAgICAgLi5jcmVhdGVTeW5jKCk7CiAgICAgICAgRmlsZShwYXRoLmpvaW4odGVzdERpci5wYXRoLCAndGVzdDFfdGVzdC5kYXJ0JykpLmNyZWF0ZVN5bmMoKTsKICAgICAgICBGaWxlKHBhdGguam9pbih0ZXN0RGlyLnBhdGgsICd0ZXN0Ml90ZXN0LmRhcnQnKSkuY3JlYXRlU3luYygpOwogICAgICAgIEZpbGUocGF0aC5qb2luKHRlc3REaXIucGF0aCwgJ25vX3Rlc3RfaGVyZS5kYXJ0JykpLmNyZWF0ZVN5bmMoKTsKCiAgICAgICAgY29udGV4dC52YXJzWydwYWNrYWdlLXJvb3QnXSA9IHRlbXBEaXJlY3RvcnkuYWJzb2x1dGUucGF0aDsKCiAgICAgICAgYXdhaXQgcHJlX2dlbi5ydW4oY29udGV4dCk7CgogICAgICAgIGZpbmFsIHRlc3RzID0gY29udGV4dC52YXJzWyd0ZXN0cyddIGFzIExpc3Q8TWFwPFN0cmluZywgU3RyaW5nPj47CiAgICAgICAgZmluYWwgdGVzdHNNYXAgPSA8U3RyaW5nLCBTdHJpbmc+e307CiAgICAgICAgZm9yIChmaW5hbCB0ZXN0IGluIHRlc3RzKSB7CiAgICAgICAgICBmaW5hbCBwYXRoID0gdGVzdFsncGF0aCddITsKICAgICAgICAgIGZpbmFsIGlkZW50aWZpZXIgPSB0ZXN0WydpZGVudGlmaWVyJ10hOwogICAgICAgICAgdGVzdHNNYXBbcGF0aF0gPSBpZGVudGlmaWVyOwogICAgICAgIH0KCiAgICAgICAgZmluYWwgcGF0aHMgPSB0ZXN0c01hcC5rZXlzOwogICAgICAgIGV4cGVjdChwYXRocywgY29udGFpbnMoJ3Rlc3QxX3Rlc3QuZGFydCcpKTsKICAgICAgICBleHBlY3QocGF0aHMsIGNvbnRhaW5zKCd0ZXN0Ml90ZXN0LmRhcnQnKSk7CiAgICAgICAgZXhwZWN0KHBhdGhzLCBpc05vdChjb250YWlucygnbm9fdGVzdF9oZXJlLmRhcnQnKSkpOwoKICAgICAgICBleHBlY3QoCiAgICAgICAgICB0ZXN0c01hcC52YWx1ZXMudG9TZXQoKS5sZW5ndGgsCiAgICAgICAgICBlcXVhbHModGVzdHMubGVuZ3RoKSwKICAgICAgICAgIHJlYXNvbjogJ0FsbCB0ZXN0cyBmaWxlcyBzaG91bGQgaGF2ZSB1bmlxdWUgaWRlbnRpZmllcnMnLAogICAgICAgICk7CgogICAgICAgIGV4cGVjdChjb250ZXh0LnZhcnNbJ2lzRmx1dHRlciddLCBmYWxzZSk7CiAgICAgIH0pOwoKICAgICAgdGVzdCgnd2l0aCBwcm9wZXIgaXNGbHV0dGVyIGlkZW50aWZpY2F0aW9uJywgKCkgYXN5bmMgewogICAgICAgIEZpbGUocGF0aC5qb2luKHRlbXBEaXJlY3RvcnkucGF0aCwgJ3B1YnNwZWMueWFtbCcpKQogICAgICAgICAgLi5jcmVhdGVTeW5jKCkKICAgICAgICAgIC4ud3JpdGVBc1N0cmluZ1N5bmMoJycnCmRlcGVuZGVuY2llczoKICBmbHV0dGVyOgogICAgc2RrOiBmbHV0dGVyJycnKTsKCiAgICAgICAgRGlyZWN0b3J5KHBhdGguam9pbih0ZW1wRGlyZWN0b3J5LnBhdGgsICd0ZXN0JykpLmNyZWF0ZVN5bmMoKTsKCiAgICAgICAgY29udGV4dC52YXJzWydwYWNrYWdlLXJvb3QnXSA9IHRlbXBEaXJlY3RvcnkuYWJzb2x1dGUucGF0aDsKCiAgICAgICAgYXdhaXQgcHJlX2dlbi5ydW4oY29udGV4dCk7CgogICAgICAgIGV4cGVjdChjb250ZXh0LnZhcnNbJ2lzRmx1dHRlciddLCB0cnVlKTsKICAgICAgfSk7CgogICAgICB0ZXN0KCd3aXRoIHByb3BlciBub3Qgb3B0aW1pemVkIHRlc3RzIGlkZW50aWZpY2F0aW9uJywgKCkgYXN5bmMgewogICAgICAgIEZpbGUocGF0aC5qb2luKHRlbXBEaXJlY3RvcnkucGF0aCwgJ3B1YnNwZWMueWFtbCcpKS5jcmVhdGVTeW5jKCk7CgogICAgICAgIGZpbmFsIHRlc3REaXIgPSBEaXJlY3RvcnkocGF0aC5qb2luKHRlbXBEaXJlY3RvcnkucGF0aCwgJ3Rlc3QnKSkKICAgICAgICAgIC4uY3JlYXRlU3luYygpOwogICAgICAgIEZpbGUocGF0aC5qb2luKHRlc3REaXIucGF0aCwgJ3Rlc3QxX3Rlc3QuZGFydCcpKS5jcmVhdGVTeW5jKCk7CiAgICAgICAgRmlsZShwYXRoLmpvaW4odGVzdERpci5wYXRoLCAndGVzdDJfdGVzdC5kYXJ0JykpLmNyZWF0ZVN5bmMoKTsKICAgICAgICBGaWxlKHBhdGguam9pbih0ZXN0RGlyLnBhdGgsICdub190ZXN0X2hlcmUuZGFydCcpKS5jcmVhdGVTeW5jKCk7CiAgICAgICAgRmlsZSgKICAgICAgICAgIHBhdGguam9pbih0ZXN0RGlyLnBhdGgsICdub3Rfb3B0aW1pemVkX3Rlc3QuZGFydCcpLAogICAgICAgICkud3JpdGVBc1N0cmluZ1N5bmMobm90T3B0aW1pemVkVGVzdENvbnRlbnQpOwogICAgICAgIEZpbGUoCiAgICAgICAgICBwYXRoLmpvaW4odGVzdERpci5wYXRoLCAnYW5vdGhlcl9ub3Rfb3B0aW1pemVkX3Rlc3QuZGFydCcpLAogICAgICAgICkud3JpdGVBc1N0cmluZ1N5bmMoYW5vdGhlck5vdE9wdGltaXplZFRlc3RDb250ZW50KTsKCiAgICAgICAgY29udGV4dC52YXJzWydwYWNrYWdlLXJvb3QnXSA9IHRlbXBEaXJlY3RvcnkuYWJzb2x1dGUucGF0aDsKCiAgICAgICAgYXdhaXQgcHJlX2dlbi5ydW4oY29udGV4dCk7CgogICAgICAgIGZpbmFsIHRlc3RzID0gY29udGV4dC52YXJzWyd0ZXN0cyddIGFzIExpc3Q8TWFwPFN0cmluZywgU3RyaW5nPj47CiAgICAgICAgZmluYWwgdGVzdHNNYXAgPSA8U3RyaW5nLCBTdHJpbmc+e307CiAgICAgICAgZm9yIChmaW5hbCB0ZXN0IGluIHRlc3RzKSB7CiAgICAgICAgICBmaW5hbCBwYXRoID0gdGVzdFsncGF0aCddITsKICAgICAgICAgIGZpbmFsIGlkZW50aWZpZXIgPSB0ZXN0WydpZGVudGlmaWVyJ10hOwogICAgICAgICAgdGVzdHNNYXBbcGF0aF0gPSBpZGVudGlmaWVyOwogICAgICAgIH0KCiAgICAgICAgZmluYWwgcGF0aHMgPSB0ZXN0c01hcC5rZXlzOwogICAgICAgIGV4cGVjdChwYXRocywgY29udGFpbnMoJ3Rlc3QxX3Rlc3QuZGFydCcpKTsKICAgICAgICBleHBlY3QocGF0aHMsIGNvbnRhaW5zKCd0ZXN0Ml90ZXN0LmRhcnQnKSk7CiAgICAgICAgZXhwZWN0KHBhdGhzLCBpc05vdChjb250YWlucygnbm9fdGVzdF9oZXJlLmRhcnQnKSkpOwogICAgICAgIGV4cGVjdChwYXRocywgaXNOb3QoY29udGFpbnMoJ25vdF9vcHRpbWl6ZWRfdGVzdC5kYXJ0JykpKTsKICAgICAgICBleHBlY3QocGF0aHMsIGlzTm90KGNvbnRhaW5zKCdhbm90aGVyX25vdF9vcHRpbWl6ZWRfdGVzdC5kYXJ0JykpKTsKCiAgICAgICAgZXhwZWN0KAogICAgICAgICAgdGVzdHNNYXAudmFsdWVzLnRvU2V0KCkubGVuZ3RoLAogICAgICAgICAgZXF1YWxzKHRlc3RzLmxlbmd0aCksCiAgICAgICAgICByZWFzb246ICdBbGwgdGVzdHMgZmlsZXMgc2hvdWxkIGhhdmUgdW5pcXVlIGlkZW50aWZpZXJzJywKICAgICAgICApOwogICAgICAgIGZpbmFsIG5vdE9wdGltaXplZFRlc3RzID0KICAgICAgICAgICAgY29udGV4dC52YXJzWydub3RPcHRpbWl6ZWRUZXN0cyddIGFzIExpc3Q8U3RyaW5nPjsKICAgICAgICBleHBlY3QoCiAgICAgICAgICBub3RPcHRpbWl6ZWRUZXN0cywKICAgICAgICAgIGNvbnRhaW5zKCdub3Rfb3B0aW1pemVkX3Rlc3QuZGFydCcpLAogICAgICAgICk7CiAgICAgICAgZXhwZWN0KAogICAgICAgICAgbm90T3B0aW1pemVkVGVzdHMsCiAgICAgICAgICBjb250YWlucygnYW5vdGhlcl9ub3Rfb3B0aW1pemVkX3Rlc3QuZGFydCcpLAogICAgICAgICk7CiAgICAgIH0pOwogICAgfSk7CgogICAgZ3JvdXAoJ0ZhaWxzJywgKCkgewogICAgICBzZXRVcCgoKSB7CiAgICAgICAgcHJlX2dlbi5leGl0Rm4gPSAoY29kZSkgewogICAgICAgICAgdGhyb3cgUHJvY2Vzc0V4Y2VwdGlvbignZXhpdCcsIFtjb2RlLnRvU3RyaW5nKCldKTsKICAgICAgICB9OwogICAgICB9KTsKCiAgICAgIHRlYXJEb3duKCgpIHsKICAgICAgICBwcmVfZ2VuLmV4aXRGbiA9IGV4aXQ7CiAgICAgIH0pOwoKICAgICAgdGVzdCgnd2hlbiB0YXJnZXQgdGVzdCBkaXIgZG9lcyBub3QgZXhpc3QnLCAoKSBhc3luYyB7CiAgICAgICAgRmlsZShwYXRoLmpvaW4odGVtcERpcmVjdG9yeS5wYXRoLCAncHVic3BlYy55YW1sJykpLmNyZWF0ZVN5bmMoKTsKCiAgICAgICAgZmluYWwgdGVzdERpciA9IERpcmVjdG9yeShwYXRoLmpvaW4odGVtcERpcmVjdG9yeS5wYXRoLCAndGVzdCcpKTsKCiAgICAgICAgY29udGV4dC52YXJzWydwYWNrYWdlLXJvb3QnXSA9IHRlbXBEaXJlY3RvcnkuYWJzb2x1dGUucGF0aDsKCiAgICAgICAgYXdhaXQgZXhwZWN0TGF0ZXIoCiAgICAgICAgICAoKSA9PiBwcmVfZ2VuLnJ1bihjb250ZXh0KSwKICAgICAgICAgIHRocm93c0EoCiAgICAgICAgICAgIGlzQTxQcm9jZXNzRXhjZXB0aW9uPigpLmhhdmluZygKICAgICAgICAgICAgICAoZXgpID0+IGV4LmFyZ3VtZW50cy5maXJzdCwKICAgICAgICAgICAgICAnZXJyb3IgY29kZScsCiAgICAgICAgICAgICAgZXF1YWxzKCcxJyksCiAgICAgICAgICAgICksCiAgICAgICAgICApLAogICAgICAgICk7CgogICAgICAgIHZlcmlmeSgKICAgICAgICAgICgpID0+IGNvbnRleHQubG9nZ2VyLmVycignQ291bGQgbm90IGZpbmQgZGlyZWN0b3J5ICR7dGVzdERpci5wYXRofScpLAogICAgICAgICkuY2FsbGVkKDEpOwoKICAgICAgICBleHBlY3QoY29udGV4dC52YXJzWyd0ZXN0cyddLCBpc051bGwpOwogICAgICAgIGV4cGVjdChjb250ZXh0LnZhcnNbJ2lzRmx1dHRlciddLCBpc051bGwpOwogICAgICB9KTsKCiAgICAgIHRlc3QoJ3doZW4gdGFyZ2V0IGRpciBkb2VzIG5vdCBjb250YWluIGEgcHVic3BlYy55YW1sJywgKCkgYXN5bmMgewogICAgICAgIGZpbmFsIHRlc3REaXIgPSBEaXJlY3RvcnkocGF0aC5qb2luKHRlbXBEaXJlY3RvcnkucGF0aCwgJ3Rlc3QnKSkKICAgICAgICAgIC4uY3JlYXRlU3luYygpOwogICAgICAgIEZpbGUocGF0aC5qb2luKHRlc3REaXIucGF0aCwgJ3Rlc3QxX3Rlc3QuZGFydCcpKS5jcmVhdGVTeW5jKCk7CiAgICAgICAgRmlsZShwYXRoLmpvaW4odGVzdERpci5wYXRoLCAndGVzdDJfdGVzdC5kYXJ0JykpLmNyZWF0ZVN5bmMoKTsKICAgICAgICBGaWxlKHBhdGguam9pbih0ZXN0RGlyLnBhdGgsICdub190ZXN0X2hlcmUuZGFydCcpKS5jcmVhdGVTeW5jKCk7CgogICAgICAgIGNvbnRleHQudmFyc1sncGFja2FnZS1yb290J10gPSB0ZW1wRGlyZWN0b3J5LmFic29sdXRlLnBhdGg7CgogICAgICAgIGF3YWl0IGV4cGVjdExhdGVyKAogICAgICAgICAgKCkgPT4gcHJlX2dlbi5ydW4oY29udGV4dCksCiAgICAgICAgICB0aHJvd3NBKAogICAgICAgICAgICBpc0E8UHJvY2Vzc0V4Y2VwdGlvbj4oKS5oYXZpbmcoCiAgICAgICAgICAgICAgKGV4KSA9PiBleC5hcmd1bWVudHMuZmlyc3QsCiAgICAgICAgICAgICAgJ2Vycm9yIGNvZGUnLAogICAgICAgICAgICAgIGVxdWFscygnMScpLAogICAgICAgICAgICApLAogICAgICAgICAgKSwKICAgICAgICApOwoKICAgICAgICB2ZXJpZnkoCiAgICAgICAgICAoKSA9PiBjb250ZXh0LmxvZ2dlci5lcnIoCiAgICAgICAgICAgICdDb3VsZCBub3QgZmluZCBwdWJzcGVjLnlhbWwgYXQgJHt0ZXN0RGlyLnBhdGh9JywKICAgICAgICAgICksCiAgICAgICAgKS5jYWxsZWQoMSk7CgogICAgICAgIGV4cGVjdChjb250ZXh0LnZhcnNbJ3Rlc3RzJ10sIGlzTnVsbCk7CiAgICAgICAgZXhwZWN0KGNvbnRleHQudmFyc1snaXNGbHV0dGVyJ10sIGlzTnVsbCk7CiAgICAgIH0pOwogICAgfSk7CgogICAgZ3JvdXAoJ3NraXBWZXJ5R29vZE9wdGltaXphdGlvblJlZ0V4cCByZWdleCcsICgpIHsKICAgICAgZmluYWwgcmVnZXggPSBwcmVfZ2VuLnNraXBWZXJ5R29vZE9wdGltaXphdGlvblJlZ0V4cDsKICAgICAgdGVzdCgnbWF0Y2hlcyBzaW5nbGUtbGluZSB0YWcnLCAoKSB7CiAgICAgICAgZmluYWwgY29udGVudCA9ICJAVGFncyhbJyR7cHJlX2dlbi5za2lwVmVyeUdvb2RPcHRpbWl6YXRpb25UYWd9J10pIjsKICAgICAgICBleHBlY3QocmVnZXguaGFzTWF0Y2goY29udGVudCksIGlzVHJ1ZSk7CiAgICAgIH0pOwoKICAgICAgdGVzdCgnbWF0Y2hlcyBzaW5nbGUtbGluZSB3aXRoIG11bHRpcGxlIHRhZ3MnLCAoKSB7CiAgICAgICAgZmluYWwgY29udGVudCA9CiAgICAgICAgICAgICJAVGFncyhbJyR7cHJlX2dlbi5za2lwVmVyeUdvb2RPcHRpbWl6YXRpb25UYWd9JywgJ2Nocm9tZSddKSI7CiAgICAgICAgZXhwZWN0KHJlZ2V4Lmhhc01hdGNoKGNvbnRlbnQpLCBpc1RydWUpOwogICAgICB9KTsKCiAgICAgIHRlc3QoJ21hdGNoZXMgbXVsdGktbGluZSB0YWcgbGlzdCcsICgpIHsKICAgICAgICBmaW5hbCBjb250ZW50ID0KICAgICAgICAgICAgJycnCiAgICAgIEBUYWdzKFsKICAgICAgICAnJHtwcmVfZ2VuLnNraXBWZXJ5R29vZE9wdGltaXphdGlvblRhZ30nLAogICAgICAgICdjaHJvbWUnLAogICAgICAgICd0ZXN0JywKICAgICAgXSkKICAgICAgJycnOwogICAgICAgIGV4cGVjdChyZWdleC5oYXNNYXRjaChjb250ZW50KSwgaXNUcnVlKTsKICAgICAgfSk7CgogICAgICB0ZXN0KCdtYXRjaGVzIG11bHRpLWxpbmUgd2hlcmUgdGFnIGlzIG5vdCB0aGUgZmlyc3QnLCAoKSB7CiAgICAgICAgZmluYWwgY29udGVudCA9CiAgICAgICAgICAgICcnJwogICAgICBAVGFncyhbCiAgICAgICAgJ2Nocm9tZScsCiAgICAgICAgJyR7cHJlX2dlbi5za2lwVmVyeUdvb2RPcHRpbWl6YXRpb25UYWd9JywKICAgICAgICAndGVzdCcsCiAgICAgIF0pCiAgICAgICcnJzsKICAgICAgICBleHBlY3QocmVnZXguaGFzTWF0Y2goY29udGVudCksIGlzVHJ1ZSk7CiAgICAgIH0pOwoKICAgICAgdGVzdCgnZG9lcyBub3QgbWF0Y2ggd2hlbiB0YWcgbWlzc2luZycsICgpIHsKICAgICAgICBjb25zdCBjb250ZW50ID0gIkBUYWdzKFsnY2hyb21lJywgJ3Rlc3QnXSkiOwogICAgICAgIGV4cGVjdChyZWdleC5oYXNNYXRjaChjb250ZW50KSwgaXNGYWxzZSk7CiAgICAgIH0pOwoKICAgICAgdGVzdCgKICAgICAgICAnZG9lcyBub3QgbWF0Y2ggc3Vic3RyaW5nIG9ubHkgKGUuZy4gc2tpcF92ZXJ5X2dvb2Rfb3B0aW1pemF0aW9uLHRlc3QpJywKICAgICAgICAoKSB7CiAgICAgICAgICBmaW5hbCBjb250ZW50ID0KICAgICAgICAgICAgICAnJycKICAgICAgQFRhZ3MoWwogICAgICAgICcke3ByZV9nZW4uc2tpcFZlcnlHb29kT3B0aW1pemF0aW9uVGFnfSx0ZXN0JywKICAgICAgICAnY2hyb21lJywKICAgICAgXSkKICAgICAgJycnOwogICAgICAgICAgZXhwZWN0KAogICAgICAgICAgICByZWdleC5oYXNNYXRjaChjb250ZW50KSwKICAgICAgICAgICAgaXNGYWxzZSwKICAgICAgICAgICk7IC8vIG9ubHkgZXhhY3QgdGFnIHNob3VsZCBtYXRjaAogICAgICAgIH0sCiAgICAgICk7CiAgICB9KTsKICAgIGdyb3VwKCdTaGFyZGluZycsICgpIHsKICAgICAgLy8vIENyZWF0ZXMgYSBwYWNrYWdlIHdpdGggW2NvdW50XSBvcHRpbWl6YWJsZSB0ZXN0IGZpbGVzLCBwbHVzIGFueQogICAgICAvLy8gW25vdE9wdGltaXplZF0gZmlsZXMgY2FycnlpbmcgdGhlIHNraXAgb3B0aW1pemF0aW9uIHRhZy4KICAgICAgRGlyZWN0b3J5IGNyZWF0ZVBhY2thZ2UoaW50IGNvdW50LCB7aW50IG5vdE9wdGltaXplZCA9IDB9KSB7CiAgICAgICAgRmlsZShwYXRoLmpvaW4odGVtcERpcmVjdG9yeS5wYXRoLCAncHVic3BlYy55YW1sJykpLmNyZWF0ZVN5bmMoKTsKICAgICAgICBmaW5hbCB0ZXN0RGlyID0gRGlyZWN0b3J5KHBhdGguam9pbih0ZW1wRGlyZWN0b3J5LnBhdGgsICd0ZXN0JykpCiAgICAgICAgICAuLmNyZWF0ZVN5bmMoKTsKICAgICAgICBmb3IgKHZhciBpID0gMDsgaSA8IGNvdW50OyBpKyspIHsKICAgICAgICAgIEZpbGUocGF0aC5qb2luKHRlc3REaXIucGF0aCwgJ3Rlc3Qke2l9X3Rlc3QuZGFydCcpKS5jcmVhdGVTeW5jKCk7CiAgICAgICAgfQogICAgICAgIGZvciAodmFyIGkgPSAwOyBpIDwgbm90T3B0aW1pemVkOyBpKyspIHsKICAgICAgICAgIEZpbGUoCiAgICAgICAgICAgIHBhdGguam9pbih0ZXN0RGlyLnBhdGgsICdza2lwJHtpfV90ZXN0LmRhcnQnKSwKICAgICAgICAgICkud3JpdGVBc1N0cmluZ1N5bmMobm90T3B0aW1pemVkVGVzdENvbnRlbnQpOwogICAgICAgIH0KICAgICAgICByZXR1cm4gdGVzdERpcjsKICAgICAgfQoKICAgICAgTGlzdDxTdHJpbmc+IHBhdGhzT2YoSG9va0NvbnRleHQgY29udGV4dCkgewogICAgICAgIGZpbmFsIHRlc3RzID0gY29udGV4dC52YXJzWyd0ZXN0cyddIGFzIExpc3Q8TWFwPFN0cmluZywgU3RyaW5nPj47CiAgICAgICAgcmV0dXJuIHRlc3RzLm1hcCgoZSkgPT4gZVsncGF0aCddISkudG9MaXN0KCk7CiAgICAgIH0KCiAgICAgIEZ1dHVyZTxMaXN0PFN0cmluZz4+IHJ1blNoYXJkKGludCBpbmRleCwgaW50IHRvdGFsKSBhc3luYyB7CiAgICAgICAgZmluYWwgY29udGV4dCA9IF9GYWtlQ29udGV4dCgpCiAgICAgICAgICAuLnZhcnNbJ3BhY2thZ2Utcm9vdCddID0gdGVtcERpcmVjdG9yeS5hYnNvbHV0ZS5wYXRoCiAgICAgICAgICAuLnZhcnNbJ3NoYXJkLWluZGV4J10gPSBpbmRleAogICAgICAgICAgLi52YXJzWyd0b3RhbC1zaGFyZHMnXSA9IHRvdGFsOwogICAgICAgIGF3YWl0IHByZV9nZW4ucnVuKGNvbnRleHQpOwogICAgICAgIHJldHVybiBbCiAgICAgICAgICAuLi5wYXRoc09mKGNvbnRleHQpLAogICAgICAgICAgLi4uKGNvbnRleHQudmFyc1snbm90T3B0aW1pemVkVGVzdHMnXSEgYXMgTGlzdCkuY2FzdDxTdHJpbmc+KCksCiAgICAgICAgXTsKICAgICAgfQoKICAgICAgdGVzdCgncnVucyBldmVyeSB0ZXN0IGV4YWN0bHkgb25jZSBhY3Jvc3MgYWxsIHNoYXJkcycsICgpIGFzeW5jIHsKICAgICAgICBjcmVhdGVQYWNrYWdlKDcsIG5vdE9wdGltaXplZDogMik7CgogICAgICAgIGZpbmFsIHNoYXJkcyA9IFsKICAgICAgICAgIGZvciAodmFyIGkgPSAxOyBpIDw9IDM7IGkrKykgYXdhaXQgcnVuU2hhcmQoaSwgMyksCiAgICAgICAgXTsKICAgICAgICBmaW5hbCB1bmlvbiA9IHNoYXJkcy5leHBhbmQoKHNoYXJkKSA9PiBzaGFyZCkudG9MaXN0KCk7CgogICAgICAgIGV4cGVjdCgKICAgICAgICAgIHVuaW9uLi5zb3J0KCksCiAgICAgICAgICBbCiAgICAgICAgICAgIGZvciAodmFyIGkgPSAwOyBpIDwgNzsgaSsrKSAndGVzdCR7aX1fdGVzdC5kYXJ0JywKICAgICAgICAgICAgZm9yICh2YXIgaSA9IDA7IGkgPCAyOyBpKyspICdza2lwJHtpfV90ZXN0LmRhcnQnLAogICAgICAgICAgXS4uc29ydCgpLAogICAgICAgICAgcmVhc29uOiAnU2hhcmRzIG11c3QgYmUgYSBjb21wbGV0ZSBhbmQgZGlzam9pbnQgcGFydGl0aW9uJywKICAgICAgICApOwogICAgICB9KTsKCiAgICAgIHRlc3QoJ3NoYXJkcyBub24gb3B0aW1pemVkIHRlc3RzIGFzIHdlbGwnLCAoKSBhc3luYyB7CiAgICAgICAgY3JlYXRlUGFja2FnZSgwLCBub3RPcHRpbWl6ZWQ6IDQpOwoKICAgICAgICBmaW5hbCBmaXJzdCA9IGF3YWl0IHJ1blNoYXJkKDEsIDIpOwogICAgICAgIGZpbmFsIHNlY29uZCA9IGF3YWl0IHJ1blNoYXJkKDIsIDIpOwoKICAgICAgICBleHBlY3QoZmlyc3QsIFsnc2tpcDBfdGVzdC5kYXJ0JywgJ3NraXAyX3Rlc3QuZGFydCddKTsKICAgICAgICBleHBlY3Qoc2Vjb25kLCBbJ3NraXAxX3Rlc3QuZGFydCcsICdza2lwM190ZXN0LmRhcnQnXSk7CiAgICAgIH0pOwoKICAgICAgdGVzdCgnaXMgZGV0ZXJtaW5pc3RpYyBhY3Jvc3MgcnVucycsICgpIGFzeW5jIHsKICAgICAgICBjcmVhdGVQYWNrYWdlKDkpOwoKICAgICAgICBleHBlY3QoYXdhaXQgcnVuU2hhcmQoMiwgNCksIGF3YWl0IHJ1blNoYXJkKDIsIDQpKTsKICAgICAgfSk7CgogICAgICB0ZXN0KCdiYWxhbmNlcyBzaGFyZHMgd2l0aGluIG9uZSBmaWxlIG9mIGVhY2ggb3RoZXInLCAoKSBhc3luYyB7CiAgICAgICAgY3JlYXRlUGFja2FnZSgxMCk7CgogICAgICAgIGZpbmFsIHNpemVzID0gWwogICAgICAgICAgZm9yICh2YXIgaSA9IDE7IGkgPD0gNDsgaSsrKSAoYXdhaXQgcnVuU2hhcmQoaSwgNCkpLmxlbmd0aCwKICAgICAgICBdOwoKICAgICAgICBleHBlY3Qoc2l6ZXMucmVkdWNlKG1heCkgLSBzaXplcy5yZWR1Y2UobWluKSwgbGVzc1RoYW5PckVxdWFsVG8oMSkpOwogICAgICB9KTsKCiAgICAgIHRlc3QoCiAgICAgICAgJ3lpZWxkcyBhbiBlbXB0eSBzaGFyZCB3aGVuIHRoZXJlIGFyZSBtb3JlIHNoYXJkcyB0aGFuIHRlc3RzJywKICAgICAgICAoKSBhc3luYyB7CiAgICAgICAgICBjcmVhdGVQYWNrYWdlKDIpOwoKICAgICAgICAgIGV4cGVjdChhd2FpdCBydW5TaGFyZCgzLCAzKSwgaXNFbXB0eSk7CiAgICAgICAgfSwKICAgICAgKTsKCiAgICAgIHRlc3QoJ2luY2x1ZGVzIGV2ZXJ5IHRlc3Qgd2hlbiBzaGFyZGluZyBpcyBub3QgcmVxdWVzdGVkJywgKCkgYXN5bmMgewogICAgICAgIGNyZWF0ZVBhY2thZ2UoMyk7CgogICAgICAgIGZpbmFsIGNvbnRleHQgPSBfRmFrZUNvbnRleHQoKQogICAgICAgICAgLi52YXJzWydwYWNrYWdlLXJvb3QnXSA9IHRlbXBEaXJlY3RvcnkuYWJzb2x1dGUucGF0aDsKICAgICAgICBhd2FpdCBwcmVfZ2VuLnJ1bihjb250ZXh0KTsKCiAgICAgICAgZXhwZWN0KHBhdGhzT2YoY29udGV4dCksIGhhc0xlbmd0aCgzKSk7CiAgICAgIH0pOwogICAgfSk7CgogICAgZ3JvdXAoJ3NoYXJkT2YnLCAoKSB7CiAgICAgIGNvbnN0IHBhdGhzID0gWydhLmRhcnQnLCAnYi5kYXJ0JywgJ2MuZGFydCcsICdkLmRhcnQnLCAnZS5kYXJ0J107CgogICAgICB0ZXN0KCdyZXR1cm5zIHBhdGhzIHVudG91Y2hlZCB3aGVuIHNoYXJkIGluZGV4IGlzIG51bGwnLCAoKSB7CiAgICAgICAgZXhwZWN0KAogICAgICAgICAgcHJlX2dlbi5zaGFyZE9mKHBhdGhzLCBzaGFyZEluZGV4OiBudWxsLCB0b3RhbFNoYXJkczogMyksCiAgICAgICAgICBwYXRocywKICAgICAgICApOwogICAgICB9KTsKCiAgICAgIHRlc3QoJ3JldHVybnMgcGF0aHMgdW50b3VjaGVkIHdoZW4gdG90YWwgc2hhcmRzIGlzIG51bGwnLCAoKSB7CiAgICAgICAgZXhwZWN0KAogICAgICAgICAgcHJlX2dlbi5zaGFyZE9mKHBhdGhzLCBzaGFyZEluZGV4OiAxLCB0b3RhbFNoYXJkczogbnVsbCksCiAgICAgICAgICBwYXRocywKICAgICAgICApOwogICAgICB9KTsKCiAgICAgIHRlc3QoJ2RlYWxzIHBhdGhzIG91dCByb3VuZCByb2JpbicsICgpIHsKICAgICAgICBleHBlY3QoCiAgICAgICAgICBwcmVfZ2VuLnNoYXJkT2YocGF0aHMsIHNoYXJkSW5kZXg6IDEsIHRvdGFsU2hhcmRzOiAyKSwKICAgICAgICAgIFsnYS5kYXJ0JywgJ2MuZGFydCcsICdlLmRhcnQnXSwKICAgICAgICApOwogICAgICAgIGV4cGVjdCgKICAgICAgICAgIHByZV9nZW4uc2hhcmRPZihwYXRocywgc2hhcmRJbmRleDogMiwgdG90YWxTaGFyZHM6IDIpLAogICAgICAgICAgWydiLmRhcnQnLCAnZC5kYXJ0J10sCiAgICAgICAgKTsKICAgICAgfSk7CgogICAgICB0ZXN0KCdyZXR1cm5zIGV2ZXJ5dGhpbmcgZm9yIGEgc2luZ2xlIHNoYXJkJywgKCkgewogICAgICAgIGV4cGVjdChwcmVfZ2VuLnNoYXJkT2YocGF0aHMsIHNoYXJkSW5kZXg6IDEsIHRvdGFsU2hhcmRzOiAxKSwgcGF0aHMpOwogICAgICB9KTsKCiAgICAgIHRlc3QoJ3JldHVybnMgZW1wdHkgZm9yIGFuIG91dCBvZiByYW5nZSBzaGFyZCcsICgpIHsKICAgICAgICBleHBlY3QoCiAgICAgICAgICBwcmVfZ2VuLnNoYXJkT2YocGF0aHMsIHNoYXJkSW5kZXg6IDksIHRvdGFsU2hhcmRzOiA5KSwKICAgICAgICAgIGlzRW1wdHksCiAgICAgICAgKTsKICAgICAgfSk7CiAgICB9KTsKICB9KTsKfQo=", + "data": "aW1wb3J0ICdkYXJ0OmlvJzsKaW1wb3J0ICdkYXJ0Om1hdGgnOwoKaW1wb3J0ICdwYWNrYWdlOmhvb2tzL3ByZV9nZW4uZGFydCcgYXMgcHJlX2dlbjsKaW1wb3J0ICdwYWNrYWdlOm1hc29uL21hc29uLmRhcnQnOwppbXBvcnQgJ3BhY2thZ2U6bW9ja3RhaWwvbW9ja3RhaWwuZGFydCc7CmltcG9ydCAncGFja2FnZTpwYXRoL3BhdGguZGFydCcgYXMgcGF0aDsKaW1wb3J0ICdwYWNrYWdlOnRlc3QvdGVzdC5kYXJ0JzsKCmNsYXNzIF9Nb2NrTG9nZ2VyIGV4dGVuZHMgTW9jayBpbXBsZW1lbnRzIExvZ2dlciB7fQoKY2xhc3MgX0Zha2VDb250ZXh0IGV4dGVuZHMgRmFrZSBpbXBsZW1lbnRzIEhvb2tDb250ZXh0IHsKICBAb3ZlcnJpZGUKICBmaW5hbCBsb2dnZXIgPSBfTW9ja0xvZ2dlcigpOwoKICBAb3ZlcnJpZGUKICBNYXA8U3RyaW5nLCBPYmplY3Q/PiB2YXJzID0ge307Cn0KCmZpbmFsIG5vdE9wdGltaXplZFRlc3RDb250ZW50ID0KICAgICcnJwpAVGFncyhbJyR7cHJlX2dlbi5za2lwVmVyeUdvb2RPcHRpbWl6YXRpb25UYWd9J10pCnZvaWQgbWFpbigpIHsKICB0ZXN0KCd0ZXN0JywgKCkgewogICAgZXhwZWN0KDEsIDEpOwogIH0pOwp9CicnJzsKCmZpbmFsIGFub3RoZXJOb3RPcHRpbWl6ZWRUZXN0Q29udGVudCA9CiAgICAnJycKQFRhZ3MoWycke3ByZV9nZW4uc2tpcFZlcnlHb29kT3B0aW1pemF0aW9uVGFnfScsICdhbm90aGVyX3RhZyddKQp2b2lkIG1haW4oKSB7CiAgdGVzdCgnYW5vdGhlciB0ZXN0JywgKCkgewogICAgZXhwZWN0KDEsIDEpOwogIH0pOwp9CicnJzsKCnZvaWQgbWFpbigpIHsKICBsYXRlIERpcmVjdG9yeSB0ZW1wRGlyZWN0b3J5OwoKICBzZXRVcCgoKSB7CiAgICB0ZW1wRGlyZWN0b3J5ID0gRGlyZWN0b3J5LnN5c3RlbVRlbXAuY3JlYXRlVGVtcFN5bmMoJ3Rlc3Rfb3B0aW1pemVyJyk7CiAgfSk7CgogIHRlYXJEb3duKCgpIHsKICAgIHRlbXBEaXJlY3RvcnkuZGVsZXRlU3luYyhyZWN1cnNpdmU6IHRydWUpOwogIH0pOwoKICBncm91cCgnUHJlIGdlbiBob29rJywgKCkgewogICAgbGF0ZSBIb29rQ29udGV4dCBjb250ZXh0OwoKICAgIHNldFVwKCgpIHsKICAgICAgY29udGV4dCA9IF9GYWtlQ29udGV4dCgpOwogICAgfSk7CgogICAgZ3JvdXAoJ0NvbXBsZXRlcycsICgpIHsKICAgICAgdGVzdCgnd2l0aCB0ZXN0IGZpbGVzIGxpc3QnLCAoKSBhc3luYyB7CiAgICAgICAgRmlsZShwYXRoLmpvaW4odGVtcERpcmVjdG9yeS5wYXRoLCAncHVic3BlYy55YW1sJykpLmNyZWF0ZVN5bmMoKTsKCiAgICAgICAgZmluYWwgdGVzdERpciA9IERpcmVjdG9yeShwYXRoLmpvaW4odGVtcERpcmVjdG9yeS5wYXRoLCAndGVzdCcpKQogICAgICAgICAgLi5jcmVhdGVTeW5jKCk7CiAgICAgICAgRmlsZShwYXRoLmpvaW4odGVzdERpci5wYXRoLCAndGVzdDFfdGVzdC5kYXJ0JykpLmNyZWF0ZVN5bmMoKTsKICAgICAgICBGaWxlKHBhdGguam9pbih0ZXN0RGlyLnBhdGgsICd0ZXN0Ml90ZXN0LmRhcnQnKSkuY3JlYXRlU3luYygpOwogICAgICAgIEZpbGUocGF0aC5qb2luKHRlc3REaXIucGF0aCwgJ25vX3Rlc3RfaGVyZS5kYXJ0JykpLmNyZWF0ZVN5bmMoKTsKCiAgICAgICAgY29udGV4dC52YXJzWydwYWNrYWdlLXJvb3QnXSA9IHRlbXBEaXJlY3RvcnkuYWJzb2x1dGUucGF0aDsKCiAgICAgICAgYXdhaXQgcHJlX2dlbi5ydW4oY29udGV4dCk7CgogICAgICAgIGZpbmFsIHRlc3RzID0gY29udGV4dC52YXJzWyd0ZXN0cyddIGFzIExpc3Q8TWFwPFN0cmluZywgU3RyaW5nPj47CiAgICAgICAgZmluYWwgdGVzdHNNYXAgPSA8U3RyaW5nLCBTdHJpbmc+e307CiAgICAgICAgZm9yIChmaW5hbCB0ZXN0IGluIHRlc3RzKSB7CiAgICAgICAgICBmaW5hbCBwYXRoID0gdGVzdFsncGF0aCddITsKICAgICAgICAgIGZpbmFsIGlkZW50aWZpZXIgPSB0ZXN0WydpZGVudGlmaWVyJ10hOwogICAgICAgICAgdGVzdHNNYXBbcGF0aF0gPSBpZGVudGlmaWVyOwogICAgICAgIH0KCiAgICAgICAgZmluYWwgcGF0aHMgPSB0ZXN0c01hcC5rZXlzOwogICAgICAgIGV4cGVjdChwYXRocywgY29udGFpbnMoJ3Rlc3QxX3Rlc3QuZGFydCcpKTsKICAgICAgICBleHBlY3QocGF0aHMsIGNvbnRhaW5zKCd0ZXN0Ml90ZXN0LmRhcnQnKSk7CiAgICAgICAgZXhwZWN0KHBhdGhzLCBpc05vdChjb250YWlucygnbm9fdGVzdF9oZXJlLmRhcnQnKSkpOwoKICAgICAgICBleHBlY3QoCiAgICAgICAgICB0ZXN0c01hcC52YWx1ZXMudG9TZXQoKS5sZW5ndGgsCiAgICAgICAgICBlcXVhbHModGVzdHMubGVuZ3RoKSwKICAgICAgICAgIHJlYXNvbjogJ0FsbCB0ZXN0cyBmaWxlcyBzaG91bGQgaGF2ZSB1bmlxdWUgaWRlbnRpZmllcnMnLAogICAgICAgICk7CgogICAgICAgIGV4cGVjdChjb250ZXh0LnZhcnNbJ2lzRmx1dHRlciddLCBmYWxzZSk7CiAgICAgIH0pOwoKICAgICAgdGVzdCgnd2l0aCBwcm9wZXIgaXNGbHV0dGVyIGlkZW50aWZpY2F0aW9uJywgKCkgYXN5bmMgewogICAgICAgIEZpbGUocGF0aC5qb2luKHRlbXBEaXJlY3RvcnkucGF0aCwgJ3B1YnNwZWMueWFtbCcpKQogICAgICAgICAgLi5jcmVhdGVTeW5jKCkKICAgICAgICAgIC4ud3JpdGVBc1N0cmluZ1N5bmMoJycnCmRlcGVuZGVuY2llczoKICBmbHV0dGVyOgogICAgc2RrOiBmbHV0dGVyJycnKTsKCiAgICAgICAgRGlyZWN0b3J5KHBhdGguam9pbih0ZW1wRGlyZWN0b3J5LnBhdGgsICd0ZXN0JykpLmNyZWF0ZVN5bmMoKTsKCiAgICAgICAgY29udGV4dC52YXJzWydwYWNrYWdlLXJvb3QnXSA9IHRlbXBEaXJlY3RvcnkuYWJzb2x1dGUucGF0aDsKCiAgICAgICAgYXdhaXQgcHJlX2dlbi5ydW4oY29udGV4dCk7CgogICAgICAgIGV4cGVjdChjb250ZXh0LnZhcnNbJ2lzRmx1dHRlciddLCB0cnVlKTsKICAgICAgfSk7CgogICAgICB0ZXN0KCd3aXRoIHByb3BlciBub3Qgb3B0aW1pemVkIHRlc3RzIGlkZW50aWZpY2F0aW9uJywgKCkgYXN5bmMgewogICAgICAgIEZpbGUocGF0aC5qb2luKHRlbXBEaXJlY3RvcnkucGF0aCwgJ3B1YnNwZWMueWFtbCcpKS5jcmVhdGVTeW5jKCk7CgogICAgICAgIGZpbmFsIHRlc3REaXIgPSBEaXJlY3RvcnkocGF0aC5qb2luKHRlbXBEaXJlY3RvcnkucGF0aCwgJ3Rlc3QnKSkKICAgICAgICAgIC4uY3JlYXRlU3luYygpOwogICAgICAgIEZpbGUocGF0aC5qb2luKHRlc3REaXIucGF0aCwgJ3Rlc3QxX3Rlc3QuZGFydCcpKS5jcmVhdGVTeW5jKCk7CiAgICAgICAgRmlsZShwYXRoLmpvaW4odGVzdERpci5wYXRoLCAndGVzdDJfdGVzdC5kYXJ0JykpLmNyZWF0ZVN5bmMoKTsKICAgICAgICBGaWxlKHBhdGguam9pbih0ZXN0RGlyLnBhdGgsICdub190ZXN0X2hlcmUuZGFydCcpKS5jcmVhdGVTeW5jKCk7CiAgICAgICAgRmlsZSgKICAgICAgICAgIHBhdGguam9pbih0ZXN0RGlyLnBhdGgsICdub3Rfb3B0aW1pemVkX3Rlc3QuZGFydCcpLAogICAgICAgICkud3JpdGVBc1N0cmluZ1N5bmMobm90T3B0aW1pemVkVGVzdENvbnRlbnQpOwogICAgICAgIEZpbGUoCiAgICAgICAgICBwYXRoLmpvaW4odGVzdERpci5wYXRoLCAnYW5vdGhlcl9ub3Rfb3B0aW1pemVkX3Rlc3QuZGFydCcpLAogICAgICAgICkud3JpdGVBc1N0cmluZ1N5bmMoYW5vdGhlck5vdE9wdGltaXplZFRlc3RDb250ZW50KTsKCiAgICAgICAgY29udGV4dC52YXJzWydwYWNrYWdlLXJvb3QnXSA9IHRlbXBEaXJlY3RvcnkuYWJzb2x1dGUucGF0aDsKCiAgICAgICAgYXdhaXQgcHJlX2dlbi5ydW4oY29udGV4dCk7CgogICAgICAgIGZpbmFsIHRlc3RzID0gY29udGV4dC52YXJzWyd0ZXN0cyddIGFzIExpc3Q8TWFwPFN0cmluZywgU3RyaW5nPj47CiAgICAgICAgZmluYWwgdGVzdHNNYXAgPSA8U3RyaW5nLCBTdHJpbmc+e307CiAgICAgICAgZm9yIChmaW5hbCB0ZXN0IGluIHRlc3RzKSB7CiAgICAgICAgICBmaW5hbCBwYXRoID0gdGVzdFsncGF0aCddITsKICAgICAgICAgIGZpbmFsIGlkZW50aWZpZXIgPSB0ZXN0WydpZGVudGlmaWVyJ10hOwogICAgICAgICAgdGVzdHNNYXBbcGF0aF0gPSBpZGVudGlmaWVyOwogICAgICAgIH0KCiAgICAgICAgZmluYWwgcGF0aHMgPSB0ZXN0c01hcC5rZXlzOwogICAgICAgIGV4cGVjdChwYXRocywgY29udGFpbnMoJ3Rlc3QxX3Rlc3QuZGFydCcpKTsKICAgICAgICBleHBlY3QocGF0aHMsIGNvbnRhaW5zKCd0ZXN0Ml90ZXN0LmRhcnQnKSk7CiAgICAgICAgZXhwZWN0KHBhdGhzLCBpc05vdChjb250YWlucygnbm9fdGVzdF9oZXJlLmRhcnQnKSkpOwogICAgICAgIGV4cGVjdChwYXRocywgaXNOb3QoY29udGFpbnMoJ25vdF9vcHRpbWl6ZWRfdGVzdC5kYXJ0JykpKTsKICAgICAgICBleHBlY3QocGF0aHMsIGlzTm90KGNvbnRhaW5zKCdhbm90aGVyX25vdF9vcHRpbWl6ZWRfdGVzdC5kYXJ0JykpKTsKCiAgICAgICAgZXhwZWN0KAogICAgICAgICAgdGVzdHNNYXAudmFsdWVzLnRvU2V0KCkubGVuZ3RoLAogICAgICAgICAgZXF1YWxzKHRlc3RzLmxlbmd0aCksCiAgICAgICAgICByZWFzb246ICdBbGwgdGVzdHMgZmlsZXMgc2hvdWxkIGhhdmUgdW5pcXVlIGlkZW50aWZpZXJzJywKICAgICAgICApOwogICAgICAgIGZpbmFsIG5vdE9wdGltaXplZFRlc3RzID0KICAgICAgICAgICAgY29udGV4dC52YXJzWydub3RPcHRpbWl6ZWRUZXN0cyddIGFzIExpc3Q8U3RyaW5nPjsKICAgICAgICBleHBlY3QoCiAgICAgICAgICBub3RPcHRpbWl6ZWRUZXN0cywKICAgICAgICAgIGNvbnRhaW5zKCdub3Rfb3B0aW1pemVkX3Rlc3QuZGFydCcpLAogICAgICAgICk7CiAgICAgICAgZXhwZWN0KAogICAgICAgICAgbm90T3B0aW1pemVkVGVzdHMsCiAgICAgICAgICBjb250YWlucygnYW5vdGhlcl9ub3Rfb3B0aW1pemVkX3Rlc3QuZGFydCcpLAogICAgICAgICk7CiAgICAgIH0pOwogICAgfSk7CgogICAgZ3JvdXAoJ0ZhaWxzJywgKCkgewogICAgICBzZXRVcCgoKSB7CiAgICAgICAgcHJlX2dlbi5leGl0Rm4gPSAoY29kZSkgewogICAgICAgICAgdGhyb3cgUHJvY2Vzc0V4Y2VwdGlvbignZXhpdCcsIFtjb2RlLnRvU3RyaW5nKCldKTsKICAgICAgICB9OwogICAgICB9KTsKCiAgICAgIHRlYXJEb3duKCgpIHsKICAgICAgICBwcmVfZ2VuLmV4aXRGbiA9IGV4aXQ7CiAgICAgIH0pOwoKICAgICAgdGVzdCgnd2hlbiB0YXJnZXQgdGVzdCBkaXIgZG9lcyBub3QgZXhpc3QnLCAoKSBhc3luYyB7CiAgICAgICAgRmlsZShwYXRoLmpvaW4odGVtcERpcmVjdG9yeS5wYXRoLCAncHVic3BlYy55YW1sJykpLmNyZWF0ZVN5bmMoKTsKCiAgICAgICAgZmluYWwgdGVzdERpciA9IERpcmVjdG9yeShwYXRoLmpvaW4odGVtcERpcmVjdG9yeS5wYXRoLCAndGVzdCcpKTsKCiAgICAgICAgY29udGV4dC52YXJzWydwYWNrYWdlLXJvb3QnXSA9IHRlbXBEaXJlY3RvcnkuYWJzb2x1dGUucGF0aDsKCiAgICAgICAgYXdhaXQgZXhwZWN0TGF0ZXIoCiAgICAgICAgICAoKSA9PiBwcmVfZ2VuLnJ1bihjb250ZXh0KSwKICAgICAgICAgIHRocm93c0EoCiAgICAgICAgICAgIGlzQTxQcm9jZXNzRXhjZXB0aW9uPigpLmhhdmluZygKICAgICAgICAgICAgICAoZXgpID0+IGV4LmFyZ3VtZW50cy5maXJzdCwKICAgICAgICAgICAgICAnZXJyb3IgY29kZScsCiAgICAgICAgICAgICAgZXF1YWxzKCcxJyksCiAgICAgICAgICAgICksCiAgICAgICAgICApLAogICAgICAgICk7CgogICAgICAgIHZlcmlmeSgKICAgICAgICAgICgpID0+IGNvbnRleHQubG9nZ2VyLmVycignQ291bGQgbm90IGZpbmQgZGlyZWN0b3J5ICR7dGVzdERpci5wYXRofScpLAogICAgICAgICkuY2FsbGVkKDEpOwoKICAgICAgICBleHBlY3QoY29udGV4dC52YXJzWyd0ZXN0cyddLCBpc051bGwpOwogICAgICAgIGV4cGVjdChjb250ZXh0LnZhcnNbJ2lzRmx1dHRlciddLCBpc051bGwpOwogICAgICB9KTsKCiAgICAgIHRlc3QoJ3doZW4gdGFyZ2V0IGRpciBkb2VzIG5vdCBjb250YWluIGEgcHVic3BlYy55YW1sJywgKCkgYXN5bmMgewogICAgICAgIGZpbmFsIHRlc3REaXIgPSBEaXJlY3RvcnkocGF0aC5qb2luKHRlbXBEaXJlY3RvcnkucGF0aCwgJ3Rlc3QnKSkKICAgICAgICAgIC4uY3JlYXRlU3luYygpOwogICAgICAgIEZpbGUocGF0aC5qb2luKHRlc3REaXIucGF0aCwgJ3Rlc3QxX3Rlc3QuZGFydCcpKS5jcmVhdGVTeW5jKCk7CiAgICAgICAgRmlsZShwYXRoLmpvaW4odGVzdERpci5wYXRoLCAndGVzdDJfdGVzdC5kYXJ0JykpLmNyZWF0ZVN5bmMoKTsKICAgICAgICBGaWxlKHBhdGguam9pbih0ZXN0RGlyLnBhdGgsICdub190ZXN0X2hlcmUuZGFydCcpKS5jcmVhdGVTeW5jKCk7CgogICAgICAgIGNvbnRleHQudmFyc1sncGFja2FnZS1yb290J10gPSB0ZW1wRGlyZWN0b3J5LmFic29sdXRlLnBhdGg7CgogICAgICAgIGF3YWl0IGV4cGVjdExhdGVyKAogICAgICAgICAgKCkgPT4gcHJlX2dlbi5ydW4oY29udGV4dCksCiAgICAgICAgICB0aHJvd3NBKAogICAgICAgICAgICBpc0E8UHJvY2Vzc0V4Y2VwdGlvbj4oKS5oYXZpbmcoCiAgICAgICAgICAgICAgKGV4KSA9PiBleC5hcmd1bWVudHMuZmlyc3QsCiAgICAgICAgICAgICAgJ2Vycm9yIGNvZGUnLAogICAgICAgICAgICAgIGVxdWFscygnMScpLAogICAgICAgICAgICApLAogICAgICAgICAgKSwKICAgICAgICApOwoKICAgICAgICB2ZXJpZnkoCiAgICAgICAgICAoKSA9PiBjb250ZXh0LmxvZ2dlci5lcnIoCiAgICAgICAgICAgICdDb3VsZCBub3QgZmluZCBwdWJzcGVjLnlhbWwgYXQgJHt0ZXN0RGlyLnBhdGh9JywKICAgICAgICAgICksCiAgICAgICAgKS5jYWxsZWQoMSk7CgogICAgICAgIGV4cGVjdChjb250ZXh0LnZhcnNbJ3Rlc3RzJ10sIGlzTnVsbCk7CiAgICAgICAgZXhwZWN0KGNvbnRleHQudmFyc1snaXNGbHV0dGVyJ10sIGlzTnVsbCk7CiAgICAgIH0pOwogICAgfSk7CgogICAgZ3JvdXAoJ3NraXBWZXJ5R29vZE9wdGltaXphdGlvblJlZ0V4cCByZWdleCcsICgpIHsKICAgICAgZmluYWwgcmVnZXggPSBwcmVfZ2VuLnNraXBWZXJ5R29vZE9wdGltaXphdGlvblJlZ0V4cDsKICAgICAgdGVzdCgnbWF0Y2hlcyBzaW5nbGUtbGluZSB0YWcnLCAoKSB7CiAgICAgICAgZmluYWwgY29udGVudCA9ICJAVGFncyhbJyR7cHJlX2dlbi5za2lwVmVyeUdvb2RPcHRpbWl6YXRpb25UYWd9J10pIjsKICAgICAgICBleHBlY3QocmVnZXguaGFzTWF0Y2goY29udGVudCksIGlzVHJ1ZSk7CiAgICAgIH0pOwoKICAgICAgdGVzdCgnbWF0Y2hlcyBzaW5nbGUtbGluZSB3aXRoIG11bHRpcGxlIHRhZ3MnLCAoKSB7CiAgICAgICAgZmluYWwgY29udGVudCA9CiAgICAgICAgICAgICJAVGFncyhbJyR7cHJlX2dlbi5za2lwVmVyeUdvb2RPcHRpbWl6YXRpb25UYWd9JywgJ2Nocm9tZSddKSI7CiAgICAgICAgZXhwZWN0KHJlZ2V4Lmhhc01hdGNoKGNvbnRlbnQpLCBpc1RydWUpOwogICAgICB9KTsKCiAgICAgIHRlc3QoJ21hdGNoZXMgbXVsdGktbGluZSB0YWcgbGlzdCcsICgpIHsKICAgICAgICBmaW5hbCBjb250ZW50ID0KICAgICAgICAgICAgJycnCiAgICAgIEBUYWdzKFsKICAgICAgICAnJHtwcmVfZ2VuLnNraXBWZXJ5R29vZE9wdGltaXphdGlvblRhZ30nLAogICAgICAgICdjaHJvbWUnLAogICAgICAgICd0ZXN0JywKICAgICAgXSkKICAgICAgJycnOwogICAgICAgIGV4cGVjdChyZWdleC5oYXNNYXRjaChjb250ZW50KSwgaXNUcnVlKTsKICAgICAgfSk7CgogICAgICB0ZXN0KCdtYXRjaGVzIG11bHRpLWxpbmUgd2hlcmUgdGFnIGlzIG5vdCB0aGUgZmlyc3QnLCAoKSB7CiAgICAgICAgZmluYWwgY29udGVudCA9CiAgICAgICAgICAgICcnJwogICAgICBAVGFncyhbCiAgICAgICAgJ2Nocm9tZScsCiAgICAgICAgJyR7cHJlX2dlbi5za2lwVmVyeUdvb2RPcHRpbWl6YXRpb25UYWd9JywKICAgICAgICAndGVzdCcsCiAgICAgIF0pCiAgICAgICcnJzsKICAgICAgICBleHBlY3QocmVnZXguaGFzTWF0Y2goY29udGVudCksIGlzVHJ1ZSk7CiAgICAgIH0pOwoKICAgICAgdGVzdCgnZG9lcyBub3QgbWF0Y2ggd2hlbiB0YWcgbWlzc2luZycsICgpIHsKICAgICAgICBjb25zdCBjb250ZW50ID0gIkBUYWdzKFsnY2hyb21lJywgJ3Rlc3QnXSkiOwogICAgICAgIGV4cGVjdChyZWdleC5oYXNNYXRjaChjb250ZW50KSwgaXNGYWxzZSk7CiAgICAgIH0pOwoKICAgICAgdGVzdCgKICAgICAgICAnZG9lcyBub3QgbWF0Y2ggc3Vic3RyaW5nIG9ubHkgKGUuZy4gc2tpcF92ZXJ5X2dvb2Rfb3B0aW1pemF0aW9uLHRlc3QpJywKICAgICAgICAoKSB7CiAgICAgICAgICBmaW5hbCBjb250ZW50ID0KICAgICAgICAgICAgICAnJycKICAgICAgQFRhZ3MoWwogICAgICAgICcke3ByZV9nZW4uc2tpcFZlcnlHb29kT3B0aW1pemF0aW9uVGFnfSx0ZXN0JywKICAgICAgICAnY2hyb21lJywKICAgICAgXSkKICAgICAgJycnOwogICAgICAgICAgZXhwZWN0KAogICAgICAgICAgICByZWdleC5oYXNNYXRjaChjb250ZW50KSwKICAgICAgICAgICAgaXNGYWxzZSwKICAgICAgICAgICk7IC8vIG9ubHkgZXhhY3QgdGFnIHNob3VsZCBtYXRjaAogICAgICAgIH0sCiAgICAgICk7CiAgICB9KTsKICAgIGdyb3VwKCdTaGFyZGluZycsICgpIHsKICAgICAgLy8vIENyZWF0ZXMgYSBwYWNrYWdlIHdpdGggW2NvdW50XSBvcHRpbWl6YWJsZSB0ZXN0IGZpbGVzLCBwbHVzIGFueQogICAgICAvLy8gW25vdE9wdGltaXplZF0gZmlsZXMgY2FycnlpbmcgdGhlIHNraXAgb3B0aW1pemF0aW9uIHRhZy4KICAgICAgRGlyZWN0b3J5IGNyZWF0ZVBhY2thZ2UoaW50IGNvdW50LCB7aW50IG5vdE9wdGltaXplZCA9IDB9KSB7CiAgICAgICAgRmlsZShwYXRoLmpvaW4odGVtcERpcmVjdG9yeS5wYXRoLCAncHVic3BlYy55YW1sJykpLmNyZWF0ZVN5bmMoKTsKICAgICAgICBmaW5hbCB0ZXN0RGlyID0gRGlyZWN0b3J5KHBhdGguam9pbih0ZW1wRGlyZWN0b3J5LnBhdGgsICd0ZXN0JykpCiAgICAgICAgICAuLmNyZWF0ZVN5bmMoKTsKICAgICAgICBmb3IgKHZhciBpID0gMDsgaSA8IGNvdW50OyBpKyspIHsKICAgICAgICAgIEZpbGUocGF0aC5qb2luKHRlc3REaXIucGF0aCwgJ3Rlc3Qke2l9X3Rlc3QuZGFydCcpKS5jcmVhdGVTeW5jKCk7CiAgICAgICAgfQogICAgICAgIGZvciAodmFyIGkgPSAwOyBpIDwgbm90T3B0aW1pemVkOyBpKyspIHsKICAgICAgICAgIEZpbGUoCiAgICAgICAgICAgIHBhdGguam9pbih0ZXN0RGlyLnBhdGgsICdza2lwJHtpfV90ZXN0LmRhcnQnKSwKICAgICAgICAgICkud3JpdGVBc1N0cmluZ1N5bmMobm90T3B0aW1pemVkVGVzdENvbnRlbnQpOwogICAgICAgIH0KICAgICAgICByZXR1cm4gdGVzdERpcjsKICAgICAgfQoKICAgICAgTGlzdDxTdHJpbmc+IHBhdGhzT2YoSG9va0NvbnRleHQgY29udGV4dCkgewogICAgICAgIGZpbmFsIHRlc3RzID0gY29udGV4dC52YXJzWyd0ZXN0cyddIGFzIExpc3Q8TWFwPFN0cmluZywgU3RyaW5nPj47CiAgICAgICAgcmV0dXJuIHRlc3RzLm1hcCgoZSkgPT4gZVsncGF0aCddISkudG9MaXN0KCk7CiAgICAgIH0KCiAgICAgIEZ1dHVyZTxMaXN0PFN0cmluZz4+IHJ1blNoYXJkKGludCBpbmRleCwgaW50IHRvdGFsKSBhc3luYyB7CiAgICAgICAgZmluYWwgY29udGV4dCA9IF9GYWtlQ29udGV4dCgpCiAgICAgICAgICAuLnZhcnNbJ3BhY2thZ2Utcm9vdCddID0gdGVtcERpcmVjdG9yeS5hYnNvbHV0ZS5wYXRoCiAgICAgICAgICAuLnZhcnNbJ3NoYXJkLWluZGV4J10gPSBpbmRleAogICAgICAgICAgLi52YXJzWyd0b3RhbC1zaGFyZHMnXSA9IHRvdGFsOwogICAgICAgIGF3YWl0IHByZV9nZW4ucnVuKGNvbnRleHQpOwogICAgICAgIHJldHVybiBbCiAgICAgICAgICAuLi5wYXRoc09mKGNvbnRleHQpLAogICAgICAgICAgLi4uKGNvbnRleHQudmFyc1snbm90T3B0aW1pemVkVGVzdHMnXSEgYXMgTGlzdCkuY2FzdDxTdHJpbmc+KCksCiAgICAgICAgXTsKICAgICAgfQoKICAgICAgdGVzdCgncnVucyBldmVyeSB0ZXN0IGV4YWN0bHkgb25jZSBhY3Jvc3MgYWxsIHNoYXJkcycsICgpIGFzeW5jIHsKICAgICAgICBjcmVhdGVQYWNrYWdlKDcsIG5vdE9wdGltaXplZDogMik7CgogICAgICAgIGZpbmFsIHNoYXJkcyA9IFsKICAgICAgICAgIGZvciAodmFyIGkgPSAxOyBpIDw9IDM7IGkrKykgYXdhaXQgcnVuU2hhcmQoaSwgMyksCiAgICAgICAgXTsKICAgICAgICBmaW5hbCB1bmlvbiA9IHNoYXJkcy5leHBhbmQoKHNoYXJkKSA9PiBzaGFyZCkudG9MaXN0KCk7CgogICAgICAgIGV4cGVjdCgKICAgICAgICAgIHVuaW9uLi5zb3J0KCksCiAgICAgICAgICBbCiAgICAgICAgICAgIGZvciAodmFyIGkgPSAwOyBpIDwgNzsgaSsrKSAndGVzdCR7aX1fdGVzdC5kYXJ0JywKICAgICAgICAgICAgZm9yICh2YXIgaSA9IDA7IGkgPCAyOyBpKyspICdza2lwJHtpfV90ZXN0LmRhcnQnLAogICAgICAgICAgXS4uc29ydCgpLAogICAgICAgICAgcmVhc29uOiAnU2hhcmRzIG11c3QgYmUgYSBjb21wbGV0ZSBhbmQgZGlzam9pbnQgcGFydGl0aW9uJywKICAgICAgICApOwogICAgICB9KTsKCiAgICAgIHRlc3QoJ3NoYXJkcyBub24gb3B0aW1pemVkIHRlc3RzIGFzIHdlbGwnLCAoKSBhc3luYyB7CiAgICAgICAgY3JlYXRlUGFja2FnZSgwLCBub3RPcHRpbWl6ZWQ6IDQpOwoKICAgICAgICBmaW5hbCBmaXJzdCA9IGF3YWl0IHJ1blNoYXJkKDEsIDIpOwogICAgICAgIGZpbmFsIHNlY29uZCA9IGF3YWl0IHJ1blNoYXJkKDIsIDIpOwoKICAgICAgICBleHBlY3QoZmlyc3QsIFsnc2tpcDBfdGVzdC5kYXJ0JywgJ3NraXAyX3Rlc3QuZGFydCddKTsKICAgICAgICBleHBlY3Qoc2Vjb25kLCBbJ3NraXAxX3Rlc3QuZGFydCcsICdza2lwM190ZXN0LmRhcnQnXSk7CiAgICAgIH0pOwoKICAgICAgdGVzdCgnaXMgZGV0ZXJtaW5pc3RpYyBhY3Jvc3MgcnVucycsICgpIGFzeW5jIHsKICAgICAgICBjcmVhdGVQYWNrYWdlKDkpOwoKICAgICAgICBleHBlY3QoYXdhaXQgcnVuU2hhcmQoMiwgNCksIGF3YWl0IHJ1blNoYXJkKDIsIDQpKTsKICAgICAgfSk7CgogICAgICB0ZXN0KCdiYWxhbmNlcyBzaGFyZHMgd2l0aGluIG9uZSBmaWxlIG9mIGVhY2ggb3RoZXInLCAoKSBhc3luYyB7CiAgICAgICAgY3JlYXRlUGFja2FnZSgxMCk7CgogICAgICAgIGZpbmFsIHNpemVzID0gWwogICAgICAgICAgZm9yICh2YXIgaSA9IDE7IGkgPD0gNDsgaSsrKSAoYXdhaXQgcnVuU2hhcmQoaSwgNCkpLmxlbmd0aCwKICAgICAgICBdOwoKICAgICAgICBleHBlY3Qoc2l6ZXMucmVkdWNlKG1heCkgLSBzaXplcy5yZWR1Y2UobWluKSwgbGVzc1RoYW5PckVxdWFsVG8oMSkpOwogICAgICB9KTsKCiAgICAgIHRlc3QoCiAgICAgICAgJ3lpZWxkcyBhbiBlbXB0eSBzaGFyZCB3aGVuIHRoZXJlIGFyZSBtb3JlIHNoYXJkcyB0aGFuIHRlc3RzJywKICAgICAgICAoKSBhc3luYyB7CiAgICAgICAgICBjcmVhdGVQYWNrYWdlKDIpOwoKICAgICAgICAgIGV4cGVjdChhd2FpdCBydW5TaGFyZCgzLCAzKSwgaXNFbXB0eSk7CiAgICAgICAgfSwKICAgICAgKTsKCiAgICAgIHRlc3QoCiAgICAgICAgJ2V4Y2x1ZGVzIG5lc3RlZCBub24gb3B0aW1pemVkIHRlc3RzIGZyb20gdGhlIG9wdGltaXplZCBzZXQnLAogICAgICAgICgpIGFzeW5jIHsKICAgICAgICAgIEZpbGUocGF0aC5qb2luKHRlbXBEaXJlY3RvcnkucGF0aCwgJ3B1YnNwZWMueWFtbCcpKS5jcmVhdGVTeW5jKCk7CiAgICAgICAgICBmaW5hbCB0ZXN0RGlyID0gRGlyZWN0b3J5KHBhdGguam9pbih0ZW1wRGlyZWN0b3J5LnBhdGgsICd0ZXN0JykpCiAgICAgICAgICAgIC4uY3JlYXRlU3luYygpOwogICAgICAgICAgZmluYWwgbmVzdGVkID0gRGlyZWN0b3J5KHBhdGguam9pbih0ZXN0RGlyLnBhdGgsICdzdWInKSkKICAgICAgICAgICAgLi5jcmVhdGVTeW5jKCk7CiAgICAgICAgICBGaWxlKAogICAgICAgICAgICBwYXRoLmpvaW4obmVzdGVkLnBhdGgsICdza2lwX3Rlc3QuZGFydCcpLAogICAgICAgICAgKS53cml0ZUFzU3RyaW5nU3luYyhub3RPcHRpbWl6ZWRUZXN0Q29udGVudCk7CgogICAgICAgICAgZmluYWwgY29udGV4dCA9IF9GYWtlQ29udGV4dCgpCiAgICAgICAgICAgIC4udmFyc1sncGFja2FnZS1yb290J10gPSB0ZW1wRGlyZWN0b3J5LmFic29sdXRlLnBhdGg7CiAgICAgICAgICBhd2FpdCBwcmVfZ2VuLnJ1bihjb250ZXh0KTsKCiAgICAgICAgICBleHBlY3QoCiAgICAgICAgICAgIHBhdGhzT2YoY29udGV4dCksCiAgICAgICAgICAgIGlzRW1wdHksCiAgICAgICAgICAgIHJlYXNvbjoKICAgICAgICAgICAgICAgICdBIHRhZ2dlZCB0ZXN0IGluIGEgc3ViZGlyZWN0b3J5IG11c3Qgbm90IGJlIG9wdGltaXplZCwgJwogICAgICAgICAgICAgICAgJ290aGVyd2lzZSBpdCBydW5zIGJvdGggaW5saW5lZCBhbmQgc3RhbmRhbG9uZScsCiAgICAgICAgICApOwogICAgICAgICAgZXhwZWN0KAogICAgICAgICAgICBjb250ZXh0LnZhcnNbJ25vdE9wdGltaXplZFRlc3RzJ10sCiAgICAgICAgICAgIFsnc3ViL3NraXBfdGVzdC5kYXJ0J10sCiAgICAgICAgICApOwogICAgICAgIH0sCiAgICAgICk7CgogICAgICB0ZXN0KCdpbmNsdWRlcyBldmVyeSB0ZXN0IHdoZW4gc2hhcmRpbmcgaXMgbm90IHJlcXVlc3RlZCcsICgpIGFzeW5jIHsKICAgICAgICBjcmVhdGVQYWNrYWdlKDMpOwoKICAgICAgICBmaW5hbCBjb250ZXh0ID0gX0Zha2VDb250ZXh0KCkKICAgICAgICAgIC4udmFyc1sncGFja2FnZS1yb290J10gPSB0ZW1wRGlyZWN0b3J5LmFic29sdXRlLnBhdGg7CiAgICAgICAgYXdhaXQgcHJlX2dlbi5ydW4oY29udGV4dCk7CgogICAgICAgIGV4cGVjdChwYXRoc09mKGNvbnRleHQpLCBoYXNMZW5ndGgoMykpOwogICAgICB9KTsKICAgIH0pOwoKICAgIGdyb3VwKCdzaGFyZE9mJywgKCkgewogICAgICBjb25zdCBwYXRocyA9IFsnYS5kYXJ0JywgJ2IuZGFydCcsICdjLmRhcnQnLCAnZC5kYXJ0JywgJ2UuZGFydCddOwoKICAgICAgdGVzdCgncmV0dXJucyBwYXRocyB1bnRvdWNoZWQgd2hlbiBzaGFyZCBpbmRleCBpcyBudWxsJywgKCkgewogICAgICAgIGV4cGVjdCgKICAgICAgICAgIHByZV9nZW4uc2hhcmRPZihwYXRocywgc2hhcmRJbmRleDogbnVsbCwgdG90YWxTaGFyZHM6IDMpLAogICAgICAgICAgcGF0aHMsCiAgICAgICAgKTsKICAgICAgfSk7CgogICAgICB0ZXN0KCdyZXR1cm5zIHBhdGhzIHVudG91Y2hlZCB3aGVuIHRvdGFsIHNoYXJkcyBpcyBudWxsJywgKCkgewogICAgICAgIGV4cGVjdCgKICAgICAgICAgIHByZV9nZW4uc2hhcmRPZihwYXRocywgc2hhcmRJbmRleDogMSwgdG90YWxTaGFyZHM6IG51bGwpLAogICAgICAgICAgcGF0aHMsCiAgICAgICAgKTsKICAgICAgfSk7CgogICAgICB0ZXN0KCdkZWFscyBwYXRocyBvdXQgcm91bmQgcm9iaW4nLCAoKSB7CiAgICAgICAgZXhwZWN0KAogICAgICAgICAgcHJlX2dlbi5zaGFyZE9mKHBhdGhzLCBzaGFyZEluZGV4OiAxLCB0b3RhbFNoYXJkczogMiksCiAgICAgICAgICBbJ2EuZGFydCcsICdjLmRhcnQnLCAnZS5kYXJ0J10sCiAgICAgICAgKTsKICAgICAgICBleHBlY3QoCiAgICAgICAgICBwcmVfZ2VuLnNoYXJkT2YocGF0aHMsIHNoYXJkSW5kZXg6IDIsIHRvdGFsU2hhcmRzOiAyKSwKICAgICAgICAgIFsnYi5kYXJ0JywgJ2QuZGFydCddLAogICAgICAgICk7CiAgICAgIH0pOwoKICAgICAgdGVzdCgncmV0dXJucyBldmVyeXRoaW5nIGZvciBhIHNpbmdsZSBzaGFyZCcsICgpIHsKICAgICAgICBleHBlY3QocHJlX2dlbi5zaGFyZE9mKHBhdGhzLCBzaGFyZEluZGV4OiAxLCB0b3RhbFNoYXJkczogMSksIHBhdGhzKTsKICAgICAgfSk7CgogICAgICB0ZXN0KCdyZXR1cm5zIGVtcHR5IGZvciBhbiBvdXQgb2YgcmFuZ2Ugc2hhcmQnLCAoKSB7CiAgICAgICAgZXhwZWN0KAogICAgICAgICAgcHJlX2dlbi5zaGFyZE9mKHBhdGhzLCBzaGFyZEluZGV4OiA5LCB0b3RhbFNoYXJkczogOSksCiAgICAgICAgICBpc0VtcHR5LAogICAgICAgICk7CiAgICAgIH0pOwogICAgfSk7CiAgfSk7Cn0K", "type": "text", }, ], diff --git a/lib/src/commands/dart/commands/dart_test_command.dart b/lib/src/commands/dart/commands/dart_test_command.dart index 61451c3cd..f617eb21d 100644 --- a/lib/src/commands/dart/commands/dart_test_command.dart +++ b/lib/src/commands/dart/commands/dart_test_command.dart @@ -410,8 +410,6 @@ This command should be run from the root of your Dart project.'''); final options = DartTestOptions.parse(_argResults, config: config); final shardingError = validateSharding( - shardIndex: options.shardIndex, - totalShards: options.totalShards, rawShardIndex: _argResults['shard-index'] as String?, rawTotalShards: _argResults['total-shards'] as String?, optimizePerformance: options.optimizePerformance, diff --git a/lib/src/commands/test/test.dart b/lib/src/commands/test/test.dart index a06507286..45fe05df9 100644 --- a/lib/src/commands/test/test.dart +++ b/lib/src/commands/test/test.dart @@ -244,10 +244,8 @@ class FlutterTestOptions { /// configuration is valid. /// /// [rawShardIndex] and [rawTotalShards] are the unparsed command line values, -/// needed to tell "not provided" apart from "provided but not a number". +/// so that "not provided" can be told apart from "provided but not a number". String? validateSharding({ - required int? shardIndex, - required int? totalShards, required String? rawShardIndex, required String? rawTotalShards, required bool optimizePerformance, @@ -259,11 +257,13 @@ String? validateSharding({ return '--shard-index and --total-shards must be used together.'; } + final totalShards = int.tryParse(rawTotalShards); if (totalShards == null || totalShards < 1) { return '--total-shards must be a positive integer, ' 'but got "$rawTotalShards".'; } + final shardIndex = int.tryParse(rawShardIndex); if (shardIndex == null || shardIndex < 1) { return '--shard-index must be a positive integer, ' 'but got "$rawShardIndex".'; @@ -285,10 +285,9 @@ String? validateSharding({ // not representative of the whole suite. Merge the lcov files from every // shard and enforce the threshold in a separate job instead. if (minCoverage != null) { - return 'Sharding cannot be combined with --min-coverage, because each ' - 'shard only covers a subset of the tests. Collect coverage per shard ' - 'with --coverage, merge the lcov reports, and check the threshold ' - 'once all shards have completed.'; + return '--min-coverage cannot be combined with sharding. Collect coverage ' + 'per shard with --coverage, merge the lcov reports, then check the ' + 'threshold in a separate job.'; } return null; @@ -543,8 +542,6 @@ This command should be run from the root of your Flutter project.'''); final options = FlutterTestOptions.parse(_argResults, config: config); final shardingError = validateSharding( - shardIndex: options.shardIndex, - totalShards: options.totalShards, rawShardIndex: _argResults['shard-index'] as String?, rawTotalShards: _argResults['total-shards'] as String?, optimizePerformance: options.optimizePerformance, diff --git a/site/docs/commands/test.md b/site/docs/commands/test.md index 5ad659f06..b256a15bb 100644 --- a/site/docs/commands/test.md +++ b/site/docs/commands/test.md @@ -105,6 +105,10 @@ not run on every runner. A shard with no test files (more shards than test files) succeeds without running anything, so an oversized matrix will not fail your build. +When combined with `--recursive`, each package is sharded independently, so a +given runner executes a slice of every package. Balance therefore degrades when +a workspace contains many packages with few tests each. + :::caution Sharding cannot be combined with `--min-coverage`. Each shard only exercises a subset of the codebase, so its coverage is not representative of the whole diff --git a/test/src/cli/test_cli_runner_test.dart b/test/src/cli/test_cli_runner_test.dart index d2a1be34d..2fdead1af 100644 --- a/test/src/cli/test_cli_runner_test.dart +++ b/test/src/cli/test_cli_runner_test.dart @@ -1188,6 +1188,58 @@ void main() { expect(testRunnerArgs, isEmpty); }); + test( + 'runs the shard when it only contains non optimized tests', + () async { + final tempDirectory = Directory.systemTemp.createTempSync(); + addTearDown(() => tempDirectory.deleteSync(recursive: true)); + + File(p.join(tempDirectory.path, 'pubspec.yaml')).createSync(); + Directory(p.join(tempDirectory.path, 'test')).createSync(); + when( + () => hooks.preGen( + vars: any(named: 'vars'), + onVarsChanged: any(named: 'onVarsChanged'), + workingDirectory: any(named: 'workingDirectory'), + ), + ).thenAnswer((invocation) async { + (invocation.namedArguments[#onVarsChanged] + as void Function(Map vars)) + .call({ + 'package-root': tempDirectory.path, + 'tests': >[], + 'notOptimizedTests': ['skip0_test.dart'], + }); + }); + + await expectLater( + TestCLIRunner.test( + testType: TestRunType.flutter, + cwd: tempDirectory.path, + logger: logger, + stdout: stdoutLogs.add, + stderr: stderrLogs.add, + buildGenerator: generatorBuilder(), + optimizePerformance: true, + shardIndex: 2, + totalShards: 2, + overrideTestRunner: testRunner( + Stream.fromIterable([ + const DoneTestEvent(success: true, time: 0), + const ExitTestEvent(exitCode: 0, time: 0), + ]), + ), + ), + completion(equals([ExitCode.success.code])), + ); + + expect( + testRunnerArgs, + contains(p.join('test', 'skip0_test.dart')), + ); + }, + ); + test('runs tests w/optimizations (passing)', () async { final tempDirectory = Directory.systemTemp.createTempSync(); addTearDown(() => tempDirectory.deleteSync(recursive: true)); diff --git a/test/src/commands/dart/commands/dart_test_test.dart b/test/src/commands/dart/commands/dart_test_test.dart index c9f920d77..5910ef444 100644 --- a/test/src/commands/dart/commands/dart_test_test.dart +++ b/test/src/commands/dart/commands/dart_test_test.dart @@ -915,10 +915,9 @@ void main() { expect(result, equals(ExitCode.usage.code)); verify( () => logger.err( - 'Sharding cannot be combined with --min-coverage, because each ' - 'shard only covers a subset of the tests. Collect coverage per ' - 'shard with --coverage, merge the lcov reports, and check the ' - 'threshold once all shards have completed.', + '--min-coverage cannot be combined with sharding. Collect ' + 'coverage per shard with --coverage, merge the lcov reports, ' + 'then check the threshold in a separate job.', ), ).called(1); }); diff --git a/test/src/commands/test/test_test.dart b/test/src/commands/test/test_test.dart index e3aa75664..6d44867e7 100644 --- a/test/src/commands/test/test_test.dart +++ b/test/src/commands/test/test_test.dart @@ -1094,10 +1094,9 @@ void main() { expect(result, equals(ExitCode.usage.code)); verify( () => logger.err( - 'Sharding cannot be combined with --min-coverage, because each ' - 'shard only covers a subset of the tests. Collect coverage per ' - 'shard with --coverage, merge the lcov reports, and check the ' - 'threshold once all shards have completed.', + '--min-coverage cannot be combined with sharding. Collect ' + 'coverage per shard with --coverage, merge the lcov reports, ' + 'then check the threshold in a separate job.', ), ).called(1); });