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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 492919b04c06523b9dca80a4d30939272ae1d435ea83c183f94cae6ab7d11501
4
- data.tar.gz: c5157a559982f91bfe2560a6283b96229dade980b35fd52f9e446c4e339772e5
3
+ metadata.gz: c389c6caddb4fe0d3b8874f96a50f9c0e4217c3e1af903b6ca9f19bca24065c5
4
+ data.tar.gz: 5c1775b10ac55262d4291a9748858776a618400dfb4fa4561357147802822ce3
5
5
  SHA512:
6
- metadata.gz: d2e3877e059b410b276486990e042722d134abb2c9a4111183a9fcfdd3f902bcd742ff426003fb4c44966e4f0d70ba651709f3833f1cc44048fd3ccddee6e764
7
- data.tar.gz: ed0f71a6940b5cde28e0c61bcdcbcd76db183a0808a0d7d97d56ba476aab4f885954ab24386f2329c167972430a04d0591105273a93ee4e63a005647ad38df17
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
- working_directory: ~/repo
4
+ #-------------------------------------------------------------------------------
10
5
 
11
- steps:
12
- - checkout
6
+ base_job: &base_job
7
+ working_directory: ~/repo
8
+ steps:
9
+ - checkout
13
10
 
14
- - restore_cache:
15
- keys:
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
- - run:
21
- name: install dependencies
22
- command: |
23
- bundle install --jobs=4 --retry=3 --path vendor/bundle
24
-
25
- - save_cache:
26
- paths:
27
- - ./vendor/bundle
28
- key: v1-dependencies-{{ checksum "Gemfile.lock" }}
29
-
30
- - run:
31
- name: run tests
32
- command: |
33
- mkdir /tmp/test-results
34
- bundle exec rspec --format progress \
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
- # collect reports
39
- - store_test_results:
40
- path: /tmp/test-results
41
- - store_artifacts:
42
- path: /tmp/test-results
43
- destination: test-results
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
@@ -9,3 +9,5 @@
9
9
 
10
10
  # rspec failure tracking
11
11
  .rspec_status
12
+
13
+ .ruby-version
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
@@ -0,0 +1,10 @@
1
+ FROM ruby:2.7
2
+
3
+ WORKDIR /app
4
+
5
+ COPY Gemfile* jet_black.gemspec /app/
6
+ COPY lib/jet_black/version.rb /app/lib/jet_black/version.rb
7
+
8
+ RUN bundle install --jobs=4 --retry=3
9
+
10
+ COPY . /app
data/Gemfile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source "https://rubygems.org"
2
4
 
3
5
  # Specify your gem's dependencies in jet_black.gemspec
data/Gemfile.lock CHANGED
@@ -1,69 +1,63 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- jet_black (0.3.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.7.2)
11
- multi_json (~> 1.3)
12
- rest-client (= 1.6.7)
13
- simplecov (>= 0.7)
14
- term-ansicolor (= 1.2.2)
15
- thor (= 0.18.1)
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.0)
18
- json (2.1.0)
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 (10.5.0)
28
- rest-client (1.6.7)
29
- mime-types (>= 1.16)
30
- rspec (3.7.0)
31
- rspec-core (~> 3.7.0)
32
- rspec-expectations (~> 3.7.0)
33
- rspec-mocks (~> 3.7.0)
34
- rspec-core (3.7.1)
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.7.0)
39
- rspec-mocks (3.7.0)
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.7.0)
42
- rspec-support (3.7.1)
43
- rspec_junit_formatter (0.3.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.2.2)
51
- tins (~> 0.8)
52
- thor (0.18.1)
53
- tins (0.13.2)
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 (~> 1.16)
53
+ bundler (>= 1.16, < 3)
60
54
  coveralls
61
55
  jet_black!
62
56
  pry
63
- rake (~> 10.0)
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.16.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=svg)](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)
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 the:
12
- - `stdout` / `stderr` content
13
- - exit status of the process
14
- - Conveniently manipulate files in the temporary directory:
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 modifying the parent test process:
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
- & modify files and run commands (like `git init`) without worrying about tidying
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 a large or non-readable file is best copied
118
- across.
152
+ for the reader, but sometimes it's better to copy across a large or
153
+ non-human-readable file.
119
154
 
120
- First create a fixture directory in your project, such as
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
- ```ruby
125
- require "jet_black"
157
+ 2. Configure the fixture path in `spec/support/jet_black.rb`:
126
158
 
127
- JetBlack.configure do |config|
128
- config.fixture_directory = File.expand_path("../fixtures/black_box", __dir__)
129
- end
130
- ```
159
+ ```ruby
160
+ require "jet_black"
131
161
 
132
- Now you can copy fixtures across into a session's temporary directory:
162
+ JetBlack.configure do |config|
163
+ config.fixture_directory = File.expand_path("../fixtures/black_box", __dir__)
164
+ end
165
+ ```
133
166
 
134
- ```ruby
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
- # Destination subdirectories are created for you:
139
- session.copy_fixture("src-config.json", "config/config.json")
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 = subject.run("echo $FOO", env: { FOO: "bar" })
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
- subject.run("bundle install", options: { clean_bundler_env: true })
199
+ session.run("bundle install", options: { clean_bundler_env: true })
200
+ ```
162
201
 
163
- # Per session
202
+ Per session:
203
+
204
+ ```ruby
164
205
  session = JetBlack::Session.new(options: { clean_bundler_env: true })
165
- subject.run("bundle install")
166
- subject.run("bundle exec rake")
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.expand("../../bin", __dir__)
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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "bundler/gem_tasks"
2
4
  require "rspec/core/rake_task"
3
5
 
data/bin/docker_test ADDED
@@ -0,0 +1,6 @@
1
+ #!/bin/sh
2
+
3
+ set -euo pipefail
4
+
5
+ docker build -t odlp/jet_black .
6
+ docker run odlp/jet_black bundle exec rspec --tty
@@ -1,3 +1,3 @@
1
- #!/usr/bin/env bash
1
+ #!/bin/sh
2
2
 
3
3
  echo "Hello from example bin"
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", "~> 1.16"
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", "~> 10.0"
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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "jet_black/configuration"
2
4
  require "jet_black/session"
3
5
  require "jet_black/version"
@@ -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 Configuration
3
5
  attr_accessor :fixture_directory, :path_prefix
@@ -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.to_s]
19
+ [key.to_s, value&.to_s]
18
20
  end.to_h
19
21
  end
20
22
 
@@ -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, :stdout, :stderr, :exit_status
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
- @stdout = stdout.chomp
8
- @stderr = stderr.chomp
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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "fileutils"
2
4
  require_relative "errors"
3
5
 
@@ -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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "rspec/core"
2
4
  require_relative "rspec/matchers"
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "rspec/expectations"
2
4
 
3
5
  module JetBlack
@@ -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
- combined_options = session_options.merge(options)
30
- executed_command = exec_command(command, stdin, env, combined_options)
31
- commands << executed_command
32
- executed_command
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 exec_command(raw_command, stdin, raw_env, options)
40
- env = Environment.new(raw_env).to_h
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
- command_context(options) do
43
- stdout, stderr, exit_status =
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
- ExecutedCommand.new(
47
- raw_command: raw_command,
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 command_context(options)
61
+ def execution_context(options)
56
62
  if options[:clean_bundler_env]
57
- Bundler.with_clean_env { yield }
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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module JetBlack
2
- VERSION = "0.3.0"
4
+ VERSION = "0.7.0"
3
5
  end
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.3.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: 2018-06-06 00:00:00.000000000 Z
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: '10.0'
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: '10.0'
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
- rubyforge_project:
159
- rubygems_version: 2.7.6
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: []