Skip to content

Commit 2d5b59a

Browse files
authored
feat: add stdout for sample tests (#46)
2 parents b9eeb18 + c1c1673 commit 2d5b59a

6 files changed

Lines changed: 208 additions & 24 deletions

File tree

app/services/execution/docker.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,13 +75,15 @@ def get_run_commands(self, lang: str, file_path: str) -> list:
7575
raise ValueError(f"Unsupported language: {lang}")
7676

7777
def run_container(
78-
self, lang: str, file_path: str, difficulty: str
78+
self, lang: str, file_path: str, difficulty: str, line_offset: int
7979
) -> ExecutionResult:
8080
"""
8181
Run the code in a Docker container.
8282
83+
:param lang: The language of the code.
8384
:param file_path: The path to the file to run.
8485
:param difficulty: The difficulty of the problem.
86+
:param line_offset: The line offset for error logs.
8587
:return: The result of the execution.
8688
"""
8789
# Get the memory and time limits for the difficulty level.
@@ -131,12 +133,14 @@ def run_container(
131133
return ExecutionResult(
132134
success=False,
133135
message="Runtime Error Detected\n" + self.last_logs.strip(),
136+
line_offset=line_offset,
134137
)
135138

136139
if self.last_stderr.strip():
137140
return ExecutionResult(
138141
success=False,
139142
message="Runtime Error Detected\n" + self.last_stderr.strip(),
143+
line_offset=line_offset,
140144
)
141145

142146
base_name = os.path.basename(file_path).split(".")[0]
@@ -150,6 +154,7 @@ def run_container(
150154
success=True,
151155
test_results=execution_data["hidden_results"]["test_results"],
152156
sample_results=execution_data["sample_results"]["test_results"],
157+
line_offset=line_offset,
153158
)
154159
else:
155160
return ExecutionResult(

app/services/execution/service.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ async def execute_code(
6464

6565
# Create a temporary file to store the test runner file.
6666
with tempfile.NamedTemporaryFile(
67-
mode="w", suffix=gen.get_file_extension(lang), delete=False
67+
mode="w", suffix=gen.get_file_extension(), delete=False
6868
) as f:
6969
# Test data are pairs of test cases and its expected results.
7070
test_data = [
@@ -83,7 +83,9 @@ async def execute_code(
8383
f.write(file_content)
8484
file_path = f.name
8585
try:
86-
result = self.docker.run_container(lang, file_path, difficulty)
86+
result = self.docker.run_container(
87+
lang, file_path, difficulty, gen.get_line_offset()
88+
)
8789

8890
# If all tests passed, get runtime analysis
8991
if result.all_cleared() and not settings.TESTING:

app/services/execution/templates.py

Lines changed: 58 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
PYTHON_TEMPLATE = r"""import json
22
import traceback
3+
import io
4+
import sys
35
from typing import *
46
57
{code}
@@ -10,23 +12,26 @@ def __init__(
1012
expected: str,
1113
passed: bool,
1214
output: Any = None,
15+
logs: str = None,
1316
error: str = None,
1417
input: str = None,
1518
):
1619
self.expected = expected
1720
self.output = str(output) if output is not None else None
1821
self.passed = passed
22+
self.logs = logs
1923
self.error = error
2024
self.input = input
2125
22-
def to_dict(self, include_input: bool = True):
26+
def to_dict(self, is_sample: bool = True):
2327
result = {{
2428
"expected": self.expected,
2529
"output": self.output,
2630
"passed": self.passed,
2731
"error": self.error,
2832
}}
29-
if include_input:
33+
if is_sample:
34+
result["logs"] = self.logs
3035
result["input"] = self.input
3136
return result
3237
@@ -47,22 +52,30 @@ def run_tests(solution, method_name, test_data, is_sample: bool = False):
4752
results = []
4853
4954
for test in test_data:
55+
old_stdout = sys.stdout
56+
new_stdout = io.StringIO()
57+
sys.stdout = new_stdout
58+
5059
try:
5160
result = eval(f"solution.{{format_test_data(method_name, test['input'])}}")
5261
passed = compare_results(result, test['expected'])
5362
results.append(TestResult(
5463
expected=test['expected'],
5564
output=result,
5665
passed=passed,
66+
logs=new_stdout.getvalue(),
5767
input=test['input'],
58-
).to_dict(include_input=is_sample))
68+
).to_dict(is_sample=is_sample))
5969
except Exception as e:
6070
results.append(TestResult(
6171
expected=test['expected'],
6272
passed=False,
73+
logs=new_stdout.getvalue(),
6374
error=traceback.format_exc(),
6475
input=test['input'],
65-
).to_dict(include_input=is_sample))
76+
).to_dict(is_sample=is_sample))
77+
finally:
78+
sys.stdout = old_stdout
6679
6780
return {{
6881
"test_results": results,
@@ -94,6 +107,7 @@ def run_tests(solution, method_name, test_data, is_sample: bool = False):
94107
import com.google.gson.*;
95108
import java.lang.reflect.*;
96109
import java.util.*;
110+
import java.io.*;
97111
98112
{code}
99113
@@ -115,11 +129,32 @@ def run_tests(solution, method_name, test_data, is_sample: bool = False):
115129
}}
116130
}}
117131
132+
private static class LogCapture {{
133+
private final ByteArrayOutputStream baos;
134+
private final PrintStream original;
135+
private final PrintStream capture;
136+
137+
public LogCapture() {{
138+
this.baos = new ByteArrayOutputStream();
139+
this.original = System.out;
140+
this.capture = new PrintStream(baos);
141+
}}
142+
143+
public void start() {{
144+
System.setOut(capture);
145+
}}
146+
147+
public String stop() {{
148+
System.setOut(original);
149+
return baos.toString().trim();
150+
}}
151+
}}
152+
118153
private static boolean compare(Object result, Object expected) {{
119154
{compare_func}
120155
}}
121156
122-
public static JsonArray runTests(Solution solution, JsonArray testData, boolean showInput) {{
157+
public static JsonArray runTests(Solution solution, JsonArray testData, boolean isSample) {{
123158
JsonArray results = new JsonArray();
124159
Gson gson = new Gson();
125160
@@ -146,21 +181,28 @@ def run_tests(solution, method_name, test_data, is_sample: bool = False):
146181
for (JsonElement testElement : testData) {{
147182
JsonObject test = testElement.getAsJsonObject();
148183
JsonObject result = new JsonObject();
184+
LogCapture logCapture = new LogCapture();
149185
150186
try {{
151187
String inputStr = test.get("input").getAsString();
152188
Object[] args = parseArguments(inputStr, paramTypes);
189+
190+
logCapture.start();
153191
Object output = targetMethod.invoke(solution, args);
192+
String logs = logCapture.stop();
193+
154194
Object expected = parseValue(test.get("expected").getAsString(), returnType);
155195
156196
boolean passed = compare(output, expected);
157197
result.addProperty("passed", passed);
158198
result.addProperty("output", gson.toJson(output));
159199
result.addProperty("expected", test.get("expected").getAsString());
160-
if (showInput) {{
200+
if (isSample) {{
201+
result.addProperty("logs", logs);
161202
result.addProperty("input", inputStr);
162203
}}
163204
}} catch (Exception e) {{
205+
logCapture.stop();
164206
Throwable cause = e;
165207
if (e instanceof InvocationTargetException && e.getCause() != null) {{
166208
cause = e.getCause();
@@ -459,6 +501,7 @@ def run_tests(solution, method_name, test_data, is_sample: bool = False):
459501
#include <type_traits>
460502
#include <bits/stdc++.h>
461503
#include <vector>
504+
#include <streambuf>
462505
463506
using namespace std;
464507
@@ -585,14 +628,19 @@ def run_tests(solution, method_name, test_data, is_sample: bool = False):
585628
{compare_func}
586629
}}
587630
588-
Json::Value runTests(Solution& solution, const Json::Value& testData, bool showInput) {{
631+
Json::Value runTests(Solution& solution, const Json::Value& testData, bool isSample) {{
589632
Json::Value results(Json::arrayValue);
590633
Json::CharReaderBuilder builder;
591634
Json::CharReader* reader = builder.newCharReader();
592635
string errors;
593636
594637
for (const auto &test : testData) {{
595638
Json::Value testResult;
639+
640+
stringstream logStream;
641+
streambuf* oldCout = cout.rdbuf();
642+
cout.rdbuf(logStream.rdbuf());
643+
596644
try {{
597645
auto args = parseArguments(test["input"].asString());
598646
@@ -615,13 +663,15 @@ def run_tests(solution, method_name, test_data, is_sample: bool = False):
615663
writer["indentation"] = "";
616664
testResult["output"] = Json::writeString(writer, output_json);
617665
testResult["expected"] = Json::writeString(writer, expected);
618-
if (showInput) {{
666+
if (isSample) {{
667+
testResult["logs"] = logStream.str();
619668
testResult["input"] = test["input"];
620669
}}
621670
}} catch (const exception &e) {{
622671
testResult["error"] = e.what();
623672
testResult["passed"] = false;
624673
}}
674+
cout.rdbuf(oldCout);
625675
results.append(testResult);
626676
}}
627677
delete reader;

app/services/execution/test_generator.py

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,12 @@ def generate_test_file(
3030
"""
3131

3232
@abstractmethod
33-
def get_file_extension(self, lang: str) -> str:
34-
"""
35-
Get the file extension for the given language.
33+
def get_file_extension(self) -> str:
34+
"""Get the file extension for the given language."""
3635

37-
:param lang: The language.
38-
"""
36+
@abstractmethod
37+
def get_line_offset(self) -> int:
38+
"""Get the line offset for the given language."""
3939

4040
def process_quotes(self, json_str: str) -> str:
4141
"""
@@ -67,9 +67,12 @@ def generate_test_file(
6767
sample_data=json.dumps(sample_data),
6868
)
6969

70-
def get_file_extension(self, lang: str) -> str:
70+
def get_file_extension(self) -> str:
7171
return ".py"
7272

73+
def get_line_offset(self) -> int:
74+
return 6
75+
7376

7477
class JavaTestGenerator(TestGenerator):
7578
def generate_test_file(
@@ -102,9 +105,6 @@ def generate_test_file(
102105
sample_data=sample_data,
103106
)
104107

105-
def get_file_extension(self, lang: str) -> str:
106-
return ".java"
107-
108108
def data_chunks(self, data: str) -> str:
109109
"""
110110
Converts the data to a string of concatenated JSON strings.
@@ -125,6 +125,12 @@ def data_chunks(self, data: str) -> str:
125125
chunks = "".join(['.append("' + s + '")' for s in json_strings])
126126
return f"new StringBuilder(){chunks}.toString()"
127127

128+
def get_file_extension(self) -> str:
129+
return ".java"
130+
131+
def get_line_offset(self) -> int:
132+
return 8
133+
128134

129135
class CppTestGenerator(TestGenerator):
130136
def generate_test_file(
@@ -198,5 +204,8 @@ def process_test_data(self, test_data: List[Dict]) -> List[Dict]:
198204
processed_data.append(data)
199205
return processed_data
200206

201-
def get_file_extension(self, lang: str) -> str:
207+
def get_file_extension(self) -> str:
202208
return ".cpp"
209+
210+
def get_line_offset(self) -> int:
211+
return 14

app/services/execution/types.py

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,36 @@
1-
from typing import Dict, List, Optional
1+
from typing import Dict, List, Any, Optional
2+
3+
4+
class TestResult:
5+
def __init__(
6+
self,
7+
expected: str,
8+
passed: bool,
9+
output: Any = None,
10+
logs: str = None,
11+
error: str = None,
12+
input: str = None,
13+
):
14+
self.expected = expected
15+
self.output = str(output) if output is not None else None
16+
self.passed = passed
17+
self.logs = logs
18+
self.error = error
19+
self.input = input
20+
21+
def to_dict(self, is_sample: bool = True):
22+
result = {
23+
{
24+
"expected": self.expected,
25+
"output": self.output,
26+
"passed": self.passed,
27+
"error": self.error,
28+
}
29+
}
30+
if is_sample:
31+
result["logs"] = self.logs
32+
result["input"] = self.input
33+
return result
234

335

436
class ExecutionResult:
@@ -10,13 +42,15 @@ def __init__(
1042
self,
1143
success: bool,
1244
message: Optional[str] = None,
13-
test_results: Optional[List[Dict]] = None,
14-
sample_results: Optional[List[Dict]] = None,
45+
line_offset: Optional[int] = None, # for error logs from templates
46+
test_results: Optional[List[TestResult]] = None,
47+
sample_results: Optional[List[TestResult]] = None,
1548
summary: Optional[Dict] = None,
1649
runtime_analysis: Optional[str] = None,
1750
):
1851
self.success = success
1952
self.message = message
53+
self.line_offset = line_offset
2054
self.test_results = test_results
2155
self.sample_results = sample_results
2256
self.summary = summary
@@ -39,6 +73,7 @@ def to_dict(self) -> Dict:
3973
return {
4074
"success": self.success,
4175
"message": self.message,
76+
"line_offset": self.line_offset,
4277
"test_results": self.test_results,
4378
"sample_results": self.sample_results,
4479
"summary": self.summary

0 commit comments

Comments
 (0)