@@ -20,26 +20,46 @@ import path from "path";
2020const __dirname = import . meta. dirname ;
2121const PROJECT_ROOT = path . join ( __dirname , "../.." ) ;
2222
23- const { values } = parseArgs ( {
24- args : process . argv . slice ( 2 ) ,
25- options : {
26- code : { type : "string" } ,
27- "coverage-dir" : { type : "string" , default : "build/coverage" } ,
28- help : { type : "boolean" , short : "h" , default : false } ,
29- } ,
30- } ) ;
23+ // The per-test coverage index (which ref test exercises which source
24+ // line/function) is rebuilt on every push to master and published to the
25+ // gh-pages branch of the pdf.js.refs repository.
26+ const PER_TEST_INDEX_URL =
27+ "https://raw.githubusercontent.com/mozilla/pdf.js.refs/gh-pages/per-test-index.json" ;
28+
29+ let values ;
30+ try {
31+ ( { values } = parseArgs ( {
32+ args : process . argv . slice ( 2 ) ,
33+ options : {
34+ code : { type : "string" } ,
35+ index : { type : "string" , default : "build/per-test-index.json" } ,
36+ "no-download" : { type : "boolean" , default : false } ,
37+ help : { type : "boolean" , short : "h" , default : false } ,
38+ } ,
39+ } ) ) ;
40+ } catch ( error ) {
41+ // parseArgs is strict, so an unknown/renamed option (e.g. the removed
42+ // --coverage-dir) would otherwise abort with an uncaught stack trace.
43+ console . error ( `Error: ${ error . message } ` ) ;
44+ console . error ( "Run with --help to see the available options." ) ;
45+ process . exit ( 1 ) ;
46+ }
3147
3248if ( values . help || ! values . code ) {
3349 console . log (
34- "Usage: coverage_search.mjs --code=<file>::<line|function> [--coverage-dir =<path>]\n\n" +
50+ "Usage: coverage_search.mjs --code=<file>::<line|function> [--index =<path>] [--no-download ]\n\n" +
3551 " --code Source file and line number or function name to search for.\n" +
3652 " Examples:\n" +
3753 " --code=canvas.js::205\n" +
3854 " --code=canvas.js::drawImageAtIntegerCoords\n" +
39- " --coverage-dir Coverage directory containing per-test-index.json [build/coverage]\n\n" +
55+ " --index Where to cache or read the per-test index.\n" +
56+ " [build/per-test-index.json]\n" +
57+ " --no-download Don't contact the network; use the cached index as-is.\n\n" +
4058 "Prints to stdout the IDs of tests whose coverage includes the given line or\n" +
41- "function (one ID per line).\n" +
42- "Run browsertest with --coverage-per-test first to generate the index."
59+ "function (one ID per line).\n\n" +
60+ "The index is downloaded from the pdf.js.refs repository and cached locally;\n" +
61+ "it is only re-downloaded when the published file has changed.\n" +
62+ `Source: ${ PER_TEST_INDEX_URL } `
4363 ) ;
4464 process . exit ( values . help ? 0 : 1 ) ;
4565}
@@ -58,18 +78,129 @@ const isLine = /^\d+$/.test(location);
5878const lineNum = isLine ? parseInt ( location , 10 ) : null ;
5979const funcName = isLine ? null : location ;
6080
61- const coverageDir = path . isAbsolute ( values [ "coverage-dir" ] )
62- ? values [ "coverage-dir" ]
63- : path . join ( PROJECT_ROOT , values [ "coverage-dir" ] ) ;
81+ const indexPath = path . isAbsolute ( values . index )
82+ ? values . index
83+ : path . join ( PROJECT_ROOT , values . index ) ;
84+ // The ETag of the cached copy is stored alongside it, so the next run can ask
85+ // the server (via If-None-Match) to only re-send the file when it has changed.
86+ const etagPath = `${ indexPath } .etag` ;
87+
88+ // Refreshes the locally cached index from the published copy, downloading it
89+ // only when it has changed since the last run. When the network is unavailable
90+ // a previously cached copy is reused if present.
91+ async function refreshIndex ( ) {
92+ if ( values [ "no-download" ] ) {
93+ return ; // Freshness check disabled; the read below validates existence.
94+ }
95+
96+ const hasCached = fs . existsSync ( indexPath ) ;
97+
98+ // On any download failure, fall back to a previously cached copy when one
99+ // exists; otherwise there's nothing to search, so fail.
100+ const fallbackOrFail = reason => {
101+ if ( hasCached ) {
102+ console . error (
103+ `Warning: couldn't refresh per-test index (${ reason } ); using the cached copy.`
104+ ) ;
105+ return ;
106+ }
107+ console . error ( `Error: couldn't download per-test index (${ reason } ).` ) ;
108+ process . exit ( 1 ) ;
109+ } ;
110+
111+ const headers = { } ;
112+ if ( hasCached && fs . existsSync ( etagPath ) ) {
113+ const etag = fs . readFileSync ( etagPath , "utf8" ) . trim ( ) ;
114+ // Only forward a syntactically valid HTTP ETag (RFC 7232), so the cached
115+ // file's contents can't be used to inject arbitrary data into the request.
116+ if ( / ^ (?: W \/ ) ? " [ \x21 \x23 - \x7e ] * " $ / . test ( etag ) ) {
117+ headers [ "If-None-Match" ] = etag ;
118+ }
119+ }
120+
121+ let response ;
122+ try {
123+ console . error ( `Fetching per-test index from ${ PER_TEST_INDEX_URL } ...` ) ;
124+ response = await fetch ( PER_TEST_INDEX_URL , { headers } ) ;
125+ } catch ( error ) {
126+ fallbackOrFail ( error . message ) ;
127+ return ;
128+ }
129+
130+ if ( response . status === 304 ) {
131+ console . error ( "Per-test index is up to date." ) ;
132+ return ;
133+ }
134+ if ( ! response . ok ) {
135+ fallbackOrFail ( `HTTP ${ response . status } ` ) ;
136+ return ;
137+ }
138+
139+ let text ;
140+ try {
141+ text = await response . text ( ) ;
142+ } catch ( error ) {
143+ fallbackOrFail ( error . message ) ;
144+ return ;
145+ }
146+
147+ // Parse the payload before caching it, and cache the re-serialized result
148+ // rather than the raw response body: only well-formed JSON produced by our
149+ // own JSON.stringify is ever written to disk.
150+ let serialized ;
151+ try {
152+ serialized = JSON . stringify ( JSON . parse ( text ) ) ;
153+ } catch {
154+ fallbackOrFail ( "the downloaded index is not valid JSON" ) ;
155+ return ;
156+ }
157+
158+ // Write to a temporary file and rename it into place.
159+ try {
160+ fs . mkdirSync ( path . dirname ( indexPath ) , { recursive : true } ) ;
161+ const tmpPath = `${ indexPath } .${ process . pid } .tmp` ;
162+ fs . writeFileSync ( tmpPath , serialized ) ;
163+ fs . renameSync ( tmpPath , indexPath ) ;
164+
165+ const etag = response . headers . get ( "etag" ) ;
166+ if ( etag ) {
167+ fs . writeFileSync ( etagPath , etag ) ;
168+ } else {
169+ fs . rmSync ( etagPath , { force : true } ) ;
170+ }
171+ } catch ( error ) {
172+ // A write failure (disk full, read-only dir, ...) shouldn't be fatal when
173+ // a usable cached copy already exists.
174+ fallbackOrFail ( error . message ) ;
175+ return ;
176+ }
177+ console . error ( `Per-test index updated (${ serialized . length } bytes).` ) ;
178+ }
179+
180+ await refreshIndex ( ) ;
64181
65- const indexPath = path . join ( coverageDir , "per-test-index.json" ) ;
66182if ( ! fs . existsSync ( indexPath ) ) {
67- console . error ( `Error: index file not found: ${ indexPath } ` ) ;
68- console . error ( "Run browsertest with --coverage-per-test first." ) ;
183+ console . error ( `Error: per-test index not found: ${ indexPath } ` ) ;
184+ console . error (
185+ "Build it locally (gulp botbrowsertest --coverage-per-test) or omit " +
186+ "--no-download to fetch it from the pdf.js.refs repository."
187+ ) ;
69188 process . exit ( 1 ) ;
70189}
71190
72- const { ids, files } = JSON . parse ( fs . readFileSync ( indexPath , "utf8" ) ) ;
191+ let ids , files ;
192+ try {
193+ ( { ids, files } = JSON . parse ( fs . readFileSync ( indexPath , "utf8" ) ) ) ;
194+ } catch ( error ) {
195+ console . error (
196+ `Error: couldn't read per-test index at ${ indexPath } : ${ error . message } `
197+ ) ;
198+ console . error (
199+ "The cached index may be corrupt; delete it and re-run without " +
200+ "--no-download to refetch it."
201+ ) ;
202+ process . exit ( 1 ) ;
203+ }
73204
74205// Find the file entry whose path matches fileName.
75206let fileEntry = null ;
0 commit comments