eslint-remote-tester
Version:
Tool for running ESLint on multiple repositories
386 lines (385 loc) • 11.8 kB
JavaScript
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();