Module: Rakit::Shell::CommandService

Defined in:
lib/rakit/shell.rb

Overview

CommandService: execute runs a Command; test runs it; format returns display string.

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

._executed_cacheObject (readonly)

Returns the value of attribute _executed_cache.



18
19
20
# File 'lib/rakit/shell.rb', line 18

def _executed_cache
  @_executed_cache
end

Class Method Details

._cache_key(command) ⇒ Object



31
32
33
34
35
36
37
38
# File 'lib/rakit/shell.rb', line 31

def self._cache_key(command)
  name = (command.respond_to?(:name) ? command.name : "").to_s
  args = (command.respond_to?(:args) ? command.args.to_a : []).map(&:to_s)
  cwd = (command.respond_to?(:working_directory) ? command.working_directory : "").to_s
  timeout_sec = command.respond_to?(:timeout_seconds) ? command.timeout_seconds.to_i : 10
  timeout_sec = 10 if timeout_sec <= 0
  [name, *args, cwd, timeout_sec].join("\0")
end

._command_with_result(command, exit_status, stdout, stderr) ⇒ Object



133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
# File 'lib/rakit/shell.rb', line 133

def self._command_with_result(command, exit_status, stdout, stderr)
  criteria = command.respond_to?(:acceptance_criteria) ? command.acceptance_criteria.to_a : []
  Command.new(
    name: command.name,
    args: command.args.to_a,
    working_directory: command.respond_to?(:working_directory) ? command.working_directory.to_s : "",
    timeout_seconds: command.respond_to?(:timeout_seconds) ? command.timeout_seconds.to_i : 0,
    expected_exit_code: command.respond_to?(:expected_exit_code) ? command.expected_exit_code : 0,
    expected_stdout: command.respond_to?(:expected_stdout) ? command.expected_stdout.to_s : "",
    expected_stderr: command.respond_to?(:expected_stderr) ? command.expected_stderr.to_s : "",
    acceptance_criteria: criteria,
    exit_status: exit_status,
    stdout: stdout,
    stderr: stderr,
  )
end

._format_command_to_s(command, fmt) ⇒ Object



56
57
58
59
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
# File 'lib/rakit/shell.rb', line 56

def self._format_command_to_s(command, fmt)
  return "" unless command
  name = (command.respond_to?(:name) ? command.name : "").to_s
  args = command.respond_to?(:args) ? command.args.to_a : []
  cwd = command.respond_to?(:working_directory) ? command.working_directory.to_s : ""
  timeout_sec = command.respond_to?(:timeout_seconds) ? command.timeout_seconds.to_i : 0

  argv = [name, *args].reject { |a| a.nil? || a.to_s.empty? }
  cmd_line = argv.map { |a| a.include?(" ") ? a.inspect : a }.join(" ")

  case fmt
  when CommandFormat::MULTI_LINE
    lines = ["name: #{name}", "args: #{args.inspect}"]
    lines << "working_directory: #{cwd}" if cwd && !cwd.empty?
    lines << "timeout_seconds: #{timeout_sec}" if timeout_sec.positive?
    lines.join("\n")
  when CommandFormat::COMPACT
    exit_status = command.respond_to?(:exit_status) ? command.exit_status.to_i : 0
    out_str = command.respond_to?(:stdout) ? command.stdout.to_s : ""
    err_str = command.respond_to?(:stderr) ? command.stderr.to_s : ""
    if exit_status != 0
      lines = ["#{CROSS} #{cmd_line}"]
      lines << "stdout:\n#{out_str}" if out_str && !out_str.empty?
      lines << "stderr:\n#{err_str}" if err_str && !err_str.empty?
      lines.join("\n")
    else
      "#{CHECK} #{cmd_line}"
    end
  else
    cmd_line
  end
end

.cacheObject

Returns the current cache of executed Commands (key => Command with result). Read-only view; use clear_cache to reset.



22
23
24
# File 'lib/rakit/shell.rb', line 22

def self.cache
  _executed_cache.dup
end

.clear_cacheObject

Clears the in-memory cache of executed commands.



27
28
29
# File 'lib/rakit/shell.rb', line 27

def self.clear_cache
  _executed_cache.clear
end

.execute(command, use_cache: true) ⇒ Object

Run the command (name + args) with optional cwd and timeout. Returns Command with exit_status, stdout, stderr set. When use_cache: true (default), returns a cached result if the same command was already executed.



91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/rakit/shell.rb', line 91

def self.execute(command, use_cache: true)
  key = _cache_key(command)
  if use_cache && _executed_cache.key?(key)
    return _executed_cache[key]
  end

  name = (command.respond_to?(:name) ? command.name : "").to_s
  args = command.respond_to?(:args) ? command.args.to_a : []
  argv = [name, *args].reject { |a| a.nil? || a.to_s.empty? }
  raise ArgumentError, "Command name is required" if argv.empty?

  cwd = command.respond_to?(:working_directory) ? command.working_directory.to_s : ""
  cwd = nil if cwd.nil? || cwd.empty?
  timeout_sec = command.respond_to?(:timeout_seconds) ? command.timeout_seconds.to_i : 10
  timeout_sec = 10 if timeout_sec <= 0

  opts = {}
  opts[:chdir] = cwd if cwd

  stdout = ""
  stderr = ""
  exit_status = -1

  run_block = lambda do
    Open3.popen3(*argv, **opts) do |_stdin, out, err, wait_thr|
      stdout = out.read
      stderr = err.read
      exit_status = wait_thr.value&.exitstatus || -1
    end
  end

  Timeout.timeout(timeout_sec) { run_block.call }

  result = _command_with_result(command, exit_status, stdout, stderr)
  _executed_cache[key] = result
  result
rescue Timeout::Error
  result = _command_with_result(command, -1, stdout, stderr + " (timeout after #{timeout_sec}s)")
  _executed_cache[key] = result
  result
end

.format(format_request) ⇒ Object

Format RPC: returns FormatResponse with the command formatted for display.



41
42
43
44
45
46
# File 'lib/rakit/shell.rb', line 41

def self.format(format_request)
  command = format_request.respond_to?(:command) ? format_request.command : nil
  fmt = format_request.respond_to?(:format) ? format_request.format : CommandFormat::ONE_LINE
  output = _format_command_to_s(command, fmt)
  FormatResponse.new(output: output)
end

.show(command, format_enum = nil) ⇒ Object

Display the command in the given format (default ONE_LINE). Prints to stdout. Builds a FormatRequest and uses the Format RPC.



50
51
52
53
54
# File 'lib/rakit/shell.rb', line 50

def self.show(command, format_enum = nil)
  fmt = format_enum || CommandFormat::ONE_LINE
  req = FormatRequest.new(command: command, format: fmt)
  puts CommandService.format(req).output
end

.test(command) ⇒ Object

Run the command and check expected_exit_code, expected_stdout, expected_stderr, and acceptance_criteria. Returns TestResult.



151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
# File 'lib/rakit/shell.rb', line 151

def self.test(command)
  result = execute(command)
  errors = []

  expected_exit = command.respond_to?(:expected_exit_code) ? command.expected_exit_code : 0
  errors << "exit_code: expected #{expected_exit}, got #{result.exit_status}" if result.exit_status != expected_exit

  if command.respond_to?(:expected_stdout) && !command.expected_stdout.to_s.empty? && result.stdout != command.expected_stdout
    errors << "stdout: expected #{command.expected_stdout.inspect}, got #{result.stdout.inspect}"
  end

  if command.respond_to?(:expected_stderr) && !command.expected_stderr.to_s.empty? && result.stderr != command.expected_stderr
    errors << "stderr: expected #{command.expected_stderr.inspect}, got #{result.stderr.inspect}"
  end

  if command.respond_to?(:acceptance_criteria)
    command.acceptance_criteria.each do |crit|
      kind = (crit.respond_to?(:kind) ? crit.kind : "").to_s
      value = (crit.respond_to?(:value) ? crit.value : "").to_s
      case kind
      when "exit_code"
        errors << "acceptance exit_code: expected #{value}, got #{result.exit_status}" unless result.exit_status.to_s == value
      when "stdout_contains"
        errors << "acceptance stdout_contains #{value.inspect}" unless result.stdout.to_s.include?(value)
      when "stderr_contains"
        errors << "acceptance stderr_contains #{value.inspect}" unless result.stderr.to_s.include?(value)
      when "stdout_matches"
        errors << "acceptance stdout_matches #{value.inspect}" unless result.stdout.to_s.match?(Regexp.new(value))
      when "stderr_matches"
        errors << "acceptance stderr_matches #{value.inspect}" unless result.stderr.to_s.match?(Regexp.new(value))
      end
    end
  end

  TestResult.new(success: errors.empty?, errors: errors)
end