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
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
|
#!/usr/bin/env python3
"""M7: layout files — the prerequisite for porting `sl0ppi up`.
A layout declares tabs, purposes and panes; the purposes it declares are
locked, and commands can be suspended so that opening twelve projects does not
start twelve servers.
"""
import os
import sys
import tempfile
from harness import Session, check, report
SH = ["/bin/sh", "-c", "stty raw -echo; cat"]
LAYOUT = """
layout {
tab name="api" purpose="project:api.a1b2" cwd="/tmp" {
pane purpose="agent:main" command="stty raw -echo; cat"
pane split="rows" {
pane purpose="service:web" command="stty raw -echo; printf web-running; cat" suspended=true
pane purpose="logs:tail" command="stty raw -echo; cat"
}
}
tab name="notes" {
pane
}
}
"""
def layout_file(text):
f = tempfile.NamedTemporaryFile("w", suffix=".kdl", delete=False)
f.write(text)
f.close()
return f.name
def session_with(text, **kw):
path = layout_file(text)
s = Session(SH, layout=path, **kw)
s._layout_path = path
return s
def test_structure():
with session_with(LAYOUT, cols=78, rows=18) as s:
s.settle()
tabs = s.tabs()
check(
"a layout builds its tabs",
[t["name"] for t in tabs] == ["api", "notes"],
str(tabs),
)
check(
"and only its tabs (the default one is replaced)", len(tabs) == 2, str(tabs)
)
check(
"tab purposes are declared, so nothing can override them",
tabs[0]["purpose"] == "project:api.a1b2" and tabs[0]["purpose_declared"],
str(tabs[0]),
)
panes = s.panes()
check("panes are built in every tab", len(panes) == 4, str(len(panes)))
purposes = [p["purpose"] for p in panes if p["tab"] == 1]
check(
"pane purposes come from the layout",
purposes == ["agent:main", "service:web", "logs:tail"],
str(purposes),
)
check(
"pane purposes are declared too",
all(p["purpose_declared"] for p in panes if p["purpose"]),
str(panes),
)
first, web, logs = panes[0], panes[1], panes[2]
check(
"the tab body splits into columns by default",
first["x"] < web["x"] and first["y"] == web["y"],
f"{first['x']},{first['y']} vs {web['x']},{web['y']}",
)
check(
"a nested pane with split=rows stacks its children",
web["x"] == logs["x"] and web["y"] < logs["y"],
f"{web['y']} vs {logs['y']}",
)
os.unlink(s._layout_path)
def test_suspended():
with session_with(LAYOUT, cols=78, rows=18) as s:
s.settle()
panes = s.panes()
web = [p for p in panes if p["purpose"] == "service:web"][0]
check("a suspended pane is marked as such", web["suspended"], str(web))
check(
"others are not",
not [p for p in panes if p["purpose"] == "agent:main"][0]["suspended"],
)
snap = s.snapshot()
check(
"a suspended pane says what it would run",
"press a key to run:" in snap.screen(),
repr(snap.screen()[:400]),
)
check(
"but has not run it",
"web-running" not in snap.pane_text(web),
repr(snap.pane_text(web)),
)
s.api("focus", id=web["id"])
s.send("x")
s.settle()
now = [p for p in s.panes() if p["id"] == web["id"]]
check("a keystroke starts it", now and not now[0]["suspended"], str(s.panes()))
check(
"and the command actually runs",
"web-running" in s.snapshot().pane_text(now[0]),
repr(s.snapshot().pane_text(now[0])),
)
check(
"the keystroke that started it is not also sent to it",
"x" not in s.snapshot().pane_text(now[0]).replace("web-running", ""),
repr(s.snapshot().pane_text(now[0])),
)
check("the session is unharmed", s.api("alive")["ok"])
os.unlink(s._layout_path)
def test_declared_purposes_are_locked():
with session_with(LAYOUT, cols=78, rows=18) as s:
s.settle()
agent = [p for p in s.panes() if p["purpose"] == "agent:main"][0]
r = s.api(
"set-purpose",
target="pane",
id=agent["id"],
purpose="evil:relabelled",
declared=False,
)
check(
"a layout-declared pane purpose cannot be taken in-band",
not r["ok"],
str(r),
)
check(
"it is unchanged",
[p for p in s.panes() if p["id"] == agent["id"]][0]["purpose"]
== "agent:main",
)
r = s.api(
"set-purpose",
target="tab",
id=s.tabs()[0]["id"],
purpose="evil:tab",
declared=False,
)
check("nor a tab purpose", not r["ok"], str(r))
os.unlink(s._layout_path)
def test_cwd():
text = """
layout {
tab name="t" cwd="/tmp" {
pane command="pwd; cat"
pane cwd="/usr" command="pwd; cat"
}
}
"""
with session_with(text, cols=70, rows=14) as s:
s.settle()
panes = s.panes()
snap = s.snapshot()
check(
"a tab's cwd is inherited by its panes",
"/tmp" in snap.pane_text(panes[0]),
repr(snap.pane_text(panes[0])),
)
check(
"a pane can override it",
"/usr" in snap.pane_text(panes[1]),
repr(snap.pane_text(panes[1])),
)
os.unlink(s._layout_path)
def test_cwd_expands_a_tilde():
"""`~` is a shell thing, and no shell has touched a path in a layout file.
Unexpanded it reached chdir() literally, failed, and left the pane in
whatever directory the session was started from — with the pane running,
so nothing looked wrong until you typed `ls`."""
# realpath: on macOS the temp dir is under /var, which is a symlink to
# /private/var, and `pwd` in the pane prints the physical path.
home = os.path.realpath(tempfile.mkdtemp(prefix="slosh-home-"))
text = """
layout {
tab name="t" {
pane cwd="~" command="pwd; cat"
pane cwd="~/sub" command="pwd; cat"
}
}
"""
os.mkdir(os.path.join(home, "sub"))
with session_with(text, cols=76, rows=14, env={"HOME": home}) as s:
s.settle()
panes = s.panes()
snap = s.snapshot()
check(
"~ is the home directory",
home in snap.pane_text(panes[0]).replace("\n", ""),
repr(snap.pane_text(panes[0])),
)
check(
"and ~/sub is under it",
os.path.join(home, "sub") in snap.pane_text(panes[1]).replace("\n", ""),
repr(snap.pane_text(panes[1])),
)
os.unlink(s._layout_path)
def test_apply_over_the_api():
"""How `sl0ppi up` will add a project tab to a running session."""
with Session(SH, cols=70, rows=14) as s:
s.settle()
check("one tab to start", len(s.tabs()) == 1)
frag = (
'layout { tab name="added" purpose="project:x.deadbeef" '
'{ pane purpose="agent:main" } }'
)
r = s.api("apply-layout", kdl=frag)
s.settle()
check(
"apply-layout answers with the tab list",
r["ok"] and "tabs" in r,
str(r)[:100],
)
check(
"the tab is added, not replacing what was there",
len(s.tabs()) == 2 and s.tabs()[1]["name"] == "added",
str(s.tabs()),
)
check(
"its purpose is declared",
s.tabs()[1]["purpose"] == "project:x.deadbeef"
and s.tabs()[1]["purpose_declared"],
str(s.tabs()[1]),
)
# idempotency belongs to the caller: it diffs, then adds what is missing
have = {t["purpose"] for t in s.tabs()}
if "project:x.deadbeef" not in have:
s.api("apply-layout", kdl=frag)
check(
"a caller that diffs first does not duplicate",
len(s.tabs()) == 2,
str(s.tabs()),
)
r = s.api("close-tab", id=s.tabs()[1]["id"])
check("close-tab removes it", r["ok"] and len(s.tabs()) == 1, str(s.tabs()))
r = s.api(
"apply-layout", kdl='layout { tab name="only" { pane } }', replace=True
)
s.settle()
check(
"replace=true rebuilds the session",
len(s.tabs()) == 1 and s.tabs()[0]["name"] == "only",
str(s.tabs()),
)
def test_bad_layouts():
with Session(SH, cols=70, rows=14) as s:
s.settle()
r = s.api("apply-layout", kdl='layout { tab name="x" ')
check("a broken layout is refused", not r["ok"], str(r))
check("with a line number", "line" in str(r.get("error", "")), str(r))
r = s.api("apply-layout", kdl="layout { }")
check("a layout with no tabs is refused", not r["ok"], str(r))
r = s.api("apply-layout", path="/nonexistent/x.layout")
check("a missing file is refused", not r["ok"], str(r))
check(
"the session survived all of it",
s.api("alive")["alive"] and len(s.tabs()) == 1,
str(s.tabs()),
)
broken = layout_file('layout { tab name="x"')
with Session(SH, cols=70, rows=14, layout=broken) as s:
s.settle()
check(
"a broken layout at startup still gives you a session",
s.api("alive")["alive"] and len(s.panes()) == 1,
str(s.panes()),
)
os.unlink(broken)
if __name__ == "__main__":
test_structure()
test_suspended()
test_declared_purposes_are_locked()
test_cwd()
test_cwd_expands_a_tilde()
test_apply_over_the_api()
test_bad_layouts()
sys.exit(report())
|