fix:fix no console output because of programs don't write ResultOutputFile
feat: add json config and seperate giteaManager.ts out of enhancedmultiFileRunner.ts
This commit is contained in:
parent
88ef4e2d63
commit
f4099a7805
8
src/actions/dockerConfig.json
Normal file
8
src/actions/dockerConfig.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Image": "fly6516.synology.me:8080/multilang:latest",
|
||||||
|
"AttachStdout": true,
|
||||||
|
"AttachStderr": true,
|
||||||
|
"Tty": false,
|
||||||
|
"WorkingDir": "/workspace",
|
||||||
|
"ResultOutputFile": "result.txt"
|
||||||
|
}
|
@ -4,104 +4,9 @@ import Docker from 'dockerode';
|
|||||||
import fs from 'fs/promises'; // 使用异步版本的 fs 模块
|
import fs from 'fs/promises'; // 使用异步版本的 fs 模块
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import dockerConfig from './dockerConfig.json';
|
||||||
|
|
||||||
const docker = new Docker();
|
const docker = new Docker();
|
||||||
const DOCKER_IMAGE_URL = process.env.DOCKER_IMAGE_URL || 'fly6516.synology.me:8080/multilang:latest'; // 使用环境变量
|
|
||||||
|
|
||||||
let giteaRepoUrl: string | undefined;
|
|
||||||
let giteaToken: string | undefined;
|
|
||||||
let testDataPath: string = 'testdata'; // 默认测试数据目录
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置 Gitea 仓库 URL
|
|
||||||
* @param url 仓库 URL
|
|
||||||
*/
|
|
||||||
export const setGiteaRepoUrl = async (url: string) => {
|
|
||||||
if (!url || !url.startsWith('http')) {
|
|
||||||
throw new Error('Invalid repository URL. Please provide a valid URL starting with "http".');
|
|
||||||
}
|
|
||||||
giteaRepoUrl = url;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置 Gitea Token
|
|
||||||
* @param token 仓库 API 访问令牌
|
|
||||||
*/
|
|
||||||
export const setGiteaToken = async (token: string) => {
|
|
||||||
if (!token || token.trim().length === 0) {
|
|
||||||
throw new Error('Invalid token. Please provide a non-empty token.');
|
|
||||||
}
|
|
||||||
giteaToken = token;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置测试数据在仓库中的路径
|
|
||||||
* @param path 测试数据目录路径
|
|
||||||
*/
|
|
||||||
export const setTestDataPath = async (path: string) => {
|
|
||||||
if (!path || path.trim().length === 0) {
|
|
||||||
throw new Error('Invalid test data path. Please provide a non-empty path.');
|
|
||||||
}
|
|
||||||
testDataPath = path;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从 Gitea 仓库中下载指定目录的文件
|
|
||||||
* @param dir 目录路径
|
|
||||||
* @param tempDir 本地存储的临时目录
|
|
||||||
*/
|
|
||||||
const fetchFilesFromGitea = async (dir: string, tempDir: string) => {
|
|
||||||
if (!giteaRepoUrl || !giteaToken) {
|
|
||||||
throw new Error('Gitea repository URL or token is not set. Please configure them using setGiteaRepoUrl and setGiteaToken.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const targetDir = path.join(tempDir, dir);
|
|
||||||
await fs.mkdir(targetDir, { recursive: true });
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await axios.get(`${giteaRepoUrl}/contents/${dir}`, {
|
|
||||||
headers: {
|
|
||||||
Authorization: `token ${giteaToken}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const files = response.data;
|
|
||||||
if (!Array.isArray(files)) {
|
|
||||||
throw new Error(`Invalid directory structure for ${dir} in repository.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 并行下载文件
|
|
||||||
await Promise.all(files.filter(file => file.type === 'file').map(async (file) => {
|
|
||||||
const fileResponse = await axios.get(file.download_url, {
|
|
||||||
headers: {
|
|
||||||
Authorization: `token ${giteaToken}`,
|
|
||||||
},
|
|
||||||
responseType: 'arraybuffer',
|
|
||||||
});
|
|
||||||
|
|
||||||
const filePath = path.join(targetDir, file.name);
|
|
||||||
await fs.writeFile(filePath, fileResponse.data);
|
|
||||||
}));
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error(`Failed to fetch files from '${dir}': ${(error as Error).message}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 比较运行结果与正确答案文件
|
|
||||||
* @param resultFilePath 运行结果文件路径
|
|
||||||
* @param expectedFilePath 正确答案文件路径
|
|
||||||
* @returns 是否一致的比较结果
|
|
||||||
*/
|
|
||||||
const compareResults = async (resultFilePath: string, expectedFilePath: string): Promise<boolean> => {
|
|
||||||
try {
|
|
||||||
const result = (await fs.readFile(resultFilePath, 'utf-8')).trim();
|
|
||||||
const expected = (await fs.readFile(expectedFilePath, 'utf-8')).trim();
|
|
||||||
return result === expected;
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error(`Failed to read files for comparison: ${(error as Error).message}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 执行多文件代码并测试结果
|
* 执行多文件代码并测试结果
|
||||||
@ -113,9 +18,8 @@ export const runMultiFileCodeWithOptionalTestData = async (params: {
|
|||||||
language: string;
|
language: string;
|
||||||
testDataFiles?: string[];
|
testDataFiles?: string[];
|
||||||
expectedAnswerFiles?: string[];
|
expectedAnswerFiles?: string[];
|
||||||
resultOutputFile?: string;
|
|
||||||
}) => {
|
}) => {
|
||||||
const { files, language, testDataFiles, expectedAnswerFiles, resultOutputFile } = params;
|
const { files, language, testDataFiles, expectedAnswerFiles } = params;
|
||||||
|
|
||||||
if (!files || files.length === 0 || !language) {
|
if (!files || files.length === 0 || !language) {
|
||||||
throw new Error('Files and language are required.');
|
throw new Error('Files and language are required.');
|
||||||
@ -143,28 +47,13 @@ export const runMultiFileCodeWithOptionalTestData = async (params: {
|
|||||||
await fs.writeFile(filePath, file.content);
|
await fs.writeFile(filePath, file.content);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// 检查是否设置了 Gitea 仓库信息
|
|
||||||
if (giteaRepoUrl && giteaToken) {
|
|
||||||
try {
|
|
||||||
await fetchFilesFromGitea(testDataPath, tempDir);
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error(`Failed to fetch test data from '${testDataPath}': ${(error as Error).message}`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.info('No Gitea repository connection set. Proceeding without testdata.');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建 Docker 容器
|
// 创建 Docker 容器
|
||||||
const container = await docker.createContainer({
|
const container = await docker.createContainer({
|
||||||
Image: DOCKER_IMAGE_URL,
|
...dockerConfig,
|
||||||
Cmd: ['sh', '-c', compileAndRunCmd],
|
Cmd: ['sh', '-c', compileAndRunCmd],
|
||||||
AttachStdout: true,
|
|
||||||
AttachStderr: true,
|
|
||||||
Tty: false,
|
|
||||||
HostConfig: {
|
HostConfig: {
|
||||||
Binds: [`${tempDir}:/workspace`], // 挂载临时目录到容器
|
Binds: [`${tempDir}:/workspace`], // 挂载临时目录到容器
|
||||||
},
|
},
|
||||||
WorkingDir: '/workspace',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await container.start();
|
await container.start();
|
||||||
@ -183,14 +72,17 @@ export const runMultiFileCodeWithOptionalTestData = async (params: {
|
|||||||
return { error: `Execution failed with exit code ${exitCode.StatusCode}. Output: ${output}` };
|
return { error: `Execution failed with exit code ${exitCode.StatusCode}. Output: ${output}` };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果指定了结果输出文件,读取其内容
|
// 检查结果输出文件是否存在
|
||||||
if (resultOutputFile) {
|
const resultOutputFile = dockerConfig.ResultOutputFile;
|
||||||
const resultFilePath = path.join(tempDir, resultOutputFile);
|
const resultFilePath = path.join(tempDir, resultOutputFile);
|
||||||
|
|
||||||
|
try {
|
||||||
const resultFileContent = await fs.readFile(resultFilePath, 'utf-8');
|
const resultFileContent = await fs.readFile(resultFilePath, 'utf-8');
|
||||||
return { output, resultFileContent };
|
return { output, resultFileContent };
|
||||||
|
} catch (error) {
|
||||||
|
// 如果结果文件不存在,返回控制台输出
|
||||||
|
return { output };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { output };
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return { error: `An error occurred: ${(error as Error).message}` };
|
return { error: `An error occurred: ${(error as Error).message}` };
|
||||||
} finally {
|
} finally {
|
||||||
|
100
src/actions/giteaManager.ts
Normal file
100
src/actions/giteaManager.ts
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
'use server';
|
||||||
|
|
||||||
|
import fs from 'fs/promises'; // 使用异步版本的 fs 模块
|
||||||
|
import path from 'path';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
let giteaRepoUrl: string | undefined;
|
||||||
|
let giteaToken: string | undefined;
|
||||||
|
let testDataPath: string = 'testdata'; // 默认测试数据目录
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置 Gitea 仓库 URL
|
||||||
|
* @param url 仓库 URL
|
||||||
|
*/
|
||||||
|
export const setGiteaRepoUrl = async (url: string) => {
|
||||||
|
if (!url || !url.startsWith('http')) {
|
||||||
|
throw new Error('Invalid repository URL. Please provide a valid URL starting with "http".');
|
||||||
|
}
|
||||||
|
giteaRepoUrl = url;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置 Gitea Token
|
||||||
|
* @param token 仓库 API 访问令牌
|
||||||
|
*/
|
||||||
|
export const setGiteaToken = async (token: string) => {
|
||||||
|
if (!token || token.trim().length === 0) {
|
||||||
|
throw new Error('Invalid token. Please provide a non-empty token.');
|
||||||
|
}
|
||||||
|
giteaToken = token;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置测试数据在仓库中的路径
|
||||||
|
* @param path 测试数据目录路径
|
||||||
|
*/
|
||||||
|
export const setTestDataPath = async (path: string) => {
|
||||||
|
if (!path || path.trim().length === 0) {
|
||||||
|
throw new Error('Invalid test data path. Please provide a non-empty path.');
|
||||||
|
}
|
||||||
|
testDataPath = path;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 Gitea 仓库中下载指定目录的文件
|
||||||
|
* @param dir 目录路径
|
||||||
|
* @param tempDir 本地存储的临时目录
|
||||||
|
*/
|
||||||
|
export const fetchFilesFromGitea = async (dir: string, tempDir: string) => {
|
||||||
|
if (!giteaRepoUrl || !giteaToken) {
|
||||||
|
throw new Error('Gitea repository URL or token is not set. Please configure them using setGiteaRepoUrl and setGiteaToken.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetDir = path.join(tempDir, dir);
|
||||||
|
await fs.mkdir(targetDir, { recursive: true });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${giteaRepoUrl}/contents/${dir}`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `token ${giteaToken}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const files = response.data;
|
||||||
|
if (!Array.isArray(files)) {
|
||||||
|
throw new Error(`Invalid directory structure for ${dir} in repository.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 并行下载文件
|
||||||
|
await Promise.all(files.filter(file => file.type === 'file').map(async (file) => {
|
||||||
|
const fileResponse = await axios.get(file.download_url, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `token ${giteaToken}`,
|
||||||
|
},
|
||||||
|
responseType: 'arraybuffer',
|
||||||
|
});
|
||||||
|
|
||||||
|
const filePath = path.join(targetDir, file.name);
|
||||||
|
await fs.writeFile(filePath, fileResponse.data);
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to fetch files from '${dir}': ${(error as Error).message}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 比较运行结果与正确答案文件
|
||||||
|
* @param resultFilePath 运行结果文件路径
|
||||||
|
* @param expectedFilePath 正确答案文件路径
|
||||||
|
* @returns 是否一致的比较结果
|
||||||
|
*/
|
||||||
|
export const compareResults = async (resultFilePath: string, expectedFilePath: string): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
const result = (await fs.readFile(resultFilePath, 'utf-8')).trim();
|
||||||
|
const expected = (await fs.readFile(expectedFilePath, 'utf-8')).trim();
|
||||||
|
return result === expected;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to read files for comparison: ${(error as Error).message}`);
|
||||||
|
}
|
||||||
|
};
|
@ -6,14 +6,12 @@ import { runMultiFileCodeWithOptionalTestData } from '@/actions/enhancedmultiFil
|
|||||||
const TestPage = () => {
|
const TestPage = () => {
|
||||||
const [code, setCode] = useState<string>('');
|
const [code, setCode] = useState<string>('');
|
||||||
const [language, setLanguage] = useState<string>('c');
|
const [language, setLanguage] = useState<string>('c');
|
||||||
const [result, setResult] = useState<string>('');
|
|
||||||
const [consoleOutput, setConsoleOutput] = useState<string>('');
|
const [consoleOutput, setConsoleOutput] = useState<string>('');
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
|
||||||
const [resultFileContent, setResultFileContent] = useState<string>('');
|
const [resultFileContent, setResultFileContent] = useState<string>('');
|
||||||
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
|
|
||||||
const handleRun = async () => {
|
const handleRun = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setResult('');
|
|
||||||
setConsoleOutput('');
|
setConsoleOutput('');
|
||||||
setResultFileContent('');
|
setResultFileContent('');
|
||||||
|
|
||||||
@ -45,10 +43,6 @@ const TestPage = () => {
|
|||||||
setConsoleOutput(result.error);
|
setConsoleOutput(result.error);
|
||||||
} else {
|
} else {
|
||||||
setConsoleOutput(result.output);
|
setConsoleOutput(result.output);
|
||||||
if (result.comparisonResults) {
|
|
||||||
setResult(JSON.stringify(result.comparisonResults, null, 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置 result.txt 的内容
|
// 设置 result.txt 的内容
|
||||||
if (result.resultFileContent) {
|
if (result.resultFileContent) {
|
||||||
setResultFileContent(result.resultFileContent);
|
setResultFileContent(result.resultFileContent);
|
||||||
@ -116,21 +110,6 @@ const TestPage = () => {
|
|||||||
{consoleOutput || 'No console output yet.'}
|
{consoleOutput || 'No console output yet.'}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
{result && (
|
|
||||||
<div style={{ marginTop: '20px' }}>
|
|
||||||
<h2>Result</h2>
|
|
||||||
<pre
|
|
||||||
style={{
|
|
||||||
background: '#f4f4f4',
|
|
||||||
padding: '10px',
|
|
||||||
border: '1px solid #ddd',
|
|
||||||
borderRadius: '5px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{result || 'No results yet.'}
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{resultFileContent && (
|
{resultFileContent && (
|
||||||
<div style={{ marginTop: '20px' }}>
|
<div style={{ marginTop: '20px' }}>
|
||||||
<h2>Result File Content (result.txt)</h2>
|
<h2>Result File Content (result.txt)</h2>
|
||||||
|
Loading…
Reference in New Issue
Block a user