Merge pull request 'Merge codecoverage function.' (#32) from lyk_codecoverage into master

Reviewed-on: https://git.zcorp.cc/easycorp/zentaopms/pulls/32
This commit is contained in:
管西迎
2023-05-06 16:00:48 +08:00
4 changed files with 637 additions and 2 deletions
+1
View File
@@ -15,3 +15,4 @@ $config->db->password = '123456';
$config->db->prefix = 'zt_';
$config->webRoot = getWebRoot();
$config->default->lang = 'zh-cn';
$config->codeCoverage = false;
+437
View File
@@ -0,0 +1,437 @@
<?php
class coverage
{
/**
* __construct
*
* @access private
* @return void
*/
public function __construct()
{
global $zentaoRoot;
$this->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.
*
* @param string $key
* @access public
* @return array|string
*/
private function loadTraceFromFile(string $key = ''): array|string
{
$report = json_decode(file_get_contents($this->traceFile), true);
if($key == '') return $report;
return isset($report[$key]) ? $report[$key] : array();
}
/**
* 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('traces');
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);
$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));
}
/**
* 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 = <<<EOT
<table border=1 id='summaryTable'>
<thead>
<tr>
<th>模块</th>
<th>执行行数</th>
<th>可执行行数</th>
<th>总行数</th>
<th>control</th>
<th>zen</th>
<th>model</th>
<th>tao</th>
<th>模块</th>
</tr>
</thead>
<tbody>
EOT;
foreach($traces as $module => $moduleTraces)
{
$summaryTable .= $this->genStatsTableByModule($module, $moduleTraces);
}
$summaryTable .= '</tbody></table>' . 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 .= "<th>$module</th>" . PHP_EOL;
$summaryTable .= '<td>' . $executedLines . '</td>' . PHP_EOL;
$summaryTable .= "<td>$effectiveLines</td>" . PHP_EOL;
$summaryTable .= "<td>$totalLines</td>" . PHP_EOL;
foreach($this->unfilteredTraces as $fileType)
{
$fileType = str_replace('.php', '', $fileType);
$summaryTable .= isset($fileCoveragePercent[$fileType]) ? "<td><a href='?module=$module&file=$fileType'>" . $fileCoveragePercent[$fileType] . '%</a></td>' . PHP_EOL : '<td>0%</td>' . PHP_EOL;
}
$summaryTable .= "<td>$coveragePercent%</td>" . PHP_EOL;
$summaryTable .= '</tr>' . 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 = <<<EOT
<table border="1">
<caption id="$file">$file</caption>
<thead>
<tr>
<th>行号</th>
<th>代码</th>
<th>调用次数</th>
</tr>
</thead>
<tbody>
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 ? '<span style="color: green;">' . $fileTraces[$line + 1] . '</span>' : '<span style="color: red;">0</span>';
$coverageTable .= '<tr>' . PHP_EOL;
$coverageTable .= '<td style="text-align: center;">' . $line + 1 . '</td>' . PHP_EOL;
$coverageTable .= "<td style='text-align: left;'><code>" . htmlspecialchars($code) . "</code></td>" . PHP_EOL;
$coverageTable .= "<td style='text-align: center;'>$calledTimes</td>" . PHP_EOL;
$coverageTable .= '</tr>' . PHP_EOL;
}
$coverageTable .= '</tbody></table>' . 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('traces');
/* Generate report. */
$reportHtml = empty($file) ? '<style>td { border: 1px solid #ccc; padding: 8px; text-align: center;}</style>' . PHP_EOL: '<style>td { border: 1px solid #ccc; padding: 8px;}</style>' . PHP_EOL;
if(empty($file))
{
$reportHtml .= $this->genModuleStatsReport($traces);
}
else
{
$file .= '.php';
$reportHtml .= $this->genCoverageTableByFile($module, $file, $traces[$module][$file]);
}
$reportHtml .= '</body></html>';
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.
*
* @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;
}
}
+18 -2
View File
@@ -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.
@@ -432,6 +445,9 @@ function getValues($value, $keys, $delimiter)
*/
function e($expect)
{
global $codeCoverageConfig;
global $coverage;
if(isset($codeCoverageConfig) and $codeCoverageConfig == 'true') $coverage->saveAndRestartCodeCoverage();
}
/**
+181
View File
@@ -0,0 +1,181 @@
<?php
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 = '';
$ztfReport = $coverage->getZtfReport();
if($ztfReport)
{
$ztfHtml = "<p>%s 执行<strong>%s个</strong>用例,耗时<strong>%s秒</strong>。<strong>%s(%s%%) </strong>通过,<strong>%s(%s%%)</strong> 失败,<strong>%s(%s%%)</strong> 忽略。</p>";
$ztfHtml = sprintf($ztfHtml, $ztfReport->time, $ztfReport->total, $ztfReport->duration, $ztfReport->pass, $ztfReport->passPercent, $ztfReport->fail, $ztfReport->failPercent, $ztfReport->skip, $ztfReport->skipPercent);
}
else
{
$ztfHtml = "<p>没有找到ZTF测试报告。</p>";
}
switch($type)
{
case 'summary':
$report = $coverage->genSummaryReport();
break;
case 'module':
$module = $_GET['module'];
$file = $_GET['file'];
$report = $coverage->genSummaryReport($module, $file);
break;
default:
$report = $coverage->genSummaryReport();
break;
}
?>
<html>
<head>
<meta charset="UTF-8">
<title>单元测试行覆盖率报告</title>
</head>
<style>
body {
font-family: Arial, sans-serif;
font-size: 16px;
line-height: 1.5;
margin: 0;
padding: 0;
}
table {
border-collapse: collapse;
max-width: 100%;
width: 100%;
margin: 20px 0;
}
th {
border: 1px solid #ccc;
padding: 8px;
text-align: center;
background-color: #eee;
white-space: nowrap;
}
caption {
font-weight: bold;
margin: 10px 0;
font-size: 18px;
}
h2 {
margin-top: 20px;
font-size: 24px;
}
.red {
color: red;
}
.green {
color: green;
}
/* table hover effect */
tbody tr:hover {
background-color: #f5f5f5;
}
/* table striped rows */
tbody tr:nth-child(even) {
background-color: #f9f9f9;
}
h1 {
text-align: center;
}
</style>
<body>
<h1>单元测试行覆盖率报告</h1>
<?php
echo $ztfHtml;
echo $report;
echo "<script>var type = '$type';</script>";
?>
<script src="./js/jquery/lib.js"></script>
<script>
$().ready(function()
{
if(type == 'summary')
{
renderColorByCoveragePercent()
implementExpand();
implementSort();
}
});
function implementSort()
{
var table = $('#summaryTable');
var tbody = table.find('tbody');
var rowsArr = tbody.find('tr').toArray();
rowsArr.sort(function(row1, row2)
{
/* Get seventh row and translate it's value into int. */
var val1 = $(row1).find('th:eq(0)').text();
var val2 = $(row2).find('th:eq(0)').text();
if (val1 < val2)
{
return -1;
}
else if (val1 > val2)
{
return 1;
}
else
{
return 0;
}
});
$.each(rowsArr, function(index, row)
{
tbody.append(row);
});
}
function renderColorByCoveragePercent()
{
$('table tbody tr td').each(function()
{
var text = $(this).text();
if(text.indexOf('%') > -1)
{
var percent = parseInt(text);
if(percent < 50)
{
$(this).css('color', 'red');
}
else if(percent < 80)
{
$(this).css('color', 'orange');
}
else
{
$(this).css('color', 'green');
}
}
});
}
function implementExpand()
{
$("tr[name$='-child']").hide();
$("tr[name$='-parent']").click(function()
{
$(this).nextUntil("tr[name$='-parent']").slideToggle('fast');
});
}
</script>