jet_black 0.3.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 +70 -32
- data/.gitignore +2 -0
- data/CHANGELOG.md +25 -0
- data/Dockerfile +10 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +29 -35
- data/README.md +82 -34
- data/Rakefile +2 -0
- data/bin/docker_test +6 -0
- data/bin/jet_black_bin_example +1 -1
- data/jet_black.gemspec +2 -2
- data/lib/jet_black.rb +2 -0
- data/lib/jet_black/ansi_scrubber.rb +22 -0
- data/lib/jet_black/configuration.rb +2 -0
- data/lib/jet_black/environment.rb +3 -1
- data/lib/jet_black/errors.rb +15 -0
- data/lib/jet_black/executed_command.rb +16 -3
- data/lib/jet_black/file_helper.rb +2 -0
- data/lib/jet_black/interactive_command.rb +24 -0
- data/lib/jet_black/non_interactive_command.rb +21 -0
- data/lib/jet_black/rspec.rb +2 -0
- data/lib/jet_black/rspec/matchers.rb +2 -0
- data/lib/jet_black/session.rb +34 -19
- data/lib/jet_black/terminal_session.rb +91 -0
- data/lib/jet_black/version.rb +3 -1
- metadata +26 -15
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
@@ -1,43 +1,81 @@
|
|
1
|
+
---
|
1
2
|
version: 2
|
2
|
-
jobs:
|
3
|
-
build:
|
4
|
-
docker:
|
5
|
-
- image: circleci/ruby:2.5
|
6
|
-
environment:
|
7
|
-
ENABLE_COVERAGE: 1
|
8
3
|
|
9
|
-
|
4
|
+
#-------------------------------------------------------------------------------
|
10
5
|
|
11
|
-
|
12
|
-
|
6
|
+
base_job: &base_job
|
7
|
+
working_directory: ~/repo
|
8
|
+
steps:
|
9
|
+
- checkout
|
13
10
|
|
14
|
-
|
15
|
-
|
11
|
+
- restore_cache:
|
12
|
+
keys:
|
16
13
|
- v1-dependencies-{{ checksum "Gemfile.lock" }}
|
17
14
|
# fallback to using the latest cache if no exact match is found
|
18
15
|
- v1-dependencies-
|
19
16
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
17
|
+
- run:
|
18
|
+
name: install dependencies
|
19
|
+
command: |
|
20
|
+
gem install bundler
|
21
|
+
bundle install --jobs=4 --retry=3 --path vendor/bundle
|
22
|
+
|
23
|
+
- save_cache:
|
24
|
+
paths:
|
25
|
+
- ./vendor/bundle
|
26
|
+
key: v1-dependencies-{{ checksum "Gemfile.lock" }}
|
27
|
+
|
28
|
+
- run:
|
29
|
+
name: run tests
|
30
|
+
command: |
|
31
|
+
mkdir /tmp/test-results
|
32
|
+
bundle exec rspec --format progress \
|
35
33
|
--format RspecJunitFormatter \
|
36
34
|
--out /tmp/test-results/rspec.xml
|
37
35
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
36
|
+
# collect reports
|
37
|
+
- store_test_results:
|
38
|
+
path: /tmp/test-results
|
39
|
+
- store_artifacts:
|
40
|
+
path: /tmp/test-results
|
41
|
+
destination: test-results
|
42
|
+
|
43
|
+
#-------------------------------------------------------------------------------
|
44
|
+
|
45
|
+
jobs:
|
46
|
+
ruby-2.4:
|
47
|
+
<<: *base_job
|
48
|
+
docker:
|
49
|
+
- image: circleci/ruby:2.4
|
50
|
+
ruby-2.5:
|
51
|
+
<<: *base_job
|
52
|
+
docker:
|
53
|
+
- image: circleci/ruby:2.5
|
54
|
+
ruby-2.6:
|
55
|
+
<<: *base_job
|
56
|
+
docker:
|
57
|
+
- image: circleci/ruby:2.6
|
58
|
+
ruby-2.7:
|
59
|
+
<<: *base_job
|
60
|
+
docker:
|
61
|
+
- image: circleci/ruby:2.7
|
62
|
+
environment:
|
63
|
+
ENABLE_COVERAGE: 1
|
64
|
+
ruby-3.0:
|
65
|
+
<<: *base_job
|
66
|
+
docker:
|
67
|
+
- image: circleci/ruby:3.0
|
68
|
+
|
69
|
+
#-------------------------------------------------------------------------------
|
70
|
+
|
71
|
+
# TODO: Migrate to CircleCI matrix
|
72
|
+
|
73
|
+
workflows:
|
74
|
+
version: 2
|
75
|
+
multiple-rubies:
|
76
|
+
jobs:
|
77
|
+
- ruby-2.4
|
78
|
+
- ruby-2.5
|
79
|
+
- ruby-2.6
|
80
|
+
- ruby-2.7
|
81
|
+
- ruby-3.0
|
data/.gitignore
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,30 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## v0.7.0
|
4
|
+
|
5
|
+
- Adds `run_interactive` to allow pseudo-terminal interaction
|
6
|
+
|
7
|
+
## v0.6.0
|
8
|
+
|
9
|
+
- Freeze string literals
|
10
|
+
- Fix deprecation warning: `Bundler.with_clean_env` has been deprecated in
|
11
|
+
favor of `Bundler.with_unbundled_env`
|
12
|
+
|
13
|
+
## v0.5.1
|
14
|
+
|
15
|
+
- Fix missing `bundler` require - thanks @lpender via [#6][pr-6]
|
16
|
+
|
17
|
+
[pr-6]: https://github.com/odlp/jet_black/pull/6
|
18
|
+
|
19
|
+
## v0.5.0
|
20
|
+
|
21
|
+
- `stdout` and `stderr` now keep any trailing newlines at the end of the string.
|
22
|
+
|
23
|
+
## v0.4.0
|
24
|
+
|
25
|
+
- Scrub ANSI escape sequences in `stdout` and `stderr`
|
26
|
+
- Allow environment variables to be unset
|
27
|
+
|
3
28
|
## v0.3.0
|
4
29
|
|
5
30
|
- Allows `stdin` data to be provided when running commands
|
data/Dockerfile
ADDED
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,69 +1,63 @@
|
|
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/
|
8
8
|
specs:
|
9
9
|
coderay (1.1.2)
|
10
|
-
coveralls (0.
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
10
|
+
coveralls (0.8.22)
|
11
|
+
json (>= 1.8, < 3)
|
12
|
+
simplecov (~> 0.16.1)
|
13
|
+
term-ansicolor (~> 1.3)
|
14
|
+
thor (~> 0.19.4)
|
15
|
+
tins (~> 1.6)
|
16
16
|
diff-lcs (1.3)
|
17
|
-
docile (1.3.
|
18
|
-
json (2.1
|
17
|
+
docile (1.3.1)
|
18
|
+
json (2.3.1)
|
19
19
|
method_source (0.9.0)
|
20
|
-
mime-types (3.1)
|
21
|
-
mime-types-data (~> 3.2015)
|
22
|
-
mime-types-data (3.2016.0521)
|
23
|
-
multi_json (1.13.1)
|
24
20
|
pry (0.11.3)
|
25
21
|
coderay (~> 1.1.0)
|
26
22
|
method_source (~> 0.9.0)
|
27
|
-
rake (
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
rspec-
|
32
|
-
|
33
|
-
rspec-
|
34
|
-
rspec-
|
35
|
-
rspec-support (~> 3.7.0)
|
36
|
-
rspec-expectations (3.7.0)
|
23
|
+
rake (13.0.1)
|
24
|
+
rspec (3.8.0)
|
25
|
+
rspec-core (~> 3.8.0)
|
26
|
+
rspec-expectations (~> 3.8.0)
|
27
|
+
rspec-mocks (~> 3.8.0)
|
28
|
+
rspec-core (3.8.0)
|
29
|
+
rspec-support (~> 3.8.0)
|
30
|
+
rspec-expectations (3.8.1)
|
37
31
|
diff-lcs (>= 1.2.0, < 2.0)
|
38
|
-
rspec-support (~> 3.
|
39
|
-
rspec-mocks (3.
|
32
|
+
rspec-support (~> 3.8.0)
|
33
|
+
rspec-mocks (3.8.0)
|
40
34
|
diff-lcs (>= 1.2.0, < 2.0)
|
41
|
-
rspec-support (~> 3.
|
42
|
-
rspec-support (3.
|
43
|
-
rspec_junit_formatter (0.
|
35
|
+
rspec-support (~> 3.8.0)
|
36
|
+
rspec-support (3.8.0)
|
37
|
+
rspec_junit_formatter (0.4.1)
|
44
38
|
rspec-core (>= 2, < 4, != 2.12.0)
|
45
39
|
simplecov (0.16.1)
|
46
40
|
docile (~> 1.1)
|
47
41
|
json (>= 1.8, < 3)
|
48
42
|
simplecov-html (~> 0.10.0)
|
49
43
|
simplecov-html (0.10.2)
|
50
|
-
term-ansicolor (1.
|
51
|
-
tins (~> 0
|
52
|
-
thor (0.
|
53
|
-
tins (
|
44
|
+
term-ansicolor (1.6.0)
|
45
|
+
tins (~> 1.0)
|
46
|
+
thor (0.19.4)
|
47
|
+
tins (1.16.3)
|
54
48
|
|
55
49
|
PLATFORMS
|
56
50
|
ruby
|
57
51
|
|
58
52
|
DEPENDENCIES
|
59
|
-
bundler (
|
53
|
+
bundler (>= 1.16, < 3)
|
60
54
|
coveralls
|
61
55
|
jet_black!
|
62
56
|
pry
|
63
|
-
rake (
|
57
|
+
rake (>= 12.0)
|
64
58
|
rspec (~> 3.0)
|
65
59
|
rspec_junit_formatter
|
66
60
|
simplecov
|
67
61
|
|
68
62
|
BUNDLED WITH
|
69
|
-
1.
|
63
|
+
2.1.4
|
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# JetBlack
|
2
2
|
|
3
|
-
[![CircleCI](https://circleci.com/gh/odlp/jet_black.svg?style=
|
3
|
+
[![Gem Version](https://badge.fury.io/rb/jet_black.svg)](https://rubygems.org/gems/jet_black) [![CircleCI](https://circleci.com/gh/odlp/jet_black.svg?style=shield)](https://circleci.com/gh/odlp/jet_black) [![Coverage Status](https://coveralls.io/repos/github/odlp/jet_black/badge.svg?branch=master)](https://coveralls.io/github/odlp/jet_black?branch=master)
|
4
4
|
|
5
5
|
A black-box testing utility for command line tools and gems. Written in Ruby,
|
6
6
|
with [RSpec] in mind. Features:
|
@@ -8,22 +8,23 @@ with [RSpec] in mind. Features:
|
|
8
8
|
[RSpec]: http://rspec.info/
|
9
9
|
|
10
10
|
- Each session takes place within a unique temporary directory, outside the project
|
11
|
-
- Synchronously [run commands](#running-commands) then write assertions on
|
12
|
-
- `stdout` / `stderr` content
|
13
|
-
- exit status of the process
|
14
|
-
-
|
11
|
+
- Synchronously [run commands](#running-commands) then write assertions on:
|
12
|
+
- The `stdout` / `stderr` content
|
13
|
+
- The exit status of the process
|
14
|
+
- Exercise [interactive command line interfaces](#running-interactive-commands)
|
15
|
+
- Manipulate files in the temporary directory:
|
15
16
|
- [Create files](#file-manipulation)
|
16
17
|
- [Create executable files](#file-manipulation)
|
17
18
|
- [Append content to files](#file-manipulation)
|
18
19
|
- [Copy fixture files](#copying-fixture-files) from your project
|
19
|
-
- Modify the environment without
|
20
|
+
- Modify the environment without changing the parent test process:
|
20
21
|
- [Override environment variables](#environment-variable-overrides)
|
21
22
|
- [Escape the current Bundler context](#clean-bundler-environment)
|
22
23
|
- [Adjust `$PATH`](#path-prefix) to include your executable / Subject Under Test
|
23
24
|
- [RSpec matchers](#rspec-matchers) (optional)
|
24
25
|
|
25
|
-
The temporary directory is discarded after each spec. This means you can write
|
26
|
-
|
26
|
+
The temporary directory is discarded after each spec. This means you can write &
|
27
|
+
modify files and run commands (like `git init`) without worrying about tidying
|
27
28
|
up after or impacting your actual project.
|
28
29
|
|
29
30
|
## Setup
|
@@ -48,6 +49,8 @@ require "jet_black/rspec"
|
|
48
49
|
Any specs you write in the `spec/black_box` folder will then have an inferred
|
49
50
|
`:black_box` meta type, and the matchers will be available in those examples.
|
50
51
|
|
52
|
+
#### Manual RSpec setup
|
53
|
+
|
51
54
|
Alternatively you can manually include the matchers:
|
52
55
|
|
53
56
|
```ruby
|
@@ -71,7 +74,7 @@ require "jet_black"
|
|
71
74
|
session = JetBlack::Session.new
|
72
75
|
result = session.run("echo foo")
|
73
76
|
|
74
|
-
result.stdout # => "foo"
|
77
|
+
result.stdout # => "foo\n"
|
75
78
|
result.stderr # => ""
|
76
79
|
result.exit_status # => 0
|
77
80
|
```
|
@@ -83,6 +86,38 @@ session = JetBlack::Session.new
|
|
83
86
|
session.run("./hello-world", stdin: "Alice")
|
84
87
|
```
|
85
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
|
+
|
86
121
|
### File manipulation
|
87
122
|
|
88
123
|
```ruby
|
@@ -114,40 +149,42 @@ TXT
|
|
114
149
|
### Copying fixture files
|
115
150
|
|
116
151
|
It's ideal to create pertinent files inline within a spec, to provide context
|
117
|
-
for the reader, but sometimes
|
118
|
-
|
152
|
+
for the reader, but sometimes it's better to copy across a large or
|
153
|
+
non-human-readable file.
|
119
154
|
|
120
|
-
|
121
|
-
`spec/fixtures/black_box`. Then configure the fixture path in
|
122
|
-
`spec/support/jet_black.rb`:
|
155
|
+
1. Create a fixture directory in your project, such as `spec/fixtures/black_box`.
|
123
156
|
|
124
|
-
|
125
|
-
require "jet_black"
|
157
|
+
2. Configure the fixture path in `spec/support/jet_black.rb`:
|
126
158
|
|
127
|
-
|
128
|
-
|
129
|
-
end
|
130
|
-
```
|
159
|
+
```ruby
|
160
|
+
require "jet_black"
|
131
161
|
|
132
|
-
|
162
|
+
JetBlack.configure do |config|
|
163
|
+
config.fixture_directory = File.expand_path("../fixtures/black_box", __dir__)
|
164
|
+
end
|
165
|
+
```
|
133
166
|
|
134
|
-
|
135
|
-
session = JetBlack::Session.new
|
136
|
-
session.copy_fixture("src-config.json", "config.json")
|
167
|
+
3. Copy fixtures across into a session's temporary directory:
|
137
168
|
|
138
|
-
|
139
|
-
session
|
140
|
-
|
169
|
+
```ruby
|
170
|
+
session = JetBlack::Session.new
|
171
|
+
session.copy_fixture("src-config.json", "config.json")
|
172
|
+
|
173
|
+
# Destination subdirectories are created for you:
|
174
|
+
session.copy_fixture("src-config.json", "config/config.json")
|
175
|
+
```
|
141
176
|
|
142
177
|
### Environment variable overrides
|
143
178
|
|
144
179
|
```ruby
|
145
180
|
session = JetBlack::Session.new
|
146
|
-
result =
|
181
|
+
result = session.run("printf $FOO", env: { FOO: "bar" })
|
147
182
|
|
148
183
|
result.stdout # => "bar"
|
149
184
|
```
|
150
185
|
|
186
|
+
Provide a `nil` value to unset an environment variable.
|
187
|
+
|
151
188
|
### Clean Bundler environment
|
152
189
|
|
153
190
|
If your project's test suite is invoked with Bundler (e.g. `bundle exec rspec`)
|
@@ -155,15 +192,19 @@ but you want to run commands like `bundle install` and `bundle exec` with a
|
|
155
192
|
different Gemfile in a given spec, you can configure the session or individual
|
156
193
|
commands to run with a clean Bundler environment.
|
157
194
|
|
195
|
+
Per command:
|
196
|
+
|
158
197
|
```ruby
|
159
|
-
# Per command
|
160
198
|
session = JetBlack::Session.new
|
161
|
-
|
199
|
+
session.run("bundle install", options: { clean_bundler_env: true })
|
200
|
+
```
|
162
201
|
|
163
|
-
|
202
|
+
Per session:
|
203
|
+
|
204
|
+
```ruby
|
164
205
|
session = JetBlack::Session.new(options: { clean_bundler_env: true })
|
165
|
-
|
166
|
-
|
206
|
+
session.run("bundle install")
|
207
|
+
session.run("bundle exec rake")
|
167
208
|
```
|
168
209
|
|
169
210
|
### `$PATH` prefix
|
@@ -179,7 +220,7 @@ Configure the `path_prefix` to the directory containing with your executable(s):
|
|
179
220
|
require "jet_black"
|
180
221
|
|
181
222
|
JetBlack.configure do |config|
|
182
|
-
config.path_prefix = File.
|
223
|
+
config.path_prefix = File.expand_path("../../bin", __dir__)
|
183
224
|
end
|
184
225
|
```
|
185
226
|
|
@@ -246,3 +287,10 @@ RSpec.describe "my command line tool" do
|
|
246
287
|
end
|
247
288
|
end
|
248
289
|
```
|
290
|
+
|
291
|
+
## More examples
|
292
|
+
|
293
|
+
- JetBlack's own [higher-level tests](https://github.com/odlp/jet_black/tree/master/spec/features)
|
294
|
+
- A more complex scenario testing a [gem in a fresh Rails app](https://github.com/thoughtbot/capybara_discoball/blob/master/spec/black_box/rails_app_spec.rb#L8-L39). Shows how to:
|
295
|
+
- Include the [gem-under-test via the Rails app's Gemfile](https://github.com/thoughtbot/capybara_discoball/blob/4e89bfe5531eea1bf6dac42c46c26d0c687d6ddf/spec/black_box/rails_app_spec.rb#L99-L104)
|
296
|
+
- Use a [clean Bundler environment](https://github.com/thoughtbot/capybara_discoball/blob/4e89bfe5531eea1bf6dac42c46c26d0c687d6ddf/spec/black_box/rails_app_spec.rb#L5) to use the Gemfile of the new Rails app (instead of the Bundler context of the gem's test suite)
|
data/Rakefile
CHANGED
data/bin/docker_test
ADDED
data/bin/jet_black_bin_example
CHANGED
data/jet_black.gemspec
CHANGED
@@ -18,10 +18,10 @@ Gem::Specification.new do |spec|
|
|
18
18
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
19
19
|
spec.require_paths = ["lib"]
|
20
20
|
|
21
|
-
spec.add_development_dependency "bundler", "
|
21
|
+
spec.add_development_dependency "bundler", ">= 1.16", "< 3"
|
22
22
|
spec.add_development_dependency "coveralls"
|
23
23
|
spec.add_development_dependency "pry"
|
24
|
-
spec.add_development_dependency "rake", "
|
24
|
+
spec.add_development_dependency "rake", ">= 12.0"
|
25
25
|
spec.add_development_dependency "rspec_junit_formatter"
|
26
26
|
spec.add_development_dependency "rspec", "~> 3.0"
|
27
27
|
spec.add_development_dependency "simplecov"
|
data/lib/jet_black.rb
CHANGED
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JetBlack
|
4
|
+
class AnsiScrubber
|
5
|
+
ESCAPE_SEQUENCE = /
|
6
|
+
\x1B # ESC char - start of the sequence
|
7
|
+
(
|
8
|
+
[\x20-\x2F]*
|
9
|
+
[\x40-\x5A\x5C-\x7E]
|
10
|
+
|
|
11
|
+
\[ # Start CSI sequence
|
12
|
+
[\x30-\x3F]+ # CSI Parameter bytes
|
13
|
+
[\x20-\x2F]* # CSI Intermediate bytes
|
14
|
+
[\x40-\x7E] # CSI Finishing byte
|
15
|
+
)
|
16
|
+
/x
|
17
|
+
|
18
|
+
def self.call(string)
|
19
|
+
string.gsub(ESCAPE_SEQUENCE, "")
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module JetBlack
|
2
4
|
class Environment
|
3
5
|
def initialize(raw_env)
|
@@ -14,7 +16,7 @@ module JetBlack
|
|
14
16
|
|
15
17
|
def stringify_env(env)
|
16
18
|
env.map do |key, value|
|
17
|
-
[key.to_s, value
|
19
|
+
[key.to_s, value&.to_s]
|
18
20
|
end.to_h
|
19
21
|
end
|
20
22
|
|
data/lib/jet_black/errors.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module JetBlack
|
2
4
|
class Error < ::StandardError
|
3
5
|
end
|
@@ -31,4 +33,17 @@ module JetBlack
|
|
31
33
|
MSG
|
32
34
|
end
|
33
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
|
34
49
|
end
|
@@ -1,11 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "ansi_scrubber"
|
4
|
+
|
1
5
|
module JetBlack
|
2
6
|
class ExecutedCommand
|
3
|
-
attr_reader :raw_command, :
|
7
|
+
attr_reader :raw_command, :raw_stdout, :raw_stderr,
|
8
|
+
:stdout, :stderr, :exit_status
|
4
9
|
|
5
10
|
def initialize(raw_command:, stdout:, stderr:, exit_status:)
|
6
11
|
@raw_command = raw_command
|
7
|
-
@
|
8
|
-
@
|
12
|
+
@raw_stdout = stdout
|
13
|
+
@raw_stderr = stderr
|
14
|
+
@stdout = scrub(stdout)
|
15
|
+
@stderr = scrub(stderr)
|
9
16
|
@exit_status = exit_status.to_i
|
10
17
|
end
|
11
18
|
|
@@ -16,5 +23,11 @@ module JetBlack
|
|
16
23
|
def failure?
|
17
24
|
!success?
|
18
25
|
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def scrub(output_string)
|
30
|
+
AnsiScrubber.call(output_string.to_s)
|
31
|
+
end
|
19
32
|
end
|
20
33
|
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/rspec.rb
CHANGED
data/lib/jet_black/session.rb
CHANGED
@@ -1,13 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "bundler"
|
3
4
|
require "fileutils"
|
4
5
|
require "forwardable"
|
5
|
-
require "open3"
|
6
6
|
require "tmpdir"
|
7
|
-
require_relative "environment"
|
8
7
|
require_relative "errors"
|
9
8
|
require_relative "executed_command"
|
10
9
|
require_relative "file_helper"
|
10
|
+
require_relative "non_interactive_command"
|
11
|
+
require_relative "interactive_command"
|
11
12
|
|
12
13
|
module JetBlack
|
13
14
|
class Session
|
@@ -26,38 +27,52 @@ module JetBlack
|
|
26
27
|
end
|
27
28
|
|
28
29
|
def run(command, stdin: nil, env: {}, options: {})
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
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
|
33
39
|
end
|
34
40
|
|
35
41
|
private
|
36
42
|
|
37
43
|
attr_reader :session_options, :file_helper
|
38
44
|
|
39
|
-
def
|
40
|
-
|
45
|
+
def exec_non_interactive(raw_command:, stdin:, raw_env:, options:)
|
46
|
+
combined_options = session_options.merge(options)
|
47
|
+
|
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
|
41
52
|
|
42
|
-
|
43
|
-
|
44
|
-
Open3.capture3(env, raw_command, chdir: directory, stdin_data: stdin)
|
53
|
+
def exec_interactive(raw_command:, raw_env:, options:, block:)
|
54
|
+
combined_options = session_options.merge(options)
|
45
55
|
|
46
|
-
|
47
|
-
|
48
|
-
stdout: stdout,
|
49
|
-
stderr: stderr,
|
50
|
-
exit_status: exit_status,
|
51
|
-
)
|
56
|
+
execution_context(combined_options) do
|
57
|
+
InteractiveCommand.new.call(raw_command: raw_command, raw_env: raw_env, directory: directory, block: block)
|
52
58
|
end
|
53
59
|
end
|
54
60
|
|
55
|
-
def
|
61
|
+
def execution_context(options)
|
56
62
|
if options[:clean_bundler_env]
|
57
|
-
Bundler.
|
63
|
+
Bundler.public_send(bundler_clean_environment_method) { yield }
|
58
64
|
else
|
59
65
|
yield
|
60
66
|
end
|
61
67
|
end
|
68
|
+
|
69
|
+
def bundler_clean_environment_method
|
70
|
+
# Bundler 2.x
|
71
|
+
if Bundler.respond_to?(:with_unbundled_env)
|
72
|
+
:with_unbundled_env
|
73
|
+
else
|
74
|
+
:with_clean_env
|
75
|
+
end
|
76
|
+
end
|
62
77
|
end
|
63
78
|
end
|
@@ -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,29 +1,35 @@
|
|
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
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- - "
|
17
|
+
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
19
|
version: '1.16'
|
20
|
+
- - "<"
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '3'
|
20
23
|
type: :development
|
21
24
|
prerelease: false
|
22
25
|
version_requirements: !ruby/object:Gem::Requirement
|
23
26
|
requirements:
|
24
|
-
- - "
|
27
|
+
- - ">="
|
25
28
|
- !ruby/object:Gem::Version
|
26
29
|
version: '1.16'
|
30
|
+
- - "<"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '3'
|
27
33
|
- !ruby/object:Gem::Dependency
|
28
34
|
name: coveralls
|
29
35
|
requirement: !ruby/object:Gem::Requirement
|
@@ -56,16 +62,16 @@ dependencies:
|
|
56
62
|
name: rake
|
57
63
|
requirement: !ruby/object:Gem::Requirement
|
58
64
|
requirements:
|
59
|
-
- - "
|
65
|
+
- - ">="
|
60
66
|
- !ruby/object:Gem::Version
|
61
|
-
version: '
|
67
|
+
version: '12.0'
|
62
68
|
type: :development
|
63
69
|
prerelease: false
|
64
70
|
version_requirements: !ruby/object:Gem::Requirement
|
65
71
|
requirements:
|
66
|
-
- - "
|
72
|
+
- - ">="
|
67
73
|
- !ruby/object:Gem::Version
|
68
|
-
version: '
|
74
|
+
version: '12.0'
|
69
75
|
- !ruby/object:Gem::Dependency
|
70
76
|
name: rspec_junit_formatter
|
71
77
|
requirement: !ruby/object:Gem::Requirement
|
@@ -108,8 +114,8 @@ dependencies:
|
|
108
114
|
- - ">="
|
109
115
|
- !ruby/object:Gem::Version
|
110
116
|
version: '0'
|
111
|
-
description:
|
112
|
-
email:
|
117
|
+
description:
|
118
|
+
email:
|
113
119
|
executables: []
|
114
120
|
extensions: []
|
115
121
|
extra_rdoc_files: []
|
@@ -118,29 +124,35 @@ files:
|
|
118
124
|
- ".gitignore"
|
119
125
|
- ".rspec"
|
120
126
|
- CHANGELOG.md
|
127
|
+
- Dockerfile
|
121
128
|
- Gemfile
|
122
129
|
- Gemfile.lock
|
123
130
|
- LICENSE.txt
|
124
131
|
- README.md
|
125
132
|
- Rakefile
|
133
|
+
- bin/docker_test
|
126
134
|
- bin/jet_black_bin_example
|
127
135
|
- bin/setup
|
128
136
|
- jet_black.gemspec
|
129
137
|
- lib/jet_black.rb
|
138
|
+
- lib/jet_black/ansi_scrubber.rb
|
130
139
|
- lib/jet_black/configuration.rb
|
131
140
|
- lib/jet_black/environment.rb
|
132
141
|
- lib/jet_black/errors.rb
|
133
142
|
- lib/jet_black/executed_command.rb
|
134
143
|
- lib/jet_black/file_helper.rb
|
144
|
+
- lib/jet_black/interactive_command.rb
|
145
|
+
- lib/jet_black/non_interactive_command.rb
|
135
146
|
- lib/jet_black/rspec.rb
|
136
147
|
- lib/jet_black/rspec/matchers.rb
|
137
148
|
- lib/jet_black/session.rb
|
149
|
+
- lib/jet_black/terminal_session.rb
|
138
150
|
- lib/jet_black/version.rb
|
139
151
|
homepage: https://github.com/odlp/jet_black
|
140
152
|
licenses:
|
141
153
|
- MIT
|
142
154
|
metadata: {}
|
143
|
-
post_install_message:
|
155
|
+
post_install_message:
|
144
156
|
rdoc_options: []
|
145
157
|
require_paths:
|
146
158
|
- lib
|
@@ -155,9 +167,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
155
167
|
- !ruby/object:Gem::Version
|
156
168
|
version: '0'
|
157
169
|
requirements: []
|
158
|
-
|
159
|
-
|
160
|
-
signing_key:
|
170
|
+
rubygems_version: 3.1.4
|
171
|
+
signing_key:
|
161
172
|
specification_version: 4
|
162
173
|
summary: Black box testing
|
163
174
|
test_files: []
|