• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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