scmd 2.3.1 → 3.0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 67125e428f1adb1fe7f302c3d0276a0384d5b41a74be0a40b52256043dc22632
4
+ data.tar.gz: c1dcc9b7152046ff0a5af224acc3c31f3124c423e818d048f8879310d2b9d645
5
+ SHA512:
6
+ metadata.gz: 74b33660ba6eab558d637797e1e03ec67607a0110be680822a22851fa7068d14b60a4d8f2fb1ff58ad1867f1c964bd34c619838efa7187c775eb491b6cef04c3
7
+ data.tar.gz: 17ea7892c22832d00868a8b9659ba8aabfc57b1c0c3df0a91e41a929d47af039ac40110b3a3b58a3688e09db61a42a531b68903a23f0ec4c8e978bec8deb3002
data/Gemfile CHANGED
@@ -1,11 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source "https://rubygems.org"
2
4
 
3
5
  gemspec
4
6
 
5
- gem 'rake'
6
- gem 'pry', "~> 0.9.0"
7
- gem 'whysoslow'
7
+ gem "pry"
8
+ gem "whysoslow"
8
9
 
9
- platform :rbx do
10
- gem 'rubysl'
11
- end
10
+ # to release, uncomment this and run `bundle exec ggem r -f`
11
+ gem 'ggem'
File without changes
data/README.md CHANGED
@@ -42,7 +42,7 @@ cmd.start
42
42
 
43
43
  begin
44
44
  cmd.wait(10)
45
- rescue Scmd::Timeout => err
45
+ rescue Scmd::TimeoutError => err
46
46
  cmd.stop # attempt to stop the cmd nicely, kill if doesn't stop in time
47
47
  cmd.kill # just kill the cmd now
48
48
  end
@@ -94,6 +94,98 @@ Raise an exception if not successful with `run!`:
94
94
  Scmd.new("cd /path/that/does/not/exist").run! #=> Scmd::Command::Failure
95
95
  ```
96
96
 
97
+ ### Environment variables
98
+
99
+ Pass environment variables:
100
+
101
+ ```ruby
102
+ cmd = Scmd.new("echo $TEST_VAR", {
103
+ :env => {
104
+ 'TEST_VAR' => 'hi'
105
+ }
106
+ })
107
+ ```
108
+
109
+ ### Process spawn options
110
+
111
+ Pass options:
112
+
113
+ ```ruby
114
+ reader, writer = IO.pipe
115
+ # this is an example that uses file descriptor redirection options
116
+ cmd = Scmd.new("echo test 1>&#{writer.fileno}", {
117
+ :options => { writer => writer }
118
+ })
119
+ reader.gets # => "test\n"
120
+ ```
121
+
122
+ For all the possible options see [posix-spawn](https://github.com/rtomayko/posix-spawn#status).
123
+
124
+ ## Testing
125
+
126
+ Scmd comes with some testing utilities built in. Specifically this includes a command spy and a "test mode" API on the main `Scmd` namespace.
127
+
128
+ ### Command Spy
129
+
130
+ ```ruby
131
+ require 'scmd/command_spy'
132
+ spy = Scmd::CommandSpy.new(cmd_str)
133
+ spy.exitstatus = 1
134
+ spy.stdout = 'some test output'
135
+ Assert.stub(Scmd, :new).with(cmd_str){ spy }
136
+
137
+ cmd = Scmd.new(cmd_str) # => spy
138
+ cmd.run('some input')
139
+
140
+ cmd.run_called? # => true
141
+ cmd.run_calls.size # => 1
142
+ cmd.run_calls.first.input # => 'some input'
143
+ ```
144
+
145
+ The spy is useful for stubbing out system commands that you don't want to call or aren't safe to call in the test suite. It responds to the same API that commands do but doesn't run any system commands.
146
+
147
+ ### "Test Mode" API
148
+
149
+ ```ruby
150
+ Scmd.add_command(cmd_str){ |cmd| cmd.stdout = 'some output' } # => raises NoMethodError
151
+
152
+ ENV['SCMD_TEST_MODE'] = '1'
153
+ Scmd.add_command(cmd_str){ |cmd| cmd.stdout = 'some output' }
154
+ Scmd.add_command(cmd_str).with({:env => { :SOME_ENV_VAR => '1' }}) do |cmd|
155
+ cmd.stdout = 'some other output'
156
+ end
157
+ Scmd.commands.empty? # => false
158
+
159
+ cmd = Scmd.new(cmd_str)
160
+ cmd.class # => Scmd::CommandSpy
161
+ cmd.stdout # => 'some output'
162
+ cmd.run('some input')
163
+ Scmd.calls.size # => 1
164
+ Scmd.calls.last.class # => Scmd::Call
165
+ Scmd.calls.last.cmd_str # => cmd_str
166
+ Scmd.calls.last.input # => 'some input'
167
+ Scmd.calls.last.cmd.class # => Scmd::CommandSpy
168
+
169
+ cmd = Scmd.new(cmd_str, {:env => { 'SOME_ENV_VAR' => '1' }})
170
+ cmd.class # => Scmd::CommandSpy
171
+ cmd.stdout # => 'some other output'
172
+ cmd.run('some input')
173
+ Scmd.calls.size # => 2
174
+ Scmd.calls.last.class # => Scmd::Call
175
+ Scmd.calls.last.cmd_str # => cmd_str
176
+ Scmd.calls.last.input # => 'some input'
177
+ Scmd.calls.last.cmd.class # => Scmd::CommandSpy
178
+ Scmd.calls.last.cmd.env # => { 'SOME_ENV_VAR' => '1' }
179
+
180
+ Scmd.reset
181
+ Scmd.commands.empty? # => true
182
+ Scmd.calls.empty? # => true
183
+ ```
184
+
185
+ Use these singleton methods on the `Scmd` namespace to add specific command spies in specific contexts and to track command calls (runs, starts). Use `reset` to reset the state of things.
186
+
187
+ **Note:** these methods are only available when test mode is enabled (when the `SCMD_TEST_MODE` env var has a non-falsey value). Otherwise these methods will raise `NoMethodError`.
188
+
97
189
  ## Installation
98
190
 
99
191
  Add this line to your application's Gemfile:
@@ -2,79 +2,79 @@ echo hi: 1 times
2
2
  ----------------
3
3
  whysoslow? ..
4
4
 
5
- mem @ start 29 MB ??
6
- mem @ finish 29 MB + 0 MB, 0%
5
+ mem @ start 17 MB ??
6
+ mem @ finish 17 MB + 0 MB, 0%
7
7
 
8
- user system total real
9
- time 0.0 ms 0.0 ms 0.0 ms 1.928 ms
8
+ user system total real
9
+ time 0.5820000000000001 ms 0.5519999999999999 ms 3.786 ms 3.7030000000000003 ms
10
10
 
11
11
  echo hi: 10 times
12
12
  -----------------
13
13
  whysoslow? ..
14
14
 
15
- mem @ start 38 MB ??
16
- mem @ finish 38 MB - 0 MB, 0%
15
+ mem @ start 17 MB ??
16
+ mem @ finish 17 MB + 0 MB, 0%
17
17
 
18
- user system total real
19
- time 0.0 ms 0.0 ms 10.0 ms 16.874 ms
18
+ user system total real
19
+ time 2.475 ms 3.822 ms 32.675999999999995 ms 34.702 ms
20
20
 
21
21
  echo hi: 100 times
22
22
  ------------------
23
23
  whysoslow? ..
24
24
 
25
- mem @ start 38 MB ??
26
- mem @ finish 38 MB - 0 MB, 0%
25
+ mem @ start 17 MB ??
26
+ mem @ finish 18 MB + 0 MB, 2%
27
27
 
28
- user system total real
29
- time 20.0 ms 20.0 ms 130.0 ms 147.925 ms
28
+ user system total real
29
+ time 19.75 ms 31.77 ms 281.766 ms 306.74899999999997 ms
30
30
 
31
31
  echo hi: 1000 times
32
32
  -------------------
33
33
  whysoslow? ..
34
34
 
35
- mem @ start 38 MB ??
36
- mem @ finish 41 MB + 3 MB, 7%
35
+ mem @ start 18 MB ??
36
+ mem @ finish 29 MB + 11 MB, 61%
37
37
 
38
- user system total real
39
- time 170.0 ms 180.0 ms 1320.0 ms 1390.406 ms
38
+ user system total real
39
+ time 161.98399999999998 ms 271.296 ms 2432.76 ms 2662.98 ms
40
40
 
41
41
  cat test/support/bigger-than-64k.txt: 1 times
42
42
  ---------------------------------------------
43
43
  whysoslow? ..
44
44
 
45
- mem @ start 41 MB ??
46
- mem @ finish 41 MB - 0 MB, 0%
45
+ mem @ start 31 MB ??
46
+ mem @ finish 31 MB + 0 MB, 1%
47
47
 
48
- user system total real
49
- time 0.0 ms 0.0 ms 0.0 ms 3.487 ms
48
+ user system total real
49
+ time 0.412 ms 0.504 ms 4.343999999999999 ms 4.654 ms
50
50
 
51
51
  cat test/support/bigger-than-64k.txt: 10 times
52
52
  ----------------------------------------------
53
53
  whysoslow? ..
54
54
 
55
- mem @ start 41 MB ??
56
- mem @ finish 43 MB + 2 MB, 4%
55
+ mem @ start 31 MB ??
56
+ mem @ finish 34 MB + 2 MB, 7%
57
57
 
58
- user system total real
59
- time 0.0 ms 0.0 ms 20.0 ms 30.533 ms
58
+ user system total real
59
+ time 3.2880000000000003 ms 4.386 ms 43.447 ms 46.552 ms
60
60
 
61
61
  cat test/support/bigger-than-64k.txt: 100 times
62
62
  -----------------------------------------------
63
63
  whysoslow? ..
64
64
 
65
- mem @ start 43 MB ??
66
- mem @ finish 85 MB + 42 MB, 98%
65
+ mem @ start 33 MB ??
66
+ mem @ finish 56 MB + 23 MB, 68%
67
67
 
68
- user system total real
69
- time 40.0 ms 40.0 ms 250.0 ms 274.06 ms
68
+ user system total real
69
+ time 28.892999999999997 ms 40.599000000000004 ms 407.804 ms 440.11 ms
70
70
 
71
71
  cat test/support/bigger-than-64k.txt: 1000 times
72
72
  ------------------------------------------------
73
73
  whysoslow? ..
74
74
 
75
- mem @ start 85 MB ??
76
- mem @ finish 506 MB + 421 MB, 497%
75
+ mem @ start 44 MB ??
76
+ mem @ finish 297 MB + 254 MB, 582%
77
77
 
78
- user system total real
79
- time 360.0 ms 400.0 ms 2540.0 ms 2649.686 ms
78
+ user system total real
79
+ time 285.236 ms 408.677 ms 4097.647 ms 4428.276999999999 ms
80
80
 
@@ -1,12 +1,13 @@
1
- require 'whysoslow'
2
- require 'scmd'
1
+ # frozen_string_literal: true
3
2
 
4
- class ScmdBenchRunner
3
+ require "whysoslow"
4
+ require "scmd"
5
5
 
6
+ class ScmdBenchRunner
6
7
  attr_reader :result
7
8
 
8
9
  def self.run(*args)
9
- self.new(*args).run
10
+ new(*args).run
10
11
  end
11
12
 
12
13
  def initialize(printer_io, cmd, num_times = 10)
@@ -15,15 +16,16 @@ class ScmdBenchRunner
15
16
  num_times.times{ cmd.run! }
16
17
  end
17
18
 
18
- @printer = Whysoslow::DefaultPrinter.new(printer_io, {
19
- :title => "#{@cmd.cmd_str}: #{num_times} times",
20
- :verbose => true
21
- })
19
+ @printer =
20
+ Whysoslow::DefaultPrinter.new(
21
+ printer_io,
22
+ title: "#{@cmd.cmd_str}: #{num_times} times",
23
+ verbose: true,
24
+ )
22
25
  @runner = Whysoslow::Runner.new(@printer)
23
26
  end
24
27
 
25
28
  def run
26
- @runner.run &@proc
29
+ @runner.run(&@proc)
27
30
  end
28
-
29
31
  end
@@ -1,10 +1,49 @@
1
- require 'scmd/version'
2
- require 'scmd/command'
1
+ # frozen_string_literal: true
2
+
3
+ require "scmd/version"
4
+ require "scmd/command"
3
5
 
4
6
  module Scmd
7
+ # Scmd can be run in "test mode". This means that command spies will be used
8
+ # in place of "live" commands, each time a command is run or started will be
9
+ # logged in a collection and option-specific spies can be added and used to
10
+ # "stub" spies with specific attributes in specific contexts.
11
+
12
+ def self.new(*args)
13
+ if !ENV["SCMD_TEST_MODE"]
14
+ Command.new(*args)
15
+ else
16
+ commands.get(*args)
17
+ end
18
+ end
5
19
 
6
- def self.new(*args, &block)
7
- Command.new(*args, &block)
20
+ def self.commands
21
+ raise NoMethodError unless ENV["SCMD_TEST_MODE"]
22
+ @commands ||= begin
23
+ require "scmd/stored_commands"
24
+ StoredCommands.new
25
+ end
26
+ end
27
+
28
+ def self.calls
29
+ raise NoMethodError unless ENV["SCMD_TEST_MODE"]
30
+ @calls ||= []
31
+ end
32
+
33
+ def self.reset
34
+ raise NoMethodError unless ENV["SCMD_TEST_MODE"]
35
+ calls.clear
36
+ commands.remove_all
37
+ end
38
+
39
+ def self.add_command(cmd_str, &block)
40
+ commands.add(cmd_str, &block)
41
+ end
42
+
43
+ class Call < Struct.new(:cmd_str, :input, :cmd)
44
+ def initialize(cmd_spy, input)
45
+ super(cmd_spy.cmd_str, input, cmd_spy)
46
+ end
8
47
  end
9
48
 
10
49
  TimeoutError = Class.new(::RuntimeError)
@@ -15,5 +54,4 @@ module Scmd
15
54
  set_backtrace(called_from || caller)
16
55
  end
17
56
  end
18
-
19
57
  end
@@ -1,6 +1,8 @@
1
- require 'thread'
2
- require 'posix-spawn'
3
- require 'scmd'
1
+ # frozen_string_literal: true
2
+
3
+ require "thread"
4
+ require "posix-spawn"
5
+ require "scmd"
4
6
 
5
7
  # Scmd::Command is a base wrapper for handling system commands. Initialize it
6
8
  # with with a string specifying the command to execute. You can then run the
@@ -8,23 +10,28 @@ require 'scmd'
8
10
  # create a more custom command wrapper.
9
11
 
10
12
  module Scmd
11
-
12
13
  class Command
13
14
  READ_SIZE = 10240 # bytes
14
15
  READ_CHECK_TIMEOUT = 0.001 # seconds
15
16
  DEFAULT_STOP_TIMEOUT = 3 # seconds
16
17
 
17
- attr_reader :cmd_str, :env
18
+ attr_reader :cmd_str, :env, :options
18
19
  attr_reader :pid, :exitstatus, :stdout, :stderr
19
20
 
20
- def initialize(cmd_str, env = nil)
21
+ def initialize(cmd_str, opts = nil)
22
+ opts ||= {}
21
23
  @cmd_str = cmd_str
22
- @env = stringify_hash(env || {})
24
+ @env = stringify_hash(opts[:env] || {})
25
+ @options = opts[:options] || {}
23
26
  reset_attrs
24
27
  end
25
28
 
26
29
  def run(input = nil)
27
- run!(input) rescue RunError
30
+ begin
31
+ run!(input)
32
+ rescue
33
+ RunError
34
+ end
28
35
  self
29
36
  end
30
37
 
@@ -32,11 +39,13 @@ module Scmd
32
39
  start_err_msg, start_err_bt = nil, nil
33
40
  begin
34
41
  start(input)
35
- rescue StandardError => err
36
- start_err_msg, start_err_bt = err.message, err.backtrace
42
+ rescue => ex
43
+ start_err_msg, start_err_bt = ex.message, ex.backtrace
37
44
  ensure
38
45
  wait # indefinitely until cmd is done running
39
- raise RunError.new(start_err_msg || @stderr, start_err_bt || caller) if !success?
46
+ unless success?
47
+ raise RunError.new(start_err_msg || @stderr, start_err_bt || caller)
48
+ end
40
49
  end
41
50
 
42
51
  self
@@ -51,15 +60,15 @@ module Scmd
51
60
  while @child_process.check_for_exit
52
61
  begin
53
62
  read_output
54
- rescue EOFError => err
63
+ rescue EOFError # rubocop:disable Lint/SuppressedException
55
64
  end
56
65
  end
57
- @stop_w.write_nonblock('.')
66
+ @stop_w.write_nonblock(".")
58
67
  end
59
68
  end
60
69
 
61
70
  def wait(timeout = nil)
62
- return if !running?
71
+ return unless running?
63
72
 
64
73
  wait_for_exit(timeout)
65
74
  if @child_process.running?
@@ -76,20 +85,20 @@ module Scmd
76
85
  end
77
86
 
78
87
  def stop(timeout = nil)
79
- return if !running?
88
+ return unless running?
80
89
 
81
90
  send_term
82
91
  begin
83
92
  wait(timeout || DEFAULT_STOP_TIMEOUT)
84
- rescue TimeoutError => err
93
+ rescue TimeoutError
85
94
  kill
86
95
  end
87
96
  end
88
97
 
89
- def kill(sig = nil)
90
- return if !running?
98
+ def kill(signal = nil)
99
+ return unless running?
91
100
 
92
- send_kill(sig)
101
+ send_kill(signal)
93
102
  wait # indefinitely until cmd is killed
94
103
  end
95
104
 
@@ -106,32 +115,35 @@ module Scmd
106
115
  end
107
116
 
108
117
  def inspect
109
- reference = '0x0%x' % (self.object_id << 1)
118
+ reference = "0x0%x" % (object_id << 1)
110
119
  "#<#{self.class}:#{reference}"\
111
- " @cmd_str=#{self.cmd_str.inspect}"\
120
+ " @cmd_str=#{cmd_str.inspect}"\
112
121
  " @exitstatus=#{@exitstatus.inspect}>"
113
122
  end
114
123
 
115
124
  private
116
125
 
117
126
  def read_output
118
- @child_process.read(READ_SIZE){ |out, err| @stdout += out; @stderr += err }
127
+ @child_process.read(READ_SIZE) do |out, err|
128
+ @stdout += out
129
+ @stderr += err
130
+ end
119
131
  end
120
132
 
121
133
  def wait_for_exit(timeout)
122
- ios, _, _ = IO.select([ @stop_r ], nil, nil, timeout)
123
- @stop_r.read_nonblock(1) if ios && ios.include?(@stop_r)
134
+ ios, _, _ = IO.select([@stop_r], nil, nil, timeout)
135
+ @stop_r.read_nonblock(1) if ios&.include?(@stop_r)
124
136
  end
125
137
 
126
138
  def reset_attrs
127
- @stdout, @stderr, @pid, @exitstatus = '', '', nil, nil
139
+ @stdout, @stderr, @pid, @exitstatus = +"", +"", nil, nil
128
140
  end
129
141
 
130
142
  def setup_run
131
143
  reset_attrs
132
144
  @stop_r, @stop_w = IO.pipe
133
145
  @read_output_thread = nil
134
- @child_process = ChildProcess.new(@cmd_str, @env)
146
+ @child_process = ChildProcess.new(@cmd_str, @env, @options)
135
147
  end
136
148
 
137
149
  def teardown_run
@@ -143,15 +155,15 @@ module Scmd
143
155
  end
144
156
 
145
157
  def send_term
146
- send_signal 'TERM'
158
+ send_signal "TERM"
147
159
  end
148
160
 
149
- def send_kill(sig = nil)
150
- send_signal(sig || 'KILL')
161
+ def send_kill(signal = nil)
162
+ send_signal(signal || "KILL")
151
163
  end
152
164
 
153
165
  def send_signal(sig)
154
- return if !running?
166
+ return unless running?
155
167
  @child_process.send_signal(sig)
156
168
  end
157
169
 
@@ -162,11 +174,14 @@ module Scmd
162
174
  end
163
175
 
164
176
  class ChildProcess
165
-
166
177
  attr_reader :pid, :stdin, :stdout, :stderr
167
178
 
168
- def initialize(cmd_str, env)
169
- @pid, @stdin, @stdout, @stderr = *::POSIX::Spawn::popen4(env, cmd_str)
179
+ def initialize(cmd_str, env, options)
180
+ @pid, @stdin, @stdout, @stderr = *::POSIX::Spawn.popen4(
181
+ env,
182
+ cmd_str,
183
+ options,
184
+ )
170
185
  @wait_pid, @wait_status = nil, nil
171
186
  end
172
187
 
@@ -188,25 +203,34 @@ module Scmd
188
203
  end
189
204
 
190
205
  def write(input)
191
- if !input.nil?
206
+ unless input.nil?
192
207
  [*input].each{ |line| @stdin.puts line.to_s }
193
208
  @stdin.close
194
209
  end
195
210
  end
196
211
 
197
212
  def read(size)
198
- ios, _, _ = IO.select([ @stdout, @stderr ], nil, nil, READ_CHECK_TIMEOUT)
213
+ ios, _, _ =
214
+ IO.select([@stdout, @stderr], nil, nil, READ_CHECK_TIMEOUT)
199
215
  if ios && block_given?
200
- yield read_if_ready(ios, @stdout, size), read_if_ready(ios, @stderr, size)
216
+ yield(
217
+ read_if_ready(ios, @stdout, size),
218
+ read_if_ready(ios, @stderr, size)
219
+ )
201
220
  end
202
221
  end
203
222
 
204
223
  def send_signal(sig)
205
- process_kill(sig, self.pid)
224
+ process_kill(sig, pid)
206
225
  end
207
226
 
208
- def flush_stdout; @stdout.read; end
209
- def flush_stderr; @stderr.read; end
227
+ def flush_stdout
228
+ @stdout.read
229
+ end
230
+
231
+ def flush_stderr
232
+ @stderr.read
233
+ end
210
234
 
211
235
  def teardown
212
236
  [@stdin, @stdout, @stderr].each{ |fd| fd.close if fd && !fd.closed? }
@@ -215,7 +239,7 @@ module Scmd
215
239
  private
216
240
 
217
241
  def read_if_ready(ready_ios, io, size)
218
- ready_ios.include?(io) ? read_by_size(io, size) : ''
242
+ ready_ios.include?(io) ? read_by_size(io, size) : ""
219
243
  end
220
244
 
221
245
  def read_by_size(io, size)
@@ -232,11 +256,8 @@ module Scmd
232
256
  end
233
257
 
234
258
  def pgrep
235
- @pgrep ||= Command.new('which pgrep').run.stdout.strip
259
+ @pgrep ||= Command.new("which pgrep").run.stdout.strip
236
260
  end
237
-
238
261
  end
239
-
240
262
  end
241
-
242
263
  end