Skip to content

Commit 6c4166a

Browse files
authored
Smarter versioned tests (#1744)
Redo how versioned tests work. The diff in the README.md and the doc comments in the .dart file changes should explain what's going on here. The basic idea is that now a test is either completely unversioned with only a single output, or completely versioned where all of the output sections have a version number. I think that's easier to read than a test like: ``` <<< some.code(); >>> 3.8 some.code(); >>> some.code(); ``` And you have to try to figure out what versions the last section applies to. This commit also updates the tests to match the new style. This just means adding `3.8` to a bunch of output sections where that was previously implicit. The set of tested versions and their expectations are the same. I also updated the test updater to support the new output styles. This change also speeds up running the formatter tests by skipping running tests on language versions where the output is the same as a previous and subsequent version. Prior to this change, running tall_format_test.dart runs 8,674 tests. If you bump the highest supported language version from 3.9 to 3.10 (which I'm about to do for dot shorthands), that jumps to 11,556 because it runs every test at every language version. With this change, it runs 5,940 tests when the maximum supported version is both 3.9 and 3.10. The number of tests being run only increases when there are actual new tests or new version-specific expectations.
1 parent ac2650a commit 6c4166a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

85 files changed

+1847
-1700
lines changed

example/format.dart

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,12 @@ Future<void> _runTest(
116116
var actualText = actual.textWithSelectionMarkers;
117117
if (!testFile.isCompilationUnit) actualText += '\n';
118118

119-
// TODO(rnystrom): Handle multiple outputs.
120-
var expectedText = formatTest.outputs.first.code.textWithSelectionMarkers;
119+
var output = switch (formatTest) {
120+
UnversionedFormatTest(:var output) => output,
121+
// Used the newest style for the expectation.
122+
VersionedFormatTest(:var outputs) => outputs.values.last,
123+
};
124+
var expectedText = output.code.textWithSelectionMarkers;
121125

122126
print('$path ${formatTest.input.description}');
123127
_drawRuler('before', pageWidth);

lib/src/testing/test_file.dart

Lines changed: 86 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ final class TestFile {
115115
var lineNumber = i + 1;
116116
var line = readLine().replaceAll('>>>', '');
117117
var (options, description) = _parseOptions(line);
118+
description = description.trim();
118119

119120
var inputComments = readComments();
120121
var inputBuffer = StringBuffer();
@@ -127,9 +128,27 @@ final class TestFile {
127128
isCompilationUnit: isCompilationUnit,
128129
);
129130

130-
var input = TestEntry(description.trim(), null, inputComments, inputCode);
131+
var input = TestEntry(description, inputComments, inputCode);
132+
133+
// Read the outputs. A single test should have outputs in one of two
134+
// forms:
135+
//
136+
// - One single unversioned output which is the expected output across
137+
// all supported versions.
138+
// - One or more versioned outputs, each of which defines the expected
139+
// output at that language version or later until reaching the next
140+
// output's version.
141+
//
142+
// The parser here collects all of the outputs, versioned and unversioned
143+
// and then reports an error if the result is not one of those two styles.
144+
void fail(String error) {
145+
throw FormatException(
146+
'Test format error in $relativePath, line $lineNumber: $error',
147+
);
148+
}
131149

132-
var outputs = <TestEntry>[];
150+
var unversionedOutputs = <TestEntry>[];
151+
var versionedOutputs = <Version, TestEntry>{};
133152
while (i < lines.length && lines[i].startsWith('<<<')) {
134153
var match = _outputPattern.firstMatch(readLine())!;
135154
var outputDescription = match[4]!;
@@ -166,17 +185,43 @@ final class TestFile {
166185
isCompilationUnit: isCompilationUnit,
167186
);
168187

169-
outputs.add(
170-
TestEntry(
171-
outputDescription.trim(),
172-
outputVersion,
173-
outputComments,
174-
outputCode,
175-
),
188+
var entry = TestEntry(
189+
outputDescription.trim(),
190+
outputComments,
191+
outputCode,
176192
);
193+
if (outputVersion != null) {
194+
if (versionedOutputs.containsKey(outputVersion)) {
195+
fail('Multiple outputs with the same version $outputVersion.');
196+
}
197+
198+
versionedOutputs[outputVersion] = entry;
199+
} else {
200+
unversionedOutputs.add(entry);
201+
}
177202
}
178203

179-
tests.add(FormatTest(lineNumber, options, input, outputs));
204+
switch ((unversionedOutputs.length, versionedOutputs.length)) {
205+
case (0, 0):
206+
fail('Test must have at least one output.');
207+
case (0, > 0):
208+
tests.add(
209+
VersionedFormatTest(lineNumber, options, input, versionedOutputs),
210+
);
211+
case (1, 0):
212+
tests.add(
213+
UnversionedFormatTest(
214+
lineNumber,
215+
options,
216+
input,
217+
unversionedOutputs.first,
218+
),
219+
);
220+
case (> 1, 0):
221+
fail('Test can\'t have multiple unversioned outputs.');
222+
default:
223+
fail('Test can\'t have both versioned and unversioned outputs.');
224+
}
180225
}
181226

182227
return TestFile._(
@@ -276,7 +321,7 @@ final class TestFile {
276321
}
277322

278323
/// A single formatting test inside a [TestFile].
279-
final class FormatTest {
324+
sealed class FormatTest {
280325
/// The 1-based index of the line where this test begins.
281326
final int line;
282327

@@ -286,13 +331,7 @@ final class FormatTest {
286331
/// The unformatted input.
287332
final TestEntry input;
288333

289-
// TODO(rnystrom): Consider making this a map of version (or null) to output
290-
// and then validating that there aren't duplicate outputs for a single
291-
// version.
292-
/// The expected output.
293-
final List<TestEntry> outputs;
294-
295-
FormatTest(this.line, this.options, this.input, this.outputs);
334+
FormatTest(this.line, this.options, this.input);
296335

297336
/// The line and description of the test.
298337
String get label {
@@ -301,20 +340,45 @@ final class FormatTest {
301340
}
302341
}
303342

343+
/// A test for formatting that should be the same across all language versions.
344+
///
345+
/// Most tests are of this form.
346+
final class UnversionedFormatTest extends FormatTest {
347+
/// The expected output.
348+
final TestEntry output;
349+
350+
UnversionedFormatTest(super.line, super.options, super.input, this.output);
351+
}
352+
353+
/// A test whose expected formatting changes at specific versions.
354+
final class VersionedFormatTest extends FormatTest {
355+
/// The expected output by version.
356+
///
357+
/// Each key is the lowest version where that output is expected. If there are
358+
/// supported versions lower than the lowest key here, then the test is not
359+
/// run on those versions at all. These tests represent new syntax that isn't
360+
/// supported in later versions. For example, if the map has only a single
361+
/// entry whose key is 3.8, then the test is skipped on 3.7, run at 3.8, and
362+
/// should be valid at any higher version.
363+
///
364+
/// If there are multiple entries in the map, they represent versions where
365+
/// the formatting style has changed.
366+
final Map<Version, TestEntry> outputs;
367+
368+
VersionedFormatTest(super.line, super.options, super.input, this.outputs);
369+
}
370+
304371
/// A single test input or output.
305372
final class TestEntry {
306373
/// Any remark on the "<<<" or ">>>" line.
307374
final String description;
308375

309-
/// If this is a test output for a specific version, the version.
310-
final Version? version;
311-
312376
/// The `###` comment lines appearing after the header line before the code.
313377
final List<String> comments;
314378

315379
final SourceCode code;
316380

317-
TestEntry(this.description, this.version, this.comments, this.code);
381+
TestEntry(this.description, this.comments, this.code);
318382
}
319383

320384
/// Options for configuring all tests in a file or an individual test.

test/README.md

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,15 +37,57 @@ description for the test. Lines after that define the input code to be
3737
formatted.
3838

3939
After the input are one or more output sections. Each output section starts
40-
with a header like:
40+
with a header that starts with `<<<`. There are two styles of output:
41+
42+
#### Unversioned output
43+
44+
Most code is supported across all language versions and formats the same way in
45+
all of them. For those, the output is a single section like:
46+
47+
```
48+
>>> Optional input description.
49+
some.code();
50+
<<< Optional description.
51+
some.code();
52+
```
53+
54+
The formatter will run this tests against the oldest and newest supported
55+
version and verify that both produce that output. (We assume that if it formats
56+
the same at two versions, it will do so in any version between those for
57+
performance reasons. Otherwise, every time a new Dart SDK release comes out,
58+
the number of tests being run increases by thousands.)
59+
60+
#### Versioned outputs
61+
62+
The formatter's behavior may depend on language version for two reasons:
63+
64+
* New syntax was added to the language in a later version, so we can't format
65+
it on an older version at all.
66+
67+
* The formatting style changed and we versioned the style change so that code
68+
at older versions keeps the older style.
69+
70+
To accommodate those, a test can have multiple output sections which each start
71+
with a version number like this:
4172

4273
```
43-
<<< 3.7 Optional description.
74+
>>> Optional input description.
75+
some.code();
76+
<<< 3.8 Optional description.
77+
some.code();
78+
<<< 3.10 Optional description.
79+
some . code();
4480
```
4581

46-
The `<<<` marks the beginning of a new output section. If it has a language
47-
version number, then this output is expected only on that language version. If
48-
it has no version number, then this is the expected output on all versions.
82+
Each output section specifies the minimum version where that output becomes
83+
expected. The version number of the first section specifies the lowest version
84+
number that the test will be run at. Every section after that specifies a
85+
version where the formatting style was changed.
86+
87+
The test is run at multiple language versions and the result compared to the
88+
appropriate output section for that version. In the example here, we won't test
89+
it at all at 3.7, will test at 3.8 and 3.9 with the first output, and at
90+
3.10 and higher with the last output.
4991

5092
### Test options
5193

test/tall/declaration/typedef.unit

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -160,26 +160,26 @@ typedef Generic<T, R> =
160160
);
161161
>>> Allow block-formatting a record typedef.
162162
typedef SomeType = (int first, int second);
163-
<<<
164-
typedef SomeType = (
165-
int first,
166-
int second,
167-
);
168163
<<< 3.7
169164
typedef SomeType =
170165
(int first, int second);
171-
>>> Don't allow block-formatting a record typedef.
172-
typedef SomeType = (int first, int second, String third);
173-
<<<
166+
<<< 3.8
174167
typedef SomeType = (
175168
int first,
176169
int second,
177-
String third,
178170
);
171+
>>> Don't allow block-formatting a record typedef.
172+
typedef SomeType = (int first, int second, String third);
179173
<<< 3.7
180174
typedef SomeType =
181175
(
182176
int first,
183177
int second,
184178
String third,
185179
);
180+
<<< 3.8
181+
typedef SomeType = (
182+
int first,
183+
int second,
184+
String third,
185+
);

test/tall/expression/assignment.stmt

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -81,13 +81,6 @@ target
8181
reallyLongValue;
8282
>>> Allow block formatting through nested assignments.
8383
outer = inner = [element1, element2, element3, element4];
84-
<<<
85-
outer = inner = [
86-
element1,
87-
element2,
88-
element3,
89-
element4,
90-
];
9184
<<< 3.7
9285
outer =
9386
inner = [
@@ -96,19 +89,26 @@ outer =
9689
element3,
9790
element4,
9891
];
92+
<<< 3.8
93+
outer = inner = [
94+
element1,
95+
element2,
96+
element3,
97+
element4,
98+
];
9999
>>> Headline format unsplit target of call chain.
100100
variable = (tar + get).method().another().third();
101-
<<<
102-
variable = (tar + get)
103-
.method()
104-
.another()
105-
.third();
106101
<<< 3.7
107102
variable =
108103
(tar + get)
109104
.method()
110105
.another()
111106
.third();
107+
<<< 3.8
108+
variable = (tar + get)
109+
.method()
110+
.another()
111+
.third();
112112
>>> Don't headline format target of call chain if target splits.
113113
variable = (veryLongTarget + expressionThatSplits).method().another().third();
114114
<<<
@@ -120,14 +120,14 @@ variable =
120120
.third();
121121
>>> Headline format unsplit properties of call chain.
122122
variable = (tar + get).prop.erty.method().another().third();
123-
<<<
124-
variable = (tar + get).prop.erty
125-
.method()
126-
.another()
127-
.third();
128123
<<< 3.7
129124
variable =
130125
(tar + get).prop.erty
131126
.method()
132127
.another()
133128
.third();
129+
<<< 3.8
130+
variable = (tar + get).prop.erty
131+
.method()
132+
.another()
133+
.third();

0 commit comments

Comments
 (0)