scriptcast 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- scriptcast/__init__.py +0 -0
- scriptcast/__main__.py +371 -0
- scriptcast/assets/__init__.py +0 -0
- scriptcast/assets/fonts/DMSans-Regular.ttf +0 -0
- scriptcast/assets/fonts/Pacifico.ttf +0 -0
- scriptcast/assets/themes/aurora.sh +20 -0
- scriptcast/assets/themes/dark.sh +19 -0
- scriptcast/assets/themes/light.sh +19 -0
- scriptcast/config.py +199 -0
- scriptcast/directives.py +444 -0
- scriptcast/export.py +595 -0
- scriptcast/generator.py +265 -0
- scriptcast/recorder.py +212 -0
- scriptcast/shell/__init__.py +20 -0
- scriptcast/shell/adapter.py +13 -0
- scriptcast/shell/bash.py +11 -0
- scriptcast/shell/zsh.py +11 -0
- scriptcast-0.1.0.dist-info/METADATA +21 -0
- scriptcast-0.1.0.dist-info/RECORD +33 -0
- scriptcast-0.1.0.dist-info/WHEEL +5 -0
- scriptcast-0.1.0.dist-info/entry_points.txt +2 -0
- scriptcast-0.1.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +0 -0
- tests/test_cli.py +304 -0
- tests/test_config.py +400 -0
- tests/test_directives.py +606 -0
- tests/test_export.py +986 -0
- tests/test_generator.py +434 -0
- tests/test_integration.py +97 -0
- tests/test_recorder.py +462 -0
- tests/test_registry.py +57 -0
- tests/test_shell.py +34 -0
- tests/test_theme.py +204 -0
tests/test_recorder.py
ADDED
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
# tests/test_recorder.py
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import shutil
|
|
5
|
+
|
|
6
|
+
from scriptcast.config import ScriptcastConfig
|
|
7
|
+
from scriptcast.directives import ScEvent
|
|
8
|
+
from scriptcast.recorder import _parse_raw, _postprocess, _preprocess, _serialise, record
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_record_creates_sc_file(tmp_path):
|
|
12
|
+
script = tmp_path / "demo.sh"
|
|
13
|
+
script.write_text("echo hello\n")
|
|
14
|
+
sc_path = tmp_path / "demo.sc"
|
|
15
|
+
config = ScriptcastConfig()
|
|
16
|
+
shell = shutil.which("bash")
|
|
17
|
+
record(script, sc_path, config, shell)
|
|
18
|
+
assert sc_path.exists()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_sc_file_has_jsonl_header(tmp_path):
|
|
22
|
+
script = tmp_path / "demo.sh"
|
|
23
|
+
script.write_text("echo hello\n")
|
|
24
|
+
sc_path = tmp_path / "demo.sc"
|
|
25
|
+
record(script, sc_path, ScriptcastConfig(), shutil.which("bash"))
|
|
26
|
+
first_line = sc_path.read_text().splitlines()[0]
|
|
27
|
+
header = json.loads(first_line)
|
|
28
|
+
assert header["version"] == 1
|
|
29
|
+
assert "shell" in header
|
|
30
|
+
assert header["directive-prefix"] == "SC"
|
|
31
|
+
assert "width" in header
|
|
32
|
+
assert "height" in header
|
|
33
|
+
assert header["pipeline-version"] == 3
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_sc_file_contains_output_event(tmp_path):
|
|
37
|
+
script = tmp_path / "demo.sh"
|
|
38
|
+
script.write_text("echo scriptcast_test_marker\n")
|
|
39
|
+
sc_path = tmp_path / "demo.sc"
|
|
40
|
+
record(script, sc_path, ScriptcastConfig(), shutil.which("bash"))
|
|
41
|
+
events = [json.loads(ln) for ln in sc_path.read_text().splitlines()[1:] if ln.strip()]
|
|
42
|
+
assert any(e[1] == "out" and "scriptcast_test_marker" in e[2] for e in events)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_record_nonzero_exit_writes_sc_and_warns(tmp_path, caplog):
|
|
46
|
+
script = tmp_path / "demo.sh"
|
|
47
|
+
script.write_text("echo before\nexit 1\n")
|
|
48
|
+
sc_path = tmp_path / "demo.sc"
|
|
49
|
+
config = ScriptcastConfig()
|
|
50
|
+
shell = shutil.which("bash")
|
|
51
|
+
with caplog.at_level(logging.WARNING):
|
|
52
|
+
record(script, sc_path, config, shell)
|
|
53
|
+
assert sc_path.exists()
|
|
54
|
+
assert "non-zero" in caplog.text
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_record_strips_shebang(tmp_path):
|
|
58
|
+
script = tmp_path / "demo.sh"
|
|
59
|
+
script.write_text("#!/usr/bin/env scriptcast\necho hello\n")
|
|
60
|
+
sc_path = tmp_path / "demo.sc"
|
|
61
|
+
config = ScriptcastConfig()
|
|
62
|
+
shell = shutil.which("bash")
|
|
63
|
+
record(script, sc_path, config, shell)
|
|
64
|
+
content = sc_path.read_text()
|
|
65
|
+
assert "hello" in content
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_preprocess_mock_rewrites_directive():
|
|
69
|
+
script = (
|
|
70
|
+
": SC mock deploy arg1 <<'EOF'\n"
|
|
71
|
+
"Deploying...\n"
|
|
72
|
+
"OK\n"
|
|
73
|
+
"EOF\n"
|
|
74
|
+
)
|
|
75
|
+
result = _preprocess(script)
|
|
76
|
+
assert "(: SC mark mock; set +x; echo + deploy arg1; cat) <<'EOF'" in result
|
|
77
|
+
assert "Deploying..." in result
|
|
78
|
+
assert result.endswith("EOF\n") # closing delimiter preserved
|
|
79
|
+
assert ": SC mock" not in result
|
|
80
|
+
# Verify only one closing "EOF\n" (not multiple)
|
|
81
|
+
lines = result.split('\n')
|
|
82
|
+
eof_lines = [ln for ln in lines if ln == 'EOF']
|
|
83
|
+
assert len(eof_lines) == 1
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_preprocess_mock_passes_through_non_mock_lines():
|
|
87
|
+
script = "echo hello\n: SC scene intro\n"
|
|
88
|
+
result = _preprocess(script)
|
|
89
|
+
assert result == script
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_preprocess_mock_uses_directive_prefix():
|
|
93
|
+
script = ": MY mock cmd <<'EOF'\nout\nEOF\n"
|
|
94
|
+
result = _preprocess(script, directive_prefix="MY")
|
|
95
|
+
assert "(: MY mark mock; set +x; echo + cmd; cat) <<'EOF'" in result
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_preprocess_mock_multiline_body():
|
|
99
|
+
script = (
|
|
100
|
+
": SC mock git status <<'DONE'\n"
|
|
101
|
+
"On branch main\n"
|
|
102
|
+
"nothing to commit\n"
|
|
103
|
+
"DONE\n"
|
|
104
|
+
)
|
|
105
|
+
result = _preprocess(script)
|
|
106
|
+
assert "echo + git status" in result
|
|
107
|
+
assert "On branch main" in result
|
|
108
|
+
assert "nothing to commit" in result
|
|
109
|
+
assert result.endswith("DONE\n")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_preprocess_expect_rewrites_directive():
|
|
113
|
+
script = (
|
|
114
|
+
": SC expect mysql -u root <<'EOF'\n"
|
|
115
|
+
'expect "Password:"\n'
|
|
116
|
+
'send "secret\\r"\n'
|
|
117
|
+
"EOF\n"
|
|
118
|
+
)
|
|
119
|
+
result = _preprocess(script)
|
|
120
|
+
assert "expect <<'EOF'" in result
|
|
121
|
+
assert "spawn mysql -u root" in result
|
|
122
|
+
assert 'send_user ": SC mark input secret\\n"' in result
|
|
123
|
+
assert 'send "secret\\r"' in result
|
|
124
|
+
assert ": SC expect" not in result
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_preprocess_expect_injects_marker_before_each_send():
|
|
128
|
+
script = (
|
|
129
|
+
": SC expect cmd <<'EOF'\n"
|
|
130
|
+
'expect "p1"\n'
|
|
131
|
+
'send "a\\r"\n'
|
|
132
|
+
'expect "p2"\n'
|
|
133
|
+
'send "b\\r"\n'
|
|
134
|
+
"EOF\n"
|
|
135
|
+
)
|
|
136
|
+
result = _preprocess(script)
|
|
137
|
+
assert result.count('send_user ": SC mark input') == 2
|
|
138
|
+
assert 'send_user ": SC mark input a\\n"' in result
|
|
139
|
+
assert 'send_user ": SC mark input b\\n"' in result
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_preprocess_expect_prepends_spawn():
|
|
143
|
+
script = ": SC expect myapp arg1 arg2 <<'EOF'\nexpect \"done\"\nEOF\n"
|
|
144
|
+
result = _preprocess(script)
|
|
145
|
+
lines = result.splitlines()
|
|
146
|
+
# First line after expect <<'EOF' should be spawn
|
|
147
|
+
eof_idx = next(i for i, ln in enumerate(lines) if ln.startswith("expect <<"))
|
|
148
|
+
assert lines[eof_idx + 1] == "spawn myapp arg1 arg2"
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _parse_sc_events(sc_text):
|
|
152
|
+
return [json.loads(ln) for ln in sc_text.splitlines() if ln.strip()]
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def test_postprocess_emits_cmd_event():
|
|
156
|
+
raw = "1.000 + echo hello\n"
|
|
157
|
+
events = _parse_sc_events(_postprocess(raw))
|
|
158
|
+
assert any(e[1] == "cmd" and e[2] == "echo hello" for e in events)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def test_postprocess_emits_output_event():
|
|
162
|
+
raw = "1.000 hello\n"
|
|
163
|
+
events = _parse_sc_events(_postprocess(raw))
|
|
164
|
+
assert any(e[1] == "out" and "hello" in e[2] for e in events)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def test_postprocess_emits_generator_directive():
|
|
168
|
+
raw = "1.000 + : SC scene intro\n"
|
|
169
|
+
events = _parse_sc_events(_postprocess(raw))
|
|
170
|
+
assert any(e[1] == "dir" and e[2] == "scene intro" for e in events)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def test_postprocess_strips_mock_marker_and_set_x():
|
|
174
|
+
raw = (
|
|
175
|
+
"1.000 + : SC mark mock\n"
|
|
176
|
+
"1.001 + set +x\n"
|
|
177
|
+
"1.002 + deploy arg1\n"
|
|
178
|
+
"1.003 Deploying...\n"
|
|
179
|
+
)
|
|
180
|
+
events = _parse_sc_events(_postprocess(raw))
|
|
181
|
+
texts = [e[2] for e in events]
|
|
182
|
+
assert not any("mark mock" in t for t in texts)
|
|
183
|
+
assert not any("set +x" in t for t in texts)
|
|
184
|
+
assert any(e[1] == "cmd" and "deploy arg1" in e[2] for e in events)
|
|
185
|
+
assert any(e[1] == "out" and "Deploying..." in e[2] for e in events)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def test_postprocess_strips_expect_trace():
|
|
189
|
+
raw = "1.000 + expect\n1.001 spawn ./fake-db\n1.002 Password:\n"
|
|
190
|
+
events = _parse_sc_events(_postprocess(raw))
|
|
191
|
+
assert not any(e[1] == "cmd" and e[2] == "expect" for e in events)
|
|
192
|
+
assert not any("spawn" in e[2] for e in events)
|
|
193
|
+
assert any(e[1] == "out" and "Password:" in e[2] for e in events)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def test_postprocess_strips_expect_trace_with_args():
|
|
197
|
+
raw = "1.000 + expect somefile.exp\n1.001 output\n"
|
|
198
|
+
events = _parse_sc_events(_postprocess(raw))
|
|
199
|
+
assert not any(e[1] == "cmd" and "expect" in e[2] for e in events)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def test_postprocess_drops_record_pause_content():
|
|
203
|
+
raw = (
|
|
204
|
+
"1.000 + : SC record pause\n"
|
|
205
|
+
"1.001 + PS1=>\n"
|
|
206
|
+
"1.002 + : SC record resume\n"
|
|
207
|
+
"1.003 + echo after\n"
|
|
208
|
+
)
|
|
209
|
+
events = _parse_sc_events(_postprocess(raw))
|
|
210
|
+
texts = [e[2] for e in events]
|
|
211
|
+
assert not any("PS1" in t for t in texts)
|
|
212
|
+
assert any("echo after" in t for t in texts)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def test_postprocess_filter_applies_to_output():
|
|
216
|
+
raw = (
|
|
217
|
+
"1.000 + : SC filter sed 's/foo/bar/g'\n"
|
|
218
|
+
"1.001 foo baz\n"
|
|
219
|
+
)
|
|
220
|
+
events = _parse_sc_events(_postprocess(raw))
|
|
221
|
+
output_texts = [e[2] for e in events if e[1] == "out"]
|
|
222
|
+
assert any("bar baz" in t for t in output_texts)
|
|
223
|
+
assert not any("foo" in t for t in output_texts)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def test_postprocess_filter_not_emitted_as_directive():
|
|
227
|
+
raw = "1.000 + : SC filter sed 's/a/b/g'\n"
|
|
228
|
+
events = _parse_sc_events(_postprocess(raw))
|
|
229
|
+
assert not any(e[1] == "dir" and "filter" in e[2] for e in events)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def test_postprocess_filter_add_appends():
|
|
233
|
+
raw = (
|
|
234
|
+
"1.000 + : SC filter sed 's/foo/bar/g'\n"
|
|
235
|
+
"1.001 + : SC filter-add sed 's/baz/qux/g'\n"
|
|
236
|
+
"1.002 foo baz\n"
|
|
237
|
+
)
|
|
238
|
+
events = _parse_sc_events(_postprocess(raw))
|
|
239
|
+
output_texts = [e[2] for e in events if e[1] == "out"]
|
|
240
|
+
assert any("bar qux" in t for t in output_texts)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def test_postprocess_custom_trace_prefix():
|
|
244
|
+
raw = "1.000 ++ echo hi\n"
|
|
245
|
+
events = _parse_sc_events(_postprocess(raw, trace_prefix="++"))
|
|
246
|
+
assert any(e[1] == "cmd" and e[2] == "echo hi" for e in events)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def test_postprocess_emits_expect_input_dir_event():
|
|
250
|
+
raw = (
|
|
251
|
+
"1.000 + expect myapp\n"
|
|
252
|
+
"1.001 spawn myapp\n"
|
|
253
|
+
"1.002 : SC mark input secret\n"
|
|
254
|
+
)
|
|
255
|
+
events = _parse_sc_events(_postprocess(raw))
|
|
256
|
+
assert any(e[1] == "dir" and "expect-input" in e[2] for e in events)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def test_postprocess_emits_output_prefix_before_expect_input():
|
|
260
|
+
raw = (
|
|
261
|
+
"1.000 + expect myapp\n"
|
|
262
|
+
"1.001 spawn myapp\n"
|
|
263
|
+
"1.002 Password: : SC mark input secret\n"
|
|
264
|
+
)
|
|
265
|
+
events = _parse_sc_events(_postprocess(raw))
|
|
266
|
+
types = [e[1] for e in events]
|
|
267
|
+
assert "out" in types
|
|
268
|
+
assert any(e[1] == "dir" and "expect-input" in e[2] for e in events)
|
|
269
|
+
assert types.index("out") < types.index("dir")
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def test_postprocess_strips_pty_echo_after_input():
|
|
273
|
+
raw = (
|
|
274
|
+
"1.000 + expect\n"
|
|
275
|
+
"1.001 spawn ./fake-db\n"
|
|
276
|
+
"1.002 Password: : SC mark input secret\n"
|
|
277
|
+
"1.003 secret\n"
|
|
278
|
+
"1.004 Welcome to FakeDB\n"
|
|
279
|
+
"1.005 mysql> : SC mark input show databases;\n"
|
|
280
|
+
"1.006 show databases;\n"
|
|
281
|
+
"1.007 (0 rows)\n"
|
|
282
|
+
)
|
|
283
|
+
events = _parse_sc_events(_postprocess(raw))
|
|
284
|
+
output_texts = [e[2] for e in events if e[1] == "out"]
|
|
285
|
+
assert not any("secret" in t for t in output_texts)
|
|
286
|
+
assert not any("show databases;" in t for t in output_texts)
|
|
287
|
+
assert any("Welcome to FakeDB" in t for t in output_texts)
|
|
288
|
+
assert any("(0 rows)" in t for t in output_texts)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def test_record_mock_directive_produces_cmd_and_output_events(tmp_path):
|
|
292
|
+
script = tmp_path / "demo.sh"
|
|
293
|
+
script.write_text(": SC mock deploy <<'EOF'\nDeploying...\nEOF\n")
|
|
294
|
+
sc_path = tmp_path / "demo.sc"
|
|
295
|
+
record(script, sc_path, ScriptcastConfig(), shutil.which("bash"))
|
|
296
|
+
events = [json.loads(ln) for ln in sc_path.read_text().splitlines()[1:] if ln.strip()]
|
|
297
|
+
assert any(e[1] == "cmd" and "deploy" in e[2] for e in events)
|
|
298
|
+
assert any(e[1] == "out" and "Deploying..." in e[2] for e in events)
|
|
299
|
+
assert not any("mark mock" in e[2] for e in events)
|
|
300
|
+
assert not any("set +x" in e[2] for e in events)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def test_preprocess_expect_injects_send_text_in_marker():
|
|
304
|
+
script = (
|
|
305
|
+
": SC expect cmd <<'EOF'\n"
|
|
306
|
+
'expect "Password:"\n'
|
|
307
|
+
'send "secret\\r"\n'
|
|
308
|
+
"EOF\n"
|
|
309
|
+
)
|
|
310
|
+
result = _preprocess(script)
|
|
311
|
+
assert 'send_user ": SC mark input secret\\n"' in result
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def test_record_cwd_is_script_directory(tmp_path):
|
|
315
|
+
"""record() runs the script with cwd set to the script's parent directory."""
|
|
316
|
+
script = tmp_path / "demo.sh"
|
|
317
|
+
helper = tmp_path / "helper.txt"
|
|
318
|
+
helper.write_text("hello from helper\n")
|
|
319
|
+
script.write_text("cat helper.txt\n")
|
|
320
|
+
sc_path = tmp_path / "demo.sc"
|
|
321
|
+
config = ScriptcastConfig()
|
|
322
|
+
shell = shutil.which("bash")
|
|
323
|
+
record(script, sc_path, config, shell)
|
|
324
|
+
content = sc_path.read_text()
|
|
325
|
+
assert "hello from helper" in content
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def test_parse_raw_cmd_line():
|
|
330
|
+
events = _parse_raw("1.000 + echo hello\n", "+", "SC")
|
|
331
|
+
assert events == [ScEvent(1.0, "cmd", "echo hello")]
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def test_parse_raw_cmd_strips_trailing_cr():
|
|
335
|
+
# PTY CRLF: \r before \n is a PTY artifact and must be stripped from cmd text.
|
|
336
|
+
events = _parse_raw("1.000 + echo hello\r\n", "+", "SC")
|
|
337
|
+
assert events == [ScEvent(1.0, "cmd", "echo hello")]
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def test_parse_raw_output_line():
|
|
341
|
+
events = _parse_raw("1.000 hello world\n", "+", "SC")
|
|
342
|
+
assert events == [ScEvent(1.0, "out", "hello world\n")]
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def test_parse_raw_directive_line():
|
|
346
|
+
events = _parse_raw("1.000 + : SC scene intro\n", "+", "SC")
|
|
347
|
+
assert events == [ScEvent(1.0, "dir", "scene intro")]
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def test_parse_raw_dir_strips_trailing_cr():
|
|
351
|
+
# PTY CRLF: \r before \n is a PTY artifact and must be stripped from dir text.
|
|
352
|
+
events = _parse_raw("1.000 + : SC scene intro\r\n", "+", "SC")
|
|
353
|
+
assert events == [ScEvent(1.0, "dir", "scene intro")]
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def test_parse_raw_skips_non_float_lines():
|
|
357
|
+
events = _parse_raw("not_a_number + echo hi\n", "+", "SC")
|
|
358
|
+
assert events == []
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def test_parse_raw_custom_prefix():
|
|
363
|
+
events = _parse_raw("1.000 ++ echo hi\n", "++", "SC")
|
|
364
|
+
assert events == [ScEvent(1.0, "cmd", "echo hi")]
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def test_serialise_cmd():
|
|
368
|
+
events = [ScEvent(1.0, "cmd", "echo hi")]
|
|
369
|
+
lines = _serialise(events).splitlines()
|
|
370
|
+
assert json.loads(lines[0]) == [1.0, "cmd", "echo hi"]
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def test_serialise_out():
|
|
374
|
+
events = [ScEvent(1.0, "out", "hello")]
|
|
375
|
+
lines = _serialise(events).splitlines()
|
|
376
|
+
assert json.loads(lines[0]) == [1.0, "out", "hello"]
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def test_serialise_dir():
|
|
380
|
+
events = [ScEvent(1.0, "dir", "scene intro")]
|
|
381
|
+
lines = _serialise(events).splitlines()
|
|
382
|
+
assert json.loads(lines[0]) == [1.0, "dir", "scene intro"]
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def test_serialise_empty():
|
|
386
|
+
assert _serialise([]) == ""
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def test_parse_raw_out_preserves_cr_in_content():
|
|
390
|
+
# With the simplified split-on-\n approach, bare \r is content, not a
|
|
391
|
+
# record separator. A line containing bare \r yields a single out event
|
|
392
|
+
# with the \r embedded in the text.
|
|
393
|
+
events = _parse_raw("1.000 Loading\r100%\n", "+", "SC")
|
|
394
|
+
assert events == [ScEvent(1.0, "out", "Loading\r100%\n")]
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def test_parse_raw_out_preserves_crlf():
|
|
398
|
+
# CRLF line ending: \r is kept, \n is the record separator
|
|
399
|
+
events = _parse_raw("1.000 hello\r\n", "+", "SC")
|
|
400
|
+
assert events == [ScEvent(1.0, "out", "hello\r\n")]
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def test_parse_raw_out_no_terminator_for_last_entry():
|
|
404
|
+
# A prompt with no trailing newline — last entry in the log
|
|
405
|
+
events = _parse_raw("1.000 Enter password: ", "+", "SC")
|
|
406
|
+
assert events == [ScEvent(1.0, "out", "Enter password: ")]
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def test_record_isatty_in_pty(tmp_path):
|
|
410
|
+
"""Script sees a real TTY — isatty(stdout) is true."""
|
|
411
|
+
script = tmp_path / "demo.sh"
|
|
412
|
+
script.write_text('[ -t 1 ] && echo is_tty || echo not_tty\n')
|
|
413
|
+
sc_path = tmp_path / "demo.sc"
|
|
414
|
+
record(script, sc_path, ScriptcastConfig(), shutil.which("bash"))
|
|
415
|
+
content = sc_path.read_text()
|
|
416
|
+
assert "is_tty" in content
|
|
417
|
+
assert "not_tty" not in content
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def test_record_pty_translates_lf_to_crlf(tmp_path):
|
|
421
|
+
"""PTY line discipline translates bare \\n to \\r\\n in out events."""
|
|
422
|
+
script = tmp_path / "demo.sh"
|
|
423
|
+
script.write_text('printf "hello\\n"\n')
|
|
424
|
+
sc_path = tmp_path / "demo.sc"
|
|
425
|
+
record(script, sc_path, ScriptcastConfig(), shutil.which("bash"))
|
|
426
|
+
events = [json.loads(ln) for ln in sc_path.read_text().splitlines()[1:] if ln.strip()]
|
|
427
|
+
out_texts = [e[2] for e in events if e[1] == "out"]
|
|
428
|
+
assert any("hello\r\n" in t for t in out_texts)
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def test_record_xtrace_log_creates_file(tmp_path):
|
|
432
|
+
script = tmp_path / "demo.sh"
|
|
433
|
+
script.write_text("echo hello\n")
|
|
434
|
+
sc_path = tmp_path / "demo.sc"
|
|
435
|
+
config = ScriptcastConfig()
|
|
436
|
+
shell = shutil.which("bash")
|
|
437
|
+
record(script, sc_path, config, shell, xtrace_log=True)
|
|
438
|
+
xtrace_path = tmp_path / "demo.xtrace"
|
|
439
|
+
assert xtrace_path.exists()
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def test_record_xtrace_log_contains_raw_output(tmp_path):
|
|
443
|
+
script = tmp_path / "demo.sh"
|
|
444
|
+
script.write_text("echo scriptcast_xtrace_marker\n")
|
|
445
|
+
sc_path = tmp_path / "demo.sc"
|
|
446
|
+
config = ScriptcastConfig()
|
|
447
|
+
shell = shutil.which("bash")
|
|
448
|
+
record(script, sc_path, config, shell, xtrace_log=True)
|
|
449
|
+
xtrace_path = tmp_path / "demo.xtrace"
|
|
450
|
+
assert "scriptcast_xtrace_marker" in xtrace_path.read_text()
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def test_record_no_xtrace_log_by_default(tmp_path):
|
|
454
|
+
script = tmp_path / "demo.sh"
|
|
455
|
+
script.write_text("echo hello\n")
|
|
456
|
+
sc_path = tmp_path / "demo.sc"
|
|
457
|
+
config = ScriptcastConfig()
|
|
458
|
+
shell = shutil.which("bash")
|
|
459
|
+
record(script, sc_path, config, shell)
|
|
460
|
+
xtrace_path = tmp_path / "demo.xtrace"
|
|
461
|
+
assert not xtrace_path.exists()
|
|
462
|
+
|
tests/test_registry.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
from scriptcast.directives import (
|
|
4
|
+
CommentDirective,
|
|
5
|
+
ExpectDirective,
|
|
6
|
+
FilterDirective,
|
|
7
|
+
HelpersDirective,
|
|
8
|
+
MockDirective,
|
|
9
|
+
RecordDirective,
|
|
10
|
+
SetDirective,
|
|
11
|
+
SleepDirective,
|
|
12
|
+
build_directives,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_build_directives_returns_list():
|
|
17
|
+
result = build_directives()
|
|
18
|
+
assert isinstance(result, list)
|
|
19
|
+
assert len(result) > 0
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_build_directives_sorted_by_priority():
|
|
23
|
+
result = build_directives()
|
|
24
|
+
priorities = [d.priority for d in result]
|
|
25
|
+
assert priorities == sorted(priorities)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_build_directives_contains_all_core():
|
|
29
|
+
result = build_directives()
|
|
30
|
+
types = {type(d) for d in result}
|
|
31
|
+
assert HelpersDirective in types
|
|
32
|
+
assert RecordDirective in types
|
|
33
|
+
assert MockDirective in types
|
|
34
|
+
assert ExpectDirective in types
|
|
35
|
+
assert FilterDirective in types
|
|
36
|
+
assert CommentDirective in types
|
|
37
|
+
assert SetDirective in types
|
|
38
|
+
assert SleepDirective in types
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_expect_before_filter():
|
|
42
|
+
result = build_directives()
|
|
43
|
+
types = [type(d) for d in result]
|
|
44
|
+
assert types.index(ExpectDirective) < types.index(FilterDirective)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_helpers_before_record():
|
|
48
|
+
result = build_directives()
|
|
49
|
+
types = [type(d) for d in result]
|
|
50
|
+
assert types.index(HelpersDirective) < types.index(RecordDirective)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_build_directives_dp_tp_propagated():
|
|
54
|
+
result = build_directives(dp="DEMO", tp=">>")
|
|
55
|
+
for d in result:
|
|
56
|
+
assert d.dp == "DEMO"
|
|
57
|
+
assert d.tp == ">>"
|
tests/test_shell.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# tests/test_shell.py
|
|
2
|
+
import pytest
|
|
3
|
+
|
|
4
|
+
from scriptcast.shell import get_adapter
|
|
5
|
+
from scriptcast.shell.bash import BashAdapter
|
|
6
|
+
from scriptcast.shell.zsh import ZshAdapter
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_get_bash():
|
|
10
|
+
assert isinstance(get_adapter("bash"), BashAdapter)
|
|
11
|
+
|
|
12
|
+
def test_get_zsh():
|
|
13
|
+
assert isinstance(get_adapter("zsh"), ZshAdapter)
|
|
14
|
+
|
|
15
|
+
def test_get_full_path():
|
|
16
|
+
assert isinstance(get_adapter("/bin/bash"), BashAdapter)
|
|
17
|
+
|
|
18
|
+
def test_get_unsupported_raises():
|
|
19
|
+
with pytest.raises(ValueError, match="Unsupported shell"):
|
|
20
|
+
get_adapter("fish")
|
|
21
|
+
|
|
22
|
+
def test_bash_preamble_contains_set_x():
|
|
23
|
+
p = BashAdapter().tracing_preamble("+")
|
|
24
|
+
assert "set -x" in p
|
|
25
|
+
assert 'PS4="+ "' in p
|
|
26
|
+
|
|
27
|
+
def test_bash_preamble_custom_prefix():
|
|
28
|
+
p = BashAdapter().tracing_preamble(">>")
|
|
29
|
+
assert 'PS4=">> "' in p
|
|
30
|
+
|
|
31
|
+
def test_zsh_preamble_contains_xtrace():
|
|
32
|
+
p = ZshAdapter().tracing_preamble("+")
|
|
33
|
+
assert "setopt xtrace" in p
|
|
34
|
+
assert 'PS4="+ "' in p
|