tui-td 0.2.2 → 0.2.4
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 +16 -1
- data/README.md +5 -0
- data/lib/tui_td/cli.rb +39 -2
- data/lib/tui_td/driver.rb +3 -2
- data/lib/tui_td/matchers.rb +27 -0
- data/lib/tui_td/test_runner.rb +73 -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: 97ed62eebe255ca568235b0ffe8bb428812d0dbc0408e24e768d76e8b11beb8a
|
|
4
|
+
data.tar.gz: 8b66334f5408398dab8dcf02535e8473c2d9af7357f4fc1db45bc67ca792706c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 404c5b2c0c411b25846855d0d5ad297493be90672ee3eba53980df429ec1263170b605d023983a4fbc17c2b4563b10ff91c98324a08651cf0997d4a9cde21667
|
|
7
|
+
data.tar.gz: 5b73ca7cccaa81b434ca418e5f246573033ac5abd6c18b7094adad6f65d8fa453be2e8c84d842f2ba82e182a9189989a2389599874e1ab9448606db30677d901
|
data/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,23 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## 0.2.4
|
|
4
|
+
|
|
5
|
+
- `assert_regex` JSON test step — match terminal output against a Ruby regex
|
|
6
|
+
- `assert_not_text` JSON test step — fail if text IS present (inverse of assert_text)
|
|
7
|
+
- `have_regex` RSpec matcher — regex assertions in spec files
|
|
8
|
+
- `have_text` negation in RSpec: `expect(state).not_to have_text("Error")`
|
|
9
|
+
- README step reference table updated (all 13 step types listed)
|
|
10
|
+
|
|
11
|
+
## 0.2.3
|
|
12
|
+
|
|
13
|
+
- `have_exit_status` RSpec matcher and `exitstatus` drive command — exit code testing on all three levels (JSON + RSpec + drive)
|
|
14
|
+
- `env` in start step — inject environment variables per test run
|
|
15
|
+
- Per-step `timeout` override
|
|
16
|
+
- `before_all` / `after_all` hooks for setup and teardown steps
|
|
17
|
+
|
|
3
18
|
## 0.2.2
|
|
4
19
|
|
|
5
|
-
- `wait_for_exit` and `assert_exit` test steps — test process exit codes
|
|
20
|
+
- `wait_for_exit` and `assert_exit` JSON test steps — test process exit codes
|
|
6
21
|
|
|
7
22
|
## 0.2.1
|
|
8
23
|
|
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:
|
|
@@ -291,9 +292,13 @@ tui-td test examples/echo_test.json
|
|
|
291
292
|
| `wait_for_text` | `"text"` | Wait until text appears |
|
|
292
293
|
| `wait_for_stable` | — | Wait until output is stable |
|
|
293
294
|
| `assert_text` | `"text"` | Assert that text exists on screen |
|
|
295
|
+
| `assert_not_text` | `"text"` | Assert that text does NOT exist on screen |
|
|
296
|
+
| `assert_regex` | `"pattern"` | Assert that regex pattern matches (e.g. `"error\|fail"`) |
|
|
294
297
|
| `assert_fg` | `[row, col], "is": "color"` | Assert foreground color |
|
|
295
298
|
| `assert_bg` | `[row, col], "is": "color"` | Assert background color |
|
|
296
299
|
| `assert_style` | `[row, col], "bold": true` | Assert cell style (bold, italic, underline) |
|
|
300
|
+
| `wait_for_exit` | — | Wait until the process exits |
|
|
301
|
+
| `assert_exit` | `N` | Assert the process exit code equals N |
|
|
297
302
|
| `screenshot` | `"path"` | Save PNG screenshot |
|
|
298
303
|
| `html` | `"path"` | Save HTML render for browser viewing |
|
|
299
304
|
| `close` | — | Close the TUI |
|
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.
|
|
@@ -350,6 +366,13 @@ module TUITD
|
|
|
350
366
|
{"assert_text": "<substring>"}
|
|
351
367
|
Fail if the text is not found in the current state.
|
|
352
368
|
|
|
369
|
+
{"assert_not_text": "<substring>"}
|
|
370
|
+
Fail if the text IS found in the current state.
|
|
371
|
+
|
|
372
|
+
{"assert_regex": "<pattern>"}
|
|
373
|
+
Fail if the regex pattern does not match anywhere.
|
|
374
|
+
Pattern syntax is Ruby regex (e.g. "error|fail|warn").
|
|
375
|
+
|
|
353
376
|
{"assert_fg": [row, col], "is": "<color>"}
|
|
354
377
|
Assert foreground color at cell. Colors: "default",
|
|
355
378
|
named ANSI (red, green, blue, cyan, ...), "bright_*",
|
|
@@ -416,6 +439,13 @@ module TUITD
|
|
|
416
439
|
have_text(expected)
|
|
417
440
|
Passes if expected text appears anywhere in the terminal state.
|
|
418
441
|
Usage: expect(state).to have_text("Hello")
|
|
442
|
+
Negate: expect(state).not_to have_text("Error")
|
|
443
|
+
|
|
444
|
+
have_regex(pattern)
|
|
445
|
+
Passes if the regex pattern matches anywhere. Accepts a Regexp
|
|
446
|
+
or a string (parsed as Ruby regex).
|
|
447
|
+
Usage: expect(state).to have_regex(/error|fail/)
|
|
448
|
+
Usage: expect(state).to have_regex("\\d{3}")
|
|
419
449
|
|
|
420
450
|
have_fg(expected).at(row, col)
|
|
421
451
|
Assert foreground color at [row, col] matches expected.
|
|
@@ -428,6 +458,13 @@ module TUITD
|
|
|
428
458
|
have_style.at(row, col).with(bold: true, italic: false, ...)
|
|
429
459
|
Assert style attributes at [row, col] match the given hash.
|
|
430
460
|
Usage: expect(state).to have_style.at(0, 0).with(bold: true)
|
|
461
|
+
|
|
462
|
+
Driver matchers (work on TUITD::Driver, not State)
|
|
463
|
+
--------------------------------------------------
|
|
464
|
+
|
|
465
|
+
have_exit_status(expected)
|
|
466
|
+
Assert the process exit status matches expected.
|
|
467
|
+
Usage: expect(driver).to have_exit_status(0)
|
|
431
468
|
HELP
|
|
432
469
|
exit 0
|
|
433
470
|
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
|
@@ -23,6 +23,17 @@ module TUITD
|
|
|
23
23
|
failure_message_when_negated { |state| "expected terminal NOT to contain #{expected.inspect}" }
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
+
RSpec::Matchers.define :have_regex do |pattern|
|
|
27
|
+
match do |state|
|
|
28
|
+
@regex = pattern.is_a?(Regexp) ? pattern : Regexp.new(pattern.to_s)
|
|
29
|
+
state.find_text(@regex).any?
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
description { "match regex #{pattern.inspect}" }
|
|
33
|
+
failure_message { |state| "expected terminal to match #{pattern.inspect}" }
|
|
34
|
+
failure_message_when_negated { |state| "expected terminal NOT to match #{pattern.inspect}" }
|
|
35
|
+
end
|
|
36
|
+
|
|
26
37
|
RSpec::Matchers.define :have_fg do |expected|
|
|
27
38
|
chain(:at) { |row, col| @row, @col = row, col }
|
|
28
39
|
|
|
@@ -68,5 +79,21 @@ module TUITD
|
|
|
68
79
|
"expected style at [#{@row},#{@col}] to be #{@expected.inspect}, but was #{@actual.inspect}"
|
|
69
80
|
end
|
|
70
81
|
end
|
|
82
|
+
|
|
83
|
+
# Works on a Driver instance, not State
|
|
84
|
+
RSpec::Matchers.define :have_exit_status do |expected|
|
|
85
|
+
match do |driver|
|
|
86
|
+
@actual = driver.exitstatus
|
|
87
|
+
@actual == expected
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
description { "have exit status #{expected}" }
|
|
91
|
+
failure_message do |driver|
|
|
92
|
+
"expected exit status #{expected}, but was #{@actual}"
|
|
93
|
+
end
|
|
94
|
+
failure_message_when_negated do |driver|
|
|
95
|
+
"expected exit status not to be #{expected}"
|
|
96
|
+
end
|
|
97
|
+
end
|
|
71
98
|
end
|
|
72
99
|
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")
|
|
@@ -84,6 +101,25 @@ module TUITD
|
|
|
84
101
|
Result.new(step: action, passed: false, message: "Text NOT found: #{value}")
|
|
85
102
|
end
|
|
86
103
|
|
|
104
|
+
when "assert_not_text"
|
|
105
|
+
ensure_driver!(driver)
|
|
106
|
+
state = State.new(driver.state_data)
|
|
107
|
+
if state.find_text(value.to_s).any?
|
|
108
|
+
Result.new(step: action, passed: false, message: "Text found but should not be: #{value}")
|
|
109
|
+
else
|
|
110
|
+
Result.new(step: action, passed: true, message: "Text not found: #{value}")
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
when "assert_regex"
|
|
114
|
+
ensure_driver!(driver)
|
|
115
|
+
state = State.new(driver.state_data)
|
|
116
|
+
pattern = Regexp.new(value.to_s)
|
|
117
|
+
if state.find_text(pattern).any?
|
|
118
|
+
Result.new(step: action, passed: true, message: "Regex matched: #{value}")
|
|
119
|
+
else
|
|
120
|
+
Result.new(step: action, passed: false, message: "Regex did not match: #{value}")
|
|
121
|
+
end
|
|
122
|
+
|
|
87
123
|
when "assert_fg"
|
|
88
124
|
ensure_driver!(driver)
|
|
89
125
|
row, col = coords(step)
|
|
@@ -165,7 +201,7 @@ module TUITD
|
|
|
165
201
|
r = Result.new(step: action, passed: false, message: "#{e.class}: #{e.message}")
|
|
166
202
|
end
|
|
167
203
|
|
|
168
|
-
|
|
204
|
+
all_results << r
|
|
169
205
|
all_passed &&= r.passed
|
|
170
206
|
|
|
171
207
|
if @on_step
|
|
@@ -176,8 +212,8 @@ module TUITD
|
|
|
176
212
|
# ignore — state retrieval is best-effort
|
|
177
213
|
end
|
|
178
214
|
@on_step.call(
|
|
179
|
-
index:
|
|
180
|
-
total:
|
|
215
|
+
index: all_results.size - 1,
|
|
216
|
+
total: total_steps,
|
|
181
217
|
action: action,
|
|
182
218
|
value: value,
|
|
183
219
|
result: r,
|
|
@@ -186,13 +222,14 @@ module TUITD
|
|
|
186
222
|
)
|
|
187
223
|
end
|
|
188
224
|
end
|
|
225
|
+
end
|
|
189
226
|
|
|
190
227
|
driver&.close
|
|
191
228
|
|
|
192
229
|
{
|
|
193
230
|
name: @plan[:name] || "(unnamed)",
|
|
194
231
|
passed: all_passed,
|
|
195
|
-
results:
|
|
232
|
+
results: all_results.map(&:to_h)
|
|
196
233
|
}
|
|
197
234
|
end
|
|
198
235
|
|
data/lib/tui_td/version.rb
CHANGED