UNPKG

eslint-remote-tester

Version:
386 lines (385 loc) 11.8 kB
import chalk from 'chalk'; import * as Templates from './log-templates.js'; import exitHandler from './exit-handler.js'; import config from '../config/index.js'; const CI_KEEP_ALIVE_INTERVAL_MS = 4.5 * 60 * 1000; const DEFAULT_COLOR = (text) => text; /** * Resolve color for message or task */ export function resolveColor(taskOrMessage) { return (taskOrMessage.color && chalk[taskOrMessage.color]) || DEFAULT_COLOR; } /** * Check whether log is filtered out by `config.logLevel` */ function isLogVisible(log) { switch (config.logLevel) { case 'verbose': return true; case 'info': return ['info', 'warn', 'error'].includes(log.level); case 'warn': return ['warn', 'error'].includes(log.level); case 'error': return log.level === 'error'; default: return false; } } /** * Check whether task is filtered out by `config.logLevel` * - Tasks are considered as "logs" on CI only. CLI mode displays only active ones. */ function isTasksVisible() { return config.CI === false || config.logLevel === 'verbose'; } /** * Logger for holding state of current progress * - Exposes different logs via `on` subscribe method */ class ProgressLogger { /** Messages printed as a list under tasks */ messages = []; /** Messages of the task runners */ tasks = []; /** Count of finished repositories */ scannedRepositories = 0; /** Total count of errors */ errorCount = 0; /** Event listeners */ listeners = { exit: [], message: [], task: [], ciKeepAlive: [], timeout: [], }; /** Indicates whether scan has reached time limit set by `config.timeLimit` */ hasTimedout = false; /** Handle of scan timeout. Used to interrupt scan once time limit has been reached. */ scanTimeoutHandle = null; /** Interval of CI status messages. Used to avoid CIs timing out due to silent stdout. */ ciKeepAliveIntervalHandle = null; constructor() { if (config.CI) { this.ciKeepAliveIntervalHandle = setInterval(() => { this.onCiStatus(); }, CI_KEEP_ALIVE_INTERVAL_MS); } this.scanTimeoutHandle = setTimeout(() => { this.onScanTimeout(); }, config.timeLimit * 1000); } /** * Subscribe on logger's events */ on(event, listener) { const eventListeners = this.listeners[event]; if (eventListeners) { eventListeners.push(listener); } return this; } /** * Unsubscribe from logger's events */ off(event, listener) { const eventListeners = this.listeners[event]; if (eventListeners) { const index = eventListeners.indexOf(listener); if (index !== -1) { eventListeners.splice(index, 1); } } return this; } /** * Add new message to logger */ addNewMessage(message) { this.messages.push(message); if (isLogVisible(message)) { this.listeners.message.forEach(listener => listener(message)); } } /** * Get current log messages */ getMessages() { return this.messages.filter(message => isLogVisible(message)); } /** * Check whether scan has timed out */ isTimeout() { return this.hasTimedout; } /** * Add final message and fire exit event */ onAllRepositoriesScanned() { this.addNewMessage({ content: Templates.SCAN_FINISHED(this.scannedRepositories), color: 'green', level: 'verbose', }); if (this.ciKeepAliveIntervalHandle !== null) { clearInterval(this.ciKeepAliveIntervalHandle); } if (this.scanTimeoutHandle !== null) { clearTimeout(this.scanTimeoutHandle); } const notifyListeners = () => this.listeners.exit.forEach(listener => listener()); exitHandler(this.scannedRepositories) .then(messages => { messages.forEach(message => this.addNewMessage(message)); notifyListeners(); }) .catch(error => { // Erroneous exit handler should not crash whole application. // Log the error and move on. console.error(error); notifyListeners(); }); } /** * Apply updates to given task */ updateTask(repository, updates) { const taskExists = this.tasks.find(task => task.repository === repository); let updatedTask; if (taskExists) { this.tasks = this.tasks.map(task => { if (task.repository !== repository) { return task; } updatedTask = { ...task, ...updates }; return updatedTask; }); } else { updatedTask = { repository, ...updates }; this.tasks.push(updatedTask); } if (isTasksVisible()) { this.listeners.task.forEach(listener => listener(updatedTask)); } } /** * Apply warning to given task. Duplicate warnings are ignored. * Returns boolean indicating whether warning did not exist on task already */ addWarningToTask(repository, warning) { const task = this.tasks.find(task => task.repository === repository); if (task) { const warnings = task.warnings || []; const hasWarnedAlready = warnings.includes(warning); if (!hasWarnedAlready) { this.updateTask(repository, { warnings: [...warnings, warning], }); return true; } } return false; } /** * Log start of task runner */ onTaskStart(repository) { this.updateTask(repository, { step: 'START', color: 'yellow', }); } /** * Log start of linting of given repository */ onLintStart(repository, fileCount) { this.updateTask(repository, { fileCount, currentFileIndex: 0, step: 'LINT', color: 'yellow', }); } /** * Log end of linting of given repository */ onLintEnd(repository, resultCount) { const hasErrors = resultCount > 0; this.scannedRepositories++; this.errorCount += resultCount; this.addNewMessage({ content: Templates.LINT_END_TEMPLATE(repository, resultCount), color: hasErrors ? 'red' : 'green', level: hasErrors ? 'error' : 'verbose', }); const task = this.tasks.find(task => task.repository === repository); if (task) { this.tasks = this.tasks.filter(t => t !== task); if (isTasksVisible()) { this.listeners.task.forEach(listener => listener(task, true)); } } } /** * Log end of a single file lint */ onFileLintEnd(repository, currentFileIndex) { this.updateTask(repository, { currentFileIndex, step: 'LINT', color: 'green', }); } /** * Log warning about slow linting */ onFileLintSlow(repository, lintTime, file) { const isNewWarning = this.addWarningToTask(repository, file); if (isNewWarning) { this.addNewMessage({ content: Templates.LINT_SLOW_TEMPLATE(lintTime, file), color: 'yellow', level: 'warn', }); } } /** * Log error about linter crashing */ onLinterCrash(repository, erroneousRule) { const isNewWarning = this.addWarningToTask(repository, erroneousRule); if (isNewWarning) { this.addNewMessage({ content: Templates.LINT_FAILURE_TEMPLATE(repository, erroneousRule), color: 'red', level: 'error', }); } } /** * Log error about worker crashing */ onWorkerCrash(repository, errorCode) { const isNewWarning = this.addWarningToTask(repository, 'worker-crash'); if (isNewWarning) { this.addNewMessage({ content: Templates.WORKER_FAILURE_TEMPLATE(repository, errorCode), color: 'red', level: 'error', }); } } /** * Log error about clone failure */ onCloneFailure(repository) { this.addNewMessage({ content: Templates.CLONE_FAILURE_TEMPLATE(repository), color: 'red', level: 'error', }); } /** * Log error about pull failure */ onPullFailure(repository) { this.addNewMessage({ content: Templates.PULL_FAILURE_TEMPLATE(repository), color: 'red', level: 'error', }); } /** * Log error about filesystem read failure */ onReadFailure(repository) { this.addNewMessage({ content: Templates.READ_FAILURE_TEMPLATE(repository), color: 'red', level: 'error', }); } /** * Log error about result writing failure */ onWriteFailure(repository, error) { this.addNewMessage({ content: Templates.WRITE_FAILURE_TEMPLATE(repository, error), color: 'red', level: 'error', }); } /** * Log start of cloning of given repository */ onRepositoryClone(repository) { this.updateTask(repository, { step: 'CLONE', color: 'yellow' }); } /** * Log start of pulling of given repository */ onRepositoryPull(repository) { this.updateTask(repository, { step: 'PULL', color: 'yellow' }); } /** * Log start of cloning of given repository */ onRepositoryRead(repository) { this.updateTask(repository, { step: 'READ', color: 'yellow' }); } /** * Log status of scanning to CI * - These are used to avoid CI timeouts */ onCiStatus() { if (['verbose', 'info'].includes(config.logLevel)) { const message = Templates.CI_STATUS_TEMPLATE(this.scannedRepositories, this.errorCount, this.tasks); this.listeners.ciKeepAlive.forEach(listener => listener(message)); } } /** * Log status of cache. Includes count of cached repositories and location of cache. * Only used in CLI */ onCacheStatus(status) { if (config.CI) return; // Prevent logging useless "0 cached repositories" cases if (status.countOfRepositories <= 0) return; if (['verbose', 'info'].includes(config.logLevel)) { this.addNewMessage({ content: Templates.CACHE_STATUS_TEMPLATE(status.countOfRepositories, status.location), level: 'info', color: 'yellow', }); } } /** * Log notification about reaching scan time limit and notify listeners */ onScanTimeout() { this.addNewMessage({ content: Templates.SCAN_TIMELIMIT_REACHED(config.timeLimit), level: 'info', color: 'yellow', }); this.hasTimedout = true; this.listeners.timeout.forEach(listener => listener()); } /** * Log debug message */ onDebug(...messages) { this.addNewMessage({ content: Templates.DEBUG_TEMPLATE(messages.join('\n')), color: 'yellow', level: 'error', // Always visible }); } } export default new ProgressLogger();