From 026988361e390ff3842221904b752aefa7342bd0 Mon Sep 17 00:00:00 2001 From: liuyongkai Date: Wed, 3 May 2023 04:42:00 +0000 Subject: [PATCH 1/3] * Finish task #Statistical code line coverage. --- test/config/README | 1 + test/lib/coverage.php | 440 ++++++++++++++++++++++++++++++++++++++++++ test/lib/init.php | 22 ++- www/coverage.php | 103 ++++++++++ 4 files changed, 563 insertions(+), 3 deletions(-) create mode 100644 test/lib/coverage.php create mode 100644 www/coverage.php diff --git a/test/config/README b/test/config/README index bfa3dcb2ae..39246a6ed3 100644 --- a/test/config/README +++ b/test/config/README @@ -15,3 +15,4 @@ $config->db->password = '123456'; $config->db->prefix = 'zt_'; $config->webRoot = getWebRoot(); $config->default->lang = 'zh-cn'; +$config->codeCoverage = false; diff --git a/test/lib/coverage.php b/test/lib/coverage.php new file mode 100644 index 0000000000..252d3648db --- /dev/null +++ b/test/lib/coverage.php @@ -0,0 +1,440 @@ +zentaoRoot = $zentaoRoot; + $this->traceFile = ''; + $this->unfilteredTraces = array('control.php', 'zen.php', 'model.php', 'tao.php'); + + $this->initTraceFile(); + } + + /** + * Init trace file. + * + * @access private + * @return bool + */ + private function initTraceFile(): bool + { + $tracePath = $this->zentaoRoot . "/tmp/coverage/"; + $this->traceFile = $tracePath . "traces.json"; + if(!is_dir($tracePath)) mkdir($tracePath, 0777, true); + if(!is_file($this->traceFile)) 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(); + } + + /** + * Load saved traces from file. + * + * @access public + * @return array + */ + private function loadTraceFromFile(): array + { + return json_decode(file_get_contents($this->traceFile), true); + } + + /** + * Get local trace file path. + * + * @access public + * @return string + */ + public function getTraceFile(): string + { + return $this->traceFile; + } + + /** + * Merge traces form local file and this called trace. + * + * @param array $traces + * @access private + * @return array + */ + private function mergeTraces(array $traces): array + { + $savedTraces = $this->loadTraceFromFile(); + + if(!is_array($savedTraces)) return $traces; + + foreach($traces as $module => $moduleTraces) + { + if(!isset($savedTraces[$module])) + { + $savedTraces[$module] = $moduleTraces; + continue; + } + + foreach($moduleTraces as $file => $fileTraces) + { + if(!isset($savedTraces[$module][$file])) + { + $savedTraces[$module][$file] = $fileTraces; + continue; + } + else + { + $savedTraces[$module][$file] += $fileTraces; + } + } + } + + return $savedTraces; + } + + /** + * Save traces to file. + * + * @param array $traces + * @access public + * @return bool + */ + private function saveTraces(array $traces): bool + { + $traces = $this->filterTraces($traces); + $traces = $this->groupTraceByModule($traces); + $traces = $this->mergeTraces($traces); + + return file_put_contents($this->traceFile, json_encode($traces)); + } + + /** + * 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); + + $groupedTraces[$moduleName][$fileName] = $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($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 + 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($module, $moduleTraces) + { + $executedLines = 0; + $effectiveLines = 0; + $totalLines = 0; + $coveragePercent = 0; + $moduleSummaryTable = ''; + $summaryTable = ''; + + foreach($moduleTraces as $file => $fileTraces) + { + $fileName = str_replace('.php', '', $file); + $file = $this->zentaoRoot . '/module/' . $module . '/' . $file; + $content = file_get_contents($file); + $fileExecutedLines = count($fileTraces); + $fileEffectiveLines = $this->getEffectiveLines($content); + $fileTotalLines = substr_count($content, PHP_EOL); + $fileCoveragePercent = array(); + $fileCoveragePercent[$fileName] = ($fileEffectiveLines > 0) ? round($fileExecutedLines / $fileEffectiveLines * 100, 2) : 0; + + $executedLines += $fileExecutedLines; + $effectiveLines += $fileEffectiveLines; + $totalLines += $fileTotalLines; + } + + $coveragePercent = ($effectiveLines > 0) ? round($executedLines / $effectiveLines * 100, 2) : 0; + + $summaryTable .= "$module" . PHP_EOL; + $summaryTable .= '' . $executedLines . '' . PHP_EOL; + $summaryTable .= "$effectiveLines" . PHP_EOL; + $summaryTable .= "$totalLines" . PHP_EOL; + foreach($this->unfilteredTraces as $fileType) + { + $fileType = str_replace('.php', '', $fileType); + + $summaryTable .= isset($fileCoveragePercent[$fileType]) ? "" . $fileCoveragePercent[$fileType] . '%' . PHP_EOL : '0%' . PHP_EOL; + } + $summaryTable .= "$coveragePercent%" . PHP_EOL; + $summaryTable .= '' . PHP_EOL; + $summaryTable .= $moduleSummaryTable; + + return $summaryTable; + } + + /** + * Generate coverage report by module. + * + * @param string $module + * @param string $file + * @param array $fileTraces + * @access private + * @return string + */ + private function genCoverageTableByFile($module, $file, $fileTraces): string + { + $file = $this->zentaoRoot . DIRECTORY_SEPARATOR . 'module' . DIRECTORY_SEPARATOR . $module . DIRECTORY_SEPARATOR . $file; + $coverageTable = << + $file + + + 行号 + 代码 + 调用次数 + + + +EOT; + $content = file($file); + + 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($fileTraces)); + $calledTimes = $isCalled ? '' . $fileTraces[$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; + } + + /** + * Generate summary report. + * + * @param string $module + * @param string $file + * @return string + */ + public function genSummaryReport(string $module='', string $file=''): string + { + /* Get trace from file. */ + $traces = $this->loadTraceFromFile(); + + /* Generate report. */ + $reportHtml = ''; + $reportHtml .= '单元测试行覆盖率报告'; + $reportHtml .= <<'; + $reportHtml .= '

单元测试行覆盖率报告

'; + if(empty($file)) + { + $reportHtml .= $this->genModuleStatsReport($traces); + } + else + { + $file .= '.php'; + $reportHtml .= $this->genCoverageTableByFile($module, $file, $traces[$module][$file]); + } + $reportHtml .= ''; + + return $reportHtml; + } + + /** + * 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; + } +} diff --git a/test/lib/init.php b/test/lib/init.php index e3ad2b4625..fbd253bba1 100644 --- a/test/lib/init.php +++ b/test/lib/init.php @@ -15,6 +15,12 @@ /* Set the error reporting. */ error_reporting(E_ALL); define('RUN_MODE', 'test'); +if(!defined('LIB_ROOT')) define('LIB_ROOT', dirname(dirname(__FILE__)) . DIRECTORY_SEPARATOR . 'lib' . DIRECTORY_SEPARATOR); + +include_once LIB_ROOT . 'coverage.php'; + +$codeCoverageConfig = dirname(LIB_ROOT) . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'my.php'; +$codeCoverageConfig = exec("sed -n 's/^\\\$config->codeCoverage *= *\\(.*\\);/\\1/p' $codeCoverageConfig"); if($argc > 1 && $argv[1] == '-extract') { @@ -22,8 +28,15 @@ if($argc > 1 && $argv[1] == '-extract') exit; } -$testPath = dirname(dirname(dirname(__FILE__))) . DIRECTORY_SEPARATOR . 'test' . DIRECTORY_SEPARATOR; -$frameworkRoot = dirname(dirname(dirname(__FILE__))) . DIRECTORY_SEPARATOR . 'framework' . DIRECTORY_SEPARATOR; +$zentaoRoot = dirname(__FILE__, 3) . DIRECTORY_SEPARATOR; +$testPath = $zentaoRoot . 'test' . DIRECTORY_SEPARATOR; +$frameworkRoot = $zentaoRoot . 'framework' . DIRECTORY_SEPARATOR; + +if(isset($codeCoverageConfig) and $codeCoverageConfig == 'true') +{ + $coverage = new coverage(); + $coverage->startCodeCoverage(); +} /** * Assert status code and set body as $_result. @@ -273,7 +286,7 @@ function genModuleAndMethod($rParams) { $param = trim($param, "'"); if($param[0] != '$') $param = trim(strchr($param, '$'), ')'); - + $objArrowCount = substr_count($param, '->'); $rParamsStructureList = explode('->', $param); @@ -413,6 +426,9 @@ function getValues($value, $keys, $delimiter) */ function e($expect) { + global $codeCoverageConfig; + global $coverage; + if(isset($codeCoverageConfig) and $codeCoverageConfig == 'true') $coverage->saveAndRestartCodeCoverage(); } /** diff --git a/www/coverage.php b/www/coverage.php new file mode 100644 index 0000000000..a78b02237d --- /dev/null +++ b/www/coverage.php @@ -0,0 +1,103 @@ +genSummaryReport(); + break; + case 'module': + $module = $_GET['module']; + $file = $_GET['file']; + $report = $coverage->genSummaryReport($module, $file); + break; + default: + $report = $coverage->genSummaryReport(); + break; +} +echo $report; +echo ""; +?> + + From 419d889153ce98a0cbb262aff30e409ba0ce9503 Mon Sep 17 00:00:00 2001 From: liuyongkai Date: Sat, 6 May 2023 07:54:56 +0000 Subject: [PATCH 2/3] * Add get ztf report function. --- test/lib/coverage.php | 137 +++++++++++++++++++++--------------------- 1 file changed, 67 insertions(+), 70 deletions(-) diff --git a/test/lib/coverage.php b/test/lib/coverage.php index 252d3648db..4e612f5228 100644 --- a/test/lib/coverage.php +++ b/test/lib/coverage.php @@ -62,12 +62,15 @@ class coverage /** * Load saved traces from file. * + * @param string $key * @access public - * @return array + * @return array|string */ - private function loadTraceFromFile(): array + private function loadTraceFromFile(string $key = ''): array|string { - return json_decode(file_get_contents($this->traceFile), true); + $report = json_decode(file_get_contents($this->traceFile), true); + if($key == '') return $report; + return isset($report[$key]) ? $report[$key] : array(); } /** @@ -75,7 +78,6 @@ class coverage * * @access public * @return string - */ public function getTraceFile(): string { return $this->traceFile; @@ -90,7 +92,7 @@ class coverage */ private function mergeTraces(array $traces): array { - $savedTraces = $this->loadTraceFromFile(); + $savedTraces = $this->loadTraceFromFile('traces'); if(!is_array($savedTraces)) return $traces; @@ -132,7 +134,12 @@ class coverage $traces = $this->groupTraceByModule($traces); $traces = $this->mergeTraces($traces); - return file_put_contents($this->traceFile, json_encode($traces)); + $log = new stdclass; + $log->time = date('Y-m-d H:i:s'); + $log->ztfPath = getenv('ZTF_REPORT_DIR'); + $log->traces = $traces; + + return file_put_contents($this->traceFile, json_encode($log)); } /** @@ -332,72 +339,10 @@ EOT; public function genSummaryReport(string $module='', string $file=''): string { /* Get trace from file. */ - $traces = $this->loadTraceFromFile(); + $traces = $this->loadTraceFromFile('traces'); /* Generate report. */ - $reportHtml = ''; - $reportHtml .= '单元测试行覆盖率报告'; - $reportHtml .= <<'; - $reportHtml .= '

单元测试行覆盖率报告

'; + $reportHtml = empty($file) ? '' . PHP_EOL: '' . PHP_EOL; if(empty($file)) { $reportHtml .= $this->genModuleStatsReport($traces); @@ -412,6 +357,58 @@ STYLE; 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->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 = ''; + $reportPath = $this->loadTraceFromFile('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. * From 23994447a2bad6aede3ccac3537a76e0fda76319 Mon Sep 17 00:00:00 2001 From: liuyongkai Date: Sat, 6 May 2023 07:55:42 +0000 Subject: [PATCH 3/3] * Show ztf report. --- www/coverage.php | 84 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 81 insertions(+), 3 deletions(-) diff --git a/www/coverage.php b/www/coverage.php index a78b02237d..4d6dd4a760 100644 --- a/www/coverage.php +++ b/www/coverage.php @@ -3,9 +3,20 @@ include_once(dirname(__FILE__, 2) . "/test/lib/coverage.php"); global $zentaoRoot; $zentaoRoot = dirname(__FILE__, 2); -$type = isset($_GET['module']) ? 'module' : 'summary'; -$coverage = new coverage(); -$report = ''; +$type = isset($_GET['module']) ? 'module' : 'summary'; +$coverage = new coverage(); +$report = ''; +$ztfReport = $coverage->getZtfReport(); +if($ztfReport) +{ + $ztfHtml = "

%s 执行%s个用例,耗时%s秒。%s(%s%%) 通过,%s(%s%%) 失败,%s(%s%%) 忽略。

"; + $ztfHtml = sprintf($ztfHtml, $ztfReport->time, $ztfReport->total, $ztfReport->duration, $ztfReport->pass, $ztfReport->passPercent, $ztfReport->fail, $ztfReport->failPercent, $ztfReport->skip, $ztfReport->skipPercent); +} +else +{ + $ztfHtml = "

没有找到ZTF测试报告。

"; +} + switch($type) { @@ -21,6 +32,73 @@ switch($type) $report = $coverage->genSummaryReport(); break; } +?> + + + + 单元测试行覆盖率报告 + + + +

单元测试行覆盖率报告

+var type = '$type';"; ?>