utils.ts (8121B)
1 /** 2 * @license 3 * Copyright 2022 Google Inc. 4 * SPDX-License-Identifier: Apache-2.0 5 */ 6 7 import fs from 'node:fs'; 8 import path from 'node:path'; 9 10 import type { 11 MochaTestResult, 12 TestExpectation, 13 MochaResults, 14 TestResult, 15 } from './types.js'; 16 17 export function extendProcessEnv(envs: object[]): NodeJS.ProcessEnv { 18 const env = envs.reduce( 19 (acc: object, item: object) => { 20 Object.assign(acc, item); 21 return acc; 22 }, 23 { 24 ...process.env, 25 }, 26 ); 27 28 if (process.env['CI']) { 29 const puppeteerEnv = Object.entries(env).reduce( 30 (acc, [key, value]) => { 31 if (key.startsWith('PUPPETEER_')) { 32 acc[key] = value; 33 } 34 35 return acc; 36 }, 37 {} as Record<string, unknown>, 38 ); 39 40 console.log( 41 'PUPPETEER env:\n', 42 JSON.stringify(puppeteerEnv, null, 2), 43 '\n', 44 ); 45 } 46 47 return env as NodeJS.ProcessEnv; 48 } 49 50 export function getFilename(file: string): string { 51 return path.basename(file).replace(path.extname(file), ''); 52 } 53 54 export function readJSON(path: string): unknown { 55 return JSON.parse(fs.readFileSync(path, 'utf-8')); 56 } 57 58 export function writeJSON(path: string, json: unknown): unknown { 59 return fs.writeFileSync(path, JSON.stringify(json, null, 2)); 60 } 61 62 export function filterByPlatform<T extends {platforms: NodeJS.Platform[]}>( 63 items: T[], 64 platform: NodeJS.Platform, 65 ): T[] { 66 return items.filter(item => { 67 return item.platforms.includes(platform); 68 }); 69 } 70 71 export function prettyPrintJSON(json: unknown): void { 72 console.log(JSON.stringify(json, null, 2)); 73 } 74 75 export function getSuggestionsForAction( 76 recommendations: RecommendedExpectation[], 77 action: RecommendedExpectation['action'], 78 ): RecommendedExpectation[] { 79 return recommendations.filter(item => { 80 return item.action === action; 81 }); 82 } 83 84 export function printSuggestions( 85 recommendations: RecommendedExpectation[], 86 message: string, 87 printBasedOn = false, 88 ): void { 89 if (recommendations.length) { 90 console.log(message); 91 prettyPrintJSON( 92 recommendations.map(item => { 93 return item.expectation; 94 }), 95 ); 96 if (printBasedOn) { 97 console.log( 98 'The recommendations are based on the following applied expectations:', 99 ); 100 prettyPrintJSON( 101 recommendations.map(item => { 102 return item.basedOn; 103 }), 104 ); 105 } 106 } 107 } 108 109 export function filterByParameters( 110 expectations: TestExpectation[], 111 parameters: string[], 112 ): TestExpectation[] { 113 const querySet = new Set(parameters); 114 return expectations.filter(ex => { 115 return ex.parameters.every(param => { 116 return querySet.has(param); 117 }); 118 }); 119 } 120 121 /** 122 * The last expectation that matches an empty string as all tests pattern 123 * or the name of the file or the whole name of the test the filter wins. 124 */ 125 export function findEffectiveExpectationForTest( 126 expectations: TestExpectation[], 127 result: MochaTestResult, 128 ): TestExpectation | undefined { 129 return expectations.find(expectation => { 130 return testIdMatchesExpectationPattern(result, expectation.testIdPattern); 131 }); 132 } 133 134 export interface RecommendedExpectation { 135 expectation: TestExpectation; 136 action: 'remove' | 'add' | 'update'; 137 basedOn?: TestExpectation; 138 } 139 140 export function isWildCardPattern(testIdPattern: string): boolean { 141 return testIdPattern.includes('*'); 142 } 143 144 export function getExpectationUpdates( 145 results: MochaResults, 146 expectations: TestExpectation[], 147 context: { 148 platforms: NodeJS.Platform[]; 149 parameters: string[]; 150 }, 151 skipPassing: boolean, 152 ): RecommendedExpectation[] { 153 const output = new Map<string, RecommendedExpectation>(); 154 155 const passesByKey = results.passes.reduce((acc, pass) => { 156 acc.add(getTestId(pass.file, pass.fullTitle)); 157 return acc; 158 }, new Set()); 159 160 for (const pass of results.passes) { 161 if (skipPassing) { 162 continue; 163 } 164 165 const expectationEntry = findEffectiveExpectationForTest( 166 expectations, 167 pass, 168 ); 169 if (expectationEntry && !expectationEntry.expectations.includes('PASS')) { 170 if (isWildCardPattern(expectationEntry.testIdPattern)) { 171 addEntry({ 172 expectation: { 173 testIdPattern: getTestId(pass.file, pass.fullTitle), 174 platforms: context.platforms, 175 parameters: context.parameters, 176 expectations: ['PASS'], 177 }, 178 action: 'add', 179 basedOn: expectationEntry, 180 }); 181 } else { 182 addEntry({ 183 expectation: expectationEntry, 184 action: 'remove', 185 basedOn: expectationEntry, 186 }); 187 } 188 } 189 } 190 191 for (const failure of results.failures) { 192 // If an error occurs during a hook 193 // the error not have a file associated with it 194 if (!failure.file) { 195 console.error('Hook failed:', failure.err); 196 addEntry({ 197 expectation: { 198 testIdPattern: failure.fullTitle, 199 platforms: context.platforms, 200 parameters: context.parameters, 201 expectations: [], 202 }, 203 action: 'add', 204 }); 205 continue; 206 } 207 208 if (passesByKey.has(getTestId(failure.file, failure.fullTitle))) { 209 continue; 210 } 211 212 const expectationEntry = findEffectiveExpectationForTest( 213 expectations, 214 failure, 215 ); 216 if (expectationEntry && !expectationEntry.expectations.includes('SKIP')) { 217 if ( 218 !expectationEntry.expectations.includes( 219 getTestResultForFailure(failure), 220 ) 221 ) { 222 // If the effective explanation is a wildcard, we recommend adding a new 223 // expectation instead of updating the wildcard that might affect multiple 224 // tests. 225 if (isWildCardPattern(expectationEntry.testIdPattern)) { 226 addEntry({ 227 expectation: { 228 testIdPattern: getTestId(failure.file, failure.fullTitle), 229 platforms: context.platforms, 230 parameters: context.parameters, 231 expectations: [getTestResultForFailure(failure)], 232 }, 233 action: 'add', 234 basedOn: expectationEntry, 235 }); 236 } else { 237 addEntry({ 238 expectation: { 239 ...expectationEntry, 240 expectations: [ 241 ...expectationEntry.expectations, 242 getTestResultForFailure(failure), 243 ], 244 }, 245 action: 'update', 246 basedOn: expectationEntry, 247 }); 248 } 249 } 250 } else if (!expectationEntry) { 251 addEntry({ 252 expectation: { 253 testIdPattern: getTestId(failure.file, failure.fullTitle), 254 platforms: context.platforms, 255 parameters: context.parameters, 256 expectations: [getTestResultForFailure(failure)], 257 }, 258 action: 'add', 259 }); 260 } 261 } 262 263 function addEntry(value: RecommendedExpectation) { 264 const key = JSON.stringify(value); 265 if (!output.has(key)) { 266 output.set(key, value); 267 } 268 } 269 270 return [...output.values()]; 271 } 272 273 export function getTestResultForFailure( 274 test: Pick<MochaTestResult, 'err'>, 275 ): TestResult { 276 return test.err?.code === 'ERR_MOCHA_TIMEOUT' ? 'TIMEOUT' : 'FAIL'; 277 } 278 279 export function getTestId(file: string, fullTitle?: string): string { 280 return fullTitle 281 ? `[${getFilename(file)}] ${fullTitle}` 282 : `[${getFilename(file)}]`; 283 } 284 285 export function testIdMatchesExpectationPattern( 286 test: MochaTestResult | Pick<Mocha.Test, 'title' | 'file' | 'fullTitle'>, 287 pattern: string, 288 ): boolean { 289 const patternRegExString = pattern 290 // Replace `*` with non special character 291 .replace(/\*/g, '--STAR--') 292 // Escape special characters https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping 293 .replace(/[.*+?^${}()|[\]\\]/g, '\\$&') 294 // Replace placeholder with greedy match 295 .replace(/--STAR--/g, '(.*)?'); 296 // Match beginning and end explicitly 297 const patternRegEx = new RegExp(`^${patternRegExString}$`); 298 const fullTitle = 299 typeof test.fullTitle === 'string' ? test.fullTitle : test.fullTitle(); 300 301 return patternRegEx.test(getTestId(test.file ?? '', fullTitle)); 302 }