tor-browser

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

tree.js (20249B)


      1 /**
      2 * AUTO-GENERATED - DO NOT EDIT. Source: https://github.com/gpuweb/cts
      3 **/import { loadMetadataForSuite } from '../framework/metadata.js';import { globalTestConfig } from '../framework/test_config.js';
      4 import { assert, now } from '../util/util.js';
      5 
      6 
      7 
      8 import { comparePublicParamsPaths, compareQueries, Ordering } from './query/compare.js';
      9 import {
     10 
     11  TestQueryMultiCase,
     12  TestQuerySingleCase,
     13  TestQueryMultiFile,
     14  TestQueryMultiTest } from
     15 './query/query.js';
     16 import { kBigSeparator, kWildcard, kPathSeparator, kParamSeparator } from './query/separators.js';
     17 import { stringifySingleParam } from './query/stringify_params.js';
     18 import { StacklessError } from './util.js';
     19 
     20 // `loadTreeForQuery()` loads a TestTree for a given queryToLoad.
     21 // The resulting tree is a linked-list all the way from `suite:*` to queryToLoad,
     22 // and under queryToLoad is a tree containing every case matched by queryToLoad.
     23 //
     24 // `subqueriesToExpand` influences the `collapsible` flag on nodes in the resulting tree.
     25 // A node is considered "collapsible" if none of the subqueriesToExpand is a StrictSubset
     26 // of that node.
     27 //
     28 // In WebKit/Blink-style web_tests, an expectation file marks individual cts.https.html "variants
     29 // as "Failure", "Crash", etc. By passing in the list of expectations as the subqueriesToExpand,
     30 // we can programmatically subdivide the cts.https.html "variants" list to be able to implement
     31 // arbitrarily-fine suppressions (instead of having to suppress entire test files, which would
     32 // lose a lot of coverage).
     33 //
     34 // `iterateCollapsedNodes()` produces the list of queries for the variants list.
     35 //
     36 // Though somewhat complicated, this system has important benefits:
     37 //   - Avoids having to suppress entire test files, which would cause large test coverage loss.
     38 //   - Minimizes the number of page loads needed for fine-grained suppressions.
     39 //     (In the naive case, we could do one page load per test case - but the test suite would
     40 //     take impossibly long to run.)
     41 //   - Enables developers to put any number of tests in one file as appropriate, without worrying
     42 //     about expectation granularity.
     43 
     44 
     45 
     46 
     47 
     48 
     49 
     50 
     51 
     52 
     53 
     54 
     55 
     56 
     57 
     58 
     59 
     60 
     61 
     62 
     63 
     64 
     65 
     66 
     67 
     68 
     69 
     70 
     71 
     72 /**
     73 * When iterating through "collapsed" tree nodes, indicates how many "query levels" to traverse
     74 * through before starting to collapse nodes.
     75 *
     76 * Corresponds with TestQueryLevel, but excludes 4 (SingleCase):
     77 * - 1 = MultiFile. Expands so every file is in the collapsed tree.
     78 * - 2 = MultiTest. Expands so every test is in the collapsed tree.
     79 * - 3 = MultiCase. Expands so every case is in the collapsed tree (i.e. collapsing disabled).
     80 */
     81 
     82 
     83 export class TestTree {
     84  /**
     85   * The `queryToLoad` that this test tree was created for.
     86   * Test trees are always rooted at `suite:*`, but they only contain nodes that fit
     87   * within `forQuery`.
     88   *
     89   * This is used for `iterateCollapsedNodes` which only starts collapsing at the next
     90   * `TestQueryLevel` after `forQuery`.
     91   */
     92 
     93 
     94 
     95  constructor(forQuery, root) {
     96    this.forQuery = forQuery;
     97    this.root = root;
     98    assert(
     99      root.query.level === 1 && root.query.depthInLevel === 0,
    100      'TestTree root must be the root (suite:*)'
    101    );
    102  }
    103 
    104  static async create(
    105  forQuery,
    106  root,
    107  maxChunkTime)
    108  {
    109    const suite = forQuery.suite;
    110 
    111    let chunking = undefined;
    112    if (Number.isFinite(maxChunkTime)) {
    113      const metadata = loadMetadataForSuite(`./src/${suite}`);
    114      assert(metadata !== null, `metadata for ${suite} is missing, but maxChunkTime was requested`);
    115      chunking = { metadata, maxChunkTime };
    116    }
    117    await TestTree.propagateCounts(root, chunking);
    118 
    119    return new TestTree(forQuery, root);
    120  }
    121 
    122  /**
    123   * Iterate through the leaves of a version of the tree which has been pruned to exclude
    124   * subtrees which:
    125   * - are at a deeper `TestQueryLevel` than `this.forQuery`, and
    126   * - were not a `Ordering.StrictSubset` of any of the `subqueriesToExpand` during tree creation.
    127   */
    128  iterateCollapsedNodes({
    129    includeIntermediateNodes = false,
    130    includeEmptySubtrees = false,
    131    alwaysExpandThroughLevel
    132 
    133 
    134 
    135 
    136 
    137 
    138 
    139  }) {
    140    const expandThroughLevel = Math.max(this.forQuery.level, alwaysExpandThroughLevel);
    141    return TestTree.iterateSubtreeNodes(this.root, {
    142      includeIntermediateNodes,
    143      includeEmptySubtrees,
    144      expandThroughLevel
    145    });
    146  }
    147 
    148  iterateLeaves() {
    149    return TestTree.iterateSubtreeLeaves(this.root);
    150  }
    151 
    152  /**
    153   * Dissolve nodes which have only one child, e.g.:
    154   *   a,* { a,b,* { a,b:* { ... } } }
    155   * collapses down into:
    156   *   a,* { a,b:* { ... } }
    157   * which is less needlessly verbose when displaying the tree in the standalone runner.
    158   */
    159  dissolveSingleChildTrees() {
    160    const newRoot = dissolveSingleChildTrees(this.root);
    161    assert(newRoot === this.root);
    162  }
    163 
    164  toString() {
    165    return TestTree.subtreeToString('(root)', this.root, '');
    166  }
    167 
    168  static *iterateSubtreeNodes(
    169  subtree,
    170  opts)
    171 
    172 
    173 
    174 
    175  {
    176    if (opts.includeIntermediateNodes) {
    177      yield subtree;
    178    }
    179 
    180    for (const [, child] of subtree.children) {
    181      if ('children' in child) {
    182        // Is a subtree
    183        const collapsible = child.collapsible && child.query.level > opts.expandThroughLevel;
    184        if (child.children.size > 0 && !collapsible) {
    185          yield* TestTree.iterateSubtreeNodes(child, opts);
    186        } else if (child.children.size > 0 || opts.includeEmptySubtrees) {
    187          // Don't yield empty subtrees (e.g. files with no tests) unless includeEmptySubtrees
    188          yield child;
    189        }
    190      } else {
    191        // Is a leaf
    192        yield child;
    193      }
    194    }
    195  }
    196 
    197  static *iterateSubtreeLeaves(subtree) {
    198    for (const [, child] of subtree.children) {
    199      if ('children' in child) {
    200        yield* TestTree.iterateSubtreeLeaves(child);
    201      } else {
    202        yield child;
    203      }
    204    }
    205  }
    206 
    207  /** Propagate the subtreeTODOs/subtreeTests state upward from leaves to parent nodes. */
    208  static async propagateCounts(
    209  subtree,
    210  chunking)
    211  {
    212    subtree.subtreeCounts ??= { tests: 0, nodesWithTODO: 0, totalTimeMS: 0 };
    213    subtree.subcaseCount = 0;
    214    for (const [, child] of subtree.children) {
    215      if ('children' in child) {
    216        const counts = await TestTree.propagateCounts(child, chunking);
    217        subtree.subtreeCounts.tests += counts.tests;
    218        subtree.subtreeCounts.nodesWithTODO += counts.nodesWithTODO;
    219        subtree.subtreeCounts.totalTimeMS += counts.totalTimeMS;
    220        subtree.subcaseCount += counts.subcaseCount;
    221      } else {
    222        subtree.subcaseCount = child.subcaseCount;
    223      }
    224    }
    225 
    226    // If we're chunking based on a maxChunkTime, then at each
    227    // TestQueryMultiCase node of the tree we look at its total time. If the
    228    // total time is larger than the maxChunkTime, we set collapsible=false to
    229    // make sure it gets split up in the output. Note:
    230    // - TestQueryMultiTest and higher nodes are never set to collapsible anyway, so we ignore them.
    231    // - TestQuerySingleCase nodes can't be collapsed, so we ignore them.
    232    if (chunking && subtree.query instanceof TestQueryMultiCase) {
    233      const testLevelQuery = new TestQueryMultiCase(
    234        subtree.query.suite,
    235        subtree.query.filePathParts,
    236        subtree.query.testPathParts,
    237        {}
    238      ).toString();
    239 
    240      const metadata = chunking.metadata;
    241 
    242      const subcaseTiming = metadata[testLevelQuery]?.subcaseMS;
    243      if (subcaseTiming !== undefined) {
    244        const totalTiming = subcaseTiming * subtree.subcaseCount;
    245        subtree.subtreeCounts.totalTimeMS = totalTiming;
    246        if (totalTiming > chunking.maxChunkTime) {
    247          subtree.collapsible = false;
    248        }
    249      }
    250    }
    251 
    252    return { ...subtree.subtreeCounts, subcaseCount: subtree.subcaseCount ?? 0 };
    253  }
    254 
    255  /** Displays counts in the format `(Nodes with TODOs) / (Total test count)`. */
    256  static countsToString(tree) {
    257    if (tree.subtreeCounts) {
    258      return `${tree.subtreeCounts.nodesWithTODO} / ${tree.subtreeCounts.tests}`;
    259    } else {
    260      return '';
    261    }
    262  }
    263 
    264  static subtreeToString(name, tree, indent) {
    265    const collapsible = 'run' in tree ? '>' : tree.collapsible ? '+' : '-';
    266    let s =
    267    indent +
    268    `${collapsible} ${TestTree.countsToString(tree)} ${JSON.stringify(name)} => ${tree.query}`;
    269    if ('children' in tree) {
    270      if (tree.description !== undefined) {
    271        s += `\n${indent}  | ${JSON.stringify(tree.description)}`;
    272      }
    273 
    274      for (const [name, child] of tree.children) {
    275        s += '\n' + TestTree.subtreeToString(name, child, indent + '  ');
    276      }
    277    }
    278    return s;
    279  }
    280 }
    281 
    282 // MAINTENANCE_TODO: Consider having subqueriesToExpand actually impact the depth-order of params
    283 // in the tree.
    284 export async function loadTreeForQuery(
    285 loader,
    286 queryToLoad,
    287 {
    288  subqueriesToExpand,
    289  fullyExpandSubtrees = [],
    290  maxChunkTime = Infinity
    291 })
    292 {
    293  const suite = queryToLoad.suite;
    294  const specs = await loader.listing(suite);
    295 
    296  const subqueriesToExpandEntries = Array.from(subqueriesToExpand.entries());
    297  const seenSubqueriesToExpand = new Array(subqueriesToExpand.length);
    298  seenSubqueriesToExpand.fill(false);
    299 
    300  const isCollapsible = (subquery) =>
    301  subqueriesToExpandEntries.every(([i, toExpand]) => {
    302    const ordering = compareQueries(toExpand, subquery);
    303 
    304    // If toExpand == subquery, no expansion is needed (but it's still "seen").
    305    if (ordering === Ordering.Equal) seenSubqueriesToExpand[i] = true;
    306    return ordering !== Ordering.StrictSubset;
    307  }) &&
    308  fullyExpandSubtrees.every((toExpand) => {
    309    const ordering = compareQueries(toExpand, subquery);
    310    return ordering === Ordering.Unordered;
    311  });
    312 
    313  // L0 = suite-level, e.g. suite:*
    314  // L1 =  file-level, e.g. suite:a,b:*
    315  // L2 =  test-level, e.g. suite:a,b:c,d:*
    316  // L3 =  case-level, e.g. suite:a,b:c,d:
    317  let foundCase = false;
    318  // L0 is suite:*
    319  const subtreeL0 = makeTreeForSuite(suite, isCollapsible);
    320 
    321  const imports_start = now();
    322  const pEntriesWithImports = []; // Promise<entry with importedSpec>[]
    323  for (const entry of specs) {
    324    if (entry.file.length === 0 && 'readme' in entry) {
    325      // Suite-level readme.
    326      setSubtreeDescriptionAndCountTODOs(subtreeL0, entry.readme);
    327      continue;
    328    }
    329 
    330    {
    331      const queryL1 = new TestQueryMultiFile(suite, entry.file);
    332      const orderingL1 = compareQueries(queryL1, queryToLoad);
    333      if (orderingL1 === Ordering.Unordered) {
    334        // File path is not matched by this query.
    335        continue;
    336      }
    337    }
    338 
    339    // We're going to be fetching+importing a bunch of things, so do it in async.
    340    const pEntryWithImport = (async () => {
    341      if ('readme' in entry) {
    342        return entry;
    343      } else {
    344        return {
    345          ...entry,
    346          importedSpec: await loader.importSpecFile(queryToLoad.suite, entry.file)
    347        };
    348      }
    349    })();
    350 
    351    const kForceSerialImporting = false;
    352    if (kForceSerialImporting) {
    353      await pEntryWithImport;
    354    }
    355    pEntriesWithImports.push(pEntryWithImport);
    356  }
    357 
    358  const entriesWithImports = await Promise.all(pEntriesWithImports);
    359  if (globalTestConfig.frameworkDebugLog) {
    360    const imported_time = performance.now() - imports_start;
    361    globalTestConfig.frameworkDebugLog(
    362      `Imported importedSpecFiles[${entriesWithImports.length}] in ${imported_time}ms.`
    363    );
    364  }
    365 
    366  for (const entry of entriesWithImports) {
    367    if ('readme' in entry) {
    368      // Entry is a README that is an ancestor or descendant of the query.
    369      // (It's included for display in the standalone runner.)
    370 
    371      // readmeSubtree is suite:a,b,*
    372      // (This is always going to dedup with a file path, if there are any test spec files under
    373      // the directory that has the README).
    374      const readmeSubtree = addSubtreeForDirPath(
    375        subtreeL0,
    376        entry.file,
    377        isCollapsible
    378      );
    379      setSubtreeDescriptionAndCountTODOs(readmeSubtree, entry.readme);
    380      continue;
    381    }
    382 
    383    // Entry is a spec file.
    384    const spec = entry.importedSpec;
    385    // subtreeL1 is suite:a,b:*
    386    const subtreeL1 = addSubtreeForFilePath(
    387      subtreeL0,
    388      entry.file,
    389      isCollapsible
    390    );
    391    setSubtreeDescriptionAndCountTODOs(subtreeL1, spec.description);
    392 
    393    let groupHasTests = false;
    394    for (const t of spec.g.iterate()) {
    395      groupHasTests = true;
    396      {
    397        const queryL2 = new TestQueryMultiCase(suite, entry.file, t.testPath, {});
    398        const orderingL2 = compareQueries(queryL2, queryToLoad);
    399        if (orderingL2 === Ordering.Unordered) {
    400          // Test path is not matched by this query.
    401          continue;
    402        }
    403      }
    404 
    405      // subtreeL2 is suite:a,b:c,d:*
    406      const subtreeL2 = addSubtreeForTestPath(
    407        subtreeL1,
    408        t.testPath,
    409        t.testCreationStack,
    410        isCollapsible
    411      );
    412      // This is 1 test. Set tests=1 then count TODOs.
    413      subtreeL2.subtreeCounts ??= { tests: 1, nodesWithTODO: 0, totalTimeMS: 0 };
    414      if (t.description) setSubtreeDescriptionAndCountTODOs(subtreeL2, t.description);
    415 
    416      let caseFilter = null;
    417      if ('params' in queryToLoad) {
    418        caseFilter = queryToLoad.params;
    419      }
    420 
    421      // MAINTENANCE_TODO: If tree generation gets too slow, avoid actually iterating the cases in a
    422      // file if there's no need to (based on the subqueriesToExpand).
    423      for (const c of t.iterate(caseFilter)) {
    424        // iterate() guarantees c's query is equal to or a subset of queryToLoad.
    425 
    426        if (queryToLoad instanceof TestQuerySingleCase) {
    427          // A subset is OK if it's TestQueryMultiCase, but for SingleCase it must match exactly.
    428          const ordering = comparePublicParamsPaths(c.id.params, queryToLoad.params);
    429          if (ordering !== Ordering.Equal) {
    430            continue;
    431          }
    432        }
    433 
    434        // Leaf for case is suite:a,b:c,d:x=1;y=2
    435        addLeafForCase(subtreeL2, c, isCollapsible);
    436        foundCase = true;
    437      }
    438    }
    439    if (!groupHasTests && !subtreeL1.subtreeCounts) {
    440      throw new StacklessError(
    441        `${subtreeL1.query} has no tests - it must have "TODO" in its description`
    442      );
    443    }
    444  }
    445 
    446  for (const [i, sq] of subqueriesToExpandEntries) {
    447    const subquerySeen = seenSubqueriesToExpand[i];
    448    if (!subquerySeen) {
    449      throw new StacklessError(
    450        `subqueriesToExpand entry did not match anything \
    451 (could be wrong, or could be redundant with a previous subquery):\n  ${sq.toString()}`
    452      );
    453    }
    454  }
    455  assert(foundCase, `Query \`${queryToLoad.toString()}\` does not match any cases`);
    456 
    457  return TestTree.create(queryToLoad, subtreeL0, maxChunkTime);
    458 }
    459 
    460 function setSubtreeDescriptionAndCountTODOs(
    461 subtree,
    462 description)
    463 {
    464  assert(subtree.description === undefined);
    465  subtree.description = description.trim();
    466  subtree.subtreeCounts ??= { tests: 0, nodesWithTODO: 0, totalTimeMS: 0 };
    467  if (subtree.description.indexOf('TODO') !== -1) {
    468    subtree.subtreeCounts.nodesWithTODO++;
    469  }
    470 }
    471 
    472 function makeTreeForSuite(
    473 suite,
    474 isCollapsible)
    475 {
    476  const query = new TestQueryMultiFile(suite, []);
    477  return {
    478    readableRelativeName: suite + kBigSeparator,
    479    query,
    480    children: new Map(),
    481    collapsible: isCollapsible(query)
    482  };
    483 }
    484 
    485 function addSubtreeForDirPath(
    486 tree,
    487 file,
    488 isCollapsible)
    489 {
    490  const subqueryFile = [];
    491  // To start, tree is suite:*
    492  // This loop goes from that -> suite:a,* -> suite:a,b,*
    493  for (const part of file) {
    494    subqueryFile.push(part);
    495    tree = getOrInsertSubtree(part, tree, () => {
    496      const query = new TestQueryMultiFile(tree.query.suite, subqueryFile);
    497      return {
    498        readableRelativeName: part + kPathSeparator + kWildcard,
    499        query,
    500        collapsible: isCollapsible(query)
    501      };
    502    });
    503  }
    504  return tree;
    505 }
    506 
    507 function addSubtreeForFilePath(
    508 tree,
    509 file,
    510 isCollapsible)
    511 {
    512  // To start, tree is suite:*
    513  // This goes from that -> suite:a,* -> suite:a,b,*
    514  tree = addSubtreeForDirPath(tree, file, isCollapsible);
    515  // This goes from that -> suite:a,b:*
    516  const subtree = getOrInsertSubtree('', tree, () => {
    517    const query = new TestQueryMultiTest(tree.query.suite, tree.query.filePathParts, []);
    518    assert(file.length > 0, 'file path is empty');
    519    return {
    520      readableRelativeName: file[file.length - 1] + kBigSeparator + kWildcard,
    521      query,
    522      collapsible: isCollapsible(query)
    523    };
    524  });
    525  return subtree;
    526 }
    527 
    528 function addSubtreeForTestPath(
    529 tree,
    530 test,
    531 testCreationStack,
    532 isCollapsible)
    533 {
    534  const subqueryTest = [];
    535  // To start, tree is suite:a,b:*
    536  // This loop goes from that -> suite:a,b:c,* -> suite:a,b:c,d,*
    537  for (const part of test) {
    538    subqueryTest.push(part);
    539    tree = getOrInsertSubtree(part, tree, () => {
    540      const query = new TestQueryMultiTest(
    541        tree.query.suite,
    542        tree.query.filePathParts,
    543        subqueryTest
    544      );
    545      return {
    546        readableRelativeName: part + kPathSeparator + kWildcard,
    547        query,
    548        collapsible: isCollapsible(query)
    549      };
    550    });
    551  }
    552  // This goes from that -> suite:a,b:c,d:*
    553  return getOrInsertSubtree('', tree, () => {
    554    const query = new TestQueryMultiCase(
    555      tree.query.suite,
    556      tree.query.filePathParts,
    557      subqueryTest,
    558      {}
    559    );
    560    assert(subqueryTest.length > 0, 'subqueryTest is empty');
    561    return {
    562      readableRelativeName: subqueryTest[subqueryTest.length - 1] + kBigSeparator + kWildcard,
    563      kWildcard,
    564      query,
    565      testCreationStack,
    566      collapsible: isCollapsible(query)
    567    };
    568  });
    569 }
    570 
    571 function addLeafForCase(
    572 tree,
    573 t,
    574 checkCollapsible)
    575 {
    576  const query = tree.query;
    577  let name = '';
    578  const subqueryParams = {};
    579 
    580  // To start, tree is suite:a,b:c,d:*
    581  // This loop goes from that -> suite:a,b:c,d:x=1;* -> suite:a,b:c,d:x=1;y=2;*
    582  for (const [k, v] of Object.entries(t.id.params)) {
    583    name = stringifySingleParam(k, v);
    584    subqueryParams[k] = v;
    585 
    586    tree = getOrInsertSubtree(name, tree, () => {
    587      const subquery = new TestQueryMultiCase(
    588        query.suite,
    589        query.filePathParts,
    590        query.testPathParts,
    591        subqueryParams
    592      );
    593      return {
    594        readableRelativeName: name + kParamSeparator + kWildcard,
    595        query: subquery,
    596        collapsible: checkCollapsible(subquery)
    597      };
    598    });
    599  }
    600 
    601  // This goes from that -> suite:a,b:c,d:x=1;y=2
    602  const subquery = new TestQuerySingleCase(
    603    query.suite,
    604    query.filePathParts,
    605    query.testPathParts,
    606    subqueryParams
    607  );
    608  checkCollapsible(subquery); // mark seenSubqueriesToExpand
    609  insertLeaf(tree, subquery, t);
    610 }
    611 
    612 function getOrInsertSubtree(
    613 key,
    614 parent,
    615 createSubtree)
    616 {
    617  let v;
    618  const child = parent.children.get(key);
    619  if (child !== undefined) {
    620    assert('children' in child); // Make sure cached subtree is not actually a leaf
    621    v = child;
    622  } else {
    623    v = { ...createSubtree(), children: new Map() };
    624    parent.children.set(key, v);
    625  }
    626  return v;
    627 }
    628 
    629 function insertLeaf(parent, query, t) {
    630  const leaf = {
    631    readableRelativeName: readableNameForCase(query),
    632    query,
    633    run: (rec, expectations) => t.run(rec, query, expectations || []),
    634    isUnimplemented: t.isUnimplemented,
    635    subcaseCount: t.computeSubcaseCount()
    636  };
    637 
    638  // This is a leaf (e.g. s:f:t:x=1;* -> s:f:t:x=1). The key is always ''.
    639  const key = '';
    640  assert(!parent.children.has(key), `Duplicate testcase: ${query}`);
    641  parent.children.set(key, leaf);
    642 }
    643 
    644 function dissolveSingleChildTrees(tree) {
    645  if ('children' in tree) {
    646    const shouldDissolveThisTree =
    647    tree.children.size === 1 && tree.query.depthInLevel !== 0 && tree.description === undefined;
    648    if (shouldDissolveThisTree) {
    649      // Loops exactly once
    650      for (const [, child] of tree.children) {
    651        // Recurse on child
    652        return dissolveSingleChildTrees(child);
    653      }
    654    }
    655 
    656    for (const [k, child] of tree.children) {
    657      // Recurse on each child
    658      const newChild = dissolveSingleChildTrees(child);
    659      if (newChild !== child) {
    660        tree.children.set(k, newChild);
    661      }
    662    }
    663  }
    664  return tree;
    665 }
    666 
    667 /** Generate a readable relative name for a case (used in standalone). */
    668 function readableNameForCase(query) {
    669  const paramsKeys = Object.keys(query.params);
    670  if (paramsKeys.length === 0) {
    671    return query.testPathParts[query.testPathParts.length - 1] + kBigSeparator;
    672  } else {
    673    const lastKey = paramsKeys[paramsKeys.length - 1];
    674    return stringifySingleParam(lastKey, query.params[lastKey]);
    675  }
    676 }