Files
hustoj/web/reinfo.php
klarkxy c22c788cfa feat(ui): 判题结果页新增输出对比视图
- 新增 parse_diff_blocks 系列函数,将 runtimeinfo 解析为结构化的测试点对比块,支持 simple 表格和 diff -y 两种格式
- 在判题结果页插入"输出对比"区块,逐测试点并列展示期望输出与用户输出,差异行高亮
- 补充 diff-section 相关样式,移动端切换为单列布局
2026-07-08 21:13:13 +08:00

345 lines
11 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
$cache_time = 10;
$OJ_CACHE_SHARE = false;
require_once('./include/cache_start.php');
require_once('./include/db_info.inc.php');
require_once('./include/setlang.php');
$view_title = "Welcome To Online Judge";
if(!isset($_SESSION[$OJ_NAME.'_'.'user_id'])){
header("location:loginpage.php");
exit(0);
}
require_once("./include/const.inc.php");
if(!isset($_GET['sid'])){
echo "No such code!\n";
require_once("oj-footer.php");
exit(0);
}
function is_valid($str2){
global $_SESSION,$OJ_NAME,$OJ_FRIENDLY_LEVEL;
if(isset($_SESSION[$OJ_NAME.'_'.'source_browser'])) return true;
if($OJ_FRIENDLY_LEVEL>3) return true;
// 如果希望能让任何人都查看对比和RE,放开行首注释,并设定$OJ_SHOW_DIFF=true;
// return true;
$n = strlen($str2);
$str = str_split($str2);
$m = 1;
for($i=0; $i<$n; $i++){
if(is_numeric($str[$i]))
$m++;
}
return $n/$m>3;
}
if(!isset($_SESSION[$OJ_NAME.'_'.'user_id'])){
$view_errors = $MSG_WARNING_ACCESS_DENIED ;
require("template/".$OJ_TEMPLATE."/error.php");
exit(0);
}
$ok = false;
$id = strval(intval($_GET['sid']));
$sql = "SELECT * FROM `solution` WHERE `solution_id`=?";
$result = pdo_query($sql,$id);
$row = $result[0];
$lang = $row['language'];
$contest_id = intval($row['contest_id']);
$isRE = $row['result']==10;
$isAC = $row['result']==4 ;
$mark=$row['pass_rate']*100;
if($isAC) $mark=100;
if((isset($_SESSION[$OJ_NAME.'_'.'user_id']) && $row && ($row['user_id']==$_SESSION[$OJ_NAME.'_'.'user_id']))||isset($_SESSION[$OJ_NAME.'_'.'source_browser']))
{
$ok = true;
}
$spj=pdo_query("select spj from problem where problem_id=?",$row['problem_id']);
if(!empty($spj)&&$spj[0][0]==2 && $OJ_HIDE_RIGHT_ANSWER && !isset($_SESSION[$OJ_NAME.'_'.'source_browser']) ){
$view_errors = "<h1>$MSG_MARK:$mark</h1><br>";
$ok = false;
}
/**
* 把 N 个相同测试点的 RE 折叠为 "1.in~5.in: Aborted (共 5 个测试点)"
*/
function dedup_runtimeinfo($text, &$dedup_count) {
$lines = explode("\n", $text);
$buckets = array(); // reason => [testcase, ...]
$other = array();
foreach ($lines as $line) {
$line = trim($line);
if ($line === "") continue;
// 形如 "data/1001/1.in:Aborted" 或 "1.in: Segmentation fault"
if (preg_match('/^(?:data\/\d+\/)?([^\s:]+(?:[.:][^\s:]+)*?):\s*(.+)$/', $line, $m)) {
$tc = $m[1];
$reason = trim($m[2]);
$buckets[$reason][] = $tc;
} else {
$other[] = $line;
}
}
$out = array();
foreach ($other as $o) $out[] = $o;
foreach ($buckets as $reason => $tcs) {
$count = count($tcs);
if ($count == 1) {
$out[] = $tcs[0] . ": " . $reason;
} else {
$first = $tcs[0];
$last = $tcs[$count - 1];
$range = ($first === $last) ? $first : ($first . "~" . $last);
$out[] = $range . ": " . $reason . " (共 " . $count . " 个测试点)";
$dedup_count += ($count - 1);
}
}
return implode("\n", $out);
}
$view_reinfo = "";
$view_reinfo_raw = ""; // 原始(管理员/有权限者看)
$view_reinfo_summary = ""; // 折叠后(普通用户看)
$view_reinfo_dedup_count = 0; // 折叠掉的测试点数
$verdict_color_class = "blue"; // semantic ui color: green/red/orange/yellow/grey
$result_icon = "info circle";
$verdict_tip = "";
if($ok){
if($row['user_id']!=$_SESSION[$OJ_NAME.'_'.'user_id']){
$view_mail_link= "<a href='mail.php?to_user=".htmlentities($row['user_id'],ENT_QUOTES,"UTF-8")."&title=$MSG_SUBMIT $id'>Mail the auther</a>";
}
$sql = "SELECT `error` FROM `runtimeinfo` WHERE `solution_id`=?";
$rt = pdo_query($sql,$id);
$raw_error = "";
if(isset($rt[0])){
$raw_error = str_replace("\n\r","\n",$rt[0]['error']);
}
// === 阶段 1基于 solution.result 给"主解释"(永远非空)===
$result_code = intval($row['result']);
$is_admin_session = isset($_SESSION[$OJ_NAME."_administrator"]);
$is_source_browser = isset($_SESSION[$OJ_NAME.'_'.'source_browser']);
// 颜色 + 图标
switch ($result_code) {
case 4: $verdict_color_class = "green"; $result_icon = "check circle"; break;
case 5: // PE
case 6: $verdict_color_class = "red"; $result_icon = "times circle"; break;
case 7: // TLE
case 8: // MLE
case 9: $verdict_color_class = "yellow";$result_icon = "hourglass end"; break;
case 10: $verdict_color_class = "red"; $result_icon = "bug"; break;
case 11: $verdict_color_class = "grey"; $result_icon = "code"; break;
case 13: $verdict_color_class = "blue"; $result_icon = "play circle"; break;
default: $verdict_color_class = "blue"; $result_icon = "info circle";
}
// 友好提示:仅保留标题性文字,具体修复建议交给 AI 点评
switch ($result_code) {
case 4: // AC
$verdict_tip = "本次提交通过了所有测试点";
break;
case 5: // PE
$verdict_tip = "输出与预期基本一致但格式不符Presentation Error";
break;
case 6: // WA
$verdict_tip = "输出与预期不符Wrong Answer";
break;
case 7: // TLE
$verdict_tip = $MSG_HINT_TLE;
break;
case 8: // MLE
$verdict_tip = $MSG_HINT_MLE;
break;
case 9: // OLE
$verdict_tip = $MSG_HINT_OLE;
break;
case 10: // RE
$verdict_tip = "程序运行中发生错误Runtime Error";
break;
case 11: // CE
$verdict_tip = "编译错误Compile Error。请查看编译信息页。";
break;
case 13: // TR (test run)
$verdict_tip = "测试运行完成Test Run";
break;
default:
$verdict_tip = "正在评判,请稍候刷新";
}
// === 阶段 3原始错误处理syscall 提示 vs 占位符修复)===
$view_reinfo_raw = $raw_error;
if (trim($raw_error) === "") {
// TLE/MLE/OLE 时 runtimeinfo 通常为空
$view_reinfo_summary = "";
} else {
// 含 "PASS" 不再显示 "error3" 占位
// 含 "php" 不再显示 "error2" 占位(用 generic 提示)
if (strpos($raw_error, "php") !== false) {
$view_reinfo_summary = $MSG_RUNTIME_GENERIC_ERROR;
$view_reinfo_raw = $raw_error; // 管理员仍能看
}
// 含 "judge/" 区分:是否是 syscall 错误
elseif (strpos($raw_error, "judge/") !== false
&& strpos($raw_error, "CALLID") === false
&& strpos($raw_error, "Forbidden system call") === false) {
$view_reinfo_summary = "潜在的数组或指针越界,请检查代码。";
}
// 默认:原文 + 去重
else {
$view_reinfo_summary = $raw_error;
// 多测试点去重:按 "infile: error" 模式分组
$view_reinfo_summary = dedup_runtimeinfo($view_reinfo_summary, $view_reinfo_dedup_count);
}
}
$view_reinfo = $view_reinfo_summary;
// 尝试解析为结构化的 diff 数据(用于 WA/PE/AC 时的对比展示)
$diff_blocks = parse_diff_blocks($raw_error);
// 管理员或 source_browser 永远看完整原文
$show_raw = $is_admin_session || $is_source_browser;
if ($show_raw) {
$view_reinfo = $view_reinfo_raw;
} else {
$view_reinfo = $view_reinfo_summary;
}
}
/**
* 把 runtimeinfo.error 解析为结构化的 diff blocks
* 返回 [['name' => 'test1.out', 'expected' => [...], 'yours' => [...], 'full_mode' => bool], ...]
* 解析失败返回空数组
*/
function parse_diff_blocks($text) {
$blocks = array();
if (empty($text)) return $blocks;
// 先按 "\n\n" 切分simple mode 每个测试点之间有空行)
// 但要避免切分 full mode 里的内容
$chunks = preg_split('/\n\s*\n/', $text);
foreach ($chunks as $chunk) {
$chunk = trim($chunk);
if ($chunk === '') continue;
// full mode: 形如 "========[test1.out]=========" 开头
if (preg_match('/^=+\[([^\]]+)\]=+$/m', $chunk, $m_head)) {
$name = $m_head[1];
// 提取 "------diff out 200 lines-----" 后面的内容
if (preg_match('/------diff out[^\n]*-+\n(.*?)\n=+\s*$/s', $chunk, $m_diff)) {
$diff_text = trim($m_diff[1]);
} else {
continue; // 没找到 diff 段,跳过
}
$lines = parse_diff_side_by_side($diff_text);
if (!empty($lines)) {
$blocks[] = array(
'name' => $name,
'expected' => $lines['expected'],
'yours' => $lines['yours'],
'full_mode' => true,
);
}
continue;
}
// simple mode: 形如 "test1.out\n--\n|Expected|Yours\n|--|--\n|..."
if (preg_match('/^([^\n|]+)\n--\n\|Expected\|Yours\n\|--\|--\n(.*)$/s', $chunk, $m)) {
$name = trim($m[1]);
$table = $m[2];
$lines = parse_pipe_diff_table($table);
if (!empty($lines)) {
$blocks[] = array(
'name' => $name,
'expected' => $lines['expected'],
'yours' => $lines['yours'],
'full_mode' => false,
);
}
}
}
return $blocks;
}
/**
* 解析 "|expected|yours\n" 格式的表格
* 已知边角:每行以 | 开头和 | 分隔;空字段表示没有该行
*/
function parse_pipe_diff_table($text) {
$expected = array();
$yours = array();
$lines = explode("\n", $text);
foreach ($lines as $line) {
if (strlen($line) < 2 || $line[0] !== '|') continue;
// 形如 "|a|b" 拆出 a 和 b
$rest = substr($line, 1);
// 找第一个 |
$pos = strpos($rest, '|');
if ($pos === false) continue;
$left = substr($rest, 0, $pos);
$right = substr($rest, $pos + 1);
$expected[] = $left;
$yours[] = $right;
}
if (empty($expected)) return array();
return array('expected' => $expected, 'yours' => $yours);
}
/**
* 解析 `diff -y` 格式: " < expected > yours"
* 中间用 | 或 / 分隔(取决于 GNU diff 版本)
*/
function parse_diff_side_by_side($text) {
$expected = array();
$yours = array();
$lines = explode("\n", $text);
foreach ($lines as $line) {
// 找中间的 | 或 / GNU diff -y 格式)
// 形如 " line1 | line2 " 或 " line1 < line2 "
if (preg_match('/^(.*?)\s+[<>|]\s+(.*)$/', $line, $m)) {
$expected[] = $m[1];
$yours[] = $m[2];
} elseif (trim($line) === '') {
// 空行:两边都空
} else {
// 单边(一边空):整行属于另一边
$expected[] = $line;
$yours[] = '';
}
}
if (empty($expected)) return array();
return array('expected' => $expected, 'yours' => $yours);
}
// 不是本人的提交,且不是 source_browser
else{
if($spj[0][0]!=2) $view_errors = $MSG_WARNING_ACCESS_DENIED;
require("template/".$OJ_TEMPLATE."/error.php");
exit(0);
}
/////////////////////////Template
if(!isset($_SESSION[$OJ_NAME.'_'.'source_browser']) && $OJ_SHOW_DIFF==false){
$view_errors = $MSG_WARNING_ACCESS_DENIED;
require("template/".$OJ_TEMPLATE."/error.php");
exit(0);
}
else{
require("template/".$OJ_TEMPLATE."/reinfo.php");
}
/////////////////////////Common foot
if(file_exists('./include/cache_end.php')){
require_once('./include/cache_end.php');
}
?>