diff options
author | Yuhao Gu <yhgu2000@outlook.com> | 2023-08-24 13:12:04 +0800 |
---|---|---|
committer | Yuhao Gu <yhgu2000@outlook.com> | 2023-08-24 13:46:12 +0800 |
commit | bea39c5443612b638aa1cc56d36e3b1fd06f6e96 (patch) | |
tree | 63bdf728f2bd4c184f7bbf3b9239e53945815829 /llvm/tools/llvm-cov | |
parent | 7a41af86041bd757b7f380d7f645403d4e1725ca (diff) | |
download | llvm-bea39c5443612b638aa1cc56d36e3b1fd06f6e96.zip llvm-bea39c5443612b638aa1cc56d36e3b1fd06f6e96.tar.gz llvm-bea39c5443612b638aa1cc56d36e3b1fd06f6e96.tar.bz2 |
[llvm-cov] Support directory layout in coverage reports
This is a GSoC 2023 project ([discourse link](https://discourse.llvm.org/t/coverage-support-a-hierarchical-directory-structure-in-generated-coverage-html-reports/68239)).
llvm-cov currently generates a single top-level index HTML file, which causes rendering scalability issues in large projects. This patch adds support for hierarchical directory structure into the HTML reports to solve scalability issues by introducing the following changes:
- Added a new command line option `--show-directory-coverage` for `llvm-cov show`. It works both for `--format=html` and `--format=text`.
- Two new classes: `CoveragePrinterHTMLDirectory` and `CoveragePrinterTextDirectory` was added to support the new option.
- A tool class `DirectoryCoverageReport` was added to support the two classes above.
- Updated the document.
- Added a new regression test for `--show-directory-coverage`.
Reviewed By: phosek, gulfem
Differential Revision: https://reviews.llvm.org/D151283
Diffstat (limited to 'llvm/tools/llvm-cov')
-rw-r--r-- | llvm/tools/llvm-cov/CodeCoverage.cpp | 5 | ||||
-rw-r--r-- | llvm/tools/llvm-cov/CoverageReport.cpp | 145 | ||||
-rw-r--r-- | llvm/tools/llvm-cov/CoverageReport.h | 70 | ||||
-rw-r--r-- | llvm/tools/llvm-cov/CoverageSummaryInfo.h | 1 | ||||
-rw-r--r-- | llvm/tools/llvm-cov/CoverageViewOptions.h | 1 | ||||
-rw-r--r-- | llvm/tools/llvm-cov/SourceCoverageView.cpp | 4 | ||||
-rw-r--r-- | llvm/tools/llvm-cov/SourceCoverageView.h | 2 | ||||
-rw-r--r-- | llvm/tools/llvm-cov/SourceCoverageViewHTML.cpp | 391 | ||||
-rw-r--r-- | llvm/tools/llvm-cov/SourceCoverageViewHTML.h | 20 | ||||
-rw-r--r-- | llvm/tools/llvm-cov/SourceCoverageViewText.cpp | 65 | ||||
-rw-r--r-- | llvm/tools/llvm-cov/SourceCoverageViewText.h | 15 | ||||
-rw-r--r-- | llvm/tools/llvm-cov/TestingSupport.cpp | 5 |
12 files changed, 636 insertions, 88 deletions
diff --git a/llvm/tools/llvm-cov/CodeCoverage.cpp b/llvm/tools/llvm-cov/CodeCoverage.cpp index 3992dfa..f26db15 100644 --- a/llvm/tools/llvm-cov/CodeCoverage.cpp +++ b/llvm/tools/llvm-cov/CodeCoverage.cpp @@ -982,6 +982,10 @@ int CodeCoverageTool::doShow(int argc, const char **argv, cl::desc("Show function instantiations"), cl::init(true), cl::cat(ViewCategory)); + cl::opt<bool> ShowDirectoryCoverage("show-directory-coverage", cl::Optional, + cl::desc("Show directory coverage"), + cl::cat(ViewCategory)); + cl::opt<std::string> ShowOutputDirectory( "output-dir", cl::init(""), cl::desc("Directory in which coverage information is written out")); @@ -1062,6 +1066,7 @@ int CodeCoverageTool::doShow(int argc, const char **argv, ViewOpts.ShowBranchPercents = ShowBranches == CoverageViewOptions::BranchOutputType::Percent; ViewOpts.ShowFunctionInstantiations = ShowInstantiations; + ViewOpts.ShowDirectoryCoverage = ShowDirectoryCoverage; ViewOpts.ShowOutputDirectory = ShowOutputDirectory; ViewOpts.TabSize = TabSize; ViewOpts.ProjectTitle = ProjectTitle; diff --git a/llvm/tools/llvm-cov/CoverageReport.cpp b/llvm/tools/llvm-cov/CoverageReport.cpp index cb0b184e..518f6cd 100644 --- a/llvm/tools/llvm-cov/CoverageReport.cpp +++ b/llvm/tools/llvm-cov/CoverageReport.cpp @@ -166,6 +166,35 @@ unsigned getRedundantPrefixLen(ArrayRef<std::string> Paths) { return PrefixLen; } +/// Determine the length of the longest redundant prefix of the substrs starts +/// from \p LCP in \p Paths. \p Paths can't be empty. If there's only one +/// element in \p Paths, the length of the substr is returned. Note this is +/// differnet from the behavior of the function above. +unsigned getRedundantPrefixLen(ArrayRef<StringRef> Paths, unsigned LCP) { + assert(!Paths.empty() && "Paths must have at least one element"); + + auto Iter = Paths.begin(); + auto IterE = Paths.end(); + auto Prefix = Iter->substr(LCP); + while (++Iter != IterE) { + auto Other = Iter->substr(LCP); + auto Len = std::min(Prefix.size(), Other.size()); + for (std::size_t I = 0; I < Len; ++I) { + if (Prefix[I] != Other[I]) { + Prefix = Prefix.substr(0, I); + break; + } + } + } + + for (auto I = Prefix.size(); --I != SIZE_MAX;) { + if (Prefix[I] == '/' || Prefix[I] == '\\') + return I + 1; + } + + return Prefix.size(); +} + } // end anonymous namespace namespace llvm { @@ -180,8 +209,14 @@ void CoverageReport::render(const FileCoverageSummary &File, determineCoveragePercentageColor(File.InstantiationCoverage); auto LineCoverageColor = determineCoveragePercentageColor(File.LineCoverage); SmallString<256> FileName = File.Name; - sys::path::remove_dots(FileName, /*remove_dot_dot=*/true); sys::path::native(FileName); + + // remove_dots will remove trailing slash, so we need to check before it. + auto IsDir = FileName.endswith(sys::path::get_separator()); + sys::path::remove_dots(FileName, /*remove_dot_dot=*/true); + if (IsDir) + FileName += sys::path::get_separator(); + OS << column(FileName, FileReportColumns[0], Column::NoTrim); if (Options.ShowRegionSummary) { @@ -436,7 +471,12 @@ void CoverageReport::renderFileReports( FileCoverageSummary Totals("TOTAL"); auto FileReports = prepareFileReports(Coverage, Totals, Files, Options, Filters); + renderFileReports(OS, FileReports, Totals, Filters.empty()); +} +void CoverageReport::renderFileReports( + raw_ostream &OS, const std::vector<FileCoverageSummary> &FileReports, + const FileCoverageSummary &Totals, bool ShowEmptyFiles) const { std::vector<StringRef> Filenames; Filenames.reserve(FileReports.size()); for (const FileCoverageSummary &FCS : FileReports) @@ -467,21 +507,20 @@ void CoverageReport::renderFileReports( renderDivider(FileReportColumns, OS); OS << "\n"; - bool EmptyFiles = false; + std::vector<const FileCoverageSummary *> EmptyFiles; for (const FileCoverageSummary &FCS : FileReports) { if (FCS.FunctionCoverage.getNumFunctions()) render(FCS, OS); else - EmptyFiles = true; + EmptyFiles.push_back(&FCS); } - if (EmptyFiles && Filters.empty()) { + if (!EmptyFiles.empty() && ShowEmptyFiles) { OS << "\n" << "Files which contain no functions:\n"; - for (const FileCoverageSummary &FCS : FileReports) - if (!FCS.FunctionCoverage.getNumFunctions()) - render(FCS, OS); + for (auto FCS : EmptyFiles) + render(*FCS, OS); } renderDivider(FileReportColumns, OS); @@ -489,4 +528,96 @@ void CoverageReport::renderFileReports( render(Totals, OS); } +Expected<FileCoverageSummary> DirectoryCoverageReport::prepareDirectoryReports( + ArrayRef<std::string> SourceFiles) { + std::vector<StringRef> Files(SourceFiles.begin(), SourceFiles.end()); + + unsigned RootLCP = getRedundantPrefixLen(Files, 0); + auto LCPath = Files.front().substr(0, RootLCP); + + ThreadPoolStrategy PoolS = hardware_concurrency(Options.NumThreads); + if (Options.NumThreads == 0) { + PoolS = heavyweight_hardware_concurrency(Files.size()); + PoolS.Limit = true; + } + ThreadPool Pool(PoolS); + + TPool = &Pool; + LCPStack = {RootLCP}; + FileCoverageSummary RootTotals(LCPath); + if (auto E = prepareSubDirectoryReports(Files, &RootTotals)) + return {std::move(E)}; + return {std::move(RootTotals)}; +} + +/// Filter out files in LCPStack.back(), group others by subdirectory name +/// and recurse on them. After returning from all subdirectories, call +/// generateSubDirectoryReport(). \p Files must be non-empty. The +/// FileCoverageSummary of this directory will be added to \p Totals. +Error DirectoryCoverageReport::prepareSubDirectoryReports( + const ArrayRef<StringRef> &Files, FileCoverageSummary *Totals) { + assert(!Files.empty() && "Files must have at least one element"); + + auto LCP = LCPStack.back(); + auto LCPath = Files.front().substr(0, LCP).str(); + + // Use ordered map to keep entries in order. + SubFileReports SubFiles; + SubDirReports SubDirs; + for (auto &&File : Files) { + auto SubPath = File.substr(LCPath.size()); + SmallVector<char, 128> NativeSubPath; + sys::path::native(SubPath, NativeSubPath); + StringRef NativeSubPathRef(NativeSubPath.data(), NativeSubPath.size()); + + auto I = sys::path::begin(NativeSubPathRef); + auto E = sys::path::end(NativeSubPathRef); + assert(I != E && "Such case should have been filtered out in the caller"); + + auto Name = SubPath.substr(0, I->size()); + if (++I == E) { + auto Iter = SubFiles.insert_or_assign(Name, SubPath).first; + // Makes files reporting overlap with subdir reporting. + TPool->async(&CoverageReport::prepareSingleFileReport, File, &Coverage, + Options, LCP, &Iter->second, &Filters); + } else { + SubDirs[Name].second.push_back(File); + } + } + + // Call recursively on subdirectories. + for (auto &&KV : SubDirs) { + auto &V = KV.second; + if (V.second.size() == 1) { + // If there's only one file in that subdirectory, we don't bother to + // recurse on it further. + V.first.Name = V.second.front().substr(LCP); + TPool->async(&CoverageReport::prepareSingleFileReport, V.second.front(), + &Coverage, Options, LCP, &V.first, &Filters); + } else { + auto SubDirLCP = getRedundantPrefixLen(V.second, LCP); + V.first.Name = V.second.front().substr(LCP, SubDirLCP); + LCPStack.push_back(LCP + SubDirLCP); + if (auto E = prepareSubDirectoryReports(V.second, &V.first)) + return E; + } + } + + TPool->wait(); + + FileCoverageSummary CurrentTotals(LCPath); + for (auto &&KV : SubFiles) + CurrentTotals += KV.second; + for (auto &&KV : SubDirs) + CurrentTotals += KV.second.first; + *Totals += CurrentTotals; + + if (auto E = generateSubDirectoryReport( + std::move(SubFiles), std::move(SubDirs), std::move(CurrentTotals))) + return E; + + LCPStack.pop_back(); + return Error::success(); +} + } // end namespace llvm diff --git a/llvm/tools/llvm-cov/CoverageReport.h b/llvm/tools/llvm-cov/CoverageReport.h index f9a092f..60f751c 100644 --- a/llvm/tools/llvm-cov/CoverageReport.h +++ b/llvm/tools/llvm-cov/CoverageReport.h @@ -16,9 +16,12 @@ #include "CoverageFilters.h" #include "CoverageSummaryInfo.h" #include "CoverageViewOptions.h" +#include <map> namespace llvm { +class ThreadPool; + /// Displays the code coverage report. class CoverageReport { const CoverageViewOptions &Options; @@ -62,6 +65,73 @@ public: /// in \p Filters. void renderFileReports(raw_ostream &OS, ArrayRef<std::string> Files, const CoverageFiltersMatchAll &Filters) const; + + /// Render file reports with given data. + void renderFileReports(raw_ostream &OS, + const std::vector<FileCoverageSummary> &FileReports, + const FileCoverageSummary &Totals, + bool ShowEmptyFiles) const; +}; + +/// Prepare reports for every non-trivial directories (which have more than 1 +/// source files) of the source files. This class uses template method pattern. +class DirectoryCoverageReport { +public: + DirectoryCoverageReport( + const CoverageViewOptions &Options, + const coverage::CoverageMapping &Coverage, + const CoverageFiltersMatchAll &Filters = CoverageFiltersMatchAll()) + : Options(Options), Coverage(Coverage), Filters(Filters) {} + + virtual ~DirectoryCoverageReport() = default; + + /// Prepare file reports for each directory in \p SourceFiles. The total + /// report for all files is returned and its Name is set to the LCP of all + /// files. The size of \p SourceFiles must be greater than 1 or else the + /// behavior is undefined, in which case you should use + /// CoverageReport::prepareSingleFileReport instead. If an error occurs, + /// the recursion will stop immediately. + Expected<FileCoverageSummary> + prepareDirectoryReports(ArrayRef<std::string> SourceFiles); + +protected: + // These member variables below are used for avoiding being passed + // repeatedly in recursion. + const CoverageViewOptions &Options; + const coverage::CoverageMapping &Coverage; + const CoverageFiltersMatchAll &Filters; + + /// For calling CoverageReport::prepareSingleFileReport asynchronously + /// in prepareSubDirectoryReports(). It's not intended to be modified by + /// generateSubDirectoryReport(). + ThreadPool *TPool; + + /// One report level may correspond to multiple directory levels as we omit + /// directories which have only one subentry. So we use this Stack to track + /// each report level's corresponding drectory level. + /// Each value in the stack is the LCP prefix length length of that report + /// level. LCPStack.front() is the root LCP. Current LCP is LCPStack.back(). + SmallVector<unsigned, 32> LCPStack; + + // Use std::map to sort table rows in order. + using SubFileReports = std::map<StringRef, FileCoverageSummary>; + using SubDirReports = + std::map<StringRef, + std::pair<FileCoverageSummary, SmallVector<StringRef, 0>>>; + + /// This method is called when a report level is prepared during the + /// recursion. \p SubFiles are the reports for those files directly in the + /// current directory. \p SubDirs are the reports for subdirectories in + /// current directory. \p SubTotals is the sum of all, and its name is the + /// current LCP. Note that this method won't be called for trivial + /// directories. + virtual Error generateSubDirectoryReport(SubFileReports &&SubFiles, + SubDirReports &&SubDirs, + FileCoverageSummary &&SubTotals) = 0; + +private: + Error prepareSubDirectoryReports(const ArrayRef<StringRef> &Files, + FileCoverageSummary *Totals); }; } // end namespace llvm diff --git a/llvm/tools/llvm-cov/CoverageSummaryInfo.h b/llvm/tools/llvm-cov/CoverageSummaryInfo.h index 84a3228..46510dc 100644 --- a/llvm/tools/llvm-cov/CoverageSummaryInfo.h +++ b/llvm/tools/llvm-cov/CoverageSummaryInfo.h @@ -222,6 +222,7 @@ struct FileCoverageSummary { FunctionCoverageInfo FunctionCoverage; FunctionCoverageInfo InstantiationCoverage; + FileCoverageSummary() = default; FileCoverageSummary(StringRef Name) : Name(Name) {} FileCoverageSummary &operator+=(const FileCoverageSummary &RHS) { diff --git a/llvm/tools/llvm-cov/CoverageViewOptions.h b/llvm/tools/llvm-cov/CoverageViewOptions.h index fedf2df..eb852859 100644 --- a/llvm/tools/llvm-cov/CoverageViewOptions.h +++ b/llvm/tools/llvm-cov/CoverageViewOptions.h @@ -38,6 +38,7 @@ struct CoverageViewOptions { bool ShowBranchSummary; bool ShowRegionSummary; bool ShowInstantiationSummary; + bool ShowDirectoryCoverage; bool ExportSummaryOnly; bool SkipExpansions; bool SkipFunctions; diff --git a/llvm/tools/llvm-cov/SourceCoverageView.cpp b/llvm/tools/llvm-cov/SourceCoverageView.cpp index ea86aca..7480b7f 100644 --- a/llvm/tools/llvm-cov/SourceCoverageView.cpp +++ b/llvm/tools/llvm-cov/SourceCoverageView.cpp @@ -76,8 +76,12 @@ std::unique_ptr<CoveragePrinter> CoveragePrinter::create(const CoverageViewOptions &Opts) { switch (Opts.Format) { case CoverageViewOptions::OutputFormat::Text: + if (Opts.ShowDirectoryCoverage) + return std::make_unique<CoveragePrinterTextDirectory>(Opts); return std::make_unique<CoveragePrinterText>(Opts); case CoverageViewOptions::OutputFormat::HTML: + if (Opts.ShowDirectoryCoverage) + return std::make_unique<CoveragePrinterHTMLDirectory>(Opts); return std::make_unique<CoveragePrinterHTML>(Opts); case CoverageViewOptions::OutputFormat::Lcov: // Unreachable because CodeCoverage.cpp should terminate with an error diff --git a/llvm/tools/llvm-cov/SourceCoverageView.h b/llvm/tools/llvm-cov/SourceCoverageView.h index 5a9fcdd..c07595f 100644 --- a/llvm/tools/llvm-cov/SourceCoverageView.h +++ b/llvm/tools/llvm-cov/SourceCoverageView.h @@ -99,7 +99,7 @@ protected: CoveragePrinter(const CoverageViewOptions &Opts) : Opts(Opts) {} /// Return `OutputDir/ToplevelDir/Path.Extension`. If \p InToplevel is - /// false, skip the ToplevelDir component. If \p Relative is false, skip the + /// true, skip the ToplevelDir component. If \p Relative is true, skip the /// OutputDir component. std::string getOutputPath(StringRef Path, StringRef Extension, bool InToplevel, bool Relative = true) const; diff --git a/llvm/tools/llvm-cov/SourceCoverageViewHTML.cpp b/llvm/tools/llvm-cov/SourceCoverageViewHTML.cpp index 49f2035..79a0494 100644 --- a/llvm/tools/llvm-cov/SourceCoverageViewHTML.cpp +++ b/llvm/tools/llvm-cov/SourceCoverageViewHTML.cpp @@ -10,12 +10,13 @@ /// //===----------------------------------------------------------------------===// -#include "CoverageReport.h" #include "SourceCoverageViewHTML.h" +#include "CoverageReport.h" #include "llvm/ADT/SmallString.h" #include "llvm/ADT/StringExtras.h" #include "llvm/Support/Format.h" #include "llvm/Support/Path.h" +#include "llvm/Support/ThreadPool.h" #include <optional> using namespace llvm; @@ -49,23 +50,41 @@ std::string escape(StringRef Str, const CoverageViewOptions &Opts) { } // Create a \p Name tag around \p Str, and optionally set its \p ClassName. -std::string tag(const std::string &Name, const std::string &Str, - const std::string &ClassName = "") { - std::string Tag = "<" + Name; - if (!ClassName.empty()) - Tag += " class='" + ClassName + "'"; - return Tag + ">" + Str + "</" + Name + ">"; +std::string tag(StringRef Name, StringRef Str, StringRef ClassName = "") { + std::string Tag = "<"; + Tag += Name; + if (!ClassName.empty()) { + Tag += " class='"; + Tag += ClassName; + Tag += "'"; + } + Tag += ">"; + Tag += Str; + Tag += "</"; + Tag += Name; + Tag += ">"; + return Tag; } // Create an anchor to \p Link with the label \p Str. -std::string a(const std::string &Link, const std::string &Str, - const std::string &TargetName = "") { - std::string Name = TargetName.empty() ? "" : ("name='" + TargetName + "' "); - return "<a " + Name + "href='" + Link + "'>" + Str + "</a>"; +std::string a(StringRef Link, StringRef Str, StringRef TargetName = "") { + std::string Tag; + Tag += "<a "; + if (!TargetName.empty()) { + Tag += "name='"; + Tag += TargetName; + Tag += "' "; + } + Tag += "href='"; + Tag += Link; + Tag += "'>"; + Tag += Str; + Tag += "</a>"; + return Tag; } const char *BeginHeader = - "<head>" + "<head>" "<meta name='viewport' content='width=device-width,initial-scale=1'>" "<meta charset='UTF-8'>"; @@ -272,6 +291,57 @@ void emitPrelude(raw_ostream &OS, const CoverageViewOptions &Opts, OS << EndHeader << "<body>"; } +void emitTableRow(raw_ostream &OS, const CoverageViewOptions &Opts, + const std::string &FirstCol, const FileCoverageSummary &FCS, + bool IsTotals) { + SmallVector<std::string, 8> Columns; + + // Format a coverage triple and add the result to the list of columns. + auto AddCoverageTripleToColumn = + [&Columns, &Opts](unsigned Hit, unsigned Total, float Pctg) { + std::string S; + { + raw_string_ostream RSO{S}; + if (Total) + RSO << format("%*.2f", 7, Pctg) << "% "; + else + RSO << "- "; + RSO << '(' << Hit << '/' << Total << ')'; + } + const char *CellClass = "column-entry-yellow"; + if (Pctg >= Opts.HighCovWatermark) + CellClass = "column-entry-green"; + else if (Pctg < Opts.LowCovWatermark) + CellClass = "column-entry-red"; + Columns.emplace_back(tag("td", tag("pre", S), CellClass)); + }; + + Columns.emplace_back(tag("td", tag("pre", FirstCol))); + AddCoverageTripleToColumn(FCS.FunctionCoverage.getExecuted(), + FCS.FunctionCoverage.getNumFunctions(), + FCS.FunctionCoverage.getPercentCovered()); + if (Opts.ShowInstantiationSummary) + AddCoverageTripleToColumn(FCS.InstantiationCoverage.getExecuted(), + FCS.InstantiationCoverage.getNumFunctions(), + FCS.InstantiationCoverage.getPercentCovered()); + AddCoverageTripleToColumn(FCS.LineCoverage.getCovered(), + FCS.LineCoverage.getNumLines(), + FCS.LineCoverage.getPercentCovered()); + if (Opts.ShowRegionSummary) + AddCoverageTripleToColumn(FCS.RegionCoverage.getCovered(), + FCS.RegionCoverage.getNumRegions(), + FCS.RegionCoverage.getPercentCovered()); + if (Opts.ShowBranchSummary) + AddCoverageTripleToColumn(FCS.BranchCoverage.getCovered(), + FCS.BranchCoverage.getNumBranches(), + FCS.BranchCoverage.getPercentCovered()); + + if (IsTotals) + OS << tag("tr", join(Columns.begin(), Columns.end(), ""), "light-row-bold"); + else + OS << tag("tr", join(Columns.begin(), Columns.end(), ""), "light-row"); +} + void emitEpilog(raw_ostream &OS) { OS << "</body>" << "</html>"; @@ -330,33 +400,44 @@ CoveragePrinterHTML::buildLinkToFile(StringRef SF, return a(LinkTarget, LinkText); } +Error CoveragePrinterHTML::emitStyleSheet() { + auto CSSOrErr = createOutputStream("style", "css", /*InToplevel=*/true); + if (Error E = CSSOrErr.takeError()) + return E; + + OwnedStream CSS = std::move(CSSOrErr.get()); + CSS->operator<<(CSSForCoverage); + + return Error::success(); +} + +void CoveragePrinterHTML::emitReportHeader(raw_ostream &OSRef, + const std::string &Title) { + // Emit some basic information about the coverage report. + if (Opts.hasProjectTitle()) + OSRef << tag(ProjectTitleTag, escape(Opts.ProjectTitle, Opts)); + OSRef << tag(ReportTitleTag, Title); + if (Opts.hasCreatedTime()) + OSRef << tag(CreatedTimeTag, escape(Opts.CreatedTimeStr, Opts)); + + // Emit a link to some documentation. + OSRef << tag("p", "Click " + + a("http://clang.llvm.org/docs/" + "SourceBasedCodeCoverage.html#interpreting-reports", + "here") + + " for information about interpreting this report."); + + // Emit a table containing links to reports for each file in the covmapping. + // Exclude files which don't contain any regions. + OSRef << BeginCenteredDiv << BeginTable; + emitColumnLabelsForIndex(OSRef, Opts); +} + /// Render a file coverage summary (\p FCS) in a table row. If \p IsTotals is /// false, link the summary to \p SF. void CoveragePrinterHTML::emitFileSummary(raw_ostream &OS, StringRef SF, const FileCoverageSummary &FCS, bool IsTotals) const { - SmallVector<std::string, 8> Columns; - - // Format a coverage triple and add the result to the list of columns. - auto AddCoverageTripleToColumn = - [&Columns, this](unsigned Hit, unsigned Total, float Pctg) { - std::string S; - { - raw_string_ostream RSO{S}; - if (Total) - RSO << format("%*.2f", 7, Pctg) << "% "; - else - RSO << "- "; - RSO << '(' << Hit << '/' << Total << ')'; - } - const char *CellClass = "column-entry-yellow"; - if (Pctg >= Opts.HighCovWatermark) - CellClass = "column-entry-green"; - else if (Pctg < Opts.LowCovWatermark) - CellClass = "column-entry-red"; - Columns.emplace_back(tag("td", tag("pre", S), CellClass)); - }; - // Simplify the display file path, and wrap it in a link if requested. std::string Filename; if (IsTotals) { @@ -365,43 +446,16 @@ void CoveragePrinterHTML::emitFileSummary(raw_ostream &OS, StringRef SF, Filename = buildLinkToFile(SF, FCS); } - Columns.emplace_back(tag("td", tag("pre", Filename))); - AddCoverageTripleToColumn(FCS.FunctionCoverage.getExecuted(), - FCS.FunctionCoverage.getNumFunctions(), - FCS.FunctionCoverage.getPercentCovered()); - if (Opts.ShowInstantiationSummary) - AddCoverageTripleToColumn(FCS.InstantiationCoverage.getExecuted(), - FCS.InstantiationCoverage.getNumFunctions(), - FCS.InstantiationCoverage.getPercentCovered()); - AddCoverageTripleToColumn(FCS.LineCoverage.getCovered(), - FCS.LineCoverage.getNumLines(), - FCS.LineCoverage.getPercentCovered()); - if (Opts.ShowRegionSummary) - AddCoverageTripleToColumn(FCS.RegionCoverage.getCovered(), - FCS.RegionCoverage.getNumRegions(), - FCS.RegionCoverage.getPercentCovered()); - if (Opts.ShowBranchSummary) - AddCoverageTripleToColumn(FCS.BranchCoverage.getCovered(), - FCS.BranchCoverage.getNumBranches(), - FCS.BranchCoverage.getPercentCovered()); - - if (IsTotals) - OS << tag("tr", join(Columns.begin(), Columns.end(), ""), "light-row-bold"); - else - OS << tag("tr", join(Columns.begin(), Columns.end(), ""), "light-row"); + emitTableRow(OS, Opts, Filename, FCS, IsTotals); } Error CoveragePrinterHTML::createIndexFile( ArrayRef<std::string> SourceFiles, const CoverageMapping &Coverage, const CoverageFiltersMatchAll &Filters) { // Emit the default stylesheet. - auto CSSOrErr = createOutputStream("style", "css", /*InToplevel=*/true); - if (Error E = CSSOrErr.takeError()) + if (Error E = emitStyleSheet()) return E; - OwnedStream CSS = std::move(CSSOrErr.get()); - CSS->operator<<(CSSForCoverage); - // Emit a file index along with some coverage statistics. auto OSOrErr = createOutputStream("index", "html", /*InToplevel=*/true); if (Error E = OSOrErr.takeError()) @@ -412,24 +466,8 @@ Error CoveragePrinterHTML::createIndexFile( assert(Opts.hasOutputDirectory() && "No output directory for index file"); emitPrelude(OSRef, Opts, getPathToStyle("")); - // Emit some basic information about the coverage report. - if (Opts.hasProjectTitle()) - OSRef << tag(ProjectTitleTag, escape(Opts.ProjectTitle, Opts)); - OSRef << tag(ReportTitleTag, "Coverage Report"); - if (Opts.hasCreatedTime()) - OSRef << tag(CreatedTimeTag, escape(Opts.CreatedTimeStr, Opts)); - - // Emit a link to some documentation. - OSRef << tag("p", "Click " + - a("http://clang.llvm.org/docs/" - "SourceBasedCodeCoverage.html#interpreting-reports", - "here") + - " for information about interpreting this report."); + emitReportHeader(OSRef, "Coverage Report"); - // Emit a table containing links to reports for each file in the covmapping. - // Exclude files which don't contain any regions. - OSRef << BeginCenteredDiv << BeginTable; - emitColumnLabelsForIndex(OSRef, Opts); FileCoverageSummary Totals("TOTALS"); auto FileReports = CoverageReport::prepareFileReports( Coverage, Totals, SourceFiles, Opts, Filters); @@ -465,6 +503,199 @@ Error CoveragePrinterHTML::createIndexFile( return Error::success(); } +struct CoveragePrinterHTMLDirectory::Reporter : public DirectoryCoverageReport { + CoveragePrinterHTMLDirectory &Printer; + + Reporter(CoveragePrinterHTMLDirectory &Printer, + const coverage::CoverageMapping &Coverage, + const CoverageFiltersMatchAll &Filters) + : DirectoryCoverageReport(Printer.Opts, Coverage, Filters), + Printer(Printer) {} + + Error generateSubDirectoryReport(SubFileReports &&SubFiles, + SubDirReports &&SubDirs, + FileCoverageSummary &&SubTotals) override { + auto &LCPath = SubTotals.Name; + assert(Options.hasOutputDirectory() && + "No output directory for index file"); + + SmallString<128> OSPath = LCPath; + sys::path::append(OSPath, "index"); + auto OSOrErr = Printer.createOutputStream(OSPath, "html", + /*InToplevel=*/false); + if (auto E = OSOrErr.takeError()) + return E; + auto OS = std::move(OSOrErr.get()); + raw_ostream &OSRef = *OS.get(); + + auto IndexHtmlPath = Printer.getOutputPath((LCPath + "index").str(), "html", + /*InToplevel=*/false); + emitPrelude(OSRef, Options, getPathToStyle(IndexHtmlPath)); + + auto NavLink = buildTitleLinks(LCPath); + Printer.emitReportHeader(OSRef, "Coverage Report (" + NavLink + ")"); + + std::vector<const FileCoverageSummary *> EmptyFiles; + + // Make directories at the top of the table. + for (auto &&SubDir : SubDirs) { + auto &Report = SubDir.second.first; + if (!Report.FunctionCoverage.getNumFunctions()) + EmptyFiles.push_back(&Report); + else + emitTableRow(OSRef, Options, buildRelLinkToFile(Report.Name), Report, + /*IsTotals=*/false); + } + + for (auto &&SubFile : SubFiles) { + auto &Report = SubFile.second; + if (!Report.FunctionCoverage.getNumFunctions()) + EmptyFiles.push_back(&Report); + else + emitTableRow(OSRef, Options, buildRelLinkToFile(Report.Name), Report, + /*IsTotals=*/false); + } + + // Emit the totals row. + emitTableRow(OSRef, Options, "Totals", SubTotals, /*IsTotals=*/false); + OSRef << EndTable << EndCenteredDiv; + + // Emit links to files which don't contain any functions. These are normally + // not very useful, but could be relevant for code which abuses the + // preprocessor. + if (!EmptyFiles.empty()) { + OSRef << tag("p", "Files which contain no functions. (These " + "files contain code pulled into other files " + "by the preprocessor.)\n"); + OSRef << BeginCenteredDiv << BeginTable; + for (auto FCS : EmptyFiles) { + auto Link = buildRelLinkToFile(FCS->Name); + OSRef << tag("tr", tag("td", tag("pre", Link)), "light-row") << '\n'; + } + OSRef << EndTable << EndCenteredDiv; + } + + // Emit epilog. + OSRef << tag("h5", escape(Options.getLLVMVersionString(), Options)); + emitEpilog(OSRef); + + return Error::success(); + } + + /// Make a title with hyperlinks to the index.html files of each hierarchy + /// of the report. + std::string buildTitleLinks(StringRef LCPath) const { + // For each report level in LCPStack, extract the path component and + // calculate the number of "../" relative to current LCPath. + SmallVector<std::pair<SmallString<128>, unsigned>, 16> Components; + + auto Iter = LCPStack.begin(), IterE = LCPStack.end(); + SmallString<128> RootPath; + if (*Iter == 0) { + // If llvm-cov works on relative coverage mapping data, the LCP of + // all source file paths can be 0, which makes the title path empty. + // As we like adding a slash at the back of the path to indicate a + // directory, in this case, we use "." as the root path to make it + // not be confused with the root path "/". + RootPath = "."; + } else { + RootPath = LCPath.substr(0, *Iter); + sys::path::native(RootPath); + sys::path::remove_dots(RootPath, /*remove_dot_dot=*/true); + } + Components.emplace_back(std::move(RootPath), 0); + + for (auto Last = *Iter; ++Iter != IterE; Last = *Iter) { + SmallString<128> SubPath = LCPath.substr(Last, *Iter - Last); + sys::path::native(SubPath); + sys::path::remove_dots(SubPath, /*remove_dot_dot=*/true); + auto Level = unsigned(SubPath.count(sys::path::get_separator())) + 1; + Components.back().second += Level; + Components.emplace_back(std::move(SubPath), Level); + } + + // Then we make the title accroding to Components. + std::string S; + for (auto I = Components.begin(), E = Components.end();;) { + auto &Name = I->first; + if (++I == E) { + S += a("./index.html", Name); + S += sys::path::get_separator(); + break; + } + + SmallString<128> Link; + for (unsigned J = I->second; J > 0; --J) + Link += "../"; + Link += "index.html"; + S += a(Link, Name); + S += sys::path::get_separator(); + } + return S; + } + + std::string buildRelLinkToFile(StringRef RelPath) const { + SmallString<128> LinkTextStr(RelPath); + sys::path::native(LinkTextStr); + + // remove_dots will remove trailing slash, so we need to check before it. + auto IsDir = LinkTextStr.endswith(sys::path::get_separator()); + sys::path::remove_dots(LinkTextStr, /*remove_dot_dot=*/true); + + SmallString<128> LinkTargetStr(LinkTextStr); + if (IsDir) { + LinkTextStr += sys::path::get_separator(); + sys::path::append(LinkTargetStr, "index.html"); + } else { + LinkTargetStr += ".html"; + } + + auto LinkText = escape(LinkTextStr, Options); + auto LinkTarget = escape(LinkTargetStr, Options); + return a(LinkTarget, LinkText); + } +}; + +Error CoveragePrinterHTMLDirectory::createIndexFile( + ArrayRef<std::string> SourceFiles, const CoverageMapping &Coverage, + const CoverageFiltersMatchAll &Filters) { + // The createSubIndexFile function only works when SourceFiles is + // more than one. So we fallback to CoveragePrinterHTML when it is. + if (SourceFiles.size() <= 1) + return CoveragePrinterHTML::createIndexFile(SourceFiles, Coverage, Filters); + + // Emit the default stylesheet. + if (Error E = emitStyleSheet()) + return E; + + // Emit index files in every subdirectory. + Reporter Report(*this, Coverage, Filters); + auto TotalsOrErr = Report.prepareDirectoryReports(SourceFiles); + if (auto E = TotalsOrErr.takeError()) + return E; + auto &LCPath = TotalsOrErr->Name; + + // Emit the top level index file. Top level index file is just a redirection + // to the index file in the LCP directory. + auto OSOrErr = createOutputStream("index", "html", /*InToplevel=*/true); + if (auto E = OSOrErr.takeError()) + return E; + auto OS = std::move(OSOrErr.get()); + auto LCPIndexFilePath = + getOutputPath((LCPath + "index").str(), "html", /*InToplevel=*/false); + *OS.get() << R"(<!DOCTYPE html> + <html> + <head> + <meta http-equiv="Refresh" content="0; url=')" + << LCPIndexFilePath << R"('" /> + </head> + <body></body> + </html> + )"; + + return Error::success(); +} + void SourceCoverageViewHTML::renderViewHeader(raw_ostream &OS) { OS << BeginCenteredDiv << BeginTable; } diff --git a/llvm/tools/llvm-cov/SourceCoverageViewHTML.h b/llvm/tools/llvm-cov/SourceCoverageViewHTML.h index 7d94675..c846379 100644 --- a/llvm/tools/llvm-cov/SourceCoverageViewHTML.h +++ b/llvm/tools/llvm-cov/SourceCoverageViewHTML.h @@ -19,6 +19,8 @@ namespace llvm { using namespace coverage; +class ThreadPool; + struct FileCoverageSummary; /// A coverage printer for html output. @@ -36,6 +38,10 @@ public: CoveragePrinterHTML(const CoverageViewOptions &Opts) : CoveragePrinter(Opts) {} +protected: + Error emitStyleSheet(); + void emitReportHeader(raw_ostream &OSRef, const std::string &Title); + private: void emitFileSummary(raw_ostream &OS, StringRef SF, const FileCoverageSummary &FCS, @@ -44,6 +50,20 @@ private: const FileCoverageSummary &FCS) const; }; +/// A coverage printer for html output, but generates index files in every +/// subdirectory to show a hierarchical view. +class CoveragePrinterHTMLDirectory : public CoveragePrinterHTML { +public: + using CoveragePrinterHTML::CoveragePrinterHTML; + + Error createIndexFile(ArrayRef<std::string> SourceFiles, + const coverage::CoverageMapping &Coverage, + const CoverageFiltersMatchAll &Filters) override; + +private: + struct Reporter; +}; + /// A code coverage view which supports html-based rendering. class SourceCoverageViewHTML : public SourceCoverageView { void renderViewHeader(raw_ostream &OS) override; diff --git a/llvm/tools/llvm-cov/SourceCoverageViewText.cpp b/llvm/tools/llvm-cov/SourceCoverageViewText.cpp index 6e0db09..44694a0 100644 --- a/llvm/tools/llvm-cov/SourceCoverageViewText.cpp +++ b/llvm/tools/llvm-cov/SourceCoverageViewText.cpp @@ -14,7 +14,9 @@ #include "CoverageReport.h" #include "llvm/ADT/SmallString.h" #include "llvm/ADT/StringExtras.h" +#include "llvm/Support/FileSystem.h" #include "llvm/Support/Format.h" +#include "llvm/Support/Path.h" #include <optional> using namespace llvm; @@ -46,6 +48,69 @@ Error CoveragePrinterText::createIndexFile( return Error::success(); } +struct CoveragePrinterTextDirectory::Reporter : public DirectoryCoverageReport { + CoveragePrinterTextDirectory &Printer; + + Reporter(CoveragePrinterTextDirectory &Printer, + const coverage::CoverageMapping &Coverage, + const CoverageFiltersMatchAll &Filters) + : DirectoryCoverageReport(Printer.Opts, Coverage, Filters), + Printer(Printer) {} + + Error generateSubDirectoryReport(SubFileReports &&SubFiles, + SubDirReports &&SubDirs, + FileCoverageSummary &&SubTotals) override { + auto &LCPath = SubTotals.Name; + assert(Options.hasOutputDirectory() && + "No output directory for index file"); + + SmallString<128> OSPath = LCPath; + sys::path::append(OSPath, "index"); + auto OSOrErr = Printer.createOutputStream(OSPath, "txt", + /*InToplevel=*/false); + if (auto E = OSOrErr.takeError()) + return E; + auto OS = std::move(OSOrErr.get()); + raw_ostream &OSRef = *OS.get(); + + std::vector<FileCoverageSummary> Reports; + for (auto &&SubDir : SubDirs) + Reports.push_back(std::move(SubDir.second.first)); + for (auto &&SubFile : SubFiles) + Reports.push_back(std::move(SubFile.second)); + + CoverageReport Report(Options, Coverage); + Report.renderFileReports(OSRef, Reports, SubTotals, Filters.empty()); + + Options.colored_ostream(OSRef, raw_ostream::CYAN) + << "\n" + << Options.getLLVMVersionString(); + + return Error::success(); + } +}; + +Error CoveragePrinterTextDirectory::createIndexFile( + ArrayRef<std::string> SourceFiles, const CoverageMapping &Coverage, + const CoverageFiltersMatchAll &Filters) { + if (SourceFiles.size() <= 1) + return CoveragePrinterText::createIndexFile(SourceFiles, Coverage, Filters); + + Reporter Report(*this, Coverage, Filters); + auto TotalsOrErr = Report.prepareDirectoryReports(SourceFiles); + if (auto E = TotalsOrErr.takeError()) + return E; + auto &LCPath = TotalsOrErr->Name; + + auto TopIndexFilePath = + getOutputPath("index", "txt", /*InToplevel=*/true, /*Relative=*/false); + auto LCPIndexFilePath = + getOutputPath((LCPath + "index").str(), "txt", /*InToplevel=*/false, + /*Relative=*/false); + return errorCodeToError( + sys::fs::copy_file(LCPIndexFilePath, TopIndexFilePath)); +} + namespace { static const unsigned LineCoverageColumnWidth = 7; diff --git a/llvm/tools/llvm-cov/SourceCoverageViewText.h b/llvm/tools/llvm-cov/SourceCoverageViewText.h index b2be060..ade47ed 100644 --- a/llvm/tools/llvm-cov/SourceCoverageViewText.h +++ b/llvm/tools/llvm-cov/SourceCoverageViewText.h @@ -35,6 +35,21 @@ public: : CoveragePrinter(Opts) {} }; +/// A coverage printer for text output, but generates index files in every +/// subdirectory to show a hierarchical view. The implementation is similar +/// to CoveragePrinterHTMLDirectory. So please refer to that for more comments. +class CoveragePrinterTextDirectory : public CoveragePrinterText { +public: + using CoveragePrinterText::CoveragePrinterText; + + Error createIndexFile(ArrayRef<std::string> SourceFiles, + const CoverageMapping &Coverage, + const CoverageFiltersMatchAll &Filters) override; + +private: + struct Reporter; +}; + /// A code coverage view which supports text-based rendering. class SourceCoverageViewText : public SourceCoverageView { void renderViewHeader(raw_ostream &OS) override; diff --git a/llvm/tools/llvm-cov/TestingSupport.cpp b/llvm/tools/llvm-cov/TestingSupport.cpp index 59e9ee4..6ad8c35 100644 --- a/llvm/tools/llvm-cov/TestingSupport.cpp +++ b/llvm/tools/llvm-cov/TestingSupport.cpp @@ -121,6 +121,11 @@ int convertForTestingMain(int argc, const char *argv[]) { return 1; } + // If this is a linked PE/COFF file, then we have to skip over the null byte + // that is allocated in the .lprfn$A section in the LLVM profiling runtime. + if (isa<COFFObjectFile>(OF) && !OF->isRelocatableObject()) + ProfileNamesData = ProfileNamesData.drop_front(1); + int FD; if (auto Err = sys::fs::openFileForWrite(OutputFilename, FD)) { errs() << "error: " << Err.message() << "\n"; |