Wrote some crashtest for running against markdown files from pub.dev
diff --git a/pkgs/markdown/dart_test.yaml b/pkgs/markdown/dart_test.yaml new file mode 100644 index 0000000..423fa52 --- /dev/null +++ b/pkgs/markdown/dart_test.yaml
@@ -0,0 +1,6 @@ +tags: + crashtest: + skip: 'Only run crashtest tests manually with `dart test -P crashtest`' + presets: + crashtest: + skip: false # Don't skip when running in -P crashtest
diff --git a/pkgs/markdown/pubspec.yaml b/pkgs/markdown/pubspec.yaml index 4a69ba5..660a558 100644 --- a/pkgs/markdown/pubspec.yaml +++ b/pkgs/markdown/pubspec.yaml
@@ -21,9 +21,12 @@ build_web_compilers: ^3.0.0 collection: ^1.15.0 html: ^0.15.0 + http: ^0.13.5 io: ^1.0.0 js: ^0.6.3 lints: ^2.0.0 path: ^1.8.0 + pool: ^1.5.1 + tar: ^0.5.5+1 test: ^1.16.0 yaml: ^3.0.0
diff --git a/pkgs/markdown/test/crash_test.dart b/pkgs/markdown/test/crash_test.dart new file mode 100644 index 0000000..b85cc97 --- /dev/null +++ b/pkgs/markdown/test/crash_test.dart
@@ -0,0 +1,122 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:convert'; +import 'dart:io'; + +import 'package:http/http.dart' as http; +import 'package:http/retry.dart' as http; +import 'package:markdown/markdown.dart'; +import 'package:pool/pool.dart'; +import 'package:tar/tar.dart'; +import 'package:test/test.dart'; + +void main() async { + // This test is a really dumb and very slow crash-test. + // It downloads the latest package version for each package on pub.dev + // and tries to parse all `*.md` files in the package, counting the number + // of times where the parser throws. + // + // Needless to say, this test is very slow and running it eats a lot of CPU. + // But it's a fairly good way to try a lot of real-world markdown text to see + // if any of the poorly formatted markdown causes the parser to crash. + test( + 'crash test', + () async { + final c = http.RetryClient(http.Client()); + Future<dynamic> _getJson(String url) async { + final u = Uri.tryParse(url); + if (u == null) { + return null; + } + try { + final data = await c.read(u); + try { + return jsonDecode(data); + } on FormatException { + return null; + } + } on http.ClientException { + return null; + } on IOException { + return null; + } + } + + final packages = + ((await _getJson('https://pub.dev/api/package-names'))['packages'] + as List) + .cast<String>(); + print('Found ${packages.length} packages to scan'); + + final errors = <String>[]; + final pool = Pool(50); + var count = 0; + var skipped = 0; + var lastStatus = DateTime.now(); + await Future.wait(packages.map((package) async { + await pool.withResource(() async { + final versionsResponse = + await _getJson('https://pub.dev/api/packages/$package'); + final archiveUrl = Uri.tryParse( + versionsResponse['latest']?['archive_url'] as String? ?? '', + ); + if (archiveUrl == null) { + skipped++; + return; + } + late List<int> archive; + try { + archive = gzip.decode(await c.readBytes(archiveUrl)); + } on http.ClientException { + skipped++; + return; + } on IOException { + skipped++; + return; + } + try { + await TarReader.forEach(Stream.value(archive), (entry) async { + if (entry.name.endsWith('.md')) { + late String str; + try { + final bytes = await http.ByteStream(entry.contents).toBytes(); + str = utf8.decode(bytes); + } on FormatException { + return; // ignore invalid utf8 + } + try { + markdownToHtml(str, extensionSet: ExtensionSet.gitHubWeb); + } catch (err, st) { + errors + .add('package:$package/${entry.name}, throws: $err\n$st'); + } + } + }); + } on FormatException { + skipped++; + return; + } + }); + count++; + if (DateTime.now().difference(lastStatus) > Duration(seconds: 30)) { + lastStatus = DateTime.now(); + print('Scanned $count / ${packages.length} (skipped $skipped),' + ' found ${errors.length} issues'); + } + })); + + await pool.close(); + c.close(); + + if (errors.isNotEmpty) { + print('Found issues:'); + errors.forEach(print); + fail('Found ${errors.length} cases where markdownToHtml threw!'); + } + }, + timeout: Timeout(Duration(hours: 1)), + tags: 'crashtest', // skipped by default, see: dart_test.yaml + ); +}