#!/usr/bin/env ruby require 'fileutils' $:.unshift(File.dirname(__FILE__)+"/../lib") require "bakeqac/options/options" require "bakeqac/filter" require 'bake/toolchain/colorizing_formatter' require 'bake/options/options' require 'common/process' require 'common/utils' require 'common/ext/file' STDOUT.sync = true STDERR.sync = true module Bake @@cipFileSize = 1 def self.checkCipSize() cips = Dir.glob(@options.qacdata + "/**/*.cip") if !cips.empty? s = File.size?(cips[0]) s = 0 if s.nil? if s < @@cipFileSize puts "bakeqac: cip file too small: #{cips[0]} has #{s} bytes" return false end @@cipFileSize = s return true end puts "bakeqac: cip file does not exist!" return false end def self.executeQacli(cmd, adminStepAndImmediateOutput = false) timeStart = Time.now cSizeCheck = true licenseError = false qacDaemonError = false consoleOutput = "" success = false processTimeout = false loop do FileUtils.rm_rf @options.qacdata if adminStepAndImmediateOutput FileUtils::mkdir_p @options.qacdata cmdDupped = cmd.dup cmdDupped << @options.qacdata # admin step should not take longer than 60 seconds, rest not longer than 80% of qacretry time if specified success, consoleOutput = ProcessHelper.run(cmdDupped, adminStepAndImmediateOutput, true, nil, [0], Dir.pwd, adminStepAndImmediateOutput ? 60 : (@options.qacretry * 0.8).to_i) licenseError = false qacDaemonError = false processTimeout = false consoleOutput.each_line do |line| if (line.include?("License Refused") && !line.include?("License Refused: C:")) licenseError = true puts "bakeqac: License refused!" break end if (line.include?("unable to connect to QAX Daemon") && !line.include?("License Refused: C:")) qacDaemonError = true puts "bakeqac: Unable to connect to QAX Daemon!" break end if (line.include?("Process timeout") || line.include?("A mutually exclusive operation is in progress")) puts line processTimeout = true if !adminStepAndImmediateOutput puts "bakeqac: Removing locks from #{@options.qacdata}..." sleep(1) successRemove, consoleOutput = ProcessHelper.run($cmdRemoveLocks, false) puts consoleOutput unless consoleOutput.empty? end break end end cSizeCheck = checkCipSize() break unless ((@options.qacretry >= (Time.now - timeStart)) && (!cSizeCheck || licenseError || qacDaemonError || processTimeout)) puts "bakeqac: Retry seconds left: %d" % (@options.qacretry - (Time.now - timeStart)) if processTimeout && adminStepAndImmediateOutput @options.incrementQacdata end end checkError = !cSizeCheck || licenseError || qacDaemonError || processTimeout if @options.qacretry > 0 && checkError puts "bakeqac: Retry timeout over: %d -> failure." % (@options.qacretry - (Time.now - timeStart)) end success = false if checkError return [success, consoleOutput, checkError] end def self.getLineNumbersOfFunctions(entities) fileFunctions = [] entities.each do |e2| if e2.has_key?("type") && e2["type"] == "function" fileFunctions << e2["line"].to_i end end fileFunctions.sort return fileFunctions end ###### PREREQUISITE 1: BAKEQAC OPTIONS ###### @options = BakeqacOptions.new(ARGV) bakeOptions = Options.new([]) @options.parse_options(bakeOptions) pfilter = ProjectFilter.new(@options) success = false ###### PREREQUISITE 2: BAKE OPTIONS ###### passedParams = [] excludeParam = false wasMinus = false ARGV.each do |x| if ["--c++11", "--c++14", "--qacnofilefilter", "--qacnomsgfilter", "--qacnofilter", "--qacnoformat", "--qacrawformat", "--qacunittest", "--qacdoc", "--qaccctpatch"].include?x excludeParam = false next elsif ["--cct", "--rcf", "--acf", "--qacdata", "--qacstep", "--qacretry"].include?x excludeParam = true next elsif excludeParam excludeParam = false next end passedParams << x end passedParams << "--rebuild" unless passedParams.include?"--rebuild" passedParams << "--compile-only" if (passedParams & ["--compile-only", "--compile_only", "-f"]).empty? passedParams << "--filter-paths" unless passedParams.include?"--filter-paths" passedParams << "--qac" unless passedParams.include?"--qac" success = true ###### PREREQUISITE 3: QACLI LOCATION ###### qaExe = [@options.qac_home+"/common/bin/qacli"] if @options.qacunittest qaExe = ["ruby", File.dirname(__FILE__)+"/../spec/bin/qactester.rb"] end $cmdRemoveLocks = qaExe + ["admin", "--qaf-project", @options.qacdata, "--remove-locks"] ###### STEP 1: CREATE ###### if (@options.qacstep.nil? or @options.qacstep.include?"admin") cmd = qaExe + ["admin", "--qaf-project-config"] @options.cct.each {|c| cmd << "--cct" << c } cmd << "--rcf" << @options.rcf cmd << "--acf" << @options.acf cmd << "--qaf-project" # << @options.qacdata puts "bakeqac: creating database..." success, consoleOutput, checkError = executeQacli(cmd, true) if success cct220 = (File.exist?(@options.qacdata+"/prqa/configs/Initial_Config/config") ? "configs/Initial_Config/" : "") cctFilename = @options.qacdata+"/prqa/" + cct220 + "config/" + File.basename(@options.cct[0]) if File.exist?cctFilename File.open(cctFilename, 'a') do |f| if @options.cct_patch f << "-coderegex='s/__attribute__\\s*\\(.*\\)//'\n" f << "-d __cdecl=\n" f << "-d _cdecl=\n" f << "-d _thiscall=\n" f << "-d __stdcall=\n" end if @options.cct_append File.open(@options.cct_append, 'r') do |a| a.each_line { |line| f << line } end end end end else puts consoleOutput unless consoleOutput.empty? end end ###### STEP 2: BUILD ###### if success and (@options.qacstep.nil? or @options.qacstep.include?"analyze") FileUtils::mkdir_p @options.qacdata cmd = qaExe + ["analyze", "-b"] begin devMode = File.exist?"c:/Projekte/git/bake/bin/bake" rescue Exception devMode = false end if devMode bcmd = "ruby c:/Projekte/git/bake/bin/bake " else bcmd = (Utils::OS.windows? ? "cmd /c bake.bat " : "bake ") end bcmd += passedParams.join(" ") cmd << bcmd cmd << "-P" # << @options.qacdata puts "bakeqac: building and analyzing files..." success, consoleOutput, checkError = executeQacli(cmd) success = false # we have to parse the output, qacli returns always an error here... if !checkError filterOutput = [] filter = [] endFound = false consoleOutput.each_line do |line| scan_res = line.scan(/Project path: ([a-zA-Z]{0,1})(:{0,1})(.*)/) if scan_res.length > 0 filter << (scan_res[0][0].downcase + scan_res[0][1] + scan_res[0][2].gsub(/\\/,"/").strip) elsif !endFound filterOutput << line if line.start_with?("Rebuilding ") endFound = true success = true if line.include?("Rebuilding done") # don't know why the return value is 2 in this case... end end end end if success ProjectFilter.writeFilter(filter) end if @options.qacmsgfilter if success puts filterOutput else puts consoleOutput # error end else puts consoleOutput # no filter end end ###### STEP 3: RESULT ###### if success and (@options.qacstep.nil? or @options.qacstep.include?"view") FileUtils::mkdir_p @options.qacdata puts "bakeqac: printing results..." cmd = qaExe + ["view", "-m", "STDOUT"] if not @options.qacnoformat cmd += ["-f", "%?u==0%(MSG: %:trc: %)%F(%l,%c): (%r:%N)%t%?v%(\n%v%)"] end cmd << "-P" # << @options.qacdata, success, consoleOutput, checkError = executeQacli(cmd) if ProjectFilter.is_valid? if !checkError # success # HACK: seems that QAC returns with error if there too much issues # 1. filter filteredLines = [] foundFile = false consoleOutput.each_line do |line| line.strip! foundFile = false if line.empty? or line.include? " ======= Results for " scan_res = line.scan(/\/\/ ======= Results for ([a-zA-Z]{0,1})(:{0,1})(.*)/) if scan_res.length > 0 converted_line = (scan_res[0][0].downcase + scan_res[0][1] + scan_res[0][2].gsub(/\\/,"/")) foundFile = ProjectFilter.localFile(converted_line) end if foundFile && (!line.include?("QAC++ Deep Flow Static Analyser") || !@options.qacmsgfilter) filteredLines << line else misraLine = ["MSG: ", "trc: ", "// ======= Results for", "QAC++ Deep Flow Static Analyser"].none? { |pattern| line.include?(pattern) } if !success && misraLine # HACK, see above filteredLines << line end end end # 2. sort sortedLines = [] linesOfFile = {} lineNum = 0 currentMessage = [] filteredLines.each do |line| if line.include? " ======= Results for " lineNum = 0 linesOfFile.sort.each { |lineNr, s| sortedLines += s } linesOfFile = {} sortedLines << line else if (!@options.qacnoformat && line.start_with?("MSG: ")) || (@options.qacnoformat && line.include?(": Msg(")) lineScan = line.scan(/[^\(]*\((\d*)/) lineNum = lineScan[0][0].to_i if lineScan.length > 0 end if linesOfFile.has_key?lineNum currentMessage = linesOfFile[lineNum] else currentMessage = [] linesOfFile[lineNum] = currentMessage end currentMessage << line end end linesOfFile.sort.each { |lineNr, s| sortedLines += s } # 3. print numberOfMessages = 0 Dir.chdir(@options.qac_home) do sortedLines.each do |line| success = true if line.include?"The specified project does not have any source files" # to proceed we assume that this is a valid scenario if (!@options.qacnoformat && line.start_with?("MSG: ")) || (@options.qacnoformat && line.include?(": Msg(")) Bake.formatter.printWarning(line) if @options.qacdoc errorNumScan = line.scan(/[^\(]*\([^\(]*\([^:]*:(\d*)/) if errorNumScan.length > 0 errorNum = errorNumScan[0][0].to_i htmlFiles = Dir.glob("components/qacpp*/doc-en_US/messages/%04d.html" % errorNum) + Dir.glob("components/mcpp*/doc-en_US/messages/%04d.html" % errorNum) htmlFiles.each do |htmlFile| puts "doc: #{File.expand_path(htmlFile)}" end end end numberOfMessages += 1 elsif line.include? " ======= Results for " Bake.formatter.printAdditionalInfo(line) else Bake.formatter.printInfo(line) end end Bake.formatter.printSuccess("\n**** Number of messages: #{numberOfMessages} ****") if !success # HACK, see above Bake.formatter.printInfo("\nNote: qacli exited with error, maybe the number of messages was too high and output was truncated.") success = true end end else puts consoleOutput # error end else puts consoleOutput # no filter end end ###### STEP 4a: REPORT SCRIPT CHECK (OPTIONAL) ###### if success and !@options.qacstep.nil? and @options.qacstep.include?("report") puts "bakeqac: preparing reports..." # check if adapted scriptsAdapted = true [@options.qac_home+"/report_plugins/Rule_Compliance_Report.py", @options.qac_home+"/report_plugins/Suppressions_Report.py"].each do |script| if !File.exist?script puts "Error: script #{script} not found" success = false break end foundVersion = false File.open(script, "r").each_line do |line| scan_res = line.scan(/v(\d+\.\d+): Adapted.+_Report\.py/) if scan_res.length > 0 puts "Info: script #{script} has version \"#{scan_res[0][0]}\"" foundVersion = true break end end puts "Info: script #{script} not adapted" if foundVersion == false end end ###### STEP 4b: REPORT SUR (OPTIONAL) ###### if success and !@options.qacstep.nil? and @options.qacstep.include?("report") FileUtils::mkdir_p @options.qacdata puts "bakeqac: generating SUR report..." cmd = qaExe + ["report", "-P", @options.qacdata, "-t", "SUR"] success, consoleOutput = ProcessHelper.run(cmd, false) puts consoleOutput end ###### STEP 4c: REPORT RCR (OPTIONAL) ###### if success and !@options.qacstep.nil? and @options.qacstep.include?("report") FileUtils::mkdir_p @options.qacdata puts "bakeqac: generating RCR report..." cmd = qaExe + ["report", "-P", @options.qacdata, "-t", "RCR"] success, consoleOutput = ProcessHelper.run(cmd, false) puts consoleOutput end ###### STEP 5: REPORT MDR (OPTIONAL) ###### if success and !@options.qacstep.nil? and @options.qacstep.include?("mdr") FileUtils::mkdir_p @options.qacdata puts "bakeqac: generating MDR report..." cmd = qaExe + ["report", "-t", "MDR"] cmd << "-P" # << @options.qacdata success, consoleOutput, checkError = executeQacli(cmd) puts consoleOutput success = true if consoleOutput.include?"The specified project does not have any source files" if success require "json" jsons = Dir.glob(@options.qacdata + "/prqa/reports/data/*.json") maxComplexity = 0 numGreaterAllowed = 0 jsons.each do |file| raw = File.read(file) data = JSON.parse(raw) filename = File.normalize(data["file"]) filename = @options.qacdata + "/../../" + filename unless File.is_absolute?(filename) # only needed for UT fileContent = nil fileFunctions = [] if ProjectFilter.localFile(filename) Bake.formatter.printAdditionalInfo(filename) entities = data["entities"] if Array === entities entities.each do |e| if e.has_key?("type") && e["type"] == "function" if e.has_key?("metrics") && e["metrics"].has_key?("STCYC") complexity = e["metrics"]["STCYC"].to_i line = e["line"].to_i allowed = 10 if complexity > 10 && File.exist?(filename) if fileContent.nil? fileContent = File.readlines(filename) fileFunctions = getLineNumbersOfFunctions(entities) end lineOfLastFunction = 0 pos = fileFunctions.find_index(line) if !pos.nil? and fileContent.length >= line fileFunctions.each { |x| lineOfLastFunction = x if x > lineOfLastFunction && x < fileFunctions[pos] } # "line" received from json count from 1... # ... so adjustment of lineOfLastFunction for array access not needed # line-2 = index of array for line above the current function (line-2).downto(lineOfLastFunction) do |i| res = fileContent[i].scan(/METRIC\s+STCYC\s+(\d+)/) if (res.length > 0) allowed = res[0][0].to_i if res[0][0].to_i > 10 end end end end str = " #{e["name"]}:#{line}: cyclomatic complexity = #{complexity}" if complexity > allowed str = str + " (warning: accepted = #{allowed})" elsif allowed > 10 str = str + " (info: accepted = #{allowed})" end complexity > allowed ? Bake.formatter.printWarning(str) : puts(str) maxComplexity = complexity if complexity > maxComplexity numGreaterAllowed +=1 if complexity > allowed end end end end end end Bake.formatter.printInfo("\n**** Maximum cyclomatic complexity: #{maxComplexity} ****") resultStr = "**** Number of functions with cyclomatic complexity more than accepted: #{numGreaterAllowed} ****" numGreaterAllowed > 0 ? Bake.formatter.printWarning(resultStr) : Bake.formatter.printSuccess(resultStr) else Bake.formatter.printError("Failed to generate MDR report.") end end #### TODO: unittest for report ###### DONE ###### exit(success ? 0 : 1) end