feat:添加了读取仓库测试数据支持
This commit is contained in:
parent
612e36e4ea
commit
4d5288ed33
62
package-lock.json
generated
62
package-lock.json
generated
@ -8,6 +8,7 @@
|
|||||||
"name": "docker-compiler-nextjs",
|
"name": "docker-compiler-nextjs",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"axios": "^1.7.9",
|
||||||
"dockerode": "^4.0.2",
|
"dockerode": "^4.0.2",
|
||||||
"next": "15.1.3",
|
"next": "15.1.3",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
@ -1731,6 +1732,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/asynckit": {
|
||||||
|
"version": "0.4.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
|
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/available-typed-arrays": {
|
"node_modules/available-typed-arrays": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@ -1753,6 +1760,17 @@
|
|||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/axios": {
|
||||||
|
"version": "1.7.9",
|
||||||
|
"resolved": "https://registry.npmmirror.com/axios/-/axios-1.7.9.tgz",
|
||||||
|
"integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"follow-redirects": "^1.15.6",
|
||||||
|
"form-data": "^4.0.0",
|
||||||
|
"proxy-from-env": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/axobject-query": {
|
"node_modules/axobject-query": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@ -2252,6 +2270,18 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/combined-stream": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"delayed-stream": "~1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/commander": {
|
"node_modules/commander": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@ -2592,6 +2622,15 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/delayed-stream": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/depd": {
|
"node_modules/depd": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz",
|
"resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz",
|
||||||
@ -3697,7 +3736,6 @@
|
|||||||
"version": "1.15.9",
|
"version": "1.15.9",
|
||||||
"resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
"resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
||||||
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
|
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "individual",
|
"type": "individual",
|
||||||
@ -3737,6 +3775,20 @@
|
|||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/form-data": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"asynckit": "^0.4.0",
|
||||||
|
"combined-stream": "^1.0.8",
|
||||||
|
"mime-types": "^2.1.12"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/forwarded": {
|
"node_modules/forwarded": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz",
|
"resolved": "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz",
|
||||||
@ -5266,7 +5318,6 @@
|
|||||||
"version": "1.52.0",
|
"version": "1.52.0",
|
||||||
"resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
|
"resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
|
||||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
@ -5276,7 +5327,6 @@
|
|||||||
"version": "2.1.35",
|
"version": "2.1.35",
|
||||||
"resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz",
|
"resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz",
|
||||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"mime-db": "1.52.0"
|
"mime-db": "1.52.0"
|
||||||
@ -6184,6 +6234,12 @@
|
|||||||
"node": ">= 0.10"
|
"node": ">= 0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/proxy-from-env": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/pump": {
|
"node_modules/pump": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmmirror.com/pump/-/pump-3.0.2.tgz",
|
"resolved": "https://registry.npmmirror.com/pump/-/pump-3.0.2.tgz",
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"axios": "^1.7.9",
|
||||||
"dockerode": "^4.0.2",
|
"dockerode": "^4.0.2",
|
||||||
"next": "15.1.3",
|
"next": "15.1.3",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
|
201
src/actions/enhancedmultiFileRunner.ts
Normal file
201
src/actions/enhancedmultiFileRunner.ts
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
'use server';
|
||||||
|
|
||||||
|
import Docker from 'dockerode';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
const docker = new Docker();
|
||||||
|
|
||||||
|
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);
|
||||||
|
fs.mkdirSync(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.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
if (file.type === 'file') {
|
||||||
|
const fileResponse = await axios.get(file.download_url, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `token ${giteaToken}`,
|
||||||
|
},
|
||||||
|
responseType: 'arraybuffer',
|
||||||
|
});
|
||||||
|
|
||||||
|
const filePath = path.join(targetDir, file.name);
|
||||||
|
fs.writeFileSync(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 = (resultFilePath: string, expectedFilePath: string): boolean => {
|
||||||
|
const result = fs.readFileSync(resultFilePath, 'utf-8').trim();
|
||||||
|
const expected = fs.readFileSync(expectedFilePath, 'utf-8').trim();
|
||||||
|
return result === expected;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行多文件代码并测试结果
|
||||||
|
* @param params 包含代码文件列表、语言、测试数据文件名和正确答案文件名的参数
|
||||||
|
* @returns 运行结果或比较结果
|
||||||
|
*/
|
||||||
|
export const runMultiFileCodeWithOptionalTestData = async (params: {
|
||||||
|
files: { name: string; content: string }[];
|
||||||
|
language: string;
|
||||||
|
testDataFiles?: string[];
|
||||||
|
expectedAnswerFiles?: string[];
|
||||||
|
resultOutputFile?: string;
|
||||||
|
}) => {
|
||||||
|
const { files, language, testDataFiles, expectedAnswerFiles, resultOutputFile } = params;
|
||||||
|
|
||||||
|
if (!files || files.length === 0 || !language) {
|
||||||
|
throw new Error('Files and language are required.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const supportedLanguages = ['c', 'java', 'python'];
|
||||||
|
if (!supportedLanguages.includes(language)) {
|
||||||
|
throw new Error(`Language '${language}' is not supported. Only 'c', 'java', and 'python' are supported.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const compileAndRunCmd =
|
||||||
|
language === 'c'
|
||||||
|
? 'gcc *.c -o main && ./main'
|
||||||
|
: language === 'java'
|
||||||
|
? 'javac *.java && java Main'
|
||||||
|
: 'python3 script.py';
|
||||||
|
|
||||||
|
const tempDir = path.join('/tmp', `${Date.now()}`);
|
||||||
|
fs.mkdirSync(tempDir, { recursive: true });
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 写入代码文件
|
||||||
|
for (const file of files) {
|
||||||
|
const filePath = path.join(tempDir, file.name);
|
||||||
|
fs.writeFileSync(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 容器
|
||||||
|
const container = await docker.createContainer({
|
||||||
|
Image: 'fly6516.synology.me:8080/multilang:latest',
|
||||||
|
Cmd: ['sh', '-c', compileAndRunCmd],
|
||||||
|
AttachStdout: true,
|
||||||
|
AttachStderr: true,
|
||||||
|
Tty: false,
|
||||||
|
HostConfig: {
|
||||||
|
Binds: [`${tempDir}:/workspace`], // 挂载临时目录到容器
|
||||||
|
},
|
||||||
|
WorkingDir: '/workspace',
|
||||||
|
});
|
||||||
|
|
||||||
|
await container.start();
|
||||||
|
|
||||||
|
const logs = await container.logs({ stdout: true, stderr: true, follow: true });
|
||||||
|
|
||||||
|
let output = '';
|
||||||
|
logs.on('data', (data: Buffer) => {
|
||||||
|
output += data.toString().replace(/[\x00-\x1F\x7F]/g, ''); // 去除不可见字符
|
||||||
|
});
|
||||||
|
|
||||||
|
const exitCode = await container.wait();
|
||||||
|
await container.remove();
|
||||||
|
|
||||||
|
if (exitCode.StatusCode !== 0) {
|
||||||
|
return { error: `Execution failed with exit code ${exitCode.StatusCode}. Output: ${output}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果指定了结果输出文件和正确答案文件,执行比较
|
||||||
|
if (resultOutputFile && expectedAnswerFiles) {
|
||||||
|
const resultFilePath = path.join(tempDir, resultOutputFile);
|
||||||
|
|
||||||
|
const comparisonResults = expectedAnswerFiles.map((expectedFile) => {
|
||||||
|
const expectedFilePath = path.join(tempDir, testDataPath, expectedFile);
|
||||||
|
const isMatch = compareResults(resultFilePath, expectedFilePath);
|
||||||
|
return { expectedFile, isMatch };
|
||||||
|
});
|
||||||
|
|
||||||
|
return { output, comparisonResults };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { output };
|
||||||
|
} catch (error) {
|
||||||
|
return { error: `An error occurred: ${(error as Error).message}` };
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
};
|
@ -1,3 +1,5 @@
|
|||||||
|
'use server';
|
||||||
|
|
||||||
import Docker from 'dockerode';
|
import Docker from 'dockerode';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
Loading…
Reference in New Issue
Block a user