Skip to content

Commit 68cfc3a

Browse files
authored
Merge pull request #39 from commit-0/coverage
Coverage
2 parents 0b549d9 + 34495a7 commit 68cfc3a

File tree

5 files changed

+95
-101
lines changed

5 files changed

+95
-101
lines changed

commit0/harness/docker_build.py

Lines changed: 1 addition & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@
77
from concurrent.futures import ThreadPoolExecutor, as_completed
88
from pathlib import Path
99
from typing import Any
10-
import sys
1110

1211
from commit0.harness.constants import (
1312
BASE_IMAGE_BUILD_DIR,
1413
REPO_IMAGE_BUILD_DIR,
1514
)
1615
from commit0.harness.spec import get_specs_from_dataset
16+
from commit0.harness.utils import setup_logger, close_logger
1717

1818
ansi_escape = re.compile(r"\x1B\[[0-?]*[ -/]*[@-~]")
1919

@@ -33,32 +33,6 @@ def __str__(self):
3333
)
3434

3535

36-
def setup_logger(repo: str, log_file: Path, mode: str = "w") -> logging.Logger:
37-
"""Used for logging the build process of images and running containers.
38-
It writes logs to the log file.
39-
"""
40-
log_file.parent.mkdir(parents=True, exist_ok=True)
41-
logger = logging.getLogger(f"{repo}.{log_file.name}")
42-
handler = logging.FileHandler(log_file, mode=mode)
43-
stdout_handler = logging.StreamHandler(sys.stdout)
44-
logger.addHandler(stdout_handler)
45-
formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
46-
handler.setFormatter(formatter)
47-
logger.addHandler(handler)
48-
logger.setLevel(logging.INFO)
49-
logger.propagate = False
50-
setattr(logger, "log_file", log_file)
51-
return logger
52-
53-
54-
def close_logger(logger: logging.Logger) -> None:
55-
"""Closes all handlers associated with the given logger to prevent too many open files."""
56-
# To avoid too many open files
57-
for handler in logger.handlers:
58-
handler.close()
59-
logger.removeHandler(handler)
60-
61-
6236
def build_image(
6337
image_name: str,
6438
setup_scripts: dict,

commit0/harness/execution_context.py

Lines changed: 47 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,6 @@
1717

1818
from commit0.harness.constants import Files
1919
from commit0.harness.spec import Spec
20-
from commit0.harness.utils import (
21-
EvaluationError,
22-
)
2320
from commit0.harness.docker_build import (
2421
close_logger,
2522
)
@@ -32,14 +29,6 @@
3229
)
3330

3431

35-
def read_stream(stream: modal.io_streams.StreamReader) -> str:
36-
"""Read stream"""
37-
strings = []
38-
for line in stream:
39-
strings.append(line)
40-
return "\n".join(strings)
41-
42-
4332
class ExecutionBackend(StrEnum):
4433
LOCAL = auto()
4534
MODAL = auto()
@@ -54,6 +43,7 @@ def __init__(
5443
num_cpus: int,
5544
log_dir: Path,
5645
files_to_copy: Optional[Files] = None,
46+
files_to_collect: Optional[list[str]] = None,
5747
):
5848
"""Create the remote execution context
5949
@@ -65,25 +55,13 @@ def __init__(
6555
self.timeout = timeout
6656
self.num_cpus = num_cpus
6757
self.log_dir = log_dir
58+
self.files_to_collect = files_to_collect
6859

6960
@abstractmethod
7061
def exec_run_with_timeout(self, command: str) -> tuple[str, bool, float]:
7162
"""Execute a test command"""
7263
raise NotImplementedError
7364

74-
def write_test_output(self, test_output: str, timed_out: bool) -> None:
75-
"""Write test output"""
76-
test_output_path = self.log_dir / "test_output.txt"
77-
with open(test_output_path, "w") as f:
78-
f.write(test_output)
79-
if timed_out:
80-
f.write(f"\n\nTimeout error: {self.timeout} seconds exceeded.")
81-
raise EvaluationError(
82-
self.spec.repo,
83-
f"Test timed out after {self.timeout} seconds.",
84-
self.logger,
85-
)
86-
8765
def __enter__(self):
8866
return self
8967

@@ -106,8 +84,17 @@ def __init__(
10684
num_cpus: int,
10785
log_dir: Path,
10886
files_to_copy: Optional[Files] = None,
87+
files_to_collect: Optional[list[str]] = None,
10988
):
110-
super().__init__(spec, logger, timeout, num_cpus, log_dir)
89+
super().__init__(
90+
spec,
91+
logger,
92+
timeout,
93+
num_cpus,
94+
log_dir,
95+
files_to_copy=files_to_copy,
96+
files_to_collect=files_to_collect,
97+
)
11198

11299
self.client = docker.from_env()
113100
self.container = create_container(
@@ -126,17 +113,16 @@ def exec_run_with_timeout(self, command: str) -> tuple[str, bool, float]:
126113
"""Exec"""
127114
output = exec_run_with_timeout(self.container, command, self.timeout)
128115

129-
# copy back report.json if there is any
130-
report_file = Path(self.spec.repo_directory) / "report.json"
131-
# Run the test command inside the container to check if the file exists
132-
exit_code, test_output = self.container.exec_run(
133-
f"test -e {report_file}", demux=True
134-
)
135-
# Check the exit code of the command
136-
if exit_code == 0:
137-
copy_from_container(
138-
self.container, report_file, self.log_dir / "report.json"
139-
)
116+
if self.files_to_collect:
117+
for fname in self.files_to_collect:
118+
file = Path(self.spec.repo_directory) / fname
119+
# Run the test command inside the container to check if the file exists
120+
exit_code, test_output = self.container.exec_run(
121+
f"test -e {file}", demux=True
122+
)
123+
# Check the exit code of the command
124+
if exit_code == 0:
125+
copy_from_container(self.container, file, self.log_dir / fname)
140126
return output
141127

142128
def __exit__(
@@ -158,8 +144,17 @@ def __init__(
158144
num_cpus: int,
159145
log_dir: Path,
160146
files_to_copy: Optional[Files] = None,
147+
files_to_collect: Optional[list[str]] = None,
161148
):
162-
super().__init__(spec, logger, timeout, num_cpus, log_dir)
149+
super().__init__(
150+
spec,
151+
logger,
152+
timeout,
153+
num_cpus,
154+
log_dir,
155+
files_to_copy=files_to_copy,
156+
files_to_collect=files_to_collect,
157+
)
163158

164159
self.app = modal.App()
165160

@@ -176,13 +171,18 @@ def exec_run_with_timeout(self, command: str) -> tuple[str, bool, float]:
176171
"""Execute command on modal sandbox"""
177172
start_time = time.time()
178173
with modal.Volume.ephemeral() as vol:
179-
# copy back report.json if there is any
180-
report_file = Path(self.spec.repo_directory) / "report.json"
181-
174+
cp_cmd = ""
175+
if self.files_to_collect:
176+
for fname in self.files_to_collect:
177+
remote_file = Path(self.spec.repo_directory) / fname
178+
curr_cp_cmd = f" && cp {str(remote_file)} /vol/{fname} 2>/dev/null"
179+
cp_cmd += curr_cp_cmd
180+
181+
command += cp_cmd
182182
self.sandbox = modal.Sandbox.create(
183183
"bash",
184184
"-c",
185-
f"{command} && cp {str(report_file)} /vol/report.json",
185+
command,
186186
image=self.image,
187187
cpu=self.num_cpus,
188188
timeout=self.timeout,
@@ -191,26 +191,22 @@ def exec_run_with_timeout(self, command: str) -> tuple[str, bool, float]:
191191
)
192192
self.sandbox.wait()
193193

194-
# stdout has been redirected to stderr
195-
stdout = read_stream(self.sandbox.stderr)
196-
197194
return_code = self.sandbox.returncode
198195
# https://github.com/modal-labs/modal-client/blob/d577b2916b5c3bf4ebbcb58fadced84d85e1cf8c/modal/sandbox.py#L413
199196
if return_code == 124:
200197
timed_out = True
201198
else:
202199
timed_out = False
203200

204-
# copy over report.json from mount
205-
with (self.log_dir / "report.json").open("wb") as f:
206-
for data in vol.read_file("report.json"):
207-
f.write(data)
201+
if self.files_to_collect:
202+
for fname in self.files_to_collect:
203+
with (self.log_dir / fname).open("wb") as f:
204+
for data in vol.read_file(fname):
205+
f.write(data)
208206

209207
self.sandbox.terminate()
210-
211208
end_time = time.time()
212-
213-
return stdout, timed_out, end_time - start_time
209+
return self.sandbox.stderr.read(), timed_out, end_time - start_time
214210

215211
def __exit__(
216212
self,

commit0/harness/run_pytest_ids.py

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,13 @@
1212
RUN_PYTEST_LOG_DIR,
1313
RepoInstance,
1414
)
15-
from commit0.harness.docker_build import (
16-
setup_logger,
17-
)
1815
from commit0.harness.spec import make_spec
1916
from commit0.harness.utils import (
2017
EvaluationError,
21-
extract_test_output,
2218
get_hash_string,
2319
generate_patch_between_commits,
20+
setup_logger,
21+
close_logger,
2422
)
2523
from commit0.harness.execution_context import (
2624
ExecutionBackend,
@@ -114,29 +112,29 @@ def main(
114112
eval_script={"src": eval_file, "dest": Path("/eval.sh")},
115113
patch={"src": patch_file, "dest": Path("/patch.diff")},
116114
)
115+
files_to_collect = [
116+
"report.json",
117+
"coverage.json",
118+
"pytest_exit_code.txt",
119+
"test_output.txt",
120+
]
117121

118122
try:
119123
with execution_context(
120-
spec, logger, timeout, num_cpus, log_dir, files_to_copy
124+
spec, logger, timeout, num_cpus, log_dir, files_to_copy, files_to_collect
121125
) as context:
122126
output, timed_out, total_runtime = context.exec_run_with_timeout(
123127
"/bin/bash /eval.sh"
124128
)
125-
logger.info(output)
126-
test_output = extract_test_output(
127-
output, "--json-report --json-report-file=report.json"
128-
)
129-
context.write_test_output(test_output, timed_out)
130-
if stdout:
131-
print(test_output)
132-
pytest_exit_code = extract_test_output(output, "echo ")
133-
try:
134-
pytest_exit_code = int(pytest_exit_code)
135-
except Exception:
136-
raise Exception(
137-
f"Fail to convert pytest_exit_code {pytest_exit_code} into an integer."
129+
if timed_out:
130+
raise EvaluationError(
131+
repo_name,
132+
f"Test timed out after {timeout} seconds.",
133+
logger,
138134
)
139-
sys.exit(pytest_exit_code)
135+
close_logger(logger)
136+
pytest_exit_code = Path(log_dir / "pytest_exit_code.txt").read_text().strip()
137+
sys.exit(int(pytest_exit_code))
140138
except EvaluationError as e:
141139
error_msg = (
142140
f"Error in running pytest for {repo_name}: {e}\n"

commit0/harness/spec.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -153,13 +153,11 @@ def make_eval_script_list(instance: RepoInstance, repo_directory: str) -> list[s
153153
f"git reset --hard {instance['base_commit']}",
154154
"git apply --allow-empty -v /patch.diff",
155155
"git status",
156-
f"{instance['test']['test_cmd']} --json-report --json-report-file=report.json --continue-on-collection-errors {{test_ids}}",
157-
"echo $?",
156+
f"{instance['test']['test_cmd']} --json-report --json-report-file=report.json --continue-on-collection-errors --cov=. --cov-branch --cov-report json {{test_ids}} > test_output.txt 2>&1",
157+
"echo $? > pytest_exit_code.txt",
158158
f"git reset --hard {instance['base_commit']}",
159159
"git status",
160160
]
161-
for i in range(len(eval_script_list)):
162-
eval_script_list[i] = f"{eval_script_list[i]} 1>&2"
163161
return eval_script_list
164162

165163

commit0/harness/utils.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import logging
55
import os
66
import time
7+
import sys
8+
from pathlib import Path
79
from typing import Optional
810

911
from fastcore.net import HTTP404NotFoundError, HTTP403ForbiddenError # type: ignore
@@ -25,6 +27,32 @@ def __str__(self):
2527
)
2628

2729

30+
def setup_logger(repo: str, log_file: Path, mode: str = "w") -> logging.Logger:
31+
"""Used for logging the build process of images and running containers.
32+
It writes logs to the log file.
33+
"""
34+
log_file.parent.mkdir(parents=True, exist_ok=True)
35+
logger = logging.getLogger(f"{repo}.{log_file.name}")
36+
handler = logging.FileHandler(log_file, mode=mode)
37+
stdout_handler = logging.StreamHandler(sys.stdout)
38+
logger.addHandler(stdout_handler)
39+
formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
40+
handler.setFormatter(formatter)
41+
logger.addHandler(handler)
42+
logger.setLevel(logging.INFO)
43+
logger.propagate = False
44+
setattr(logger, "log_file", log_file)
45+
return logger
46+
47+
48+
def close_logger(logger: logging.Logger) -> None:
49+
"""Closes all handlers associated with the given logger to prevent too many open files."""
50+
# To avoid too many open files
51+
for handler in logger.handlers:
52+
handler.close()
53+
logger.removeHandler(handler)
54+
55+
2856
def get_hash_string(input_string: str) -> str:
2957
# Create a new SHA-256 hash object
3058
sha256 = hashlib.sha256()

0 commit comments

Comments
 (0)