Skip to content

Commit 0719b06

Browse files
authored
Merge branch 'development' into feat/communication
2 parents 9749003 + 563a247 commit 0719b06

File tree

45 files changed

+834
-363
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+834
-363
lines changed

backend/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@
1414

1515
3. Enter `host.docker.internal` as the Host.
1616

17+
4. Enter the port used by the respective service:
18+
- User Service: `6379`
19+
- Collab Service: `6380`
20+
1721
4. Follow the instructions [here](https://nodejs.org/en/download/package-manager) to set up Node v20.
1822

1923
## Setting-up cloud MongoDB (in production)
Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
NODE_ENV=development
22
SERVICE_PORT=3004
33

4+
# Origins for cors
45
ORIGINS=http://localhost:5173,http://127.0.0.1:5173
56

6-
# One Compiler
7+
# Other service APIs
8+
QUESTION_SERVICE_URL=http://question-service:3000/api/questions
9+
10+
# One Compiler configuration
711
ONE_COMPILER_URL=https://onecompiler-apis.p.rapidapi.com/api/v1/run
8-
ONE_COMPILER_KEY=<ONE_COMPILER_KEY>
12+
ONE_COMPILER_KEY=<ONE_COMPILER_KEY>

backend/code-execution-service/README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,22 @@
66

77
2. Sign up for a free OneCompiler API [here](https://rapidapi.com/onecompiler-onecompiler-default/api/onecompiler-apis).
88

9-
3. Update `ONE_COMPILER_KEY` in `.env` with the the value of `x-rapidapi-key`.
9+
3. Update `ONE_COMPILER_KEY` in the `.env` file with the value of `x-rapidapi-key`.
1010

11-
## Running Code Execution Service without Docker
11+
## Running Code Execution Service Locally
1212

1313
1. Open Command Line/Terminal and navigate into the `code-execution-service` directory.
1414

1515
2. Run the command: `npm install`. This will install all the necessary dependencies.
1616

1717
3. Run the command `npm start` to start the Code Execution Service in production mode, or use `npm run dev` for development mode, which includes features like automatic server restart when you make code changes.
1818

19+
## Running Code Execution Service with Docker
20+
21+
1. Open the Command Line/Terminal.
22+
23+
2. Run the command `docker compose run code-execution-service` to start up the Code Execution Service and its dependencies.
24+
1925
## After running
2026

2127
1. To view Code Execution Service documentation, go to http://localhost:3004/docs.

backend/code-execution-service/src/controllers/codeExecutionControllers.ts

Lines changed: 63 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ import {
77
ERROR_FAILED_TO_EXECUTE_MESSAGE,
88
ERROR_NOT_SAME_LENGTH_MESSAGE,
99
SUCCESS_MESSAGE,
10+
ERROR_INVALID_TEST_CASES_MESSAGE,
1011
} from "../utils/constants";
12+
import { questionService } from "../utils/questionApi";
13+
import { testCasesApi } from "../utils/testCasesApi";
1114

1215
interface CompilerResult {
1316
status: string;
@@ -19,14 +22,9 @@ interface CompilerResult {
1922
}
2023

2124
export const executeCode = async (req: Request, res: Response) => {
22-
const {
23-
language,
24-
code,
25-
stdinList,
26-
stdoutList: expectedStdoutList,
27-
} = req.body;
25+
const { questionId, language, code } = req.body;
2826

29-
if (!language || !code || !stdinList || !expectedStdoutList) {
27+
if (!language || !code || !questionId) {
3028
res.status(400).json({
3129
message: ERROR_MISSING_REQUIRED_FIELDS_MESSAGE,
3230
});
@@ -40,48 +38,71 @@ export const executeCode = async (req: Request, res: Response) => {
4038
return;
4139
}
4240

43-
if (stdinList.length !== expectedStdoutList.length) {
44-
res.status(400).json({
45-
message: ERROR_NOT_SAME_LENGTH_MESSAGE,
46-
});
47-
return;
48-
}
49-
5041
try {
51-
const response = await oneCompilerApi(language, stdinList, code);
42+
// Get question test case files
43+
const qnsResponse = await questionService.get(`/${questionId}`);
44+
const { testcaseInputFileUrl, testcaseOutputFileUrl } =
45+
qnsResponse.data.question;
5246

53-
const data = (response.data as CompilerResult[]).map((result, index) => {
54-
const {
55-
status,
56-
exception,
57-
stdout: actualStdout,
58-
stderr,
59-
stdin,
60-
executionTime,
61-
} = result;
62-
const expectedStdout = expectedStdoutList[index];
47+
// Extract test cases from input and output files
48+
const testCases = await testCasesApi(
49+
testcaseInputFileUrl,
50+
testcaseOutputFileUrl
51+
);
6352

64-
return {
65-
status,
66-
exception,
67-
expectedStdout,
68-
actualStdout,
69-
stderr,
70-
stdin,
71-
executionTime,
72-
isMatch:
73-
stderr !== null
74-
? false
75-
: actualStdout.trim() === expectedStdout.trim(),
76-
};
77-
});
53+
const stdinList: string[] = testCases.input;
54+
const expectedResultList: string[] = testCases.output;
55+
56+
if (stdinList.length !== expectedResultList.length) {
57+
res.status(400).json({
58+
message: ERROR_NOT_SAME_LENGTH_MESSAGE,
59+
});
60+
return;
61+
}
62+
63+
if (stdinList.length === 0) {
64+
res.status(400).json({
65+
message: ERROR_INVALID_TEST_CASES_MESSAGE,
66+
});
67+
return;
68+
}
69+
70+
// Execute code for each test case
71+
const compilerResponse = await oneCompilerApi(language, stdinList, code);
72+
73+
const compilerData = (compilerResponse.data as CompilerResult[]).map(
74+
(result, index) => {
75+
let { stdout, ...restofResult } = result; // eslint-disable-line
76+
const expectedResultValue = expectedResultList[index].trim();
77+
78+
if (!stdout) {
79+
stdout = "";
80+
}
81+
82+
// Extract the last line as the result value
83+
// and the rest as stdout
84+
const lines = stdout.trim().split("\n");
85+
const resultValue = lines.pop() || "";
86+
stdout = lines.join("\n");
87+
88+
return {
89+
...restofResult,
90+
stdout,
91+
actualResult: resultValue,
92+
expectedResult: expectedResultValue,
93+
isMatch:
94+
result.stderr !== null
95+
? false
96+
: resultValue === expectedResultValue,
97+
};
98+
}
99+
);
78100

79101
res.status(200).json({
80102
message: SUCCESS_MESSAGE,
81-
data,
103+
data: compilerData,
82104
});
83-
} catch (err) {
84-
console.log(err);
105+
} catch {
85106
res.status(500).json({ message: ERROR_FAILED_TO_EXECUTE_MESSAGE });
86107
}
87108
};

backend/code-execution-service/src/utils/constants.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
export const SUPPORTED_LANGUAGES = ["python", "java", "c"];
22

33
export const ERROR_MISSING_REQUIRED_FIELDS_MESSAGE =
4-
"Missing required fields: language, code, stdinList, or stdoutList";
4+
"Missing required fields: language, code, or questionId.";
55

66
export const ERROR_UNSUPPORTED_LANGUAGE_MESSAGE = "Unsupported language.";
77

@@ -11,3 +11,5 @@ export const ERROR_NOT_SAME_LENGTH_MESSAGE =
1111
export const ERROR_FAILED_TO_EXECUTE_MESSAGE = "Failed to execute code";
1212

1313
export const SUCCESS_MESSAGE = "Code executed successfully";
14+
15+
export const ERROR_INVALID_TEST_CASES_MESSAGE = "Invalid test cases";

backend/code-execution-service/src/utils/oneCompilerApi.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ interface FileType {
1010

1111
export const oneCompilerApi = async (
1212
language: string,
13-
stdin: string,
13+
stdin: string[],
1414
userCode: string
1515
) => {
1616
let files: FileType[] = [];
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import axios from "axios";
2+
3+
const QUESTION_SERVICE_URL =
4+
process.env.QUESTION_SERVICE_URL ||
5+
"http://question-service:3000/api/questions";
6+
7+
export const questionService = axios.create({
8+
baseURL: QUESTION_SERVICE_URL,
9+
headers: {
10+
"Content-Type": "application/json",
11+
},
12+
});
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import axios from "axios";
2+
3+
export const testCasesApi = async (
4+
inputFileUrl: string,
5+
outputFileUrl: string
6+
) => {
7+
try {
8+
const inputFileUrlResponse = await axios.get(inputFileUrl);
9+
const outputFileUrlResponse = await axios.get(outputFileUrl);
10+
11+
// Split the input and output files by double new line
12+
return {
13+
input: inputFileUrlResponse.data.replace(/\r\n/g, "\n").split("\n\n"),
14+
output: outputFileUrlResponse.data.replace(/\r\n/g, "\n").split("\n\n"),
15+
};
16+
} catch {
17+
console.log("Failed to fetch test cases");
18+
return { input: [], output: [] };
19+
}
20+
};

backend/code-execution-service/swagger.yml

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,13 @@ definitions:
1010
stdin:
1111
type: string
1212
required: true
13-
expectedStdout:
13+
stdout:
1414
type: string
1515
required: true
16-
actualStdout:
16+
expectedResult:
17+
type: string
18+
required: true
19+
actualResult:
1720
type: string
1821
isMatch:
1922
type: boolean
@@ -82,23 +85,11 @@ paths:
8285
code:
8386
type: string
8487
description: The source code to execute.
85-
example: "name = input()\nage = input()\nprint('Hello ' + name + '. You are ' + age + ' years old this year?')\n\n"
86-
stdinList:
87-
type: array
88-
description: List of standard input values to pass to the code.
89-
items:
90-
type: string
91-
example: ["Alice\n21", "Peter\n22"]
92-
stdoutList:
93-
type: array
94-
description: Expected standard output values to compare against.
95-
items:
96-
type: string
97-
example:
98-
[
99-
"Hello Alice. You are 21 years old this year?\n",
100-
"Hello Peter. You are 22 years old this year?\n",
101-
]
88+
example: "a=input()\nb=input()\nc=input()\nd=input()\n\nprint(int(a)+int(b)+int(c)+int(d))"
89+
questionId:
90+
type: string
91+
description: Question ID.
92+
example: "123456789"
10293
responses:
10394
200:
10495
description: Execution Result

backend/code-execution-service/tests/codeExecutionRoutes.spec.ts

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,28 @@ import {
66
ERROR_NOT_SAME_LENGTH_MESSAGE,
77
SUCCESS_MESSAGE,
88
} from "../src/utils/constants";
9+
import { testCasesApi } from "../src/utils/testCasesApi";
10+
import { questionService } from "../src/utils/questionApi";
911

1012
const request = supertest(app);
1113

1214
const BASE_URL = "/api";
1315

16+
jest.mock("../src/utils/questionApi", () => ({
17+
questionService: {
18+
get: jest.fn(),
19+
},
20+
}));
21+
22+
jest.mock("../src/utils/testCasesApi", () => ({
23+
testCasesApi: jest.fn(),
24+
}));
25+
1426
describe("Code execution routes", () => {
27+
beforeEach(() => {
28+
jest.clearAllMocks();
29+
});
30+
1531
describe("GET /", () => {
1632
it("should return 200 OK", (done) => {
1733
request.get("/").expect(200, done);
@@ -31,35 +47,63 @@ describe("Code execution routes", () => {
3147
const response = await request.post(`${BASE_URL}/run`).send({
3248
language: "testing1234",
3349
code: "print('Hello, world!')",
34-
stdinList: ["input"],
35-
stdoutList: ["Hello, world!"],
50+
questionId: "1234",
3651
});
3752
expect(response.status).toBe(400);
3853
expect(response.body.message).toBe(ERROR_UNSUPPORTED_LANGUAGE_MESSAGE);
3954
});
4055

4156
it("should return 400 if stdinList and stdoutList lengths do not match", async () => {
57+
(questionService.get as jest.Mock).mockResolvedValue({
58+
data: {
59+
question: {
60+
testcaseInputFileUrl: "https://peerprep.com/input",
61+
testcaseOutputFileUrl: "https://peerprep.com/output",
62+
},
63+
},
64+
});
65+
66+
(testCasesApi as jest.Mock).mockResolvedValue({
67+
input: ["1", "2"],
68+
output: ["1"],
69+
});
70+
4271
const response = await request.post(`${BASE_URL}/run`).send({
4372
language: "python",
4473
code: "print('Hello, world!')",
45-
stdinList: ["input1"],
46-
stdoutList: ["output1", "output2"],
74+
questionId: "1234",
4775
});
4876
expect(response.status).toBe(400);
4977
expect(response.body.message).toBe(ERROR_NOT_SAME_LENGTH_MESSAGE);
5078
});
5179

5280
it("should return 200 and execution result when code executes successfully", async () => {
81+
(questionService.get as jest.Mock).mockResolvedValue({
82+
data: {
83+
question: {
84+
testcaseInputFileUrl: "https://peerprep.com/input",
85+
testcaseOutputFileUrl: "https://peerprep.com/output",
86+
},
87+
},
88+
});
89+
90+
(testCasesApi as jest.Mock).mockResolvedValue({
91+
input: ["1", "2"],
92+
output: ["1", "4"],
93+
});
94+
5395
const response = await request.post(`${BASE_URL}/run`).send({
5496
language: "python",
5597
code: "print(input())",
56-
stdinList: ["Hello, world!"],
57-
stdoutList: ["Hello, world!"],
98+
questionId: "1234",
5899
});
100+
59101
expect(response.status).toBe(200);
60102
expect(response.body.message).toBe(SUCCESS_MESSAGE);
61103
expect(response.body.data).toBeInstanceOf(Array);
62104
expect(response.body.data[0]).toHaveProperty("isMatch", true);
105+
expect(response.body.data[0]["isMatch"]).toBe(true);
106+
expect(response.body.data[1]["isMatch"]).toBe(false);
63107
});
64108
});
65109
});

0 commit comments

Comments
 (0)