Class: Ibex::GrammarTests::Runner
- Inherits:
-
Object
- Object
- Ibex::GrammarTests::Runner
- Defined in:
- lib/ibex/grammar_tests.rb,
sig/ibex/grammar_tests.rbs
Overview
Runs all examples in one isolated process while creating a fresh parser instance for every case.
Constant Summary collapse
- CHILD_RUNNER =
<<~'RUBY' require "json" load ARGV.fetch(0) parser_class = ARGV.fetch(1).split("::").reject(&:empty?).reduce(Object) do |scope, name| scope.const_get(name, false) end tests = JSON.parse(ARGV.fetch(2)) results = tests.map do |test| production_ids = [] begin parser = parser_class.new raise NoMethodError, "#{parser_class} must define parse(source)" unless parser.respond_to?(:parse) parser.observe do |event| production_ids << event.data.fetch("production_id") if event.type == :reduce end parser.parse(test.fetch("source")) result = { "actual" => "accept", "error_class" => nil, "error_message" => nil } rescue Ibex::Runtime::ParseError => error result = { "actual" => "reject", "error_class" => error.class.name, "error_message" => error.message } rescue SystemExit, SignalException, StandardError => error result = { "actual" => "error", "error_class" => error.class.name, "error_message" => error.message } end result.merge("production_ids" => production_ids) end puts "IBEX_GRAMMAR_TEST_RESULT=#{JSON.generate(results)}" RUBY
Instance Method Summary collapse
- #build_result(test, document) ⇒ Result
- #decode_results(output) ⇒ Array[Result]
- #execute_child(parser_path, runner_path) ⇒ Array[Result]
- #generated_parser ⇒ String
-
#initialize(automaton, timeout: DEFAULT_TIMEOUT) ⇒ Runner
constructor
A new instance of Runner.
- #production_coverage(results) ⇒ ProductionCoverage
- #raise_child_failure(status, errors) ⇒ bot
- #run ⇒ Array[Result]
- #wait_for_child(pid) ⇒ Process::Status
Constructor Details
#initialize(automaton, timeout: DEFAULT_TIMEOUT) ⇒ Runner
Returns a new instance of Runner.
90 91 92 93 94 95 |
# File 'lib/ibex/grammar_tests.rb', line 90 def initialize(automaton, timeout: DEFAULT_TIMEOUT) raise ArgumentError, "timeout must be a positive Integer" unless timeout.is_a?(Integer) && timeout.positive? @automaton = automaton @timeout = timeout end |
Instance Method Details
#build_result(test, document) ⇒ Result
186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 |
# File 'lib/ibex/grammar_tests.rb', line 186 def build_result(test, document) unless document.is_a?(Hash) && %w[accept reject error].include?(document["actual"]) raise Ibex::Error, "(test):1:1: grammar test process returned an invalid case result" end production_ids = document["production_ids"] production_count = @automaton.grammar.productions.length unless production_ids.is_a?(Array) && production_ids.all? { |id| id.is_a?(Integer) && id.between?(0, production_count - 1) } raise Ibex::Error, "(test):1:1: grammar test process returned invalid production coverage" end Result.new( expectation: test[:expectation], actual: document.fetch("actual").to_sym, error_class: document["error_class"], error_message: document["error_message"], location: test[:loc], production_ids: production_ids.freeze ).freeze end |
#decode_results(output) ⇒ Array[Result]
170 171 172 173 174 175 176 177 178 179 180 181 182 183 |
# File 'lib/ibex/grammar_tests.rb', line 170 def decode_results(output) line = output.lines.reverse.find { |candidate| candidate.start_with?(RESULT_MARKER) } raise Ibex::Error, "(test):1:1: grammar test process produced no result" unless line documents = JSON.parse(line.delete_prefix(RESULT_MARKER).strip) tests = @automaton.grammar.grammar_tests unless documents.is_a?(Array) && documents.length == tests.length raise Ibex::Error, "(test):1:1: grammar test process returned an invalid result count" end tests.zip(documents).map { |test, document| build_result(test, document) } rescue JSON::ParserError, ArgumentError => e raise Ibex::Error, "(test):1:1: invalid grammar test process result: #{e.}" end |
#execute_child(parser_path, runner_path) ⇒ Array[Result]
132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 |
# File 'lib/ibex/grammar_tests.rb', line 132 def execute_child(parser_path, runner_path) payload = JSON.generate(@automaton.grammar.grammar_tests) stdout = Tempfile.new("ibex-grammar-test-stdout") stderr = Tempfile.new("ibex-grammar-test-stderr") stdout_path = stdout.path || raise(Ibex::Error, "(test):1:1: missing temporary stdout path") stderr_path = stderr.path || raise(Ibex::Error, "(test):1:1: missing temporary stderr path") pid = spawn( RbConfig.ruby, runner_path, parser_path, @automaton.grammar.class_name, payload, out: stdout_path, err: stderr_path ) status = wait_for_child(pid) output = File.binread(stdout_path) errors = File.binread(stderr_path) raise_child_failure(status, errors) unless status.success? decode_results(output) ensure stdout&.close! stderr&.close! end |
#generated_parser ⇒ String
127 128 129 |
# File 'lib/ibex/grammar_tests.rb', line 127 def generated_parser Codegen::Ruby.new(@automaton, table: :compact, embedded: true, line_convert: true).generate end |
#production_coverage(results) ⇒ ProductionCoverage
115 116 117 118 119 120 121 122 |
# File 'lib/ibex/grammar_tests.rb', line 115 def production_coverage(results) production_count = @automaton.grammar.productions.length covered_ids = results.flat_map(&:production_ids).uniq.sort.freeze missing_ids = ((0...production_count).to_a - covered_ids).freeze ProductionCoverage.new( covered_ids: covered_ids, missing_ids: missing_ids, production_count: production_count ).freeze end |
#raise_child_failure(status, errors) ⇒ bot
163 164 165 166 167 |
# File 'lib/ibex/grammar_tests.rb', line 163 def raise_child_failure(status, errors) detail = errors.lines.last&.strip suffix = detail && !detail.empty? ? ": #{detail}" : "" raise Ibex::Error, "(test):1:1: grammar test process exited with status #{status.exitstatus}#{suffix}" end |
#run ⇒ Array[Result]
98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 |
# File 'lib/ibex/grammar_tests.rb', line 98 def run grammar = @automaton.grammar raise Ibex::Error, "(test):1:1: grammar declares no %test cases" if grammar.grammar_tests.empty? unless grammar.parser_parameters.empty? raise Ibex::Error, "(test):1:1: %test cannot instantiate a parser with required %param declarations" end Dir.mktmpdir("ibex-grammar-tests") do |directory| parser_path = File.join(directory, "parser.rb") runner_path = File.join(directory, "runner.rb") File.binwrite(parser_path, generated_parser) File.binwrite(runner_path, CHILD_RUNNER) execute_child(parser_path, runner_path) end end |
#wait_for_child(pid) ⇒ Process::Status
154 155 156 157 158 159 160 |
# File 'lib/ibex/grammar_tests.rb', line 154 def wait_for_child(pid) Timeout.timeout(@timeout) { Process.wait2(pid).fetch(1) } rescue Timeout::Error Process.kill("KILL", pid) Process.wait(pid) raise Ibex::Error, "(test):1:1: grammar tests exceeded #{@timeout} seconds" end |