Skip to content

Commit 30aa541

Browse files
authored
Require a definition named main in test suites (#2569)
Closes #2567 The error when there is a compilation failure due to a missing `main` is less direct than it could be and exposes details of wrapping the test suite in another library. Add an explicit check for a function declared with that name when parsing metadata before attempting any compiles. Add a check to `parseMetadata` that throws a `FormatException` if there is no function declared directly in the library named `main`. This will prevent some previously working designs such as exposing a main in a part or with `export`, but we do not expect those designs are used in practice.
1 parent c15153b commit 30aa541

File tree

8 files changed

+86
-42
lines changed

8 files changed

+86
-42
lines changed

pkgs/test/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
* Add `--coverage-package` flag, which filters the coverage report to specific
44
packages using RegExps.
5+
* Require a function definition named `main` directly in a test suite and
6+
provide a more direct error message than a failing compiler output.
57

68
## 1.28.0
79

pkgs/test/test/runner/browser/runner_test.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ void main() {
3333

3434
group('fails gracefully if', () {
3535
test('a test file fails to compile', () async {
36-
await d.file('test.dart', 'invalid Dart file').create();
36+
await d.file('test.dart', 'void main() {invalid Dart}').create();
3737
var test = await runTest(['-p', 'chrome', 'test.dart']);
3838

3939
expect(

pkgs/test/test/runner/node/runner_test.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ void main() {
7878

7979
group('fails gracefully if', () {
8080
test('a test file fails to compile', () async {
81-
await d.file('test.dart', 'invalid Dart file').create();
81+
await d.file('test.dart', 'void main() {invalid Dart}').create();
8282
var test = await runTest(['-p', 'node', 'test.dart']);
8383

8484
expect(

pkgs/test/test/runner/parse_metadata_test.dart

Lines changed: 55 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,16 @@ import 'package:test_core/src/runner/parse_metadata.dart';
1414
final _path = 'test.dart';
1515

1616
void main() {
17-
test('returns empty metadata for an empty file', () {
18-
var metadata = parseMetadata(_path, '', {});
17+
test('returns empty metadata for a file without annotations', () {
18+
var metadata = parseMetadata(_path, 'main(){}', {});
1919
expect(metadata.testOn, equals(PlatformSelector.all));
2020
expect(metadata.timeout.scaleFactor, equals(1));
2121
});
2222

2323
test('ignores irrelevant annotations', () {
2424
var metadata = parseMetadata(
2525
_path,
26-
'@Fblthp\n@Fblthp.foo\nlibrary foo;',
26+
'@Fblthp\n@Fblthp.foo\nlibrary foo;main(){}',
2727
{},
2828
);
2929
expect(metadata.testOn, equals(PlatformSelector.all));
@@ -33,7 +33,8 @@ void main() {
3333
var metadata = parseMetadata(
3434
_path,
3535
"@foo.TestOn('vm')\n"
36-
"import 'package:test/test.dart' as foo;",
36+
"import 'package:test/test.dart' as foo;\n"
37+
'main(){}',
3738
{},
3839
);
3940
expect(
@@ -46,9 +47,20 @@ void main() {
4647
);
4748
});
4849

50+
test('throws for missing definition of `main`', () {
51+
expect(
52+
() => parseMetadata(_path, 'void notMain() {}', {}),
53+
throwsFormatException,
54+
);
55+
});
56+
4957
group('@TestOn:', () {
5058
test('parses a valid annotation', () {
51-
var metadata = parseMetadata(_path, "@TestOn('vm')\nlibrary foo;", {});
59+
var metadata = parseMetadata(
60+
_path,
61+
"@TestOn('vm')\nlibrary foo;\nmain(){}",
62+
{},
63+
);
5264
expect(
5365
metadata.testOn.evaluate(SuitePlatform(Runtime.vm, compiler: null)),
5466
isTrue,
@@ -62,7 +74,7 @@ void main() {
6274
test('ignores a constructor named TestOn', () {
6375
var metadata = parseMetadata(
6476
_path,
65-
"@foo.TestOn('foo')\nlibrary foo;",
77+
"@foo.TestOn('foo')\nlibrary foo;\nmain(){}",
6678
{},
6779
);
6880
expect(metadata.testOn, equals(PlatformSelector.all));
@@ -93,6 +105,7 @@ void main() {
93105
microseconds: 5))
94106
95107
library foo;
108+
main(){}
96109
''', {});
97110
expect(
98111
metadata.timeout.duration,
@@ -118,6 +131,7 @@ library foo;
118131
microseconds: 5))
119132
120133
library foo;
134+
main(){}
121135
''', {});
122136
expect(
123137
metadata.timeout.duration,
@@ -142,6 +156,7 @@ library foo;
142156
milliseconds: 4,
143157
microseconds: 5))
144158
import 'dart:core' as core;
159+
main(){}
145160
''', {});
146161
expect(
147162
metadata.timeout.duration,
@@ -162,6 +177,7 @@ import 'dart:core' as core;
162177
@Timeout.factor(1)
163178
164179
library foo;
180+
main(){}
165181
''', {});
166182
expect(metadata.timeout.scaleFactor, equals(1));
167183
});
@@ -170,6 +186,7 @@ library foo;
170186
var metadata = parseMetadata(_path, '''
171187
@test.Timeout.factor(1)
172188
import 'package:test/test.dart' as test;
189+
main(){}
173190
''', {});
174191
expect(metadata.timeout.scaleFactor, equals(1));
175192
});
@@ -179,6 +196,7 @@ import 'package:test/test.dart' as test;
179196
@Timeout.factor(0.5)
180197
181198
library foo;
199+
main(){}
182200
''', {});
183201
expect(metadata.timeout.scaleFactor, equals(0.5));
184202
});
@@ -188,14 +206,15 @@ library foo;
188206
@Timeout.none
189207
190208
library foo;
209+
main(){}
191210
''', {});
192211
expect(metadata.timeout, same(Timeout.none));
193212
});
194213

195214
test('ignores a constructor named Timeout', () {
196215
var metadata = parseMetadata(
197216
_path,
198-
"@foo.Timeout('foo')\nlibrary foo;",
217+
"@foo.Timeout('foo')\nlibrary foo;\nmain(){}",
199218
{},
200219
);
201220
expect(metadata.timeout.scaleFactor, equals(1));
@@ -217,19 +236,31 @@ library foo;
217236

218237
group('@Skip:', () {
219238
test('parses a valid annotation', () {
220-
var metadata = parseMetadata(_path, '@Skip()\nlibrary foo;', {});
239+
var metadata = parseMetadata(
240+
_path,
241+
'@Skip()\nlibrary foo;\nmain(){}',
242+
{},
243+
);
221244
expect(metadata.skip, isTrue);
222245
expect(metadata.skipReason, isNull);
223246
});
224247

225248
test('parses a valid annotation with a reason', () {
226-
var metadata = parseMetadata(_path, "@Skip('reason')\nlibrary foo;", {});
249+
var metadata = parseMetadata(
250+
_path,
251+
"@Skip('reason')\nlibrary foo;\nmain(){}",
252+
{},
253+
);
227254
expect(metadata.skip, isTrue);
228255
expect(metadata.skipReason, equals('reason'));
229256
});
230257

231258
test('ignores a constructor named Skip', () {
232-
var metadata = parseMetadata(_path, "@foo.Skip('foo')\nlibrary foo;", {});
259+
var metadata = parseMetadata(
260+
_path,
261+
"@foo.Skip('foo')\nlibrary foo;\nmain(){}",
262+
{},
263+
);
233264
expect(metadata.skip, isFalse);
234265
});
235266

@@ -249,12 +280,20 @@ library foo;
249280

250281
group('@Tags:', () {
251282
test('parses a valid annotation', () {
252-
var metadata = parseMetadata(_path, "@Tags(['a'])\nlibrary foo;", {});
283+
var metadata = parseMetadata(
284+
_path,
285+
"@Tags(['a'])\nlibrary foo;\nmain(){}",
286+
{},
287+
);
253288
expect(metadata.tags, equals(['a']));
254289
});
255290

256291
test('ignores a constructor named Tags', () {
257-
var metadata = parseMetadata(_path, "@foo.Tags(['a'])\nlibrary foo;", {});
292+
var metadata = parseMetadata(
293+
_path,
294+
"@foo.Tags(['a'])\nlibrary foo;\nmain(){}",
295+
{},
296+
);
258297
expect(metadata.tags, isEmpty);
259298
});
260299

@@ -290,7 +329,8 @@ library foo;
290329
'chrome': Timeout.factor(2),
291330
'vm': [Skip(), Timeout.factor(3)]
292331
})
293-
library foo;''', {});
332+
library foo;
333+
main(){}''', {});
294334

295335
var key = metadata.onPlatform.keys.first;
296336
expect(
@@ -319,6 +359,7 @@ library foo;''', {});
319359
'vm': [test.Skip(), test.Timeout.factor(3)]
320360
})
321361
import 'package:test/test.dart' as test;
362+
main(){}
322363
''', {});
323364

324365
var key = metadata.onPlatform.keys.first;
@@ -344,7 +385,7 @@ import 'package:test/test.dart' as test;
344385
test('ignores a constructor named OnPlatform', () {
345386
var metadata = parseMetadata(
346387
_path,
347-
"@foo.OnPlatform('foo')\nlibrary foo;",
388+
"@foo.OnPlatform('foo')\nlibrary foo;main(){}",
348389
{},
349390
);
350391
expect(metadata.testOn, equals(PlatformSelector.all));

pkgs/test/test/runner/precompiled_test.dart

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,9 @@ void main() {
107107
preamble.getPreamble(minified: true) + await jsFile.readAsString(),
108108
);
109109

110-
await d.dir('test', [d.file('test.dart', 'invalid dart}')]).create();
110+
await d.dir('test', [
111+
d.file('test.dart', 'void main() {invalid dart}'),
112+
]).create();
111113
});
112114

113115
test(
@@ -278,5 +280,5 @@ Future<void> _precompileBrowserTest(String testPath) async {
278280
], workingDirectory: d.sandbox);
279281
await dart2js.shouldExit(0);
280282

281-
await d.file(testPath, 'invalid dart}').create();
283+
await d.file(testPath, 'void main() {invalid dart}').create();
282284
}

pkgs/test/test/runner/runner_test.dart

Lines changed: 8 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -186,15 +186,14 @@ $_usage''');
186186
});
187187

188188
test('a test file fails to load', () async {
189-
await d.file('test.dart', 'invalid Dart file').create();
189+
await d.file('test.dart', 'void main(){invalid Dart}').create();
190190
var test = await runTest(['test.dart']);
191191

192192
expect(
193193
test.stdout,
194194
containsInOrder([
195195
'Failed to load "test.dart":',
196-
"test.dart:1:9: Error: Expected ';' after this.",
197-
'invalid Dart file',
196+
"Error: Expected ';' after this.",
198197
]),
199198
);
200199

@@ -222,7 +221,7 @@ $_usage''');
222221
// This is slightly different from the above test because it's an error
223222
// that's caught first by the analyzer when it's used to parse the file.
224223
test('a test file fails to parse', () async {
225-
await d.file('test.dart', '@TestOn)').create();
224+
await d.file('test.dart', '@TestOn) void main() {}').create();
226225
var test = await runTest(['test.dart']);
227226

228227
expect(
@@ -239,7 +238,9 @@ $_usage''');
239238
});
240239

241240
test("an annotation's contents are invalid", () async {
242-
await d.file('test.dart', "@TestOn('zim')\nlibrary foo;").create();
241+
await d
242+
.file('test.dart', "@TestOn('zim')\nlibrary foo;\nvoid main() {}")
243+
.create();
243244
var test = await runTest(['test.dart']);
244245

245246
expect(
@@ -276,12 +277,7 @@ $_usage''');
276277
expect(test.stdout, emitsThrough(contains('-1: loading test.dart [E]')));
277278
expect(
278279
test.stdout,
279-
emitsThrough(
280-
anyOf([
281-
contains("Error: Getter not found: 'main'"),
282-
contains("Error: Undefined name 'main'"),
283-
]),
284-
),
280+
emitsThrough(contains('Missing definition of `main` method')),
285281
);
286282

287283
await test.shouldExit(1);
@@ -294,18 +290,7 @@ $_usage''');
294290
expect(test.stdout, emitsThrough(contains('-1: loading test.dart [E]')));
295291
expect(
296292
test.stdout,
297-
emitsThrough(
298-
anyOf([
299-
contains(
300-
"A value of type 'int' can't be assigned to a variable of type "
301-
"'Function'",
302-
),
303-
contains(
304-
"A value of type 'int' can't be returned from a function with "
305-
"return type 'Function'",
306-
),
307-
]),
308-
),
293+
emitsThrough(contains('Missing definition of `main` method')),
309294
);
310295

311296
await test.shouldExit(1);

pkgs/test_core/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
* Add `--coverage-package` flag, which filters the coverage report to specific
44
packages using RegExps.
5+
* Require a function definition named `main` directly in a test suite and
6+
provide a more direct error message than a failing compiler output.
57

68
## 0.6.14
79

pkgs/test_core/lib/src/runner/parse_metadata.dart

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import '../util/dart.dart';
2020
/// allowed everywhere.
2121
///
2222
/// Throws an [AnalysisError] if parsing fails or a [FormatException] if the
23-
/// test annotations are incorrect.
23+
/// test suite is unrunnable due to incorrect annotations or a missing main.
2424
Metadata parseMetadata(
2525
String path,
2626
String contents,
@@ -48,6 +48,12 @@ class _Parser {
4848
/// The language version override comment if one was present, otherwise null.
4949
String? _languageVersionComment;
5050

51+
/// Whether any member of the compilation unit is named 'main'.
52+
///
53+
/// When main is missing a call to [parse] will throw a [FormatException] with
54+
/// a descriptive message.
55+
late final bool _hasMain;
56+
5157
_Parser(this._path, this._contents, this._platformVariables) {
5258
var result = parseString(
5359
content: _contents,
@@ -57,6 +63,9 @@ class _Parser {
5763
var directives = result.unit.directives;
5864
_annotations = directives.isEmpty ? [] : directives.first.metadata;
5965
_languageVersionComment = result.unit.languageVersionToken?.value();
66+
_hasMain = result.unit.declarations.any(
67+
(d) => d is FunctionDeclaration && d.name.lexeme == 'main',
68+
);
6069

6170
// We explicitly *don't* just look for "package:test" imports here,
6271
// because it could be re-exported from another library.
@@ -75,6 +84,9 @@ class _Parser {
7584

7685
/// Parses the metadata.
7786
Metadata parse() {
87+
if (!_hasMain) {
88+
throw const FormatException('Missing definition of `main` method.');
89+
}
7890
Timeout? timeout;
7991
PlatformSelector? testOn;
8092
Object? /*String|bool*/ skip;

0 commit comments

Comments
 (0)