Spaces:
Build error
Build error
"""Edit-related tests for the DockerRuntime.""" | |
import os | |
import pytest | |
from conftest import TEST_IN_CI, _close_test_runtime, _load_runtime | |
from openhands_aci.utils.diff import get_diff | |
from openhands.core.logger import openhands_logger as logger | |
from openhands.events.action import FileEditAction, FileReadAction | |
from openhands.events.observation import FileEditObservation | |
ORGINAL = """from flask import Flask | |
app = Flask(__name__) | |
@app.route('/') | |
def index(): | |
numbers = list(range(1, 11)) | |
return str(numbers) | |
if __name__ == '__main__': | |
app.run(port=5000) | |
""" | |
def test_edit_from_scratch(temp_dir, runtime_cls, run_as_openhands): | |
runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands) | |
try: | |
action = FileEditAction( | |
content=ORGINAL, | |
start=-1, | |
path=os.path.join('/workspace', 'app.py'), | |
) | |
logger.info(action, extra={'msg_type': 'ACTION'}) | |
obs = runtime.run_action(action) | |
logger.info(obs, extra={'msg_type': 'OBSERVATION'}) | |
assert isinstance(obs, FileEditObservation), ( | |
'The observation should be a FileEditObservation.' | |
) | |
action = FileReadAction( | |
path=os.path.join('/workspace', 'app.py'), | |
) | |
obs = runtime.run_action(action) | |
logger.info(obs, extra={'msg_type': 'OBSERVATION'}) | |
assert obs.content.strip() == ORGINAL.strip() | |
finally: | |
_close_test_runtime(runtime) | |
EDIT = """# above stays the same | |
@app.route('/') | |
def index(): | |
numbers = list(range(1, 11)) | |
return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>' | |
# below stays the same | |
""" | |
def test_edit(temp_dir, runtime_cls, run_as_openhands): | |
runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands) | |
try: | |
action = FileEditAction( | |
content=ORGINAL, | |
path=os.path.join('/workspace', 'app.py'), | |
) | |
logger.info(action, extra={'msg_type': 'ACTION'}) | |
obs = runtime.run_action(action) | |
logger.info(obs, extra={'msg_type': 'OBSERVATION'}) | |
assert isinstance(obs, FileEditObservation), ( | |
'The observation should be a FileEditObservation.' | |
) | |
action = FileReadAction( | |
path=os.path.join('/workspace', 'app.py'), | |
) | |
obs = runtime.run_action(action) | |
logger.info(obs, extra={'msg_type': 'OBSERVATION'}) | |
assert obs.content.strip() == ORGINAL.strip() | |
action = FileEditAction( | |
content=EDIT, | |
path=os.path.join('/workspace', 'app.py'), | |
) | |
obs = runtime.run_action(action) | |
logger.info(obs, extra={'msg_type': 'OBSERVATION'}) | |
assert ( | |
obs.content.strip() | |
== ( | |
'--- /workspace/app.py\n' | |
'+++ /workspace/app.py\n' | |
'@@ -4,7 +4,7 @@\n' | |
" @app.route('/')\n" | |
' def index():\n' | |
' numbers = list(range(1, 11))\n' | |
'- return str(numbers)\n' | |
"+ return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'\n" | |
'\n' | |
" if __name__ == '__main__':\n" | |
' app.run(port=5000)\n' | |
).strip() | |
) | |
finally: | |
_close_test_runtime(runtime) | |
ORIGINAL_LONG = '\n'.join([f'This is line {i}' for i in range(1, 1000)]) | |
EDIT_LONG = """ | |
This is line 100 + 10 | |
This is line 101 + 10 | |
""" | |
def test_edit_long_file(temp_dir, runtime_cls, run_as_openhands): | |
runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands) | |
try: | |
action = FileEditAction( | |
content=ORIGINAL_LONG, | |
path=os.path.join('/workspace', 'app.py'), | |
start=-1, | |
) | |
logger.info(action, extra={'msg_type': 'ACTION'}) | |
obs = runtime.run_action(action) | |
logger.info(obs, extra={'msg_type': 'OBSERVATION'}) | |
assert isinstance(obs, FileEditObservation), ( | |
'The observation should be a FileEditObservation.' | |
) | |
action = FileReadAction( | |
path=os.path.join('/workspace', 'app.py'), | |
) | |
obs = runtime.run_action(action) | |
logger.info(obs, extra={'msg_type': 'OBSERVATION'}) | |
assert obs.content.strip() == ORIGINAL_LONG.strip() | |
action = FileEditAction( | |
content=EDIT_LONG, | |
path=os.path.join('/workspace', 'app.py'), | |
start=100, | |
end=200, | |
) | |
obs = runtime.run_action(action) | |
logger.info(obs, extra={'msg_type': 'OBSERVATION'}) | |
assert ( | |
obs.content.strip() | |
== ( | |
'--- /workspace/app.py\n' | |
'+++ /workspace/app.py\n' | |
'@@ -97,8 +97,8 @@\n' | |
' This is line 97\n' | |
' This is line 98\n' | |
' This is line 99\n' | |
'-This is line 100\n' | |
'-This is line 101\n' | |
'+This is line 100 + 10\n' | |
'+This is line 101 + 10\n' | |
' This is line 102\n' | |
' This is line 103\n' | |
' This is line 104\n' | |
).strip() | |
) | |
finally: | |
_close_test_runtime(runtime) | |
# ====================================================================================== | |
# Test FileEditObservation (things that are displayed to the agent) | |
# ====================================================================================== | |
def test_edit_obs_insert_only(): | |
EDIT_LONG_INSERT_ONLY = ( | |
'\n'.join([f'This is line {i}' for i in range(1, 100)]) | |
+ EDIT_LONG | |
+ '\n'.join([f'This is line {i}' for i in range(100, 1000)]) | |
) | |
diff = get_diff(ORIGINAL_LONG, EDIT_LONG_INSERT_ONLY, '/workspace/app.py') | |
obs = FileEditObservation( | |
content=diff, | |
path='/workspace/app.py', | |
prev_exist=True, | |
old_content=ORIGINAL_LONG, | |
new_content=EDIT_LONG_INSERT_ONLY, | |
) | |
assert ( | |
str(obs).strip() | |
== """ | |
[Existing file /workspace/app.py is edited with 1 changes.] | |
[begin of edit 1 / 1] | |
(content before edit) | |
98|This is line 98 | |
99|This is line 99 | |
100|This is line 100 | |
101|This is line 101 | |
(content after edit) | |
98|This is line 98 | |
99|This is line 99 | |
+100|This is line 100 + 10 | |
+101|This is line 101 + 10 | |
102|This is line 100 | |
103|This is line 101 | |
[end of edit 1 / 1] | |
""".strip() | |
) | |
def test_edit_obs_replace(): | |
_new_content = ( | |
'\n'.join([f'This is line {i}' for i in range(1, 100)]) | |
+ EDIT_LONG | |
+ '\n'.join([f'This is line {i}' for i in range(102, 1000)]) | |
) | |
diff = get_diff(ORIGINAL_LONG, _new_content, '/workspace/app.py') | |
obs = FileEditObservation( | |
content=diff, | |
path='/workspace/app.py', | |
prev_exist=True, | |
old_content=ORIGINAL_LONG, | |
new_content=_new_content, | |
) | |
print(str(obs)) | |
assert ( | |
str(obs).strip() | |
== """ | |
[Existing file /workspace/app.py is edited with 1 changes.] | |
[begin of edit 1 / 1] | |
(content before edit) | |
98|This is line 98 | |
99|This is line 99 | |
-100|This is line 100 | |
-101|This is line 101 | |
102|This is line 102 | |
103|This is line 103 | |
(content after edit) | |
98|This is line 98 | |
99|This is line 99 | |
+100|This is line 100 + 10 | |
+101|This is line 101 + 10 | |
102|This is line 102 | |
103|This is line 103 | |
[end of edit 1 / 1] | |
""".strip() | |
) | |
def test_edit_obs_replace_with_empty_line(): | |
_new_content = ( | |
'\n'.join([f'This is line {i}' for i in range(1, 100)]) | |
+ '\n' | |
+ EDIT_LONG | |
+ '\n'.join([f'This is line {i}' for i in range(102, 1000)]) | |
) | |
diff = get_diff(ORIGINAL_LONG, _new_content, '/workspace/app.py') | |
obs = FileEditObservation( | |
content=diff, | |
path='/workspace/app.py', | |
prev_exist=True, | |
old_content=ORIGINAL_LONG, | |
new_content=_new_content, | |
) | |
print(str(obs)) | |
assert ( | |
str(obs).strip() | |
== """ | |
[Existing file /workspace/app.py is edited with 1 changes.] | |
[begin of edit 1 / 1] | |
(content before edit) | |
98|This is line 98 | |
99|This is line 99 | |
-100|This is line 100 | |
-101|This is line 101 | |
102|This is line 102 | |
103|This is line 103 | |
(content after edit) | |
98|This is line 98 | |
99|This is line 99 | |
+100| | |
+101|This is line 100 + 10 | |
+102|This is line 101 + 10 | |
103|This is line 102 | |
104|This is line 103 | |
[end of edit 1 / 1] | |
""".strip() | |
) | |
def test_edit_obs_multiple_edits(): | |
_new_content = ( | |
'\n'.join([f'This is line {i}' for i in range(1, 50)]) | |
+ '\nbalabala\n' | |
+ '\n'.join([f'This is line {i}' for i in range(50, 100)]) | |
+ EDIT_LONG | |
+ '\n'.join([f'This is line {i}' for i in range(102, 1000)]) | |
) | |
diff = get_diff(ORIGINAL_LONG, _new_content, '/workspace/app.py') | |
obs = FileEditObservation( | |
content=diff, | |
path='/workspace/app.py', | |
prev_exist=True, | |
old_content=ORIGINAL_LONG, | |
new_content=_new_content, | |
) | |
assert ( | |
str(obs).strip() | |
== """ | |
[Existing file /workspace/app.py is edited with 2 changes.] | |
[begin of edit 1 / 2] | |
(content before edit) | |
48|This is line 48 | |
49|This is line 49 | |
50|This is line 50 | |
51|This is line 51 | |
(content after edit) | |
48|This is line 48 | |
49|This is line 49 | |
+50|balabala | |
51|This is line 50 | |
52|This is line 51 | |
[end of edit 1 / 2] | |
------------------------- | |
[begin of edit 2 / 2] | |
(content before edit) | |
98|This is line 98 | |
99|This is line 99 | |
-100|This is line 100 | |
-101|This is line 101 | |
102|This is line 102 | |
103|This is line 103 | |
(content after edit) | |
99|This is line 98 | |
100|This is line 99 | |
+101|This is line 100 + 10 | |
+102|This is line 101 + 10 | |
103|This is line 102 | |
104|This is line 103 | |
[end of edit 2 / 2] | |
""".strip() | |
) | |
def test_edit_visualize_failed_edit(): | |
_new_content = ( | |
'\n'.join([f'This is line {i}' for i in range(1, 50)]) | |
+ '\nbalabala\n' | |
+ '\n'.join([f'This is line {i}' for i in range(50, 100)]) | |
+ EDIT_LONG | |
+ '\n'.join([f'This is line {i}' for i in range(102, 1000)]) | |
) | |
diff = get_diff(ORIGINAL_LONG, _new_content, '/workspace/app.py') | |
obs = FileEditObservation( | |
content=diff, | |
path='/workspace/app.py', | |
prev_exist=True, | |
old_content=ORIGINAL_LONG, | |
new_content=_new_content, | |
) | |
assert ( | |
obs.visualize_diff(change_applied=False).strip() | |
== """ | |
[Changes are NOT applied to /workspace/app.py - Here's how the file looks like if changes are applied.] | |
[begin of ATTEMPTED edit 1 / 2] | |
(content before ATTEMPTED edit) | |
48|This is line 48 | |
49|This is line 49 | |
50|This is line 50 | |
51|This is line 51 | |
(content after ATTEMPTED edit) | |
48|This is line 48 | |
49|This is line 49 | |
+50|balabala | |
51|This is line 50 | |
52|This is line 51 | |
[end of ATTEMPTED edit 1 / 2] | |
------------------------- | |
[begin of ATTEMPTED edit 2 / 2] | |
(content before ATTEMPTED edit) | |
98|This is line 98 | |
99|This is line 99 | |
-100|This is line 100 | |
-101|This is line 101 | |
102|This is line 102 | |
103|This is line 103 | |
(content after ATTEMPTED edit) | |
99|This is line 98 | |
100|This is line 99 | |
+101|This is line 100 + 10 | |
+102|This is line 101 + 10 | |
103|This is line 102 | |
104|This is line 103 | |
[end of ATTEMPTED edit 2 / 2] | |
""".strip() | |
) | |