jet_black 0.6.0 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|