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 =
10 * 1024 * 1024
Instance Method Summary collapse
- #bounded_reduction_source(path) ⇒ String
- #positive_reduce_option!(name, value) ⇒ Integer
- #prepare_reduction_run ⇒ void
- #reduce_option_parser ⇒ OptionParser
- #reduction_failure?(command, candidate, mode) ⇒ Boolean
- #reduction_items(source, mode) ⇒ Array[String | Integer]
- #run_reduce_command(arguments) ⇒ Integer
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
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
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_run ⇒ void
This method returns an undefined value.
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_parser ⇒ 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 || . = "Usage: ibex reduce --command=COMMAND [options] input" .on("--command=COMMAND", "nonzero exit means the failure persists") do |value| @options[:reduce_command] = value end .on("--mode=MODE", %w[tokens lines bytes], "tokens, lines, or bytes") do |value| @options[:reduce_mode] = value.to_sym end .on("--max-trials=N", Integer, "subprocess trial budget (default 1000)") do |value| @options[:reduce_max_trials] = positive_reduce_option!("max-trials", value) end .on("--timeout=SECONDS", Integer, "checker timeout per trial (default 10)") do |value| @options[:reduce_timeout] = positive_reduce_option!("timeout", value) end .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 .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 .on("--format=FORMAT", %w[json text], "json or text (default json)") do |value| @options[:reduce_format] = value end .on("--help", "show help") { @options[:help] = true } end end |
#reduction_failure?(command, candidate, mode) ⇒ 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" = result.timed_out ? "checker timed out" : "checker exceeded its output byte budget" raise ReductionBudgetExceeded.new(kind: kind, 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]
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.}" end |
#run_reduce_command(arguments) ⇒ 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 |