zentaoRoot = $zentaoRoot; $this->traceFile = ''; $this->backtrace = debug_backtrace()[count(debug_backtrace())-1]; $this->runFile = $this->backtrace['file']; $this->moduleName = basename(dirname(dirname(dirname($this->runFile)))); $this->testType = basename(dirname($this->runFile)); $this->unfilteredTraces = array('control.php', 'zen.php', 'model.php', 'tao.php'); $this->initTraceFile(); } /** * Init trace file. * * @access private * @return bool */ public function initTraceFile(): bool { $tracePath = $this->zentaoRoot . "/tmp/coverage/"; $this->traceFile = $tracePath . $this->moduleName . '_' . $this->testType . '_' . basename($this->runFile, '.php') . '.json'; if(!is_dir($tracePath)) mkdir($tracePath, 0777, true); if(!is_file($this->traceFile)) { if(is_writable($tracePath) && $this->moduleName != 'calc') file_put_contents($this->traceFile, json_encode(array())); } return true; } /** * Start code coverage. * * @access public * @return void */ public function startCodeCoverage(): void { xdebug_start_code_coverage(); } /** * Save traces and restart code coverage. * * @access public * @return void */ public function saveAndRestartCodeCoverage(): void { $traces = xdebug_get_code_coverage(); $this->saveTraces($traces); xdebug_stop_code_coverage(); xdebug_start_code_coverage(); } /** * Get local trace file path. * * @access public * @return string */ public function getTraceFile(): string { return $this->traceFile; } /** * Reset traceFile. * * @access public * @return bool */ public function reset() { if(!is_file($this->traceFile)) return true; return exec("rm $this->traceFile") !== false; } /** * Save traces to file. * * @param array $traces * @access private * @return bool */ private function saveTraces(array $traces): bool { if(!file_exists($this->traceFile)) return false ; $traces = $this->filterTraces($traces); $traces = $this->groupTraceByModule($traces); $traces = $this->computeMethodCoverage($traces); $log = new stdclass; $log->time = date('Y-m-d H:i:s'); $log->ztfPath = getenv('ZTF_REPORT_DIR'); $log->traces = $traces; if(!$log->traces) return false; return file_put_contents($this->traceFile, json_encode($log)); } /** * Compute the line coverage test method. * @param array $traces * @access private * @return array|false */ private function computeMethodCoverage(array $traces): array|false { if(!isset($traces['executeLines'] )) return false; $executeLines = $traces['executeLines']; $testMethod = $this->getMethodInfo(); $methodLines = $this->getMethodLines(); $startLine = (int)$testMethod->getStartLine(); $endLine = (int)$testMethod->getEndLine(); foreach($executeLines as $executeLine => $executeCount) { if((int)$executeLine < $startLine || (int)$executeLine > $endLine) unset($executeLines[$executeLine]); } $traces['executeLines'] = $executeLines; $methodCoverage = round(count($executeLines)/$methodLines, 2); $traces['coverage'] = $methodCoverage === true ? 1 : $methodCoverage; if($traces['coverage'] > 1) $traces['coverage'] = 1; return $traces; } /** * Test method for total number of rows. * * @access private * @return int */ private function getMethodLines(): int { $testMethod = $this->getMethodInfo(); $funcFile = $this->getClassFile(); $startLine = $testMethod->getStartLine(); $endLine = $testMethod->getEndLine(); $funcLines = array_slice(explode(PHP_EOL, file_get_contents($funcFile)), $startLine, $endLine - $startLine); $lines = $this->getEffectiveLines(implode(PHP_EOL, $funcLines)); return $lines; } /** * Get test method info. * * @access private * @return object */ private function getMethodInfo(): object { $testMethod = new stdclass(); $class = $this->getClassInfo(); foreach($class->getMethods() as $method) { $methodName = $method->getName(); if(strtolower($methodName) == basename($this->runFile, '.php')) $testMethod = $method; } return $testMethod; } /** * Get the total number of methods in class. * * @param string $moduleName * @param string $type * @access private * @return int */ private function getClassMethodCount(string $moduleName, string $type): int { if(file_exists($this->getClassFile($moduleName, $type)) === false) return 0; $classFileContents = file_get_contents($this->getClassFile($moduleName, $type)); preg_match_all('/function\s+\w+\s*\(/', $classFileContents, $matches); $methodCount = count($matches[0]); return $methodCount; } /** * Get test method file. * * @param string $moduleName * @param string $type * @access private * @return string */ private function getClassFile(string $moduleName = '', string $type = ''): string { if(!$type) $type = $this->testType; if(!$moduleName) $moduleName = $this->moduleName; return "{$this->zentaoRoot}/module/{$moduleName}/{$type}.php"; } /** * Get test class Info. * * @param string $moduleName * @param string $type * @access private * @return object */ private function getClassInfo(string $moduleName = '', string $type = ''): object { if(!$type) $type = $this->testType; if(!$moduleName) $moduleName = $this->moduleName; if($type == 'control') { $class = new ReflectionClass($moduleName); } else { $class = new ReflectionClass($moduleName . ucfirst($type)); } return $class; } /** * Filter traces by file name. * * @param array $traces * @access private * @return array */ private function filterTraces(array $traces): array { foreach($traces as $filePath => $fileTrace) { $fileName = basename($filePath); if(!in_array($fileName, $this->unfilteredTraces)) unset($traces[$filePath]); } return $traces; } /** * Group traces by module. * * @param array $traces * @access private * @return array */ private function groupTraceByModule(array $traces): array { $groupedTraces = array(); foreach($traces as $filePath => $fileTrace) { $moduleName = $this->getModuleByFilePath($filePath); $fileName = basename($filePath, '.php'); $groupedTraces['module'] = $this->moduleName; $groupedTraces['type'] = $this->testType; $groupedTraces['method'] = $this->getMethodInfo()->getName(); if($moduleName == $this->moduleName && $fileName == $this->testType) $groupedTraces['executeLines'] = $fileTrace; } return $groupedTraces; } /** * Get current fileTrace belog to which module. * * @param string filePath eg: /home/liuyongkai/sites/local/max/max41/module/bug/model/bug.php * @access private * @return string moduleName eg: bug */ private function getModuleByFilePath(string $filePath): string { $moduleName = ''; preg_match('/\/module\/(\w+)\//', $filePath, $matches); $moduleName = $matches[1]; return $moduleName; } /** * Generate module stats report. * * @param array $traces * @access public * @return string */ private function genModuleStatsReport(array $traces): string { $summaryTable = << 模块 control 方法 control 覆盖方法 zen 方法 zen 覆盖方法 model 方法 model 覆盖方法 tao 方法 tao 覆盖方法 control 覆盖率 zen 覆盖率 model 覆盖率 tao 覆盖率 EOT; foreach($traces as $module => $moduleTraces) { $summaryTable .= $this->genStatsTableByModule($module, $moduleTraces); } $summaryTable .= '' . PHP_EOL; return $summaryTable; } /** * Generate stats table by module. * * @param string $module * @param array $moduleTraces * @access private * @return void */ private function genStatsTableByModule(string $module, array $moduleTraces) { $modelMethodesCount = $this->getClassMethodCount($module, 'model'); $controlMethodesCount = $this->getClassMethodCount($module, 'control'); $zenMethodesCount = $this->getClassMethodCount($module, 'zen'); $taoMethodesCount = $this->getClassMethodCount($module, 'tao'); $controlCoverageCount = isset($moduleTraces['control']) ? count($moduleTraces['control']) : 0; $zenCoverageCount = isset($moduleTraces['zen']) ? count($moduleTraces['zen']) : 0; $modelCoverageCount = isset($moduleTraces['model']) ? count($moduleTraces['model']) : 0; $taoCoverageCount = isset($moduleTraces['tao']) ? count($moduleTraces['tao']) : 0; $moduleCoverageList = array(); foreach($moduleTraces as $type => $methods) { $moduleCoverageList[$type] = 0; foreach($methods as $methodCoverage) $moduleCoverageList[$type] += $methodCoverage['coverage']; } $summaryTable = '' . PHP_EOL; $summaryTable .= "$module" . PHP_EOL; $summaryTable .= "$controlMethodesCount" . PHP_EOL; $summaryTable .= "$controlCoverageCount" . PHP_EOL; $summaryTable .= "$zenMethodesCount" . PHP_EOL; $summaryTable .= "$zenCoverageCount" . PHP_EOL; $summaryTable .= "$modelMethodesCount" . PHP_EOL; $summaryTable .= "$modelCoverageCount" . PHP_EOL; $summaryTable .= "$taoMethodesCount" . PHP_EOL; $summaryTable .= "$taoCoverageCount" . PHP_EOL; foreach($this->unfilteredTraces as $fileType) { $fileType = str_replace('.php', '', $fileType); $methodCount = $this->getClassMethodCount($module, $fileType); $summaryTable .= isset($moduleCoverageList[$fileType]) ? "" . round($moduleCoverageList[$fileType] / $methodCount, 2) * 100 . '%' . PHP_EOL : '0%' . PHP_EOL; } $summaryTable .= '' . PHP_EOL; return $summaryTable; } /** * Generate coverage report by module. * * @param string $module * @param string $file * @param array $fileTraces * @access private * @return string */ private function genCoverageTableByFile(string $module, string $file, array $fileTraces): string { $file = $this->getClassFile($module, $file); $coverageTable = << $file 行号 代码 调用次数 EOT; $content = file($file); $coverageLineList = array(); foreach($fileTraces as $method => $methodInfo) { foreach($methodInfo['executeLines'] as $executeLine => $count) $coverageLineList[$executeLine] = $count; } foreach($content as $line => $code) { /* The function file() give the line number start from 0, so we offset to end one more on index. */ $isCalled = in_array($line + 1, array_keys($coverageLineList)); $calledTimes = $isCalled ? '' . $coverageLineList[$line + 1] . '' : '0'; $coverageTable .= '' . PHP_EOL; $coverageTable .= '' . $line + 1 . '' . PHP_EOL; $coverageTable .= "" . htmlspecialchars($code) . "" . PHP_EOL; $coverageTable .= "$calledTimes" . PHP_EOL; $coverageTable .= '' . PHP_EOL; } $coverageTable .= '' . PHP_EOL; return $coverageTable; } /** * Get traces list from save path. * * @param string $filePath * @param string $key * @return array|string|false * */ public function loadTraceFromFiles(string $filePath, string $key = ''): array|string|false { $tracesFiles = glob("{$filePath}/*.json"); if(!$tracesFiles) return false ; $tracesList = array(); foreach($tracesFiles as $file) { $tracesInfo = json_decode(file_get_contents($file), true); if(!isset($tracesInfo['traces'])) continue; $traces = $tracesInfo['traces']; $tracesList['traces'][$traces['module']][$traces['type']][$traces['method']]['executeLines'] = $traces['executeLines']; $tracesList['traces'][$traces['module']][$traces['type']][$traces['method']]['coverage'] = $traces['coverage']; $tracesList['time'] = $tracesInfo['time']; $tracesList['ztfPath'] = $tracesInfo['ztfPath']; } if($key == '') return $tracesList; return isset($tracesList[$key]) ? $tracesList[$key] : array(); } /** * Generate summary report. * * @param string $module * @param string $file * @return string */ public function genSummaryReport(string $module='', string $file=''): string { /* Get trace from file. */ $tracesPath = $this->zentaoRoot . '/tmp/coverage'; $traces = $this->loadTraceFromFiles($tracesPath, 'traces'); /* Generate report. */ $reportHtml = empty($file) ? '' . PHP_EOL : '' . PHP_EOL; if(empty($file)) { $reportHtml .= $this->genModuleStatsReport($traces); } else { $reportHtml .= $this->genCoverageTableByFile($module, $file, $traces[$module][$file]); } $reportHtml .= ''; return $reportHtml; } /** * Get ztf report. * * @access public * @return object */ public function getZtfReport(): object|false { $reportFile = $this->getZtfReportFile(); if(!$reportFile) return false; $content = file_get_contents($reportFile); $report = json_decode($content); if(!is_object($report) || !isset($report->funcResult)) return false; $report->logFile = $reportFile; $report->funcResult = ''; $report->log = ''; $report->time = date('Y-m-d H:i:s', $report->endTime); $report->passPercent = round($report->pass / $report->total * 100, 2); $report->failPercent = round($report->fail / $report->total * 100, 2); $report->skipPercent = round($report->skip / $report->total * 100, 2); return $report; } /** * Get ztf report. * * @access public * @return string|false */ public function getZtfReportFile(): string|false { $latestTime = 0; $latestFile = ''; $tracePath = $this->zentaoRoot . "/tmp/coverage/"; $reportPath = $this->loadTraceFromFiles($tracePath, 'ztfPath'); exec("find $reportPath -type f -name result.json", $files, $returnCode); if($returnCode !== 0 || empty($files)) return false; foreach($files as $file) { if(is_file($file) && filemtime($file) > $latestTime) { $latestFile = $file; $latestTime = filemtime($file); } } return $latestFile; } /** * Get the effective lines of code. * * @param int $content * @access public * @return int */ private function getEffectiveLines(string $content): int { $content = preg_replace('#/\*.*?\*/#s', '', $content); $lines = 0; $content = preg_replace('/\r\n|\r/', "\n", $content); $content = trim($content); $content = explode("\n", $content); foreach($content as $line) { if(trim($line) === '') continue; if(trim($line) === '{') continue; if(trim($line) === '}') continue; $lines++; } return $lines; } }