isFile() && $file->getExtension() === 'php') { $phpFiles[] = $file->getPathname(); } } } // Also check root-level PHP files foreach (glob($ROOT . '/*.php') as $f) { $phpFiles[] = $f; } $syntaxErrors = []; foreach ($phpFiles as $file) { $output = []; $ret = 0; exec('php -l ' . escapeshellarg($file) . ' 2>&1', $output, $ret); if ($ret !== 0) { $relPath = str_replace($ROOT . '/', '', $file); $syntaxErrors[] = $relPath . ': ' . implode(' ', $output); } } if (empty($syntaxErrors)) { test_pass("PHP syntax check: all " . count($phpFiles) . " files pass"); } else { foreach ($syntaxErrors as $err) { test_fail("PHP syntax error", $err); } } // ============================================================ // 2. 路由完整性验证 // ============================================================ echo "=== 2. Route Integrity ===\n"; $routes = include $ROOT . '/routes.php'; $seenPaths = []; $routeIssues = 0; foreach ($routes as $idx => $route) { $method = $route[0]; $path = $route[1]; $action = $route[2]; // 检查重复路由 path (method+path) $key = $method . ':' . $path; if (isset($seenPaths[$key])) { test_fail("Route duplicate", "'{$key}' duplicated at line " . ($idx + 1)); $routeIssues++; } $seenPaths[$key] = true; // 解析 Controller@method if (strpos($action, '@') === false) { test_fail("Route format", "'{$path}' action '{$action}' missing @method"); $routeIssues++; continue; } list($controllerClass, $methodName) = explode('@', $action); // 转换类名到文件路径 $controllerFile = $ROOT . '/' . str_replace('\\', '/', ltrim($controllerClass, '\\')) . '.php'; if (!file_exists($controllerFile)) { test_fail("Route controller missing", "'{$path}' -> {$controllerClass} file not found: {$controllerFile}"); $routeIssues++; continue; } // 检查方法是否存在(通过正则搜索) $content = file_get_contents($controllerFile); if (!preg_match('/function\s+' . preg_quote($methodName) . '\s*\(/', $content)) { test_fail("Route method missing", "'{$path}' -> {$controllerClass}@{$methodName} method not found"); $routeIssues++; } } if ($routeIssues === 0) { test_pass("Route integrity: all " . count($routes) . " routes valid"); } // ============================================================ // 3. Service 层验证 // ============================================================ echo "=== 3. Service Layer ===\n"; // TransactionService 方法检查 $tsFile = $ROOT . '/App/Services/TransactionService.php'; if (!file_exists($tsFile)) { test_fail("TransactionService", "File not found"); } else { $tsContent = file_get_contents($tsFile); $tsMethods = ['transfer', 'fundRequest']; foreach ($tsMethods as $m) { if (preg_match('/function\s+' . $m . '\s*\(/', $tsContent)) { test_pass("TransactionService has {$m}() method"); } else { test_fail("TransactionService", "Missing method {$m}()"); } } } // ReportService 方法检查 $rsFile = $ROOT . '/App/Services/ReportService.php'; if (!file_exists($rsFile)) { test_fail("ReportService", "File not found"); } else { $rsContent = file_get_contents($rsFile); $rsMethods = ['getUserReport', 'getOverviewStats', 'getDailyStats', 'getUserWinLoss', 'getTopUsers', 'getAgentStats']; foreach ($rsMethods as $m) { if (preg_match('/function\s+' . $m . '\s*\(/', $rsContent)) { test_pass("ReportService has {$m}() method"); } else { test_fail("ReportService", "Missing method {$m}()"); } } } // Service 纯业务逻辑检查(不含 HTTP 操作) $serviceFiles = glob($ROOT . '/App/Services/*.php'); $forbiddenPatterns = [ 'header\s*\(' => 'header()', '\becho\b' => 'echo', '\$_GET' => '$_GET', '\$_POST' => '$_POST', '\$_SESSION' => '$_SESSION', ]; foreach ($serviceFiles as $sf) { $sfContent = file_get_contents($sf); $sfName = basename($sf); $sfClean = true; foreach ($forbiddenPatterns as $pattern => $label) { if (preg_match('/' . $pattern . '/', $sfContent)) { test_fail("Service purity: {$sfName}", "Contains forbidden pattern: {$label}"); $sfClean = false; } } if ($sfClean) { test_pass("Service purity: {$sfName} is clean (no HTTP operations)"); } } // ============================================================ // 4. Controller → Service 调用链验证 // ============================================================ echo "=== 4. Controller-Service Call Chain ===\n"; // TransactionController must call TransactionService $tcFile = $ROOT . '/App/Controllers/Web/TransactionController.php'; if (file_exists($tcFile)) { $tcContent = file_get_contents($tcFile); // fundRequest calls TransactionService if (preg_match('/TransactionService/', $tcContent) && preg_match('/function\s+fundRequest/', $tcContent)) { test_pass("TransactionController::fundRequest calls TransactionService"); } else { test_fail("TransactionController", "fundRequest does not call TransactionService"); } // transfer calls TransactionService if (preg_match('/TransactionService/', $tcContent) && preg_match('/function\s+transfer/', $tcContent)) { test_pass("TransactionController::transfer calls TransactionService"); } else { test_fail("TransactionController", "transfer does not call TransactionService"); } } else { test_fail("TransactionController", "File not found"); } // ReportWebController must call ReportService $rwcFile = $ROOT . '/App/Controllers/Web/ReportWebController.php'; if (file_exists($rwcFile)) { $rwcContent = file_get_contents($rwcFile); if (preg_match('/ReportService/', $rwcContent) && preg_match('/function\s+userReportApi/', $rwcContent)) { test_pass("ReportWebController::userReportApi calls ReportService"); } else { test_fail("ReportWebController", "userReportApi does not call ReportService"); } } else { test_fail("ReportWebController", "File not found"); } // Admin\ReportController must call ReportService $arcFile = $ROOT . '/App/Controllers/Admin/ReportController.php'; if (file_exists($arcFile)) { $arcContent = file_get_contents($arcFile); $arcOk = true; if (!preg_match('/ReportService/', $arcContent)) { test_fail("Admin\\ReportController", "Does not reference ReportService"); $arcOk = false; } if (preg_match('/function\s+index/', $arcContent) && preg_match('/ReportService/', $arcContent)) { test_pass("Admin\\ReportController::index calls ReportService"); } else if ($arcOk) { test_fail("Admin\\ReportController", "index does not call ReportService"); } if (preg_match('/function\s+export/', $arcContent) && preg_match('/ReportService/', $arcContent)) { test_pass("Admin\\ReportController::export calls ReportService"); } else if ($arcOk) { test_fail("Admin\\ReportController", "export does not call ReportService"); } } else { test_fail("Admin\\ReportController", "File not found"); } // HomeController must NOT contain old methods $hcFile = $ROOT . '/App/Controllers/Web/HomeController.php'; if (file_exists($hcFile)) { $hcContent = file_get_contents($hcFile); $removedMethods = ['fundRequest', 'transfer', 'report', 'followPlan', 'userReportApi']; $hcClean = true; foreach ($removedMethods as $rm) { if (preg_match('/function\s+' . $rm . '\s*\(/', $hcContent)) { test_fail("HomeController cleanup", "Still contains method {$rm}()"); $hcClean = false; } } if ($hcClean) { test_pass("HomeController no longer contains removed methods (fundRequest/transfer/report/followPlan/userReportApi)"); } } else { test_fail("HomeController", "File not found"); } // ============================================================ // 5. JS 文件验证 // ============================================================ echo "=== 5. JS File Validation ===\n"; $jsFiles = ['pk10-sound.js', 'pk10-race.js', 'pk10-bet.js', 'pk10-poll.js']; foreach ($jsFiles as $jsf) { $jsPath = $ROOT . '/Static/js/' . $jsf; if (!file_exists($jsPath)) { test_fail("JS file exists: {$jsf}", "File not found"); continue; } $jsSize = filesize($jsPath); if ($jsSize === 0) { test_fail("JS file non-empty: {$jsf}", "File is empty"); continue; } test_pass("JS file exists and non-empty: {$jsf} ({$jsSize} bytes)"); // No PHP tags $jsContent = file_get_contents($jsPath); if (preg_match('/<\?php|<\?=/', $jsContent)) { test_fail("JS no PHP tags: {$jsf}", "Contains PHP tags"); } else { test_pass("JS no PHP tags: {$jsf}"); } } // pk10.php checks $pk10File = $ROOT . '/App/Views/Web/pk10.php'; if (file_exists($pk10File)) { $pk10Content = file_get_contents($pk10File); // window.PK10_CONFIG if (strpos($pk10Content, 'PK10_CONFIG') !== false) { test_pass("pk10.php contains PK10_CONFIG configuration block"); } else { test_fail("pk10.php PK10_CONFIG", "Missing window.PK10_CONFIG block"); } // 4 script src references $scriptCount = 0; foreach ($jsFiles as $jsf) { if (strpos($pk10Content, $jsf) !== false) { $scriptCount++; } } if ($scriptCount === 4) { test_pass("pk10.php includes all 4 pk10-*.js script references"); } else { test_fail("pk10.php script references", "Found {$scriptCount}/4 JS file references"); } // JS 引用顺序: sound → race → bet → poll $positions = []; foreach ($jsFiles as $jsf) { $pos = strpos($pk10Content, $jsf); if ($pos !== false) { $positions[$jsf] = $pos; } } if (count($positions) === 4) { $ordered = array_keys($positions); usort($ordered, function($a, $b) use ($positions) { return $positions[$a] - $positions[$b]; }); $expected = ['pk10-sound.js', 'pk10-race.js', 'pk10-bet.js', 'pk10-poll.js']; if ($ordered === $expected) { test_pass("pk10.php JS include order correct: sound -> race -> bet -> poll"); } else { test_fail("pk10.php JS order", "Expected: " . implode(' -> ', $expected) . " Got: " . implode(' -> ', $ordered)); } } } else { test_fail("pk10.php", "File not found"); } // ============================================================ // 6. JS 全局变量/函数完整性 // ============================================================ echo "=== 6. JS Global Function Integrity ===\n"; $jsSoundContent = file_exists($ROOT . '/Static/js/pk10-sound.js') ? file_get_contents($ROOT . '/Static/js/pk10-sound.js') : ''; $jsRaceContent = file_exists($ROOT . '/Static/js/pk10-race.js') ? file_get_contents($ROOT . '/Static/js/pk10-race.js') : ''; $jsBetContent = file_exists($ROOT . '/Static/js/pk10-bet.js') ? file_get_contents($ROOT . '/Static/js/pk10-bet.js') : ''; $jsPollContent = file_exists($ROOT . '/Static/js/pk10-poll.js') ? file_get_contents($ROOT . '/Static/js/pk10-poll.js') : ''; // SoundFX in pk10-sound.js if (preg_match('/\bSoundFX\b/', $jsSoundContent)) { test_pass("SoundFX defined in pk10-sound.js"); } else { test_fail("JS global: SoundFX", "Not found in pk10-sound.js"); } // Race functions in pk10-race.js $raceFuncs = ['animateRace', 'startIdleAnimation', 'resetRace', 'updateSceneHeader']; foreach ($raceFuncs as $rf) { if (preg_match('/\b' . $rf . '\b/', $jsRaceContent)) { test_pass("{$rf} defined in pk10-race.js"); } else { test_fail("JS global: {$rf}", "Not found in pk10-race.js"); } } // Bet functions in pk10-bet.js $betFuncs = ['renderMyBets', 'renderLastMyBets', 'formatBetLabel']; foreach ($betFuncs as $bf) { if (preg_match('/\b' . $bf . '\b/', $jsBetContent)) { test_pass("{$bf} defined in pk10-bet.js"); } else { test_fail("JS global: {$bf}", "Not found in pk10-bet.js"); } } // pollPeriod in pk10-poll.js if (preg_match('/\bpollPeriod\b/', $jsPollContent)) { test_pass("pollPeriod defined in pk10-poll.js"); } else { test_fail("JS global: pollPeriod", "Not found in pk10-poll.js"); } // ============================================================ // 7. View 文件验证 // ============================================================ echo "=== 7. View File Validation ===\n"; // report.php $reportView = $ROOT . '/App/Views/Web/report.php'; if (file_exists($reportView)) { $rvContent = file_get_contents($reportView); test_pass("report.php exists"); if (strpos($rvContent, '/api/user-report') !== false) { test_pass("report.php contains /api/user-report fetch call"); } else { test_fail("report.php content", "Missing /api/user-report fetch call"); } } else { test_fail("report.php", "File not found"); } // follow_plan.php $fpView = $ROOT . '/App/Views/Web/follow_plan.php'; if (file_exists($fpView)) { $fpContent = file_get_contents($fpView); test_pass("follow_plan.php exists"); if (strpos($fpContent, 'PK10_CONFIG') !== false || strpos($fpContent, 'pk10') !== false || strpos($fpContent, 'follow') !== false) { test_pass("follow_plan.php contains expected content"); } else { test_fail("follow_plan.php content", "Missing expected PK10_CONFIG or related content"); } } else { test_fail("follow_plan.php", "File not found"); } // profile.php $profileView = $ROOT . '/App/Views/Web/profile.php'; if (file_exists($profileView)) { $pvContent = file_get_contents($profileView); $profileOk = true; if (strpos($pvContent, '/report') !== false) { test_pass("profile.php contains /report link"); } else { test_fail("profile.php link", "Missing /report link"); $profileOk = false; } if (strpos($pvContent, '/follow-plan') !== false || strpos($pvContent, 'follow_plan') !== false || strpos($pvContent, 'follow-plan') !== false) { test_pass("profile.php contains /follow-plan link"); } else { test_fail("profile.php link", "Missing /follow-plan link"); } } else { test_fail("profile.php", "File not found"); } // pk10.php localLockCountdown if (file_exists($pk10File)) { $pk10Content = $pk10Content ?? file_get_contents($pk10File); if (strpos($pk10Content, 'localLockCountdown') !== false) { test_pass("pk10.php contains localLockCountdown in statusBadge code"); } else { test_fail("pk10.php localLockCountdown", "Missing localLockCountdown reference"); } } else { test_fail("pk10.php", "File not found"); } // ============================================================ // 8. i18n 验证 // ============================================================ echo "=== 8. i18n Validation ===\n"; $zhFile = $ROOT . '/Lang/zh.php'; $enFile = $ROOT . '/Lang/en.php'; if (!file_exists($zhFile) || !file_exists($enFile)) { test_fail("i18n files", "zh.php or en.php not found"); } else { $zhLang = include $zhFile; $enLang = include $enFile; $requiredKeys = ['report_query', 'follow_plan', 'today', 'yesterday']; foreach ($requiredKeys as $rk) { if (isset($zhLang[$rk])) { test_pass("zh.php has key '{$rk}'"); } else { test_fail("zh.php i18n", "Missing key '{$rk}'"); } if (isset($enLang[$rk])) { test_pass("en.php has key '{$rk}'"); } else { test_fail("en.php i18n", "Missing key '{$rk}'"); } } // Key count match $zhCount = count($zhLang); $enCount = count($enLang); if ($zhCount === $enCount) { test_pass("i18n key count match: zh={$zhCount}, en={$enCount}"); } else { $diff = abs($zhCount - $enCount); // Find missing keys $missingInEn = array_diff(array_keys($zhLang), array_keys($enLang)); $missingInZh = array_diff(array_keys($enLang), array_keys($zhLang)); $detail = "zh={$zhCount}, en={$enCount} (diff={$diff})"; if (!empty($missingInEn)) { $detail .= " | Missing in en: " . implode(', ', array_slice($missingInEn, 0, 10)); } if (!empty($missingInZh)) { $detail .= " | Missing in zh: " . implode(', ', array_slice($missingInZh, 0, 10)); } test_fail("i18n key count mismatch", $detail); } } // ============================================================ // 9. 路由无死链验证 // ============================================================ echo "=== 9. Route Dead Link Check ===\n"; $deadLinks = 0; foreach ($routes as $route) { $action = $route[2]; if (strpos($action, '@') === false) continue; list($controllerClass, $methodName) = explode('@', $action); $controllerFile = $ROOT . '/' . str_replace('\\', '/', ltrim($controllerClass, '\\')) . '.php'; if (!file_exists($controllerFile)) { // Already reported in section 2 $deadLinks++; continue; } $content = file_get_contents($controllerFile); if (!preg_match('/function\s+' . preg_quote($methodName) . '\s*\(/', $content)) { // Already reported in section 2 $deadLinks++; } } if ($deadLinks === 0) { test_pass("No dead route links: all " . count($routes) . " routes point to existing methods"); } else { test_fail("Dead route links", "{$deadLinks} routes point to non-existent controllers/methods (see section 2 for details)"); } // ============================================================ // 汇总输出 // ============================================================ echo "\n" . str_repeat('=', 60) . "\n"; echo "TEST RESULTS\n"; echo str_repeat('=', 60) . "\n\n"; foreach ($results as $r) { echo $r . "\n"; } $total = $pass + $fail; echo "\n" . str_repeat('-', 60) . "\n"; echo "Total: {$total} tests | Pass: {$pass} | Fail: {$fail}\n"; echo str_repeat('-', 60) . "\n"; if ($fail > 0) { echo "\n*** {$fail} FAILURE(S) DETECTED ***\n"; exit(1); } else { echo "\n*** ALL TESTS PASSED ***\n"; exit(0); }