Skip to content

Commit ef619c1

Browse files
committed
feat(grader): support 'debug_step' for VS Code config generation
This introduces a `debug_step` field in the test configuration. When a step fails, the grader will generate debug configurations for the referenced step instead of the current one. This improves the debugging workflow by allowing runtime failures (exec) to automatically target the linker (ld) step in VS Code.
1 parent d72fb5c commit ef619c1

File tree

19 files changed

+87
-19
lines changed

19 files changed

+87
-19
lines changed

grader.py

Lines changed: 67 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,16 @@
1414

1515
# --- BOOTSTRAP: Auto-setup environment ---
1616
import bootstrap
17+
1718
bootstrap.initialize()
1819
# -----------------------------------------
1920

2021
import tomli # noqa: E402
2122
from rich.console import Console # noqa: E402
2223
from rich.panel import Panel # noqa: E402
23-
from rich.progress import Progress, SpinnerColumn, TextColumn # noqa: E402
24+
from rich.progress import Progress, SpinnerColumn, Task, TextColumn # noqa: E402
2425
from rich.table import Table # noqa: E402
26+
from rich.text import Text # noqa: E402
2527

2628

2729
@dataclass
@@ -191,6 +193,16 @@ def _resolve_path(self, path: str, test_dir: Path) -> str:
191193
return path
192194

193195

196+
class StatusSpinnerColumn(SpinnerColumn):
197+
"""Spinner that swaps to a custom status icon when provided."""
198+
199+
def render(self, task: Task) -> Text:
200+
icon = task.fields.get("status_icon")
201+
if icon:
202+
return Text.from_markup(icon)
203+
return super().render(task)
204+
205+
194206
class SpecialJudgeChecker:
195207
def check(
196208
self,
@@ -525,7 +537,7 @@ def _execute_single_step(
525537
self._resolve_path(str(arg), test.path, test.path)
526538
for arg in step.get("args", [])
527539
]
528-
540+
529541
# 构建环境变量
530542
step_env = os.environ.copy()
531543
if "env" in step:
@@ -884,9 +896,10 @@ def _print_basic_summary(self, total_score: float, max_score: float) -> None:
884896
class VSCodeConfigGenerator:
885897
"""Generate and manage VS Code debug configurations"""
886898

887-
def __init__(self, project_root: Path, config: Config):
899+
def __init__(self, project_root: Path, config: Config, verbose: bool = False):
888900
self.project_root = project_root
889901
self.config = config
902+
self.verbose = verbose
890903
self.vscode_dir = project_root / ".vscode"
891904
self.launch_file = self.vscode_dir / "launch.json"
892905
self.tasks_file = self.vscode_dir / "tasks.json"
@@ -917,24 +930,43 @@ def _generate_launch_config(
917930
self, test_case: TestCase, failed_step: Dict[str, Any]
918931
) -> List[Dict[str, Any]]:
919932
"""Generate launch configuration based on debug type"""
933+
target_step = failed_step
934+
935+
if "debug_step" in failed_step:
936+
step_name = failed_step["debug_step"]
937+
# 在测试步骤中查找匹配名字的步骤
938+
found_step = next(
939+
(s for s in test_case.run_steps if s.get("name") == step_name), None
940+
)
941+
942+
if found_step:
943+
if self.verbose:
944+
print(
945+
f"Step '{failed_step.get('name')}' failed. Using debug step '{step_name}'."
946+
)
947+
948+
target_step = found_step
949+
else:
950+
print(f"Warning: debug step '{step_name}' not found in steps.")
951+
920952
debug_type = (
921-
failed_step.get("debug", {}).get("type")
953+
target_step.get("debug", {}).get("type")
922954
or test_case.meta.get("debug", {}).get("type")
923955
or self.config.debug_config["default_type"]
924956
)
925957

926958
cwd = str(self.config.project_root)
927959
program = self._resolve_path(
928-
failed_step["command"], test_case.path, self.config.project_root
960+
target_step["command"], test_case.path, self.config.project_root
929961
)
930962
args = [
931963
self._resolve_path(arg, test_case.path, self.config.project_root)
932-
for arg in failed_step.get("args", [])
964+
for arg in target_step.get("args", [])
933965
]
934966

935967
if debug_type == "cpp":
936968
configs = []
937-
base_name = f"Debug {test_case.meta['name']} - Step {failed_step.get('name', 'failed step')}"
969+
base_name = f"Debug {test_case.meta['name']} - Step {target_step.get('name', 'failed step')}"
938970

939971
# Add GDB configuration
940972
configs.append(
@@ -978,7 +1010,7 @@ def _generate_launch_config(
9781010
elif debug_type == "python":
9791011
return [
9801012
{
981-
"name": f"Debug {test_case.meta['name']} - Step {failed_step.get('name', 'failed step')}",
1013+
"name": f"Debug {test_case.meta['name']} - Step {target_step.get('name', 'failed step')}",
9821014
"type": "python",
9831015
"request": "launch",
9841016
"program": program,
@@ -1126,7 +1158,7 @@ def __init__(
11261158
JsonFormatter() if json_output else TableFormatter(self.console)
11271159
)
11281160
self.results: Dict[str, TestResult] = {}
1129-
self.vscode_generator = VSCodeConfigGenerator(Path.cwd(), self.config)
1161+
self.vscode_generator = VSCodeConfigGenerator(Path.cwd(), self.config, verbose=self.verbose)
11301162

11311163
def _save_test_history(
11321164
self,
@@ -1394,33 +1426,48 @@ def _run_setup_steps(self) -> bool:
13941426
return True
13951427

13961428
if self.console and not isinstance(self.console, type):
1429+
spinner_column = StatusSpinnerColumn()
13971430
with Progress(
1398-
SpinnerColumn(),
1399-
TextColumn("[progress.description]{task.description}"),
1431+
spinner_column,
1432+
TextColumn("[progress.description]{task.description}", markup=True),
14001433
console=self.console,
14011434
) as progress:
14021435
total_steps = len(self.config.setup_steps)
14031436
task = progress.add_task(
14041437
f"Running setup steps [0/{total_steps}]...",
14051438
total=total_steps,
1439+
status_icon="",
14061440
)
14071441

14081442
for i, step in enumerate(self.config.setup_steps, 1):
1409-
step_name = step.get("message", "Setup step")
1443+
step_label = step.get("name") or step.get("command") or "Setup step"
1444+
running_text = step.get("message") or f"Running {step_label}..."
1445+
success_text = (
1446+
step.get("success_message") or f"{step_label} completed"
1447+
)
1448+
failure_text = step.get("failure_message") or f"{step_label} failed"
1449+
step_prefix = f"Running setup steps [{i}/{total_steps}]: "
14101450
progress.update(
14111451
task,
1412-
description=f"Running setup steps [{i}/{total_steps}]: {step_name}",
1452+
description=f"{step_prefix}{running_text}",
14131453
completed=i - 1,
1454+
status_icon="",
14141455
)
14151456

14161457
if not self._run_setup_step(step):
1417-
progress.update(task, completed=total_steps)
1458+
progress.update(
1459+
task,
1460+
description=f"{step_prefix}{failure_text}",
1461+
completed=i,
1462+
status_icon="[red]✗[/red]",
1463+
)
14181464
return False
14191465

14201466
progress.update(
14211467
task,
1422-
description=f"Running setup steps [{i}/{total_steps}]: {step_name}",
1468+
description=f"{step_prefix}{success_text}",
14231469
completed=i,
1470+
status_icon="[green]✓[/green]",
14241471
)
14251472
return True
14261473
else:
@@ -1440,10 +1487,11 @@ def _run_setup_step(self, step: Dict[str, Any]) -> bool:
14401487

14411488
cmd = [step["command"]]
14421489
if "args" in step:
1443-
if isinstance(step["args"], list):
1444-
cmd.extend(step["args"])
1445-
else:
1446-
cmd.append(step["args"])
1490+
args = step["args"]
1491+
if isinstance(args, (list, tuple)):
1492+
cmd.extend(str(arg) for arg in args)
1493+
elif args is not None:
1494+
cmd.append(str(args))
14471495

14481496
process = subprocess.run(
14491497
cmd,

tests/cases/10-local-symbol/config.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ command = "${root_dir}/exec"
9999
args = ["${build_dir}/program2"]
100100
score = 5
101101
must_pass = false
102+
debug_step = "Link program2"
102103

103104
[run.check]
104105
return_code = 0

tests/cases/13-bss-link/config.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ files = ["${build_dir}/program"]
6868
name = "Run program"
6969
command = "${root_dir}/exec"
7070
args = ["${build_dir}/program"]
71+
debug_step = "Link program"
7172

7273
[run.check]
7374
stdout = "ans.out"

tests/cases/14-section-perm/config.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ command = "${root_dir}/exec"
118118
args = ["${build_dir}/program1"]
119119
score = 3.5
120120
must_pass = false
121+
debug_step = "Link program (test case 1)"
121122

122123
[run.check]
123124
return_code = -11 # SIGSEGV when writing to read-only section
@@ -129,6 +130,7 @@ command = "${root_dir}/exec"
129130
args = ["${build_dir}/program2"]
130131
score = 3.5
131132
must_pass = false
133+
debug_step = "Link program (test case 2)"
132134

133135
[run.check]
134136
return_code = -11 # SIGSEGV when executing data section
@@ -140,6 +142,7 @@ command = "${root_dir}/exec"
140142
args = ["${build_dir}/program3"]
141143
score = 3.0
142144
must_pass = false
145+
debug_step = "Link program (test case 3)"
143146

144147
[run.check]
145148
return_code = -11 # SIGSEGV when writing to code section

tests/cases/15-static-libs/config.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ return_code = 0
6666
name = "Execute program"
6767
command = "${root_dir}/exec"
6868
args = ["${build_dir}/program"]
69+
debug_step = "Link program"
6970
score = 10
7071

7172
[run.check]

tests/cases/16-complex-static-libs/config.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ return_code = 0
9393
name = "Execute program"
9494
command = "${root_dir}/exec"
9595
args = ["${build_dir}/program"]
96+
debug_step = "Link program"
9697
score = 5
9798
[run.check]
9899
return_code = 0

tests/cases/17-shared-lib-basic/config.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ return_code = 0
5353
name = "Execute program"
5454
command = "${root_dir}/exec"
5555
args = ["${build_dir}/program"]
56+
debug_step = "Link executable with shared library"
5657
score = 3
5758
[run.env]
5859
FLE_LIBRARY_PATH = "${build_dir}"

tests/cases/18-shared-lib-external/config.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ return_code = 0
6767
name = "Execute program"
6868
command = "${root_dir}/exec"
6969
args = ["${build_dir}/program"]
70+
debug_step = "Link executable"
7071
score = 3
7172
[run.env]
7273
FLE_LIBRARY_PATH = "${build_dir}"

tests/cases/19-shared-lib-weak/config.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ return_code = 0
5353
name = "Execute program"
5454
command = "${root_dir}/exec"
5555
args = ["${build_dir}/program"]
56+
debug_step = "Link executable"
5657
score = 3
5758
[run.env]
5859
FLE_LIBRARY_PATH = "${build_dir}"

tests/cases/2-single-file/config.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ files = ["${build_dir}/program"]
3030
name = "Run program"
3131
command = "${root_dir}/exec"
3232
args = ["${build_dir}/program"]
33+
debug_step = "Link program"
3334

3435
[run.check]
3536
return_code = 100

0 commit comments

Comments
 (0)