Skip to content

Commit 4eab627

Browse files
committed
Stronger test for fg and bg jobs
1 parent 56e7f30 commit 4eab627

File tree

5 files changed

+176
-167
lines changed

5 files changed

+176
-167
lines changed

docs/guide.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
2-
31
# ShellLab:构建 Unix Shell 🐚
42

53
## 实验指导文档

grader.py

Lines changed: 131 additions & 141 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,8 @@ def _load_config(self) -> Dict[str, Any]:
164164
"common_dir": "tests/common",
165165
},
166166
"debug": {
167-
"default_type": "gdb", # or "lldb", "python"
167+
"default_type": "gdb", # or "lldb", "python", "rust"
168+
"show_test_build_hint": True, # 是否在失败时显示 TEST_BUILD 环境变量设置提示
168169
},
169170
}
170171
with open(config_path, "rb") as f:
@@ -194,6 +195,7 @@ def debug_config(self) -> Dict[str, Any]:
194195
"debug",
195196
{
196197
"default_type": "gdb",
198+
"show_test_build_hint": True,
197199
},
198200
)
199201

@@ -2221,15 +2223,13 @@ def _print_summary(self, total_score: float, max_score: float) -> None:
22212223
)
22222224
self.console.print()
22232225
self.console.print(summary)
2224-
self.console.print()
22252226

22262227
def _print_basic_summary(self, total_score: float, max_score: float) -> None:
22272228
self.console.print()
22282229
self.console.print(
22292230
f"Total Score: {total_score:.1f}/{max_score:.1f} "
22302231
f"({total_score / max_score * 100:.1f}%)"
22312232
)
2232-
self.console.print()
22332233

22342234

22352235
class VSCodeConfigGenerator:
@@ -2341,6 +2341,59 @@ def _generate_launch_config(
23412341
"preLaunchTask": f"build-{test_case.path.name}",
23422342
}
23432343
]
2344+
elif debug_type == "rust":
2345+
base_name = f"Debug {test_case.meta['name']} - Step {failed_step.get('name', 'failed step')}"
2346+
configs = []
2347+
2348+
# Add CodeLLDB configuration for Rust
2349+
configs.append({
2350+
"name": f"{base_name} (CodeLLDB)",
2351+
"type": "lldb",
2352+
"request": "launch",
2353+
"program": program,
2354+
"args": args,
2355+
"cwd": cwd,
2356+
"sourceLanguages": ["rust"],
2357+
"internalConsoleOptions": "neverOpen",
2358+
"preLaunchTask": f"build-{test_case.path.name}",
2359+
"env": {
2360+
"RUST_BACKTRACE": "1"
2361+
}
2362+
})
2363+
2364+
# Add GDB configuration for Rust
2365+
configs.append({
2366+
"name": f"{base_name} (GDB)",
2367+
"type": "cppdbg",
2368+
"request": "launch",
2369+
"program": program,
2370+
"args": args,
2371+
"stopOnEntry": True,
2372+
"cwd": cwd,
2373+
"environment": [
2374+
{
2375+
"name": "RUST_BACKTRACE",
2376+
"value": "1"
2377+
}
2378+
],
2379+
"internalConsoleOptions": "neverOpen",
2380+
"MIMode": "gdb",
2381+
"setupCommands": [
2382+
{
2383+
"description": "Enable pretty-printing for gdb",
2384+
"text": "-enable-pretty-printing",
2385+
"ignoreFailures": True,
2386+
},
2387+
{
2388+
"description": "Enable Rust pretty-printing",
2389+
"text": "set language rust",
2390+
"ignoreFailures": True,
2391+
}
2392+
],
2393+
"preLaunchTask": f"build-{test_case.path.name}",
2394+
})
2395+
2396+
return configs
23442397
else:
23452398
raise ValueError(f"Unsupported debug type: {debug_type}")
23462399

@@ -2582,6 +2635,10 @@ def _print_debug_instructions(
25822635
self.console.print(" - C/C++: ms-vscode.cpptools")
25832636
elif debug_type == "python":
25842637
self.console.print(" - Python: ms-python.python")
2638+
elif debug_type == "rust":
2639+
self.console.print(" - rust-analyzer: rust-lang.rust-analyzer")
2640+
self.console.print(" - CodeLLDB: vadimcn.vscode-lldb")
2641+
self.console.print(" - C/C++ (optional, for GDB): ms-vscode.cpptools")
25852642
self.console.print(" c. Press F5 or use the Run and Debug view")
25862643
self.console.print(
25872644
" d. Select the auto-generated configuration for this test"
@@ -2594,6 +2651,11 @@ def _print_debug_instructions(
25942651
self.console.print(" - Continue (F5)")
25952652
self.console.print(" - Inspect variables in the Variables view")
25962653
self.console.print(" - Use Debug Console for expressions")
2654+
if debug_type == "rust":
2655+
self.console.print("\n4. Rust-specific debug features:")
2656+
self.console.print(" - RUST_BACKTRACE=1 is enabled for better error messages")
2657+
self.console.print(" - CodeLLDB provides native Rust debugging experience")
2658+
self.console.print(" - GDB configuration is also available as a backup option")
25972659
self.console.print(
25982660
"\nNote: The test will be automatically rebuilt before debugging starts"
25992661
)
@@ -3058,6 +3120,28 @@ def get_current_shell() -> str:
30583120
# 默认返回bash
30593121
return "bash"
30603122

3123+
def get_last_failed_tests(history_file: Path | str) -> List[Any]:
3124+
"""Get the last failed tests from the history file
3125+
3126+
Args:
3127+
history_file (Path): The path to the history file
3128+
3129+
Returns:
3130+
List[Any]: The last failed tests
3131+
"""
3132+
if isinstance(history_file, str):
3133+
history_file = Path(history_file)
3134+
3135+
if not history_file.exists():
3136+
return []
3137+
3138+
with open(history_file, "r", encoding="utf-8") as f:
3139+
history = json.load(f)
3140+
if not history:
3141+
return []
3142+
3143+
last_result = history[-1]
3144+
return filter(lambda x: x["status"] != "PASS", last_result["tests"])
30613145

30623146
def main():
30633147
parser = argparse.ArgumentParser(description="Grade student submissions")
@@ -3135,123 +3219,24 @@ def main():
31353219
args = parser.parse_args()
31363220

31373221
try:
3138-
# 如果是获取上次失败测试点的模式
31393222
if args.get_last_failed:
31403223
try:
3141-
history_file = Path(".test_history")
3142-
if not history_file.exists():
3143-
print("No test history found", file=sys.stderr)
3144-
sys.exit(1)
3145-
3146-
with open(history_file, "r", encoding="utf-8") as f:
3147-
history = json.load(f)
3148-
if not history:
3149-
print("Test history is empty", file=sys.stderr)
3150-
sys.exit(1)
3151-
3152-
# 获取最近一次的测试结果
3153-
last_result = history[-1]
3154-
3155-
# 查找第一个失败的测试点
3156-
for test in last_result["tests"]:
3157-
if test["status"] != "PASS":
3158-
# 获取shell类型
3159-
shell_type = args.shell or get_current_shell()
3160-
3161-
# 根据不同shell类型生成相应的命令
3162-
if shell_type == "fish":
3163-
print(f"set -x TEST_BUILD {test['build_path']}")
3164-
else: # bash 或 zsh
3165-
print(f"export TEST_BUILD={test['build_path']}")
3166-
sys.exit(0)
3167-
3224+
failed_tests = get_last_failed_tests(".test_history")
3225+
if not failed_tests:
31683226
print("No failed test found in last run", file=sys.stderr)
31693227
sys.exit(1)
3228+
3229+
first_failed_test = failed_tests[0]
3230+
shell_type = args.shell or get_current_shell()
31703231

3171-
except Exception as e:
3172-
print(f"Error reading test history: {str(e)}", file=sys.stderr)
3173-
sys.exit(1)
3174-
3175-
# 如果是重新运行失败测试点的模式
3176-
if args.rerun_failed:
3177-
try:
3178-
history_file = Path(".test_history")
3179-
if not history_file.exists():
3180-
print("No test history found", file=sys.stderr)
3181-
sys.exit(1)
3182-
3183-
with open(history_file, "r", encoding="utf-8") as f:
3184-
history = json.load(f)
3185-
if not history:
3186-
print("Test history is empty", file=sys.stderr)
3187-
sys.exit(1)
3188-
3189-
# 获取最近一次的测试结果
3190-
last_result = history[-1]
3191-
3192-
# 获取所有失败的测试点路径
3193-
failed_paths = []
3194-
for test in last_result["tests"]:
3195-
if test["status"] != "PASS":
3196-
failed_paths.append(Path(test["path"]))
3197-
3198-
if not failed_paths:
3199-
print("No failed test found in last run", file=sys.stderr)
3200-
sys.exit(0)
3201-
3202-
# 直接传入失败测试点的路径
3203-
grader = Grader(json_output=args.json)
3204-
grader.runner = TestRunner(
3205-
grader.config, grader.console, verbose=args.verbose
3206-
)
3207-
grader.run_all_tests(specific_paths=failed_paths)
3208-
3209-
# 检查是否所有测试都通过
3210-
total_score = sum(
3211-
result.score for result in grader.results.values()
3212-
)
3213-
max_score = sum(
3214-
test.meta["score"]
3215-
for test in grader._load_test_cases(specific_paths=failed_paths)
3216-
)
3217-
percentage = (total_score / max_score * 100) if max_score > 0 else 0
3218-
3219-
# 如果需要写入结果文件
3220-
if args.write_result:
3221-
with open(".autograder_result", "w") as f:
3222-
f.write(f"{percentage:.2f}")
3223-
3224-
# 只要有测试点失败,输出提示信息
3225-
if total_score < max_score:
3226-
if not args.json:
3227-
console = Console()
3228-
shell_type = args.shell or get_current_shell()
3229-
3230-
console.print(
3231-
"\n[bold yellow]To set TEST_BUILD environment variable to the failed test case's build directory:[/bold yellow]"
3232-
)
3233-
3234-
if shell_type == "fish":
3235-
console.print(
3236-
"$ [bold green]python3 grader.py -l | source[/bold green]"
3237-
)
3238-
else:
3239-
console.print(
3240-
'$ [bold green]eval "$(python3 grader.py -l)"[/bold green]'
3241-
)
3242-
else:
3243-
shell_type = args.shell or get_current_shell()
3244-
print(
3245-
"\nTo set TEST_BUILD to the first failed test case's build directory, run:"
3246-
)
3247-
if shell_type == "fish":
3248-
print("python3 grader.py -l | source")
3249-
else:
3250-
print('eval "$(python3 grader.py -l)"')
3251-
3252-
# 只要不是0分就通过
3253-
sys.exit(0 if percentage > 0 else 1)
3254-
3232+
# 根据不同shell类型生成相应的命令
3233+
if shell_type == "fish":
3234+
print(f"set -x TEST_BUILD {first_failed_test['build_path']}")
3235+
else: # bash 或 zsh
3236+
print(f"export TEST_BUILD={first_failed_test['build_path']}")
3237+
3238+
sys.exit(0)
3239+
32553240
except Exception as e:
32563241
print(f"Error reading test history: {str(e)}", file=sys.stderr)
32573242
sys.exit(1)
@@ -3271,11 +3256,27 @@ def main():
32713256
verbose=args.verbose,
32723257
generate_vscode=args.vscode,
32733258
vscode_no_merge=args.vscode_no_merge,
3274-
compare=args.compare, # 传递对拍参数
3275-
)
3276-
total_score, max_score = grader.run_all_tests(
3277-
args.test, prefix_match=args.prefix, group=args.group
3259+
compare=args.compare,
32783260
)
3261+
3262+
if args.rerun_failed:
3263+
try:
3264+
failed_tests = get_last_failed_tests(".test_history")
3265+
if not failed_tests:
3266+
print("No failed test found in last run", file=sys.stderr)
3267+
sys.exit(1)
3268+
3269+
failed_paths = [Path(test["path"]) for test in failed_tests]
3270+
total_score, max_score = grader.run_all_tests(specific_paths=failed_paths)
3271+
3272+
except Exception as e:
3273+
print(f"Error reading test history: {str(e)}", file=sys.stderr)
3274+
sys.exit(1)
3275+
3276+
else:
3277+
total_score, max_score = grader.run_all_tests(
3278+
args.test, prefix_match=args.prefix, group=args.group
3279+
)
32793280

32803281
# 如果是dry-run模式,直接退出
32813282
if args.dry_run:
@@ -3289,32 +3290,21 @@ def main():
32893290
f.write(f"{percentage:.2f}")
32903291

32913292
# 如果有测试点失败,输出提示信息
3292-
if total_score < max_score:
3293-
if not args.json:
3294-
console = Console()
3295-
shell_type = args.shell or get_current_shell()
3293+
if total_score < max_score and not args.json and grader.config.debug_config.get("show_test_build_hint", True):
3294+
shell_type = args.shell or get_current_shell()
32963295

3297-
console.print(
3298-
"\n[bold yellow]To set TEST_BUILD environment variable to the failed test case's build directory:[/bold yellow]"
3299-
)
3296+
grader.console.print(
3297+
"\n[bold yellow]To set TEST_BUILD environment variable to the failed test case's build directory:[/bold yellow]"
3298+
)
33003299

3301-
if shell_type == "fish":
3302-
console.print(
3303-
"$ [bold green]python3 grader.py -l | source[/bold green]"
3304-
)
3305-
else:
3306-
console.print(
3307-
'$ [bold green]eval "$(python3 grader.py -l)"[/bold green]'
3308-
)
3300+
if shell_type == "fish":
3301+
grader.console.print(
3302+
"$ [bold green]python3 grader.py -l | source[/bold green]"
3303+
)
33093304
else:
3310-
shell_type = args.shell or get_current_shell()
3311-
print(
3312-
"\nTo set TEST_BUILD to the first failed test case's build directory, run:"
3305+
grader.console.print(
3306+
'$ [bold green]eval "$(python3 grader.py -l)"[/bold green]'
33133307
)
3314-
if shell_type == "fish":
3315-
print("python3 grader.py -l | source")
3316-
else:
3317-
print('eval "$(python3 grader.py -l)"')
33183308

33193309
# 只要不是0分就通过
33203310
sys.exit(0 if percentage > 0 else 1)

grader_config.toml

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
[debug]
22
default_type = "cpp"
3+
show_test_build_hint = false
34

45
[setup]
56
# 在运行测试之前需要执行的准备步骤
@@ -14,25 +15,14 @@ message = "Preparing tsh..."
1415
success_message = "tsh compiled successfully"
1516
timeout = 60.0
1617

17-
# 可以添加更多setup步骤
18-
# [[setup.steps]]
19-
# name = "其他准备步骤"
20-
# type = "command" # 可以是普通命令
21-
# command = "./some_script.sh"
22-
# args = ["arg1", "arg2"]
23-
# required = false # 这一步失败不会终止测试
24-
2518
[paths]
26-
# 定义项目中重要路径的配置
2719
tests_dir = "tests"
2820
cases_dir = "tests/cases"
2921
common_dir = "tests/common"
3022

3123
[grader]
32-
# 评分器的全局配置
33-
default_timeout = 5.0 # 默认超时时间(秒)
24+
default_timeout = 5.0
3425

3526
[executables]
36-
# 预定义的可执行文件
3727
tsh = "${root_dir}/tsh"
3828
tshref = "${root_dir}/tshref"

0 commit comments

Comments
 (0)