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
|
#!/usr/bin/env python3
"""A program in a pane can act on *its own* pane, without using focus.
The bug this pins: everything a script could ask about a pane was answered
relative to focus, and focus is the human's. A tool spawning work "beside
itself" had nothing else to go on, so it read `panes` for whichever pane was
focused, and landed its work in whatever tab somebody else happened to be
looking at -- in a session with several agents in it, reliably the wrong one.
Worse, applying a layout jumped the view to the tab it built, so the *next*
caller read a focus that the previous spawn had moved.
So four things are checked here, which together are "background work lands where
the caller lives and nobody's view moves":
* $SLOSH_PANE tells a program which pane it is in (and survives a rerun).
* `panes` reports each pane's `pid`, so a process with no environment to read
-- no controlling tty, a scrubbed env -- can still find itself by walking
its own parents.
* `apply-layout focus:false` builds tabs without going to them, and the panes
it builds are still given a real size (a pane nobody looks at used to stay
1x1, which was invisible only because applying a layout always jumped).
* `move-pane beside:<id> focus:false` lands a pane next to a named pane and
leaves focus, and the view, alone.
* `raw id:<id>` and `capture id:<id>` drive and read a pane in another tab
with no focus change and no select-tab.
"""
import os
import sys
from harness import Session, check, report
# A pane that reports its own identity into a file, then waits. This is the
# question under test: what does a program in a pane know about its own pane?
WHOAMI = "/tmp/slosh-test-whoami.txt"
SHELL = ["/bin/sh", "-c", f'echo "pane=$SLOSH_PANE pid=$$" > {WHOAMI}; read x']
# Something with visible output, for the capture checks.
TALKER = ["/bin/sh", "-c", "echo MARKER-ONE; read x"]
def layout(kdl):
import tempfile
f = tempfile.NamedTemporaryFile("w", suffix=".layout", delete=False)
f.write(kdl)
f.close()
return f.name
def test_a_pane_is_told_which_pane_it_is():
if os.path.exists(WHOAMI):
os.unlink(WHOAMI)
with Session(SHELL, cols=60, rows=12) as s:
s.settle(200)
pane = s.pane(0)
said = open(WHOAMI).read().strip() if os.path.exists(WHOAMI) else "<nothing>"
check(
"$SLOSH_PANE is the pane's own id",
f"pane={pane['id']} " in said + " ",
f"pane id {pane['id']}, program said {said!r}",
)
check(
"panes reports the pid of the program in the pane",
f"pid={pane['pid']}" in said,
f"panes says pid {pane['pid']}, program said {said!r}",
)
def test_the_id_survives_a_rerun():
"""A pane's identity is the pane's, not the process's: `rerun` keeps it.
Otherwise a tool that recorded "my pane is 3" would be talking about
somebody else after the pane it meant was run again."""
log = "/tmp/slosh-test-rerun.txt"
if os.path.exists(log):
os.unlink(log)
# A *commanded* pane, because only those keep a husk to rerun -- a pane
# that is just a shell goes when its shell does (see test_dead.py).
lay = layout(
'layout {\n tab {\n pane command="echo pane=$SLOSH_PANE >> %s"\n pane\n }\n}\n'
% log
)
with Session(["/bin/sh", "-c", "read x"], cols=60, rows=12, layout=lay) as s:
s.until(lambda sn: not s.pane(0)["alive"])
pane_id = s.pane(0)["id"]
s.api("rerun", id=pane_id)
s.until(lambda sn: len(open(log).read().splitlines()) >= 2)
said = open(log).read().split()
check(
"the same pane reports the same id after a rerun",
said == [f"pane={pane_id}", f"pane={pane_id}"],
f"pane {pane_id} logged {said}",
)
def test_a_layout_can_be_applied_without_stealing_the_view():
with Session(TALKER, cols=60, rows=12) as s:
s.settle(100)
s.api("new-tab", name="human-tab")
s.api("select-tab", index=2)
s.settle(50)
before = [t["id"] for t in s.tabs() if t["active"]]
s.api(
"apply-layout",
focus=False,
kdl='layout { tab name="bg" { pane purpose="task:bg" command="sh -c \'echo MARKER-BG; read x\'" } }',
)
s.settle(200)
after = [t["id"] for t in s.tabs() if t["active"]]
check(
"the view stays on the tab it was on",
before == after,
f"was viewing {before}, now viewing {after}",
)
built = [p for p in s.panes() if p["purpose"] == "task:bg"]
check("the layout still built its pane", len(built) == 1, str(s.panes()))
if built:
# The whole reason background panes were never seen to be broken:
# the view used to jump to them, and the jump is what sized them.
check(
"a pane in a tab nobody looked at still has a real size",
built[0]["w"] > 2 and built[0]["h"] > 2,
f"{built[0]['w']}x{built[0]['h']}",
)
check(
"and its program ran at that size, so its output is readable",
"MARKER-BG" in s.api("capture", id=built[0]["id"])["text"],
repr(s.api("capture", id=built[0]["id"])["text"][:120]),
)
def test_a_pane_lands_beside_the_pane_that_asked_not_beside_focus():
"""The reported bug, in one test.
Pane 1 is "the agent". The human is in another tab, so the only focused
pane in the session is theirs. The agent asks for a pane beside *itself*
by id, and it has to arrive in the agent's tab with nobody's view moving.
"""
with Session(TALKER, cols=80, rows=24) as s:
s.settle(100)
agent = s.pane(0)["id"]
agent_tab = s.pane(0)["tab_id"]
s.api("new-tab", name="human-tab")
s.api("select-tab", index=2)
s.settle(50)
human = s.focused()
s.api(
"apply-layout",
focus=False,
kdl='layout { tab name="work" { pane purpose="task:x" command="sh -c \'echo MARKER-TWO; read x\'" } }',
)
s.settle(150)
new = [p for p in s.panes() if p["purpose"] == "task:x"][0]["id"]
reply = s.api("move-pane", id=new, tab=agent_tab, beside=agent, focus=False)
check("the move is accepted", reply.get("ok"), str(reply))
s.settle(50)
rows = {p["id"]: p for p in s.panes()}
check(
"the pane lands in the tab of the pane that asked for it",
rows[new]["tab_id"] == agent_tab,
f"wanted tab {agent_tab}, got {rows[new]['tab_id']}",
)
check(
"focus does not move to it",
not rows[new]["focused"],
"the new pane took focus",
)
check(
"the human keeps focus",
rows[human["id"]]["focused"],
f"focus went to {[p['id'] for p in s.panes() if p['focused']]}",
)
check(
"and keeps the tab they were looking at",
[t["id"] for t in s.tabs() if t["active"]] == [human["tab_id"]],
str(s.tabs()),
)
# `beside` has to be in the destination tab, or the caller has the
# wrong idea of where things are and should hear so.
bad = s.api("move-pane", id=new, tab=human["tab_id"], beside=agent)
check("a `beside` in the wrong tab is refused", not bad.get("ok"), str(bad))
def test_a_pane_elsewhere_can_be_read_and_driven_without_focus():
with Session(TALKER, cols=80, rows=24) as s:
s.settle(100)
mine = s.pane(0)["id"]
s.api("new-tab", name="elsewhere")
s.api("select-tab", index=2)
s.settle(50)
viewing = [t["id"] for t in s.tabs() if t["active"]]
cap = s.api("capture", id=mine)
check(
"capture reads a pane in a tab that is not on screen",
"MARKER-ONE" in cap["text"],
repr(cap["text"][:120]),
)
check(
"capture does not change which tab is on screen",
[t["id"] for t in s.tabs() if t["active"]] == viewing,
str(s.tabs()),
)
check(
"capture does not move focus",
s.focused()["tab_id"] != s.pane(0)["tab_id"] or len(s.panes()) == 1,
str(s.focused()),
)
# raw into a named pane: the pane's program is `read x`, so a line ends
# it -- observable as the pane no longer being alive.
# raw into a named pane: its program is `read x`, so a line ends it.
# A real newline in `data`: the JSON form carries bytes, only the
# bare-verb form unescapes (`raw q\n`).
s.api("raw", id=mine, data="q\n")
def ended(_):
rows = [p for p in s.panes() if p["id"] == mine]
# The session's own shell pane leaves no husk when it ends, so
# "gone" and "not alive" are both the line having been delivered.
return not rows or not rows[0]["alive"]
s.until(ended)
check(
"raw id:<pane> reaches a pane that is not focused",
ended(None),
str([p for p in s.panes() if p["id"] == mine]),
)
check(
"and still did not move the view",
[t["id"] for t in s.tabs() if t["active"]] == viewing,
str(s.tabs()),
)
check(
"raw at an id that does not exist is refused",
not s.api("raw", id=99999, data="x").get("ok"),
"a write to a dead id was accepted",
)
check(
"capture at an id that does not exist is refused",
not s.api("capture", id=99999).get("ok"),
"a read of a dead id was accepted",
)
for name, fn in sorted(list(globals().items())):
if name.startswith("test_"):
fn()
sys.exit(report())
|