Files
CLOAK/tools/regression_test.php
T

337 lines
18 KiB
PHP
Raw Normal View History

2026-06-14 14:00:24 +08:00
#!/usr/bin/env php
<?php
/**
* Cloaka 完整回归 + PHP 8 兼容性检测
*
* 用法:php tools/regression_test.php [--verbose]
*/
if (PHP_SAPI !== 'cli') {
fwrite(STDERR, "请在 CLI 下运行:php tools/regression_test.php\n");
exit(1);
}
$root = dirname(__DIR__);
$verbose = in_array('--verbose', $argv, true) || in_array('-v', $argv, true);
require_once $root . '/lib/CloakPhpCompat.php';
$passed = 0;
$failed = 0;
$skipped = 0;
function reg_assert($name, $condition, $detail = '')
{
global $passed, $failed, $verbose;
if ($condition) {
$passed++;
if ($verbose) {
echo "{$name}\n";
}
return true;
}
$failed++;
echo " ✗ FAIL: {$name}\n";
if ($detail !== '') {
echo " {$detail}\n";
}
return false;
}
function reg_skip($name, $reason)
{
global $skipped, $verbose;
$skipped++;
if ($verbose) {
echo " ○ SKIP: {$name}{$reason}\n";
}
}
echo "=== Cloaka 回归测试 ===\n";
echo 'PHP ' . PHP_VERSION . ' (' . PHP_SAPI . ")\n\n";
// ── 1. PHP 8 环境 ─────────────────────────────────────────────
echo "[1] PHP 8 兼容性\n";
$compat = CloakPhpCompat::buildReport();
reg_assert('PHP 版本 >= ' . CloakPhpCompat::MIN_PHP_VERSION, $compat['ok'], implode('; ', $compat['errors']));
foreach ($compat['warnings'] as $w) {
if ($verbose) {
echo " ! WARN: {$w}\n";
}
}
// ── 2. 语法检查 ───────────────────────────────────────────────
echo "[2] 全项目 php -l\n";
$lint = CloakPhpCompat::lintProject($root);
reg_assert('所有 PHP 文件语法正确', $lint['ok'], implode("\n ", array_slice($lint['failures'], 0, 5)));
// ── 3. Bootstrap 链 ───────────────────────────────────────────
echo "[3] 引导与配置\n";
require_once $root . '/cong.php';
require_once $root . '/config/ConfigLoader.php';
require_once $root . '/lib/bootstrap.php';
ConfigLoader::loadByName('index', $root);
reg_assert('ConfigLoader 可加载 index 配置', defined('SHOW_SITE_MODE_SWITCH'));
reg_assert('bootstrap 已加载 CheckPipeline', class_exists('CheckPipeline', false));
// ── 4. ConfigWriter 输出 ──────────────────────────────────────
echo "[4] 后台 ConfigWriter\n";
require_once $root . '/admin/ConfigWriter.php';
$sample = ConfigWriter::buildDefineSource('index.php', [
'methods' => 'ip_check', 'blacklist_group_id' => 0, 'whitelist_group_id' => 0,
'country' => 'US', 'show_content' => '302', 'DB_fp' => ['https://x.com/'],
'costm_ip_score' => 'test', 'auto_black' => 'OFF', 'is_virtual' => '0',
'auto_black_times' => '5', 'white_params' => '', 'iphone_model' => 0,
'write_log' => 0, 'zh_on' => 'OFF', 'os_on' => 'OFF', 'keep_params' => 'OFF',
'redirect_method' => '302', 'DB_zp' => 'https://z.com', 'mobile_on' => 'OFF',
'v_referer' => 'OFF', 'risk_number' => '0', 'risk_enhanced' => 'OFF', 'url_args_timeout' => '0',
'debug_mode' => 'OFF',
'ad_fb' => 'OFF', 'ad_google' => 'OFF', 'ad_tiktok' => 'OFF', 'ad_strict' => 'OFF', 'ad_spm_id' => '',
]);
$tmpCfg = sys_get_temp_dir() . '/cloak_reg_cfg_' . getmypid() . '.php';
file_put_contents($tmpCfg, $sample);
exec(escapeshellarg(PHP_BINARY) . ' -l ' . escapeshellarg($tmpCfg) . ' 2>&1', $lintOut, $lintCode);
@unlink($tmpCfg);
reg_assert('ConfigWriter 生成合法 PHP', $lintCode === 0, implode(' ', $lintOut));
reg_assert('ConfigWriter 含 deprecated SHOW_SITE_MOBILE', strpos($sample, 'SHOW_SITE_MOBILE') !== false);
reg_assert('ConfigWriter 含 CLOAK_RISK_ENHANCED', strpos($sample, 'CLOAK_RISK_ENHANCED') !== false);
reg_assert('ConfigWriter 含 CLOAK_AD_FB', strpos($sample, 'CLOAK_AD_FB') !== false);
reg_assert('ConfigWriter 含 CLOAK_AD_SPM_ID', strpos($sample, 'CLOAK_AD_SPM_ID') !== false);
$adGuardOut = shell_exec(escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg($root . '/tools/verify_ad_source_guard.php') . ' 2>/dev/null');
$adGuardJson = json_decode((string) $adGuardOut, true);
reg_assert('广告来源 Guard 验证', ($adGuardJson['ok'] ?? false) === true, (string) $adGuardOut);
$timerOut = shell_exec(escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg($root . '/tools/verify_pipeline_timer.php') . ' 2>/dev/null');
$timerJson = json_decode((string) $timerOut, true);
reg_assert('PipelineTimer 验证', ($timerJson['ok'] ?? false) === true, (string) $timerOut);
$realVisitorOut = shell_exec(escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg($root . '/tools/verify_session_real_visitor.php') . ' 2>/dev/null');
$realVisitorJson = json_decode((string) $realVisitorOut, true);
reg_assert('真实访客 Session 缓存', ($realVisitorJson['ok'] ?? false) === true, (string) $realVisitorOut);
$reqUrlOut = shell_exec(escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg($root . '/tools/verify_request_url.php') . ' 2>/dev/null');
$reqUrlJson = json_decode((string) $reqUrlOut, true);
reg_assert('请求 URL 辅助函数', ($reqUrlJson['ok'] ?? false) === true, (string) $reqUrlOut);
// ── 5. CheckPipeline 三种模式 ─────────────────────────────────
echo "[5] CheckPipeline 模式\n";
function run_pipeline_subprocess($root, $fixtureName)
{
$fixture = $root . '/tools/fixtures/' . $fixtureName;
$cmd = escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg($root . '/tools/run_pipeline_fixture.php')
. ' ' . escapeshellarg($fixture) . ' 2>&1';
exec($cmd, $out, $code);
$json = json_decode(implode("\n", $out), true);
return is_array($json) ? $json : ['error' => implode("\n", $out), 'code' => $code];
}
$ipResult = run_pipeline_subprocess($root, 'regression_ip_check.php');
reg_assert('ip_check + fbclid 产生 check_result', ($ipResult['result'] ?? null) !== null, json_encode($ipResult, JSON_UNESCAPED_UNICODE));
reg_assert('ip_check 无意外 HTML 输出', (int) ($ipResult['output_len'] ?? -1) === 0);
$fpResult = run_pipeline_subprocess($root, 'regression_fp.php');
reg_assert('fp 模式配置生效', ($fpResult['mode'] ?? '') === 'fp', json_encode($fpResult, JSON_UNESCAPED_UNICODE));
$zpResult = run_pipeline_subprocess($root, 'regression_zp.php');
reg_assert('zp 模式配置生效', ($zpResult['mode'] ?? '') === 'zp', json_encode($zpResult, JSON_UNESCAPED_UNICODE));
// ── 7. 指纹跳转模板 ───────────────────────────────────────────
echo "[7] 指纹跳转模板\n";
require_once $root . '/lib/Cloak/Page/FingerprintRedirectRenderer.php';
$tpl = $root . '/resources/fingerprint_redirect.html.php';
reg_assert('指纹模板文件存在', is_readable($tpl));
$tplBody = file_get_contents($tpl);
reg_assert('模板含 CONFIG_NAME 占位符', strpos($tplBody, '{{CONFIG_NAME}}') !== false);
reg_assert('模板 time 参数存在时停止重定向', strpos($tplBody, "urlParams.has('time')") !== false && strpos($tplBody, 'return;') !== false);
reg_assert('模板在加载 dist 前注册回调', strpos($tplBody, '__cloakFingerprintDone') !== false && strpos($tplBody, '<script src="/dist/?c={{CONFIG_NAME}}"></script>') !== false);
require_once $root . '/lib/CloakHttpHelpers.php';
$_GET = ['time' => '1234567890'];
reg_assert('cloak_fingerprint_time_param_present', cloak_fingerprint_time_param_present());
$_GET = [];
$_COOKIE = ['ctime' => '1', 'cyyek' => 'x', 'cl' => 'oklot'];
reg_assert('cloak_fingerprint_cookies_ready', cloak_fingerprint_cookies_ready());
$_SERVER['HTTP_HOST'] = 'shop.example.com';
reg_assert('cloak_fingerprint_cookie_domain', cloak_fingerprint_cookie_domain() === '.example.com');
$_GET = ['c' => 'index'];
ob_start();
include $root . '/dist/index.php';
$distJs = ob_get_clean();
reg_assert('dist/index.php 输出 Cookies.set', strpos($distJs, "fpCookies.set('ctime'") !== false);
reg_assert('dist/index.php 输出指纹完成回调', strpos($distJs, '__cloakFingerprintDone') !== false);
reg_assert('dist/index.php 无 PHP Fatal', stripos($distJs, 'Fatal error') === false);
$virtualDetectOut = shell_exec(escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg($root . '/tools/verify_virtual_detect.php') . ' 2>/dev/null');
$virtualDetectJson = json_decode((string) $virtualDetectOut, true);
reg_assert('虚拟设备检测规则引擎', ($virtualDetectJson['ok'] ?? false) === true, (string) $virtualDetectOut);
$_COOKIE['mmr'] = '18';
reg_assert('mmr 18 映射', cloak_fingerprint_mmr_reason() === '移动端无有效加速度计/陀螺仪');
unset($_COOKIE['mmr']);
// ── 8. f_check JSON 契约(405) ──────────────────────────────
echo "[8] f_check API 边界\n";
$fcheckCmd = escapeshellarg(PHP_BINARY) . ' -r ' . escapeshellarg(
'$_SERVER["REQUEST_METHOD"]="GET"; include ' . var_export($root . '/f_check.php', true) . ';'
) . ' 2>/dev/null';
$fcheckOut = shell_exec($fcheckCmd);
$fcJson = json_decode((string) $fcheckOut, true);
reg_assert('f_check 非 POST 返回 JSON status=error', is_array($fcJson) && ($fcJson['status'] ?? '') === 'error', (string) $fcheckOut);
$fcheckBadCmd = escapeshellarg(PHP_BINARY) . ' -r ' . escapeshellarg(
'$_SERVER["REQUEST_METHOD"]="POST"; $_POST=[]; include ' . var_export($root . '/f_check.php', true) . ';'
) . ' 2>/dev/null';
$fcheckBadOut = shell_exec($fcheckBadCmd);
$fcBadJson = json_decode((string) $fcheckBadOut, true);
reg_assert('f_check 无效 hdata 返回 JSON status=error', is_array($fcBadJson) && ($fcBadJson['status'] ?? '') === 'error', (string) $fcheckBadOut);
// ── 11. 二次风控 cloakjs / RouteResolver ────────────────────────
echo "[11] 二次风控 (cloakjs / RouteResolver)\n";
$riskCmd = escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg($root . '/tools/run_risk_route_fixture.php') . ' 2>&1';
exec($riskCmd, $riskOut, $riskCode);
$riskResult = json_decode(implode("\n", $riskOut), true);
reg_assert(
'RouteResolver 风险系数>0 输出二次风控页',
($riskResult['risk_page'] ?? false) === true && ($riskResult['has_fatal'] ?? true) === false,
json_encode($riskResult, JSON_UNESCAPED_UNICODE)
);
reg_assert('二次风控页嵌入访客 IP', ($riskResult['has_ip'] ?? false) === true, json_encode($riskResult, JSON_UNESCAPED_UNICODE));
reg_assert('二次风控页嵌入 log_id', ($riskResult['has_log_id'] ?? false) === true, json_encode($riskResult, JSON_UNESCAPED_UNICODE));
reg_assert('二次风控页含 f_check.php AJAX', ($riskResult['has_f_check'] ?? false) === true, json_encode($riskResult, JSON_UNESCAPED_UNICODE));
reg_assert('visit_to_3 缺省时自动初始化', ($riskResult['visit_to_3_was_unset'] ?? false) && ($riskResult['visit_to_3_init'] ?? '') === '1', json_encode($riskResult, JSON_UNESCAPED_UNICODE));
reg_assert('二次风控首次写库为 wait 非 true', ($riskResult['log_result'] ?? '') === 'wait', json_encode($riskResult, JSON_UNESCAPED_UNICODE));
reg_assert('cloakjs WebGL extension 空值防护', ($riskResult['webgl_null_guard'] ?? false) === true, json_encode($riskResult, JSON_UNESCAPED_UNICODE));
$cloakJsModeOut = shell_exec(escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg($root . '/tools/verify_cloakjs_mode.php') . ' 2>/dev/null');
$cloakJsModeJson = json_decode((string) $cloakJsModeOut, true);
reg_assert('cloakjs 基础/加强模式输出', ($cloakJsModeJson['ok'] ?? false) === true, (string) $cloakJsModeOut);
$v_infoStub = new stdClass();
$v_infoStub->v_ip = '198.51.100.7';
$v_infoStub->v_country = 'CA';
$log_idStub = 778899;
ob_start();
extract(['v_info' => $v_infoStub, 'log_id' => $log_idStub], EXTR_SKIP);
include $root . '/cloakjs.php';
$cloakJsOut = ob_get_clean();
reg_assert('cloakjs 独立渲染含 IP', strpos($cloakJsOut, '198.51.100.7') !== false);
reg_assert('cloakjs 独立渲染含 log_id', strpos($cloakJsOut, '778899') !== false);
reg_assert('cloakjs 默认模式不含 WebGPU 采集', strpos($cloakJsOut, 'getWebGPUFingerprint') === false);
reg_assert('cloakjs 独立渲染无 PHP Fatal', stripos($cloakJsOut, 'Fatal error') === false);
$ntVerifyCmd = escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg($root . '/tools/verify_fcheck_nt.php') . ' 2>/dev/null';
$ntVerifyOut = shell_exec($ntVerifyCmd);
$ntVerifyJson = json_decode((string) $ntVerifyOut, true);
reg_assert('f_check nt.txt 畸形行跳过且 fp 索引合法', ($ntVerifyJson['ok'] ?? false) === true, (string) $ntVerifyOut);
// ── 12. ConfigLoader / 宝塔部署文件 ───────────────────────────
echo "[12] ConfigLoader 与宝塔部署\n";
require_once $root . '/config/ConfigLoader.php';
reg_assert('ConfigLoader::sanitizeConfigName', ConfigLoader::sanitizeConfigName('ab.php') === 'ab');
reg_assert('ConfigLoader::sanitizeConfigName 非法回退 index', ConfigLoader::sanitizeConfigName('../x') === 'index');
$cfgLoaderOut = shell_exec(escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg($root . '/tools/verify_config_loader.php') . ' 2>/dev/null');
$cfgLoaderJson = json_decode((string) $cfgLoaderOut, true);
reg_assert('DomainRepository 规范化', ($cfgLoaderJson['tests']['normalize_strips_www'] ?? false) === true, (string) $cfgLoaderOut);
$btNginxOut = shell_exec(escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg($root . '/tools/verify_baota_nginx.php') . ' 2>/dev/null');
$btNginxJson = json_decode((string) $btNginxOut, true);
reg_assert('宝塔 deploy 模板文件齐全', ($btNginxJson['ok'] ?? false) === true, (string) $btNginxOut);
$cfDomainOut = shell_exec(escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg($root . '/tools/verify_cloudflare_domain.php') . ' 2>/dev/null');
$cfDomainJson = json_decode((string) $cfDomainOut, true);
reg_assert('Cloudflare 域名自动化 Mock 流转', ($cfDomainJson['ok'] ?? false) === true, (string) $cfDomainOut);
// ── 9. 数据库连通(可选) ─────────────────────────────────────
echo "[9] 数据库\n";
if (defined('DB_NAME')) {
$conn = @new mysqli('localhost', DB_USERNAME, DB_PASSWORD, DB_NAME);
if ($conn->connect_error) {
reg_skip('MySQL 连接', $conn->connect_error);
} else {
reg_assert('MySQL 连接', true);
require_once $root . '/lib/Cloak/VisitorLogWorker.php';
$prev = error_reporting(E_ALL);
$GLOBALS['__cloak_dbg_run'] = 'regression-db-test';
$asyncOn = defined('VISITOR_LOG_ASYNC') && strtoupper(VISITOR_LOG_ASYNC) === 'ON';
$sampleLogs = [
'campagin_id' => 'reg_test',
'visit_md5_code' => 'reg_async_' . substr(md5((string) microtime(true)), 0, 16),
'site_name' => 'reg.test',
'customers_ip' => '127.0.0.1',
'country' => null,
'result' => 'false',
'reason' => 'regression',
'v_referer' => null,
'Client' => 'PC',
'v_Browser' => null,
'v_PageURL' => '/',
'accept_language' => null,
'fp_url' => null,
'device' => null,
];
$logId = @write_log_db($sampleLogs);
if ($asyncOn) {
VisitorLogWorker::processBatch(20);
if ($logId === null) {
$md5Esc = $conn->real_escape_string($sampleLogs['visit_md5_code']);
$res = $conn->query("SELECT `id` FROM `visitor_logs` WHERE `visit_md5_code`='{$md5Esc}' ORDER BY `id` DESC LIMIT 1");
$row = $res ? $res->fetch_assoc() : null;
$logId = $row ? (int) $row['id'] : null;
}
reg_assert('write_log_db 异步模式 worker 落库', $logId !== null && (int) $logId > 0);
} else {
reg_assert('write_log_db 含 null 字段可正常执行', $logId !== null && (int) $logId > 0);
}
$waitId = @write_log_db(array_merge($sampleLogs, [
'visit_md5_code' => 'reg_wait_' . substr(md5((string) microtime(true)), 0, 16),
'result' => 'wait',
'reason' => '二次风控检测中',
'fp_url' => '',
]));
reg_assert('write_log_db wait 路径返回 log_id', $waitId !== null && (int) $waitId > 0);
if ($asyncOn) {
VisitorLogWorker::processBatch(20);
}
error_reporting($prev);
if ($logId) {
$conn->query('DELETE FROM `visitor_logs` WHERE `id` = ' . (int) $logId);
}
if ($waitId) {
$conn->query('DELETE FROM `visitor_logs` WHERE `id` = ' . (int) $waitId);
}
$conn->close();
}
} else {
reg_skip('MySQL 连接', 'cong.php 未定义 DB_NAME');
}
// ── 10. DEBUG 组件 ────────────────────────────────────────────
echo "[10] DEBUG 组件\n";
reg_assert('CloakDebugPage 可初始化', class_exists('CloakDebugPage', false));
CloakDebugPage::init();
CloakDebugPage::stepAt(__LINE__, 'regression', 'info', 'ok');
reg_assert('CloakDebugPage::stepAt 可调用', true);
// ── 汇总 ──────────────────────────────────────────────────────
echo "\n=== 汇总 ===\n";
echo "通过: {$passed} 失败: {$failed} 跳过: {$skipped}\n";
if ($failed > 0) {
echo "\n回归未通过,请修复上述失败项。\n";
exit(1);
}
echo "\n全部回归通过。\n";
exit(0);