Module: Bulldogger::Exec

Defined in:
lib/bulldogger/exec.rb

Overview

Runs one command with one addressed statement injection. The collector owns frame selection and statement evaluation.

Constant Summary collapse

SCHEMA_VERSION =
1

Class Method Summary collapse

Class Method Details

.run(fid, command, line:, statement:, visit: 1, index: nil, output_dir: ENV.fetch("BULLDOGGER_OUTPUT_DIR", "tmp/bulldogger"), stdout: $stdout, stderr: $stderr) ⇒ Object

Raises:

  • (ArgumentError)


18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# File 'lib/bulldogger/exec.rb', line 18

def run(fid, command, line:, statement:, visit: 1, index: nil, output_dir: ENV.fetch("BULLDOGGER_OUTPUT_DIR", "tmp/bulldogger"), stdout: $stdout, stderr: $stderr)
  target = ExecutionTarget.parse(fid, "exec")
  raise ArgumentError, "exec requires a command after --" if command.empty?
  raise ArgumentError, "exec requires a positive line" unless line.positive?
  raise ArgumentError, "exec requires a positive visit" unless visit.positive?

  code_state = CodeState.capture(target.root)
  return 1 unless ExecutionTarget.acceptable?(target, index: index, code_state: code_state, verb: "exec", stderr: stderr, fid: fid)

  output_dir = File.expand_path(output_dir)
  FileUtils.mkdir_p(output_dir)
  base_path = File.join(output_dir, "exec")
  env = CollectorEnvironment.build(
    "exec_collector.rb",
    "BULLDOGGER_EXEC" => "1", "BULLDOGGER_EXEC_OUT" => base_path,
    "BULLDOGGER_EXEC_FID" => fid, "BULLDOGGER_EXEC_LINE" => line.to_s,
    "BULLDOGGER_EXEC_VISIT" => visit.to_s, "BULLDOGGER_EXEC_STATEMENT" => statement
  )
  pid = Process.spawn(env, *command)
  _waited_pid, status = Process.wait2(pid)
  path = "#{base_path}-#{pid}.jsonl"
  outcome = Frames.send(:outcome, status)
  records = File.file?(path) ? File.readlines(path, chomp: true).map { |entry| JSON.parse(entry) } : []
  evaluation = records.reverse.find { |record| record["type"] == "evaluation" }
  summary = records.reverse.find { |record| record["type"] == "target_summary" }
  visit_summary = records.reverse.find { |record| record["type"] == "evaluation_summary" }
  result = {
    "type" => "result", "fid" => fid, "line" => line, "visit" => visit,
    "outcome" => outcome, "exit_status" => status.exitstatus
  }
  result.merge!(evaluation.reject { |key, _value| key == "type" }) if evaluation
  envelope = { "type" => "envelope", "schema_version" => SCHEMA_VERSION, "code_state" => code_state, "command" => command }
  if summary
    envelope["observed_calls"] = summary["observed_calls"]
    envelope["target_index"] = summary["target_index"]
    envelope["traced"] = false
  elsif visit_summary
    envelope["line_visits_observed"] = visit_summary["line_visits_observed"]
    envelope["target_visit"] = visit_summary["target_visit"]
    envelope["evaluated"] = false
  end
  File.open(path, "a") do |file|
    file.puts JSON.generate(result)
    file.puts JSON.generate(envelope)
  end
  stdout.puts "bulldogger exec: #{path}"
  stdout.puts "bulldogger note: #{never_traced_note(summary)}" if summary
  stdout.puts "bulldogger note: #{never_evaluated_note(fid, visit_summary)}" if visit_summary
  stdout.puts "bulldogger value: #{result.dig('value', 'value')}" if result["value"]
  stdout.puts "bulldogger result: #{outcome} (exit #{status.exitstatus || status.termsig})"
  status
end