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_generator.py
ADDED
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
# tests/test_generator.py
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
from scriptcast.generator import generate_from_sc, generate_from_sc_text
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _make_sc(*events, width=80, height=24):
|
|
8
|
+
header = {"version": 1, "width": width, "height": height,
|
|
9
|
+
"directive-prefix": "SC", "pipeline-version": 3}
|
|
10
|
+
lines = [json.dumps(header)]
|
|
11
|
+
ts = 1.0
|
|
12
|
+
for typ, text in events:
|
|
13
|
+
lines.append(json.dumps([ts, typ, text]))
|
|
14
|
+
ts += 0.001
|
|
15
|
+
return "\n".join(lines) + "\n"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _zero_sc(*events):
|
|
19
|
+
zero = [
|
|
20
|
+
("dir", "set type_speed 0"),
|
|
21
|
+
("dir", "set cmd_wait 0"),
|
|
22
|
+
("dir", "set exit_wait 0"),
|
|
23
|
+
("dir", "set enter_wait 0"),
|
|
24
|
+
("dir", "set input_wait 0"),
|
|
25
|
+
]
|
|
26
|
+
return _make_sc(*zero, *events)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _cast(path):
|
|
30
|
+
lines = [ln for ln in path.read_text().strip().splitlines() if ln]
|
|
31
|
+
return json.loads(lines[0]), [json.loads(ln) for ln in lines[1:]]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_cast_header_version(tmp_path):
|
|
35
|
+
paths = generate_from_sc_text(_zero_sc(), tmp_path)
|
|
36
|
+
header, _ = _cast(paths[0])
|
|
37
|
+
assert header["version"] == 2
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_cast_header_dimensions(tmp_path):
|
|
41
|
+
sc = _make_sc(width=120, height=30)
|
|
42
|
+
paths = generate_from_sc_text(sc, tmp_path)
|
|
43
|
+
header, _ = _cast(paths[0])
|
|
44
|
+
assert header["width"] == 120
|
|
45
|
+
assert header["height"] == 30
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_cmd_event_typed_as_output(tmp_path):
|
|
49
|
+
paths = generate_from_sc_text(_zero_sc(("cmd", "echo hi")), tmp_path)
|
|
50
|
+
_, cast = _cast(paths[0])
|
|
51
|
+
assert "echo hi" in "".join(e[2] for e in cast)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_output_event_in_cast(tmp_path):
|
|
55
|
+
paths = generate_from_sc_text(_zero_sc(("cmd", "echo hi"), ("out", "hi")), tmp_path)
|
|
56
|
+
_, cast = _cast(paths[0])
|
|
57
|
+
assert "hi" in "".join(e[2] for e in cast)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_exit_wait_at_scene_end(tmp_path):
|
|
61
|
+
sc = _make_sc(
|
|
62
|
+
("dir", "set type_speed 0"),
|
|
63
|
+
("dir", "set cmd_wait 0"),
|
|
64
|
+
("dir", "set enter_wait 0"),
|
|
65
|
+
("dir", "set exit_wait 400"),
|
|
66
|
+
("cmd", "echo x"),
|
|
67
|
+
)
|
|
68
|
+
paths = generate_from_sc_text(sc, tmp_path)
|
|
69
|
+
_, cast = _cast(paths[0])
|
|
70
|
+
assert max(e[0] for e in cast) >= 0.4
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_expect_input_advances_cursor(tmp_path):
|
|
74
|
+
sc = _make_sc(
|
|
75
|
+
("dir", "set type_speed 0"),
|
|
76
|
+
("dir", "set cmd_wait 0"),
|
|
77
|
+
("dir", "set exit_wait 0"),
|
|
78
|
+
("dir", "set enter_wait 0"),
|
|
79
|
+
("dir", "set input_wait 300"),
|
|
80
|
+
("dir", "expect-input secret"),
|
|
81
|
+
)
|
|
82
|
+
paths = generate_from_sc_text(sc, tmp_path)
|
|
83
|
+
_, cast = _cast(paths[0])
|
|
84
|
+
assert max(e[0] for e in cast) >= 0.3
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_sleep_directive_advances_cursor(tmp_path):
|
|
88
|
+
sc = _make_sc(
|
|
89
|
+
("dir", "set type_speed 0"),
|
|
90
|
+
("dir", "set cmd_wait 0"),
|
|
91
|
+
("dir", "set exit_wait 0"),
|
|
92
|
+
("dir", "set enter_wait 0"),
|
|
93
|
+
("cmd", "a"),
|
|
94
|
+
("dir", "sleep 500"),
|
|
95
|
+
("cmd", "b"),
|
|
96
|
+
)
|
|
97
|
+
paths = generate_from_sc_text(sc, tmp_path)
|
|
98
|
+
_, cast = _cast(paths[0])
|
|
99
|
+
assert max(e[0] for e in cast) >= 0.5
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_scene_split_mode(tmp_path):
|
|
103
|
+
sc = _zero_sc(
|
|
104
|
+
("dir", "scene intro"),
|
|
105
|
+
("cmd", "echo a"),
|
|
106
|
+
("dir", "scene outro"),
|
|
107
|
+
("cmd", "echo b"),
|
|
108
|
+
)
|
|
109
|
+
paths = generate_from_sc_text(sc, tmp_path, split_scenes=True)
|
|
110
|
+
assert {p.stem for p in paths} == {"intro", "outro"}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_single_cast_default(tmp_path):
|
|
114
|
+
sc = _zero_sc(
|
|
115
|
+
("dir", "scene first"),
|
|
116
|
+
("cmd", "echo a"),
|
|
117
|
+
("dir", "scene second"),
|
|
118
|
+
("cmd", "echo b"),
|
|
119
|
+
)
|
|
120
|
+
paths = generate_from_sc_text(sc, tmp_path, output_stem="demo")
|
|
121
|
+
assert len(paths) == 1
|
|
122
|
+
assert paths[0].name == "demo.cast"
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def test_single_cast_timestamps_continuous(tmp_path):
|
|
126
|
+
sc = _make_sc(
|
|
127
|
+
("dir", "set type_speed 0"),
|
|
128
|
+
("dir", "set cmd_wait 0"),
|
|
129
|
+
("dir", "set enter_wait 0"),
|
|
130
|
+
("dir", "set exit_wait 200"),
|
|
131
|
+
("dir", "scene a"),
|
|
132
|
+
("cmd", "x"),
|
|
133
|
+
("dir", "scene b"),
|
|
134
|
+
("cmd", "y"),
|
|
135
|
+
)
|
|
136
|
+
paths = generate_from_sc_text(sc, tmp_path, output_stem="out")
|
|
137
|
+
_, cast = _cast(paths[0])
|
|
138
|
+
times = [e[0] for e in cast]
|
|
139
|
+
assert times == sorted(times)
|
|
140
|
+
assert max(times) > 0.2
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def test_generate_from_sc_reads_file(tmp_path):
|
|
144
|
+
sc = _zero_sc(("cmd", "echo hello"))
|
|
145
|
+
sc_file = tmp_path / "demo.sc"
|
|
146
|
+
sc_file.write_text(sc)
|
|
147
|
+
out = tmp_path / "out"
|
|
148
|
+
out.mkdir()
|
|
149
|
+
paths = generate_from_sc(sc_file, out)
|
|
150
|
+
_, cast = _cast(paths[0])
|
|
151
|
+
assert "echo hello" in "".join(e[2] for e in cast)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def test_word_speed_adds_pause_between_words(tmp_path):
|
|
155
|
+
# "echo hello world" has 2 spaces → 2 word pauses of 200ms each = 400ms
|
|
156
|
+
sc = _make_sc(
|
|
157
|
+
("dir", "set type_speed 0"),
|
|
158
|
+
("dir", "set cmd_wait 0"),
|
|
159
|
+
("dir", "set exit_wait 0"),
|
|
160
|
+
("dir", "set enter_wait 0"),
|
|
161
|
+
("dir", "set word_speed 200"),
|
|
162
|
+
("cmd", "echo hello world"),
|
|
163
|
+
)
|
|
164
|
+
paths = generate_from_sc_text(sc, tmp_path)
|
|
165
|
+
_, cast = _cast(paths[0])
|
|
166
|
+
assert max(e[0] for e in cast) >= 0.4
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def test_word_speed_none_mirrors_type_speed(tmp_path):
|
|
170
|
+
# word_speed=None → word pause = type_speed (100ms here); "a b" has 1 space
|
|
171
|
+
# chars: 'a' at 0.1, ' ' at 0.2 + word_pause 0.1 = 0.3, 'b' at 0.4, prompt/sentinel at 0.5
|
|
172
|
+
sc = _make_sc(
|
|
173
|
+
("dir", "set type_speed 100"),
|
|
174
|
+
("dir", "set cmd_wait 0"),
|
|
175
|
+
("dir", "set exit_wait 0"),
|
|
176
|
+
("dir", "set enter_wait 0"),
|
|
177
|
+
("cmd", "a b"),
|
|
178
|
+
)
|
|
179
|
+
paths = generate_from_sc_text(sc, tmp_path)
|
|
180
|
+
_, cast = _cast(paths[0])
|
|
181
|
+
assert max(e[0] for e in cast) >= 0.5
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def test_word_speed_applies_to_expect_input(tmp_path):
|
|
185
|
+
# "foo bar" has 1 space → 1 word pause of 300ms
|
|
186
|
+
sc = _make_sc(
|
|
187
|
+
("dir", "set type_speed 0"),
|
|
188
|
+
("dir", "set cmd_wait 0"),
|
|
189
|
+
("dir", "set exit_wait 0"),
|
|
190
|
+
("dir", "set enter_wait 0"),
|
|
191
|
+
("dir", "set input_wait 0"),
|
|
192
|
+
("dir", "set word_speed 300"),
|
|
193
|
+
("dir", "expect-input foo bar"),
|
|
194
|
+
)
|
|
195
|
+
paths = generate_from_sc_text(sc, tmp_path)
|
|
196
|
+
_, cast = _cast(paths[0])
|
|
197
|
+
assert max(e[0] for e in cast) >= 0.3
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def test_quoted_prompt_in_pre_scene_set(tmp_path):
|
|
201
|
+
# bash traces `"$ "` as `'$ '`; shlex.split must handle shell quoting in the
|
|
202
|
+
# pre-scene directive loop so the cast shows "$ " not "'$"
|
|
203
|
+
sc = _make_sc(
|
|
204
|
+
("dir", "set prompt '$ '"),
|
|
205
|
+
("dir", "scene main"),
|
|
206
|
+
("cmd", "echo hi"),
|
|
207
|
+
)
|
|
208
|
+
paths = generate_from_sc_text(sc, tmp_path)
|
|
209
|
+
_, cast = _cast(paths[0])
|
|
210
|
+
prompt_events = [e for e in cast if e[2] == "$ "]
|
|
211
|
+
assert prompt_events, f"expected '$ ' prompt in cast, got: {cast}"
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def test_build_config_from_sc_text_reads_header():
|
|
215
|
+
import json
|
|
216
|
+
|
|
217
|
+
from scriptcast.generator import build_config_from_sc_text
|
|
218
|
+
header = json.dumps({"version": 1, "shell": "bash", "width": 80, "height": 24,
|
|
219
|
+
"directive-prefix": "SC"})
|
|
220
|
+
sc = header + "\n"
|
|
221
|
+
cfg = build_config_from_sc_text(sc)
|
|
222
|
+
assert cfg.width == 80
|
|
223
|
+
assert cfg.height == 24
|
|
224
|
+
assert cfg.directive_prefix == "SC"
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def test_build_config_from_sc_text_applies_pre_scene_set():
|
|
228
|
+
import json
|
|
229
|
+
|
|
230
|
+
from scriptcast.generator import build_config_from_sc_text
|
|
231
|
+
header = json.dumps({"version": 1, "width": 100, "height": 28, "directive-prefix": "SC"})
|
|
232
|
+
events = [
|
|
233
|
+
json.dumps([0.0, "dir", "set type_speed 20"]),
|
|
234
|
+
json.dumps([0.1, "dir", "set theme-radius 16"]),
|
|
235
|
+
json.dumps([0.2, "dir", "scene main"]),
|
|
236
|
+
json.dumps([0.3, "cmd", "echo hello"]),
|
|
237
|
+
]
|
|
238
|
+
sc = header + "\n" + "\n".join(events)
|
|
239
|
+
cfg = build_config_from_sc_text(sc)
|
|
240
|
+
assert cfg.type_speed == 20
|
|
241
|
+
assert cfg.theme.radius == 16
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def test_build_config_from_sc_text_stops_at_scene():
|
|
245
|
+
import json
|
|
246
|
+
|
|
247
|
+
from scriptcast.generator import build_config_from_sc_text
|
|
248
|
+
header = json.dumps({"version": 1, "width": 100, "height": 28, "directive-prefix": "SC"})
|
|
249
|
+
events = [
|
|
250
|
+
json.dumps([0.0, "dir", "scene main"]),
|
|
251
|
+
json.dumps([0.1, "dir", "set type_speed 99"]),
|
|
252
|
+
]
|
|
253
|
+
sc = header + "\n" + "\n".join(events)
|
|
254
|
+
cfg = build_config_from_sc_text(sc)
|
|
255
|
+
assert cfg.type_speed == 40 # default, not overridden after scene
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def test_build_config_from_sc_text_empty_returns_defaults():
|
|
259
|
+
from scriptcast.generator import build_config_from_sc_text
|
|
260
|
+
cfg = build_config_from_sc_text("")
|
|
261
|
+
assert cfg.type_speed == 40
|
|
262
|
+
assert cfg.theme.frame is True
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
# --- build_config_from_sc_text: base parameter ---
|
|
266
|
+
|
|
267
|
+
def test_build_config_from_sc_text_base_provides_defaults():
|
|
268
|
+
import json
|
|
269
|
+
|
|
270
|
+
from scriptcast.config import ScriptcastConfig
|
|
271
|
+
from scriptcast.generator import build_config_from_sc_text
|
|
272
|
+
base = ScriptcastConfig(prompt="THEME_PROMPT")
|
|
273
|
+
base.theme.radius = 99
|
|
274
|
+
header = json.dumps({"version": 1, "width": 80, "height": 24, "directive-prefix": "SC"})
|
|
275
|
+
sc = header + "\n"
|
|
276
|
+
cfg = build_config_from_sc_text(sc, base=base)
|
|
277
|
+
assert cfg.prompt == "THEME_PROMPT"
|
|
278
|
+
assert cfg.theme.radius == 99
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def test_build_config_from_sc_text_sc_overrides_base():
|
|
282
|
+
import json
|
|
283
|
+
|
|
284
|
+
from scriptcast.config import ScriptcastConfig
|
|
285
|
+
from scriptcast.generator import build_config_from_sc_text
|
|
286
|
+
base = ScriptcastConfig(prompt="THEME_PROMPT")
|
|
287
|
+
header = json.dumps({"version": 1, "width": 80, "height": 24, "directive-prefix": "SC"})
|
|
288
|
+
events = [json.dumps([0.0, "dir", "set prompt SC_PROMPT"])]
|
|
289
|
+
sc = header + "\n" + "\n".join(events)
|
|
290
|
+
cfg = build_config_from_sc_text(sc, base=base)
|
|
291
|
+
assert cfg.prompt == "SC_PROMPT"
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def test_build_config_from_sc_text_base_none_is_default():
|
|
295
|
+
import json
|
|
296
|
+
|
|
297
|
+
from scriptcast.generator import build_config_from_sc_text
|
|
298
|
+
header = json.dumps({"version": 1, "width": 80, "height": 24, "directive-prefix": "SC"})
|
|
299
|
+
sc = header + "\n"
|
|
300
|
+
cfg = build_config_from_sc_text(sc, base=None)
|
|
301
|
+
assert cfg.prompt == "$ "
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
# --- generate_from_sc_text: base parameter ---
|
|
305
|
+
|
|
306
|
+
def test_generate_from_sc_text_base_prompt_in_cast(tmp_path):
|
|
307
|
+
from scriptcast.config import ScriptcastConfig
|
|
308
|
+
base = ScriptcastConfig(prompt="THEME>")
|
|
309
|
+
sc = _zero_sc(
|
|
310
|
+
("dir", "scene main"),
|
|
311
|
+
("cmd", "echo hi"),
|
|
312
|
+
)
|
|
313
|
+
paths = generate_from_sc_text(sc, tmp_path, base=base)
|
|
314
|
+
_, cast = _cast(paths[0])
|
|
315
|
+
prompt_text = "".join(e[2] for e in cast)
|
|
316
|
+
assert "THEME>" in prompt_text, f"Expected 'THEME>' in cast output, got: {prompt_text!r}"
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def test_generate_from_sc_text_sc_prompt_overrides_base(tmp_path):
|
|
320
|
+
from scriptcast.config import ScriptcastConfig
|
|
321
|
+
base = ScriptcastConfig(prompt="THEME>")
|
|
322
|
+
sc = _zero_sc(
|
|
323
|
+
("dir", "set prompt SCRIPT>"),
|
|
324
|
+
("dir", "scene main"),
|
|
325
|
+
("cmd", "echo hi"),
|
|
326
|
+
)
|
|
327
|
+
paths = generate_from_sc_text(sc, tmp_path, base=base)
|
|
328
|
+
_, cast = _cast(paths[0])
|
|
329
|
+
prompt_text = "".join(e[2] for e in cast)
|
|
330
|
+
assert "SCRIPT>" in prompt_text, "Expected 'SCRIPT>' prompt override in cast"
|
|
331
|
+
assert "THEME>" not in prompt_text, "Theme prompt should be overridden"
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def test_generate_from_sc_text_base_type_speed(tmp_path):
|
|
335
|
+
from scriptcast.config import ScriptcastConfig
|
|
336
|
+
# base sets type_speed=200; sc has no override; "ab" = 2 chars → 400ms
|
|
337
|
+
base = ScriptcastConfig(cmd_wait=0, exit_wait=0, enter_wait=0, type_speed=200)
|
|
338
|
+
sc = _make_sc(("cmd", "ab"))
|
|
339
|
+
paths = generate_from_sc_text(sc, tmp_path, base=base)
|
|
340
|
+
_, cast = _cast(paths[0])
|
|
341
|
+
assert max(e[0] for e in cast) >= 0.4
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def test_generate_from_sc_reads_file_with_base(tmp_path):
|
|
345
|
+
from scriptcast.config import ScriptcastConfig
|
|
346
|
+
base = ScriptcastConfig(prompt="FILE>")
|
|
347
|
+
sc = _zero_sc(("dir", "scene main"), ("cmd", "echo hi"))
|
|
348
|
+
sc_file = tmp_path / "demo.sc"
|
|
349
|
+
sc_file.write_text(sc)
|
|
350
|
+
out = tmp_path / "out"
|
|
351
|
+
out.mkdir()
|
|
352
|
+
paths = generate_from_sc(sc_file, out, base=base)
|
|
353
|
+
_, cast = _cast(paths[0])
|
|
354
|
+
prompt_text = "".join(e[2] for e in cast)
|
|
355
|
+
assert "FILE>" in prompt_text
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def test_expect_input_directive_advances_cursor(tmp_path):
|
|
359
|
+
sc = _make_sc(
|
|
360
|
+
("dir", "set type_speed 0"),
|
|
361
|
+
("dir", "set cmd_wait 0"),
|
|
362
|
+
("dir", "set exit_wait 0"),
|
|
363
|
+
("dir", "set enter_wait 0"),
|
|
364
|
+
("dir", "set input_wait 300"),
|
|
365
|
+
("dir", "expect-input secret"),
|
|
366
|
+
)
|
|
367
|
+
paths = generate_from_sc_text(sc, tmp_path)
|
|
368
|
+
_, cast = _cast(paths[0])
|
|
369
|
+
assert max(e[0] for e in cast) >= 0.3
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def test_expect_input_directive_emits_chars(tmp_path):
|
|
373
|
+
sc = _make_sc(
|
|
374
|
+
("dir", "set type_speed 0"),
|
|
375
|
+
("dir", "set cmd_wait 0"),
|
|
376
|
+
("dir", "set exit_wait 0"),
|
|
377
|
+
("dir", "set enter_wait 0"),
|
|
378
|
+
("dir", "set input_wait 0"),
|
|
379
|
+
("dir", "expect-input hi"),
|
|
380
|
+
)
|
|
381
|
+
paths = generate_from_sc_text(sc, tmp_path)
|
|
382
|
+
_, cast = _cast(paths[0])
|
|
383
|
+
all_text = "".join(e[2] for e in cast)
|
|
384
|
+
assert "h" in all_text
|
|
385
|
+
assert "i" in all_text
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def test_out_event_no_suffix_added(tmp_path):
|
|
389
|
+
# Generator must NOT append \r\n — text is emitted verbatim
|
|
390
|
+
paths = generate_from_sc_text(_zero_sc(("out", "hello")), tmp_path)
|
|
391
|
+
_, cast = _cast(paths[0])
|
|
392
|
+
# There must be a cast event with exactly "hello" (no \r\n appended)
|
|
393
|
+
assert any(e[2] == "hello" for e in cast), \
|
|
394
|
+
"expected 'hello' verbatim; old code would emit 'hello\\r\\n'"
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def test_cr_delay_splits_bare_cr(tmp_path):
|
|
398
|
+
# out event with bare \r: with cr_delay=80, two cast events emitted at different times
|
|
399
|
+
sc = _make_sc(
|
|
400
|
+
("dir", "set type_speed 0"),
|
|
401
|
+
("dir", "set cmd_wait 0"),
|
|
402
|
+
("dir", "set exit_wait 0"),
|
|
403
|
+
("dir", "set enter_wait 0"),
|
|
404
|
+
("dir", "set cr_delay 80"),
|
|
405
|
+
("out", "Loading\rDone\n"),
|
|
406
|
+
)
|
|
407
|
+
paths = generate_from_sc_text(sc, tmp_path)
|
|
408
|
+
_, cast = _cast(paths[0])
|
|
409
|
+
all_text = "".join(e[2] for e in cast)
|
|
410
|
+
assert "Loading" in all_text
|
|
411
|
+
assert "\rDone" in all_text
|
|
412
|
+
# The \r split creates two events at different timestamps
|
|
413
|
+
loading_events = [e for e in cast if "Loading" in e[2] and "\r" not in e[2]]
|
|
414
|
+
done_events = [e for e in cast if "\rDone" in e[2]]
|
|
415
|
+
assert loading_events and done_events
|
|
416
|
+
assert done_events[0][0] >= loading_events[0][0] + 0.079
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def test_crlf_not_split_by_cr_delay(tmp_path):
|
|
420
|
+
# \r\n is a line ending — cr_delay must NOT split it
|
|
421
|
+
sc = _make_sc(
|
|
422
|
+
("dir", "set type_speed 0"),
|
|
423
|
+
("dir", "set cmd_wait 0"),
|
|
424
|
+
("dir", "set exit_wait 0"),
|
|
425
|
+
("dir", "set enter_wait 0"),
|
|
426
|
+
("dir", "set cr_delay 80"),
|
|
427
|
+
("out", "hello\r\n"),
|
|
428
|
+
)
|
|
429
|
+
paths = generate_from_sc_text(sc, tmp_path)
|
|
430
|
+
_, cast = _cast(paths[0])
|
|
431
|
+
# hello\r\n should appear as a single event (no split)
|
|
432
|
+
hello_events = [e for e in cast if "hello" in e[2]]
|
|
433
|
+
assert len(hello_events) == 1
|
|
434
|
+
assert hello_events[0][2] == "hello\r\n"
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# tests/test_integration.py
|
|
2
|
+
"""Integration test using examples/tutorial.sh as a real end-to-end fixture."""
|
|
3
|
+
import json
|
|
4
|
+
import shutil
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
from click.testing import CliRunner
|
|
9
|
+
|
|
10
|
+
from scriptcast.__main__ import cli
|
|
11
|
+
|
|
12
|
+
requires_agg = pytest.mark.skipif(shutil.which("agg") is None, reason="agg not installed")
|
|
13
|
+
|
|
14
|
+
TUTORIAL_SCRIPT = Path(__file__).parent.parent / "examples" / "tutorial.sh"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@requires_agg
|
|
18
|
+
def test_basic_example_end_to_end(tmp_path):
|
|
19
|
+
"""examples/tutorial.sh produces a single tutorial.cast with valid asciinema v2 header."""
|
|
20
|
+
runner = CliRunner()
|
|
21
|
+
result = runner.invoke(cli, ["--output-dir", str(tmp_path), str(TUTORIAL_SCRIPT)])
|
|
22
|
+
assert result.exit_code == 0, result.output
|
|
23
|
+
|
|
24
|
+
cast_file = tmp_path / "tutorial.cast"
|
|
25
|
+
assert cast_file.exists()
|
|
26
|
+
|
|
27
|
+
lines = cast_file.read_text().splitlines()
|
|
28
|
+
header = json.loads(lines[0])
|
|
29
|
+
assert header["version"] == 2
|
|
30
|
+
assert header["width"] == 80
|
|
31
|
+
assert header["height"] == 8
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@requires_agg
|
|
35
|
+
def test_basic_example_end_to_end_split_mode(tmp_path):
|
|
36
|
+
"""--split-scenes produces one .cast per scene."""
|
|
37
|
+
runner = CliRunner()
|
|
38
|
+
result = runner.invoke(
|
|
39
|
+
cli,
|
|
40
|
+
["--output-dir", str(tmp_path), "--split-scenes", str(TUTORIAL_SCRIPT)],
|
|
41
|
+
)
|
|
42
|
+
assert result.exit_code == 0, result.output
|
|
43
|
+
|
|
44
|
+
cast_files = sorted(tmp_path.glob("*.cast"))
|
|
45
|
+
names = {f.stem for f in cast_files}
|
|
46
|
+
expected = {"intro", "mock", "expect", "filter", "comment", "sleep", "word_speed", "record"}
|
|
47
|
+
assert names == expected
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_basic_example_record_stage(tmp_path):
|
|
51
|
+
"""scriptcast --no-export on tutorial.sh produces a .sc file with JSONL header."""
|
|
52
|
+
import json as _json
|
|
53
|
+
runner = CliRunner()
|
|
54
|
+
result = runner.invoke(
|
|
55
|
+
cli, ["--output-dir", str(tmp_path), "--no-export", str(TUTORIAL_SCRIPT)],
|
|
56
|
+
)
|
|
57
|
+
assert result.exit_code == 0, result.output
|
|
58
|
+
|
|
59
|
+
sc_file = tmp_path / "tutorial.sc"
|
|
60
|
+
assert sc_file.exists()
|
|
61
|
+
header = _json.loads(sc_file.read_text().splitlines()[0])
|
|
62
|
+
assert header["version"] == 1
|
|
63
|
+
assert "shell" in header
|
|
64
|
+
assert header["directive-prefix"] == "SC"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@requires_agg
|
|
68
|
+
def test_basic_example_mock_scene_shows_command(tmp_path):
|
|
69
|
+
"""`tutorial.cast` contains the mock deploy command as a typed command trace."""
|
|
70
|
+
runner = CliRunner()
|
|
71
|
+
result = runner.invoke(cli, ["--output-dir", str(tmp_path), str(TUTORIAL_SCRIPT)])
|
|
72
|
+
assert result.exit_code == 0, result.output
|
|
73
|
+
|
|
74
|
+
cast_file = tmp_path / "tutorial.cast"
|
|
75
|
+
content = cast_file.read_text()
|
|
76
|
+
# "deploy" should appear as typed output (the mock command)
|
|
77
|
+
all_text = "".join(
|
|
78
|
+
json.loads(ln)[2]
|
|
79
|
+
for ln in content.splitlines()[1:]
|
|
80
|
+
if ln.strip()
|
|
81
|
+
)
|
|
82
|
+
assert "deploy" in all_text
|
|
83
|
+
assert "Deploying to production" in all_text
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@requires_agg
|
|
87
|
+
def test_basic_example_filter_applied(tmp_path):
|
|
88
|
+
"""`tutorial.cast` must not contain the raw working directory path."""
|
|
89
|
+
runner = CliRunner()
|
|
90
|
+
result = runner.invoke(cli, ["--output-dir", str(tmp_path), str(TUTORIAL_SCRIPT)])
|
|
91
|
+
assert result.exit_code == 0, result.output
|
|
92
|
+
|
|
93
|
+
cast_file = tmp_path / "tutorial.cast"
|
|
94
|
+
assert cast_file.exists()
|
|
95
|
+
content = cast_file.read_text()
|
|
96
|
+
assert "/workspaces/scriptcast" not in content
|
|
97
|
+
assert "<project>" in content
|