Module: CLI::Kit::System

Extended by:
T::Sig
Defined in:
lib/cli/kit/system.rb,
lib/cli/kit/support/test_helper.rb

Constant Summary collapse

SUDO_PROMPT =
CLI::UI.fmt('{{info:(sudo)}} Password: ')

Class Method Summary collapse

Methods included from T::Sig

sig

Class Method Details

.capture2(cmd, *a, sudo: false, env: {}, **kwargs) ⇒ Object



61
62
63
# File 'lib/cli/kit/system.rb', line 61

def capture2(cmd, *args, sudo: false, env: ENV.to_h, **kwargs)
  delegate_open3(cmd, args, kwargs, sudo: sudo, env: env, method: :capture2)
end

.capture2e(cmd, *a, sudo: false, env: {}, **kwargs) ⇒ Object



92
93
94
# File 'lib/cli/kit/system.rb', line 92

def capture2e(cmd, *args, sudo: false, env: ENV.to_h, **kwargs)
  delegate_open3(cmd, args, kwargs, sudo: sudo, env: env, method: :capture2e)
end

.capture3(cmd, *a, sudo: false, env: {}, **kwargs) ⇒ Object



124
125
126
# File 'lib/cli/kit/system.rb', line 124

def capture3(cmd, *args, sudo: false, env: ENV.to_h, **kwargs)
  delegate_open3(cmd, args, kwargs, sudo: sudo, env: env, method: :capture3)
end

.error_messageObject

Returns the errors associated to a test run

#### Returns ‘errors` (String) a string representing errors found on this run, nil if none



178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
# File 'lib/cli/kit/support/test_helper.rb', line 178

def error_message
  errors = {
    unexpected: [],
    not_run: [],
    other: {},
  }

  @delegate_open3.each do |cmd, opts|
    if opts[:unexpected]
      errors[:unexpected] << cmd
    elsif opts[:run]
      error = []

      if opts[:expected][:sudo] != opts[:actual][:sudo]
        error << "- sudo was supposed to be #{opts[:expected][:sudo]} but was #{opts[:actual][:sudo]}"
      end

      if opts[:expected][:env] != opts[:actual][:env]
        error << "- env was supposed to be #{opts[:expected][:env]} but was #{opts[:actual][:env]}"
      end

      errors[:other][cmd] = error.join("\n") unless error.empty?
    else
      errors[:not_run] << cmd
    end
  end

  final_error = []

  unless errors[:unexpected].empty?
    final_error << CLI::UI.fmt(<<~EOF)
      {{bold:Unexpected command invocations:}}
      {{command:#{errors[:unexpected].join("\n")}}}
    EOF
  end

  unless errors[:not_run].empty?
    final_error << CLI::UI.fmt(<<~EOF)
      {{bold:Expected commands were not run:}}
      {{command:#{errors[:not_run].join("\n")}}}
    EOF
  end

  unless errors[:other].empty?
    final_error << CLI::UI.fmt(<<~EOF)
      {{bold:Commands were not run as expected:}}
      #{errors[:other].map { |cmd, msg| "{{command:#{cmd}}}\n#{msg}" }.join("\n\n")}
    EOF
  end

  return if final_error.empty?

  "\n" + final_error.join("\n") # Initial new line for formatting reasons
end

.fake(*a, stdout: '', stderr: '', allow: nil, success: nil, sudo: false, env: {}) ⇒ Object

Sets up an expectation for a command and stubs out the call (unless allow is true)

#### Parameters ‘*a` : the command, represented as a splat `stdout` : stdout to stub the command with (defaults to empty string) `stderr` : stderr to stub the command with (defaults to empty string) `allow` : allow determines if the command will be actually run, or stubbed. Defaults to nil (stub) `success` : success status to stub the command with (Defaults to nil) `sudo` : expectation of sudo being set or not (defaults to false) `env` : expectation of env being set or not (defaults to {})

Note: Must set allow or success

Raises:

  • (ArgumentError)


147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
# File 'lib/cli/kit/support/test_helper.rb', line 147

def fake(*a, stdout: '', stderr: '', allow: nil, success: nil, sudo: false, env: {})
  raise ArgumentError, 'success or allow must be set' if success.nil? && allow.nil?

  @delegate_open3 ||= {}
  @delegate_open3[a.join(' ')] = {
    expected: {
      sudo: sudo,
      env: env,
    },
    actual: {
      sudo: nil,
      env: nil,
    },
    stdout: stdout,
    stderr: stderr,
    allow: allow,
    success: success,
    run: false,
  }
end

.original_capture2Object



76
77
78
# File 'lib/cli/kit/support/test_helper.rb', line 76

def capture2(cmd, *args, sudo: false, env: ENV.to_h, **kwargs)
  delegate_open3(cmd, args, kwargs, sudo: sudo, env: env, method: :capture2)
end

.original_capture2eObject



95
96
97
# File 'lib/cli/kit/support/test_helper.rb', line 95

def capture2e(cmd, *args, sudo: false, env: ENV.to_h, **kwargs)
  delegate_open3(cmd, args, kwargs, sudo: sudo, env: env, method: :capture2e)
end

.original_capture3Object



114
115
116
# File 'lib/cli/kit/support/test_helper.rb', line 114

def capture3(cmd, *args, sudo: false, env: ENV.to_h, **kwargs)
  delegate_open3(cmd, args, kwargs, sudo: sudo, env: env, method: :capture3)
end

.original_systemObject



60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/cli/kit/support/test_helper.rb', line 60

def system(cmd, *args, sudo: false, env: ENV.to_h, stdin: nil, **kwargs, &block)
  cmd, args = apply_sudo(cmd, args, sudo)

  out_r, out_w = IO.pipe
  err_r, err_w = IO.pipe
  in_stream = if stdin
    stdin
  elsif STDIN.closed?
    :close
  else
    STDIN
  end
  cmd, args = resolve_path(cmd, args, env)
  pid = T.unsafe(Process).spawn(env, cmd, *args, 0 => in_stream, :out => out_w, :err => err_w, **kwargs)
  out_w.close
  err_w.close

  handlers = if block_given?
    {
      out_r => ->(data) { yield(data.force_encoding(Encoding::UTF_8), '') },
      err_r => ->(data) { yield('', data.force_encoding(Encoding::UTF_8)) },
    }
  else
    {
      out_r => ->(data) { STDOUT.write(data) },
      err_r => ->(data) { STDOUT.write(data) },
    }
  end

  previous_trailing = Hash.new('')
  loop do
    break if Process.wait(pid, Process::WNOHANG)

    ios = [err_r, out_r].reject(&:closed?)
    next if ios.empty?

    readers, = IO.select(ios, [], [], 1)
    next if readers.nil? # If IO.select times out we iterate again so we can check if the process has exited

    readers.each do |io|
      data, trailing = split_partial_characters(io.readpartial(4096))
      handlers[io].call(previous_trailing[io] + data)
      previous_trailing[io] = trailing
    rescue IOError
      io.close
    end
  end

  $CHILD_STATUS
end

.osObject



300
301
302
303
304
305
306
# File 'lib/cli/kit/system.rb', line 300

def os
  return :mac if /darwin/.match(RUBY_PLATFORM)
  return :linux if /linux/.match(RUBY_PLATFORM)
  return :windows if /mingw/.match(RUBY_PLATFORM)

  raise "Could not determine OS from platform #{RUBY_PLATFORM}"
end

.popen2(cmd, *args, sudo: false, env: ENV.to_h, **kwargs, &block) ⇒ Object



142
143
144
# File 'lib/cli/kit/system.rb', line 142

def popen2(cmd, *args, sudo: false, env: ENV.to_h, **kwargs, &block)
  delegate_open3(cmd, args, kwargs, sudo: sudo, env: env, method: :popen2, &block)
end

.popen2e(cmd, *args, sudo: false, env: ENV.to_h, **kwargs, &block) ⇒ Object



160
161
162
# File 'lib/cli/kit/system.rb', line 160

def popen2e(cmd, *args, sudo: false, env: ENV.to_h, **kwargs, &block)
  delegate_open3(cmd, args, kwargs, sudo: sudo, env: env, method: :popen2e, &block)
end

.popen3(cmd, *args, sudo: false, env: ENV.to_h, **kwargs, &block) ⇒ Object



178
179
180
# File 'lib/cli/kit/system.rb', line 178

def popen3(cmd, *args, sudo: false, env: ENV.to_h, **kwargs, &block)
  delegate_open3(cmd, args, kwargs, sudo: sudo, env: env, method: :popen3, &block)
end

.reset!Object

Resets the faked commands



170
171
172
# File 'lib/cli/kit/support/test_helper.rb', line 170

def reset!
  @delegate_open3 = {}
end

.split_partial_characters(data) ⇒ Object



264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
# File 'lib/cli/kit/system.rb', line 264

def split_partial_characters(data)
  last_byte = T.must(data.getbyte(-1))
  return [data, ''] if (last_byte & 0b1000_0000).zero?

  # UTF-8 is up to 4 characters per rune, so we could never want to trim more than that, and we want to avoid
  # allocating an array for the whole of data with bytes
  min_bound = -[4, data.bytesize].min
  final_bytes = T.must(data.byteslice(min_bound..-1)).bytes
  partial_character_sub_index = final_bytes.rindex { |byte| byte & 0b1100_0000 == 0b1100_0000 }

  # Bail out for non UTF-8
  return [data, ''] unless partial_character_sub_index

  start_byte = final_bytes[partial_character_sub_index]
  full_size = if start_byte & 0b1111_1000 == 0b1111_0000
    4
  elsif start_byte & 0b1111_0000 == 0b1110_0000
    3
  elsif start_byte & 0b1110_0000 == 0b110_00000
    2
  else
    nil # Not a valid UTF-8 character
  end
  return [data, ''] if full_size.nil? # Bail out for non UTF-8

  if final_bytes.size - partial_character_sub_index == full_size
    # We have a full UTF-8 character, so we can just return the data
    return [data, '']
  end

  partial_character_index = min_bound + partial_character_sub_index

  [T.must(data.byteslice(0...partial_character_index)), T.must(data.byteslice(partial_character_index..-1))]
end

.sudo_reason(msg) ⇒ Object



24
25
26
27
28
29
30
31
32
# File 'lib/cli/kit/system.rb', line 24

def sudo_reason(msg)
  # See if sudo has a cached password
  %x(env SUDO_ASKPASS=/usr/bin/false sudo -A true > /dev/null 2>&1)
  return if $CHILD_STATUS.success?

  CLI::UI.with_frame_color(:blue) do
    puts(CLI::UI.fmt("{{i}} #{msg}"))
  end
end

.system(cmd, *a, sudo: false, env: {}, stdin: nil, **kwargs) ⇒ Object



209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
# File 'lib/cli/kit/system.rb', line 209

def system(cmd, *args, sudo: false, env: ENV.to_h, stdin: nil, **kwargs, &block)
  cmd, args = apply_sudo(cmd, args, sudo)

  out_r, out_w = IO.pipe
  err_r, err_w = IO.pipe
  in_stream = if stdin
    stdin
  elsif STDIN.closed?
    :close
  else
    STDIN
  end
  cmd, args = resolve_path(cmd, args, env)
  pid = T.unsafe(Process).spawn(env, cmd, *args, 0 => in_stream, :out => out_w, :err => err_w, **kwargs)
  out_w.close
  err_w.close

  handlers = if block_given?
    {
      out_r => ->(data) { yield(data.force_encoding(Encoding::UTF_8), '') },
      err_r => ->(data) { yield('', data.force_encoding(Encoding::UTF_8)) },
    }
  else
    {
      out_r => ->(data) { STDOUT.write(data) },
      err_r => ->(data) { STDOUT.write(data) },
    }
  end

  previous_trailing = Hash.new('')
  loop do
    break if Process.wait(pid, Process::WNOHANG)

    ios = [err_r, out_r].reject(&:closed?)
    next if ios.empty?

    readers, = IO.select(ios, [], [], 1)
    next if readers.nil? # If IO.select times out we iterate again so we can check if the process has exited

    readers.each do |io|
      data, trailing = split_partial_characters(io.readpartial(4096))
      handlers[io].call(previous_trailing[io] + data)
      previous_trailing[io] = trailing
    rescue IOError
      io.close
    end
  end

  $CHILD_STATUS
end

.which(cmd, env) ⇒ Object



309
310
311
312
313
314
315
316
317
318
319
# File 'lib/cli/kit/system.rb', line 309

def which(cmd, env)
  exts = os == :windows ? (env['PATHEXT'] || 'exe').split(';') : ['']
  (env['PATH'] || '').split(File::PATH_SEPARATOR).each do |path|
    exts.each do |ext|
      exe = File.join(path, "#{cmd}#{ext}")
      return exe if File.executable?(exe) && !File.directory?(exe)
    end
  end

  nil
end