chrome-extension-test-framework
Version:
Universal testing framework for Chrome extensions - Fast static analysis without browser dependencies
331 lines (281 loc) • 9.75 kB
JavaScript
/**
* ParallelRunner - テストの並列実行を管理
*/
const { Worker } = require('worker_threads');
const os = require('os');
const path = require('path');
const EventEmitter = require('events');
class ParallelRunner extends EventEmitter {
constructor(options = {}) {
super();
// CPUコア数に基づいてワーカー数を決定
this.maxWorkers = options.maxWorkers || Math.max(1, os.cpus().length - 1);
this.workers = [];
this.taskQueue = [];
this.activeWorkers = 0;
this.results = new Map();
this.startTime = null;
// 統計情報
this.stats = {
totalTasks: 0,
completedTasks: 0,
failedTasks: 0,
workerUtilization: new Map()
};
}
/**
* テストスイートを並列実行
*/
async runSuites(suites, config) {
this.startTime = Date.now();
this.stats.totalTasks = suites.length;
console.log(`\n🚀 Starting parallel execution with ${this.maxWorkers} workers`);
console.log(` Total test suites: ${suites.length}\n`);
// タスクキューに追加
suites.forEach((suite, index) => {
this.taskQueue.push({
id: index,
suite: suite.constructor.name,
config: config
});
});
// ワーカープールを作成
await this.createWorkerPool();
// すべてのタスクが完了するまで待機
await this.waitForCompletion();
// 結果を集約
return this.aggregateResults();
}
/**
* ワーカープールを作成
*/
async createWorkerPool() {
const workerPath = path.join(__dirname, 'worker.js');
for (let i = 0; i < this.maxWorkers; i++) {
await this.createWorker(i, workerPath);
}
}
/**
* ワーカーを作成
*/
async createWorker(workerId, workerPath) {
const worker = new Worker(workerPath, {
workerData: { workerId }
});
// ワーカーのイベントハンドラを設定
worker.on('message', (message) => {
this.handleWorkerMessage(workerId, message);
});
worker.on('error', (error) => {
console.error(`❌ Worker ${workerId} error:`, error);
this.handleWorkerError(workerId, error);
});
worker.on('exit', (code) => {
if (code !== 0) {
console.error(`❌ Worker ${workerId} exited with code ${code}`);
}
this.handleWorkerExit(workerId);
});
this.workers[workerId] = worker;
this.stats.workerUtilization.set(workerId, 0);
// 初期タスクを割り当て
this.assignNextTask(workerId);
}
/**
* ワーカーメッセージを処理
*/
handleWorkerMessage(workerId, message) {
switch (message.type) {
case 'task-start':
this.emit('suite-start', {
workerId,
suite: message.suite
});
break;
case 'progress':
this.emit('progress', {
workerId,
suite: message.suite,
test: message.test,
status: message.status
});
break;
case 'task-complete':
this.handleTaskComplete(workerId, message);
break;
case 'task-error':
this.handleTaskError(workerId, message);
break;
}
}
/**
* タスク完了を処理
*/
handleTaskComplete(workerId, message) {
const { taskId, result, duration } = message;
// 結果を保存
this.results.set(taskId, result);
this.stats.completedTasks++;
// ワーカーの使用率を更新
const utilization = this.stats.workerUtilization.get(workerId) || 0;
this.stats.workerUtilization.set(workerId, utilization + 1);
console.log(`✅ Worker ${workerId} completed ${result.suite} (${duration}ms)`);
this.emit('suite-complete', {
workerId,
suite: result.suite,
duration,
passed: result.summary.passed,
failed: result.summary.failed
});
// 次のタスクを割り当て
this.assignNextTask(workerId);
}
/**
* タスクエラーを処理
*/
handleTaskError(workerId, message) {
const { taskId, error } = message;
console.error(`❌ Worker ${workerId} task ${taskId} failed:`, error);
this.stats.failedTasks++;
this.results.set(taskId, {
suite: 'Unknown',
error: error,
summary: { total: 0, passed: 0, failed: 0 }
});
// 次のタスクを割り当て
this.assignNextTask(workerId);
}
/**
* ワーカーエラーを処理
*/
handleWorkerError(workerId, error) {
// ワーカーを再起動
console.log(`🔄 Restarting worker ${workerId}...`);
this.workers[workerId] = null;
setTimeout(() => {
const workerPath = path.join(__dirname, 'worker.js');
this.createWorker(workerId, workerPath);
}, 1000);
}
/**
* ワーカー終了を処理
*/
handleWorkerExit(workerId) {
this.activeWorkers--;
this.workers[workerId] = null;
}
/**
* 次のタスクを割り当て
*/
assignNextTask(workerId) {
if (this.taskQueue.length === 0) {
this.activeWorkers--;
return;
}
const task = this.taskQueue.shift();
const worker = this.workers[workerId];
if (worker) {
this.activeWorkers++;
worker.postMessage({
type: 'run-task',
task: task
});
}
}
/**
* すべてのタスクの完了を待機
*/
async waitForCompletion() {
return new Promise((resolve) => {
const checkInterval = setInterval(() => {
if (this.stats.completedTasks + this.stats.failedTasks >= this.stats.totalTasks) {
clearInterval(checkInterval);
this.terminateWorkers();
resolve();
}
}, 100);
});
}
/**
* ワーカーを終了
*/
terminateWorkers() {
this.workers.forEach((worker, workerId) => {
if (worker) {
worker.terminate();
console.log(`🛑 Worker ${workerId} terminated`);
}
});
}
/**
* 結果を集約
*/
aggregateResults() {
const duration = Date.now() - this.startTime;
const results = Array.from(this.results.values());
// サマリーを計算
const summary = {
total: 0,
passed: 0,
failed: 0
};
results.forEach(result => {
if (result.summary) {
summary.total += result.summary.total;
summary.passed += result.summary.passed;
summary.failed += result.summary.failed;
}
});
// ワーカー使用率を計算
const workerStats = [];
this.stats.workerUtilization.forEach((tasks, workerId) => {
workerStats.push({
workerId,
tasksCompleted: tasks,
utilization: Math.round((tasks / this.stats.totalTasks) * 100)
});
});
console.log('\n📊 Parallel Execution Summary:');
console.log(` Total duration: ${duration}ms`);
console.log(` Workers used: ${this.maxWorkers}`);
console.log(` Tasks completed: ${this.stats.completedTasks}`);
console.log(` Tasks failed: ${this.stats.failedTasks}`);
console.log('\n Worker utilization:');
workerStats.forEach(stat => {
console.log(` - Worker ${stat.workerId}: ${stat.tasksCompleted} tasks (${stat.utilization}%)`);
});
return {
suites: results,
summary,
execution: {
duration,
parallel: true,
workers: this.maxWorkers,
workerStats
}
};
}
/**
* 並列実行が有効かチェック
*/
static isSupported() {
try {
require('worker_threads');
return true;
} catch (error) {
return false;
}
}
/**
* 最適なワーカー数を計算
*/
static getOptimalWorkerCount(suiteCount) {
const cpuCount = os.cpus().length;
const optimal = Math.min(
Math.max(1, cpuCount - 1), // CPUコア数 - 1
suiteCount // スイート数を超えない
);
return optimal;
}
}
module.exports = ParallelRunner;