jet_black 0.6.0 → 0.7.0
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/.circleci/config.yml +7 -0
- data/CHANGELOG.md +4 -0
- data/Gemfile.lock +3 -3
- data/README.md +33 -0
- data/lib/jet_black/errors.rb +13 -0
- data/lib/jet_black/interactive_command.rb +24 -0
- data/lib/jet_black/non_interactive_command.rb +21 -0
- data/lib/jet_black/session.rb +23 -18
- data/lib/jet_black/terminal_session.rb +91 -0
- data/lib/jet_black/version.rb +1 -1
- metadata +11 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c389c6caddb4fe0d3b8874f96a50f9c0e4217c3e1af903b6ca9f19bca24065c5
|
|
4
|
+
data.tar.gz: 5c1775b10ac55262d4291a9748858776a618400dfb4fa4561357147802822ce3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9ddb6513193a3395baef52fab1bf86b3cf1de9f5d04091b82f52cd35e8bc97c2a3e21ca7549855e7d9db7947cb1af3b5f7263ce1b9929382dd7743ceb05d7d91
|
|
7
|
+
data.tar.gz: eac5b131ecce2f9b5aff51b1220d3276f320972bbf712f2e1efc7794265972c72f1e6fa5197f3c69548d574ac0f1bbae5193cf16961c9daa2b382c7a652d88b6
|
data/.circleci/config.yml
CHANGED
|
@@ -61,9 +61,15 @@ jobs:
|
|
|
61
61
|
- image: circleci/ruby:2.7
|
|
62
62
|
environment:
|
|
63
63
|
ENABLE_COVERAGE: 1
|
|
64
|
+
ruby-3.0:
|
|
65
|
+
<<: *base_job
|
|
66
|
+
docker:
|
|
67
|
+
- image: circleci/ruby:3.0
|
|
64
68
|
|
|
65
69
|
#-------------------------------------------------------------------------------
|
|
66
70
|
|
|
71
|
+
# TODO: Migrate to CircleCI matrix
|
|
72
|
+
|
|
67
73
|
workflows:
|
|
68
74
|
version: 2
|
|
69
75
|
multiple-rubies:
|
|
@@ -72,3 +78,4 @@ workflows:
|
|
|
72
78
|
- ruby-2.5
|
|
73
79
|
- ruby-2.6
|
|
74
80
|
- ruby-2.7
|
|
81
|
+
- ruby-3.0
|
data/CHANGELOG.md
CHANGED
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
jet_black (0.
|
|
4
|
+
jet_black (0.7.0)
|
|
5
5
|
|
|
6
6
|
GEM
|
|
7
7
|
remote: https://rubygems.org/
|
|
@@ -15,7 +15,7 @@ GEM
|
|
|
15
15
|
tins (~> 1.6)
|
|
16
16
|
diff-lcs (1.3)
|
|
17
17
|
docile (1.3.1)
|
|
18
|
-
json (2.1
|
|
18
|
+
json (2.3.1)
|
|
19
19
|
method_source (0.9.0)
|
|
20
20
|
pry (0.11.3)
|
|
21
21
|
coderay (~> 1.1.0)
|
|
@@ -60,4 +60,4 @@ DEPENDENCIES
|
|
|
60
60
|
simplecov
|
|
61
61
|
|
|
62
62
|
BUNDLED WITH
|
|
63
|
-
2.1.
|
|
63
|
+
2.1.4
|
data/README.md
CHANGED
|
@@ -11,6 +11,7 @@ with [RSpec] in mind. Features:
|
|
|
11
11
|
- Synchronously [run commands](#running-commands) then write assertions on:
|
|
12
12
|
- The `stdout` / `stderr` content
|
|
13
13
|
- The exit status of the process
|
|
14
|
+
- Exercise [interactive command line interfaces](#running-interactive-commands)
|
|
14
15
|
- Manipulate files in the temporary directory:
|
|
15
16
|
- [Create files](#file-manipulation)
|
|
16
17
|
- [Create executable files](#file-manipulation)
|
|
@@ -85,6 +86,38 @@ session = JetBlack::Session.new
|
|
|
85
86
|
session.run("./hello-world", stdin: "Alice")
|
|
86
87
|
```
|
|
87
88
|
|
|
89
|
+
### Running interactive commands
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
session = JetBlack::Session.new
|
|
93
|
+
|
|
94
|
+
result = session.run_interactive("./hello-world") do |terminal|
|
|
95
|
+
terminal.expect("What's your name?", reply: "Alice")
|
|
96
|
+
terminal.expect("What's your location?", reply: "Wonderland")
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
expect(result.exit_status).to eq 0
|
|
100
|
+
expect(result.stdout).to eq <<~TXT
|
|
101
|
+
What's your name?
|
|
102
|
+
Alice
|
|
103
|
+
What's your location?
|
|
104
|
+
Wonderland
|
|
105
|
+
Hello Alice in Wonderland
|
|
106
|
+
TXT
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
If you don't want to wait for a process to finish, you can end the interactive
|
|
110
|
+
session early:
|
|
111
|
+
|
|
112
|
+
```ruby
|
|
113
|
+
session = JetBlack::Session.new
|
|
114
|
+
|
|
115
|
+
result = session.run_interactive("./long-cli-flow") do |terminal|
|
|
116
|
+
terminal.expect("Question 1", reply: "Y")
|
|
117
|
+
terminal.end_session(signal: "INT")
|
|
118
|
+
end
|
|
119
|
+
```
|
|
120
|
+
|
|
88
121
|
### File manipulation
|
|
89
122
|
|
|
90
123
|
```ruby
|
data/lib/jet_black/errors.rb
CHANGED
|
@@ -33,4 +33,17 @@ module JetBlack
|
|
|
33
33
|
MSG
|
|
34
34
|
end
|
|
35
35
|
end
|
|
36
|
+
|
|
37
|
+
class TerminalSessionTimeoutError < Error
|
|
38
|
+
attr_reader :terminal
|
|
39
|
+
|
|
40
|
+
def initialize(terminal, expected_value, timeout)
|
|
41
|
+
@terminal = terminal
|
|
42
|
+
|
|
43
|
+
super <<~MSG
|
|
44
|
+
Interactive terminal session timed out after #{timeout} second(s).
|
|
45
|
+
Waiting for: '#{expected_value}'
|
|
46
|
+
MSG
|
|
47
|
+
end
|
|
48
|
+
end
|
|
36
49
|
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
require_relative "environment"
|
|
2
|
+
require_relative "terminal_session"
|
|
3
|
+
|
|
4
|
+
module JetBlack
|
|
5
|
+
class InteractiveCommand
|
|
6
|
+
def call(raw_command:, raw_env:, directory:, block:)
|
|
7
|
+
env = Environment.new(raw_env).to_h
|
|
8
|
+
terminal = TerminalSession.new(raw_command, env: env, directory: directory)
|
|
9
|
+
|
|
10
|
+
unless block.nil?
|
|
11
|
+
block.call(terminal)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
terminal.wait_for_finish
|
|
15
|
+
|
|
16
|
+
ExecutedCommand.new(
|
|
17
|
+
raw_command: raw_command,
|
|
18
|
+
stdout: terminal.stdout,
|
|
19
|
+
stderr: terminal.stderr,
|
|
20
|
+
exit_status: terminal.exit_status,
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
require "open3"
|
|
2
|
+
require_relative "environment"
|
|
3
|
+
|
|
4
|
+
module JetBlack
|
|
5
|
+
class NonInteractiveCommand
|
|
6
|
+
def call(raw_command:, stdin:, raw_env:, directory:)
|
|
7
|
+
env = Environment.new(raw_env).to_h
|
|
8
|
+
|
|
9
|
+
stdout, stderr, exit_status = Open3.capture3(
|
|
10
|
+
env, raw_command, chdir: directory, stdin_data: stdin
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
ExecutedCommand.new(
|
|
14
|
+
raw_command: raw_command,
|
|
15
|
+
stdout: stdout,
|
|
16
|
+
stderr: stderr,
|
|
17
|
+
exit_status: exit_status,
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
data/lib/jet_black/session.rb
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
require "bundler"
|
|
4
4
|
require "fileutils"
|
|
5
5
|
require "forwardable"
|
|
6
|
-
require "open3"
|
|
7
6
|
require "tmpdir"
|
|
8
|
-
require_relative "environment"
|
|
9
7
|
require_relative "errors"
|
|
10
8
|
require_relative "executed_command"
|
|
11
9
|
require_relative "file_helper"
|
|
10
|
+
require_relative "non_interactive_command"
|
|
11
|
+
require_relative "interactive_command"
|
|
12
12
|
|
|
13
13
|
module JetBlack
|
|
14
14
|
class Session
|
|
@@ -27,33 +27,38 @@ module JetBlack
|
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
def run(command, stdin: nil, env: {}, options: {})
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
exec_non_interactive(raw_command: command, stdin: stdin, raw_env: env, options: options).tap do |executed_command|
|
|
31
|
+
commands << executed_command
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def run_interactive(command, env: {}, options: {}, &block)
|
|
36
|
+
exec_interactive(raw_command: command, raw_env: env, options: options, block: block).tap do |executed_command|
|
|
37
|
+
commands << executed_command
|
|
38
|
+
end
|
|
34
39
|
end
|
|
35
40
|
|
|
36
41
|
private
|
|
37
42
|
|
|
38
43
|
attr_reader :session_options, :file_helper
|
|
39
44
|
|
|
40
|
-
def
|
|
41
|
-
|
|
45
|
+
def exec_non_interactive(raw_command:, stdin:, raw_env:, options:)
|
|
46
|
+
combined_options = session_options.merge(options)
|
|
42
47
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
execution_context(combined_options) do
|
|
49
|
+
NonInteractiveCommand.new.call(raw_command: raw_command, stdin: stdin, raw_env: raw_env, directory: directory)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def exec_interactive(raw_command:, raw_env:, options:, block:)
|
|
54
|
+
combined_options = session_options.merge(options)
|
|
46
55
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
stdout: stdout,
|
|
50
|
-
stderr: stderr,
|
|
51
|
-
exit_status: exit_status,
|
|
52
|
-
)
|
|
56
|
+
execution_context(combined_options) do
|
|
57
|
+
InteractiveCommand.new.call(raw_command: raw_command, raw_env: raw_env, directory: directory, block: block)
|
|
53
58
|
end
|
|
54
59
|
end
|
|
55
60
|
|
|
56
|
-
def
|
|
61
|
+
def execution_context(options)
|
|
57
62
|
if options[:clean_bundler_env]
|
|
58
63
|
Bundler.public_send(bundler_clean_environment_method) { yield }
|
|
59
64
|
else
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
require "pty"
|
|
2
|
+
require "expect"
|
|
3
|
+
require_relative "errors"
|
|
4
|
+
|
|
5
|
+
module JetBlack
|
|
6
|
+
class TerminalSession
|
|
7
|
+
DEFAULT_TIMEOUT = 10
|
|
8
|
+
|
|
9
|
+
attr_reader :exit_status
|
|
10
|
+
|
|
11
|
+
def initialize(raw_command, env:, directory:)
|
|
12
|
+
@stderr_reader, @stderr_writer = IO.pipe
|
|
13
|
+
@output, @input, @pid = PTY.spawn(env, raw_command, chdir: directory, err: stderr_writer.fileno)
|
|
14
|
+
self.raw_stdout = []
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def expect(expected_value, reply: nil, timeout: DEFAULT_TIMEOUT, signal_on_timeout: "KILL")
|
|
18
|
+
output_matches = output.expect(expected_value, timeout)
|
|
19
|
+
|
|
20
|
+
if output_matches.nil?
|
|
21
|
+
end_session(signal: signal_on_timeout)
|
|
22
|
+
raise TerminalSessionTimeoutError.new(self, expected_value, timeout)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
raw_stdout.concat(output_matches)
|
|
26
|
+
|
|
27
|
+
if reply != nil
|
|
28
|
+
input.puts(reply)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def stdout
|
|
33
|
+
raw_stdout.join.gsub("\r", "")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def stderr
|
|
37
|
+
raw_std_err
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def wait_for_finish
|
|
41
|
+
return if finished?
|
|
42
|
+
|
|
43
|
+
finalize_io
|
|
44
|
+
|
|
45
|
+
self.exit_status = wait_for_exit_status
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def end_session(signal: "INT")
|
|
49
|
+
Process.kill(signal, pid)
|
|
50
|
+
finalize_io
|
|
51
|
+
|
|
52
|
+
self.exit_status = wait_for_exit_status
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def finished?
|
|
56
|
+
!exit_status.nil?
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
attr_accessor :raw_stdout, :raw_std_err
|
|
62
|
+
attr_reader :input, :output, :pid, :stderr_reader, :stderr_writer
|
|
63
|
+
attr_writer :exit_status
|
|
64
|
+
|
|
65
|
+
def finalize_io
|
|
66
|
+
drain_stdout
|
|
67
|
+
drain_stderr
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def wait_for_exit_status
|
|
71
|
+
_, pty_status = Process.waitpid2(pid)
|
|
72
|
+
pty_status.exitstatus || pty_status.termsig
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def drain_stdout
|
|
76
|
+
until output.eof? do
|
|
77
|
+
raw_stdout << output.readline
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
input.close
|
|
81
|
+
output.close
|
|
82
|
+
rescue Errno::EIO => e # https://github.com/ruby/ruby/blob/57fb2199059cb55b632d093c2e64c8a3c60acfbb/ext/pty/pty.c#L521
|
|
83
|
+
warn("Rescued #{e.message}") if ENV.key?("DEBUG")
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def drain_stderr
|
|
87
|
+
stderr_writer.close
|
|
88
|
+
self.raw_std_err = stderr_reader.read
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
data/lib/jet_black/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: jet_black
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Oli Peate
|
|
8
|
-
autorequire:
|
|
8
|
+
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2021-02-12 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bundler
|
|
@@ -114,8 +114,8 @@ dependencies:
|
|
|
114
114
|
- - ">="
|
|
115
115
|
- !ruby/object:Gem::Version
|
|
116
116
|
version: '0'
|
|
117
|
-
description:
|
|
118
|
-
email:
|
|
117
|
+
description:
|
|
118
|
+
email:
|
|
119
119
|
executables: []
|
|
120
120
|
extensions: []
|
|
121
121
|
extra_rdoc_files: []
|
|
@@ -141,15 +141,18 @@ files:
|
|
|
141
141
|
- lib/jet_black/errors.rb
|
|
142
142
|
- lib/jet_black/executed_command.rb
|
|
143
143
|
- lib/jet_black/file_helper.rb
|
|
144
|
+
- lib/jet_black/interactive_command.rb
|
|
145
|
+
- lib/jet_black/non_interactive_command.rb
|
|
144
146
|
- lib/jet_black/rspec.rb
|
|
145
147
|
- lib/jet_black/rspec/matchers.rb
|
|
146
148
|
- lib/jet_black/session.rb
|
|
149
|
+
- lib/jet_black/terminal_session.rb
|
|
147
150
|
- lib/jet_black/version.rb
|
|
148
151
|
homepage: https://github.com/odlp/jet_black
|
|
149
152
|
licenses:
|
|
150
153
|
- MIT
|
|
151
154
|
metadata: {}
|
|
152
|
-
post_install_message:
|
|
155
|
+
post_install_message:
|
|
153
156
|
rdoc_options: []
|
|
154
157
|
require_paths:
|
|
155
158
|
- lib
|
|
@@ -164,8 +167,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
164
167
|
- !ruby/object:Gem::Version
|
|
165
168
|
version: '0'
|
|
166
169
|
requirements: []
|
|
167
|
-
rubygems_version: 3.1.
|
|
168
|
-
signing_key:
|
|
170
|
+
rubygems_version: 3.1.4
|
|
171
|
+
signing_key:
|
|
169
172
|
specification_version: 4
|
|
170
173
|
summary: Black box testing
|
|
171
174
|
test_files: []
|