feat:添加了读取仓库测试数据支持

This commit is contained in:
fly6516 2025-01-03 03:38:42 +08:00
parent 612e36e4ea
commit 4d5288ed33
4 changed files with 263 additions and 3 deletions

62
package-lock.json generated
View File

@ -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",

View File

@ -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",

View 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 });
}
};

View File

@ -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';