1#!/usr/bin/env ruby 2# Copyright (c) 2021-2022 Huawei Device Co., Ltd. 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15require 'optparse' 16require 'ostruct' 17require 'logger' 18require 'fileutils' 19require 'open3' 20 21options = OpenStruct.new 22OptionParser.new do |opts| 23 opts.banner = 'Usage: checker.rb [options] TEST_FILE' 24 25 opts.on('--run-prefix=PREFIX', 'Prefix that will be inserted before panda run command') do |v| 26 options.run_prefix = v 27 end 28 opts.on('--source=FILE', 'Path to source file') 29 opts.on('--test-file=FILE', 'Path to test file') do |v| 30 options.test_file = v 31 end 32 opts.on('--panda=PANDA', 'Path to panda') 33 opts.on('--paoc=PAOC', 'Path to paoc') do |v| 34 options.paoc = v 35 end 36 opts.on('--panda-options=OPTIONS', 'Default options for panda run') do |v| 37 options.panda_options = v 38 end 39 opts.on('--paoc-options=OPTIONS', 'Default options for paoc run') do |v| 40 options.paoc_options = v 41 end 42 opts.on('--command-token=STRING', 'String that is recognized as command start') do |v| 43 options.command_token = v 44 end 45 opts.on('--release', 'Run in release mode. EVENT, INST and other will not be checked') 46 opts.on('-v', '--verbose', 'Verbose logging') 47 opts.on('--arch=ARCHITECTURE', 'Architecture of system where start panda') 48 opts.on("--keep-data", "Do not remove generated data from disk") { |v| options.keep_data = true } 49end.parse!(into: options) 50 51$LOG_LEVEL = options.verbose ? Logger::DEBUG : Logger::ERROR 52$curr_cmd = nil 53 54def log 55 @log ||= Logger.new($stdout, level: $LOG_LEVEL) 56end 57 58def raise_error(msg) 59 log.error "Test failed: #{@name}" 60 log.error msg 61 log.error "Command to reproduce: #{$curr_cmd}" 62 raise msg 63end 64 65def match_str(match) 66 match.is_a?(Regexp) ? "/#{match.source}/" : match 67end 68 69def contains?(str, match) 70 return str =~ match if match.is_a? Regexp 71 72 raise_error "Wrong type for search: #{match.class}" unless match.is_a? String 73 str.include? match 74end 75 76# Provides methods to search lines in a given array 77class SearchScope 78 79 attr_reader :lines 80 81 def initialize(lines, name) 82 @lines = lines 83 @name = name 84 @current_index = 0 85 end 86 87 def self.from_file(fname, name) 88 SearchScope.new(File.readlines(fname), name) 89 end 90 91 def find(match) 92 return if match.nil? 93 94 @current_index = @lines.index { |line| contains?(line, match) } 95 raise_error "#{@name} not found: #{match_str(match)}" if @current_index.nil? 96 @current_index += 1 97 end 98 99 def find_next(match) 100 return if match.nil? 101 102 index = @lines.drop(@current_index).index { |line| contains?(line, match) } 103 raise_error "#{@name} not found: #{match_str(match)}" if index.nil? 104 @current_index += index + 1 105 end 106 107 def find_not(match) 108 return if match.nil? 109 110 @lines.each do |line| 111 raise_error "#{@name} should not occur: #{match_str(match)}" if contains?(line, match) 112 end 113 end 114 115 def find_next_not(match) 116 return if match.nil? 117 118 @lines.drop(@current_index).each do |line| 119 raise_error "#{@name} should not occur: #{match_str(match)}" if contains?(line, match) 120 end 121 end 122 123 def to_s 124 "Scope '#{@name}', current=#{@current_index}\n#{@lines.join}" 125 end 126end 127 128class Checker 129 attr_reader :name 130 131 def initialize(options, name) 132 @name = name 133 @lines = [] 134 @code = "" 135 @cwd = "#{Dir.getwd}/#{name.gsub(/[ -:()]/, '_')}" 136 @options = options 137 @args = '' 138 @ir_files = [] 139 @architecture = options.arch 140 @aot_file = '' 141 142 # Events scope for 'events.csv' 143 @events_scope = nil 144 # IR scope for IR dumps files 'ir_dump/*.ir' 145 @ir_scope = nil 146 147 # Disassembly file lines, that were read from 'disasm.txt' 148 @disasm_lines = nil 149 # Currently processing disasm method 150 @disasm_method_scope = nil 151 # Current search scope 152 @disasm_scope = nil 153 154 Dir.mkdir(@cwd) unless File.exists?(@cwd) 155 clear_data 156 end 157 158 def append_line(line) 159 @code << line 160 end 161 162 def RUN(**args) 163 expected_result = 0 164 aborted_sig = 0 165 entry = '_GLOBAL::main' 166 env = '' 167 args.each do |name, value| 168 if name == :force_jit and value 169 @args << '--compiler-hotness-threshold=0 --no-async-jit=true --compiler-enable-jit=true ' 170 elsif name == :options 171 @args << value 172 elsif name == :entry 173 entry = value 174 elsif name == :result 175 expected_result = value 176 elsif name == :abort 177 aborted_sig = value 178 elsif name == :env 179 env = value 180 end 181 end 182 183 clear_data 184 aot_arg = @aot_file.empty? ? '' : "--aot-file #{@aot_file}" 185 186 $curr_cmd = "#{env} #{@options.run_prefix} #{@options.panda} --compiler-ignore-failures=false #{@options.panda_options} \ 187 #{aot_arg} #{@args} --events-output=csv --compiler-dump --compiler-disasm-dump:single-file #{@options.test_file} #{entry}" 188 log.debug "Panda command: #{$curr_cmd}" 189 190 output, status = Open3.capture2e($curr_cmd.to_s, chdir: @cwd.to_s) 191 if status.signaled? 192 if status.termsig != aborted_sig 193 puts output 194 log.error "panda aborted with signal #{status.termsig}, but expected #{aborted_sig}" 195 raise_error "Test '#{@name}' failed" 196 end 197 elsif status.exitstatus != expected_result 198 puts output 199 log.error "panda returns code #{status.exitstatus}, but expected #{expected_result}" 200 raise_error "Test '#{@name}' failed" 201 end 202 log.debug output 203 204 @events_scope = SearchScope.from_file("#{@cwd}/events.csv", 'Events') 205 @ir_files = Dir['ir_dump/*.ir'] 206 end 207 208 def RUN_PAOC(**args) 209 @aot_file = "#{Dir.getwd}/#{File.basename(@options.test_file, File.extname(@options.test_file))}.an" 210 211 inputs = @options.test_file 212 aot_output_option = '--paoc-output' 213 output = @aot_file 214 options = '' 215 env = '' 216 aborted_sig = 0 217 218 args.each do |name, value| 219 case name 220 when :options 221 options = value 222 when :boot 223 aot_output_option = '--paoc-boot-output' 224 when :env 225 env = value 226 when :inputs 227 inputs = value 228 when :abort 229 aborted_sig = value 230 when :output 231 output = value 232 end 233 end 234 235 paoc_args = "--paoc-panda-files #{inputs} --events-output=csv --compiler-dump #{options} #{aot_output_option} #{output}" 236 237 clear_data 238 239 $curr_cmd = "#{env} #{@options.run_prefix} #{@options.paoc} --compiler-ignore-failures=false --compiler-disasm-dump:single-file #{@options.paoc_options} #{paoc_args}" 240 log.debug "Paoc command: #{$curr_cmd}" 241 242 output, status = Open3.capture2e($curr_cmd.to_s, chdir: @cwd.to_s) 243 if status.signaled? 244 if status.termsig != aborted_sig 245 puts output 246 log.error "panda aborted with signal #{status.termsig}, but expected #{aborted_sig}" 247 raise_error "Test '#{@name}' failed" 248 end 249 elsif status.exitstatus != 0 250 puts output 251 log.error "paoc failed: #{status.exitstatus}" 252 raise_error "Test '#{@name}' failed" 253 end 254 log.debug output 255 256 @events_scope = SearchScope.from_file("#{@cwd}/events.csv", 'Events') 257 @ir_files = Dir['ir_dump/*.ir'] 258 end 259 260 def EVENT(match) 261 return if @options.release 262 263 @events_scope.find(match) 264 end 265 266 def EVENT_NEXT(match) 267 return if @options.release 268 269 @events_scope.find_next(match) 270 end 271 272 def EVENT_COUNT(match) 273 return 0 if @options.release 274 275 @events_scope.lines.count { |event| contains?(event, match) } 276 end 277 278 def EVENT_NOT(match) 279 return if @options.release 280 281 @events_scope.find_not(match) 282 end 283 284 def EVENT_NEXT_NOT(match) 285 return if @options.release 286 287 @events_scope.find_next_not(match) 288 end 289 290 def EVENTS_COUNT(match, count) 291 return if @options.release 292 293 res = @events_scope.lines.count { |event| contains?(event, match) } 294 raise_error "Events count missmatch for #{match}, expected: #{count}, real: #{res}" unless res == count 295 end 296 297 def TRUE(condition) 298 return if @options.release 299 300 raise_error "Not true condition: \"#{condition}\"" unless condition 301 end 302 303 class SkipException < StandardError 304 end 305 306 def SKIP_IF(condition) 307 return if @options.release 308 raise SkipException if condition 309 end 310 311 def IR_COUNT(match) 312 return 0 if @options.release 313 314 @ir_scope.lines.count { |inst| contains?(inst, match) } 315 end 316 317 def BLOCK_COUNT 318 IR_COUNT('BB ') 319 end 320 321 def INST(match) 322 return if @options.release 323 324 @ir_scope.find(match) 325 end 326 327 def INST_NEXT(match) 328 return if @options.release 329 330 @ir_scope.find_next(match) 331 end 332 333 def INST_NOT(match) 334 return if @options.release 335 336 @ir_scope.find_not(match) 337 end 338 339 def INST_NEXT_NOT(match) 340 return if @options.release 341 342 @ir_scope.find_next_not(match) 343 end 344 345 def INST_COUNT(match, count) 346 return if @options.release 347 348 real_count = IR_COUNT(match) 349 raise_error "IR_COUNT mismatch: expected=#{count}, real=#{real_count}" unless real_count == count 350 end 351 352 module SearchState 353 NONE = 0 354 SEARCH_BODY = 1 355 SEARCH_END = 2 356 end 357 358 def ASM_METHOD(match) 359 ensure_disasm 360 state = SearchState::NONE 361 start_index = nil 362 end_index = -1 363 @disasm_lines.each_with_index do |line, index| 364 case state 365 when SearchState::NONE 366 if line.start_with?('METHOD_INFO:') && contains?(@disasm_lines[index + 1].split(':', 2)[1].strip, match) 367 state = SearchState::SEARCH_BODY 368 end 369 when SearchState::SEARCH_BODY 370 if line.start_with?('DISASSEMBLY') 371 start_index = index + 1 372 state = SearchState::SEARCH_END 373 end 374 when SearchState::SEARCH_END 375 if line.start_with?('METHOD_INFO:') 376 end_index = index - 1 377 break 378 end 379 end 380 end 381 raise "Method not found: #{match_str(match)}" if start_index.nil? 382 383 @disasm_method_scope = SearchScope.new(@disasm_lines[start_index..end_index], "Method: #{match_str(match)}") 384 @disasm_scope = @disasm_method_scope 385 end 386 387 def ASM_INST(match) 388 ensure_disasm 389 state = SearchState::NONE 390 start_index = nil 391 end_index = -1 392 prefix = nil 393 @disasm_method_scope.lines.each_with_index do |line, index| 394 case state 395 when SearchState::NONE 396 if contains?(line, match) 397 prefix = line.sub(/#.*/, '#').gsub("\n", '') 398 start_index = index + 1 399 state = SearchState::SEARCH_END 400 end 401 when SearchState::SEARCH_END 402 if line.start_with?(prefix) 403 end_index = index - 1 404 break 405 end 406 end 407 end 408 raise "Can not find asm instruction: #{match}" if start_index.nil? 409 410 @disasm_scope = SearchScope.new(@disasm_method_scope.lines[start_index..end_index], "Inst: #{match_str(match)}") 411 end 412 413 def ASM_RESET 414 @disasm_scope = @disasm_method_scope 415 end 416 417 def ASM(**kwargs) 418 ensure_disasm 419 @disasm_scope.find(select_asm(kwargs)) 420 end 421 422 def ASM_NEXT(**kwargs) 423 ensure_disasm 424 @disasm_scope.find_next(select_asm(kwargs)) 425 end 426 427 def ASM_NOT(**kwargs) 428 ensure_disasm 429 @disasm_scope.find_not(select_asm(kwargs)) 430 end 431 432 def ASM_NEXT_NOT(**kwargs) 433 ensure_disasm 434 @disasm_scope.find_next_not(select_asm(kwargs)) 435 end 436 437 def select_asm(kwargs) 438 kwargs[@options.arch.to_sym] 439 end 440 441 def ensure_disasm 442 @disasm_lines ||= File.readlines("#{@cwd}/disasm.txt") 443 end 444 445 def METHOD(method) 446 return if @options.release 447 448 @ir_files = Dir["#{@cwd}/ir_dump/*#{method.sub('::', '_')}*.ir"] 449 @ir_files.sort! 450 raise_error "IR dumps not found for method: #{method.sub('::', '_')}" if @ir_files.empty? 451 @current_method = method 452 end 453 454 def PASS_AFTER(pass) 455 return if @options.release 456 457 fname = @ir_files.detect { |x| File.basename(x).include? pass } 458 raise_error "IR file not found for pass: #{pass}" unless fname 459 @ir_scope = SearchScope.from_file(fname, 'IR') 460 end 461 462 def PASS_BEFORE(pass) 463 return if @options.release 464 465 index = @ir_files.index { |x| File.basename(x).include? pass } 466 raise_error "IR file not found for pass: #{pass}" unless index 467 @ir_scope = SearchScope.from_file(@ir_files[index - 1], 'IR') 468 end 469 470 def run 471 log.info "Running \"#{@name}\"" 472 begin 473 self.instance_eval @code 474 rescue SkipException 475 log.info "Skipped: \"#{@name}\"" 476 else 477 log.info "Success: \"#{@name}\"" 478 end 479 clear_data 480 end 481 482 def clear_data 483 if !@options.keep_data 484 FileUtils.rm_rf("#{@cwd}/ir_dump") 485 FileUtils.rm_rf("#{@cwd}/events.csv") 486 FileUtils.rm_rf("#{@cwd}/disasm.txt") 487 end 488 end 489end 490 491def read_checks(options) 492 checks = [] 493 check = nil 494 checker_start = "#{options.command_token} CHECKER" 495 File.readlines(options.source).each do |line| 496 if check 497 unless line.start_with? options.command_token 498 check = nil 499 next 500 end 501 check.append_line(line[options.command_token.size..-1]) 502 else 503 next unless line.start_with? checker_start 504 505 name = line.split(' ', 3)[2].strip 506 raise "Checker with name '#{name}'' already exists" if checks.any? { |x| x.name == name } 507 508 check = Checker.new(options, name) 509 checks << check 510 end 511 end 512 checks 513end 514 515def main(options) 516 read_checks(options).each(&:run) 517 0 518end 519 520if __FILE__ == $PROGRAM_NAME 521 main(options) 522end 523 524# Somehow ruby resolves `Checker` name to another class in a Testing scope, so make this global 525# variable to refer to it from unit tests. I believe there is more proper way to do it, but I 526# didn't find it at first glance. 527$CheckerForTest = Checker