const io = require('socket.io-client'); const axios = require('axios'); const { execSync } = require('child_process'); const yargs = require('yargs/yargs'); const { hideBin } = require('yargs/helpers'); const argv = yargs(hideBin(process.argv)) .option('trips', { alias: 't', type: 'number', description: 'Number of concurrent trips to simulate', default: 1 }) .option('base-url', { alias: 'b', type: 'string', description: 'Base URL for backend APIs', default: 'http://localhost/backend' }) .option('socket-url', { alias: 's', type: 'string', description: 'Location server socket URL', default: 'http://localhost:2020' }) .option('debug', { alias: 'd', type: 'boolean', description: 'Enable debug logging', default: false }) .argv; function debug(msg) { if (argv.debug) console.log(` [DEBUG] ${msg}`); } // Helper to get JWT and FP for mock user function getMockJwt(userId, role = 'driver') { try { const output = execSync(`php generate_mock_data.php ${userId} ${role}`, { timeout: 5000 }).toString(); debug(`JWT output for ${role} ${userId}: ${output.substring(0, 80)}...`); const jsonString = output.substring(output.indexOf('{')); const parsed = JSON.parse(jsonString); if (!parsed.jwt) { console.error(`[Error] JWT generation returned empty for ${role} ${userId}: ${output}`); return { jwt: '', fp: '' }; } return { jwt: parsed.jwt, fp: parsed.fp || `mock_device_fp_${userId}`, price_token: parsed.price_token || '' }; } catch (err) { console.error(`[Error] Failed to generate JWT for ${role} ${userId}:`, err.message); return { jwt: '', fp: '' }; } } let metrics = { successful: 0, failed: 0, connectionErrors: 0, apiErrors: 0, jwtErrors: 0, totalTime: 0 }; async function simulateTrip(tripIndex) { const driverId = 900000 + tripIndex; const passengerId = 800000 + tripIndex; const driverAuth = getMockJwt(driverId, 'driver'); const passengerAuth = getMockJwt(passengerId, 'passenger'); if (!driverAuth.jwt || !passengerAuth.jwt) { console.error(`[Trip ${tripIndex}] Skipping — JWT generation failed.`); metrics.jwtErrors++; metrics.failed++; return false; } console.log(`[Trip ${tripIndex}] Starting... Driver: ${driverId}, Passenger: ${passengerId}`); const tripStartTime = Date.now(); // 1. Connect Driver to Socket const socketUrl = argv.socketUrl; debug(`Connecting to socket at: ${socketUrl}`); const socket = io(socketUrl, { query: { driver_id: String(driverId), platform: 'android', jwt: driverAuth.jwt }, reconnection: false, timeout: 10000, forceNew: true }); return new Promise((resolve) => { let rideId = null; let resolved = false; function finish(success) { if (resolved) return; resolved = true; try { socket.disconnect(); } catch(e) {} resolve(success); } socket.on('connect_error', (err) => { console.error(`[Trip ${tripIndex}] ❌ Socket Connection Error: ${err.message}`); metrics.connectionErrors++; metrics.failed++; finish(false); }); socket.on('error', (err) => { console.error(`[Trip ${tripIndex}] ❌ Socket Error: ${err}`); metrics.connectionErrors++; metrics.failed++; finish(false); }); socket.on('connect', async () => { console.log(`[Trip ${tripIndex}] ✅ Driver connected to socket.`); // Register driver location in Redis available pool socket.emit('update_location', { lat: 31.95, lng: 35.91, heading: 90, speed: 0, status: 'off' }); await new Promise(r => setTimeout(r, 750)); // 2. Passenger Requests Ride try { const addRideFormData = new URLSearchParams({ passenger_id: String(passengerId), start_location: '31.95,35.91', end_location: '31.96,35.92', price: '5', price_token: passengerAuth.price_token || 'dummy', distance: '2', carType: 'Economy', passenger_name: 'Test Passenger', passenger_phone: '0790000000', start_name: 'Start', end_name: 'End' }); const reqStart = Date.now(); const res = await axios.post( `${argv.baseUrl}/ride/rides/add_ride.php`, addRideFormData.toString(), { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization': `Bearer ${passengerAuth.jwt}`, 'X-Device-FP': passengerAuth.fp }, timeout: 10000 } ); debug(`add_ride response: ${JSON.stringify(res.data).substring(0, 100)}`); console.log(`[Trip ${tripIndex}] Passenger requested ride. (API: ${Date.now() - reqStart}ms)`); } catch (err) { console.error(`[Trip ${tripIndex}] ❌ API Error (add_ride): ${err.message}`); if (err.response) debug(`Response: ${JSON.stringify(err.response.data).substring(0, 200)}`); metrics.apiErrors++; metrics.failed++; finish(false); return; } }); socket.on('market_new_ride', async (payload) => { rideId = payload.id || payload[0]?.id; console.log(`[Trip ${tripIndex}] 📨 Received ride offer #${rideId}. Accepting...`); try { const acceptFormData = new URLSearchParams({ id: String(rideId), driver_id: String(driverId), status: 'accepted' }); const acceptStart = Date.now(); await axios.post( `${argv.baseUrl}/ride/rides/acceptRide.php`, acceptFormData.toString(), { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization': `Bearer ${driverAuth.jwt}`, 'X-Device-FP': driverAuth.fp }, timeout: 10000 } ); console.log(`[Trip ${tripIndex}] Accepted ride #${rideId}. (API: ${Date.now() - acceptStart}ms)`); // 4. Start Ride API Call try { const startFormData = new URLSearchParams({ id: String(rideId), driver_id: String(driverId), status: 'Begin' }); await axios.post( `${argv.baseUrl}/ride/rides/start_ride.php`, startFormData.toString(), { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization': `Bearer ${driverAuth.jwt}`, 'X-Device-FP': driverAuth.fp }, timeout: 10000 } ); console.log(`[Trip ${tripIndex}] 🚗 Started ride #${rideId}`); } catch (e) { debug(`start_ride error: ${e.message}`); } // 5. Simulate Driving (4 Random GPS Location Updates) let currentLat = 31.95; let currentLng = 35.91; let steps = 0; const driveInterval = setInterval(async () => { steps++; currentLat += 0.0005 + (Math.random() * 0.0005); currentLng += 0.0005 + (Math.random() * 0.0005); socket.emit('update_location', { lat: currentLat, lng: currentLng, heading: Math.floor(Math.random() * 360), speed: 40 + Math.floor(Math.random() * 20), status: 'on' }); console.log(`[Trip ${tripIndex}] 📍 Driver GPS update #${steps}: (${currentLat.toFixed(4)}, ${currentLng.toFixed(4)})`); if (steps >= 4) { clearInterval(driveInterval); // 6. Finish Ride API Call try { const finishFormData = new URLSearchParams({ rideId: String(rideId), driver_id: String(driverId), passengerId: String(passengerId), status: 'Finished', actualDistance: '2.5', actualDuration: '10', country_code: 'Jordan' }); await axios.post( `${argv.baseUrl}/ride/rides/finish_ride_updates.php`, finishFormData.toString(), { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization': `Bearer ${driverAuth.jwt}`, 'X-Device-FP': driverAuth.fp }, timeout: 10000 } ); console.log(`[Trip ${tripIndex}] 🏁 Finished ride #${rideId} successfully.`); } catch (e) { debug(`finish_ride error: ${e.message}`); } metrics.successful++; finish(true); } }, 500); } catch (err) { console.error(`[Trip ${tripIndex}] ❌ API Error (acceptRide): ${err.message}`); metrics.apiErrors++; metrics.failed++; finish(false); } }); socket.on('disconnect', (reason) => { debug(`[Trip ${tripIndex}] Disconnected: ${reason}`); }); // Timeout fallback setTimeout(() => { if (!resolved) { console.error(`[Trip ${tripIndex}] ⏰ Timeout: No 'market_new_ride' received within 20s.`); metrics.failed++; finish(false); } }, 20000); }); } async function runTest() { console.log(`\n======================================================`); console.log(`🚀 Siro Stress Test`); console.log(`======================================================`); console.log(`📡 Backend API: ${argv.baseUrl}`); console.log(`🔌 Socket URL: ${argv.socketUrl}`); console.log(`🔢 Trips: ${argv.trips}`); console.log(`🐛 Debug: ${argv.debug ? 'ON' : 'OFF'}`); console.log(`------------------------------------------------------`); console.log(`\n🔑 Pre-flight: Testing JWT generation...`); const testJwt = getMockJwt(999999, 'driver'); if (!testJwt.jwt) { console.error(`❌ FATAL: Cannot generate JWT tokens.`); process.exit(1); } console.log(` ✅ JWT generation works.`); console.log(`\n🔌 Pre-flight: Testing socket connection...`); const testResult = await new Promise((resolve) => { const testSocket = io(argv.socketUrl, { query: { driver_id: '999999', platform: 'android', jwt: testJwt.jwt }, reconnection: false, timeout: 5000, forceNew: true }); testSocket.on('connect', () => { console.log(` ✅ Socket connection successful!`); testSocket.disconnect(); resolve(true); }); testSocket.on('connect_error', (err) => { console.error(` ❌ Socket connection failed: ${err.message}`); testSocket.disconnect(); resolve(false); }); testSocket.on('error', (err) => { console.error(` ❌ Socket error: ${err}`); testSocket.disconnect(); resolve(false); }); setTimeout(() => { testSocket.disconnect(); resolve(false); }, 6000); }); if (!testResult) { console.error(`\n❌ Cannot proceed — socket server unreachable. Aborting.`); process.exit(1); } console.log(`\n------------------------------------------------------`); console.log(`🏁 Starting ${argv.trips} concurrent trips...\n`); const startTime = Date.now(); const promises = []; for (let i = 1; i <= argv.trips; i++) { promises.push(simulateTrip(i)); await new Promise(r => setTimeout(r, 50)); } await Promise.all(promises); const durationSec = ((Date.now() - startTime) / 1000).toFixed(2); const tps = metrics.successful > 0 ? (metrics.successful / durationSec).toFixed(2) : '0.00'; console.log(`\n======================================================`); console.log(`📊 STRESS TEST RESULTS`); console.log(`======================================================`); console.log(`⏱️ Duration: ${durationSec}s`); console.log(`⚡ Throughput: ${tps} trips/sec`); console.log(`✅ Successful: ${metrics.successful} / ${argv.trips}`); console.log(`❌ Failed: ${metrics.failed} / ${argv.trips}`); if (metrics.failed > 0) { console.log(` ├─ JWT Errors: ${metrics.jwtErrors}`); console.log(` ├─ Socket Errors: ${metrics.connectionErrors}`); console.log(` └─ API Errors: ${metrics.apiErrors}`); } console.log(`======================================================\n`); process.exit(0); } runTest();