tui-td 0.2.1 → 0.2.3
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +11 -0
- data/README.md +1 -0
- data/lib/tui_td/cli.rb +32 -3
- data/lib/tui_td/driver.rb +12 -2
- data/lib/tui_td/matchers.rb +16 -0
- data/lib/tui_td/test_runner.rb +70 -36
- data/lib/tui_td/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 739ccd98a6d9a5ee8f29eadfdaaaa86cde8ee67cebf7bd9c293a20e1785f4bdb
|
|
4
|
+
data.tar.gz: bbeb1b4fcc9def290a68ee73df31d63d4fb3cb5a7a9a55b8d8cf5b8a57394885
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6bb830eee8c25605c0cc1ecf7e5692fe5db1144c5c7cc2a6144367139056c6a2aa8650390429647d51b263058c21e78b46ee01c085fd0662ef3879ca551184e3
|
|
7
|
+
data.tar.gz: cbefe0d9f7483787190763563d5a1919aa8c476c1b57cddd704b981974b652b3b1734c59c60031ffd12e59d940a4a07f1abe937ab35012249a88a7303ca29073
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## 0.2.3
|
|
4
|
+
|
|
5
|
+
- `have_exit_status` RSpec matcher and `exitstatus` drive command — exit code testing on all three levels (JSON + RSpec + drive)
|
|
6
|
+
- `env` in start step — inject environment variables per test run
|
|
7
|
+
- Per-step `timeout` override
|
|
8
|
+
- `before_all` / `after_all` hooks for setup and teardown steps
|
|
9
|
+
|
|
10
|
+
## 0.2.2
|
|
11
|
+
|
|
12
|
+
- `wait_for_exit` and `assert_exit` JSON test steps — test process exit codes
|
|
13
|
+
|
|
3
14
|
## 0.2.1
|
|
4
15
|
|
|
5
16
|
- `Driver#refresh` — explicit state re-parse for MCP server clients
|
data/README.md
CHANGED
|
@@ -120,6 +120,7 @@ Interactive commands (drive mode):
|
|
|
120
120
|
key <name> Send keystroke (enter, tab, escape, up, down, left, right,
|
|
121
121
|
backspace, ctrl_c, ctrl_d)
|
|
122
122
|
<text> Send text to the TUI
|
|
123
|
+
exitstatus Show process exit status (nil if running)
|
|
123
124
|
exit Quit drive mode
|
|
124
125
|
|
|
125
126
|
Global options:
|
data/lib/tui_td/cli.rb
CHANGED
|
@@ -41,6 +41,7 @@ module TUITD
|
|
|
41
41
|
opts.separator " key <name> Send keystroke (enter, tab, escape, up, down, left, right,"
|
|
42
42
|
opts.separator " backspace, ctrl_c, ctrl_d)"
|
|
43
43
|
opts.separator " <text> Send text to the TUI"
|
|
44
|
+
opts.separator " exitstatus Show process exit status (nil if running)"
|
|
44
45
|
opts.separator " exit Quit drive mode"
|
|
45
46
|
opts.separator ""
|
|
46
47
|
opts.separator "Global options:"
|
|
@@ -188,6 +189,9 @@ module TUITD
|
|
|
188
189
|
puts driver.state_json(pretty: true)
|
|
189
190
|
elsif input == "raw"
|
|
190
191
|
puts driver.raw_output[0..2000]
|
|
192
|
+
elsif input == "exitstatus"
|
|
193
|
+
status = driver.exitstatus
|
|
194
|
+
puts status ? "Exit status: #{status}" : "Process still running"
|
|
191
195
|
elsif input.start_with?("key ")
|
|
192
196
|
driver.send_keys(input.split(" ", 2).last.to_sym)
|
|
193
197
|
else
|
|
@@ -327,12 +331,24 @@ module TUITD
|
|
|
327
331
|
A test is a Hash or JSON string: {"name": "...", "steps": [...]}
|
|
328
332
|
|
|
329
333
|
Top-level keys: name, steps, rows (default 40), cols (default 120),
|
|
330
|
-
timeout (default 30), chdir
|
|
334
|
+
timeout (default 30), chdir, before_all, after_all
|
|
335
|
+
|
|
336
|
+
before_all / after_all are arrays of steps that run before and
|
|
337
|
+
after the main steps list. Useful for setup/teardown:
|
|
338
|
+
|
|
339
|
+
"before_all": [{"start": "my_tui"}, {"wait_for_text": "> "}],
|
|
340
|
+
"steps": [{"send": "hello\\n"}],
|
|
341
|
+
"after_all": [{"close": true}]
|
|
342
|
+
|
|
343
|
+
Each step can also set a per-step "timeout" (in seconds):
|
|
344
|
+
|
|
345
|
+
{"wait_for_text": "Slow", "timeout": 60}
|
|
331
346
|
|
|
332
347
|
Each step is an object with a single action key:
|
|
333
348
|
|
|
334
349
|
{"start": "<command>"}
|
|
335
|
-
Start a TUI process in a PTY.
|
|
350
|
+
Start a TUI process in a PTY. Environment variables can be
|
|
351
|
+
passed via "env": {"FOO": "bar", "BAZ": "qux"}.
|
|
336
352
|
|
|
337
353
|
{"send": "<text>"}
|
|
338
354
|
Send text to the TUI. Use "\\n" for Enter.
|
|
@@ -367,8 +383,14 @@ module TUITD
|
|
|
367
383
|
{"html": "<path>"}
|
|
368
384
|
Save an HTML render. Path defaults to /tmp/tui_td_<ts>.html.
|
|
369
385
|
|
|
386
|
+
{"wait_for_exit": true}
|
|
387
|
+
Wait until the process exits naturally.
|
|
388
|
+
|
|
389
|
+
{"assert_exit": <N>}
|
|
390
|
+
Assert the process exit status equals N.
|
|
391
|
+
|
|
370
392
|
{"close": true}
|
|
371
|
-
Close the driver session.
|
|
393
|
+
Close the driver session (force-kill if needed).
|
|
372
394
|
|
|
373
395
|
Example test file: examples/echo_test.json
|
|
374
396
|
HELP
|
|
@@ -422,6 +444,13 @@ module TUITD
|
|
|
422
444
|
have_style.at(row, col).with(bold: true, italic: false, ...)
|
|
423
445
|
Assert style attributes at [row, col] match the given hash.
|
|
424
446
|
Usage: expect(state).to have_style.at(0, 0).with(bold: true)
|
|
447
|
+
|
|
448
|
+
Driver matchers (work on TUITD::Driver, not State)
|
|
449
|
+
--------------------------------------------------
|
|
450
|
+
|
|
451
|
+
have_exit_status(expected)
|
|
452
|
+
Assert the process exit status matches expected.
|
|
453
|
+
Usage: expect(driver).to have_exit_status(0)
|
|
425
454
|
HELP
|
|
426
455
|
exit 0
|
|
427
456
|
end
|
data/lib/tui_td/driver.rb
CHANGED
|
@@ -19,12 +19,13 @@ module TUITD
|
|
|
19
19
|
class Driver
|
|
20
20
|
attr_reader :command, :state
|
|
21
21
|
|
|
22
|
-
def initialize(command, rows: 40, cols: 120, timeout: 30, chdir: nil)
|
|
22
|
+
def initialize(command, rows: 40, cols: 120, timeout: 30, chdir: nil, env: {})
|
|
23
23
|
@command = command
|
|
24
24
|
@rows = rows
|
|
25
25
|
@cols = cols
|
|
26
26
|
@timeout = timeout
|
|
27
27
|
@chdir = chdir
|
|
28
|
+
@env = env
|
|
28
29
|
@state = nil
|
|
29
30
|
@stdin = nil
|
|
30
31
|
@stdout = nil
|
|
@@ -37,7 +38,7 @@ module TUITD
|
|
|
37
38
|
|
|
38
39
|
# Start the TUI application in a PTY
|
|
39
40
|
def start
|
|
40
|
-
env = { "TERM" => "xterm-256color", "COLUMNS" => @cols.to_s, "LINES" => @rows.to_s }
|
|
41
|
+
env = { "TERM" => "xterm-256color", "COLUMNS" => @cols.to_s, "LINES" => @rows.to_s }.merge(@env.transform_keys(&:to_s))
|
|
41
42
|
spawn_opts = {}
|
|
42
43
|
spawn_opts[:chdir] = @chdir if @chdir
|
|
43
44
|
|
|
@@ -117,6 +118,15 @@ module TUITD
|
|
|
117
118
|
@wait_thr&.value
|
|
118
119
|
end
|
|
119
120
|
|
|
121
|
+
# Get the process exit status (nil if still running)
|
|
122
|
+
def exitstatus
|
|
123
|
+
return nil unless @wait_thr
|
|
124
|
+
status = @wait_thr.value
|
|
125
|
+
status&.exitstatus
|
|
126
|
+
rescue NoMethodError
|
|
127
|
+
nil
|
|
128
|
+
end
|
|
129
|
+
|
|
120
130
|
# Get the terminal output (raw ANSI + text)
|
|
121
131
|
def raw_output
|
|
122
132
|
read_available!
|
data/lib/tui_td/matchers.rb
CHANGED
|
@@ -68,5 +68,21 @@ module TUITD
|
|
|
68
68
|
"expected style at [#{@row},#{@col}] to be #{@expected.inspect}, but was #{@actual.inspect}"
|
|
69
69
|
end
|
|
70
70
|
end
|
|
71
|
+
|
|
72
|
+
# Works on a Driver instance, not State
|
|
73
|
+
RSpec::Matchers.define :have_exit_status do |expected|
|
|
74
|
+
match do |driver|
|
|
75
|
+
@actual = driver.exitstatus
|
|
76
|
+
@actual == expected
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
description { "have exit status #{expected}" }
|
|
80
|
+
failure_message do |driver|
|
|
81
|
+
"expected exit status #{expected}, but was #{@actual}"
|
|
82
|
+
end
|
|
83
|
+
failure_message_when_negated do |driver|
|
|
84
|
+
"expected exit status not to be #{expected}"
|
|
85
|
+
end
|
|
86
|
+
end
|
|
71
87
|
end
|
|
72
88
|
end
|
data/lib/tui_td/test_runner.rb
CHANGED
|
@@ -13,16 +13,20 @@ module TUITD
|
|
|
13
13
|
# {
|
|
14
14
|
# "name": "My test",
|
|
15
15
|
# "rows": 24, "cols": 80, "timeout": 10,
|
|
16
|
+
# "chdir": "/path/to/workdir",
|
|
17
|
+
# "before_all": [{"start": "my_tui", "env": {"FOO": "bar"}}],
|
|
16
18
|
# "steps": [
|
|
17
|
-
# {"start": "my_tui"},
|
|
18
19
|
# {"wait_for_text": "> "},
|
|
19
20
|
# {"send": "hello\n"},
|
|
20
21
|
# {"assert_text": "hello"},
|
|
21
|
-
# {"assert_fg": [0, 0], "is": "cyan"}
|
|
22
|
-
#
|
|
23
|
-
# ]
|
|
22
|
+
# {"assert_fg": [0, 0], "is": "cyan"}
|
|
23
|
+
# ],
|
|
24
|
+
# "after_all": [{"close": true}]
|
|
24
25
|
# }
|
|
25
26
|
#
|
|
27
|
+
# Per-step "timeout" overrides the top-level default:
|
|
28
|
+
# {"wait_for_text": "Slow", "timeout": 60}
|
|
29
|
+
#
|
|
26
30
|
class TestRunner
|
|
27
31
|
Result = Struct.new(:step, :passed, :message, keyword_init: true)
|
|
28
32
|
|
|
@@ -30,47 +34,60 @@ module TUITD
|
|
|
30
34
|
raw = source.is_a?(String) ? JSON.parse(source) : source
|
|
31
35
|
@plan = raw.transform_keys(&:to_sym)
|
|
32
36
|
@plan[:steps] = @plan[:steps].map { |s| s.transform_keys(&:to_sym) }
|
|
37
|
+
@plan[:before_all] = @plan[:before_all]&.map { |s| s.transform_keys(&:to_sym) }
|
|
38
|
+
@plan[:after_all] = @plan[:after_all]&.map { |s| s.transform_keys(&:to_sym) }
|
|
33
39
|
@on_step = on_step
|
|
34
40
|
end
|
|
35
41
|
|
|
36
42
|
def run
|
|
37
|
-
results = []
|
|
38
|
-
all_passed = true
|
|
39
43
|
driver = nil
|
|
40
44
|
rows = @plan[:rows] || 40
|
|
41
45
|
cols = @plan[:cols] || 120
|
|
42
46
|
timeout = @plan[:timeout] || 30
|
|
43
47
|
chdir = @plan[:chdir]
|
|
44
48
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
+
hooks = [
|
|
50
|
+
{ label: :before_all, steps: @plan[:before_all] || [] },
|
|
51
|
+
{ label: :main, steps: @plan[:steps] },
|
|
52
|
+
{ label: :after_all, steps: @plan[:after_all] || [] }
|
|
53
|
+
]
|
|
49
54
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
driver&.close
|
|
54
|
-
driver = Driver.new(value.to_s, rows: rows, cols: cols, timeout: timeout, chdir: chdir)
|
|
55
|
-
driver.start
|
|
56
|
-
Result.new(step: action, passed: true, message: "Started: #{value}")
|
|
57
|
-
|
|
58
|
-
when "send"
|
|
59
|
-
ensure_driver!(driver)
|
|
60
|
-
driver.send(value.to_s)
|
|
61
|
-
Result.new(step: action, passed: true, message: "Sent #{value.to_s.length} characters")
|
|
62
|
-
|
|
63
|
-
when "send_key"
|
|
64
|
-
ensure_driver!(driver)
|
|
65
|
-
driver.send_keys(value.to_s.to_sym)
|
|
66
|
-
Result.new(step: action, passed: true, message: "Sent key: #{value}")
|
|
55
|
+
all_results = []
|
|
56
|
+
all_passed = true
|
|
57
|
+
total_steps = hooks.sum { |p| p[:steps].size }
|
|
67
58
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
59
|
+
hooks.each do |phase|
|
|
60
|
+
phase[:steps].each do |step|
|
|
61
|
+
action = step.keys.first.to_s
|
|
62
|
+
value = step.values.first
|
|
72
63
|
|
|
73
|
-
|
|
64
|
+
begin
|
|
65
|
+
step_timeout = step[:timeout] || timeout
|
|
66
|
+
r = case action
|
|
67
|
+
when "start"
|
|
68
|
+
driver&.close
|
|
69
|
+
env = step[:env] || {}
|
|
70
|
+
env = env.transform_keys(&:to_sym).transform_values(&:to_s) if env.is_a?(Hash)
|
|
71
|
+
driver = Driver.new(value.to_s, rows: rows, cols: cols, timeout: step_timeout, chdir: chdir, env: env)
|
|
72
|
+
driver.start
|
|
73
|
+
Result.new(step: action, passed: true, message: "Started: #{value}")
|
|
74
|
+
|
|
75
|
+
when "send"
|
|
76
|
+
ensure_driver!(driver)
|
|
77
|
+
driver.send(value.to_s)
|
|
78
|
+
Result.new(step: action, passed: true, message: "Sent #{value.to_s.length} characters")
|
|
79
|
+
|
|
80
|
+
when "send_key"
|
|
81
|
+
ensure_driver!(driver)
|
|
82
|
+
driver.send_keys(value.to_s.to_sym)
|
|
83
|
+
Result.new(step: action, passed: true, message: "Sent key: #{value}")
|
|
84
|
+
|
|
85
|
+
when "wait_for_text"
|
|
86
|
+
ensure_driver!(driver)
|
|
87
|
+
driver.wait_for_text(value.to_s)
|
|
88
|
+
Result.new(step: action, passed: true, message: "Found: #{value}")
|
|
89
|
+
|
|
90
|
+
when "wait_for_stable"
|
|
74
91
|
ensure_driver!(driver)
|
|
75
92
|
driver.wait_for_stable
|
|
76
93
|
Result.new(step: action, passed: true, message: "Stable")
|
|
@@ -136,6 +153,22 @@ module TUITD
|
|
|
136
153
|
HtmlRenderer.new(driver.state_data).render(path)
|
|
137
154
|
Result.new(step: action, passed: true, message: "Saved: #{path}")
|
|
138
155
|
|
|
156
|
+
when "wait_for_exit"
|
|
157
|
+
ensure_driver!(driver)
|
|
158
|
+
driver.wait_for_exit
|
|
159
|
+
status = driver.exitstatus
|
|
160
|
+
Result.new(step: action, passed: true, message: "Exited with status #{status}")
|
|
161
|
+
|
|
162
|
+
when "assert_exit"
|
|
163
|
+
ensure_driver!(driver)
|
|
164
|
+
expected = value.to_s.to_i
|
|
165
|
+
actual = driver.exitstatus
|
|
166
|
+
if actual == expected
|
|
167
|
+
Result.new(step: action, passed: true, message: "Exit status #{expected} matches")
|
|
168
|
+
else
|
|
169
|
+
Result.new(step: action, passed: false, message: "Exit status #{actual}, expected #{expected}")
|
|
170
|
+
end
|
|
171
|
+
|
|
139
172
|
when "close"
|
|
140
173
|
driver&.close
|
|
141
174
|
driver = nil
|
|
@@ -149,7 +182,7 @@ module TUITD
|
|
|
149
182
|
r = Result.new(step: action, passed: false, message: "#{e.class}: #{e.message}")
|
|
150
183
|
end
|
|
151
184
|
|
|
152
|
-
|
|
185
|
+
all_results << r
|
|
153
186
|
all_passed &&= r.passed
|
|
154
187
|
|
|
155
188
|
if @on_step
|
|
@@ -160,8 +193,8 @@ module TUITD
|
|
|
160
193
|
# ignore — state retrieval is best-effort
|
|
161
194
|
end
|
|
162
195
|
@on_step.call(
|
|
163
|
-
index:
|
|
164
|
-
total:
|
|
196
|
+
index: all_results.size - 1,
|
|
197
|
+
total: total_steps,
|
|
165
198
|
action: action,
|
|
166
199
|
value: value,
|
|
167
200
|
result: r,
|
|
@@ -170,13 +203,14 @@ module TUITD
|
|
|
170
203
|
)
|
|
171
204
|
end
|
|
172
205
|
end
|
|
206
|
+
end
|
|
173
207
|
|
|
174
208
|
driver&.close
|
|
175
209
|
|
|
176
210
|
{
|
|
177
211
|
name: @plan[:name] || "(unnamed)",
|
|
178
212
|
passed: all_passed,
|
|
179
|
-
results:
|
|
213
|
+
results: all_results.map(&:to_h)
|
|
180
214
|
}
|
|
181
215
|
end
|
|
182
216
|
|
data/lib/tui_td/version.rb
CHANGED