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 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: []