tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

crawl.ts (6018B)


      1 // Node can look at the filesystem, but JS in the browser can't.
      2 // This crawls the file tree under src/suites/${suite} to generate a (non-hierarchical) static
      3 // listing file that can then be used in the browser to load the modules containing the tests.
      4 
      5 import * as fs from 'fs';
      6 import * as path from 'path';
      7 
      8 import { loadMetadataForSuite } from '../framework/metadata.js';
      9 import { SpecFile } from '../internal/file_loader.js';
     10 import { TestQueryMultiCase, TestQueryMultiFile } from '../internal/query/query.js';
     11 import { validQueryPart } from '../internal/query/validQueryPart.js';
     12 import { TestSuiteListingEntry, TestSuiteListing } from '../internal/test_suite_listing.js';
     13 import { assert, unreachable } from '../util/util.js';
     14 
     15 const specFileSuffix = __filename.endsWith('.ts') ? '.spec.ts' : '.spec.js';
     16 
     17 async function crawlFilesRecursively(dir: string): Promise<string[]> {
     18  const subpathInfo = await Promise.all(
     19    (await fs.promises.readdir(dir)).map(async d => {
     20      const p = path.join(dir, d);
     21      const stats = await fs.promises.stat(p);
     22      return {
     23        path: p,
     24        isDirectory: stats.isDirectory(),
     25        isFile: stats.isFile(),
     26      };
     27    })
     28  );
     29 
     30  const files = subpathInfo
     31    .filter(
     32      i =>
     33        i.isFile &&
     34        (i.path.endsWith(specFileSuffix) ||
     35          i.path.endsWith(`${path.sep}README.txt`) ||
     36          i.path === 'README.txt')
     37    )
     38    .map(i => i.path);
     39 
     40  return files.concat(
     41    await subpathInfo
     42      .filter(i => i.isDirectory)
     43      .map(i => crawlFilesRecursively(i.path))
     44      .reduce(async (a, b) => (await a).concat(await b), Promise.resolve([]))
     45  );
     46 }
     47 
     48 export async function crawl(
     49  suiteDir: string,
     50  opts: { validate: boolean; printMetadataWarnings: boolean } | null = null
     51 ): Promise<TestSuiteListingEntry[]> {
     52  if (!fs.existsSync(suiteDir)) {
     53    throw new Error(`Could not find suite: ${suiteDir}`);
     54  }
     55 
     56  let validateTimingsEntries;
     57  if (opts?.validate) {
     58    const metadata = loadMetadataForSuite(suiteDir);
     59    if (metadata) {
     60      validateTimingsEntries = {
     61        metadata,
     62        testsFoundInFiles: new Set<string>(),
     63      };
     64    }
     65  }
     66 
     67  // Crawl files and convert paths to be POSIX-style, relative to suiteDir.
     68  const filesToEnumerate = (await crawlFilesRecursively(suiteDir))
     69    .map(f => path.relative(suiteDir, f).replace(/\\/g, '/'))
     70    .sort();
     71 
     72  const entries: TestSuiteListingEntry[] = [];
     73  for (const file of filesToEnumerate) {
     74    // |file| is the suite-relative file path.
     75    if (file.endsWith(specFileSuffix)) {
     76      const filepathWithoutExtension = file.substring(0, file.length - specFileSuffix.length);
     77      const pathSegments = filepathWithoutExtension.split('/');
     78 
     79      const suite = path.basename(suiteDir);
     80 
     81      if (opts?.validate) {
     82        const filename = `../../${suite}/${filepathWithoutExtension}.spec.js`;
     83 
     84        assert(!process.env.STANDALONE_DEV_SERVER);
     85        const mod = (await import(filename)) as SpecFile;
     86        assert(mod.description !== undefined, 'Test spec file missing description: ' + filename);
     87        assert(mod.g !== undefined, 'Test spec file missing TestGroup definition: ' + filename);
     88 
     89        mod.g.validate(new TestQueryMultiFile(suite, pathSegments));
     90 
     91        for (const { testPath } of mod.g.collectNonEmptyTests()) {
     92          const testQuery = new TestQueryMultiCase(suite, pathSegments, testPath, {}).toString();
     93          if (validateTimingsEntries) {
     94            validateTimingsEntries.testsFoundInFiles.add(testQuery);
     95          }
     96        }
     97      }
     98 
     99      for (const p of pathSegments) {
    100        assert(validQueryPart.test(p), `Invalid directory name ${p}; must match ${validQueryPart}`);
    101      }
    102      entries.push({ file: pathSegments });
    103    } else if (path.basename(file) === 'README.txt') {
    104      const dirname = path.dirname(file);
    105      const readme = fs.readFileSync(path.join(suiteDir, file), 'utf8').trim();
    106 
    107      const pathSegments = dirname !== '.' ? dirname.split('/') : [];
    108      entries.push({ file: pathSegments, readme });
    109    } else {
    110      unreachable(`Matched an unrecognized filename ${file}`);
    111    }
    112  }
    113 
    114  if (validateTimingsEntries) {
    115    const zeroEntries = [];
    116    const staleEntries = [];
    117    for (const [metadataKey, metadataValue] of Object.entries(validateTimingsEntries.metadata)) {
    118      if (metadataKey.startsWith('_')) {
    119        // Ignore json "_comments".
    120        continue;
    121      }
    122      if (metadataValue.subcaseMS <= 0) {
    123        zeroEntries.push(metadataKey);
    124      }
    125      if (!validateTimingsEntries.testsFoundInFiles.has(metadataKey)) {
    126        staleEntries.push(metadataKey);
    127      }
    128    }
    129    if (zeroEntries.length && opts?.printMetadataWarnings) {
    130      console.warn(
    131        'WARNING: subcaseMS ≤ 0 found in listing_meta.json (see docs/adding_timing_metadata.md):'
    132      );
    133      for (const metadataKey of zeroEntries) {
    134        console.warn(`  ${metadataKey}`);
    135      }
    136    }
    137 
    138    if (opts?.printMetadataWarnings) {
    139      const missingEntries = [];
    140      for (const metadataKey of validateTimingsEntries.testsFoundInFiles) {
    141        if (!(metadataKey in validateTimingsEntries.metadata)) {
    142          missingEntries.push(metadataKey);
    143        }
    144      }
    145      if (missingEntries.length) {
    146        console.error(
    147          'WARNING: Tests missing from listing_meta.json (see docs/adding_timing_metadata.md):'
    148        );
    149        for (const metadataKey of missingEntries) {
    150          console.error(`  ${metadataKey}`);
    151        }
    152      }
    153    }
    154 
    155    if (staleEntries.length) {
    156      console.error('ERROR: Non-existent tests found in listing_meta.json. Please update:');
    157      for (const metadataKey of staleEntries) {
    158        console.error(`  ${metadataKey}`);
    159      }
    160      unreachable();
    161    }
    162  }
    163 
    164  return entries;
    165 }
    166 
    167 export function makeListing(filename: string): Promise<TestSuiteListing> {
    168  // Don't validate. This path is only used for the dev server and running tests with Node.
    169  // Validation is done for listing generation and presubmit.
    170  return crawl(path.dirname(filename));
    171 }