jet_black 0.6.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: a21eedaa0beebe53a809bb73bd3f264f4993d0eb4d6040e6521b6ef7d044b4b7
4
- data.tar.gz: 1c805ee9c3c4921e3978caced024e3c9332dcc50ef215dd10b1e3cacc366e807
3
+ metadata.gz: c389c6caddb4fe0d3b8874f96a50f9c0e4217c3e1af903b6ca9f19bca24065c5
4
+ data.tar.gz: 5c1775b10ac55262d4291a9748858776a618400dfb4fa4561357147802822ce3
5
5
  SHA512:
6
- metadata.gz: ac2904ee5ebf833094905c22ef0e8064bbc6618fab3f7319ee80feecdaf78fdb05ee7ba5e29f657221fff2fc16e8a05ac88717eee3ab9380e6e90b847aa1db20
7
- data.tar.gz: e29ddc28dfc5cb2b450a4b61bfeb037b56d34c297fde88844558168191d15166d2e4405168da10768c93f6e51eac6737158c4874a27ae7d4d210d596b86893c3
6
+ metadata.gz: 9ddb6513193a3395baef52fab1bf86b3cf1de9f5d04091b82f52cd35e8bc97c2a3e21ca7549855e7d9db7947cb1af3b5f7263ce1b9929382dd7743ceb05d7d91
7
+ data.tar.gz: eac5b131ecce2f9b5aff51b1220d3276f320972bbf712f2e1efc7794265972c72f1e6fa5197f3c69548d574ac0f1bbae5193cf16961c9daa2b382c7a652d88b6
data/.circleci/config.yml CHANGED
@@ -61,9 +61,15 @@ jobs:
61
61
  - image: circleci/ruby:2.7
62
62
  environment:
63
63
  ENABLE_COVERAGE: 1
64
+ ruby-3.0:
65
+ <<: *base_job
66
+ docker:
67
+ - image: circleci/ruby:3.0
64
68
 
65
69
  #-------------------------------------------------------------------------------
66
70
 
71
+ # TODO: Migrate to CircleCI matrix
72
+
67
73
  workflows:
68
74
  version: 2
69
75
  multiple-rubies:
@@ -72,3 +78,4 @@ workflows:
72
78
  - ruby-2.5
73
79
  - ruby-2.6
74
80
  - ruby-2.7
81
+ - ruby-3.0
data/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.7.0
4
+
5
+ - Adds `run_interactive` to allow pseudo-terminal interaction
6
+
3
7
  ## v0.6.0
4
8
 
5
9
  - Freeze string literals
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- jet_black (0.6.0)
4
+ jet_black (0.7.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
@@ -15,7 +15,7 @@ GEM
15
15
  tins (~> 1.6)
16
16
  diff-lcs (1.3)
17
17
  docile (1.3.1)
18
- json (2.1.0)
18
+ json (2.3.1)
19
19
  method_source (0.9.0)
20
20
  pry (0.11.3)
21
21
  coderay (~> 1.1.0)
@@ -60,4 +60,4 @@ DEPENDENCIES
60
60
  simplecov
61
61
 
62
62
  BUNDLED WITH
63
- 2.1.2
63
+ 2.1.4
data/README.md CHANGED
@@ -11,6 +11,7 @@ with [RSpec] in mind. Features:
11
11
  - Synchronously [run commands](#running-commands) then write assertions on:
12
12
  - The `stdout` / `stderr` content
13
13
  - The exit status of the process
14
+ - Exercise [interactive command line interfaces](#running-interactive-commands)
14
15
  - Manipulate files in the temporary directory:
15
16
  - [Create files](#file-manipulation)
16
17
  - [Create executable files](#file-manipulation)
@@ -85,6 +86,38 @@ session = JetBlack::Session.new
85
86
  session.run("./hello-world", stdin: "Alice")
86
87
  ```
87
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
+
88
121
  ### File manipulation
89
122
 
90
123
  ```ruby
@@ -33,4 +33,17 @@ module JetBlack
33
33
  MSG
34
34
  end
35
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
36
49
  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
@@ -3,12 +3,12 @@
3
3
  require "bundler"
4
4
  require "fileutils"
5
5
  require "forwardable"
6
- require "open3"
7
6
  require "tmpdir"
8
- require_relative "environment"
9
7
  require_relative "errors"
10
8
  require_relative "executed_command"
11
9
  require_relative "file_helper"
10
+ require_relative "non_interactive_command"
11
+ require_relative "interactive_command"
12
12
 
13
13
  module JetBlack
14
14
  class Session
@@ -27,33 +27,38 @@ module JetBlack
27
27
  end
28
28
 
29
29
  def run(command, stdin: nil, env: {}, options: {})
30
- combined_options = session_options.merge(options)
31
- executed_command = exec_command(command, stdin, env, combined_options)
32
- commands << executed_command
33
- 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
34
39
  end
35
40
 
36
41
  private
37
42
 
38
43
  attr_reader :session_options, :file_helper
39
44
 
40
- def exec_command(raw_command, stdin, raw_env, options)
41
- 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)
42
47
 
43
- command_context(options) do
44
- stdout, stderr, exit_status =
45
- Open3.capture3(env, raw_command, chdir: directory, stdin_data: stdin)
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
52
+
53
+ def exec_interactive(raw_command:, raw_env:, options:, block:)
54
+ combined_options = session_options.merge(options)
46
55
 
47
- ExecutedCommand.new(
48
- raw_command: raw_command,
49
- stdout: stdout,
50
- stderr: stderr,
51
- exit_status: exit_status,
52
- )
56
+ execution_context(combined_options) do
57
+ InteractiveCommand.new.call(raw_command: raw_command, raw_env: raw_env, directory: directory, block: block)
53
58
  end
54
59
  end
55
60
 
56
- def command_context(options)
61
+ def execution_context(options)
57
62
  if options[:clean_bundler_env]
58
63
  Bundler.public_send(bundler_clean_environment_method) { yield }
59
64
  else
@@ -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,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JetBlack
4
- VERSION = "0.6.0"
4
+ VERSION = "0.7.0"
5
5
  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.6.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: 2020-02-13 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
@@ -114,8 +114,8 @@ dependencies:
114
114
  - - ">="
115
115
  - !ruby/object:Gem::Version
116
116
  version: '0'
117
- description:
118
- email:
117
+ description:
118
+ email:
119
119
  executables: []
120
120
  extensions: []
121
121
  extra_rdoc_files: []
@@ -141,15 +141,18 @@ files:
141
141
  - lib/jet_black/errors.rb
142
142
  - lib/jet_black/executed_command.rb
143
143
  - lib/jet_black/file_helper.rb
144
+ - lib/jet_black/interactive_command.rb
145
+ - lib/jet_black/non_interactive_command.rb
144
146
  - lib/jet_black/rspec.rb
145
147
  - lib/jet_black/rspec/matchers.rb
146
148
  - lib/jet_black/session.rb
149
+ - lib/jet_black/terminal_session.rb
147
150
  - lib/jet_black/version.rb
148
151
  homepage: https://github.com/odlp/jet_black
149
152
  licenses:
150
153
  - MIT
151
154
  metadata: {}
152
- post_install_message:
155
+ post_install_message:
153
156
  rdoc_options: []
154
157
  require_paths:
155
158
  - lib
@@ -164,8 +167,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
164
167
  - !ruby/object:Gem::Version
165
168
  version: '0'
166
169
  requirements: []
167
- rubygems_version: 3.1.2
168
- signing_key:
170
+ rubygems_version: 3.1.4
171
+ signing_key:
169
172
  specification_version: 4
170
173
  summary: Black box testing
171
174
  test_files: []