| import 'dart:convert'; |
| import 'dart:typed_data'; |
| |
| import 'package:_fe_analyzer_shared/src/scanner/token.dart'; |
| import 'package:front_end/src/api_prototype/memory_file_system.dart'; |
| import 'package:front_end/src/fasta/hybrid_file_system.dart'; |
| |
| import "../tool/dart_doctest_impl.dart" as impl; |
| |
| Future<void> main() async { |
| expectCategory = "comment extraction"; |
| testCommentExtraction(); |
| |
| expectCategory = "test extraction"; |
| testTestExtraction(); |
| |
| expectCategory = "test runs"; |
| await testRunningTests(); |
| } |
| |
| Future<void> testRunningTests() async { |
| MemoryFileSystem memoryFileSystem = |
| new MemoryFileSystem(new Uri(scheme: "darttest", path: "/")); |
| HybridFileSystem hybridFileSystem = new HybridFileSystem(memoryFileSystem); |
| impl.DartDocTest dartDocTest = new impl.DartDocTest( |
| underlyingFileSystem: hybridFileSystem, silent: true); |
| |
| // Good test |
| Uri test1 = new Uri(scheme: "darttest", path: "/test1.dart"); |
| String test = """ |
| // DartDocTest(1+1, 2) |
| main() { |
| print("Hello from main"); |
| } |
| |
| // DartDocTest(_internal(), 42) |
| int _internal() { |
| return 42; |
| } |
| """; |
| List<impl.Test> tests = extractTests(test, test1); |
| expect(tests.length, 2); |
| List<impl.TestResult> expected = [ |
| new impl.TestResult(tests[0], impl.TestOutcome.Pass), |
| new impl.TestResult(tests[1], impl.TestOutcome.Pass), |
| ]; |
| memoryFileSystem.entityForUri(test1).writeAsStringSync(test); |
| expect(await dartDocTest.process(test1), expected); |
| |
| // Mixed good/bad. |
| Uri test2 = new Uri(scheme: "darttest", path: "/test2.dart"); |
| test = """ |
| // DartDocTest(1+1, 3) |
| main() { |
| print("Hello from main"); |
| } |
| |
| // DartDocTest(_internal(), 43) |
| // DartDocTest(_internal(), 42) |
| int _internal() { |
| return 42; |
| } |
| """; |
| tests = extractTests(test, test2); |
| expect(tests.length, 3); |
| expected = [ |
| new impl.TestResult(tests[0], impl.TestOutcome.Failed) |
| ..message = "Expected '3'; got '2'.", |
| new impl.TestResult(tests[1], impl.TestOutcome.Failed) |
| ..message = "Expected '43'; got '42'.", |
| new impl.TestResult(tests[2], impl.TestOutcome.Pass), |
| ]; |
| memoryFileSystem.entityForUri(test2).writeAsStringSync(test); |
| expect(await dartDocTest.process(test2), expected); |
| |
| // Good case using await. |
| Uri test3 = new Uri(scheme: "darttest", path: "/test3.dart"); |
| test = """ |
| // DartDocTest(await _internal(), 42) |
| Future<int> _internal() async { |
| await Future.delayed(new Duration(milliseconds: 1)); |
| return 42; |
| } |
| """; |
| tests = extractTests(test, test3); |
| expect(tests.length, 1); |
| expected = [ |
| new impl.TestResult(tests[0], impl.TestOutcome.Pass), |
| ]; |
| memoryFileSystem.entityForUri(test3).writeAsStringSync(test); |
| expect(await dartDocTest.process(test3), expected); |
| |
| // One test parse error and one good case. |
| Uri test4 = new Uri(scheme: "darttest", path: "/test4.dart"); |
| test = """ |
| // DartDocTest(_internal() 42) |
| // DartDocTest(_internal(), 42) |
| int _internal() { |
| return 42; |
| } |
| """; |
| tests = extractTests(test, test4); |
| expect(tests.length, 2); |
| expected = [ |
| new impl.TestResult(tests[0], impl.TestOutcome.TestCompilationError) |
| ..message = "Parse error @ 27", |
| new impl.TestResult(tests[1], impl.TestOutcome.Pass), |
| ]; |
| memoryFileSystem.entityForUri(test4).writeAsStringSync(test); |
| expect(await dartDocTest.process(test4), expected); |
| |
| // Test with compile-time error. Note that this means no tests are compiled at |
| // all and that while the error messages are passed it spills the internals of |
| // the dartdocs stuff (e.g. the uri "dartdoctest:tester", |
| // calls to 'dartDocTest.test' etc). |
| Uri test5 = new Uri(scheme: "darttest", path: "/test5.dart"); |
| test = """ |
| // DartDocTest(_internal() + 2, 42) |
| // // DartDocTest(2+2, 4) |
| void _internal() { |
| return; |
| } |
| """; |
| tests = extractTests(test); |
| expect(tests.length, 2); |
| expected = [ |
| new impl.TestResult(null, impl.TestOutcome.CompilationError) |
| ..message = |
| """dartdoctest:tester:3:20: Error: This expression has type 'void' and can't be used. |
| dartDocTest.test(_internal() + 2, 42); |
| ^ |
| dartdoctest:tester:3:32: Error: The operator '+' isn't defined for the class 'void'. |
| Try correcting the operator to an existing operator, or defining a '+' operator. |
| dartDocTest.test(_internal() + 2, 42); |
| ^""", |
| ]; |
| memoryFileSystem.entityForUri(test5).writeAsStringSync(test); |
| expect(await dartDocTest.process(test5), expected); |
| |
| // Test with runtime error. |
| Uri test6 = new Uri(scheme: "darttest", path: "/test6.dart"); |
| test = """ |
| // DartDocTest(_internal() + 2, 42) |
| // // DartDocTest(2+2, 4) |
| dynamic _internal() { |
| return "hello"; |
| } |
| """; |
| tests = extractTests(test); |
| expect(tests.length, 2); |
| expected = [ |
| new impl.TestResult(tests[0], impl.TestOutcome.Crash) |
| // this weird message is from the VM! |
| ..message = "type 'int' is not a subtype of type 'String' of 'other'", |
| new impl.TestResult(tests[1], impl.TestOutcome.Pass), |
| ]; |
| memoryFileSystem.entityForUri(test6).writeAsStringSync(test); |
| expect(await dartDocTest.process(test6), expected); |
| |
| // Good/bad test with private static class method. |
| Uri test7 = new Uri(scheme: "darttest", path: "/test7.dart"); |
| test = """ |
| class Foo { |
| // DartDocTest(Foo._internal(), 42) |
| // DartDocTest(Foo._internal(), 44) |
| static int _internal() { |
| return 42; |
| } |
| } |
| """; |
| tests = extractTests(test, test7); |
| expect(tests.length, 2); |
| expected = [ |
| new impl.TestResult(tests[0], impl.TestOutcome.Pass), |
| new impl.TestResult(tests[1], impl.TestOutcome.Failed) |
| ..message = "Expected '44'; got '42'.", |
| ]; |
| memoryFileSystem.entityForUri(test7).writeAsStringSync(test); |
| expect(await dartDocTest.process(test7), expected); |
| } |
| |
| void testTestExtraction() { |
| // No tests. |
| expect(extractTests(""), <impl.Test>[]); |
| |
| // One test. |
| expect(extractTests("// DartDocTest(1+1, 2)"), <impl.Test>[ |
| new impl.ExpectTest("1+1", "2"), |
| ]); |
| |
| // Two tests. |
| expect(extractTests(""" |
| // DartDocTest(1+1, 2) |
| // DartDocTest(2+40, 42) |
| """), <impl.Test>[ |
| new impl.ExpectTest("1+1", "2"), |
| new impl.ExpectTest("2+40", "42"), |
| ]); |
| |
| // Two valid tests. Four invalid ones. |
| expect(extractTests(""" |
| // DartDocTest(1+1, 2) |
| // DartDocTest(2+40; 42] |
| // DartDocTest(2+40+, 42] |
| // DartDocTest(2+40, 42] |
| // DartDocTest(2+40, 42) |
| // DartDocTest(2+40, 42+) |
| """), <impl.Test>[ |
| new impl.ExpectTest("1+1", "2"), |
| new impl.TestParseError( |
| """darttest:/foo.dart:2:20: Expected ',' before this. |
| // DartDocTest(2+40; 42] |
| ^""", 42), |
| new impl.TestParseError("""Parse error(s): |
| |
| darttest:/foo.dart:3:21: Expected an identifier, but got ','. |
| // DartDocTest(2+40+, 42] |
| ^""", 68), |
| new impl.TestParseError( |
| """darttest:/foo.dart:4:24: Expected ')' before this. |
| // DartDocTest(2+40, 42] |
| ^""", 97), |
| new impl.ExpectTest("2+40", "42"), |
| new impl.TestParseError("""Parse error(s): |
| |
| darttest:/foo.dart:6:25: Expected an identifier, but got ')'. |
| // DartDocTest(2+40, 42+) |
| ^""", 148), |
| ]); |
| |
| // Two tests in block comments with back-ticks around tests. |
| expect(extractTests(""" |
| /** |
| * This is a test: |
| * ``` |
| * DartDocTest(1+1, 2) |
| * ``` |
| * and so is this: |
| * ``` |
| * DartDocTest(2+40, 42) |
| * ``` |
| */ |
| """), <impl.Test>[ |
| new impl.ExpectTest("1+1", "2"), |
| new impl.ExpectTest("2+40", "42"), |
| ]); |
| |
| // Two tests --- include linebreaks. |
| expect(extractTests(""" |
| /* |
| This is a test: |
| DartDocTest(1+1, |
| 2) |
| and so is this: |
| DartDocTest(2+ |
| 40, |
| 42) |
| */ |
| """), <impl.Test>[ |
| new impl.ExpectTest("1+1", "2"), |
| // The linebreak etc here is not stripped (at the moment at least) |
| new impl.ExpectTest("2+\n 40", "42"), |
| ]); |
| |
| // Two tests --- with parens and commas as string... |
| expect(extractTests(""" |
| // DartDocTest("(", "(") |
| // and so is this: |
| // DartDocTest(",)", ",)") |
| """), <impl.Test>[ |
| new impl.ExpectTest('"("', '"("'), |
| new impl.ExpectTest('",)"', '",)"'), |
| ]); |
| |
| // Await expression. |
| expect(extractTests(""" |
| // DartDocTest(await foo(), 42) |
| """), <impl.Test>[ |
| new impl.ExpectTest('await foo()', '42'), |
| ]); |
| } |
| |
| void testCommentExtraction() { |
| // No comment |
| expect(extractFirstComment(""), null); |
| |
| // Simple line comment at position 0. |
| expect( |
| extractFirstComment("// Hello"), new impl.CommentString(" Hello", 0)); |
| |
| // Simple line comment at position 5. |
| expect(extractFirstComment(" // Hello"), |
| new impl.CommentString(" Hello", 5)); |
| |
| // Multiline simple comment at position 20. |
| expect(extractFirstComment(""" |
| import 'foo.dart'; |
| |
| // This is |
| // a |
| // multiline |
| // comment |
| |
| import 'bar.dart'"""), new impl.CommentString(""" |
| This is |
| a |
| multiline |
| comment""", 20)); |
| |
| // Multiline simple comment (with 3 slashes) at position 20. |
| expect(extractFirstComment(""" |
| import 'foo.dart'; |
| |
| /// This is |
| /// a |
| /// multiline |
| /// comment |
| |
| import 'bar.dart'"""), new impl.CommentString(""" |
| This is |
| a |
| multiline |
| comment""", 20)); |
| |
| // Multiline comments with /* at position 20. |
| expect(extractFirstComment(""" |
| import 'foo.dart'; |
| |
| /* This is |
| a |
| multiline |
| comment |
| */ |
| |
| import 'bar.dart'"""), new impl.CommentString(""" |
| This is |
| a |
| multiline |
| comment |
| """, 20)); |
| |
| // Multiline comments with /* at position 20. Note that the comment has |
| // * at the start of the line but that they aren't stripped because the |
| // comments starts with /* and NOT with /**. |
| expect(extractFirstComment(""" |
| import 'foo.dart'; |
| |
| /* This is |
| *a |
| * multiline |
| *comment |
| */ |
| |
| import 'bar.dart'"""), new impl.CommentString(""" |
| This is |
| *a |
| * multiline |
| *comment |
| """, 20)); |
| |
| // Multiline comments with /** */ at position 20. Note that the comment has |
| // * at the start of the line and that they are stripped because the |
| // comments starts with /** and NOT with only /*. |
| expect(extractFirstComment(""" |
| import 'foo.dart'; |
| |
| /** This is |
| *a |
| * multiline |
| *comment |
| */ |
| |
| import 'bar.dart'"""), new impl.CommentString(""" |
| This is |
| a |
| multiline |
| comment |
| """, 20)); |
| |
| // Multiline comment with block comment inside at position 20. |
| // The block comment data is not stripped. |
| expect(extractFirstComment(""" |
| import 'foo.dart'; |
| |
| /// This is |
| /// /*a*/ |
| /// multiline comment"""), new impl.CommentString(""" |
| This is |
| /*a*/ |
| multiline comment""", 20)); |
| } |
| |
| int expectCalls = 0; |
| String? expectCategory; |
| |
| void expect(dynamic actual, dynamic expected) { |
| expectCalls++; |
| StringBuffer sb = new StringBuffer(); |
| if (!_expectImpl(actual, expected, sb)) { |
| if (sb.isNotEmpty) { |
| throw "Error! Expected '$expected' but got '$actual'" |
| "\n\n" |
| "Explanation: $sb."; |
| } else { |
| throw "Error! Expected '$expected' but got '$actual'"; |
| } |
| } |
| print("Expect #$expectCalls ($expectCategory) OK."); |
| } |
| |
| bool _expectImpl(dynamic actual, dynamic expected, StringBuffer explainer) { |
| if (identical(actual, expected)) return true; |
| if (actual == expected) return true; |
| if (actual == null || expected == null) return false; |
| if (actual is List && expected is List) { |
| if (actual.runtimeType != expected.runtimeType) { |
| explainer.write("List runtimeType difference: " |
| "${actual.runtimeType} vs ${expected.runtimeType}"); |
| return false; |
| } |
| if (actual.length != expected.length) { |
| explainer.write("List length difference: " |
| "${actual.length} vs ${expected.length}"); |
| return false; |
| } |
| for (int i = 0; i < actual.length; i++) { |
| if (actual[i] != expected[i]) { |
| explainer.write("List difference at index $i: " |
| "${actual[i]} vs ${expected[i]}"); |
| return false; |
| } |
| } |
| return true; |
| } |
| if (actual is List || expected is List) return false; |
| |
| if (actual is Map && expected is Map) { |
| if (actual.runtimeType != expected.runtimeType) return false; |
| if (actual.length != expected.length) return false; |
| for (dynamic key in actual.keys) { |
| if (!expected.containsKey(key)) return false; |
| if (actual[key] != expected[key]) return false; |
| } |
| return true; |
| } |
| if (actual is Map || expected is Map) return false; |
| |
| if (actual is Set && expected is Set) { |
| if (actual.runtimeType != expected.runtimeType) return false; |
| if (actual.length != expected.length) return false; |
| for (dynamic value in actual) { |
| if (!expected.contains(value)) return false; |
| } |
| return true; |
| } |
| if (actual is Set || expected is Set) return false; |
| |
| // More stuff? |
| return false; |
| } |
| |
| impl.CommentString? extractFirstComment(String test) { |
| Token firstToken = impl.scanRawBytes(utf8.encode(test) as Uint8List); |
| Token token = firstToken; |
| while (true) { |
| CommentToken? comment = token.precedingComments; |
| if (comment != null) { |
| return impl.extractComments(comment, test); |
| } |
| if (token.isEof) break; |
| Token? next = token.next; |
| if (next == null) break; |
| token = next; |
| } |
| return null; |
| } |
| |
| List<impl.Test> extractTests(String test, [Uri? uri]) { |
| return impl.extractTests(utf8.encode(test) as Uint8List, |
| uri ?? new Uri(scheme: "darttest", path: "/foo.dart")); |
| } |