UNPKG

31.3 kBJavaScriptView Raw
1"use strict";
2
3Object.defineProperty(exports, "__esModule", {
4 value: true
5});
6exports.name = exports.description = exports.TEST_DIR = void 0;
7exports.run = run;
8exports.selectFlowTestVersions = selectFlowTestVersions;
9exports.setup = setup;
10exports.writeFlowConfig = writeFlowConfig;
11
12var _colors = _interopRequireDefault(require("colors"));
13
14var _node = require("../lib/node.js");
15
16var _fileUtils = require("../lib/fileUtils.js");
17
18var _github = require("../lib/github.js");
19
20var _npmLibDefs = require("../lib/npm/npmLibDefs");
21
22var _libDefs = require("../lib/libDefs.js");
23
24var _logger = require("../lib/logger");
25
26var _isInFlowTypedRepo = _interopRequireDefault(require("../lib/isInFlowTypedRepo"));
27
28var _flowVersion = require("../lib/flowVersion");
29
30var _git = require("../lib/git");
31
32var _got = _interopRequireDefault(require("got"));
33
34var semver = _interopRequireWildcard(require("semver"));
35
36var _nodeStreamZip = _interopRequireDefault(require("node-stream-zip"));
37
38var _ValidationError = require("../lib/ValidationError");
39
40function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
41
42function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
43
44function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
45
46/**
47 * Used to decide which binary to fetch for each version of Flow.
48 *
49 * Return type matches the binaries returned by flow releases
50 * https://github.com/facebook/flow/releases
51 *
52 * eg: If the binary for the OS is `flow-linux-arm64-v0.206.0.zip`
53 * we will return `linux-arm64`
54 */
55const BIN_PLATFORM = (() => {
56 const arch = _node.os.arch();
57
58 switch (_node.os.type()) {
59 case 'Linux':
60 if (arch === 'arm') {
61 return 'linux-arm64';
62 }
63
64 return 'linux64';
65
66 case 'Darwin':
67 if (arch === 'arm') {
68 return 'osx-arm64';
69 }
70
71 return 'osx';
72
73 case 'Windows_NT':
74 return 'win64';
75
76 default:
77 throw new Error('Unsupported os.type()! ' + _node.os.type());
78 }
79})();
80
81const PKG_ROOT_DIR = _node.path.join(__dirname, '..', '..');
82
83const TEST_DIR = _node.path.join(PKG_ROOT_DIR, '.test-dir');
84
85exports.TEST_DIR = TEST_DIR;
86
87const BIN_DIR = _node.path.join(PKG_ROOT_DIR, '.flow-bins-cache');
88
89const P = Promise;
90
91/**
92 * Scan the definitions/ directory to extract a flat list of TestGroup
93 * structs. Each TestGroup represents a Package/PackageVersion/FlowVersion
94 * directory.
95 */
96const basePathRegex = new RegExp('definitions/(npm|environments)/(@[^/]*/)?[^/]*/?');
97
98async function getTestGroups(repoDirPath, envDirPath, onlyChanged = false) {
99 let libDefs = await (0, _libDefs.getLibDefs)(repoDirPath);
100 let envDefs = await (0, _libDefs.getLibDefs)(envDirPath);
101
102 if (onlyChanged) {
103 const diff = await (0, _git.getDefinitionsDiff)();
104 const baseDiff = diff.map(d => {
105 if (d.endsWith('CODEOWNERS')) {
106 return '';
107 }
108
109 const match = d.match(basePathRegex);
110
111 if (match) {
112 return match[0];
113 }
114
115 return '';
116 }).filter(d => d !== '');
117 const changedDefs = baseDiff.map(d => {
118 const {
119 pkgName,
120 pkgVersion
121 } = (0, _libDefs.parseRepoDirItem)(d);
122 const {
123 major,
124 minor,
125 patch
126 } = pkgVersion;
127 return {
128 name: pkgName,
129 version: `v${major}.${minor}.${patch}`
130 };
131 });
132 libDefs = [...libDefs, ...envDefs].filter(def => {
133 return changedDefs.some(d => {
134 // This is for env defs
135 if (d.version === 'vx.x.x') {
136 return d.name === def.pkgName;
137 } // If the definition is a dependant of a changed package
138
139
140 if (def.configPath) {
141 const deps = JSON.parse(_node.fs.readFileSync(def.configPath, 'utf-8')).deps;
142 const isDependantOfChanged = Object.keys(deps).some(dep => dep === d.name && deps[dep].some(s => s === d.version));
143 if (isDependantOfChanged) return true;
144 }
145
146 return d.name === def.pkgName && d.version === def.pkgVersionStr;
147 });
148 });
149 }
150
151 return libDefs.map(libDef => {
152 const groupID = `${libDef.pkgName}_${libDef.pkgVersionStr}/${libDef.flowVersionStr}`;
153 const deps = libDef.configPath ? JSON.parse(_node.fs.readFileSync(libDef.configPath, 'utf-8')).deps : {};
154 return {
155 id: groupID,
156 testFilePaths: libDef.testFilePaths,
157 libDefPath: libDef.path,
158 flowVersion: libDef.flowVersion,
159 deps
160 };
161 });
162}
163
164function printSkipMessage(flowVersion, githubUrl) {
165 console.log('==========================================================================================');
166 console.log(`We are temporarily skipping ${flowVersion} due to ${githubUrl}`);
167 console.log('==========================================================================================');
168} // Once we've fetched flow bin releases they will be cached
169// For subsequent tests within the same test run.
170
171
172let _flowReleases = [];
173/**
174 * Memoized function that queries the GitHub releases for Flow, downloads the
175 * zip for each version, extracts the zip, and moves the binary to
176 * TEST_BIN/flow-vXXX for use later when running tests.
177 */
178
179async function getOrderedFlowBinVersions(numberOfReleases, flowVersion) {
180 (0, _logger.sectionHeader)('Fetching flow binaries...');
181 const IS_WINDOWS = _node.os.type() === 'Windows_NT';
182 const GH_CLIENT = (0, _github.gitHubClient)();
183 const OS_ARCH_FILTER_RE = new RegExp(`flow-${BIN_PLATFORM}-v`); // Fetching all available flow versions
184 // before deciding which to run
185
186 if (_flowReleases.length === 0) {
187 let foundAllFlowVersions = false;
188 let page = 1;
189
190 while (!foundAllFlowVersions) {
191 try {
192 const pageVersions = await GH_CLIENT.repos.listReleases({
193 owner: 'facebook',
194 repo: 'flow',
195 page,
196 per_page: 50
197 });
198 (0, _logger.listItem)(`Fetched page: ${page}`);
199 page += 1;
200
201 _flowReleases.push(...pageVersions.data);
202
203 if (pageVersions.data.length === 0) {
204 foundAllFlowVersions = true;
205 }
206 } catch (e) {
207 console.error(e);
208 foundAllFlowVersions = true;
209 }
210 }
211 }
212
213 const filteredVersions = selectFlowTestVersions(_flowReleases.map(o => o.tag_name), flowVersion, numberOfReleases);
214
215 const flowBins = _flowReleases.filter(ver => filteredVersions.includes(ver.tag_name)).filter(rel => {
216 if (rel.tag_name.endsWith('-rc')) {
217 return false;
218 }
219
220 if (rel.tag_name === 'v0.67.0') {
221 printSkipMessage(rel.tag_name, 'https://github.com/facebook/flow/issues/5922');
222 return false;
223 } else if (rel.tag_name === 'v0.63.0' || rel.tag_name === 'v0.70.0') {
224 printSkipMessage(rel.tag_name, 'https://github.com/flowtype/flow-typed/issues/2422');
225 return false;
226 } else if (semver.lt(rel.tag_name, '0.53.0')) {
227 console.log('flow-typed only supports flow 0.53.0 and newer');
228 return false;
229 } else if (IS_WINDOWS && (semver.eq(rel.tag_name, '0.57.0') || semver.eq(rel.tag_name, '0.57.1') || semver.eq(rel.tag_name, '0.57.2'))) {
230 // Because flow 0.57 was broken before 0.57.3 on the Windows platform, we also skip those versions when running on windows.
231 return false;
232 }
233
234 return true;
235 }).map(rel => {
236 // Find the binary zip in the list of assets
237 const binZip = rel.assets.filter(({
238 name
239 }) => {
240 return OS_ARCH_FILTER_RE.test(name) && !/-latest.zip$/.test(name);
241 }).map(asset => asset.browser_download_url);
242
243 if (binZip.length !== 1) {
244 throw new Error('Unexpected number of ' + BIN_PLATFORM + ' assets for flow-' + rel.tag_name + '! ' + JSON.stringify(binZip));
245 } else {
246 const version = rel.tag_name[0] === 'v' ? rel.tag_name : 'v' + rel.tag_name;
247 return {
248 version,
249 binURL: binZip[0]
250 };
251 }
252 }).sort((a, b) => {
253 return semver.lt(a.version, b.version) ? -1 : 1;
254 });
255
256 await P.all(flowBins.map(async ({
257 version,
258 binURL
259 }) => {
260 const zipPath = _node.path.join(BIN_DIR, 'flow-' + version + '.zip');
261
262 const binPath = _node.path.join(BIN_DIR, 'flow-' + version + (IS_WINDOWS ? '.exe' : ''));
263
264 if (await _node.fs.exists(binPath)) {
265 return;
266 } // Download the zip file
267
268
269 await new Promise((res, rej) => {
270 console.log(' Fetching flow-%s...', version);
271
272 _got.default.stream(binURL, {
273 headers: {
274 'User-Agent': 'flow-typed Test Runner (github.com/flowtype/flow-typed)'
275 }
276 }).on('error', err => rej(err)).pipe(_node.fs.createWriteStream(zipPath).on('close', () => {
277 console.log(' flow-%s finished downloading.', version);
278 res();
279 }));
280 }); // Extract the flow binary
281
282 const flowBinDirPath = _node.path.join(BIN_DIR, 'TMP-flow-' + version);
283
284 await _node.fs.mkdir(flowBinDirPath);
285 console.log(' Extracting flow-%s...', version);
286 const zip = new _nodeStreamZip.default.async({
287 file: zipPath
288 });
289 const extractedCount = await zip.extract(null, flowBinDirPath);
290 console.log(' Extracted %s entries', extractedCount);
291
292 if (IS_WINDOWS) {
293 await _node.fs.rename(_node.path.join(flowBinDirPath, 'flow', 'flow.exe'), _node.path.join(BIN_DIR, 'flow-' + version + '.exe'));
294 } else {
295 await _node.fs.rename(_node.path.join(flowBinDirPath, 'flow', 'flow'), _node.path.join(BIN_DIR, 'flow-' + version));
296 await _node.child_process.execP(['chmod', '755', _node.path.join(BIN_DIR, 'flow-' + version)].join(' '));
297 }
298
299 console.log(' Removing flow-%s artifacts...', version);
300 await P.all([(0, _fileUtils.recursiveRmdir)(flowBinDirPath), _node.fs.unlink(zipPath)]);
301 console.log(' flow-%s complete!', version);
302 }));
303 console.log('Finished fetching Flow binaries.\n');
304 return flowBins.map(bin => bin.version);
305}
306
307const flowNameRegex = /^flow-v[0-9]+.[0-9]+.[0-9]+(\.exe)?$/;
308/**
309 * flow filename should be `flow-vx.x.x`
310 * @param {string} name
311 */
312
313function checkFlowFilename(name) {
314 return flowNameRegex.test(name);
315}
316/**
317 * Return the sorted list of cached flow binaries that have previously been retrieved from github
318 * and cached in the `.flow-bins-cache` directory. This function is usually called when a failure
319 * has occurred when attempting to refresh the flow releases from github, i.e. offline or over
320 * API limit.
321 */
322
323
324async function getCachedFlowBinVersions(numberOfReleases, flowVersion) {
325 // read the files with name `flow-vx.x.x` from the bin dir and remove the leading `flow-` prefix
326 const versions = (await _node.fs.readdir(_node.path.join(BIN_DIR))).filter(checkFlowFilename).map(dir => dir.slice('flow-'.length)); // sort the versions that we have in-place
327 // removing the `v` prefix before semver comparison
328
329 versions.sort((a, b) => {
330 return semver.lt(a.substring(1), b.substring(1)) ? -1 : 1;
331 });
332 return selectFlowTestVersions(versions, flowVersion, numberOfReleases);
333}
334
335async function writeFlowConfig(repoDirPath, testDirPath, libDefPath, version, depPaths) {
336 // /!\---------------------------------------------------------------------/!\
337 // Whenever you introduce a new difference depending on the version, don't
338 // forget to update the constant array CONFIGURATION_CHANGE_VERSIONS.
339 // /!\---------------------------------------------------------------------/!\
340 const destFlowConfigPath = _node.path.join(testDirPath, '.flowconfig');
341
342 const flowConfigData = ['[libs]', ...depPaths, _node.path.basename(libDefPath), _node.path.join(repoDirPath, '..', '__util__', 'tdd_framework.js'), '', '[options]', 'include_warnings=true', 'server.max_workers=0', semver.gte(version, '0.200.0') ? 'exact_by_default=true' : '', // from version 0.202.0 default is true
343 // Fixes out of shared memory error for Mac Rosetta 2, see https://github.com/facebook/flow/issues/8538
344 'sharedmemory.heap_size=3221225472', semver.lt(version, '0.125.0') ? 'suppress_comment=\\\\(.\\\\|\\n\\\\)*\\\\$FlowExpectedError' : '', '', // Be sure to ignore stuff in the node_modules directory of the flow-typed
345 // CLI repository!
346 '[ignore]', _node.path.join(testDirPath, '..', '..', 'node_modules'), '', '[lints]', semver.gte(version, '0.104.0') && semver.lt(version, '0.201.0') ? 'implicit-inexact-object=error' : '', semver.gte(version, '0.201.0') ? 'ambiguous-object-type=error' : ''].join('\n');
347 await _node.fs.writeFile(destFlowConfigPath, flowConfigData);
348}
349
350function testTypeDefinition(flowVer, testDirPath) {
351 return new Promise(res => {
352 const IS_WINDOWS = _node.os.type() === 'Windows_NT';
353
354 const child = _node.child_process.exec([_node.path.join(BIN_DIR, 'flow-' + flowVer + (IS_WINDOWS ? '.exe' : '')), 'check', '--strip-root', '--all', testDirPath].join(' '));
355
356 let stdErrOut = '';
357 child.stdout.on('data', data => stdErrOut += data);
358 child.stderr.on('data', data => stdErrOut += data);
359 child.on('error', execError => {
360 res({
361 stdErrOut,
362 errCode: null,
363 execError
364 });
365 });
366 child.on('close', errCode => {
367 res({
368 stdErrOut,
369 errCode,
370 execError: null
371 });
372 });
373 });
374}
375
376async function runFlowTypeDefTests(flowVersionsToRun, groupId, testDirPath) {
377 const errors = [];
378
379 while (flowVersionsToRun.length > 0) {
380 // Run tests in batches to avoid saturation
381 const testBatch = flowVersionsToRun.slice(0, Math.min(flowVersionsToRun.length, 5)).map(group => (flowVersionsToRun.shift(), group));
382 await P.all(testBatch.map(async flowVer => {
383 const testRunId = groupId + ' (flow-' + flowVer + ')';
384 console.log('Testing %s...', testRunId);
385 const {
386 stdErrOut,
387 errCode,
388 execError
389 } = await testTypeDefinition(flowVer, testDirPath);
390
391 if (execError !== null) {
392 errors.push(testRunId + ': Error executing Flow process: ' + execError.stack);
393 } else if (!stdErrOut.endsWith('Found 0 errors\n')) {
394 errors.push(testRunId + ': Unexpected Flow errors (' + String(errCode) + '):\n\n' + stdErrOut);
395 }
396 }));
397 }
398
399 return errors;
400}
401
402async function testLowestCapableFlowVersion(lowerVersions, testDirPath, lowestFlowVersionRan) {
403 let lowerFlowVersionsToRun = lowerVersions;
404 let lowestCapableFlowVersion = lowestFlowVersionRan;
405
406 while (lowerFlowVersionsToRun.length > 0) {
407 const lowerTestBatch = lowerFlowVersionsToRun.slice(0, Math.min(lowerFlowVersionsToRun.length, 5)).map(group => (lowerFlowVersionsToRun.shift(), group));
408 await P.all(lowerTestBatch.map(async flowVer => {
409 const {
410 stdErrOut,
411 execError
412 } = await testTypeDefinition(flowVer, testDirPath);
413
414 if (execError !== null || !stdErrOut.endsWith('Found 0 errors\n')) {
415 lowerFlowVersionsToRun = [];
416 } else {
417 lowestCapableFlowVersion = semver.lt(lowestCapableFlowVersion, flowVer) ? lowestCapableFlowVersion : flowVer;
418 }
419 }));
420 }
421
422 return lowestCapableFlowVersion;
423}
424
425async function findLowestCapableFlowVersion(repoDirPath, orderedFlowVersions, lowestFlowVersionRan, testDirPath, libDefPath, depPaths) {
426 let lowerFlowVersionsToRun = orderedFlowVersions.filter(flowVer => {
427 return semver.lt(flowVer, lowestFlowVersionRan);
428 });
429 lowerFlowVersionsToRun.reverse();
430 await writeFlowConfig(repoDirPath, testDirPath, libDefPath, lowestFlowVersionRan, depPaths);
431 return await testLowestCapableFlowVersion(lowerFlowVersionsToRun, testDirPath, lowestFlowVersionRan);
432}
433/**
434 * Remove all files except flow instances
435 */
436
437
438async function removeTrashFromBinDir() {
439 (await _node.fs.readdir(_node.path.join(BIN_DIR))).filter(name => !checkFlowFilename(name)).forEach(async el => {
440 const dir = _node.path.resolve(BIN_DIR, el);
441
442 if (!_node.fs.exists(dir)) {
443 await _node.fs.unlink(dir);
444 }
445 });
446} // These versions introduce a change in the .flowconfig file. We need to make
447// sure that the flowconfig file is rewritten when reaching these versions.
448// MAKE SURE TO PREPEND THE VERSION WITH "v".
449
450
451const CONFIGURATION_CHANGE_VERSIONS = ['v0.104.0', // Adding lint
452'v0.125.0', // Remove suppress_comments
453'v0.200.0' // exact_by_default become required
454];
455/**
456 * This function splits a list of flow versions into a list of lists of versions
457 * where the flowconfig configuration is compatible.
458 * The list of versions need to be sorted for this function to work properly.
459 */
460
461function partitionListOfFlowVersionsPerConfigChange(flowVersions) {
462 const result = [];
463 let lastIndex = 0;
464
465 for (const pivotVersion of CONFIGURATION_CHANGE_VERSIONS) {
466 const index = flowVersions.indexOf(pivotVersion, lastIndex + 1);
467
468 if (index < 0) {
469 continue;
470 } // We extract a new group up to the pivot version excluded (because the
471 // pivot version introduced the configuration incompatibility).
472
473
474 result.push(flowVersions.slice(lastIndex, index));
475 lastIndex = index;
476 }
477
478 result.push(flowVersions.slice(lastIndex));
479 return result;
480}
481/**
482 * Map through every dependency + their versions and replace with
483 * the definition's path,
484 * recursively including those dependency's dependencies.
485 *
486 * Then shuffle to create a new Array<Array<>> that will test
487 * All dependencies across various supported versions.
488 */
489
490
491function getDepTestGroups(testGroup) {
492 const flowDirVersion = (0, _flowVersion.extractFlowDirFromFlowDirPath)(testGroup.id);
493 const depBasePath = (0, _npmLibDefs.getNpmLibDefDirFromNested)(testGroup.libDefPath);
494
495 const getMappedDepPaths = deps => {
496 return Object.keys(deps).map(depName => {
497 const nameSplit = depName.split('/');
498 const scope = nameSplit.length > 1 ? `${nameSplit[0]}/` : '';
499 const packageName = nameSplit.length > 1 ? nameSplit[1] : depName;
500 return deps[depName].map(version => {
501 const flowDirPath = `${depBasePath}${scope}${packageName}_${version}/${flowDirVersion}`;
502 const defPath = `${flowDirPath}/${packageName}_${version}.js`;
503
504 if (!_node.fs.existsSync(defPath)) {
505 throw new Error(_colors.default.red(`${depName}@${version} cannot be a dependency of ${testGroup.id} because either the dependency@version does't exist or they do not have matching flow version ranges`));
506 } // For the current dependency check if it has nested dependencies
507
508
509 let defDeps;
510
511 try {
512 defDeps = JSON.parse(_node.fs.readFileSync(`${flowDirPath}/config.json`, 'utf-8')).deps;
513 } catch (e) {}
514
515 return {
516 main: defPath,
517 // Create a recursive list of main def and def dep paths
518 // that are later used to inject into the test group so that
519 // dependencies can resolve their dependencies.
520 //
521 // Note: This strategy doesn't create an exhaustive list validating
522 // a dependency to each dep version of dependency's dependency.
523 // That isn't necessary in this instance and would be tested by
524 // the dependency's own test group.
525 deps: [...(defDeps ? getMappedDepPaths(defDeps).reduce((acc, cur) => {
526 return [...acc, cur[0].main, ...cur[0].deps];
527 }, []) : [])]
528 };
529 });
530 });
531 };
532
533 const mappedDepPaths = getMappedDepPaths(testGroup.deps);
534 const longestDep = mappedDepPaths.reduce((acc, cur) => {
535 if (cur.length > acc) {
536 return cur.length;
537 }
538
539 return acc;
540 }, 0);
541 const depGroup = [];
542
543 for (let i = 0, len = longestDep; i < len; i++) {
544 const newGroup = [];
545 mappedDepPaths.forEach(o => {
546 if (!o[i]) {
547 newGroup.push(o[0].main, ...o[0].deps);
548 } else {
549 newGroup.push(o[i].main, ...o[i].deps);
550 }
551 });
552 depGroup.push(newGroup);
553 }
554
555 return depGroup;
556}
557/**
558 * Given a TestGroup structure determine all versions of Flow that match the
559 * FlowVersion specification and, for each, run `flow check` on the test
560 * directory.
561 */
562
563
564async function runTestGroup(repoDirPath, testGroup, orderedFlowVersions) {
565 // Some older versions of Flow choke on ">"/"<"/"="
566 const testDirName = testGroup.id.replace(/\//g, '--').replace(/>/g, 'gt').replace(/</g, 'lt').replace(/=/g, 'eq');
567
568 const testDirPath = _node.path.join(TEST_DIR, testDirName);
569
570 if (await _node.fs.exists(testDirPath)) {
571 throw new Error(`Trying to run ${testGroup.id}, but test dir already exists! ` + `Bailing out!`);
572 }
573
574 try {
575 await _node.fs.mkdir(testDirPath); // Copy files into the test dir
576
577 const destLibDefPath = _node.path.join(testDirPath, _node.path.basename(testGroup.libDefPath));
578
579 const copiedFileNames = new Set();
580 await P.all([P.all(testGroup.testFilePaths.map(async (filePath, idx) => {
581 // Because there could be multiple test files with the same basename,
582 // we disambiguate each one with a locally-unique index.
583 //
584 // i.e. underscore/v1.x.x/test-underscore.js
585 // underscore/v1.x.x/flow-v0.22.x/test-underscore.js
586 //
587 // Only mangles the name when there's a naming collision. Otherwise, uses the original.
588 const fileName = _node.path.basename(filePath);
589
590 const destBasename = copiedFileNames.has(fileName) ? `${idx}-${fileName}` : fileName;
591 copiedFileNames.add(destBasename);
592 await (0, _fileUtils.copyFile)(filePath, _node.path.join(testDirPath, destBasename));
593 })), await (0, _fileUtils.copyFile)(testGroup.libDefPath, destLibDefPath)]); // For each compatible version of Flow, run `flow check` and verify there
594 // are no errors.
595
596 const testGrpFlowSemVerRange = (0, _flowVersion.toSemverString)(testGroup.flowVersion);
597 const flowVersionsToRun = orderedFlowVersions.filter(flowVer => {
598 return semver.satisfies(flowVer, testGrpFlowSemVerRange);
599 }); // Windows hasn't flow < 30.0 but we have tests for flow < 30.0. We need skip it. Example: redux_v3
600
601 if (!flowVersionsToRun.length) {
602 return [];
603 }
604
605 const groups = partitionListOfFlowVersionsPerConfigChange(flowVersionsToRun);
606 const flowErrors = [];
607
608 const executeTests = async (depPaths = []) => {
609 for (const sublistOfFlowVersions of groups) {
610 const lowestFlowVersionRanInThisGroup = sublistOfFlowVersions[0];
611 await writeFlowConfig(repoDirPath, testDirPath, testGroup.libDefPath, lowestFlowVersionRanInThisGroup, depPaths);
612 const flowErrorsForThisGroup = await runFlowTypeDefTests([...sublistOfFlowVersions], testGroup.id, testDirPath);
613 flowErrors.push(...flowErrorsForThisGroup);
614 }
615
616 const lowestFlowVersionRan = flowVersionsToRun[0];
617 const lowestCapableFlowVersion = await findLowestCapableFlowVersion(repoDirPath, orderedFlowVersions, lowestFlowVersionRan, testDirPath, testGroup.libDefPath, depPaths);
618
619 if (lowestCapableFlowVersion !== lowestFlowVersionRan) {
620 console.log(`Tests for ${testGroup.id} ran successfully on flow ${lowestCapableFlowVersion}.
621 Consider setting ${lowestCapableFlowVersion} as the lower bound!`);
622 }
623 };
624
625 if (Object.keys(testGroup.deps).length > 0) {
626 const depsTestGroups = getDepTestGroups(testGroup);
627
628 for (const depPaths of depsTestGroups) {
629 await executeTests(depPaths);
630 }
631 } else {
632 await executeTests();
633 }
634
635 return flowErrors;
636 } finally {
637 if (await _node.fs.exists(testDirPath)) {
638 await (0, _fileUtils.recursiveRmdir)(testDirPath);
639 }
640 }
641}
642/**
643 * Takes a list of ordered flow version string in the format of
644 * `vx.x.x` from latest to oldest and returns a filtered list of flow versions
645 * depending on the definitions testing requirements
646 */
647
648
649function selectFlowTestVersions(orderedFlowVersions, flowVersion, numberOfFlowVersions) {
650 // This retains original logic in cases
651 // where we want all or specific flow versions for testing.
652 // This is a pretty uncommon use case though.
653 if (flowVersion.kind === 'all' || flowVersion.kind === 'specific') {
654 return orderedFlowVersions.slice(0, numberOfFlowVersions);
655 }
656
657 const {
658 upper,
659 lower
660 } = flowVersion;
661 const selectedFlowVersions = [];
662
663 const findSelectedFlowVersions = flowVersions => {
664 let versionExceedsLower = false;
665
666 while (selectedFlowVersions.length < numberOfFlowVersions && !versionExceedsLower) {
667 const version = flowVersions[selectedFlowVersions.length];
668 const [major, minor, patch] = version.substring(1).split('.');
669
670 if (lower && (lower.major === 'x' || lower.major <= Number(major)) && (lower.minor === 'x' || lower.minor <= Number(minor)) && (lower.patch === 'x' || lower.patch <= Number(patch))) {
671 selectedFlowVersions.push(version);
672 } else {
673 versionExceedsLower = true;
674 }
675 }
676 };
677
678 if (upper == null) {
679 findSelectedFlowVersions(orderedFlowVersions);
680 } else {
681 // If upper exists we should find the first version that satisfies
682 // upper and find the first `numberOfFlowVersions` until we hit the
683 // lower boundary.
684 const upperFlowVersion = orderedFlowVersions.findIndex(o => {
685 const [major, minor, patch] = o.substring(1).split('.');
686
687 if ((upper.major === 'x' || upper.major >= Number(major)) && (upper.minor === 'x' || upper.minor >= Number(minor)) && (upper.patch === 'x' || upper.patch >= Number(patch))) {
688 return true;
689 }
690 });
691 const upperBoundFlowVersions = orderedFlowVersions.slice(upperFlowVersion);
692 findSelectedFlowVersions(upperBoundFlowVersions);
693 }
694
695 return selectedFlowVersions;
696}
697
698async function runTests(repoDirPath, envDirPath, testPatterns, onlyChanged, numberOfFlowVersions) {
699 const testPatternRes = testPatterns.map(patt => new RegExp(patt, 'g'));
700 const testGroups = (await getTestGroups(repoDirPath, envDirPath, onlyChanged)).filter(testGroup => {
701 if (testPatternRes.length === 0) {
702 return true;
703 }
704
705 for (var i = 0; i < testPatternRes.length; i++) {
706 const pattern = testPatternRes[i];
707
708 if (testGroup.id.match(pattern) != null) {
709 return true;
710 } // For a definition, if their dependencies have modifications
711 // we should run tests against the definition to ensure changes
712 // to the dependency has not broken it's dependents
713 // by adding the matched definition to the test group.
714
715
716 const depsList = Object.keys(testGroup.deps);
717
718 if (depsList.length > 0) {
719 return depsList.some(dep => {
720 return testGroup.deps[dep].some(o => `${dep}_${o}`.match(pattern));
721 });
722 }
723 }
724
725 return false;
726 });
727
728 try {
729 // Create a temp dir to copy files into to run the tests
730 if (await _node.fs.exists(TEST_DIR)) {
731 await (0, _fileUtils.recursiveRmdir)(TEST_DIR);
732 }
733
734 await _node.fs.mkdir(TEST_DIR);
735 const results = new Map();
736
737 while (testGroups.length > 0) {
738 const testGroup = testGroups.shift(); // Prepare bin folder to collect flow instances
739
740 await removeTrashFromBinDir();
741 let orderedFlowVersions;
742
743 try {
744 orderedFlowVersions = await getOrderedFlowBinVersions(numberOfFlowVersions, testGroup.flowVersion);
745 } catch (e) {
746 orderedFlowVersions = await getCachedFlowBinVersions(numberOfFlowVersions, testGroup.flowVersion);
747 }
748
749 const testGroupErrors = await runTestGroup(repoDirPath, testGroup, orderedFlowVersions);
750
751 if (testGroupErrors.length > 0) {
752 const errors = results.get(testGroup.id) || [];
753 testGroupErrors.forEach(err => errors.push(err));
754 results.set(testGroup.id, errors);
755 }
756 }
757
758 return results;
759 } finally {
760 if (await _node.fs.exists(TEST_DIR)) {
761 await (0, _fileUtils.recursiveRmdir)(TEST_DIR);
762 }
763 }
764}
765
766const name = 'run-tests [testPatterns...]';
767exports.name = name;
768const description = 'Run definition tests for library definitions in the flow-typed project';
769exports.description = description;
770
771function setup(yargs) {
772 return yargs.usage(`$0 ${name} - ${description}`).positional('testPatterns', {
773 describe: 'Test patterns',
774 default: []
775 }).options({
776 path: {
777 describe: 'Override default path for libdef root (Mainly for testing purposes)',
778 type: 'string',
779 demandOption: false
780 },
781 onlyChanged: {
782 type: 'boolean',
783 description: 'Run only changed definition tests',
784 demandOption: false
785 },
786 numberOfFlowVersions: {
787 type: 'number',
788 description: 'Only run against the latest X versions of flow',
789 demandOption: false
790 }
791 });
792}
793
794async function run(argv) {
795 if (!(await _node.fs.exists(BIN_DIR))) {
796 await _node.fs.mkdir(BIN_DIR);
797 }
798
799 if (!(0, _isInFlowTypedRepo.default)()) {
800 console.log('This command only works in a clone of flowtype/flow-typed. ' + 'It is a tool used to run tests of the library definitions in the flow-typed project.');
801 return 1;
802 }
803
804 const testPatterns = Array.isArray(argv.testPatterns) ? argv.testPatterns.map(String) : [];
805 const onlyChanged = Boolean(argv.onlyChanged);
806 const numberOfFlowVersions = Number(argv.numberOfFlowVersions) || 15;
807 const cwd = process.cwd();
808 const basePath = argv.path ? String(argv.path) : cwd;
809
810 const cwdDefsNPMPath = _node.path.join(basePath, 'definitions', 'npm');
811
812 const repoDirPath = (await _node.fs.exists(cwdDefsNPMPath)) ? cwdDefsNPMPath : _node.path.join(__dirname, '..', '..', '..', 'definitions', 'npm');
813
814 const cwdDefsEnvPath = _node.path.join(basePath, 'definitions', 'environments');
815
816 const envDirPath = (await _node.fs.exists(cwdDefsEnvPath)) ? cwdDefsEnvPath : _node.path.join(__dirname, '..', '..', '..', 'definitions', 'environments');
817 console.info(`Running ${onlyChanged ? 'changed' : ''} definition tests against latest ${_colors.default.green(numberOfFlowVersions.toString())} flow versions in ${_node.path.join(repoDirPath, '..')}...\n`);
818 let results;
819
820 try {
821 results = await runTests(repoDirPath, envDirPath, testPatterns, onlyChanged, numberOfFlowVersions);
822 } catch (e) {
823 if (e instanceof _ValidationError.ValidationError) {
824 console.error(e.message);
825 return 1;
826 } else {
827 throw e;
828 }
829 }
830
831 Array.from(results).forEach(([testGroupName, errors]) => {
832 console.log('ERROR: %s', testGroupName);
833 errors.forEach(err => console.log('* %s\n', err.split('\n').map((line, idx) => {
834 return idx === 0 ? line : ' ' + line;
835 }).join('\n')));
836 });
837
838 if (results.size === 0) {
839 console.log('\n ✨ All tests passed!');
840 return 0;
841 }
842
843 return 1;
844}
\No newline at end of file