530 lines
14 KiB
JavaScript
530 lines
14 KiB
JavaScript
// ========================================
|
|
// SERVEUR DASHBOARD TESTS - WebSocket + API
|
|
// Interface temps réel pour monitoring des tests systématiques
|
|
// ========================================
|
|
|
|
const WebSocket = require('ws');
|
|
const express = require('express');
|
|
const http = require('http');
|
|
const path = require('path');
|
|
const fs = require('fs').promises;
|
|
const { SystematicTestRunner } = require('../runners/SystematicTestRunner');
|
|
const { ModuleTestGenerator } = require('../systematic/ModuleTestGenerator');
|
|
const { logSh } = require('../../lib/ErrorReporting');
|
|
|
|
/**
|
|
* Serveur dashboard avec WebSocket pour tests en temps réel
|
|
*/
|
|
class TestDashboardServer {
|
|
|
|
constructor(options = {}) {
|
|
this.port = options.port || 8082;
|
|
this.httpPort = options.httpPort || 8083;
|
|
|
|
this.server = null;
|
|
this.wss = null;
|
|
this.httpServer = null;
|
|
this.app = null;
|
|
|
|
this.clients = new Set();
|
|
this.currentTestRun = null;
|
|
this.systemStats = {
|
|
modules: 0,
|
|
tests: 0,
|
|
coverage: 0,
|
|
aiScore: 0,
|
|
status: 'idle'
|
|
};
|
|
|
|
this.testHistory = [];
|
|
}
|
|
|
|
/**
|
|
* Démarrage du serveur dashboard
|
|
*/
|
|
async start() {
|
|
try {
|
|
logSh(`🚀 Démarrage dashboard tests (WebSocket: ${this.port}, HTTP: ${this.httpPort})`, 'INFO');
|
|
|
|
// Serveur HTTP pour l'interface
|
|
await this.setupHTTPServer();
|
|
|
|
// Serveur WebSocket pour communication temps réel
|
|
await this.setupWebSocketServer();
|
|
|
|
logSh('✅ Dashboard tests opérationnel', 'INFO');
|
|
logSh(`🌐 Interface: http://localhost:${this.httpPort}`, 'INFO');
|
|
logSh(`📡 WebSocket: ws://localhost:${this.port}`, 'INFO');
|
|
|
|
} catch (error) {
|
|
logSh(`❌ Erreur démarrage dashboard: ${error.message}`, 'ERROR');
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Configuration du serveur HTTP
|
|
*/
|
|
async setupHTTPServer() {
|
|
this.app = express();
|
|
|
|
// Middleware
|
|
this.app.use(express.json());
|
|
this.app.use(express.static(path.join(__dirname, '.')));
|
|
|
|
// Routes API
|
|
this.setupAPIRoutes();
|
|
|
|
// Route principale - dashboard
|
|
this.app.get('/', (req, res) => {
|
|
res.sendFile(path.join(__dirname, 'TestDashboard.html'));
|
|
});
|
|
|
|
// Démarrage serveur HTTP
|
|
this.httpServer = this.app.listen(this.httpPort, () => {
|
|
logSh(`🌐 Serveur HTTP dashboard démarré sur port ${this.httpPort}`, 'DEBUG');
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Configuration des routes API
|
|
*/
|
|
setupAPIRoutes() {
|
|
// Status système
|
|
this.app.get('/api/status', (req, res) => {
|
|
res.json({
|
|
status: this.systemStats.status,
|
|
stats: this.systemStats,
|
|
clients: this.clients.size,
|
|
currentRun: this.currentTestRun ? this.currentTestRun.id : null,
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
});
|
|
|
|
// Historique des tests
|
|
this.app.get('/api/history', (req, res) => {
|
|
const limit = parseInt(req.query.limit) || 10;
|
|
res.json({
|
|
history: this.testHistory.slice(-limit),
|
|
total: this.testHistory.length
|
|
});
|
|
});
|
|
|
|
// Démarrage de tests via API
|
|
this.app.post('/api/run-tests', async (req, res) => {
|
|
const { mode = 'quick' } = req.body;
|
|
|
|
try {
|
|
await this.runTests(mode);
|
|
res.json({ success: true, message: `Tests ${mode} démarrés` });
|
|
} catch (error) {
|
|
res.status(500).json({ success: false, error: error.message });
|
|
}
|
|
});
|
|
|
|
// Génération de tests
|
|
this.app.post('/api/generate-tests', async (req, res) => {
|
|
try {
|
|
await this.generateTests();
|
|
res.json({ success: true, message: 'Génération de tests démarrée' });
|
|
} catch (error) {
|
|
res.status(500).json({ success: false, error: error.message });
|
|
}
|
|
});
|
|
|
|
// Arrêt de tests
|
|
this.app.post('/api/stop-tests', (req, res) => {
|
|
this.stopCurrentTests();
|
|
res.json({ success: true, message: 'Arrêt des tests demandé' });
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Configuration du serveur WebSocket
|
|
*/
|
|
async setupWebSocketServer() {
|
|
this.server = http.createServer();
|
|
this.wss = new WebSocket.Server({ server: this.server });
|
|
|
|
this.wss.on('connection', (ws, req) => {
|
|
this.handleClientConnection(ws, req);
|
|
});
|
|
|
|
this.server.listen(this.port, () => {
|
|
logSh(`📡 Serveur WebSocket dashboard démarré sur port ${this.port}`, 'DEBUG');
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Gestion des connexions WebSocket
|
|
*/
|
|
handleClientConnection(ws, req) {
|
|
const clientId = `client_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
|
|
logSh(`🔌 Client connecté: ${clientId}`, 'DEBUG');
|
|
this.clients.add(ws);
|
|
|
|
// Envoi de l'état initial
|
|
this.sendToClient(ws, {
|
|
type: 'connection_established',
|
|
clientId,
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
|
|
this.sendToClient(ws, {
|
|
type: 'system_stats',
|
|
stats: this.systemStats
|
|
});
|
|
|
|
// Gestion des messages clients
|
|
ws.on('message', (message) => {
|
|
try {
|
|
const data = JSON.parse(message);
|
|
this.handleClientMessage(ws, data);
|
|
} catch (error) {
|
|
logSh(`⚠️ Message client invalide: ${error.message}`, 'WARNING');
|
|
}
|
|
});
|
|
|
|
// Déconnexion client
|
|
ws.on('close', () => {
|
|
this.clients.delete(ws);
|
|
logSh(`🔌 Client déconnecté: ${clientId}`, 'DEBUG');
|
|
});
|
|
|
|
ws.on('error', (error) => {
|
|
logSh(`❌ Erreur WebSocket client: ${error.message}`, 'ERROR');
|
|
this.clients.delete(ws);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Gestion des messages des clients
|
|
*/
|
|
async handleClientMessage(ws, data) {
|
|
try {
|
|
switch (data.action) {
|
|
case 'run_tests':
|
|
await this.runTests(data.mode || 'quick');
|
|
break;
|
|
|
|
case 'generate_tests':
|
|
await this.generateTests();
|
|
break;
|
|
|
|
case 'stop_tests':
|
|
this.stopCurrentTests();
|
|
break;
|
|
|
|
case 'get_status':
|
|
this.sendToClient(ws, {
|
|
type: 'system_stats',
|
|
stats: this.systemStats
|
|
});
|
|
break;
|
|
|
|
default:
|
|
logSh(`⚠️ Action inconnue: ${data.action}`, 'WARNING');
|
|
}
|
|
} catch (error) {
|
|
logSh(`❌ Erreur traitement message client: ${error.message}`, 'ERROR');
|
|
this.sendToClient(ws, {
|
|
type: 'error',
|
|
message: error.message
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Exécution des tests avec monitoring
|
|
*/
|
|
async runTests(mode) {
|
|
if (this.currentTestRun && this.currentTestRun.active) {
|
|
throw new Error('Des tests sont déjà en cours');
|
|
}
|
|
|
|
const runId = `run_${Date.now()}`;
|
|
this.currentTestRun = {
|
|
id: runId,
|
|
mode,
|
|
startTime: new Date(),
|
|
active: true,
|
|
progress: 0
|
|
};
|
|
|
|
this.systemStats.status = 'running';
|
|
|
|
// Notification clients
|
|
this.broadcast({
|
|
type: 'test_started',
|
|
runId,
|
|
mode,
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
|
|
logSh(`🧪 Démarrage tests ${mode} (${runId})`, 'INFO');
|
|
|
|
try {
|
|
// Exécution avec callbacks de progression
|
|
const results = await this.runTestsWithProgress(mode);
|
|
|
|
// Finalisation
|
|
this.currentTestRun.active = false;
|
|
this.currentTestRun.endTime = new Date();
|
|
this.currentTestRun.results = results;
|
|
|
|
this.systemStats.status = 'idle';
|
|
this.updateSystemStats(results);
|
|
|
|
// Sauvegarde historique
|
|
this.testHistory.push({
|
|
...this.currentTestRun,
|
|
summary: {
|
|
modules: results.overview.totalModules,
|
|
passed: results.overview.successful,
|
|
failed: results.overview.failed,
|
|
duration: results.overview.duration
|
|
}
|
|
});
|
|
|
|
// Notification clients
|
|
this.broadcast({
|
|
type: 'test_completed',
|
|
runId,
|
|
results: results.overview,
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
|
|
logSh(`✅ Tests terminés (${runId}): ${results.overview.successful}/${results.overview.totalModules}`, 'INFO');
|
|
|
|
} catch (error) {
|
|
this.currentTestRun.active = false;
|
|
this.currentTestRun.error = error.message;
|
|
this.systemStats.status = 'error';
|
|
|
|
logSh(`❌ Erreur tests (${runId}): ${error.message}`, 'ERROR');
|
|
|
|
this.broadcast({
|
|
type: 'test_error',
|
|
runId,
|
|
error: error.message,
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Exécution avec callbacks de progression
|
|
*/
|
|
async runTestsWithProgress(mode) {
|
|
const progressCallback = (progress, message, moduleData) => {
|
|
this.currentTestRun.progress = progress;
|
|
|
|
this.broadcast({
|
|
type: 'test_progress',
|
|
runId: this.currentTestRun.id,
|
|
progress,
|
|
message,
|
|
module: moduleData ? moduleData.name : null,
|
|
status: moduleData ? moduleData.status : null,
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
};
|
|
|
|
const validationCallback = (module, validation) => {
|
|
this.broadcast({
|
|
type: 'validation_result',
|
|
runId: this.currentTestRun.id,
|
|
module,
|
|
validation,
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
};
|
|
|
|
// Hooks personnalisés pour SystematicTestRunner
|
|
const options = {
|
|
progressCallback,
|
|
validationCallback,
|
|
mode
|
|
};
|
|
|
|
if (mode === 'quick') {
|
|
return await SystematicTestRunner.runQuick(options);
|
|
} else {
|
|
return await SystematicTestRunner.runDetailed(options);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Génération de tests
|
|
*/
|
|
async generateTests() {
|
|
logSh('🔧 Génération des tests démarrée', 'INFO');
|
|
|
|
this.broadcast({
|
|
type: 'log',
|
|
level: 'INFO',
|
|
message: 'Génération automatique des tests démarrée',
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
|
|
try {
|
|
const results = await ModuleTestGenerator.generateAllTests();
|
|
|
|
this.systemStats.tests = results.modules.reduce((sum, m) => sum + m.testCount, 0);
|
|
this.systemStats.modules = results.generated;
|
|
|
|
this.broadcast({
|
|
type: 'generation_completed',
|
|
results: {
|
|
generated: results.generated,
|
|
errors: results.errors,
|
|
totalTests: this.systemStats.tests
|
|
},
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
|
|
logSh(`✅ Génération terminée: ${results.generated} modules, ${this.systemStats.tests} tests`, 'INFO');
|
|
|
|
} catch (error) {
|
|
logSh(`❌ Erreur génération: ${error.message}`, 'ERROR');
|
|
|
|
this.broadcast({
|
|
type: 'generation_error',
|
|
error: error.message,
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Arrêt des tests en cours
|
|
*/
|
|
stopCurrentTests() {
|
|
if (this.currentTestRun && this.currentTestRun.active) {
|
|
this.currentTestRun.active = false;
|
|
this.currentTestRun.stopped = true;
|
|
this.systemStats.status = 'stopping';
|
|
|
|
logSh(`🛑 Arrêt des tests demandé (${this.currentTestRun.id})`, 'INFO');
|
|
|
|
this.broadcast({
|
|
type: 'test_stopped',
|
|
runId: this.currentTestRun.id,
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Mise à jour des statistiques système
|
|
*/
|
|
updateSystemStats(results) {
|
|
if (results && results.overview) {
|
|
this.systemStats.modules = results.overview.totalModules || 0;
|
|
this.systemStats.coverage = results.overview.coverage || 0;
|
|
|
|
// Calcul score IA moyen
|
|
if (results.phases && results.phases.aiValidation && results.phases.aiValidation.validations) {
|
|
const validations = Object.values(results.phases.aiValidation.validations);
|
|
const scores = validations.filter(v => v.overall).map(v => v.overall);
|
|
this.systemStats.aiScore = scores.length > 0
|
|
? Math.round(scores.reduce((a, b) => a + b, 0) / scores.length)
|
|
: 0;
|
|
}
|
|
}
|
|
|
|
this.broadcast({
|
|
type: 'system_stats',
|
|
stats: this.systemStats
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Communication WebSocket
|
|
*/
|
|
sendToClient(ws, data) {
|
|
if (ws.readyState === WebSocket.OPEN) {
|
|
ws.send(JSON.stringify(data));
|
|
}
|
|
}
|
|
|
|
broadcast(data) {
|
|
this.clients.forEach(client => {
|
|
this.sendToClient(client, data);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Arrêt du serveur
|
|
*/
|
|
async stop() {
|
|
logSh('🛑 Arrêt du serveur dashboard', 'INFO');
|
|
|
|
// Arrêt des tests en cours
|
|
this.stopCurrentTests();
|
|
|
|
// Fermeture connexions WebSocket
|
|
this.clients.forEach(client => {
|
|
client.close();
|
|
});
|
|
|
|
// Arrêt serveurs
|
|
if (this.wss) {
|
|
this.wss.close();
|
|
}
|
|
|
|
if (this.server) {
|
|
this.server.close();
|
|
}
|
|
|
|
if (this.httpServer) {
|
|
this.httpServer.close();
|
|
}
|
|
|
|
logSh('✅ Dashboard arrêté', 'INFO');
|
|
}
|
|
|
|
/**
|
|
* État du serveur
|
|
*/
|
|
getStatus() {
|
|
return {
|
|
running: !!this.server && !!this.httpServer,
|
|
clients: this.clients.size,
|
|
ports: {
|
|
websocket: this.port,
|
|
http: this.httpPort
|
|
},
|
|
currentRun: this.currentTestRun,
|
|
stats: this.systemStats,
|
|
uptime: process.uptime()
|
|
};
|
|
}
|
|
}
|
|
|
|
// Exécution directe pour tests
|
|
if (require.main === module) {
|
|
const dashboard = new TestDashboardServer();
|
|
|
|
dashboard.start()
|
|
.then(() => {
|
|
console.log('🎯 Dashboard de tests démarré avec succès!');
|
|
console.log(`🌐 Interface: http://localhost:${dashboard.httpPort}`);
|
|
|
|
// Gestion arrêt propre
|
|
process.on('SIGINT', async () => {
|
|
console.log('\n🛑 Arrêt du dashboard...');
|
|
await dashboard.stop();
|
|
process.exit(0);
|
|
});
|
|
})
|
|
.catch(error => {
|
|
console.error('💥 Erreur démarrage dashboard:', error.message);
|
|
process.exit(1);
|
|
});
|
|
}
|
|
|
|
module.exports = { TestDashboardServer }; |