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
158
159
160
161
162
163
164
165
166
|
#!/usr/bin/env python3
"""The logo splash: a greeting on attach, briefly, wearing a shader effect.
A scripted session never attaches, so these drive it through the control
API's `splash` verb -- the same call the server makes on MSG_HELLO. What is
asserted is the contract: the glyphs fly in and land exactly, centered,
temporary, colourful, ended early by input, and absent when splash_ms says
never. Assembly takes the first two fifths of splash_ms, which is why each
test picks a duration to suit what it is watching.
"""
import os
import sys
import tempfile
from harness import Session, check, report
SH = ["/bin/sh", "-c", 'printf "\\033]2;p\\007"; stty raw -echo; cat']
GLYPHS = "\u2584\u2588\u2588\u2588\u2588\u2588\u2588\u2584" # ▄██████▄, line one
BLOCKS = ("\u2584", "\u2588", "\u2580") # any glyph a particle could be
def flying(snap):
return any(b in snap.screen() for b in BLOCKS)
def cfg(text):
f = tempfile.NamedTemporaryFile("w", suffix=".kdl", delete=False)
f.write(text)
f.close()
return f.name
def test_the_splash_assembles_centered_and_goes_away():
conf = cfg("splash_ms 1600\n") # assembly done by 640ms, margin under load
with Session(SH, cols=80, rows=24, config=conf) as s:
s.settle(30)
check("no greeting until asked", not flying(s.snapshot()), "")
s.api("splash")
# The logo's first line contains its opening run three times over, and
# assembly is staggered, so a middle occurrence can land before the
# leftmost -- under load, whole checks before it. "Landed" is therefore
# all three occurrences present (the row in its final form), with the
# first block glyph where the match begins so a particle still passing
# to its left keeps us waiting rather than shifting the answer.
def landed_row(sn):
for i, line in enumerate(sn.text):
if line.count(GLYPHS) == 3 and line.index(GLYPHS) == line.index(
"\u2584"
):
return i
return None
snap = s.until(lambda sn: landed_row(sn) is not None, timeout_ms=6000)
y = landed_row(snap)
check("the glyphs land as the logo", y is not None, snap.screen()[:200])
# Centered: the logo is 44 columns in an 80-column screen, so its
# first glyph lands at (80 - 44) / 2 give or take the box margin.
x = snap.text[y].index(GLYPHS) if y is not None else -1
check("h-centered", 14 <= x <= 22, f"x={x}")
check("v-centered", y is not None and 6 <= y <= 12, f"y={y}")
snap = s.until(lambda sn: not flying(sn))
check("and it goes away by itself", not flying(snap), "")
os.unlink(conf)
def test_every_motion_lands_exactly():
"""Five ways to fly in, one place to land: whatever the motion, assembly
ends with every particle on its cell -- an almost-assembled logo reads as
a bug, which is what the per-particle delay rescaling exists to prevent."""
conf = cfg("splash_ms 1600\n") # landed window: 960ms, wide enough under load
with Session(SH, cols=80, rows=24, config=conf) as s:
s.settle(30)
for motion in range(5):
s.api("splash", fx=0, motion=motion)
# Landed means the line exists *and* nothing is still passing to
# its left: the first block glyph on that row is the logo's own.
landed = lambda sn: any(
GLYPHS in line and line.index("\u2584") == 18 for line in sn.text
)
snap = s.until(landed, timeout_ms=6000)
check(
f"motion {motion} lands the logo, exactly placed",
landed(snap),
snap.screen()[:200],
)
s.until(lambda sn: not flying(sn), timeout_ms=6000) # expire between rounds
os.unlink(conf)
def test_the_splash_is_colourful_not_a_tinted_logo():
"""The first version painted every effect in the theme accent, which made
six effects into one dull one. An effect is a chain of vivid tints now, so
the logo must show genuinely different *hues* -- counted as dominant-
channel families, because two hundred shades of the same blue is exactly
the failure this guards against."""
conf = cfg("splash_ms 2500\n") # assembled by 1000ms, coloured until 2500
with Session(SH, cols=80, rows=24, config=conf) as s:
s.settle(30)
s.api("splash", fx=0) # spectrum: deterministic, and the loudest
snap = s.until(lambda sn: GLYPHS in sn.screen(), timeout_ms=6000)
rows = [i for i, line in enumerate(snap.text) if GLYPHS in line]
families = set()
for y in rows:
for x in range(16, 64):
st = snap.style_at(x, y)
fg = (st or {}).get("fg")
if not fg or fg == "#000000":
continue
r, g, b = int(fg[1:3], 16), int(fg[3:5], 16), int(fg[5:7], 16)
families.add((r > g + 32, g > b + 32, r > b + 32))
check(
"the logo wears several hues at once",
len(families) >= 3,
f"{len(families)} families: {sorted(families)}",
)
os.unlink(conf)
def test_any_key_ends_it_mid_flight_and_still_lands():
conf = cfg("splash_ms 10000\n") # long enough that expiry cannot pass this
with Session(SH, cols=80, rows=24, config=conf) as s:
s.settle(30)
s.api("splash")
snap = s.until(flying)
check("particles are in the air", flying(snap), "")
s.send("marker") # plain keys: dismiss the greeting, reach the shell
snap = s.until(lambda sn: not flying(sn))
check("a keystroke puts the whole flight away", not flying(snap), "")
snap = s.until_text("marker")
check(
"...and still reaches the pane it was aimed at",
"marker" in snap.screen(),
snap.screen()[:200],
)
os.unlink(conf)
def test_splash_ms_zero_means_never():
conf = cfg("splash_ms 0\n")
with Session(SH, cols=80, rows=24, config=conf) as s:
s.settle(30)
s.api("splash")
s.settle(30)
check("nothing appears", not flying(s.snapshot()), "")
os.unlink(conf)
def test_a_screen_too_small_gets_no_greeting():
"""Cropped branding is worse than none: the box needs its 48 columns."""
conf = cfg("splash_ms 10000\n")
with Session(SH, cols=40, rows=12, config=conf) as s:
s.settle(30)
s.api("splash")
s.settle(30)
check("no logo on 40 columns", not flying(s.snapshot()), "")
os.unlink(conf)
for name, fn in sorted(list(globals().items())):
if name.startswith("test_"):
fn()
sys.exit(report())
|