// Copyright 2018 The Bazel Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package com.google.devtools.lcovmerger; import static com.google.devtools.lcovmerger.Constants.BA_MARKER; import static com.google.devtools.lcovmerger.Constants.BRDA_MARKER; import static com.google.devtools.lcovmerger.Constants.BRF_MARKER; import static com.google.devtools.lcovmerger.Constants.BRH_MARKER; import static com.google.devtools.lcovmerger.Constants.DA_MARKER; import static com.google.devtools.lcovmerger.Constants.DELIMITER; import static com.google.devtools.lcovmerger.Constants.END_OF_RECORD_MARKER; import static com.google.devtools.lcovmerger.Constants.FNDA_MARKER; import static com.google.devtools.lcovmerger.Constants.FNF_MARKER; import static com.google.devtools.lcovmerger.Constants.FNH_MARKER; import static com.google.devtools.lcovmerger.Constants.FN_MARKER; import static com.google.devtools.lcovmerger.Constants.LF_MARKER; import static com.google.devtools.lcovmerger.Constants.LH_MARKER; import static com.google.devtools.lcovmerger.Constants.SF_MARKER; import static com.google.devtools.lcovmerger.Constants.TAKEN; import static java.nio.charset.StandardCharsets.UTF_8; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; /** * A parser for the lcov tracefile format used by geninfo. See * lcov documentation */ class LcovParser { private static final Logger logger = Logger.getLogger(LcovParser.class.getName()); private final InputStream inputStream; private SourceFileCoverage currentSourceFileCoverage; private LcovParser(InputStream inputStream) { this.inputStream = inputStream; } public static List parse(InputStream inputStream) throws IOException { return new LcovParser(inputStream).parse(); } /** * Reads the tracefile line by line and creates a SourceFileCoverage object * for each section of the file between a SF: line and an * end_of_record line. * * @return a list of each source file path found in the tracefile */ private List parse() throws IOException { List allSourceFiles = new ArrayList<>(); try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, UTF_8))) { String line; while ((line = bufferedReader.readLine()) != null) { parseLine(line, allSourceFiles); } bufferedReader.close(); } return allSourceFiles; } /** * Merges {@code currentSourceFileCoverage} into {@code allSourceFilesCoverageData} and resets * {@code currentSourceFileCoverage} to null. */ private void reset(List allSourceFiles) { allSourceFiles.add(currentSourceFileCoverage); currentSourceFileCoverage = null; } /* * Reads the line and redirects the parsing to the corresponding {@code parseXLine} method. Every * {@code parseXLine} methods fills in data to {@code currentSourceFileCoverage} accordingly. */ private boolean parseLine(String line, List allSourceFiles) { if (line.startsWith(SF_MARKER)) { return parseSFLine(line); } // currentSourceFileCoverage should be null only before calling an SF line, otherwise // the object should have been created in parseSFLine. If currentSourceFileCoverage is null // here it means the parser arrived in an invalid state. if (currentSourceFileCoverage == null) { return false; } if (line.startsWith(FN_MARKER)) { return parseFNLine(line); } if (line.startsWith(FNDA_MARKER)) { return parseFNDALine(line); } if (line.startsWith(FNF_MARKER)) { return parseFNFLine(line); } if (line.startsWith(FNH_MARKER)) { return parseFNHLine(line); } if (line.startsWith(BRDA_MARKER)) { return parseBRDALine(line); } if (line.startsWith(BA_MARKER)) { return parseBALine(line); } if (line.startsWith(BRF_MARKER)) { return parseBRFLine(line); } if (line.startsWith(BRH_MARKER)) { return parseBRHLine(line); } if (line.startsWith(DA_MARKER)) { return parseDALine(line); } if (line.startsWith(LH_MARKER)) { return parseLHLine(line); } if (line.startsWith(LF_MARKER)) { return parseLFLine(line); } if (line.equals(END_OF_RECORD_MARKER)) { reset(allSourceFiles); return true; } logger.log(Level.WARNING, "Tracefile includes invalid line: " + line); return false; } // SF: private boolean parseSFLine(String line) { if (currentSourceFileCoverage != null) { logger.log(Level.WARNING, "Tracefile doesn't have SF: line before" + line); return false; } String sourcefile = line.substring(SF_MARKER.length()); if (sourcefile.isEmpty()) { logger.log(Level.WARNING, "Tracefile doesn't contain source file name on line: " + line); return false; } currentSourceFileCoverage = new SourceFileCoverage(sourcefile); return true; } // FN:, private boolean parseFNLine(String line) { String lineContent = line.substring(FN_MARKER.length()); String[] funcData = lineContent.split(DELIMITER, -1); if (funcData.length != 2 || funcData[0].isEmpty() || funcData[1].isEmpty()) { logger.log(Level.WARNING, "Tracefile contains invalid FN line " + line); return false; } try { int lineNrFunctionStart = Integer.parseInt(funcData[0]); String functionName = funcData[1]; currentSourceFileCoverage.addLineNumber(functionName, lineNrFunctionStart); } catch (NumberFormatException e) { logger.log(Level.WARNING, "Tracefile contains invalid line number on FN line " + line); return false; } return true; } // FNDA:, private boolean parseFNDALine(String line) { String lineContent = line.substring(FNDA_MARKER.length()); String[] funcData = lineContent.split(DELIMITER, -1); if (funcData.length != 2 || funcData[0].isEmpty() || funcData[1].isEmpty()) { logger.log(Level.WARNING, "Tracefile contains invalid FNDA line " + line); return false; } try { int executionCount = Integer.parseInt(funcData[0]); String functionName = funcData[1]; currentSourceFileCoverage.addFunctionExecution(functionName, executionCount); } catch (NumberFormatException e) { logger.log(Level.WARNING, "Tracefile contains invalid execution count on FN line " + line); return false; } return true; } // FNF: private boolean parseFNFLine(String line) { String lineContent = line.substring(FNF_MARKER.length()); if (lineContent.isEmpty()) { logger.log(Level.WARNING, "Tracefile contains invalid FNF line " + line); return false; } try { int nrFunctionsFound = Integer.parseInt(lineContent); assert currentSourceFileCoverage.nrFunctionsFound() == nrFunctionsFound; } catch (NumberFormatException e) { logger.log(Level.WARNING, "Tracefile contains invalid number of functions on FNF line " + line); return false; } return true; } // FNH: private boolean parseFNHLine(String line) { String lineContent = line.substring(FNH_MARKER.length()); if (lineContent.isEmpty()) { logger.log(Level.WARNING, "Tracefile contains invalid FNH line " + line); return false; } try { int nrFunctionsHit = Integer.parseInt(lineContent); assert currentSourceFileCoverage.nrFunctionsHit() == nrFunctionsHit; } catch (NumberFormatException e) { logger.log(Level.WARNING, "Tracefile contains invalid number of functions hit on FNH line " + line); return false; } return true; } // BA:, private boolean parseBALine(String line) { String lineContent = line.substring(BA_MARKER.length()); String[] lineData = lineContent.split(DELIMITER, -1); if (lineData.length != 2) { logger.log(Level.WARNING, "Tracefile contains invalid BRDA line " + line); return false; } for (String data : lineData) { if (data.isEmpty()) { logger.log(Level.WARNING, "Tracefile contains invalid BRDA line " + line); return false; } } try { int lineNumber = Integer.parseInt(lineData[0]); int taken = Integer.parseInt(lineData[1]); BranchCoverage branchCoverage = BranchCoverage.create(lineNumber, taken); currentSourceFileCoverage.addBranch(lineNumber, branchCoverage); } catch (NumberFormatException e) { logger.log(Level.WARNING, "Tracefile contains an invalid number BA line " + line); return false; } return true; } // BRDA:,,, private boolean parseBRDALine(String line) { String lineContent = line.substring(BRDA_MARKER.length()); String[] lineData = lineContent.split(DELIMITER, -1); if (lineData.length != 4) { logger.log(Level.WARNING, "Tracefile contains invalid BRDA line " + line); return false; } for (String data : lineData) { if (data.isEmpty()) { logger.log(Level.WARNING, "Tracefile contains invalid BRDA line " + line); return false; } } try { int lineNumber = Integer.parseInt(lineData[0]); String blockNumber = lineData[1]; String branchNumber = lineData[2]; String taken = lineData[3]; boolean wasExecuted = false; int executionCount = 0; if (taken.equals(TAKEN)) { executionCount = Integer.parseInt(taken); wasExecuted = true; } BranchCoverage branchCoverage = BranchCoverage.createWithBlockAndBranch( lineNumber, blockNumber, branchNumber, executionCount); currentSourceFileCoverage.addBranch(lineNumber, branchCoverage); } catch (NumberFormatException e) { logger.log(Level.WARNING, "Tracefile contains an invalid number BRDA line " + line); return false; } return true; } // BRF: private boolean parseBRFLine(String line) { String lineContent = line.substring(BRF_MARKER.length()); if (lineContent.isEmpty()) { logger.log(Level.WARNING, "Tracefile contains invalid BRF line " + line); return false; } try { int nrBranchesFound = Integer.parseInt(lineContent); assert currentSourceFileCoverage.nrBranchesFound() == nrBranchesFound; } catch (NumberFormatException e) { logger.log(Level.WARNING, "Tracefile contains invalid number of branches in BRDA line " + line); return false; } return true; } // BRH: private boolean parseBRHLine(String line) { String lineContent = line.substring(BRH_MARKER.length()); if (lineContent.isEmpty()) { logger.log(Level.WARNING, "Tracefile contains invalid BRH line " + line); return false; } try { int nrBranchesHit = Integer.parseInt(lineContent); assert currentSourceFileCoverage.nrBranchesHit() == nrBranchesHit; } catch (NumberFormatException e) { logger.log(Level.WARNING, "Tracefile contains invalid number of branches hit in BRH line " + line); return false; } return true; } // DA:,,[,] private boolean parseDALine(String line) { String lineContent = line.substring(DA_MARKER.length()); String[] lineData = lineContent.split(DELIMITER, -1); if (lineData.length != 2 && lineData.length != 3) { logger.log(Level.WARNING, "Tracefile contains invalid DA line " + line); return false; } for (String data : lineData) { if (data.isEmpty()) { logger.log(Level.WARNING, "Tracefile contains invalid DA line " + line); return false; } } try { int lineNumber = Integer.parseInt(lineData[0]); int executionCount = Integer.parseInt(lineData[1]); String checkSum = null; if (lineData.length == 3) { checkSum = lineData[2]; } LineCoverage lineCoverage = LineCoverage.create(lineNumber, executionCount, checkSum); currentSourceFileCoverage.addLine( lineNumber, lineCoverage); } catch (NumberFormatException e) { logger.log(Level.WARNING, "Tracefile contains an invalid number on DA line " + line); return false; } return true; } // LH: private boolean parseLHLine(String line) { String lineContent = line.substring(LH_MARKER.length()); if (lineContent.isEmpty()) { logger.log(Level.WARNING, "Tracefile contains invalid LHL line " + line); return false; } try { int nrLines = Integer.parseInt(lineContent); assert currentSourceFileCoverage.nrOfLinesWithNonZeroExecution() == nrLines; } catch (NumberFormatException e) { logger.log(Level.WARNING, "Tracefile contains an invalid number on LHL line " + line); return false; } return true; } // LF: private boolean parseLFLine(String line) { String lineContent = line.substring(LF_MARKER.length()); if (lineContent.isEmpty()) { logger.log(Level.WARNING, "Tracefile contains invalid LF line " + line); return false; } try { int nrLines = Integer.parseInt(lineContent); assert currentSourceFileCoverage.nrOfInstrumentedLines() == nrLines; } catch (NumberFormatException e) { logger.log(Level.WARNING, "Tracefile contains an invalid number on LF line " + line); return false; } return true; } }