jet_black 0.3.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 +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
|
-
[](https://rubygems.org/gems/jet_black) [](https://circleci.com/gh/odlp/jet_black) [](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: []
|