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:
@@ -15,3 +15,4 @@ $config->db->password = '123456';
|
||||
$config->db->prefix = 'zt_';
|
||||
$config->webRoot = getWebRoot();
|
||||
$config->default->lang = 'zh-cn';
|
||||
$config->codeCoverage = false;
|
||||
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user