Skip to content

Commit 2d42e9a

Browse files
Add a script to automatically update formatting test expectations. (#1230)
* Add a script to automatically update formatting test expectations. It's not fully featured. It doesn't handle tests with selection markers or Unicode escapes. But it handles everything else. This should make it easier for me to update the tests as I work on the Flutter style experimental branch. Co-authored-by: Nate Bosch <[email protected]>
1 parent 2956b1a commit 2d42e9a

File tree

14 files changed

+376
-171
lines changed

14 files changed

+376
-171
lines changed

lib/src/dart_formatter.dart

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,8 +158,7 @@ class DartFormatter {
158158
offset: token.offset - inputOffset,
159159
length: math.max(token.length, 1),
160160
errorCode: ParserErrorCode.UNEXPECTED_TOKEN,
161-
arguments: [token.lexeme],
162-
);
161+
arguments: [token.lexeme]);
163162
throw FormatterException([error]);
164163
}
165164
}

lib/src/testing/test_file.dart

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import 'dart:io';
2+
import 'dart:isolate';
3+
4+
import 'package:path/path.dart' as p;
5+
6+
import '../../dart_style.dart';
7+
8+
final _indentPattern = RegExp(r'\(indent (\d+)\)');
9+
final _fixPattern = RegExp(r'\(fix ([a-x-]+)\)');
10+
final _unicodeUnescapePattern = RegExp(r'×([0-9a-fA-F]{2,4})');
11+
final _unicodeEscapePattern = RegExp('[\x0a\x0c\x0d]');
12+
13+
/// Get the absolute local file path to the package's "test" directory.
14+
Future<String> findTestDirectory() async {
15+
var libraryUri = await Isolate.resolvePackageUri(
16+
Uri.parse('package:dart_style/src/testing/test_file.dart'));
17+
return p.normalize(
18+
p.join(p.dirname(libraryUri!.toFilePath()), '../../../test'));
19+
}
20+
21+
/// A file containing a series of formatting tests.
22+
class TestFile {
23+
/// Finds all test files in the given directory relative to the package's
24+
/// `test/` directory.
25+
static Future<List<TestFile>> listDirectory(String name) async {
26+
var testDir = await findTestDirectory();
27+
var entries = Directory(p.join(testDir, name))
28+
.listSync(recursive: true, followLinks: false);
29+
entries.sort((a, b) => a.path.compareTo(b.path));
30+
31+
return [
32+
for (var entry in entries)
33+
if (entry is File &&
34+
(entry.path.endsWith('.stmt') || entry.path.endsWith('.unit')))
35+
TestFile._load(entry, p.relative(entry.path, from: testDir))
36+
];
37+
}
38+
39+
/// Reads the test file from [path], which is relative to the package's
40+
/// `test/` directory.
41+
static Future<TestFile> read(String path) async {
42+
var testDir = await findTestDirectory();
43+
var file = File(p.join(testDir, path));
44+
return TestFile._load(file, p.relative(file.path, from: testDir));
45+
}
46+
47+
/// Reads the test file from [file].
48+
factory TestFile._load(File file, String relativePath) {
49+
var lines = file.readAsLinesSync();
50+
51+
// The first line may have a "|" to indicate the page width.
52+
var i = 0;
53+
int? pageWidth;
54+
if (lines[i].endsWith('|')) {
55+
pageWidth = lines[i].indexOf('|');
56+
i++;
57+
}
58+
59+
var tests = <FormatTest>[];
60+
61+
while (i < lines.length) {
62+
var line = i + 1;
63+
var description = lines[i++].replaceAll('>>>', '');
64+
var fixes = <StyleFix>[];
65+
66+
// Let the test specify a leading indentation. This is handy for
67+
// regression tests which often come from a chunk of nested code.
68+
var leadingIndent = 0;
69+
description = description.replaceAllMapped(_indentPattern, (match) {
70+
leadingIndent = int.parse(match[1]!);
71+
return '';
72+
});
73+
74+
// Let the test specify fixes to apply.
75+
description = description.replaceAllMapped(_fixPattern, (match) {
76+
fixes.add(StyleFix.all.firstWhere((fix) => fix.name == match[1]));
77+
return '';
78+
});
79+
80+
var inputBuffer = StringBuffer();
81+
while (!lines[i].startsWith('<<<')) {
82+
inputBuffer.writeln(lines[i++]);
83+
}
84+
85+
var outputDescription = lines[i].replaceAll('<<<', '');
86+
87+
var outputBuffer = StringBuffer();
88+
while (++i < lines.length && !lines[i].startsWith('>>>')) {
89+
outputBuffer.writeln(lines[i]);
90+
}
91+
92+
var isCompilationUnit = file.path.endsWith('.unit');
93+
var input = _extractSelection(_unescapeUnicode(inputBuffer.toString()),
94+
isCompilationUnit: isCompilationUnit);
95+
var output = _extractSelection(_unescapeUnicode(outputBuffer.toString()),
96+
isCompilationUnit: isCompilationUnit);
97+
98+
tests.add(FormatTest(input, output, description.trim(),
99+
outputDescription.trim(), line, fixes, leadingIndent));
100+
}
101+
102+
return TestFile._(relativePath, pageWidth, tests);
103+
}
104+
105+
TestFile._(this.path, this.pageWidth, this.tests);
106+
107+
/// The path to the test file, relative to the `test/` directory.
108+
final String path;
109+
110+
/// The page width for tests in this file or `null` if the default should be
111+
/// used.
112+
final int? pageWidth;
113+
114+
/// The tests in this file.
115+
final List<FormatTest> tests;
116+
117+
bool get isCompilationUnit => path.endsWith('.unit');
118+
}
119+
120+
/// A single formatting test inside a [TestFile].
121+
class FormatTest {
122+
/// The unformatted input.
123+
final SourceCode input;
124+
125+
/// The expected output.
126+
final SourceCode output;
127+
128+
/// The optional description of the test.
129+
final String description;
130+
131+
/// If there is a remark on the "<<<" line, this is it.
132+
final String outputDescription;
133+
134+
/// The 1-based index of the line where this test begins.
135+
final int line;
136+
137+
/// The style fixes this test is applying.
138+
final List<StyleFix> fixes;
139+
140+
/// The number of spaces of leading indentation that should be added to each
141+
/// line.
142+
final int leadingIndent;
143+
144+
FormatTest(this.input, this.output, this.description, this.outputDescription,
145+
this.line, this.fixes, this.leadingIndent);
146+
147+
/// The line and description of the test.
148+
String get label {
149+
if (description.isEmpty) return 'line $line';
150+
return 'line $line: $description';
151+
}
152+
}
153+
154+
/// Given a source string that contains ‹ and › to indicate a selection, returns
155+
/// a [SourceCode] with the text (with the selection markers removed) and the
156+
/// correct selection range.
157+
SourceCode _extractSelection(String source, {bool isCompilationUnit = false}) {
158+
var start = source.indexOf('‹');
159+
source = source.replaceAll('‹', '');
160+
161+
var end = source.indexOf('›');
162+
source = source.replaceAll('›', '');
163+
164+
return SourceCode(source,
165+
isCompilationUnit: isCompilationUnit,
166+
selectionStart: start == -1 ? null : start,
167+
selectionLength: end == -1 ? null : end - start);
168+
}
169+
170+
/// Turn the special Unicode escape marker syntax used in the tests into real
171+
/// Unicode characters.
172+
///
173+
/// This does not use Dart's own string escape sequences so that we don't
174+
/// accidentally modify the Dart code being formatted.
175+
String _unescapeUnicode(String input) {
176+
return input.replaceAllMapped(_unicodeUnescapePattern, (match) {
177+
var codePoint = int.parse(match[1]!, radix: 16);
178+
return String.fromCharCode(codePoint);
179+
});
180+
}
181+
182+
/// Turn the few Unicode characters used in tests back to their escape syntax.
183+
String escapeUnicode(String input) {
184+
return input.replaceAllMapped(_unicodeEscapePattern, (match) {
185+
return '×${match[0]!.codeUnitAt(0).toRadixString(16)}';
186+
});
187+
}

test/comments/switch.stmt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,4 +238,4 @@ e = switch (n) {
238238
>>> inline block comment
239239
e = switch (n) { /* comment */ };
240240
<<<
241-
e = switch (n) {/* comment */};
241+
e = switch (n) {/* comment */};

test/fix_test.dart

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@ import 'package:test/test.dart';
1010

1111
import 'utils.dart';
1212

13-
void main() {
14-
testFile(
13+
void main() async {
14+
await testFile(
1515
'fixes/named_default_separator.unit', [StyleFix.namedDefaultSeparator]);
16-
testFile('fixes/doc_comments.stmt', [StyleFix.docComments]);
17-
testFile('fixes/function_typedefs.unit', [StyleFix.functionTypedefs]);
18-
testFile('fixes/optional_const.unit', [StyleFix.optionalConst]);
19-
testFile('fixes/optional_new.stmt', [StyleFix.optionalNew]);
20-
testFile('fixes/single_cascade_statements.stmt',
16+
await testFile('fixes/doc_comments.stmt', [StyleFix.docComments]);
17+
await testFile('fixes/function_typedefs.unit', [StyleFix.functionTypedefs]);
18+
await testFile('fixes/optional_const.unit', [StyleFix.optionalConst]);
19+
await testFile('fixes/optional_new.stmt', [StyleFix.optionalNew]);
20+
await testFile('fixes/single_cascade_statements.stmt',
2121
[StyleFix.singleCascadeStatements]);
2222
}

test/formatter_test.dart

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@ import 'package:test/test.dart';
1010

1111
import 'utils.dart';
1212

13-
void main() {
14-
testDirectory('comments');
15-
testDirectory('regression');
16-
testDirectory('selections');
17-
testDirectory('splitting');
18-
testDirectory('whitespace');
13+
void main() async {
14+
await testDirectory('comments');
15+
await testDirectory('regression');
16+
await testDirectory('selections');
17+
await testDirectory('splitting');
18+
await testDirectory('whitespace');
1919

2020
test('throws a FormatterException on failed parse', () {
2121
var formatter = DartFormatter();

test/regression/0000/0025.stmt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,4 @@ return _parseHtml(content, id.path, logger,
2121
from: builder.join('/', builder.dirname(sourceId.path)));
2222
<<<
2323
return builder.relative(builder.join('/', id.path),
24-
from: builder.join('/', builder.dirname(sourceId.path)));
24+
from: builder.join('/', builder.dirname(sourceId.path)));

test/regression/0100/0141.unit

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@
1212
Map<ast.SwitchCase, int> caseIndex,
1313
bool hasDefault) {
1414
;
15-
}
15+
}

test/regression/0100/0158.unit

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,4 @@ class ResultSet {
4040
json['platform'],
4141
json['release'],
4242
json['results'].map((result) => new Result.fromJson(result)).toList());
43-
}
43+
}

test/regression/1100/1198.stmt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,4 @@ e = switch ([1,]) {};
2828
<<<
2929
e = switch ([
3030
1,
31-
]) {};
31+
]) {};

test/regression/1200/1205.stmt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,4 @@ error(
99
error(offset: 10, (
1010
code: 'unclosed-block',
1111
message: 'Block was left open',
12-
));
12+
));

0 commit comments

Comments
 (0)