New flag --shard-by-suite#2655
Conversation
PR HealthChangelog Entry ✔️
Changes to files need to be accounted for in their respective changelogs. This check can be disabled by tagging the PR with |
|
Meta comment, but we call test "files" test "suites". So it might be best to make this I am curious what @natebosch thinks though. |
…t-lang/test into shard-by-file-docs-and-tests
I think as this is raw sharding by file, I like to keep the terminology there. Also I don't really see a reason for package:test to invent its own name for a file. I added a parenthetical explanation in the help-string and readme for people who know what a suite is, but don't know what a file is. |
I don't disagree with you but the decision was made a long time ago to call test files suites. I couldn't tell you why. This does leak out to users via |
|
Sorry for the slow response. I spent some time thinking through alternative sharding designs that can take into account at least some of the differences between test suites but I came up short. I think I lean towards landing something very close to this design. Some thoughts: RE
With the "file" naming, as a user I might expect that a pattern like RE shard before or after reading top level
It feels quite natural to me that we would honor suite-level configuration when we are doing suite-level sharding. We should expect that test suites which are entirely configured for one platform would do that at the suite level instead of the test or group level The fact that we do sorting by file path could make a situation where a big run of tests for a single platform end up in a single shard that ends up doing no testing because they were all filtered out. We should maybe consider doing a stable shuffle then sublists, or sort and round-robin distribute suites to shards so that similarly named tests are more likely to end up on different shards. |
| split across shards and helps maximize the re-use of `setUpAll` and | ||
| `tearDownAll` setups. | ||
| * When sharding by file (using `--shard-by-file`): Distribute entire test files (suites) | ||
| across shards. This can be faster for suites with many small files as it |
There was a problem hiding this comment.
[nit] This usage of "suites" is wrong - replace with maybe "packages" or "test invocations"?
| ### Interaction with Test Filters | ||
|
|
||
| The sharding modes interact differently with filters like `--name` or `--tags`: | ||
|
|
||
| * When sharding by test (default), the sharding partition is calculated *after* | ||
| applying filters. This guarantees that the matching tests are distributed | ||
| as evenly as possible across all shards. | ||
| * When sharding by file (using `--shard-by-file`), the files are partitioned | ||
| *before* they are loaded and filtered. If a filter only matches tests in a | ||
| few files, some shards might run no tests because those files were allocated | ||
| to other shards. |
There was a problem hiding this comment.
I think we should make top level test annotations work for this strategy.
I think the way we should talk about the test-case level filtering is a little bit more general. Something maybe like
Test filters which apply to individual test cases or groups (as opposed to suite level annotations at the top of a file) will be reflected in the distribution when sharding by test case, but not when sharding by test suite. Test invocations with uneven number of tests cases to run between test suites can cause uneven workloads across the shards.
|
I had gemini take a try at handling the filtering before sharding. At a skim the solution it found looks fine to me - add a cache of the |
|
Fair, I can try to give it a go next week to have top-level filtering before splitting. |
…tion, and eager filtering
…rm_test.dart, and node/runner_test.dart
sigurdm
left a comment
There was a problem hiding this comment.
Now suites are filtered by top-level annotations before being sharded.
The flag is now --shard-by-suite, and the phrasing has been updated everywhere.
| /// Cache of metadata parsed from test files. | ||
| final _metadataCache = <String, Object>{}; | ||
|
|
||
| /// Parses and returns the suite metadata for [path], caching the result. | ||
| Metadata parseSuiteMetadata(String path) { | ||
| final cached = _metadataCache.putIfAbsent(path, () { | ||
| if (!File(path).existsSync()) return Metadata.empty; | ||
| try { | ||
| return parseMetadata( | ||
| path, | ||
| File(path).readAsStringSync(), | ||
| _runtimeVariables.toSet(), | ||
| ); | ||
| } catch (e) { | ||
| return e; | ||
| } | ||
| }); | ||
| if (cached is Metadata) return cached; | ||
| throw cached; | ||
| } |
There was a problem hiding this comment.
[nit] I prefer being a bit more explicit than Metadat/ not Metadat. WDYT about using Result from package:async (even though we're only using it in a synchronous way)
| /// Cache of metadata parsed from test files. | |
| final _metadataCache = <String, Object>{}; | |
| /// Parses and returns the suite metadata for [path], caching the result. | |
| Metadata parseSuiteMetadata(String path) { | |
| final cached = _metadataCache.putIfAbsent(path, () { | |
| if (!File(path).existsSync()) return Metadata.empty; | |
| try { | |
| return parseMetadata( | |
| path, | |
| File(path).readAsStringSync(), | |
| _runtimeVariables.toSet(), | |
| ); | |
| } catch (e) { | |
| return e; | |
| } | |
| }); | |
| if (cached is Metadata) return cached; | |
| throw cached; | |
| } | |
| /// Cache of metadata parsed from test files. | |
| final _metadataCache = <String, Result<Metadata>>{}; | |
| /// Parses and returns the suite metadata for [path], caching the result. | |
| Metadata parseSuiteMetadata(String path) { | |
| final cached = _metadataCache.putIfAbsent(path, () { | |
| if (!File(path).existsSync()) return .value(Metadata.empty); | |
| try { | |
| return .value( | |
| parseMetadata( | |
| path, | |
| File(path).readAsStringSync(), | |
| _runtimeVariables.toSet(), | |
| ), | |
| ); | |
| } catch (e, stackTrace) { | |
| return .error(e, stackTrace); | |
| } | |
| }); | |
| return switch (cached) { | |
| ValueResult(:var value) => value, | |
| ErrorResult(:var error, :var stackTrace) => Error.throwWithStackTrace( | |
| error, | |
| stackTrace, | |
| ), | |
| _ => throw StateError('Unexpected result type: $cached'), | |
| }; | |
| } |
| /// Returns whether the test suite at [path] matches target platform and tag filters | ||
| /// specified in [suiteConfig]. | ||
| bool matchesSuite(String path, SuiteConfiguration suiteConfig) { | ||
| if (!File(path).existsSync()) return true; |
There was a problem hiding this comment.
I would have expected false. Is this a code path we'll never hit in practice? If this branch does get executed I think we should describe it in the doc.
| } | ||
|
|
||
| var hasMatchingPlatform = false; | ||
| for (var runtimeName in mergedConfig.runtimes) { |
There was a problem hiding this comment.
I think this is somewhat duplicative. I think we could pull out a List<SuitePlatform> _matchingPlatforms(SuiteConfiguration suiteConfig) method if the throw behavior is OK for both.
Would it cause any problem if we use the exception behavior here instead of silently ignoring unknown platforms? I think we wouldn't expect them here any more than we would when loading would we?
Running all setupAll hooks is expensive when what we are trying to do is to minimize the time spent for each shard.
This allows choosing a less fine-grained strategy of sharding by individual test files.
Fixes #2614