class RSpec::Bash::ScriptEvaluator

Constants

BLOCK_SIZE
CONDITIONAL_EXPR_STUB
FRAME_ARG
FRAME_NAME
FRAME_TRACE
MESSAGE_ACK
MESSAGE_REQ

Public Instance Methods

eval(script, args = [], **opts) click to toggle source

(String, Object?): Boolean

@param [String] script @param [Hash?] options @param [Number?] options.read_fd @param [Number?] options.write_fd @param [Number?] options.throttle

# File lib/rspec/bash/script_evaluator.rb, line 27
def eval(script, args = [], **opts)
  file = opts.fetch(:file, Tempfile.new('rspec_bash'))
  file.write(script.to_s)
  file.close
  verbose = opts.fetch(:verbose, Bash.configuration.verbose)

  bus_file = Tempfile.new("rspec_bash#{File.basename(script.source_file).gsub(/\W+/, '_')}")
  bus_file.close

  Bash::Open3.popen3X([ '/usr/bin/env', 'bash', file.path ].concat(args), {
    read_fd: opts.fetch(:read_fd, Bash.configuration.read_fd),
    write_fd: opts.fetch(:write_fd, Bash.configuration.write_fd),
    env: opts.fetch(:env, {})
  }) do |input, stdout, stderr, r2b, b2r, wait_thr|
    workers = []

    # transmit stdout
    workers << NoisyThread.new do
      FD.poll(stdout, throttle: Bash.configuration.throttle) do
        buffer = stdout.read_nonblock(BLOCK_SIZE)

        script.stdout << buffer

        if verbose
          STDOUT.write buffer
          STDOUT.flush
        end
      end
    end

    # transmit stderr
    workers << NoisyThread.new do
      FD.poll(stderr, throttle: Bash.configuration.throttle) do
        buffer = stderr.read_nonblock(BLOCK_SIZE)

        script.stderr << buffer

        if verbose
          STDERR.write buffer
          STDERR.flush
        end
      end
    end

    # accept & respond to prompts
    workers << NoisyThread.new do
      FD.poll(b2r, throttle: 0) do
        respond_to_prompts(r2b, b2r, bus_file, script)
      end
    end

    # wait for the script to finish executing
    wait_thr.join

    # clean up
    try_hard "close r2b" do r2b.close end
    try_hard "close b2r" do b2r.close end
    try_hard "shut off workers" do workers.map(&:join) end
    try_hard "kill them all" do workers.map(&:kill) end
    try_hard "clean up temp bus file" do bus_file.unlink end
    try_hard "clean up source file" do file.unlink unless opts.key?(:file) end

    script.track_exit_code wait_thr.value.exitstatus

    wait_thr.value.success?
  end
end

Private Instance Methods

relay_command_stub(fd_in, fd_out, bus_file, script:, stub:) click to toggle source
# File lib/rspec/bash/script_evaluator.rb, line 155
def relay_command_stub(fd_in, fd_out, bus_file, script:, stub:)
  if !script.has_stub?(stub[:expr])
    fail(
      "#{stub[:expr]} is not stubbed!\n\n" +
      "Call stack:\n" +
      stub[:stacktrace].map { |x| "- #{x}" }.join("\n")
    )
  end

  File.write(bus_file, script.stubbed(stub[:expr], stub[:args]))

  fd_in.puts bus_file.path
  fd_in.flush

  fd_out.expect(MESSAGE_ACK, 1)

  script.track_call(stub[:expr], stub[:args])
end
relay_conditional_stub(fd_in, fd_out, bus_file, script:, stub:) click to toggle source
# File lib/rspec/bash/script_evaluator.rb, line 174
def relay_conditional_stub(fd_in, fd_out, bus_file, script:, stub:)
  if !script.has_conditional_stubs? && !Bash.configuration.allow_unstubbed_conditionals
    fail "conditional expressions are not stubbed!\n#{stub[:stacktrace]}"
  end

  File.write(bus_file, script.stubbed_conditional(stub[:args]))

  fd_in.puts bus_file.path
  fd_in.flush

  fd_out.expect(MESSAGE_ACK, 1)

  script.track_conditional_call(stub[:args])
end
respond_to_prompts(fd_in, fd_out, bus_file, script) click to toggle source
# File lib/rspec/bash/script_evaluator.rb, line 97
      def respond_to_prompts(fd_in, fd_out, bus_file, script)
        fd_out.expect(MESSAGE_REQ, 1) do |result|
          break if result.nil?

          lines = result[0].split("\n").reject(&:empty?)

          stubs = lines.each_with_index.reduce([]) do |acc, (line, index)|
            next acc unless line == MESSAGE_REQ

            message = lines[index-1]
            frames, err = MessageDecoder.decode(message)

            if err
              STDERR.puts <<-EOF
                bash-rspec: communication between Ruby and Bash failed, this is
                most likely an internal error.

                #{err}
              EOF

              next acc
            end

            stub = frames.reduce({ expr: nil, type: nil, args: [], stacktrace: [] }) do |x, (type, content)|
              case type
              when FRAME_NAME
                x[:expr] = content
                x[:type] = content == CONDITIONAL_EXPR_STUB ? :conditional : :function
              when FRAME_ARG
                x[:args] << content
              when FRAME_TRACE
                x[:stacktrace] << content unless content.empty?
              else
                STDERR.puts "rspec-bash: unrecognized frame '#{type}' => '#{content}'"
                STDERR.puts "rspec-bash: source:\n#{message}"
              end

              x
            end.tap do |stub|
              stub[:args] = stub[:args].join(' ')
            end

            acc.push(stub)
          end

          stubs.each do |stub|
            case stub[:type]
            when :conditional
              relay_conditional_stub(fd_in, fd_out, bus_file, script: script, stub: stub)
            when :function
              relay_command_stub(fd_in, fd_out, bus_file, script: script, stub: stub)
            when :unknown
              STDERR.puts "[err] unexpected message from bash: #{stub.inspect}"
            end
          end
        end
      end
try_hard(what) { || ... } click to toggle source
# File lib/rspec/bash/script_evaluator.rb, line 189
def try_hard(what)
  begin
    yield
  rescue StandardError => e
    puts what
    puts e
    puts e.backtrace
  end
end