#!/usr/bin/env ruby require 'fileutils' require 'set' $:.unshift(File.dirname(__FILE__)+"/../lib") require_relative "../lib/bakeqac/options/options" require_relative "../lib/bakeqac/filter" require_relative '../lib/bake/toolchain/colorizing_formatter' require_relative '../lib/bake/options/options' require_relative '../lib/common/process' require_relative '../lib/common/utils' require_relative '../lib/common/ext/file' STDOUT.sync = true STDERR.sync = true module Bake @@cipFileSize = 1 @@unsuppressibleErrorsList = [ "0019", "0020", "0031", "0034", "0035", "0036", "0061", "0098", "0099", "0224", "0622", "0800", "0801", "0802", "0996", "1950", "1951", "1962", "1963", "1964", "1965", "1966", "1967", "1968", "1969", "1970", "1971", "1972", "1973", "1974", "1975", "1976", "1977", "1978", "1979", "1980", "1982", "1984", "1985", "1986", "1987", "1989", "1990", "1991", "3104", "0014", "0015", "0016", "0021", "0022", "0028", "0032", "0039", "0043", "0045", "0046", "0050", "0052", "0053", "0054", "0058", "0059", "0079", "0083", "0101", "0133", "0150", "0165", "0170", "0174", "0175", "0177", "0200", "0201", "0208", "0217", "0231", "0246", "0248", "0251", "0253", "0255", "0257", "0271", "0276", "0278", "0279", "0282", "0284", "0285", "0286", "0287", "0307", "0313", "0320", "0321", "0322", "0323", "0333", "0334", "0335", "0336", "0339", "0400", "0401", "0403", "0404", "0405", "0415", "0424", "0429", "0439", "0440", "0441", "0445", "0448", "0450", "0453", "0454", "0455", "0456", "0457", "0458", "0459", "0469", "0481", "0482", "0483", "0484", "0491", "0492", "0493", "0494", "0496", "0497", "0498", "0600", "0601", "0602", "0607", "0608", "0611", "0616", "0617", "0619", "0620", "0621", "0627", "0631", "0632"] @@unsuppressibleErrors = Set.new() @@unsuppressibleErrorsList.each { |elem| @@unsuppressibleErrors << elem } 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 Utils.gitIgnore(@options.qacdata) cmdDupped = cmd.dup cmdDupped << @options.qacdata puts cmdDupped.join(" ") if @options.qacverbose # 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", "--qacverbose"].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 if File.exist?(@options.qacdata+"/prqa/configs/Initial/config") cct220 = "configs/Initial/" elsif File.exist?(@options.qacdata+"/prqa/configs/Initial_Config/config") cct220 = "configs/Initial_Config/" else cct220 = "" end 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 else Bake.formatter.printWarning("Could not find cct file in .qacdata folder") end else puts consoleOutput unless consoleOutput.empty? end end ###### STEP 2: BUILD ###### if success and (@options.qacstep.nil? or @options.qacstep.include?"analyze") Utils.gitIgnore(@options.qacdata) cmd = qaExe + ["analyze", "-b"] begin devMode = File.exist?"e:/bake/bake/bin/bake" rescue Exception devMode = false end if devMode bcmd = "ruby e:/bake/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") Utils.gitIgnore(@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 line.include?("requires QA C++ version") filteredLines << line # either downgrade gcc or upgrade QA C++ elsif 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 = [] suppressLine = false 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(") || line.include?(": Err("))) begin scanResult = line.scan(/([^\(\s]+)\s*\((\d+)[^\)]*\):\s*(Err|)\([^:]*:(\d+)\)/) if scanResult.length > 0 fileName = scanResult[0][0].gsub("\\", "/") lineNum = scanResult[0][1].to_i errorNum = scanResult[0][3] if (@@unsuppressibleErrors.include?errorNum) srcLine = File.readlines(fileName)[lineNum-1] if srcLine.match(Regexp.new("PRQA\s+S.+#{errorNum}")) suppressLine = true end end end rescue Exception =>ex if Bake.options.debug puts ex.message puts ex.backtrace end end elsif (!@options.qacnoformat && line.start_with?("trc: ")) || (@options.qacnoformat && line.include?("): -->(")) # nothing to be done here else suppressLine = false end if (!suppressLine) if linesOfFile.has_key?lineNum currentMessage = linesOfFile[lineNum] else currentMessage = [] linesOfFile[lineNum] = currentMessage end currentMessage << line end end end linesOfFile.sort.each { |lineNr, s| sortedLines += s } # 3. print numberOfMessages = 0 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(") || line.include?(": Err("))) Bake.formatter.printWarning(line) if @options.qacdoc errorNumScan = line.scan(/[^\(]*\([^\(]*\([^:]*:(\d*)/) if errorNumScan.length > 0 errorNum = errorNumScan[0][0].to_i Dir.chdir(@options.qac_home) do htmlFiles = Dir.glob("components/qacpp*/doc-en_US/messages/%04d.html" % errorNum).each do |htmlFile| puts "doc: #{File.expand_path(htmlFile)}" end end Dir.chdir(@options.mcpp_home) do htmlFiles = Dir.glob("components/mcpp*/doc-en_US/messages/%04d.html" % errorNum).each do |htmlFile| puts "doc: #{File.expand_path(htmlFile)}" end 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 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") Utils.gitIgnore(@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") Utils.gitIgnore(@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") Utils.gitIgnore(@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" cct220 = (File.exist?(@options.qacdata+"/prqa/configs/Initial_Config/reports") ? "configs/Initial_Config/" : "") jsons = Dir.glob(@options.qacdata + "/prqa/" + cct220 + "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| # skip invalid character, otherwise exception might occur fileContent[i].encode!('UTF-8', :invalid => :replace, :undef => :replace, :replace => '') 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