/** * @hidden * @ignore * @internal */ /** * Dummy comment. */ import * as fs from 'fs'; import * as path from 'path'; import * as _ from 'lodash'; import * as nodeUtils from '../utils/nodejs-utils'; import * as fileUtils from '../utils/file-utils'; import * as suiteUtils from '../utils/suite-utils'; import * as os from 'os'; import { TestInfo } from './test-info'; import { handleSuiteConfig } from '../utils/config-utils'; function getOs() { return `${os.platform()}_${os.release()}`; } function isIterator(result: any) { if (!result) { return false; } const funcs = ['next', 'return', 'throw']; return funcs.every(func => typeof result[func] === 'function'); } function exceptionHandler(e: Error) { // TODO: why did I disable exceptions to console here? gIn.logger.exception('Exception in runner: ', e); gIn.logger.logResourcesUsage(); gIn.tInfo.addFail(); } async function runTestFile(file: string) { gIn.tracer.msg2(`Starting new test: ${file}`); gIn.errRecursionCount = 0; gIn.cancelThisTest = false; try { let testObject = nodeUtils.requireEx(file, true).result; if (testObject.__esModule === true) { testObject = testObject.default; } if (typeof testObject === 'function') { const funcRes = await testObject(gT, gIn, gT.a); // TIA :) Test Inner Assertions. if (isIterator(funcRes)) { await gT.u.misc.iterateSafe(funcRes); } } else { await testObject; } } catch (e) { exceptionHandler(e); } // To allow test read events from event loop. await gT.u.promise.delayed(gT.config.delayAfterTest); } // return null - means skipped by --pattern or --new or due to etalon absence. async function handleTestFile(file: string, dirConfig: any) { // Restore the state which could be damaged by previous test and any other initialization. gIn.tInfo.setPassCountingEnabled(gT.engineConsts.defIsPassCountingEnabled); gIn.loggerCfg.setDefLLLogAction(gT.engineConsts.defLLLogAction); gT.config = _.cloneDeep(dirConfig); // Config for current test, can be changed by test. // It is not safe to create such structure in the test and return it from test, // because test can be terminated with exception. // console.log('File: ' + file); if ( gT.cLParams.pattern && file.lastIndexOf(gT.cLParams.pattern) < gT.cLParams.minPathSearchIndex ) { return null; } const etalonAbsent = fileUtils.isEtalonAbsent(file); if (gT.cLParams.new && !etalonAbsent) { return null; } const skippedDir = dirConfig.skip && !gT.cLParams.ignoreSkipFlag; if (!skippedDir && etalonAbsent) { if (gT.cLParams.new) { gIn.tracer.msg0(`Found new test: ${file}`); } else { gIn.tracer.msg0(`Skipped new test: ${file}`); if (!gT.cLParams.pattern) { suiteUtils.saveNewTestInfo(file); } return null; } } gIn.tInfo.setData(gIn.tInfo.createTestInfo(false, '', file)); // Test should change this title. if (skippedDir) { gIn.tInfo.setSkipped(1); return gIn.tInfo.getData(); } gIn.tInfo.setRun(1); if (gT.config.DISPLAY && gT.cLParams.xvfb) { process.env.DISPLAY = gT.config.DISPLAY; } else { process.env.DISPLAY = gT.engineConsts.defDisplay; } fileUtils.createEmptyLog(file); fileUtils.rmPngs(file); if (gT.cLParams.extLog) { fileUtils.safeUnlink(gT.cLParams.extLog); } const startTime = gT.timeUtils.startTimer(); await runTestFile(file).catch(e => { const asdf = 5; }); // Can be sync. gIn.logger.testSummary(); if (gT.cLParams.extLog) { const extLog = fileUtils.safeReadFile(gT.cLParams.extLog); gIn.logger.log(extLog); } gIn.tInfo.setTime(gT.timeUtils.stopTimer(startTime)); if (!gT.cLParams.new) { gIn.diffUtils.diff({ jsTest: file, }); } return gIn.tInfo.getData(); // Return value to be uniform in handleDir. } /** * Removes config from files. Merges current config to parrent config. * Also removes names specified by config.ignoreNames. * @param {String} dir * @param {Array} files * @param {Object} parentDirConfig * @return {Object} directory config. */ function handleDirConfig(dir: string, files: string[], parentDirConfig: any) { let config; if (files.includes(gT.engineConsts.dirConfigName)) { config = nodeUtils.requireEx(path.join(dir, gT.engineConsts.dirConfigName), true).result; } else { config = {}; } // TODO: some error when suite configs or root configs is met in wrong places. _.pullAll(files, [ gT.engineConsts.suiteConfigName, gT.engineConsts.dirConfigName, gT.engineConsts.suiteResDirName, gT.engineConsts.rootResDirName, gT.engineConsts.dirRootConfigName, gT.engineConsts.suiteRootConfigName, ]); if (config.requireMods) { nodeUtils.requireArray(config.requireMods); } const dirCfg = _.merge(_.cloneDeep(parentDirConfig), config); if (dirCfg.ignoreNames) { _.pullAll(files, dirCfg.ignoreNames); } if (config.browserProfileDir) { dirCfg.browserProfilePath = path.join(gIn.suite.browserProfilesPath, config.browserProfileDir); } else { dirCfg.browserProfilePath = gT.defaultRootProfile; } gIn.tracer.msg3(`Profile path: ${dirCfg.browserProfilePath}`); return dirCfg; } /** * Read directory. Create test info. Start Timer. * Goes into subdirs recursively, runs test files, collects info for suite log. * * @param dir * @param parentDirConfig */ async function handleTestDir(dir: string, parentDirConfig: any) { gIn.tracer.msg3(`handleDir Dir: ${dir}`); let filesOrDirs = fs .readdirSync(dir) .filter(fileName => { for (const pattern of gT.engineConsts.patternsToIgnore) { // eslint-disable-line no-restricted-syntax if (pattern.test(fileName)) { return false; } } return true; }) .sort(); const dirConfig = handleDirConfig(dir, filesOrDirs, parentDirConfig); if (gIn.dirArr && gIn.dirArr.length > 0) { filesOrDirs = [gIn.dirArr.shift()!]; } const dirInfo = gIn.tInfo.createTestInfo(true, dirConfig.sectionTitle, dir); const startTime = gT.timeUtils.startTimer(); for (const fileOrDir of filesOrDirs) { // eslint-disable-line no-restricted-syntax const fileOrDirPath = path.join(dir, fileOrDir); let stat; try { stat = fs.statSync(fileOrDirPath); } catch (e) { continue; // We remove some files in process. } let innerCurInfo; if (stat.isFile() && fileOrDirPath.endsWith(gT.engineConsts.tiaJsSuffix)) { const tsFile = gIn.textUtils.jsToTs(fileOrDirPath); if (fs.existsSync(tsFile)) { throw new Error( `You must use either TS or JS file for test, but not both: ${fileOrDirPath}` ); } innerCurInfo = await handleTestFile(fileOrDirPath, dirConfig); } else if (stat.isFile() && fileOrDirPath.endsWith(gT.engineConsts.tiaTsSuffix)) { innerCurInfo = await handleTestFile(fileOrDirPath, dirConfig); } else if (stat.isDirectory()) { if (fileOrDir === gT.engineConsts.browserProfilesRootDirName) { gIn.tracer.msg3(`Skipping directory ${fileOrDirPath}, because it is browser profile`); continue; } innerCurInfo = await handleTestDir(fileOrDirPath, dirConfig); } else { gIn.tracer.msg3(`Skipping file: ${fileOrDirPath}, because it is not TIA test.`); continue; } // console.log('handleDir, innerCurInfo: ' + innerCurInfo); if (innerCurInfo) { dirInfo.run += innerCurInfo.run; dirInfo.passed += innerCurInfo.passed; dirInfo.failed += innerCurInfo.failed; dirInfo.diffed += innerCurInfo.diffed; dirInfo.expDiffed += innerCurInfo.expDiffed; dirInfo.skipped += innerCurInfo.skipped; dirInfo.children!.push(innerCurInfo); } } dirInfo.time = gT.timeUtils.stopTimer(startTime); return dirInfo; } interface SuiteResult { dirInfo?: any; path?: string; // TODO: path is just for TS. suiteEqualToEtalon?: boolean; diffed?: number; emailSubj?: string; emailSubjConsole?: string; err?: any; } async function runTestSuite(suiteData: any): Promise { const { root, log: suiteLog } = suiteData; // console.log('runAsync Dir: ' + dir); const procInfoFilePath = `${root}/${gT.engineConsts.suiteResDirName}/.procInfo`; const txtAttachments = [suiteLog]; const noTimeSuiteLogFName = `${suiteLog}.notime`; const noTimePlusTestDifsSuiteLogFName = `${suiteLog}.notime.plus.difs`; const prevDifFName = `${noTimeSuiteLogFName}.prev.dif`; const etDifHtmlFName = `${noTimeSuiteLogFName}.et.dif.html`; const etDifTxtFName = `${noTimeSuiteLogFName}.et.dif`; const noTimeSuiteLogPrevFName = `${noTimeSuiteLogFName}.prev`; if (!gT.cLParams.new && !gT.cLParams.dir && !gT.cLParams.pattern) { fileUtils.safeUnlink(suiteLog); fileUtils.safeUnlink(prevDifFName); fileUtils.safeUnlink(etDifHtmlFName); fileUtils.safeUnlink(etDifTxtFName); fileUtils.safeRename(noTimeSuiteLogFName, noTimeSuiteLogPrevFName); suiteUtils.rmNewTestsInfo(); } const dirInfo = await handleTestDir(root, gT.rootDirConfig); dirInfo.isSuiteRoot = true; const { diffed } = dirInfo; if (gT.cLParams.dir) { // gIn.cLogger.msgln(JSON.stringify(dirInfo, null, 2)); gIn.logger.printSuiteLog(dirInfo); } if (gT.cLParams.new) { return { path: '' }; // TODO: path is just for TS. } if (gT.cLParams.pattern || gT.cLParams.dir) { return { dirInfo, path: '' }; // TODO: path is just for TS. } // dirInfo.title = path.basename(dir); gIn.logger.saveSuiteLog({ dirInfo, log: noTimePlusTestDifsSuiteLogFName, noTime: true, }); gIn.logger.saveSuiteLog({ dirInfo, log: noTimeSuiteLogFName, noTime: true, noTestDifs: true, }); const noPrevSLog = fileUtils.isAbsent(noTimeSuiteLogPrevFName); const suiteLogPrevDifRes = gIn.diffUtils.getDiff({ dir: '.', oldFile: noTimeSuiteLogFName, newFile: noTimeSuiteLogPrevFName, }); const suiteLogPrevDifResBool = Boolean(suiteLogPrevDifRes); if (suiteLogPrevDifResBool) { fs.writeFileSync(prevDifFName, suiteLogPrevDifRes, { encoding: gT.engineConsts.logEncoding }); txtAttachments.push(prevDifFName); } let etDifTxt = ''; const suiteLogEtDifResStr = gIn.diffUtils.getDiff({ dir: '.', oldFile: noTimeSuiteLogFName, newFile: gIn.suite.etLog, highlight: 'html', htmlWrap: true, }); const suiteEqualToEtalon = !suiteLogEtDifResStr; if (!suiteEqualToEtalon) { fs.writeFileSync(etDifHtmlFName, suiteLogEtDifResStr, { encoding: gT.engineConsts.logEncoding, }); etDifTxt = gIn.diffUtils.getDiff({ dir: '.', oldFile: noTimeSuiteLogFName, newFile: gIn.suite.etLog, highlight: 'ansi', }); fs.writeFileSync(etDifTxtFName, etDifTxt, { encoding: gT.engineConsts.logEncoding }); } gIn.tracer.msg3(`equalToEtalon: ${suiteEqualToEtalon}`); const etSLogInfoEmail = suiteEqualToEtalon ? 'ET_SLOG, ' : 'DIF_SLOG, '; const etSLogInfoConsole = suiteEqualToEtalon ? `${gIn.cLogger.chalkWrap('green', 'ET_SLOG')}, ` : `${gIn.cLogger.chalkWrap('red', 'DIF_SLOG')}, `; const subjTimeMark = dirInfo.time > gT.cLParams.tooLongTime ? ', TOO_LONG' : ''; const changedEDiffsStr = gIn.suite.changedEDiffs ? `(${gIn.suite.changedEDiffs} dif(s) changed)` : ''; let emailSubj; if (noPrevSLog) { emailSubj = 'NO PREV'; } else if (suiteLogPrevDifResBool) { emailSubj = 'DIF FROM PREV'; } else { emailSubj = `AS PREV${changedEDiffsStr}`; } emailSubj = `${emailSubj}${subjTimeMark},${gIn.logger.saveSuiteLog({ dirInfo, log: suiteLog, })}, ${getOs()}`; const emailSubjConsole = etSLogInfoConsole + emailSubj; emailSubj = etSLogInfoEmail + emailSubj; dirInfo.suiteLogDiff = suiteLogPrevDifResBool; dirInfo.os = getOs(); fileUtils.saveJson(dirInfo, `${suiteLog}.json`); const arcPath = fileUtils.archiveSuiteDir(dirInfo); const procInfo = nodeUtils.getProcInfo(); fs.writeFileSync(procInfoFilePath, procInfo, { encoding: gT.engineConsts.logEncoding }); txtAttachments.push(procInfoFilePath); txtAttachments.push(suiteUtils.getNoEtalonTestsInfoPath()); await gIn.mailUtils.send( emailSubj, suiteLogEtDifResStr, etDifTxt, txtAttachments, arcPath ? [arcPath] : undefined ); const suiteNotEmpty = dirInfo.run + dirInfo.skipped; if (gT.cLParams.slogDifToConsole && etDifTxt && (gT.cLParams.showEmptySuites || suiteNotEmpty)) { gIn.cLogger.msg(`\n${emailSubjConsole}\n`); gIn.cLogger.msgln(etDifTxt); } if (gT.suiteConfig.suiteLogToStdout && (gT.cLParams.showEmptySuites || suiteNotEmpty)) { gIn.cLogger.msg(`\n${emailSubjConsole}\n`); gIn.logger.printSuiteLog(dirInfo); // fileUtils.fileToStdout(log); } if (gT.cLParams.printProcInfo) { gIn.cLogger.msgln(procInfo); } if (gT.suiteConfig.removeZipAfterSend && arcPath) { fileUtils.safeUnlink(arcPath); } return { path: '', // TODO: path is just for TS. suiteEqualToEtalon, diffed, emailSubj, emailSubjConsole, }; } async function prepareAndRunTestSuite(root: string) { const browserProfilesPath = path.resolve( root, gT.engineConsts.suiteResDirName, gT.engineConsts.browserProfilesRootDirName ); const log = path.join( root, gT.engineConsts.suiteResDirName, gT.engineConsts.suiteLogName + gT.engineConsts.logExtension ); const etLog = path.join( root, gT.engineConsts.suiteResDirName, gT.engineConsts.suiteLogName + gT.engineConsts.etalonExtension ); const configPath = path.join( root, gT.engineConsts.suiteResDirName, gT.engineConsts.suiteConfigName ); const suite = { root, browserProfilesPath, log, etLog, configPath, changedEDiffs: 0, }; gIn.suite = suite; handleSuiteConfig(); const suiteResult = await runTestSuite(suite).catch(err => { gIn.tracer.err(`Runner ERR: ${gIn.textUtils.excToStr(err)}`); return { err, } as SuiteResult; }); suiteResult.path = path.relative(gT.cLParams.rootDir, root); return suiteResult; } function getTestSuitePaths() { const suitePaths: string[] = []; function walkSubDirs(parentDir: string) { const dirs = fs.readdirSync(parentDir).filter(childDir => { const fullPath = path.join(parentDir, childDir); if (!fileUtils.isDirectory(fullPath)) { return false; } for (const pattern of gT.engineConsts.patternsToIgnore) { // eslint-disable-line no-restricted-syntax if (pattern.test(childDir)) { return false; } } if (childDir === gT.engineConsts.suiteDirName) { if (fileUtils.isDirectory(path.join(fullPath, gT.engineConsts.suiteResDirName))) { suitePaths.push(fullPath); } else { gIn.tracer.msg1( `Directory ${fullPath} is ignored because does not contain TIA results subdirectory.` ); } return false; } return childDir !== gT.engineConsts.suiteResDirName; }); dirs.forEach(subDir => { const fullPath = path.join(parentDir, subDir); walkSubDirs(fullPath); }); } walkSubDirs(gT.cLParams.rootDir); return suitePaths; } function extractDiffedPaths(testOrDirInfo: TestInfo, result: string[], suiteRoot: string) { if (testOrDirInfo.children) { for (const child of testOrDirInfo.children) { if (child.diffed) { extractDiffedPaths(child, result, suiteRoot); } } } else { result.push(path.relative(suiteRoot, testOrDirInfo.path)); } } // Returns subject for email. export async function runTestSuites() { fileUtils.safeUnlink(gT.rootLog); if (gT.cLParams.stopRemoteDriver) { gIn.remoteDriverUtils.stop(); return 'Just removing of remote driver'; } if (gT.cLParams.useRemoteDriver) { await gIn.remoteDriverUtils.start(); // .catch((err) => { // gIn.tracer.err(`Runner ERR, remoteDriverUtils.start: ${err}`); // }); } const suitePaths = gT.cLParams.suite ? [gT.cLParams.suite] : getTestSuitePaths(); gIn.tracer.msg1(`Following suite paths are found: ${suitePaths}`); const results = []; for (const suitePath of suitePaths) { // eslint-disable-line no-restricted-syntax results.push(await prepareAndRunTestSuite(suitePath)); } if (!gT.cLParams.useRemoteDriver) { await gT.s.driver.quitIfInited(); } else { gIn.tracer.msg3('No force driver.quit() for the last test, due to useRemoteDriver option'); } if (gT.cLParams.new) { gIn.cLogger.msgln('All new tests are finished.'); process.exitCode = 0; return; } if (gT.cLParams.pattern || gT.cLParams.dir) { let isDiffed = false; let run = 0; for (const { dirInfo } of results) { run += dirInfo.run; if (dirInfo.diffed) { isDiffed = true; gIn.cLogger.msg(`Following tests are diffed in suite: ${dirInfo.path}:\n- `); const result: string[] = []; extractDiffedPaths(dirInfo, result, dirInfo.path); gIn.cLogger.msgln(`${result.join('\n- ')}\n`); } } gIn.cLogger.msgln(`Total run count: ${run}.`); process.exitCode = isDiffed ? 1 : 0; return; } const wasError = results.some(result => !result || result.diffed || !result.suiteEqualToEtalon); process.exitCode = wasError ? 1 : 0; const resumeFileStr = wasError ? 'FAILED' : 'PASSED'; const resumeConsoleStr = gIn.cLogger.chalkWrap(wasError ? 'red' : 'green', resumeFileStr); const head = '<<<< Summary of tests results: >>>>'; fs.appendFileSync(gT.rootLog, `${head} \n`); gIn.cLogger.msgln(head); fs.appendFileSync(gT.rootLog, `${resumeFileStr}\n`); gIn.cLogger.msgln(resumeConsoleStr); results.forEach(result => { if (result.err) { fs.appendFileSync(gT.rootLog, `${result.err}\n`); gIn.cLogger.errln(result.err); return; } fs.appendFileSync(gT.rootLog, `${result.emailSubj}\n`); gIn.cLogger.msgln(result.emailSubjConsole!); }); const tail = '<<<< ===================== >>>>'; fs.appendFileSync(gT.rootLog, `${tail} \n`); gIn.cLogger.msgln(tail); }