Module: Ibex::CLIReduce

Includes:
CLIReduceReporting
Defined in:
lib/ibex/cli/reduce.rb,
sig/ibex/cli/reduce.rbs

Overview

CLI entry point for generic bounded delta debugging.

Defined Under Namespace

Classes: ReductionBudgetExceeded

Constant Summary collapse

DEFAULT_MAX_INPUT_BYTES =

Signature:

  • Integer

Returns:

  • (Integer)
10 * 1024 * 1024

Instance Method Summary collapse

Methods included from CLIReduceReporting

#default_reduction_bounds, #reduction_budget_report, #reduction_success_report, #write_reduction_budget, #write_reduction_success

Instance Method Details

#bounded_reduction_source(path) ⇒ String

RBS:

  • (String path) -> String

Parameters:

  • path (String)

Returns:

  • (String)


146
147
148
149
150
151
152
153
154
# File 'lib/ibex/cli/reduce.rb', line 146

def bounded_reduction_source(path)
  maximum = @reduction_bounds.fetch(:max_input_bytes)
  source = File.binread(path, maximum + 1)
  return source if source.bytesize <= maximum

  raise ReductionBudgetExceeded.new(
    kind: "input_bytes", message: "input exceeds #{maximum} bytes", trials: 0
  )
end

#positive_reduce_option!(name, value) ⇒ Integer

RBS:

  • (String name, Integer value) -> Integer

Parameters:

  • name (String)
  • value (Integer)

Returns:

  • (Integer)


196
197
198
199
200
# File 'lib/ibex/cli/reduce.rb', line 196

def positive_reduce_option!(name, value)
  return value if value.positive?

  raise OptionParser::InvalidArgument, "--#{name} must be positive"
end

#prepare_reduction_runvoid

This method returns an undefined value.

RBS:

  • () -> void



128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
# File 'lib/ibex/cli/reduce.rb', line 128

def prepare_reduction_run
  @reduction_mode = @options.fetch(:reduce_mode, :tokens)
  @reduction_bounds = {
    max_trials: @options.fetch(:reduce_max_trials, 1_000),
    timeout_seconds: @options.fetch(:reduce_timeout, BoundedSubprocess::DEFAULT_TIMEOUT_SECONDS),
    max_output_bytes: @options.fetch(
      :reduce_max_output_bytes, BoundedSubprocess::DEFAULT_MAX_OUTPUT_BYTES
    ),
    max_input_bytes: @options.fetch(:reduce_max_input_bytes, DEFAULT_MAX_INPUT_BYTES)
  }
  @reduction_trials = 0
  @reduction_subprocess = BoundedSubprocess.new(
    timeout_seconds: @reduction_bounds.fetch(:timeout_seconds),
    max_output_bytes: @reduction_bounds.fetch(:max_output_bytes)
  )
end

#reduce_option_parserOptionParser

RBS:

  • () -> OptionParser

Returns:

  • (OptionParser)


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
# File 'lib/ibex/cli/reduce.rb', line 99

def reduce_option_parser
  OptionParser.new do |options|
    options.banner = "Usage: ibex reduce --command=COMMAND [options] input"
    options.on("--command=COMMAND", "nonzero exit means the failure persists") do |value|
      @options[:reduce_command] = value
    end
    options.on("--mode=MODE", %w[tokens lines bytes], "tokens, lines, or bytes") do |value|
      @options[:reduce_mode] = value.to_sym
    end
    options.on("--max-trials=N", Integer, "subprocess trial budget (default 1000)") do |value|
      @options[:reduce_max_trials] = positive_reduce_option!("max-trials", value)
    end
    options.on("--timeout=SECONDS", Integer, "checker timeout per trial (default 10)") do |value|
      @options[:reduce_timeout] = positive_reduce_option!("timeout", value)
    end
    options.on("--max-output-bytes=N", Integer, "checker stdout/stderr byte budget") do |value|
      @options[:reduce_max_output_bytes] = positive_reduce_option!("max-output-bytes", value)
    end
    options.on("--max-input-bytes=N", Integer, "input byte budget (default 10485760)") do |value|
      @options[:reduce_max_input_bytes] = positive_reduce_option!("max-input-bytes", value)
    end
    options.on("--format=FORMAT", %w[json text], "json or text (default json)") do |value|
      @options[:reduce_format] = value
    end
    options.on("--help", "show help") { @options[:help] = true }
  end
end

#reduction_failure?(command, candidate, mode) ⇒ Boolean

RBS:

  • (Array[String] command, Array[String | Integer] candidate, Symbol mode) -> bool

Parameters:

  • command (Array[String])
  • candidate (Array[String | Integer])
  • mode (Symbol)

Returns:

  • (Boolean)


174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
# File 'lib/ibex/cli/reduce.rb', line 174

def reduction_failure?(command, candidate, mode)
  input = case mode
          when :tokens then JSON.generate(candidate)
          when :lines then candidate.join
          when :bytes then candidate.pack("C*")
          else raise Ibex::Error, "(reduce):1:1: unknown reduction mode #{mode.inspect}"
          end
  @reduction_trials += 1
  result = @reduction_subprocess.run(command, input: input)
  if result.timed_out || result.output_limited
    kind = result.timed_out ? "subprocess_timeout" : "subprocess_output"
    message = result.timed_out ? "checker timed out" : "checker exceeded its output byte budget"
    raise ReductionBudgetExceeded.new(kind: kind, message: message, trials: @reduction_trials)
  end
  unless result.status.exited?
    raise Ibex::Error, "(reduce):1:1: checker terminated by signal #{result.status.termsig}"
  end

  !result.status.success?
end

#reduction_items(source, mode) ⇒ Array[String | Integer]

RBS:

  • (String source, Symbol mode) -> Array[String | Integer]

Parameters:

  • source (String)
  • mode (Symbol)

Returns:

  • (Array[String | Integer])


157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
# File 'lib/ibex/cli/reduce.rb', line 157

def reduction_items(source, mode)
  case mode
  when :tokens
    parsed = JSON.parse(source)
    raise Ibex::Error, "(reduce):1:1: token input must be a JSON array" unless parsed.is_a?(Array)
    raise Ibex::Error, "(reduce):1:1: every token must be a string" unless parsed.all?(String)

    parsed
  when :lines then source.lines
  when :bytes then source.bytes
  else raise Ibex::Error, "(reduce):1:1: unknown reduction mode #{mode.inspect}"
  end
rescue JSON::ParserError => e
  raise Ibex::Error, "(reduce):1:1: invalid token JSON: #{e.message}"
end

#run_reduce_command(arguments) ⇒ Integer

RBS:

  • (Array[String] arguments) -> Integer

Parameters:

  • arguments (Array[String])

Returns:

  • (Integer)


74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/ibex/cli/reduce.rb', line 74

def run_reduce_command(arguments)
  parser = reduce_option_parser
  remaining = parser.parse(arguments)
  return print_help(parser) if @options[:help]

  path = input_path(remaining)
  configured_command = @options[:reduce_command]
  raise OptionParser::MissingArgument, "--command" unless configured_command

  command = Shellwords.split(configured_command)
  raise OptionParser::InvalidArgument, "--command must not be empty" if command.empty?

  prepare_reduction_run
  source = bounded_reduction_source(path)
  items = reduction_items(source, @reduction_mode)
  reducer = DeltaReducer.new(max_trials: @reduction_bounds.fetch(:max_trials))
  result = reducer.minimize(items) { |candidate| reduction_failure?(command, candidate, @reduction_mode) }
  write_reduction_success(reduction_success_report(result))
  result.complete ? 0 : 2
rescue ReductionBudgetExceeded => e
  write_reduction_budget(reduction_budget_report(e.details))
  2
end