Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions bases/rsptx/book_server_api/routers/coach.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,11 +108,18 @@ def clean_python_testcase(raw_test_code: str) -> str:

def extract_parsons_code(html_block):
"""
Given the full HTML/pre block for a Parsons problem extracted from DB,
return only the Parsons code part.
Given the full HTML for a Parsons question fetched from the DB, return only
the code inside <pre class="parsonsblocks">...</pre> -- the question prompt
text lives in a sibling <div class="parsons_question"> and must be excluded,
otherwise it gets treated as (and rendered as) one of the draggable blocks.
"""
pre_match = re.search(
r'<pre[^>]*class="parsonsblocks"[^>]*>(.*?)</pre>', html_block, flags=re.DOTALL
)
pre_content = pre_match.group(1) if pre_match else html_block

# Remove all HTML tags and extract the code lines
text = re.sub(r"<.*?>", "", html_block, flags=re.DOTALL)
text = re.sub(r"<.*?>", "", pre_content, flags=re.DOTALL)
lines = text.strip().splitlines()
if "-----" in lines:
idx = lines.index("-----")
Expand All @@ -123,7 +130,8 @@ def extract_parsons_code(html_block):
clean_lines = [
line for line in code_lines if line.strip() and line.strip() != "====="
]
return "\n".join(clean_lines)
result = "\n".join(clean_lines)
return result


def _build_static_parsons_response(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,43 +67,15 @@ def _ensure_file_on_jobe(
CHECK_PROXY = "/ns/rsproxy/jobeCheckFile/"


def inject_pass_fail_prints(test_code):
"""
Inserts System.out.println("PASS") before System.exit(0)
and System.out.println("FAIL") + message before System.exit(1),
inside the BackendTest main method.

Assumes test_code contains:
public class BackendTest { public static void main(...) { ... } }
"""

# Insert PASS before System.exit(0) if not already present
if 'System.out.println("PASS")' not in test_code:
test_code = re.sub(
r"(TestHelper\.runAllTests\(\);\s*)(System\.exit\(0\);)",
r'\1System.out.println("PASS");\n \2',
test_code,
)

# Insert FAIL prints before System.exit(1) inside catch(Exception e)
if 'System.out.println("FAIL")' not in test_code:
test_code = re.sub(
r"(catch\s*\(\s*Exception\s+e\s*\)\s*\{\s*)(System\.exit\(1\);)",
r'\1System.out.println("FAIL");\n System.out.println(e.getMessage());\n \2',
test_code,
)

return test_code


# modified from rsproxy.py and livecode.js logic
def load_and_run_java_tests(java_code, test_code):
"""
Compile and run Java code with test cases.
Inputs:
java_code (str): The Java code to be tested.
test_code (str): The Java test cases. The test code should contain a public class with a main method to run the tests.
The test code is automatically reformatted based on the unittest_code provided by instructors in the RST file.
test_code (str): The Java test cases -- a JUnit test class (extends CodeTestHelper,
methods annotated with @Test, no main()), matching the standard
Runestone suffix_code convention used for regular activecode runs.
Output: bool: True if all tests pass, False otherwise.
"""

Expand All @@ -114,8 +86,6 @@ def extract_class_name(code):
else:
raise ValueError("Could not find a public class declaration.")

test_code = inject_pass_fail_prints(test_code)
print("modified_test_code\n", test_code)
student_class = extract_class_name(java_code)
test_class = extract_class_name(test_code)

Expand Down Expand Up @@ -172,18 +142,28 @@ def extract_class_name(code):
"body": put.text[:500],
}

# JOBE runs this, and it calls test class main()
runner_code = f"""public class TestRunner {{
# Mirrors livecode.js: run the JUnit test class via JUnitCore rather than
# calling a main() the test class doesn't have.
runner_code = f"""import org.junit.runner.JUnitCore;
import org.junit.runner.Result;

public class TestRunner {{
public static void main(String[] args) {{
{test_class}.main(args);
CodeTestHelper.resetFinalResults();
Result result = JUnitCore.runClasses({test_class}.class);
System.out.println(result.wasSuccessful() ? "PASS" : "FAIL");
}}
}}"""

runspec = {
"language_id": "java",
"sourcecode": runner_code,
"sourcefilename": "",
"parameters": {},
# RunestoneTests/CodeTestHelper only reference the student class by name
# (reflectively, at runtime), so javac never sees a static reference to it
# and won't compile it unless told to explicitly -- mirrors livecode.js,
# which pushes the student filename onto compileargs for the same reason.
"parameters": {"compileargs": [student_filename]},
"file_list": [
[student_id, student_filename],
[test_id, test_filename],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,22 +74,16 @@ def break_and_indent(text, max_line_length, indent=4):

def split_java_code_into_blocks(java_code):
"""
Rule-based criteria to split Java code into Parsons blocks.
Aim to not make empty {} into individual blocks.
Rule-based criteria to split Java code into Parsons blocks: one statement per block.
Aim to not make empty {} into individual blocks -- an opening brace stays attached to
the header line above it (e.g. class/method declarations), and a closing brace stays
attached to the last statement inside the block it closes.
"""
lines = java_code.split("\n")
blocks = []
i = 0
n = len(lines)

def get_indent(line):
m = re.match(r"^(\s*)", line)
return len(m.group(1)) if m else 0

def is_real_code(line):
s = line.strip()
return s and s not in ["{", "}"]

def is_open_brace(line):
return line.strip() == "{"

Expand All @@ -104,7 +98,6 @@ def is_close_brace(line):
break

block_lines = []
base_indent = get_indent(lines[i])

# Add first line to block
block_lines.append(lines[i])
Expand All @@ -115,12 +108,7 @@ def is_close_brace(line):
block_lines.append(lines[i])
i += 1

# Add all real code lines with same indentation
while i < n and get_indent(lines[i]) == base_indent and is_real_code(lines[i]):
block_lines.append(lines[i])
i += 1

# If next line is `}`, add it to the last real code block
# If next line is `}`, add it to this block
if i < n and is_close_brace(lines[i]):
block_lines.append(lines[i])
i += 1
Expand Down Expand Up @@ -432,10 +420,10 @@ def aggregate_code_to_Parsons_block_with_distractor(blocks):
elif (distractor_indent == "") & (
this_indent != current_indent_in_block_stack
):
# use the first line number of the block as the line sequence number
all_Parsons_blocks[block_stack[0][0]] = "".join(
str(block[1]) for block in block_stack
)
# each accumulated same-indent line becomes its own Parsons block,
# keyed by its own position so ordering is preserved
for stacked_index, stacked_content in block_stack:
all_Parsons_blocks[stacked_index] = stacked_content
block_stack = [(index, block[3])]
current_indent_in_block_stack = this_indent
# distractor_indent != "" means that detected that this is an end of a distractor block stack or a start of a distractor block stack -- how to distinguish?
Expand Down Expand Up @@ -467,10 +455,9 @@ def aggregate_code_to_Parsons_block_with_distractor(blocks):

if index == len(blocks) - 1:
if distractor_indent == "":
# use the first line number of the block as the line sequence number
all_Parsons_blocks[block_stack[0][0]] = "".join(
str(block[1]) for block in block_stack
)
# each accumulated same-indent line becomes its own Parsons block
for stacked_index, stacked_content in block_stack:
all_Parsons_blocks[stacked_index] = stacked_content
elif distractor_indent != "":
count_fixed = sum(
1 for block in block_stack if "#matched-fixed" in block[1]
Expand Down
Loading