| // 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.coverageoutputgenerator; |
| |
| import static com.google.devtools.coverageoutputgenerator.Constants.BA_MARKER; |
| import static com.google.devtools.coverageoutputgenerator.Constants.BRDA_MARKER; |
| import static com.google.devtools.coverageoutputgenerator.Constants.BRF_MARKER; |
| import static com.google.devtools.coverageoutputgenerator.Constants.BRH_MARKER; |
| import static com.google.devtools.coverageoutputgenerator.Constants.DA_MARKER; |
| import static com.google.devtools.coverageoutputgenerator.Constants.DELIMITER; |
| import static com.google.devtools.coverageoutputgenerator.Constants.END_OF_RECORD_MARKER; |
| import static com.google.devtools.coverageoutputgenerator.Constants.FNDA_MARKER; |
| import static com.google.devtools.coverageoutputgenerator.Constants.FNF_MARKER; |
| import static com.google.devtools.coverageoutputgenerator.Constants.FNH_MARKER; |
| import static com.google.devtools.coverageoutputgenerator.Constants.FN_MARKER; |
| import static com.google.devtools.coverageoutputgenerator.Constants.LF_MARKER; |
| import static com.google.devtools.coverageoutputgenerator.Constants.LH_MARKER; |
| import static com.google.devtools.coverageoutputgenerator.Constants.SF_MARKER; |
| import static com.google.devtools.coverageoutputgenerator.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 <a |
| * href="http://ltp.sourceforge.net/coverage/lcov/geninfo.1.php">lcov documentation</a> |
| */ |
| 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<SourceFileCoverage> 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:<source file> line and an end_of_record line. |
| * |
| * @return a list of each source file path found in the tracefile |
| */ |
| private List<SourceFileCoverage> parse() throws IOException { |
| List<SourceFileCoverage> 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<SourceFileCoverage> 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<SourceFileCoverage> 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:<path to source file name> |
| private boolean parseSFLine(String line) { |
| if (currentSourceFileCoverage != null) { |
| logger.log(Level.WARNING, "Tracefile doesn't have SF:<source file> 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:<line number of function start>,<function name> |
| 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:<execution count>,<function name> |
| 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:<number of functions found> |
| 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:<number of function hit> |
| 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:<line number>,<taken> |
| 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:<line number>,<block number>,<branch number>,<taken> |
| 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:<number of branches found> |
| 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:<number of branches hit> |
| 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:<line number>,<execution count>,[,<checksum>] |
| 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:<nr of lines with non-zero exec count> |
| 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:<number of instrumented lines> |
| 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; |
| } |
| } |