-
Notifications
You must be signed in to change notification settings - Fork 8
Expand file tree
/
Copy pathtest-e2e.js
More file actions
executable file
·511 lines (415 loc) · 16.6 KB
/
test-e2e.js
File metadata and controls
executable file
·511 lines (415 loc) · 16.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
#!/usr/bin/env node
/**
* Comprehensive End-to-End Test Runner
*
* This test runner orchestrates all testing levels:
* 1. Unit Tests (TypeScript + Python)
* 2. Integration Tests (MCP Server + Python Bridge)
* 3. End-to-End Tests (Full UE + Demo Project)
* 4. Code Coverage (Combined reporting)
*/
const { spawn, execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
const os = require('os');
class E2ETestRunner {
constructor() {
this.config = {
demoProjectPath: path.join(__dirname, 'Demo'),
serverPath: path.join(__dirname, 'server'),
pythonBridge: 'http://localhost:8765',
mcpPort: process.env.MCP_PORT || 3000,
timeout: 30000, // 30 second timeout for UE operations
coverage: process.env.COVERAGE === 'true',
verbose: process.env.VERBOSE === 'true'
};
this.results = {
unit: { passed: 0, failed: 0, coverage: null },
integration: { passed: 0, failed: 0, coverage: null },
e2e: { passed: 0, failed: 0, coverage: null },
total: { passed: 0, failed: 0, duration: 0 }
};
}
log(message, level = 'info') {
const timestamp = new Date().toISOString();
const prefix = {
info: '🔍',
success: '✅',
error: '❌',
warning: '⚠️',
debug: '🐛'
}[level] || 'ℹ️';
console.log(`${prefix} [${timestamp}] ${message}`);
}
async runUnitTests() {
this.log('Running Unit Tests (TypeScript + Python)', 'info');
try {
// TypeScript unit tests
this.log('📦 Running TypeScript unit tests...');
const tsResult = execSync('npm test', {
cwd: this.config.serverPath,
stdio: this.config.verbose ? 'inherit' : 'pipe'
});
this.results.unit.passed += 1;
this.log('TypeScript unit tests passed', 'success');
return true;
} catch (error) {
this.results.unit.failed += 1;
this.log(`Unit tests failed: ${error.message}`, 'error');
return false;
}
}
async checkUEConnection() {
this.log('🔌 Checking Unreal Engine connection...');
try {
const response = await fetch(this.config.pythonBridge, {
method: 'GET',
timeout: 5000
});
if (response.ok) {
const status = await response.json();
this.log(`UE Connected: ${status.project || 'Unknown'} (${status.engine_version || 'Unknown'})`, 'success');
return true;
}
} catch (error) {
this.log('UE connection failed - will run tests in mock mode', 'warning');
return false;
}
return false;
}
async runIntegrationTests() {
this.log('Running Integration Tests (MCP + Python Bridge)', 'info');
const integrationTests = [
'test-connection.js',
'test-server.js',
'test-mcp-direct.js',
'test-python-proxy.js'
];
let passed = 0, failed = 0;
for (const test of integrationTests) {
const testPath = path.join(__dirname, 'tests', 'integration', test);
if (!fs.existsSync(testPath)) {
this.log(`Test file not found: ${test}`, 'warning');
continue;
}
try {
this.log(`Running ${test}...`);
execSync(`node "${testPath}"`, {
stdio: this.config.verbose ? 'inherit' : 'pipe',
timeout: this.config.timeout
});
passed++;
this.log(`${test} passed`, 'success');
} catch (error) {
failed++;
this.log(`${test} failed: ${error.message}`, 'error');
}
}
this.results.integration = { passed, failed };
return failed === 0;
}
async runE2ETests() {
this.log('Running End-to-End Tests (Full UE + Demo Project)', 'info');
const ueConnected = await this.checkUEConnection();
if (!ueConnected) {
this.log('Skipping E2E tests - UE not connected', 'warning');
this.results.e2e = { passed: 0, failed: 0, skipped: true };
return true; // Don't fail the overall test run
}
const e2eTests = [
'test-ue-live.js',
'test-socket-snapping.js',
'test-mcp-integration.js',
'test-comprehensive-mcp.js' // Complete MCP tool coverage test
];
let passed = 0, failed = 0;
for (const test of e2eTests) {
const testPath = path.join(__dirname, 'tests', 'integration', test);
if (!fs.existsSync(testPath)) {
this.log(`Test file not found: ${test}`, 'warning');
continue;
}
try {
this.log(`Running E2E test: ${test}...`);
execSync(`node "${testPath}"`, {
stdio: this.config.verbose ? 'inherit' : 'pipe',
timeout: this.config.timeout,
env: {
...process.env,
UE_PROJECT_PATH: this.config.demoProjectPath
}
});
passed++;
this.log(`${test} passed`, 'success');
} catch (error) {
failed++;
this.log(`${test} failed: ${error.message}`, 'error');
}
}
this.results.e2e = { passed, failed };
return failed === 0;
}
async generateCoverageReport() {
if (!this.config.coverage) return;
this.log('📊 Generating Combined Coverage Report...', 'info');
try {
// Generate TypeScript coverage
execSync('npm run test:coverage', {
cwd: this.config.serverPath,
stdio: 'pipe'
});
// TODO: Implement combined coverage reporting
// This would merge TypeScript + Python + Integration coverage
this.log('Coverage reports generated in coverage/', 'success');
} catch (error) {
this.log(`Coverage generation failed: ${error.message}`, 'error');
}
}
async cleanupTestActors() {
this.log('🧹 Cleaning up all test actors and folders...', 'info');
try {
const { MCPClient } = require('./tests/utils/mcp-client.js');
const client = new MCPClient();
// Get current level outliner to see folder structure
const outlinerResult = await client.callTool('level_outliner', {});
let totalDeleted = 0;
if (outlinerResult.success) {
this.log('Checking for Test folder structure...', 'info');
// Get all actors with higher limit to catch all test actors
const levelResult = await client.callTool('level_actors', { limit: 100 });
if (levelResult.success && levelResult.actors) {
// Find all actors in Test folder or with test names
const testActorPatterns = [
'Wall1', 'Wall2', 'Door1', // Legacy names
'Foundation', 'WallSegment_', // Comprehensive test actors
'BuildingWall', 'DoorFrame', 'CornerPiece', // Socket test assets
'Complex_Wall', 'Complex_Door', 'Complex_Window', 'Complex_Corner', // Complex socket test
'SocketActor' // Python-created socket test actors
];
const actorsToClean = levelResult.actors.filter(actor =>
testActorPatterns.some(pattern => actor.name.includes(pattern))
);
if (actorsToClean.length > 0) {
this.log(`Found ${actorsToClean.length} test actors to clean up`, 'info');
// Delete each actor
for (const actor of actorsToClean) {
try {
const deleteResult = await client.callTool('actor_delete', {
actorName: actor.name
});
if (deleteResult.success) {
totalDeleted++;
this.log(` ✓ Deleted ${actor.name}`);
} else {
this.log(` ✗ Failed to delete ${actor.name}`, 'warning');
}
} catch (error) {
this.log(` ✗ Error deleting ${actor.name}: ${error.message}`, 'warning');
}
}
this.log(`Successfully cleaned up ${totalDeleted}/${actorsToClean.length} test actors`, 'success');
// Also clean up the Test folder structure to ensure no World Outliner changes
this.log('Cleaning up Test folder structure...', 'info');
try {
const folderCleanupResult = await client.callTool('python_proxy', {
code: `
import unreal
# Get the world outliner subsystem
outliner_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
# Since UE doesn't have direct folder deletion API, we just ensure all actors are gone
# The folder structure will automatically clean up when empty
print("Folder cleanup: All test actors removed, empty folders will auto-cleanup")
result = {"success": True, "message": "Test folder structure cleaned"}
`
});
if (folderCleanupResult.success) {
this.log('✓ Test folder structure cleanup completed', 'success');
} else {
this.log('⚠️ Test folder cleanup had issues but actors are removed', 'warning');
}
} catch (error) {
this.log(`⚠️ Test folder cleanup error: ${error.message}`, 'warning');
}
} else {
this.log('No test actors found to clean up', 'success');
}
}
}
} catch (error) {
this.log(`Failed to clean test actors: ${error.message}`, 'warning');
}
}
async clearUELogs() {
this.log('🧹 Clearing UE logs for clean test baseline...', 'info');
try {
const logPath = path.join(os.homedir(), 'Library', 'Logs', 'Unreal Engine', 'DemoEditor', 'Demo.log');
if (fs.existsSync(logPath)) {
// Clear the log file by truncating it
fs.writeFileSync(logPath, '');
this.log('✅ UE log file cleared successfully', 'success');
} else {
this.log('⚠️ UE log file not found - will be created on first log entry', 'warning');
}
} catch (error) {
this.log(`⚠️ Failed to clear UE logs: ${error.message}`, 'warning');
}
}
async checkUELogs() {
this.log('🔍 Checking Unreal Engine logs for errors...', 'info');
// Constants for log reading configuration
const LOG_BUFFER_SIZE = 50000; // 50KB buffer for recent log content
try {
// Read UE log file directly (more reliable than going through MCP tools)
const logPath = path.join(os.homedir(), 'Library', 'Logs', 'Unreal Engine', 'DemoEditor', 'Demo.log');
let logContent = '';
if (fs.existsSync(logPath)) {
const logStats = fs.statSync(logPath);
const fileSize = logStats.size;
const readFromPos = Math.max(0, fileSize - LOG_BUFFER_SIZE);
let fd;
try {
fd = fs.openSync(logPath, 'r');
const buffer = Buffer.alloc(LOG_BUFFER_SIZE);
const bytesRead = fs.readSync(fd, buffer, 0, LOG_BUFFER_SIZE, readFromPos);
logContent = buffer.toString('utf8', 0, bytesRead);
} finally {
if (fd !== undefined) {
fs.closeSync(fd);
}
}
} else {
this.log('⚠️ UE log file not found at expected location', 'warning');
return;
}
const logLines = logContent.split('\n').filter(line => line.trim());
const errorLines = [];
let suppressedCount = 0;
const criticalPatterns = [
/Error:/i,
/Fatal error/i,
/Critical error/i,
/=== Critical error: ===/i,
/Assertion failed/i,
];
// Expected errors produced intentionally by error-handling test cases.
// Each pattern allows up to maxSuppress occurrences; further hits are treated
// as real errors so regressions producing the same messages still surface in CI.
const expectedTestErrorPatterns = [
{ regex: /LogPython: Error: UEMCP: actor operation failed: Socket .* not found/, maxSuppress: 5 },
{ regex: /LogPython: Error: UEMCP: actor operation failed: Actor .* not found in level/, maxSuppress: 3 },
];
const suppressionCounts = new Map();
// Scan for errors
lineLoop: for (const line of logLines) {
for (const { regex, maxSuppress } of expectedTestErrorPatterns) {
if (regex.test(line)) {
const key = regex.toString();
const count = suppressionCounts.get(key) || 0;
if (count < maxSuppress) {
suppressionCounts.set(key, count + 1);
suppressedCount++;
continue lineLoop;
}
// maxSuppress reached — fall through and treat as a real error
break;
}
}
for (const pattern of criticalPatterns) {
if (pattern.test(line)) {
errorLines.push(line);
break;
}
}
}
if (suppressedCount > 0) {
this.log(`Suppressed ${suppressedCount} expected error-handling test error(s) from UE log scan`, 'info');
}
if (errorLines.length > 0) {
this.log(`❌ Found ${errorLines.length} errors in UE logs:`, 'error');
// Show first 10 errors
errorLines.slice(0, 10).forEach(line => {
console.log(` ${line.trim()}`);
});
if (errorLines.length > 10) {
console.log(` ... and ${errorLines.length - 10} more errors`);
}
this.results.e2e.ueLogErrors = errorLines.length;
} else {
this.log('✅ No errors found in UE logs', 'success');
this.results.e2e.ueLogErrors = 0;
}
} catch (error) {
this.log(`⚠️ Failed to check UE logs: ${error.message}`, 'warning');
}
}
printResults() {
const { unit, integration, e2e, total } = this.results;
console.log('\n' + '='.repeat(60));
console.log('🧪 UEMCP E2E Test Results');
console.log('='.repeat(60));
console.log(`📦 Unit Tests: ${unit.passed} passed, ${unit.failed} failed`);
console.log(`🔗 Integration Tests: ${integration.passed} passed, ${integration.failed} failed`);
if (e2e.skipped) {
console.log(`🎮 E2E Tests: Skipped (UE not connected)`);
} else {
console.log(`🎮 E2E Tests: ${e2e.passed} passed, ${e2e.failed} failed`);
// Show UE log error summary if errors were found
if (e2e.ueLogErrors && e2e.ueLogErrors > 0) {
console.log(`📋 UE Log Errors: ${e2e.ueLogErrors} errors detected`);
}
}
const totalPassed = unit.passed + integration.passed + (e2e.passed || 0);
const totalFailed = unit.failed + integration.failed + (e2e.failed || 0);
console.log('\n' + '-'.repeat(40));
console.log(`🎯 TOTAL: ${totalPassed} passed, ${totalFailed} failed`);
// Consider UE log errors in overall assessment
const hasUEErrors = e2e.ueLogErrors && e2e.ueLogErrors > 0;
if (totalFailed === 0 && !hasUEErrors) {
console.log('\n🎉 All tests passed! UEMCP is working correctly.');
} else if (totalFailed === 0 && hasUEErrors) {
console.log('\n⚠️ Tests passed but UE log errors detected. Check UE console.');
} else if (hasUEErrors) {
console.log(`\n💥 ${totalFailed} test(s) failed + ${e2e.ueLogErrors} UE log errors. Check logs above.`);
} else {
console.log(`\n💥 ${totalFailed} test(s) failed. Check logs above.`);
}
console.log('='.repeat(60) + '\n');
return totalFailed === 0 && !hasUEErrors;
}
async run() {
const startTime = Date.now();
this.log('🚀 Starting UEMCP Comprehensive Test Suite', 'info');
this.log(`Demo Project: ${this.config.demoProjectPath}`);
this.log(`Coverage: ${this.config.coverage ? 'Enabled' : 'Disabled'}`);
// Clear UE logs for clean baseline
await this.clearUELogs();
// Run all test levels
await this.runUnitTests();
await this.runIntegrationTests();
await this.runE2ETests();
// Generate coverage if requested
if (this.config.coverage) {
await this.generateCoverageReport();
}
// Clean up all test actors before checking logs
await this.cleanupTestActors();
// Check UE logs for errors
await this.checkUELogs();
this.results.total.duration = Date.now() - startTime;
const success = this.printResults();
process.exit(success ? 0 : 1);
}
}
// CLI interface
if (require.main === module) {
const runner = new E2ETestRunner();
process.on('SIGINT', () => {
console.log('\n⚠️ Test run interrupted');
process.exit(1);
});
runner.run().catch(error => {
console.error('❌ Test runner failed:', error);
process.exit(1);
});
}
module.exports = E2ETestRunner;