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
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
|
## The keypad logic is pure Nim, so it can be driven without a window:
## press keys straight into onPress and read back what the display would say.
import std/unittest
import ../examples/calculator
proc press(c: var Calc; ids: varargs[int32]): string =
for id in ids:
onPress(addr c, id)
c.display
proc fresh(): Calc = newCalc()
suite "calculator":
test "digits accumulate into one entry":
var c = fresh()
check c.press(1, 2, 3) == "123"
test "no leading zeros":
var c = fresh()
check c.press(0, 0, 5) == "5"
test "addition":
var c = fresh()
check c.press(2, IdAdd, 3, IdEquals) == "5"
test "chaining resolves the pending op as you go":
var c = fresh()
check c.press(2, IdAdd, 3, IdMul, 4) == "4" # 2+3 folded, 4 being typed
check c.press(IdEquals) == "20" # then (5)*4
test "subtraction and division":
var c = fresh()
check c.press(9, IdSub, 4, IdEquals) == "5"
c = fresh()
check c.press(8, IdDiv, 2, IdEquals) == "4"
test "division by zero errors, and only C recovers":
var c = fresh()
check c.press(1, IdDiv, 0, IdEquals) == "Error"
check c.reason == "Cannot divide by zero"
check c.press(5) == "Error" # other keys are ignored
check c.reason == "Cannot divide by zero"
check c.press(IdClear) == "0"
check c.reason == "" # cleared along with the error
check c.press(7) == "7" # usable again
test "an operator commits the pending divide, so it errors there too":
# The error surfaces on the key that commits the division, which is the
# next operator just as much as it is equals.
var c = fresh()
check c.press(1, IdDiv, 0, IdAdd) == "Error"
check c.reason == "Cannot divide by zero"
test "pressing divide is not itself an error":
var c = fresh()
check c.press(1, IdDiv) == "1"
check not c.failed
test "dividing by an implied zero operand is caught":
var c = fresh()
check c.press(0, IdDiv, IdEquals) == "Error" # 0 / 0
c = fresh()
check c.press(0, IdSign, 1, IdDiv, 0, IdSign, IdEquals) == "Error" # -0
test "a result too large to represent says so, rather than showing inf":
var c = fresh()
# Each pass multiplies by ~1e18; float64 gives up a little past 1e308.
discard c.press(9, IdMul, 9, IdEquals)
for _ in 1 .. 20:
discard c.press(IdMul, 9, 9, 9, 9, 9, 9, 9, 9, 9, IdEquals)
discard c.press(IdMul, 9, 9, 9, 9, 9, 9, 9, 9, 9, IdEquals)
check c.display == "Error"
check c.reason == "Result is out of range"
test "a healthy calculation has no reason":
var c = fresh()
check c.press(2, IdAdd, 2, IdEquals) == "4"
check c.reason == ""
check not c.failed
test "decimals":
var c = fresh()
check c.press(1, IdDot, 5, IdAdd, 2, IdDot, 2, 5, IdEquals) == "3.75"
test "a bare dot starts at zero":
var c = fresh()
check c.press(IdDot, 5) == "0.5"
test "one dot per number":
var c = fresh()
check c.press(1, IdDot, 2, IdDot, 3) == "1.23"
test "sign flips the entry, then the result":
var c = fresh()
check c.press(4, IdSign) == "-4"
check c.press(IdSign) == "4"
c = fresh()
check c.press(2, IdAdd, 3, IdEquals, IdSign) == "-5"
test "percent":
var c = fresh()
check c.press(5, 0, IdPercent) == "0.5"
test "integers do not grow a decimal point":
var c = fresh()
check c.press(6, IdDiv, 3, IdEquals) == "2"
test "backspace rubs out typed digits only":
var c = fresh()
check c.press(1, 2, 3, IdBackspace) == "12"
check c.press(IdBackspace, IdBackspace) == "0" # empty entry shows acc
c = fresh()
check c.press(2, IdAdd, 3, IdEquals) == "5"
check c.press(IdBackspace) == "5" # a result is not an entry
test "a new number replaces the result, not appends to it":
var c = fresh()
check c.press(2, IdAdd, 3, IdEquals) == "5"
check c.press(7) == "7"
suite "keyboard":
proc typed(c: var Calc; keys: varargs[string]): string =
for k in keys:
let id = keyToId(k)
if id >= 0: onPress(addr c, id)
c.display
test "digits and operators come from the characters you type":
var c = fresh()
check c.typed("1", "2", "+", "3", "Enter") == "15"
test "both spellings of an operator work":
var c = fresh()
check c.typed("8", "/", "2", "=") == "4"
c = fresh()
check c.typed("8", "\u00f7", "2", "=") == "4" # the divide sign on the key
c = fresh()
check c.typed("3", "*", "4", "=") == "12"
c = fresh()
check c.typed("3", "\u00d7", "4", "=") == "12"
test "escape and c both clear":
var c = fresh()
check c.typed("9", "Escape") == "0"
check c.typed("9", "c") == "0"
test "backspace is wired to the key":
var c = fresh()
check c.typed("1", "2", "3", "Backspace") == "12"
test "unmapped keys are ignored, not mistaken for a digit":
var c = fresh()
check keyToId("F1") == -1
check keyToId("ArrowUp") == -1
check keyToId("q") == -1
check c.typed("5", "F1", "ArrowUp", "q") == "5"
|