Skip to content
This repository was archived by the owner on Feb 24, 2025. It is now read-only.

Commit 0745989

Browse files
authored
Merge d5d7432 into dd47c5d
2 parents dd47c5d + d5d7432 commit 0745989

File tree

2 files changed

+157
-39
lines changed

2 files changed

+157
-39
lines changed

.github/workflows/crash_test.yaml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Run against all markdown files in latest version of packages on pub.dev to
2+
# see if any can provoke a crash
3+
4+
name: Crash Tests
5+
6+
on:
7+
schedule:
8+
# “At 00:00 (UTC) on Sunday.”
9+
- cron: '0 0 * * 0'
10+
11+
jobs:
12+
crash-test:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
16+
- uses: dart-lang/setup-dart@fedb1266e91cf51be2fdb382869461a434b920a3
17+
- name: Install dependencies
18+
run: dart pub get
19+
- name: Run crash_test.dart
20+
run: dart test -P crash_test test/crash_test.dart

test/crash_test.dart

Lines changed: 137 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import 'dart:convert';
66
import 'dart:io';
7+
import 'dart:isolate';
78

89
import 'package:http/http.dart' as http;
910
import 'package:http/retry.dart' as http;
@@ -14,6 +15,18 @@ import 'package:test/test.dart';
1415

1516
// ignore_for_file: avoid_dynamic_calls
1617

18+
const extensions = [
19+
'.md',
20+
'.mkd',
21+
'.mdwn',
22+
'.mdown',
23+
'.mdtxt',
24+
'.mdtext',
25+
'.markdown',
26+
'README',
27+
'CHANGELOG',
28+
];
29+
1730
void main() async {
1831
// This test is a really dumb and very slow crash-test.
1932
// It downloads the latest package version for each package on pub.dev
@@ -26,6 +39,16 @@ void main() async {
2639
test(
2740
'crash test',
2841
() async {
42+
final started = DateTime.now();
43+
var lastStatus = DateTime(0);
44+
void status(String Function() message) {
45+
if (DateTime.now().difference(lastStatus) >
46+
const Duration(seconds: 30)) {
47+
lastStatus = DateTime.now();
48+
print(message());
49+
}
50+
}
51+
2952
final c = http.RetryClient(http.Client());
3053
Future<dynamic> getJson(String url) async {
3154
final u = Uri.tryParse(url);
@@ -50,79 +73,154 @@ void main() async {
5073
((await getJson('https://pub.dev/api/package-names'))['packages']
5174
as List)
5275
.cast<String>();
53-
print('Found ${packages.length} packages to scan');
76+
//.take(3).toList(); // useful when testing
77+
print('## Found ${packages.length} packages to scan');
5478

55-
final errors = <String>[];
56-
final pool = Pool(50);
5779
var count = 0;
58-
var skipped = 0;
59-
var lastStatus = DateTime.now();
80+
final pool = Pool(50);
81+
final packageVersions = <PackageVersion>[];
6082
await Future.wait(packages.map((package) async {
6183
await pool.withResource(() async {
62-
final versionsResponse =
63-
await getJson('https://pub.dev/api/packages/$package');
64-
final archiveUrl = Uri.tryParse(
65-
versionsResponse['latest']?['archive_url'] as String? ?? '',
84+
final response = await getJson(
85+
'https://pub.dev/api/packages/$package',
6686
);
87+
final entry = response['latest'] as Map?;
88+
if (entry != null) {
89+
packageVersions.add(PackageVersion(
90+
package: package,
91+
version: entry['version'] as String,
92+
archiveUrl: entry['archive_url'] as String,
93+
));
94+
}
95+
count++;
96+
status(
97+
() => 'Listed versions for $count / ${packages.length} packages',
98+
);
99+
});
100+
}));
101+
102+
print('## Found ${packageVersions.length} package versions to scan');
103+
104+
count = 0;
105+
final errors = <String>[];
106+
var skipped = 0;
107+
await Future.wait(packageVersions.map((pv) async {
108+
await pool.withResource(() async {
109+
final archiveUrl = Uri.tryParse(pv.archiveUrl);
67110
if (archiveUrl == null) {
68111
skipped++;
69112
return;
70113
}
71114
late List<int> archive;
72115
try {
73-
archive = gzip.decode(await c.readBytes(archiveUrl));
116+
archive = await c.readBytes(archiveUrl);
74117
} on http.ClientException {
75118
skipped++;
76119
return;
77120
} on IOException {
78121
skipped++;
79122
return;
80123
}
81-
try {
82-
await TarReader.forEach(Stream.value(archive), (entry) async {
83-
if (entry.name.endsWith('.md')) {
84-
late String contents;
85-
try {
86-
final bytes = await http.ByteStream(entry.contents).toBytes();
87-
contents = utf8.decode(bytes);
88-
} on FormatException {
89-
return; // ignore invalid utf8
90-
}
91-
try {
92-
markdownToHtml(
93-
contents,
94-
extensionSet: ExtensionSet.gitHubWeb,
95-
);
96-
} catch (err, st) {
97-
errors
98-
.add('package:$package/${entry.name}, throws: $err\n$st');
99-
}
100-
}
101-
});
102-
} on FormatException {
124+
125+
final result = await _findMarkdownIssues(
126+
pv.package,
127+
pv.version,
128+
archive,
129+
);
130+
131+
// If tar decoding fails.
132+
if (result == null) {
103133
skipped++;
104134
return;
105135
}
136+
137+
errors.addAll(result);
138+
result.forEach(print);
106139
});
107140
count++;
108-
if (DateTime.now().difference(lastStatus) >
109-
const Duration(seconds: 30)) {
110-
lastStatus = DateTime.now();
111-
print('Scanned $count / ${packages.length} (skipped $skipped),'
112-
' found ${errors.length} issues');
113-
}
141+
status(() =>
142+
'Scanned $count / ${packageVersions.length} (skipped $skipped),'
143+
' found ${errors.length} issues');
114144
}));
115145

116146
await pool.close();
117147
c.close();
118148

149+
print('## Finished scanning');
150+
print('Scanned ${packageVersions.length} package versions in '
151+
'${DateTime.now().difference(started)}');
152+
119153
if (errors.isNotEmpty) {
120154
print('Found issues:');
121155
errors.forEach(print);
122156
fail('Found ${errors.length} cases where markdownToHtml threw!');
123157
}
124158
},
125-
timeout: const Timeout(Duration(hours: 1)),
159+
timeout: const Timeout(Duration(hours: 5)),
126160
tags: 'crash_test', // skipped by default, see: dart_test.yaml
127161
);
128162
}
163+
164+
class PackageVersion {
165+
final String package;
166+
final String version;
167+
final String archiveUrl;
168+
169+
PackageVersion({
170+
required this.package,
171+
required this.version,
172+
required this.archiveUrl,
173+
});
174+
}
175+
176+
/// Scans [gzippedArchive] for markdown files and tries to parse them all.
177+
///
178+
/// Creates a list of issues that arose when parsing markdown files. The
179+
/// [package] and [version] strings are used to construct nice issues.
180+
/// An issue string may be multi-line, but should be printable.
181+
///
182+
/// Returns a list of issues, or `null` if decoding and parsing [gzippedArchive]
183+
/// failed.
184+
Future<List<String>?> _findMarkdownIssues(
185+
String package,
186+
String version,
187+
List<int> gzippedArchive,
188+
) async {
189+
return Isolate.run<List<String>?>(() async {
190+
try {
191+
final archive = gzip.decode(gzippedArchive);
192+
final issues = <String>[];
193+
await TarReader.forEach(Stream.value(archive), (entry) async {
194+
if (extensions.any((ext) => entry.name.endsWith(ext))) {
195+
late String contents;
196+
try {
197+
final bytes = await http.ByteStream(entry.contents).toBytes();
198+
contents = utf8.decode(bytes);
199+
} on FormatException {
200+
return; // ignore invalid utf8
201+
}
202+
final start = DateTime.now();
203+
try {
204+
markdownToHtml(
205+
contents,
206+
extensionSet: ExtensionSet.gitHubWeb,
207+
);
208+
} catch (err, st) {
209+
issues.add(
210+
'package:$package-$version/${entry.name}, throws: $err\n$st');
211+
}
212+
final time = DateTime.now().difference(start);
213+
if (time.inSeconds > 30) {
214+
issues.add(
215+
'package:$package-$version/${entry.name} took $time to process');
216+
}
217+
}
218+
});
219+
return issues;
220+
} on FormatException {
221+
return null;
222+
}
223+
}).timeout(const Duration(minutes: 2), onTimeout: () {
224+
return ['package:$package-$version failed to be processed in 2 minutes'];
225+
});
226+
}

0 commit comments

Comments
 (0)