[CFE] DartDoc tests support expecting throws
With this CL one could do a test like
```
DartDocTestThrows(LocalStack<int>([]).current)
```
which would then expect the call to throw and fail if it doesn't throw
in which case you might get something like
```
Failure:
Test from [...]/pkg/front_end/lib/src/util/local_stack.dart:19:25 failed with this message:
Expected a crash, but didn't get one.
Processed 19 test(s) in 33 ms.
18 OK; 1 bad; 0 crashed; 0 parse errors.
```
Change-Id: I52deb657cb95b9b1f866a58f7433f0c57162b5c9
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/378480
Commit-Queue: Jens Johansen <jensj@google.com>
Reviewed-by: Johnni Winther <johnniwinther@google.com>
diff --git a/pkg/front_end/lib/src/util/local_stack.dart b/pkg/front_end/lib/src/util/local_stack.dart
index fde11da..e8e5617 100644
--- a/pkg/front_end/lib/src/util/local_stack.dart
+++ b/pkg/front_end/lib/src/util/local_stack.dart
@@ -16,6 +16,7 @@
/// Return the current top of the stack.
///
/// ```
+ /// DartDocTestThrows(LocalStack<int>([]).current)
/// DartDocTest(LocalStack<int>([0]).current, 0)
/// DartDocTest(LocalStack<int>([0, 1]).current, 1)
/// ```
@@ -33,6 +34,8 @@
/// Returns the second-most element on the stack.
///
/// ```
+ /// DartDocTestThrows(LocalStack<int>([]).previous)
+ /// DartDocTestThrows(LocalStack<int>([0]).previous)
/// DartDocTest(LocalStack<int>([0, 1]).previous, 0)
/// DartDocTest(LocalStack<int>([0, 1, 2]).previous, 1)
/// ```
@@ -55,6 +58,7 @@
/// Pops the top of the stack.
///
/// ```
+ /// DartDocTestThrows(LocalStack<int>([]).pop())
/// DartDocTest(LocalStack<int>([0]).pop(), 0)
/// DartDocTest((LocalStack<int>([0, 1, 2])..pop()).pop(), 1)
/// ```
diff --git a/pkg/front_end/test/dartdoc_test_test.dart b/pkg/front_end/test/dartdoc_test_test.dart
index 339898e..4588e5f 100644
--- a/pkg/front_end/test/dartdoc_test_test.dart
+++ b/pkg/front_end/test/dartdoc_test_test.dart
@@ -176,6 +176,39 @@
];
memoryFileSystem.entityForUri(test7).writeAsStringSync(test);
expect(await dartDocTest.process(test7), expected);
+
+ // Throws test
+ Uri test8 = new Uri(scheme: "darttest", path: "/test8.dart");
+ test = """
+ // DartDocTestThrows(1~/0)
+ main() {
+ print("Hello from main");
+ }
+ """;
+ tests = extractTests(test, test8);
+ expect(tests.length, 1);
+ expected = [
+ new impl.TestResult(tests[0], impl.TestOutcome.Pass),
+ ];
+ memoryFileSystem.entityForUri(test8).writeAsStringSync(test);
+ expect(await dartDocTest.process(test8), expected);
+
+ // Good throws case using await.
+ Uri test9 = new Uri(scheme: "darttest", path: "/test9.dart");
+ test = """
+// DartDocTestThrows(await _internal())
+Future<void> _internal() async {
+ await Future.delayed(new Duration(milliseconds: 1));
+ if (1+1==2) throw "I threw!";
+}
+""";
+ tests = extractTests(test, test9);
+ expect(tests.length, 1);
+ expected = [
+ new impl.TestResult(tests[0], impl.TestOutcome.Pass),
+ ];
+ memoryFileSystem.entityForUri(test9).writeAsStringSync(test);
+ expect(await dartDocTest.process(test9), expected);
}
void testTestExtraction() {
@@ -303,6 +336,31 @@
"""), <impl.Test>[
new impl.ExpectTest('await foo()', '42', "darttest:/foo.dart:1:16"),
]);
+
+ // One throws test.
+ expect(extractTests("""
+ // not a test comment
+ void foo_bar_long_name() {}
+
+ // DartDocTestThrows(1~/0)"""), <impl.Test>[
+ new impl.ThrowsTest("1~/0", "darttest:/foo.dart:4:26"),
+ ]);
+
+ // Mixture of expect and throws tests.
+ expect(extractTests("""
+ // not a test comment
+ void foo_bar_long_name() {}
+
+ // DartDocTestThrows(1~/0)
+ // DartDocTest(1+1, 2)
+ // DartDocTest(2+2, 4)
+ // DartDocTestThrows(2~/0)"""), <impl.Test>[
+ // For now the order is expect tests first.
+ new impl.ExpectTest("1+1", "2", "darttest:/foo.dart:5:20"),
+ new impl.ExpectTest("2+2", "4", "darttest:/foo.dart:6:20"),
+ new impl.ThrowsTest("1~/0", "darttest:/foo.dart:4:26"),
+ new impl.ThrowsTest("2~/0", "darttest:/foo.dart:7:26"),
+ ]);
}
void testCommentExtraction() {
@@ -311,7 +369,9 @@
// Simple line comment at position 0.
expect(
- extractFirstComment("// Hello"), new impl.CommentString(" Hello", 0));
+ extractFirstComment("// Hello"),
+ new impl.CommentString(" Hello", 0),
+ );
// Simple line comment at position 5.
expect(extractFirstComment(" // Hello"),
diff --git a/pkg/front_end/test/spell_checking_list_tests.txt b/pkg/front_end/test/spell_checking_list_tests.txt
index cc95db5..8e02c02 100644
--- a/pkg/front_end/test/spell_checking_list_tests.txt
+++ b/pkg/front_end/test/spell_checking_list_tests.txt
@@ -517,6 +517,7 @@
misspelled
mistake
mistakes
+mixture
mixups
ml
mmethod
diff --git a/pkg/front_end/tool/dart_doctest_impl.dart b/pkg/front_end/tool/dart_doctest_impl.dart
index cf32984..cbccbda 100644
--- a/pkg/front_end/tool/dart_doctest_impl.dart
+++ b/pkg/front_end/tool/dart_doctest_impl.dart
@@ -115,17 +115,23 @@
sb.writeln(
r"Future<void> $dart$doc$test$tester(dynamic dartDocTest) async {");
for (Test test in tests) {
- if (test is TestParseError) {
- sb.writeln(
- "dartDocTest.parseError(\"Parse error @ ${test.position}\");");
- } else if (test is ExpectTest) {
- sb.writeln("try {");
- sb.writeln(" dartDocTest.test(${test.call}, ${test.result});");
- sb.writeln("} catch (e) {");
- sb.writeln(" dartDocTest.crash(e);");
- sb.writeln("}");
- } else {
- throw "Unknown test type: ${test.runtimeType}";
+ switch (test) {
+ case TestParseError():
+ sb.writeln(
+ "dartDocTest.parseError(\"Parse error @ ${test.position}\");");
+ case ExpectTest():
+ sb.writeln("try {");
+ sb.writeln(" dartDocTest.test(${test.call}, ${test.result});");
+ sb.writeln("} catch (e) {");
+ sb.writeln(" dartDocTest.crash(e);");
+ sb.writeln("}");
+ case ThrowsTest():
+ sb.writeln("try {");
+ sb.writeln(
+ " await dartDocTest.throws(() async { ${test.call}; });");
+ sb.writeln("} catch (e) {");
+ sb.writeln(" dartDocTest.crash(e);");
+ sb.writeln("}");
}
}
sb.writeln("}");
@@ -357,6 +363,16 @@
}
}
+ Future<void> throws(Function() computation) async {
+ port.send("$_portMessageTest");
+ try {
+ await computation();
+ port.send("$_portMessageBad: Expected a crash, but didn't get one.");
+ } catch(e) {
+ port.send("$_portMessageGood");
+ }
+ }
+
void crash(dynamic error) {
port.send("$_portMessageTest");
port.send("$_portMessageCrash: \$error");
@@ -486,6 +502,27 @@
}
}
+class ThrowsTest implements Test {
+ final String call;
+ @override
+ final String location;
+
+ ThrowsTest(this.call, this.location);
+
+ @override
+ bool operator ==(Object other) {
+ if (other is! ThrowsTest) return false;
+ if (other.call != call) return false;
+ if (other.location != location) return false;
+ return true;
+ }
+
+ @override
+ String toString() {
+ return "ThrowsTest[$call, $location]";
+ }
+}
+
class TestParseError implements Test {
final String message;
final int position;
@@ -548,11 +585,7 @@
CommentToken comment, String rawString, kernel.Source source) {
CommentString commentsData = extractComments(comment, rawString);
final String comments = commentsData.string;
- List<Test> result = [];
- int index = comments.indexOf("DartDocTest(");
- if (index < 0) {
- return result;
- }
+ int index = -1;
String getLocation(int offset) {
return source
@@ -560,7 +593,11 @@
.toString();
}
- Test scanDartDoc(int scanOffset) {
+ Test scanVariableDartDoc(
+ int scanOffset,
+ String expectedLexeme,
+ int expressionCount,
+ Test Function(List<String> expressions, String location) testCreator) {
final Token firstToken =
scanRawBytes(utf8.encode(comments.substring(scanOffset)));
final ErrorListener listener = new ErrorListener();
@@ -570,7 +607,7 @@
final Token pastErrors = parser.skipErrorTokens(firstToken);
assert(pastErrors.isIdentifier);
- assert(pastErrors.lexeme == "DartDocTest");
+ assert(pastErrors.lexeme == expectedLexeme);
final Token startParen = pastErrors.next!;
assert(identical("(", startParen.stringValue));
@@ -578,68 +615,75 @@
// Advance index so we don't parse the same thing again (for error cases).
index = scanOffset + startParen.charEnd;
- final Token beforeComma = parser.parseExpression(startParen);
- final Token comma = beforeComma.next!;
+ Token parseFrom = startParen;
+ final Token firstExpressionToken = parseFrom.next!;
+ List<String> expressionsText = [];
- if (listener.hasErrors) {
- StringBuffer sb = new StringBuffer();
- int firstPosition = _createParseErrorMessages(
- listener, sb, commentsData, scanOffset, source);
- return new TestParseError(
- sb.toString(),
- firstPosition,
- getLocation(firstPosition),
- );
- } else if (!identical(",", comma.stringValue)) {
- int position = commentsData.charOffset + scanOffset + comma.charOffset;
- Message message = codes.templateExpectedButGot.withArguments(',');
- return new TestParseError(
- _createParseErrorMessage(source, position, comma, comma, message),
- position,
- getLocation(position),
- );
+ for (int i = 1; i <= expressionCount; i++) {
+ final Token expressionFirstToken = parseFrom.next!;
+ final Token beforeNextSeparator = parser.parseExpression(parseFrom);
+ final Token nextSeparator = parseFrom = beforeNextSeparator.next!;
+ final String expectedSeparator = i == expressionCount ? ")" : ",";
+
+ if (listener.hasErrors) {
+ StringBuffer sb = new StringBuffer();
+ int firstPosition = _createParseErrorMessages(
+ listener, sb, commentsData, scanOffset, source);
+ return new TestParseError(
+ sb.toString(),
+ firstPosition,
+ getLocation(firstPosition),
+ );
+ } else if (!identical(expectedSeparator, nextSeparator.stringValue)) {
+ int position =
+ commentsData.charOffset + scanOffset + nextSeparator.charOffset;
+ Message message =
+ codes.templateExpectedButGot.withArguments(expectedSeparator);
+ return new TestParseError(
+ _createParseErrorMessage(
+ source, position, nextSeparator, nextSeparator, message),
+ position,
+ getLocation(position),
+ );
+ } else {
+ // Good.
+ expressionsText.add(comments.substring(
+ scanOffset + expressionFirstToken.charOffset,
+ scanOffset + beforeNextSeparator.charEnd));
+ }
}
- Token beforeEndParen = parser.parseExpression(comma);
- Token endParen = beforeEndParen.next!;
+ assert(expressionsText.length == expressionCount);
- if (listener.hasErrors) {
- StringBuffer sb = new StringBuffer();
- int firstPosition = _createParseErrorMessages(
- listener, sb, commentsData, scanOffset, source);
- return new TestParseError(
- sb.toString(),
- firstPosition,
- getLocation(firstPosition),
- );
- } else if (!identical(")", endParen.stringValue)) {
- int position = commentsData.charOffset + scanOffset + endParen.charOffset;
- Message message = codes.templateExpectedButGot.withArguments(')');
- return new TestParseError(
- _createParseErrorMessage(source, position, comma, comma, message),
- position,
- getLocation(position),
- );
- }
-
- // Advance index so we don't parse the same thing again (success case).
- index = scanOffset + endParen.charEnd;
-
- int startPos = scanOffset + startParen.next!.charOffset;
- int midEndPos = scanOffset + beforeComma.charEnd;
- int midStartPos = scanOffset + comma.next!.charOffset;
- int endPos = scanOffset + beforeEndParen.charEnd;
- return new ExpectTest(
- comments.substring(startPos, midEndPos),
- comments.substring(midStartPos, endPos),
- getLocation(commentsData.charOffset + startPos),
+ return testCreator(
+ expressionsText,
+ getLocation(
+ commentsData.charOffset + scanOffset + firstExpressionToken.charOffset,
+ ),
);
}
+ List<Test> result = [];
+ index = comments.indexOf("DartDocTest(");
while (index >= 0) {
- result.add(scanDartDoc(index));
+ result.add(scanVariableDartDoc(
+ index,
+ "DartDocTest",
+ 2,
+ (List<String> expressions, String location) =>
+ new ExpectTest(expressions[0], expressions[1], location)));
index = comments.indexOf("DartDocTest(", index);
}
+ index = comments.indexOf("DartDocTestThrows(");
+ while (index >= 0) {
+ result.add(scanVariableDartDoc(
+ index,
+ "DartDocTestThrows",
+ 1,
+ (List<String> expressions, String location) =>
+ new ThrowsTest(expressions[0], location)));
+ index = comments.indexOf("DartDocTestThrows(", index);
+ }
return result;
}