-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest_debug.py
More file actions
139 lines (109 loc) · 5.53 KB
/
Copy pathtest_debug.py
File metadata and controls
139 lines (109 loc) · 5.53 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
"""The debugger, driven by a script the way a person drives it by hand.
Nothing here calls into the interpreter directly: the commands go in on stdin
and the transcript comes out on stdout. A debugger that only works behind an
interactive prompt cannot be tested, and an untested debugger misleads at
exactly the moment someone is leaning on it.
Run: python3 test_debug.py
"""
import os
import subprocess
import sys
import tempfile
ZERDALI = os.path.join(os.path.dirname(os.path.abspath(__file__)), "zerdali.py")
PROG = """effect io { print(msg: Str@public) -> Unit }
fn double(x: Int) -> Int { x * 2 }
fn add(a: Int, b: Int) -> Int { a + b }
fn main() -> Int !{io.print} {
let one: Int = double(3);
let two: Int = add(one, 4);
io.print("sum " + show(two));
two
}
"""
def debug(path: str, commands: str, *args) -> str:
run = subprocess.run(
[sys.executable, ZERDALI, "debug", path, *args],
input=commands, capture_output=True, text=True, timeout=300,
)
return run.stdout + run.stderr
def main() -> int:
failed = 0
tmp = tempfile.mkdtemp(prefix="zddbg")
path = os.path.join(tmp, "prog.zd")
open(path, "w", encoding="utf-8").write(PROG)
def check(name: str, ok: bool, got: str):
nonlocal failed
if ok:
print(f"ok {name}")
else:
print(f"FAIL {name}\n{got[:400]}")
failed += 1
# -- a breakpoint on a function stops inside it -------------------------
out = debug(path, "continue\n", "--break", "double")
check("break: stops in the named function",
"stopped double at" in out and "=> 10" in out, out)
# -- the stack is the call chain, innermost first -----------------------
out = debug(path, "where\ncontinue\n", "--break", "double")
check("where: innermost frame first, with the caller under it",
"#0 double" in out and "#1 main" in out, out)
# -- locals are the frame's, not the program's --------------------------
out = debug(path, "locals\ncontinue\n", "--break", "double")
check("locals: the parameter of the frame that is stopped",
"x = 3" in out and "one" not in out.split("stopped")[1], out)
# -- a line breakpoint --------------------------------------------------
out = debug(path, "locals\ncontinue\n", "--break", "9")
check("break: a line number stops on that line",
"at 9:" in out and "one = 6" in out, out)
# -- effects are a view of their own ------------------------------------
#
# The one thing a print statement cannot give you: what this program has
# reached out and done, in order, at the moment it is stopped.
out = debug(path, "effects\ncontinue\n", "--break", "double")
check("effects: nothing performed yet is said, not shown as blank",
"(none performed yet)" in out, out)
# Line 11 is after the `io.print` on line 10, and the stop happens *before*
# the expression it names is evaluated -- so 10 would be too early. That
# ordering is the whole reason to break on the line after the one you care
# about, and it is worth a test rather than a surprise.
out = debug(path, "continue\neffects\ncontinue\n", "--break", "double,11")
check("effects: what has been performed by the time it is asked",
"io.print" in out.split("(none")[0] or "io.print" in out, out)
# -- stepping -----------------------------------------------------------
out = debug(path, "step\nstep\nwhere\ncontinue\n", "--break", "double")
check("step: goes on to the next line and keeps the frame honest",
out.count("stopped") >= 3, out)
# -- `next` does not descend into a call --------------------------------
out = debug(path, "next\nwhere\ncontinue\n", "--break", "9")
check("next: stays in the frame it started in",
"#0 main" in out.split("stopped")[2], out)
# -- quitting is a runtime error, not a silent success ------------------
out = debug(path, "quit\n", "--break", "double")
check("quit: says the debugger stopped it, and does not print a result",
"stopped by the debugger" in out and "=> 10" not in out, out)
# -- an unknown command lists the real ones -----------------------------
out = debug(path, "frobnicate\ncontinue\n", "--break", "double")
check("unknown command: refused, and the session goes on",
"no such command" in out and "=> 10" in out, out)
# -- the script running out lets the program finish ---------------------
out = debug(path, "", "--break", "double")
check("end of script: the program runs to completion rather than hanging",
"=> 10" in out, out)
# -- with no breakpoint, it steps from the first line -------------------
out = debug(path, "continue\n")
check("no breakpoint: stops once at the start, then runs",
"stopped main at" in out and "=> 10" in out, out)
# -- a program that does not check is refused ---------------------------
bad = os.path.join(tmp, "bad.zd")
open(bad, "w", encoding="utf-8").write("fn main() -> Int { \"s\" }\n")
out = debug(bad, "continue\n")
check("refuses a program that does not check",
"refusing to debug" in out, out)
# -- the summary counts both axes ---------------------------------------
out = debug(path, "continue\n", "--break", "double")
check("summary: how many stops, and how many effects",
"stopped 1 time(s); performed 1 effect(s)" in out, out)
total = 14
print(f"\n{total - failed}/{total} debugger behaviours hold")
return 1 if failed else 0
if __name__ == "__main__":
sys.exit(main())