tui-td 0.2.2 → 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 +8 -1
- data/README.md +1 -0
- data/lib/tui_td/cli.rb +25 -2
- data/lib/tui_td/driver.rb +3 -2
- data/lib/tui_td/matchers.rb +16 -0
- data/lib/tui_td/test_runner.rb +54 -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,8 +1,15 @@
|
|
|
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
|
+
|
|
3
10
|
## 0.2.2
|
|
4
11
|
|
|
5
|
-
- `wait_for_exit` and `assert_exit` test steps — test process exit codes
|
|
12
|
+
- `wait_for_exit` and `assert_exit` JSON test steps — test process exit codes
|
|
6
13
|
|
|
7
14
|
## 0.2.1
|
|
8
15
|
|
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.
|
|
@@ -428,6 +444,13 @@ module TUITD
|
|
|
428
444
|
have_style.at(row, col).with(bold: true, italic: false, ...)
|
|
429
445
|
Assert style attributes at [row, col] match the given hash.
|
|
430
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)
|
|
431
454
|
HELP
|
|
432
455
|
exit 0
|
|
433
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
|
|
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
|
-
|
|
50
|
-
begin
|
|
51
|
-
r = case action
|
|
52
|
-
when "start"
|
|
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")
|
|
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
|
+
]
|
|
62
54
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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")
|
|
@@ -165,7 +182,7 @@ module TUITD
|
|
|
165
182
|
r = Result.new(step: action, passed: false, message: "#{e.class}: #{e.message}")
|
|
166
183
|
end
|
|
167
184
|
|
|
168
|
-
|
|
185
|
+
all_results << r
|
|
169
186
|
all_passed &&= r.passed
|
|
170
187
|
|
|
171
188
|
if @on_step
|
|
@@ -176,8 +193,8 @@ module TUITD
|
|
|
176
193
|
# ignore — state retrieval is best-effort
|
|
177
194
|
end
|
|
178
195
|
@on_step.call(
|
|
179
|
-
index:
|
|
180
|
-
total:
|
|
196
|
+
index: all_results.size - 1,
|
|
197
|
+
total: total_steps,
|
|
181
198
|
action: action,
|
|
182
199
|
value: value,
|
|
183
200
|
result: r,
|
|
@@ -186,13 +203,14 @@ module TUITD
|
|
|
186
203
|
)
|
|
187
204
|
end
|
|
188
205
|
end
|
|
206
|
+
end
|
|
189
207
|
|
|
190
208
|
driver&.close
|
|
191
209
|
|
|
192
210
|
{
|
|
193
211
|
name: @plan[:name] || "(unnamed)",
|
|
194
212
|
passed: all_passed,
|
|
195
|
-
results:
|
|
213
|
+
results: all_results.map(&:to_h)
|
|
196
214
|
}
|
|
197
215
|
end
|
|
198
216
|
|
data/lib/tui_td/version.rb
CHANGED