Bug triage
Two findings with one shared cause patched where each was seen, one report with two causes patched to its own inputs, and a backlog that outruns its fixes worked in arrival order and worst first.
Every screen below was recorded under CPython. When this page was built, the EML interpreter replayed each session from the same input and printed the same bytes.
About
Three ways a bug report misleads the people who fix it: two findings with one cause, one report with two, and a backlog that grows faster than it is fixed.
main.eml- the menu, the two findings, the one report, and the backlog worked two waystriage.eml- the calendar screens and the date parser they share, the states as fixes go in, the balance as shown and the fixes for it, the findings in the backlog, and working them
How each part works:
- As in the corpus case
two-findings-one-defect, the weekly report starts a day late and reminders come a day late. Each candidate cause is run alone: the shared date parser makes the report, the reminder and the audit log wrong; each screen's own code only itself. Only the parser explains both findings, and it predicts a third nobody filed. Patched where each was seen, both findings close and the audit log stays wrong; fix the parser later and the two patches make their screens wrong again. - As in the corpus case
one-report-two-mechanisms, one report of three wrong balances - -4, 150, -7 - has two causes: the sign dropped and anything past 99 clamped. Special-casing the reported inputs makes the report pass and leaves 4 of 12 inputs wrong, each explained by one mechanism run alone; fixing both mechanisms leaves none. - As in the corpus case
finding-outruns-fixing, two findings arrive in each of five periods. Fixing one a period, the queue never empties, and working the worst first instead of in arrival order cuts the harm from 127 to 71 with the same fixes; fixing two a period, the queue empties and the order changes nothing.
What is checked: menu choices 1 to 4; fixes each period 1 or 2. An empty answer cancels.
Sessions: sessions/basic.in gives: - the two findings and the one report; - the backlog at one fix a period and at two.
sessions/bad-input.in gives: - menu choices 0 and x; - fixes each period: a cancel, 3 and x; then the one report.
Built on the verified corpus cases two-findings-one-defect (two symptoms patched locally over one shared cause), one-report-two-mechanisms (a patch fitted to the reported inputs) and finding-outruns-fixing (the same fixing capacity in arrival order and by harm).
Recorded sessions
What the screen shows while someone uses the program. Each typed line appears after its prompt, the way a terminal shows it.
bad-input
interpreter: byte-equal== Bug triage ==
Two findings with one cause, one report with two, and a backlog that outruns its fixes.
1) two findings 2) one report 3) the backlog 4) quit
choice> 0
Pick a number from 1 to 4.
1) two findings 2) one report 3) the backlog 4) quit
choice> x
Pick a number from 1 to 4.
1) two findings 2) one report 3) the backlog 4) quit
choice> 3
fixes each period (1 or 2)>
Cancelled.
1) two findings 2) one report 3) the backlog 4) quit
choice> 3
fixes each period (1 or 2)> 3
Type 1 or 2.
1) two findings 2) one report 3) the backlog 4) quit
choice> 3
fixes each period (1 or 2)> x
Type 1 or 2.
1) two findings 2) one report 3) the backlog 4) quit
choice> 2
One report, three wrong balances:
-4 shows 4, should be -4
150 shows 99, should be 150
-7 shows 7, should be -7
Over the 12 inputs -9, -4, 0, 3, 42, 99, 100, 150, 260, -7, -1, 77:
fix wrong
none 7 -9, -4, 100, 150, 260, -7, -1
the reported inputs 4 -9, 100, 260, -1
keep the sign 3 100, 150, 260
no clamp at 99 4 -9, -4, -7, -1
both 0 none
Fitted to the report, the report passes and 4 failures remain; each mechanism run alone accounts for its share:
the sign dropped: -9, -1
past 99 clamped: 100, 260
The report could not show that two of its rows came from one cause and the third from another.
1) two findings 2) one report 3) the backlog 4) quit
choice> 4
Bye.
What was typed (10 lines)
0
x
3
3
3
3
x
2
4
basic
interpreter: byte-equal== Bug triage ==
Two findings with one cause, one report with two, and a backlog that outruns its fixes.
1) two findings 2) one report 3) the backlog 4) quit
choice> 1
Two findings, filed a week apart: the weekly report starts a day late; reminders come a day late.
Each candidate cause alone, and the screens it makes wrong on the days 3, 10, 17, 24 and 31:
the date parser: report, reminder, audit
the report's code: report
the reminder's code: reminder
Only the parser explains both findings, and it predicts a third nobody filed: the audit log.
Days right of 5, as the fixes go in:
report reminder audit
as shipped 0 0 0
patched where seen 5 5 0
parser fixed 5 5 5
parser fixed, patches left 0 0 5
Patched where seen, both findings close and the audit log stays wrong; fix the parser later and the two patches make their screens wrong again.
1) two findings 2) one report 3) the backlog 4) quit
choice> 2
One report, three wrong balances:
-4 shows 4, should be -4
150 shows 99, should be 150
-7 shows 7, should be -7
Over the 12 inputs -9, -4, 0, 3, 42, 99, 100, 150, 260, -7, -1, 77:
fix wrong
none 7 -9, -4, 100, 150, 260, -7, -1
the reported inputs 4 -9, 100, 260, -1
keep the sign 3 100, 150, 260
no clamp at 99 4 -9, -4, -7, -1
both 0 none
Fitted to the report, the report passes and 4 failures remain; each mechanism run alone accounts for its share:
the sign dropped: -9, -1
past 99 clamped: 100, 260
The report could not show that two of its rows came from one cause and the third from another.
1) two findings 2) one report 3) the backlog 4) quit
choice> 3
fixes each period (1 or 2)> 1
Two findings arrive in each of periods 1 to 5; 1 fixed a period; the harm each does every period it waits is summed over 6 periods.
in arrival order: harm 127; still open: f7, f8, f9, f10
worst first: harm 71; still open: f1, f3, f5, f9
The same number fixed each period, and the order changes the harm by 56: once findings outrun fixes, the order is not a preference.
1) two findings 2) one report 3) the backlog 4) quit
choice> 3
fixes each period (1 or 2)> 2
Two findings arrive in each of periods 1 to 5; 2 fixed a period; the harm each does every period it waits is summed over 6 periods.
in arrival order: harm 48; still open: none
worst first: harm 48; still open: none
The order changes nothing here.
With fixes keeping up, the queue empties either way.
1) two findings 2) one report 3) the backlog 4) quit
choice> 4
Bye.
What was typed (7 lines)
1
2
3
1
3
2
4
Modules
The program as written, entry module first. Each module transpiles to its own Python file, which is what eml project run executes.
main.eml(entry)
eml# P158 bug triage: two findings with one cause, one report with two causes,
# and a backlog that grows faster than it is fixed.
import triage
def plain(s):
0 => i
len(s) => j
while i < j and s[i] == " ":
i + 1 => i
while j > i and s[j - 1] == " ":
j - 1 => j
return s[i:j]
def pad(s, n):
s => out
while len(out) < n:
out + " " => out
return out
def lpad(s, n):
s => out
while len(out) < n:
" " + out => out
return out
def listed(xs):
"" => out
for x in xs:
if out == "":
str(x) => out
else:
out + ", " + str(x) => out
if out == "":
return "none"
return out
def wrong_screens(parser_bug, report_bug, reminder_bug):
[] => out
for d in triage.days():
if triage.report(d, parser_bug, report_bug) != d % 7:
out + ["report"] => out
if triage.reminder(d, parser_bug, reminder_bug) != d % 7:
out + ["reminder"] => out
if triage.audit(d, parser_bug) != d % 7:
out + ["audit"] => out
[] => names
for s in ["report", "reminder", "audit"]:
for x in out:
if x == s:
False => have
for n in names:
if n == s:
True => have
if not have:
names + [s] => names
return names
def two_findings():
"Two findings, filed a week apart: the weekly report starts a day late; reminders come a day late." ^0
"Each candidate cause alone, and the screens it makes wrong on the days 3, 10, 17, 24 and 31:" ^0
(" the date parser: " + listed(wrong_screens(True, False, False))) ^0
(" the report's code: " + listed(wrong_screens(False, True, False))) ^0
(" the reminder's code: " + listed(wrong_screens(False, False, True))) ^0
"Only the parser explains both findings, and it predicts a third nobody filed: the audit log." ^0
"Days right of 5, as the fixes go in:" ^0
(" " + pad("", 28) + "report reminder audit") ^0
for st in triage.states():
(" " + pad(st[0], 28) + lpad(str(triage.right_days("report", st[1], st[2])), 6) + lpad(str(triage.right_days("reminder", st[1], st[2])), 10) + lpad(str(triage.right_days("audit", st[1], st[2])), 7)) ^0
"Patched where seen, both findings close and the audit log stays wrong; fix the parser later and the two patches make their screens wrong again." ^0
def one_report():
"One report, three wrong balances:" ^0
for r in triage.reported():
(" " + lpad(str(r), 5) + " shows " + lpad(triage.render(r, True, True), 4) + ", should be " + triage.truth(r)) ^0
("Over the " + str(len(triage.inputs())) + " inputs " + listed(triage.inputs()) + ":") ^0
(" " + pad("fix", 22) + "wrong") ^0
for fx in ["none", "fitted", "sign", "clamp", "both"]:
[] => bad
for v in triage.inputs():
if triage.shown(v, fx) != triage.truth(v):
bad + [v] => bad
if fx == "fitted":
"the reported inputs" => name
elif fx == "sign":
"keep the sign" => name
elif fx == "clamp":
"no clamp at 99" => name
else:
fx => name
(" " + pad(name, 22) + pad(str(len(bad)), 4) + listed(bad)) ^0
[] => s
[] => c
0 => left
for v in triage.inputs():
if triage.shown(v, "fitted") != triage.truth(v):
left + 1 => left
if triage.render(v, True, False) != triage.truth(v):
s + [v] => s
if triage.render(v, False, True) != triage.truth(v):
c + [v] => c
("Fitted to the report, the report passes and " + str(left) + " failures remain; each mechanism run alone accounts for its share:") ^0
(" the sign dropped: " + listed(s)) ^0
(" past 99 clamped: " + listed(c)) ^0
"The report could not show that two of its rows came from one cause and the third from another." ^0
def backlog():
plain(input("fixes each period (1 or 2)> ")) => t
if t == "":
"Cancelled." ^0
return
if t != "1" and t != "2":
"Type 1 or 2." ^0
return
int(t) => cap
("Two findings arrive in each of periods 1 to 5; " + t + " fixed a period; the harm each does every period it waits is summed over 6 periods.") ^0
triage.work("fifo", cap, 6) => a
triage.work("harm", cap, 6) => b
(" in arrival order: harm " + lpad(str(a[0]), 3) + "; still open: " + listed(a[2])) ^0
(" worst first: harm " + lpad(str(b[0]), 3) + "; still open: " + listed(b[2])) ^0
if a[0] != b[0]:
("The same number fixed each period, and the order changes the harm by " + str(a[0] - b[0]) + ": once findings outrun fixes, the order is not a preference.") ^0
else:
"The order changes nothing here." ^0
if len(a[2]) == 0 and len(b[2]) == 0:
"With fixes keeping up, the queue empties either way." ^0
"== Bug triage ==" ^0
"Two findings with one cause, one report with two, and a backlog that outruns its fixes." ^0
True => running
while running:
"" ^0
"1) two findings 2) one report 3) the backlog 4) quit" ^0
plain(input("choice> ")) => choice
if choice == "1":
two_findings()
elif choice == "2":
one_report()
elif choice == "3":
backlog()
elif choice == "4":
False => running
else:
"Pick a number from 1 to 4." ^0
"Bye." ^0
Python projection (main.py)
import triage
def plain(s):
i = 0
j = len(s)
while i < j and s[i] == " ":
i = i + 1
while j > i and s[j - 1] == " ":
j = j - 1
return s[i:j]
def pad(s, n):
out = s
while len(out) < n:
out = out + " "
return out
def lpad(s, n):
out = s
while len(out) < n:
out = " " + out
return out
def listed(xs):
out = ""
for x in xs:
if out == "":
out = str(x)
else:
out = out + ", " + str(x)
if out == "":
return "none"
return out
def wrong_screens(parser_bug, report_bug, reminder_bug):
out = []
for d in triage.days():
if triage.report(d, parser_bug, report_bug) != d % 7:
out = out + ["report"]
if triage.reminder(d, parser_bug, reminder_bug) != d % 7:
out = out + ["reminder"]
if triage.audit(d, parser_bug) != d % 7:
out = out + ["audit"]
names = []
for s in ["report", "reminder", "audit"]:
for x in out:
if x == s:
have = False
for n in names:
if n == s:
have = True
if not have:
names = names + [s]
return names
def two_findings():
print("Two findings, filed a week apart: the weekly report starts a day late; reminders come a day late.")
print("Each candidate cause alone, and the screens it makes wrong on the days 3, 10, 17, 24 and 31:")
print(" the date parser: " + listed(wrong_screens(True, False, False)))
print(" the report's code: " + listed(wrong_screens(False, True, False)))
print(" the reminder's code: " + listed(wrong_screens(False, False, True)))
print("Only the parser explains both findings, and it predicts a third nobody filed: the audit log.")
print("Days right of 5, as the fixes go in:")
print(" " + pad("", 28) + "report reminder audit")
for st in triage.states():
print(" " + pad(st[0], 28) + lpad(str(triage.right_days("report", st[1], st[2])), 6) + lpad(str(triage.right_days("reminder", st[1], st[2])), 10) + lpad(str(triage.right_days("audit", st[1], st[2])), 7))
print("Patched where seen, both findings close and the audit log stays wrong; fix the parser later and the two patches make their screens wrong again.")
def one_report():
print("One report, three wrong balances:")
for r in triage.reported():
print(" " + lpad(str(r), 5) + " shows " + lpad(triage.render(r, True, True), 4) + ", should be " + triage.truth(r))
print("Over the " + str(len(triage.inputs())) + " inputs " + listed(triage.inputs()) + ":")
print(" " + pad("fix", 22) + "wrong")
for fx in ["none", "fitted", "sign", "clamp", "both"]:
bad = []
for v in triage.inputs():
if triage.shown(v, fx) != triage.truth(v):
bad = bad + [v]
if fx == "fitted":
name = "the reported inputs"
elif fx == "sign":
name = "keep the sign"
elif fx == "clamp":
name = "no clamp at 99"
else:
name = fx
print(" " + pad(name, 22) + pad(str(len(bad)), 4) + listed(bad))
s = []
c = []
left = 0
for v in triage.inputs():
if triage.shown(v, "fitted") != triage.truth(v):
left = left + 1
if triage.render(v, True, False) != triage.truth(v):
s = s + [v]
if triage.render(v, False, True) != triage.truth(v):
c = c + [v]
print("Fitted to the report, the report passes and " + str(left) + " failures remain; each mechanism run alone accounts for its share:")
print(" the sign dropped: " + listed(s))
print(" past 99 clamped: " + listed(c))
print("The report could not show that two of its rows came from one cause and the third from another.")
def backlog():
t = plain(input("fixes each period (1 or 2)> "))
if t == "":
print("Cancelled.")
return
if t != "1" and t != "2":
print("Type 1 or 2.")
return
cap = int(t)
print("Two findings arrive in each of periods 1 to 5; " + t + " fixed a period; the harm each does every period it waits is summed over 6 periods.")
a = triage.work("fifo", cap, 6)
b = triage.work("harm", cap, 6)
print(" in arrival order: harm " + lpad(str(a[0]), 3) + "; still open: " + listed(a[2]))
print(" worst first: harm " + lpad(str(b[0]), 3) + "; still open: " + listed(b[2]))
if a[0] != b[0]:
print("The same number fixed each period, and the order changes the harm by " + str(a[0] - b[0]) + ": once findings outrun fixes, the order is not a preference.")
else:
print("The order changes nothing here.")
if len(a[2]) == 0 and len(b[2]) == 0:
print("With fixes keeping up, the queue empties either way.")
print("== Bug triage ==")
print("Two findings with one cause, one report with two, and a backlog that outruns its fixes.")
running = True
while running:
print("")
print("1) two findings 2) one report 3) the backlog 4) quit")
choice = plain(input("choice> "))
if choice == "1":
two_findings()
elif choice == "2":
one_report()
elif choice == "3":
backlog()
elif choice == "4":
running = False
else:
print("Pick a number from 1 to 4.")
print("Bye.")
triage.eml
eml# P158 bug triage - three ways a bug report misleads the people who fix it.
# As in the corpus case two-findings-one-defect, two findings on two
# screens, a report a day late and a reminder a day late, come from one
# shared date parser; patched where each was seen, both go quiet, a third
# consumer stays wrong, and fixing the parser later breaks the two patches.
# As in the corpus case one-report-two-mechanisms, one report of three wrong
# outputs has two causes underneath, and a patch fitted to the reported
# inputs fixes the report and neither cause. As in the corpus case
# finding-outruns-fixing, once findings arrive faster than they are fixed
# the queue never empties, and the order it is worked in decides the harm.
def days():
return [3, 10, 17, 24, 31]
def weekday(day, parser_bug):
if parser_bug:
return (day + 1) % 7
return day % 7
def report(day, parser_bug, patched):
# the first weekday of the weekly report; the local patch takes a day off
weekday(day, parser_bug) => w
if patched:
return (w + 6) % 7
return w
def reminder(day, parser_bug, patched):
weekday(day, parser_bug) => w
if patched:
return (w + 6) % 7
return w
def audit(day, parser_bug):
return weekday(day, parser_bug)
def states():
# [name, parser bug, local patches]
return [["as shipped", True, False], ["patched where seen", True, True], ["parser fixed", False, False], ["parser fixed, patches left", False, True]]
def right_days(screen, parser_bug, patched):
0 => n
for d in days():
if screen == "report":
report(d, parser_bug, patched) => v
elif screen == "reminder":
reminder(d, parser_bug, patched) => v
else:
audit(d, parser_bug) => v
if v == d % 7:
n + 1 => n
return n
def truth(v):
if v < 0:
return "-" + str(0 - v)
return str(v)
def render(v, sign_bug, clamp_bug):
# the balance as shown: the sign dropped, and anything past 99 clamped
v => x
if sign_bug and x < 0:
0 - x => x
if clamp_bug and x > 99:
99 => x
if x < 0:
return "-" + str(0 - x)
return str(x)
def reported():
return [0 - 4, 150, 0 - 7]
def inputs():
return [0 - 9, 0 - 4, 0, 3, 42, 99, 100, 150, 260, 0 - 7, 0 - 1, 77]
def shown(v, fix):
# the balance under a fix: none, fitted (the reported inputs special
# cased), sign, clamp, or both
if fix == "fitted":
for r in reported():
if r == v:
return truth(v)
return render(v, True, True)
return render(v, fix != "sign" and fix != "both", fix != "clamp" and fix != "both")
def findings():
# [name, period it arrives, harm each period it stays unfixed]
return [["f1", 1, 1], ["f2", 1, 9], ["f3", 2, 2], ["f4", 2, 8], ["f5", 3, 1], ["f6", 3, 7], ["f7", 4, 3], ["f8", 4, 6], ["f9", 5, 1], ["f10", 5, 10]]
def work(policy, capacity, periods):
# [harm in all, fixed in order, still waiting]; each period the arrivals
# join, every waiting finding does its harm, then capacity are fixed
[] => waiting
[] => fixed
0 => harm
1 => p
while p <= periods:
for f in findings():
if f[1] == p:
waiting + [f] => waiting
for f in waiting:
harm + f[2] => harm
0 => k
while k < capacity and len(waiting) > 0:
0 => pick
if policy == "harm":
0 => i
for f in waiting:
if f[2] > waiting[pick][2]:
i => pick
i + 1 => i
fixed + [waiting[pick][0]] => fixed
[] => rest
0 => i
for f in waiting:
if i != pick:
rest + [f] => rest
i + 1 => i
rest => waiting
k + 1 => k
p + 1 => p
[] => names
for f in waiting:
names + [f[0]] => names
return [harm, fixed, names]
Python projection (triage.py)
def days():
return [3, 10, 17, 24, 31]
def weekday(day, parser_bug):
if parser_bug:
return (day + 1) % 7
return day % 7
def report(day, parser_bug, patched):
w = weekday(day, parser_bug)
if patched:
return (w + 6) % 7
return w
def reminder(day, parser_bug, patched):
w = weekday(day, parser_bug)
if patched:
return (w + 6) % 7
return w
def audit(day, parser_bug):
return weekday(day, parser_bug)
def states():
return [["as shipped", True, False], ["patched where seen", True, True], ["parser fixed", False, False], ["parser fixed, patches left", False, True]]
def right_days(screen, parser_bug, patched):
n = 0
for d in days():
if screen == "report":
v = report(d, parser_bug, patched)
elif screen == "reminder":
v = reminder(d, parser_bug, patched)
else:
v = audit(d, parser_bug)
if v == d % 7:
n = n + 1
return n
def truth(v):
if v < 0:
return "-" + str(0 - v)
return str(v)
def render(v, sign_bug, clamp_bug):
x = v
if sign_bug and x < 0:
x = 0 - x
if clamp_bug and x > 99:
x = 99
if x < 0:
return "-" + str(0 - x)
return str(x)
def reported():
return [0 - 4, 150, 0 - 7]
def inputs():
return [0 - 9, 0 - 4, 0, 3, 42, 99, 100, 150, 260, 0 - 7, 0 - 1, 77]
def shown(v, fix):
if fix == "fitted":
for r in reported():
if r == v:
return truth(v)
return render(v, True, True)
return render(v, fix != "sign" and fix != "both", fix != "clamp" and fix != "both")
def findings():
return [["f1", 1, 1], ["f2", 1, 9], ["f3", 2, 2], ["f4", 2, 8], ["f5", 3, 1], ["f6", 3, 7], ["f7", 4, 3], ["f8", 4, 6], ["f9", 5, 1], ["f10", 5, 10]]
def work(policy, capacity, periods):
waiting = []
fixed = []
harm = 0
p = 1
while p <= periods:
for f in findings():
if f[1] == p:
waiting = waiting + [f]
for f in waiting:
harm = harm + f[2]
k = 0
while k < capacity and len(waiting) > 0:
pick = 0
if policy == "harm":
i = 0
for f in waiting:
if f[2] > waiting[pick][2]:
pick = i
i = i + 1
fixed = fixed + [waiting[pick][0]]
rest = []
i = 0
for f in waiting:
if i != pick:
rest = rest + [f]
i = i + 1
waiting = rest
k = k + 1
p = p + 1
names = []
for f in waiting:
names = names + [f[0]]
return [harm, fixed, names]