#!/usr/bin/env node const { execSync } = require("child_process"); const fs = require("fs"); const path = require("path"); /** * 포괄적인 테스트 실행 스크립트 * CI/CD에서 사용하기 위한 테스트 스위트 */ const projectRoot = path.join(__dirname, ".."); // 테스트 설정 const testConfig = { unit: { command: "npm run test:run", name: "Unit Tests", required: true, }, type: { command: "npm run type-check", name: "Type Checking", required: true, }, lint: { command: "npm run lint", name: "ESLint", required: true, }, e2e: { command: "npx playwright test", name: "E2E Tests", required: false, }, build: { command: "npm run build:prod", name: "Build Test", required: true, }, }; // 명령행 인수 파싱 const args = process.argv.slice(2); const options = { failFast: args.includes("--fail-fast"), skipOptional: args.includes("--skip-optional"), environment: args.find((arg) => arg.startsWith("--env="))?.split("=")[1] || "test", coverage: args.includes("--coverage"), verbose: args.includes("--verbose"), }; console.log("🧪 Starting comprehensive test suite...\n"); console.log(`Environment: ${options.environment}`); console.log(`Options: ${JSON.stringify(options, null, 2)}\n`); const results = { passed: [], failed: [], skipped: [], startTime: Date.now(), }; // 테스트 실행 함수 function runTest(testName, config) { const startTime = Date.now(); console.log(`\n🔍 Running ${config.name}...`); if (!config.required && options.skipOptional) { console.log(`⏭️ Skipping optional test: ${config.name}`); results.skipped.push(testName); return true; } try { const output = execSync(config.command, { cwd: projectRoot, encoding: "utf8", stdio: options.verbose ? "inherit" : "pipe", env: { ...process.env, NODE_ENV: options.environment, CI: "true", }, }); const duration = Date.now() - startTime; console.log(`✅ ${config.name} passed (${duration}ms)`); if (options.verbose && output) { console.log("Output:", output.slice(-500)); // 마지막 500자만 출력 } results.passed.push({ name: testName, duration, config, }); return true; } catch (error) { const duration = Date.now() - startTime; console.log(`❌ ${config.name} failed (${duration}ms)`); console.log("Error:", error.message); if (error.stdout) { console.log("STDOUT:", error.stdout.slice(-1000)); } if (error.stderr) { console.log("STDERR:", error.stderr.slice(-1000)); } results.failed.push({ name: testName, duration, config, error: error.message, }); return false; } } // 메인 테스트 실행 async function runAllTests() { console.log("🚀 Test execution started...\n"); // 환경 설정 if (options.environment !== "test") { console.log(`🔧 Setting up ${options.environment} environment...`); try { execSync(`node scripts/build-env.cjs ${options.environment}`, { cwd: projectRoot, stdio: "inherit", }); } catch (error) { console.log("⚠️ Environment setup failed, continuing with defaults"); } } // 테스트 순서 정의 const testOrder = ["lint", "type", "unit", "build", "e2e"]; for (const testName of testOrder) { const config = testConfig[testName]; if (!config) continue; const success = runTest(testName, config); if (!success && config.required && options.failFast) { console.log("\n💥 Fail-fast mode: Stopping on first failure"); break; } } // 커버리지 실행 (요청된 경우) if (options.coverage && !results.failed.length) { console.log("\n📊 Running coverage analysis..."); try { execSync("npm run test:coverage", { cwd: projectRoot, stdio: "inherit", }); } catch (error) { console.log("⚠️ Coverage analysis failed"); } } // 결과 요약 printResults(); // 종료 코드 결정 const hasRequiredFailures = results.failed.some( (failure) => testConfig[failure.name]?.required ); if (hasRequiredFailures) { process.exit(1); } } // 결과 출력 function printResults() { const totalTime = Date.now() - results.startTime; console.log("\n" + "=".repeat(60)); console.log("📋 TEST RESULTS SUMMARY"); console.log("=".repeat(60)); console.log(`\n⏱️ Total execution time: ${totalTime}ms`); console.log(`✅ Passed: ${results.passed.length}`); console.log(`❌ Failed: ${results.failed.length}`); console.log(`⏭️ Skipped: ${results.skipped.length}`); if (results.passed.length > 0) { console.log("\n✅ PASSED TESTS:"); results.passed.forEach((test) => { console.log(` • ${test.config.name} (${test.duration}ms)`); }); } if (results.failed.length > 0) { console.log("\n❌ FAILED TESTS:"); results.failed.forEach((test) => { const required = test.config.required ? "[REQUIRED]" : "[OPTIONAL]"; console.log(` • ${test.config.name} ${required} (${test.duration}ms)`); console.log(` Error: ${test.error.slice(0, 100)}...`); }); } if (results.skipped.length > 0) { console.log("\n⏭️ SKIPPED TESTS:"); results.skipped.forEach((testName) => { console.log(` • ${testConfig[testName].name}`); }); } // 상태 결정 const hasRequiredFailures = results.failed.some( (failure) => testConfig[failure.name]?.required ); if (hasRequiredFailures) { console.log("\n🚨 OVERALL STATUS: FAILED"); console.log("Required tests failed. Build should not proceed."); } else if (results.failed.length > 0) { console.log("\n⚠️ OVERALL STATUS: PASSED WITH WARNINGS"); console.log("Optional tests failed, but required tests passed."); } else { console.log("\n🎉 OVERALL STATUS: PASSED"); console.log("All tests passed successfully!"); } // 결과를 JSON 파일로 저장 (CI에서 활용 가능) const reportPath = path.join(projectRoot, "test-results.json"); const report = { timestamp: new Date().toISOString(), environment: options.environment, options, results, summary: { passed: results.passed.length, failed: results.failed.length, skipped: results.skipped.length, totalTime, status: hasRequiredFailures ? "FAILED" : results.failed.length > 0 ? "WARNING" : "PASSED", }, }; fs.writeFileSync(reportPath, JSON.stringify(report, null, 2)); console.log(`\n📄 Detailed report saved to: test-results.json`); } // 스크립트 실행 runAllTests().catch((error) => { console.error("💥 Test runner crashed:", error); process.exit(1); });