4
4
5
5
import 'dart:convert' ;
6
6
import 'dart:io' ;
7
+ import 'dart:isolate' ;
7
8
8
9
import 'package:http/http.dart' as http;
9
10
import 'package:http/retry.dart' as http;
@@ -14,6 +15,18 @@ import 'package:test/test.dart';
14
15
15
16
// ignore_for_file: avoid_dynamic_calls
16
17
18
+ const extensions = [
19
+ '.md' ,
20
+ '.mkd' ,
21
+ '.mdwn' ,
22
+ '.mdown' ,
23
+ '.mdtxt' ,
24
+ '.mdtext' ,
25
+ '.markdown' ,
26
+ 'README' ,
27
+ 'CHANGELOG' ,
28
+ ];
29
+
17
30
void main () async {
18
31
// This test is a really dumb and very slow crash-test.
19
32
// It downloads the latest package version for each package on pub.dev
@@ -26,6 +39,16 @@ void main() async {
26
39
test (
27
40
'crash test' ,
28
41
() 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
+
29
52
final c = http.RetryClient (http.Client ());
30
53
Future <dynamic > getJson (String url) async {
31
54
final u = Uri .tryParse (url);
@@ -50,79 +73,154 @@ void main() async {
50
73
((await getJson ('https://pub.dev/api/package-names' ))['packages' ]
51
74
as List )
52
75
.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' );
54
78
55
- final errors = < String > [];
56
- final pool = Pool (50 );
57
79
var count = 0 ;
58
- var skipped = 0 ;
59
- var lastStatus = DateTime . now () ;
80
+ final pool = Pool ( 50 ) ;
81
+ final packageVersions = < PackageVersion > [] ;
60
82
await Future .wait (packages.map ((package) async {
61
83
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 ' ,
66
86
);
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);
67
110
if (archiveUrl == null ) {
68
111
skipped++ ;
69
112
return ;
70
113
}
71
114
late List <int > archive;
72
115
try {
73
- archive = gzip. decode ( await c.readBytes (archiveUrl) );
116
+ archive = await c.readBytes (archiveUrl);
74
117
} on http.ClientException {
75
118
skipped++ ;
76
119
return ;
77
120
} on IOException {
78
121
skipped++ ;
79
122
return ;
80
123
}
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 ) {
103
133
skipped++ ;
104
134
return ;
105
135
}
136
+
137
+ errors.addAll (result);
138
+ result.forEach (print);
106
139
});
107
140
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' );
114
144
}));
115
145
116
146
await pool.close ();
117
147
c.close ();
118
148
149
+ print ('## Finished scanning' );
150
+ print ('Scanned ${packageVersions .length } package versions in '
151
+ '${DateTime .now ().difference (started )}' );
152
+
119
153
if (errors.isNotEmpty) {
120
154
print ('Found issues:' );
121
155
errors.forEach (print);
122
156
fail ('Found ${errors .length } cases where markdownToHtml threw!' );
123
157
}
124
158
},
125
- timeout: const Timeout (Duration (hours: 1 )),
159
+ timeout: const Timeout (Duration (hours: 5 )),
126
160
tags: 'crash_test' , // skipped by default, see: dart_test.yaml
127
161
);
128
162
}
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