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
|
#!/usr/bin/env python3
"""The themes in contrib/.
Mostly a guard: a theme file that has fallen behind the config silently keeps
the compiled-in default for whatever it forgot, which looks like a bug in the
theme rather than a missing line in it.
"""
import glob
import os
import re
import subprocess
import sys
import tempfile
from harness import BIN, Session, check, report
SH = ["/bin/sh", "-c", 'printf "\\033]2;p\\007"; stty raw -echo; cat']
HERE = os.path.dirname(os.path.abspath(__file__))
THEMEDIR = os.path.join(HERE, "..", "contrib", "themes")
THEMES = sorted(glob.glob(os.path.join(THEMEDIR, "*.kdl")))
# Colours a theme is *not* expected to name. Two kinds, and both are the
# point of the feature rather than an oversight: the derived ones are mixed
# from colours the theme does define (so a theme gets them in its own family
# without a line), and the opt-in ones are unset on purpose, because a band
# behind a row takes a translucent terminal's own background away.
DERIVED = {
"tab_status_fg",
"tab_status_stripe",
"tab_status_busy",
"tab_status_spinner",
"tab_idle_bg",
}
OPT_IN = {"tab_status_bg", "attach_bg"}
# The scrollback search tints: one theme in contrib names them and the rest
# take the compiled-in pair, which reads the same on every palette here.
SHARED = {"search_fg", "search_bg", "search_cur_fg", "search_cur_bg"}
def declared_in_config():
"""Every colour the loader knows, from the table it walks.
Matched against `offsetof`, which is how the table is written. It used to
grep for `&c->` -- the spelling before the table moved to offsets -- and
matched nothing at all for however many months, so this guard was passing
by finding no colours to check. A guard that cannot fail is worse than no
guard, so the count is asserted too.
"""
src = open(os.path.join(HERE, "..", "src", "config.c")).read()
return set(re.findall(r'\{"([a-z_]+)", offsetof\(config_t', src))
def set_by(path):
return set(re.findall(r'^ ([a-z_]+) "', open(path).read(), re.M))
def test_there_are_themes():
check("contrib ships some", len(THEMES) >= 3, str(THEMES))
def test_the_guard_has_something_to_guard():
want = declared_in_config()
check("the colour table was found", len(want) > 40, str(len(want)))
def test_each_sets_every_colour_the_config_knows():
want = declared_in_config() - DERIVED - OPT_IN - SHARED
for t in THEMES:
missing = want - set_by(t)
check(
f"{os.path.basename(t)} sets all {len(want)} of them",
not missing,
"missing: " + ", ".join(sorted(missing)),
)
def test_default_is_the_compiled_palette_written_out():
"""`theme_name "default"` and naming no theme at all have to be the same
session -- that claim is the whole reason the file exists, and since the
config dump stopped writing colours, --dump-theme is what can check it."""
path = os.path.join(HERE, "..", "contrib", "themes", "default.kdl")
theirs = dict(
re.findall(r'^ ([a-z_]+) "(#[0-9a-fA-F]{6})"', open(path).read(), re.M)
)
env = dict(os.environ, SLOSH_CONFIG="/nonexistent/slosh.kdl")
dumped = subprocess.run(
[BIN, "--dump-theme"], capture_output=True, text=True, env=env
).stdout
ours = dict(re.findall(r'^ ([a-z_]+)\s+"(#[0-9a-fA-F]{6})"', dumped, re.M))
check("the dump has a palette in it", len(ours) > 40, str(len(ours)))
wrong = {k: (v, ours.get(k)) for k, v in theirs.items() if ours.get(k) != v}
check(
"default.kdl matches the compiled-in palette",
not wrong,
"; ".join(f"{k}: file {a}, code {b}" for k, (a, b) in sorted(wrong.items())),
)
def test_each_one_parses_and_reaches_the_screen():
for t in THEMES:
declared = dict(
re.findall(r'^ ([a-z_]+) "(#[0-9a-fA-F]{6})"', open(t).read(), re.M)
)
with Session(SH, cols=50, rows=10, config=os.path.abspath(t)) as s:
s.settle(30)
snap, p = s.snapshot(), s.pane()
drawn = (snap.style_at(p["x"], p["y"] + 1) or {}).get("fg")
check(
f"{os.path.basename(t)} draws its own frame colour",
drawn == declared["frame_focus"],
f"{drawn} != {declared['frame_focus']}",
)
check(f"{os.path.basename(t)} leaves the session alive", s.alive(), "")
def test_each_one_can_be_worn_by_name():
"""What a person actually types: `theme_name "amber"`, not a path to a file
in a checkout. Every shipped theme has to answer to its own filename."""
d = tempfile.mkdtemp()
cfg = os.path.join(d, "config.kdl")
for t in THEMES:
name = os.path.basename(t)[:-4]
declared = dict(
re.findall(r'^ ([a-z_]+) "(#[0-9a-fA-F]{6})"', open(t).read(), re.M)
)
with open(cfg, "w") as f:
f.write(
'theme_dir "%s"\ntheme_name "%s"\n' % (os.path.abspath(THEMEDIR), name)
)
with Session(SH, cols=50, rows=10, config=cfg) as s:
s.settle(30)
snap, p = s.snapshot(), s.pane()
drawn = (snap.style_at(p["x"], p["y"] + 1) or {}).get("fg")
check(
f"theme_name {name!r} is the theme that gets drawn",
drawn == declared["frame_focus"],
f"{drawn} != {declared['frame_focus']}",
)
if __name__ == "__main__":
test_there_are_themes()
test_the_guard_has_something_to_guard()
test_each_sets_every_colour_the_config_knows()
test_default_is_the_compiled_palette_written_out()
test_each_one_parses_and_reaches_the_screen()
test_each_one_can_be_worn_by_name()
sys.exit(report())
|