jet_black 0.2.0 → 0.3.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: aa0fa42b8b1218e280038ce18fa583a3a8288d4cd91c5a6445c34d03b8c68c5f
4
- data.tar.gz: d5c94f0eb5852106f2959dd4745f27d401f67adcc2d79247e9979dd3760a8935
3
+ metadata.gz: 492919b04c06523b9dca80a4d30939272ae1d435ea83c183f94cae6ab7d11501
4
+ data.tar.gz: c5157a559982f91bfe2560a6283b96229dade980b35fd52f9e446c4e339772e5
5
5
  SHA512:
6
- metadata.gz: 5ca15f316fac15876965b704f37432ee80ceb4867c13b1a0cc0e4063f79bd50ec3b60a2a462f600afb753c08c260c665a71065ba3e35d8dd470436acb0128e83
7
- data.tar.gz: c371bf859447eeac0514167f9f1869132595cf745d6086dcb1482d0feb7622c886ed3f89ffd8cd881c8f5855718be2386bb0c17bd236ad664a9594e8353dbdde
6
+ metadata.gz: d2e3877e059b410b276486990e042722d134abb2c9a4111183a9fcfdd3f902bcd742ff426003fb4c44966e4f0d70ba651709f3833f1cc44048fd3ccddee6e764
7
+ data.tar.gz: ed0f71a6940b5cde28e0c61bcdcbcd76db183a0808a0d7d97d56ba476aab4f885954ab24386f2329c167972430a04d0591105273a93ee4e63a005647ad38df17
@@ -2,7 +2,9 @@ version: 2
2
2
  jobs:
3
3
  build:
4
4
  docker:
5
- - image: circleci/ruby:2.5
5
+ - image: circleci/ruby:2.5
6
+ environment:
7
+ ENABLE_COVERAGE: 1
6
8
 
7
9
  working_directory: ~/repo
8
10
 
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.3.0
4
+
5
+ - Allows `stdin` data to be provided when running commands
6
+ - Adds `create_executable` to which writes a file and adds execute permission
7
+
3
8
  ## v0.2.0
4
9
 
5
10
  - Adds RSpec matchers `have_stdout`, `have_stderr`, `have_no_stdout`, &
@@ -1,18 +1,32 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- jet_black (0.2.0)
4
+ jet_black (0.3.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
16
  diff-lcs (1.3)
17
+ docile (1.3.0)
18
+ json (2.1.0)
11
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)
12
24
  pry (0.11.3)
13
25
  coderay (~> 1.1.0)
14
26
  method_source (~> 0.9.0)
15
27
  rake (10.5.0)
28
+ rest-client (1.6.7)
29
+ mime-types (>= 1.16)
16
30
  rspec (3.7.0)
17
31
  rspec-core (~> 3.7.0)
18
32
  rspec-expectations (~> 3.7.0)
@@ -28,17 +42,28 @@ GEM
28
42
  rspec-support (3.7.1)
29
43
  rspec_junit_formatter (0.3.0)
30
44
  rspec-core (>= 2, < 4, != 2.12.0)
45
+ simplecov (0.16.1)
46
+ docile (~> 1.1)
47
+ json (>= 1.8, < 3)
48
+ simplecov-html (~> 0.10.0)
49
+ 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)
31
54
 
32
55
  PLATFORMS
33
56
  ruby
34
57
 
35
58
  DEPENDENCIES
36
59
  bundler (~> 1.16)
60
+ coveralls
37
61
  jet_black!
38
62
  pry
39
63
  rake (~> 10.0)
40
64
  rspec (~> 3.0)
41
65
  rspec_junit_formatter
66
+ simplecov
42
67
 
43
68
  BUNDLED WITH
44
69
  1.16.1
data/README.md CHANGED
@@ -1,16 +1,248 @@
1
1
  # JetBlack
2
2
 
3
- [![CircleCI](https://circleci.com/gh/odlp/jet_black.svg?style=svg)](https://circleci.com/gh/odlp/jet_black)
4
-
5
- A black-box testing utility for command line tools, written in Ruby. Features:
6
-
7
- - Each session runs in a temporary directory
8
- - Captures `stdout`, `stderr` and exit status of a command
9
- - Keeps a history of commands executed
10
- - Temporarily overriding environment variables for a command
11
- - Copying fixture files to the temporary directory
12
- - Appending content to files in the temporary directory
13
- - Adding a path prefix to include your executable
14
- - Option to escape the Bundler environment (allowing `bundle exec` with
15
- different Gemfiles)
16
- - RSpec matchers for `stdout` and `stderr`
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)
4
+
5
+ A black-box testing utility for command line tools and gems. Written in Ruby,
6
+ with [RSpec] in mind. Features:
7
+
8
+ [RSpec]: http://rspec.info/
9
+
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:
15
+ - [Create files](#file-manipulation)
16
+ - [Create executable files](#file-manipulation)
17
+ - [Append content to files](#file-manipulation)
18
+ - [Copy fixture files](#copying-fixture-files) from your project
19
+ - Modify the environment without modifying the parent test process:
20
+ - [Override environment variables](#environment-variable-overrides)
21
+ - [Escape the current Bundler context](#clean-bundler-environment)
22
+ - [Adjust `$PATH`](#path-prefix) to include your executable / Subject Under Test
23
+ - [RSpec matchers](#rspec-matchers) (optional)
24
+
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
27
+ up after or impacting your actual project.
28
+
29
+ ## Setup
30
+
31
+ ```ruby
32
+ group :test do
33
+ gem "jet_black"
34
+ end
35
+ ```
36
+
37
+ ### RSpec setup
38
+
39
+ If you're using RSpec, you can load matchers with the following require
40
+ (optional):
41
+
42
+ ```ruby
43
+ # spec/spec_helper.rb
44
+
45
+ require "jet_black/rspec"
46
+ ```
47
+
48
+ Any specs you write in the `spec/black_box` folder will then have an inferred
49
+ `:black_box` meta type, and the matchers will be available in those examples.
50
+
51
+ Alternatively you can manually include the matchers:
52
+
53
+ ```ruby
54
+ # spec/cli/example_spec.rb
55
+
56
+ require "jet_black"
57
+ require "jet_black/rspec/matchers"
58
+
59
+ RSpec.describe "my command line tool" do
60
+ include JetBlack::RSpec::Matchers
61
+ end
62
+ ```
63
+
64
+ ## Usage
65
+
66
+ ### Running commands
67
+
68
+ ```ruby
69
+ require "jet_black"
70
+
71
+ session = JetBlack::Session.new
72
+ result = session.run("echo foo")
73
+
74
+ result.stdout # => "foo"
75
+ result.stderr # => ""
76
+ result.exit_status # => 0
77
+ ```
78
+
79
+ Providing `stdin` data:
80
+
81
+ ```ruby
82
+ session = JetBlack::Session.new
83
+ session.run("./hello-world", stdin: "Alice")
84
+ ```
85
+
86
+ ### File manipulation
87
+
88
+ ```ruby
89
+ session = JetBlack::Session.new
90
+
91
+ session.create_file "file.txt", <<~TXT
92
+ The quick brown fox
93
+ jumps over the lazy dog
94
+ TXT
95
+
96
+ session.create_executable "hello-world.sh", <<~SH
97
+ #!/bin/sh
98
+ echo "Hello world"
99
+ SH
100
+
101
+ session.append_to_file "file.txt", <<~TXT
102
+ shiny
103
+ new
104
+ lines
105
+ TXT
106
+
107
+ # Subdirectories are created for you:
108
+ session.create_file "deeper/underground/jamiroquai.txt", <<~TXT
109
+ I'm going deeper underground, hey ha
110
+ There's too much panic in this town
111
+ TXT
112
+ ```
113
+
114
+ ### Copying fixture files
115
+
116
+ 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.
119
+
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`:
123
+
124
+ ```ruby
125
+ require "jet_black"
126
+
127
+ JetBlack.configure do |config|
128
+ config.fixture_directory = File.expand_path("../fixtures/black_box", __dir__)
129
+ end
130
+ ```
131
+
132
+ Now you can copy fixtures across into a session's temporary directory:
133
+
134
+ ```ruby
135
+ session = JetBlack::Session.new
136
+ session.copy_fixture("src-config.json", "config.json")
137
+
138
+ # Destination subdirectories are created for you:
139
+ session.copy_fixture("src-config.json", "config/config.json")
140
+ ```
141
+
142
+ ### Environment variable overrides
143
+
144
+ ```ruby
145
+ session = JetBlack::Session.new
146
+ result = subject.run("echo $FOO", env: { FOO: "bar" })
147
+
148
+ result.stdout # => "bar"
149
+ ```
150
+
151
+ ### Clean Bundler environment
152
+
153
+ If your project's test suite is invoked with Bundler (e.g. `bundle exec rspec`)
154
+ but you want to run commands like `bundle install` and `bundle exec` with a
155
+ different Gemfile in a given spec, you can configure the session or individual
156
+ commands to run with a clean Bundler environment.
157
+
158
+ ```ruby
159
+ # Per command
160
+ session = JetBlack::Session.new
161
+ subject.run("bundle install", options: { clean_bundler_env: true })
162
+
163
+ # Per session
164
+ session = JetBlack::Session.new(options: { clean_bundler_env: true })
165
+ subject.run("bundle install")
166
+ subject.run("bundle exec rake")
167
+ ```
168
+
169
+ ### `$PATH` prefix
170
+
171
+ Given the root of your project contains a `bin` directory containing
172
+ `my_awesome_bin`.
173
+
174
+ Configure the `path_prefix` to the directory containing with your executable(s):
175
+
176
+ ```ruby
177
+ # spec/support/jet_black.rb
178
+
179
+ require "jet_black"
180
+
181
+ JetBlack.configure do |config|
182
+ config.path_prefix = File.expand("../../bin", __dir__)
183
+ end
184
+ ```
185
+
186
+ Then the `$PATH` of each session will include the configured directory, and your
187
+ executable should be invokable:
188
+
189
+ ```ruby
190
+ JetBlack::Session.new.run("my_awesome_bin")
191
+ ```
192
+
193
+ ### RSpec matchers
194
+
195
+ Given the [RSpec setup](#rspec-setup) is configured, you'll have access to the
196
+ following matchers:
197
+
198
+ - `have_stdout` which accepts a string or regular expression
199
+ - `have_stderr` which accepts a string or regular expression
200
+ - `have_no_stdout` which asserts the `stdout` is empty
201
+ - `have_no_stderr` which asserts the `stderr` is empty
202
+
203
+ And the following predicate matchers:
204
+
205
+ - `be_a_success` / `be_success` asserts the exit status was zero
206
+ - `be_a_failure` / `be_failure` asserts the exit status was not zero
207
+
208
+ #### Example assertions
209
+
210
+ ```ruby
211
+ # spec/black_box/cli_spec.rb
212
+
213
+ RSpec.describe "my command line tool" do
214
+ let(:session) { JetBlack::Session.new }
215
+
216
+ it "does the work" do
217
+ expect(session.run("my_tool --good")).
218
+ to be_a_success.and have_stdout(/It worked/)
219
+ end
220
+
221
+ it "explodes with incorrect arguments" do
222
+ expect(session.run("my_tool --bad")).
223
+ to be_a_failure.and have_stderr("Oh no!")
224
+ end
225
+ end
226
+ ```
227
+
228
+ However these assertions can be made with built-in matchers too:
229
+
230
+ ```ruby
231
+ RSpec.describe "my command line tool" do
232
+ let(:session) { JetBlack::Session.new }
233
+
234
+ it "does the work" do
235
+ result = session.run("my_tool --good")
236
+
237
+ expect(result.stdout).to match(/It worked/)
238
+ expect(result.exit_status).to eq 0
239
+ end
240
+
241
+ it "explodes with incorrect arguments" do
242
+ result = session.run("my_tool --bad")
243
+
244
+ expect(result.stderr).to match("Oh no!")
245
+ expect(result.exit_status).to eq 1
246
+ end
247
+ end
248
+ ```
@@ -19,8 +19,10 @@ Gem::Specification.new do |spec|
19
19
  spec.require_paths = ["lib"]
20
20
 
21
21
  spec.add_development_dependency "bundler", "~> 1.16"
22
+ spec.add_development_dependency "coveralls"
22
23
  spec.add_development_dependency "pry"
23
24
  spec.add_development_dependency "rake", "~> 10.0"
24
25
  spec.add_development_dependency "rspec_junit_formatter"
25
26
  spec.add_development_dependency "rspec", "~> 3.0"
27
+ spec.add_development_dependency "simplecov"
26
28
  end
@@ -0,0 +1,66 @@
1
+ require "fileutils"
2
+ require_relative "errors"
3
+
4
+ module JetBlack
5
+ class FileHelper
6
+ def initialize(working_directory)
7
+ @working_directory = working_directory
8
+ end
9
+
10
+ def create_file(file_path, file_content)
11
+ resolved_file_path = resolve_working_path(file_path)
12
+ resolved_dir = File.dirname(resolved_file_path)
13
+
14
+ FileUtils.mkdir_p(resolved_dir)
15
+ File.write(resolved_file_path, file_content)
16
+ end
17
+
18
+ def create_executable(file_path, file_content)
19
+ resolved_file_path = resolve_working_path(file_path)
20
+
21
+ create_file(file_path, file_content)
22
+ FileUtils.chmod("+x", resolved_file_path)
23
+ end
24
+
25
+ def append_to_file(file_path, append_content)
26
+ resolved_file_path = resolve_working_path(file_path)
27
+
28
+ unless File.exist?(resolved_file_path)
29
+ raise JetBlack::NonExistentFileError.new(file_path, resolved_file_path)
30
+ end
31
+
32
+ File.open(resolved_file_path, "a") do |file|
33
+ file.write(append_content)
34
+ end
35
+ end
36
+
37
+ def copy_fixture(source_path, destination_path)
38
+ source_fixture_dir = JetBlack.configuration.fixture_directory
39
+ resolved_source_path = File.expand_path(source_path, source_fixture_dir)
40
+
41
+ resolved_destination_path = resolve_working_path(destination_path)
42
+ resolved_destination_dir = File.dirname(resolved_destination_path)
43
+
44
+ if source_fixture_dir.nil?
45
+ raise Error.new("Please configure the fixture_directory")
46
+ end
47
+
48
+ FileUtils.mkdir_p(resolved_destination_dir)
49
+ FileUtils.cp(resolved_source_path, resolved_destination_path)
50
+ end
51
+
52
+ private
53
+
54
+ attr_reader :working_directory
55
+
56
+ def resolve_working_path(file_path)
57
+ resolved_file_path = File.expand_path(file_path, working_directory)
58
+
59
+ unless resolved_file_path.start_with?(working_directory)
60
+ raise JetBlack::InvalidPathError.new(file_path, resolved_file_path)
61
+ end
62
+
63
+ resolved_file_path
64
+ end
65
+ end
66
+ end
@@ -1,86 +1,47 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "fileutils"
4
+ require "forwardable"
4
5
  require "open3"
5
6
  require "tmpdir"
6
7
  require_relative "environment"
7
8
  require_relative "errors"
8
9
  require_relative "executed_command"
10
+ require_relative "file_helper"
9
11
 
10
12
  module JetBlack
11
13
  class Session
12
- attr_reader :commands
14
+ extend Forwardable
15
+
16
+ def_delegators :file_helper, :create_file, :create_executable,
17
+ :append_to_file, :copy_fixture
18
+
19
+ attr_reader :commands, :directory
13
20
 
14
21
  def initialize(options: {})
15
22
  @commands = []
16
23
  @session_options = options
24
+ @directory = File.realpath(Dir.mktmpdir("jet_black"))
25
+ @file_helper = FileHelper.new(directory)
17
26
  end
18
27
 
19
- def run(command, env: {}, options: {})
28
+ def run(command, stdin: nil, env: {}, options: {})
20
29
  combined_options = session_options.merge(options)
21
- executed_command = exec_command(command, env, combined_options)
30
+ executed_command = exec_command(command, stdin, env, combined_options)
22
31
  commands << executed_command
23
32
  executed_command
24
33
  end
25
34
 
26
- def directory
27
- @_directory ||= File.realpath(Dir.mktmpdir("jet_black"))
28
- end
29
-
30
- def create_file(file_path, file_content)
31
- expanded_file_path = File.expand_path(file_path, directory)
32
- expanded_dir = File.dirname(expanded_file_path)
33
-
34
- unless expanded_file_path.start_with?(directory)
35
- raise JetBlack::InvalidPathError.new(file_path, expanded_file_path)
36
- end
37
-
38
- FileUtils.mkdir_p(expanded_dir)
39
- File.write(expanded_file_path, file_content)
40
- end
41
-
42
- def append_to_file(file_path, append_content)
43
- expanded_file_path = File.expand_path(file_path, directory)
44
-
45
- unless File.exist?(expanded_file_path)
46
- raise JetBlack::NonExistentFileError.new(file_path, expanded_file_path)
47
- end
48
-
49
- File.open(expanded_file_path, "a") do |file|
50
- file.write(append_content)
51
- end
52
- end
53
-
54
- def copy_fixture(source_path, destination_path)
55
- src_fixture_dir = JetBlack.configuration.fixture_directory
56
- expanded_source_path = File.expand_path(source_path, src_fixture_dir)
57
- expanded_destination_path = File.expand_path(destination_path, directory)
58
- expanded_destination_dir = File.dirname(expanded_destination_path)
59
-
60
- if src_fixture_dir.nil?
61
- raise Error.new("Please configure the fixture_directory")
62
- end
63
-
64
- unless expanded_destination_path.start_with?(directory)
65
- raise JetBlack::InvalidPathError.new(
66
- destination_path, expanded_destination_path
67
- )
68
- end
69
-
70
- FileUtils.mkdir_p(expanded_destination_dir)
71
- FileUtils.cp(expanded_source_path, expanded_destination_path)
72
- end
73
-
74
35
  private
75
36
 
76
- attr_reader :session_options
37
+ attr_reader :session_options, :file_helper
77
38
 
78
- def exec_command(raw_command, raw_env, options)
39
+ def exec_command(raw_command, stdin, raw_env, options)
79
40
  env = Environment.new(raw_env).to_h
80
41
 
81
42
  command_context(options) do
82
43
  stdout, stderr, exit_status =
83
- Open3.capture3(env, raw_command, chdir: directory)
44
+ Open3.capture3(env, raw_command, chdir: directory, stdin_data: stdin)
84
45
 
85
46
  ExecutedCommand.new(
86
47
  raw_command: raw_command,
@@ -1,3 +1,3 @@
1
1
  module JetBlack
2
- VERSION = "0.2.0"
2
+ VERSION = "0.3.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jet_black
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Oli Peate
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-04-27 00:00:00.000000000 Z
11
+ date: 2018-06-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '1.16'
27
+ - !ruby/object:Gem::Dependency
28
+ name: coveralls
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: pry
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -80,6 +94,20 @@ dependencies:
80
94
  - - "~>"
81
95
  - !ruby/object:Gem::Version
82
96
  version: '3.0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: simplecov
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
83
111
  description:
84
112
  email:
85
113
  executables: []
@@ -89,7 +117,6 @@ files:
89
117
  - ".circleci/config.yml"
90
118
  - ".gitignore"
91
119
  - ".rspec"
92
- - ".travis.yml"
93
120
  - CHANGELOG.md
94
121
  - Gemfile
95
122
  - Gemfile.lock
@@ -104,6 +131,7 @@ files:
104
131
  - lib/jet_black/environment.rb
105
132
  - lib/jet_black/errors.rb
106
133
  - lib/jet_black/executed_command.rb
134
+ - lib/jet_black/file_helper.rb
107
135
  - lib/jet_black/rspec.rb
108
136
  - lib/jet_black/rspec/matchers.rb
109
137
  - lib/jet_black/session.rb
@@ -1,5 +0,0 @@
1
- sudo: false
2
- language: ruby
3
- rvm:
4
- - 2.4.3
5
- before_install: gem install bundler -v 1.16.1