diff --git a/Coverage-x86/Coverage-x86.vcxproj b/Coverage-x86/Coverage-x86.vcxproj index a7a6fbd..c322a08 100644 --- a/Coverage-x86/Coverage-x86.vcxproj +++ b/Coverage-x86/Coverage-x86.vcxproj @@ -106,6 +106,7 @@ true true stdcpp20 + NDEBUG;_MBCS;%(PreprocessorDefinitions) true diff --git a/Coverage/Coverage.vcxproj b/Coverage/Coverage.vcxproj index e64799f..f7a315e 100644 --- a/Coverage/Coverage.vcxproj +++ b/Coverage/Coverage.vcxproj @@ -130,6 +130,7 @@ true $(IntDir)vc$(PlatformToolsetVersion).pdb stdcpp20 + NDEBUG;%(PreprocessorDefinitions) true diff --git a/Coverage/CoverageRunner.cpp b/Coverage/CoverageRunner.cpp new file mode 100644 index 0000000..bca9392 --- /dev/null +++ b/Coverage/CoverageRunner.cpp @@ -0,0 +1,113 @@ +#include "CoverageRunner.h" + +#include +//--------------------------------------------------------------------------------------- + +SourceManager CoverageRunner::_sources; + +//--------------------------------------------------------------------------------------- + +void SourceManager::setupExcludeFilter(const RuntimeOptions& opts) +{ + _excludeFilter.clear(); + for (const auto& filter : opts.excludeFilter) + { + _excludeFilter.emplace_back(std::regex(filter, std::regex_constants::ECMAScript /*| std::regex_constants::icase*/)); + } +} + +bool SourceManager::isExcluded(const std::filesystem::path& originalPath) const +{ + for (const auto& filter : _excludeFilter) + { + if (std::regex_search(originalPath.string(), filter)) + { + return true; + } + } + return false; +} + +void SourceManager::searchRealPath(std::filesystem::path& finalPath, const std::filesystem::path& original, bool& exclude) const +{ + // Search if original path exist into valid filter + bool isInGoodPath = false; + for (const auto& codepath : RuntimeOptionsSingleton::Instance().CodePaths) + { + if (original.string().starts_with(codepath.string())) + { + isInGoodPath = true; + break; + } + } + + // If file not exists on disk but path look like good, return missing file + if ( isInGoodPath && !std::filesystem::exists(original) ) + { + finalPath = std::filesystem::path(); + exclude = false; + return; + } + + // Try to remap path based on CodePath (happens when CI run coverage on different disk or path ) + for (const auto& codepath : RuntimeOptionsSingleton::Instance().CodePaths) + { + // Try to reinterpret path (file from another server ?) + finalPath = original; + auto allFolders = finalPath.parent_path(); + // Start with filename + finalPath = finalPath.filename(); + const auto source = codepath; + + while (!allFolders.filename().string().empty()) + { + auto testPath = source / finalPath; + if (std::filesystem::exists(testPath)) + { + finalPath = testPath; + exclude = false; + return; + } + else + { + finalPath = allFolders.filename() / finalPath; + allFolders = allFolders.parent_path(); + } + } + // If found nothing, reset path and consider exclude file + finalPath = std::filesystem::path(); + exclude = true; + } +} + +SourceManager::SearchResult SourceManager::searchFromCodePath(const PSRCCODEINFO& lineInfo, const FileCallbackInfo& fileInfo, std::filesystem::path& finalPath) +{ + SearchResult result; + const auto originalPath = std::filesystem::path(lineInfo->FileName); + const auto itPath = _conversion.find(originalPath); + if (itPath != _conversion.cend()) + { + finalPath = itPath->second; + } + else + { + result.isNew = true; + finalPath = std::filesystem::path(); + + // Search file is not inside exclude list and into CodePaths range + if ( !isExcluded(originalPath) ) + { + searchRealPath(finalPath, originalPath, result.isExcluded); + } + else + { + result.isExcluded = true; + } + + // Save already meet path + _conversion.emplace( originalPath, finalPath ); + } + result.isFound = !finalPath.empty(); + + return result; +} \ No newline at end of file diff --git a/Coverage/CoverageRunner.h b/Coverage/CoverageRunner.h index be1b462..1fb1612 100644 --- a/Coverage/CoverageRunner.h +++ b/Coverage/CoverageRunner.h @@ -10,9 +10,9 @@ #include "Disassembler/ReachabilityAnalysis.h" #include -#include #include #include +#include #include #include #include @@ -24,40 +24,67 @@ #include #pragma warning(default : 4091) +/// +/// Allow to keep real path between PDB info and real path. +/// +struct SourceManager +{ + struct SearchResult + { + bool isNew = false; + bool isFound = false; + bool isExcluded = false; + }; + + std::vector _excludeFilter; + std::unordered_map _conversion; + + SourceManager() noexcept = default; + + void setupExcludeFilter(const RuntimeOptions& opts); + bool isExcluded(const std::filesystem::path& originalPath) const; + + /// + /// Search if PDB file exists (search into pdb path and inside CodePath). + /// Then save it into map to not compute it again. + /// + SearchResult searchFromCodePath(const PSRCCODEINFO& lineInfo, const FileCallbackInfo& fileInfo, std::filesystem::path& finalPath); + +private: + void searchRealPath(std::filesystem::path& finalPath, const std::filesystem::path& original, bool& exclude) const; +}; + struct CoverageRunner { + /// + /// Keep met source file (can be remap if PDB have another path) + /// + static SourceManager _sources; + CoverageRunner(const RuntimeOptions& opts) : options(opts), debugInfoAvailable(false), debuggerPresentPatched(false), coverageContext(opts.Executable), profileInfo() - {} + { + // Initialize static data + _sources.setupExcludeFilter(opts); + } static BOOL CALLBACK SymEnumLinesCallback(PSRCCODEINFO lineInfo, PVOID userContext) { CallbackInfo* info = reinterpret_cast(userContext); - if (info->fileInfo->PathMatches(lineInfo->FileName)) + std::filesystem::path filepath; + const auto result = _sources.searchFromCodePath(lineInfo, *info->fileInfo, filepath); + if (result.isFound) { - // Try to find if file exists (and can be covered) - auto file = std::string(lineInfo->FileName); - if (!std::filesystem::exists(file)) - { -#ifndef NDEBUG - if (RuntimeOptions::Instance().isAtLeastLevel(VerboseLevel::Error)) - { - std::cerr << std::format("Impossible to find file : {0}", file) << std::endl; - } -#endif - return FALSE; - } - PVOID addr = reinterpret_cast(lineInfo->Address); auto it = info->breakpointsToSet.find(addr); if (it == info->breakpointsToSet.end()) { // Find line info - auto fileLineInfo = info->fileInfo->LineInfo(file, lineInfo->LineNumber); + auto fileLineInfo = info->fileInfo->LineInfo(filepath.string(), lineInfo->LineNumber); if (fileLineInfo) { // Only create breakpoint if we haven't already. @@ -73,6 +100,15 @@ struct CoverageRunner } } } + else + { + // Show one time into log + if (result.isNew && !result.isExcluded && RuntimeOptionsSingleton::Instance().isAtLeastLevel(VerboseLevel::Error)) + { + std::cerr << std::format("Impossible to find file : {0}", lineInfo->FileName) << std::endl; + } + return FALSE; + } return TRUE; } diff --git a/Coverage/Disassembler/ReachabilityAnalysis.cpp b/Coverage/Disassembler/ReachabilityAnalysis.cpp index f89a94e..d2326a9 100644 --- a/Coverage/Disassembler/ReachabilityAnalysis.cpp +++ b/Coverage/Disassembler/ReachabilityAnalysis.cpp @@ -29,7 +29,7 @@ struct Helper SIZE_T numberBytesRead; if (!ReadProcessMemory(ptr, reinterpret_cast(address + arg->offset), &result, 1, &numberBytesRead)) { - if (RuntimeOptions::Instance().isAtLeastLevel(VerboseLevel::Error)) + if (RuntimeOptionsSingleton::Instance().isAtLeastLevel(VerboseLevel::Error)) { auto err = Util::GetLastErrorAsString(); std::cout << "Error reading memory from target process: " << err << std::endl; @@ -178,7 +178,7 @@ ReachabilityAnalysis::ReachabilityAnalysis(HANDLE processHandle, DWORD64 methodS SIZE_T numberBytesRead; if (!ReadProcessMemory(processHandle, reinterpret_cast(methodStart), data, numberBytes, &numberBytesRead)) { - if( RuntimeOptions::Instance().isAtLeastLevel(VerboseLevel::Error) ) + if(RuntimeOptionsSingleton::Instance().isAtLeastLevel(VerboseLevel::Error) ) { auto err = Util::GetLastErrorAsString(); std::cout << "Error while reading symbol: " << err << std::endl; diff --git a/Coverage/FileCallbackInfo.h b/Coverage/FileCallbackInfo.h index adb287a..7bafcb0 100644 --- a/Coverage/FileCallbackInfo.h +++ b/Coverage/FileCallbackInfo.h @@ -24,7 +24,7 @@ struct FileCallbackInfo FileCallbackInfo(const std::string& filename) : filename(filename) { - if (RuntimeOptions::Instance().CodePaths.empty()) + if (RuntimeOptionsSingleton::Instance().CodePaths.empty()) { auto idx = filename.find("x64"); if (idx == std::string::npos) @@ -66,7 +66,7 @@ struct FileCallbackInfo std::swap(tmp, it.second); newLineData[it.first] = std::move(tmp); } - else if (RuntimeOptions::Instance().isAtLeastLevel(VerboseLevel::Trace)) + else if (RuntimeOptionsSingleton::Instance().isAtLeastLevel(VerboseLevel::Trace)) { std::cout << "Removing file " << it.first << std::endl; } @@ -74,7 +74,7 @@ struct FileCallbackInfo std::swap(lineData, newLineData); } - bool PathMatches(const char* first, const std::string& second) + bool PathMatches(const char* first, const std::string& second) const { const char* ptr = first; const char* gt = second.data(); @@ -90,17 +90,17 @@ struct FileCallbackInfo return true; } - bool PathMatches(const char* filename) + bool PathMatches(const char* filename) const { if (!sourcePath.empty()) { return PathMatches(filename, sourcePath); } - const auto& codePaths = RuntimeOptions::Instance().CodePaths; + const auto& codePaths = RuntimeOptionsSingleton::Instance().CodePaths; for (const auto& codePath : codePaths) { - if (PathMatches(filename, codePath)) + if (PathMatches(filename, codePath.string())) { return true; } @@ -170,7 +170,7 @@ struct FileCallbackInfo // stream << "coveredstatements=\"300\" statements=\"500\" coveredmethods=\"50\" methods=\"80\" "; // stream << "coveredconditionals=\"100\" conditionals=\"120\" coveredelements=\"900\" elements=\"1000\" "; stream << "complexity=\"0\" />" << std::endl; - stream << "" << std::endl; + stream << "" << std::endl; for (auto& it : lineData) { auto ptr = it.second.get(); @@ -229,7 +229,7 @@ struct FileCallbackInfo stream << "" << std::endl; stream << "\t" << "" << std::endl; - stream << "\t\t" << "" << std::endl; + stream << "\t\t" << "" << std::endl; stream << "\t\t\t" << "" << std::endl; for (auto& it : lineData) { @@ -379,7 +379,9 @@ struct FileCallbackInfo filepaths.push_back(item.first); } - for (const auto& dirPath : RuntimeOptions::Instance().CodePaths) + const auto& options = RuntimeOptionsSingleton::Instance(); + + for (const auto& dirPath : options.CodePaths) { bool dirPartAdded = false; @@ -401,7 +403,7 @@ struct FileCallbackInfo if (!dirPartAdded && !dirPath.empty()) { dirPartAdded = true; - FileCoverageV2::openDirectory(stream, dirPath); + FileCoverageV2::openDirectory(stream, dirPath == options.SolutionPath, dirPath); } auto coverage = encodeCoverage(*it.second.get()); @@ -417,7 +419,7 @@ struct FileCallbackInfo } } - if (!filepaths.empty() && RuntimeOptions::Instance().isAtLeastLevel(VerboseLevel::Warning)) + if (!filepaths.empty() && options.isAtLeastLevel(VerboseLevel::Warning)) { std::cerr << "List of refuse coverage files (because not relative to any code path):" << std::endl; @@ -428,9 +430,9 @@ struct FileCallbackInfo std::cerr << std::endl << "List of code paths:" << std::endl; - for (const auto& dirPath : RuntimeOptions::Instance().CodePaths) + for (const auto& dirPath : RuntimeOptionsSingleton::Instance().CodePaths) { - std::cerr << std::format("- {0}", dirPath) << std::endl; + std::cerr << std::format("- {0}", dirPath.string()) << std::endl; } } diff --git a/Coverage/FileCoverageV2.h b/Coverage/FileCoverageV2.h index 8f20416..e5ad3ec 100644 --- a/Coverage/FileCoverageV2.h +++ b/Coverage/FileCoverageV2.h @@ -3,6 +3,8 @@ #include "base64.h" #include "FileInfo.h" +#include +#include #include #include @@ -50,19 +52,27 @@ struct FileCoverageV2 return code; } + /// + /// Compute from scratch the line stats. + /// Warning: Nb code lines can variate too if template (don't produce code line if not used) + /// Maybe compilation option are badly choose by user ? + /// void updateStats() { _nbLinesCovered = 0; + _nbLinesCode = 0; for (const auto& line : _code) { if ((line & maskIsCode) == maskIsCode) { + _nbLinesCode += 1; if ((line & maskCount) > 0) { _nbLinesCovered += 1; } } } + assert(_nbLinesCovered <= _nbLinesCode); } bool merge(const FileCoverageV2& other) @@ -76,7 +86,7 @@ struct FileCoverageV2 { const size_t count = (size_t) (line & maskCount) + (size_t) (*src & maskCount); - const bool isCode = (line & maskIsCode) == maskIsCode; + const bool isCode = ((line & maskIsCode) | (*src & maskIsCode)) == maskIsCode || count > 0; const bool isPartial = (line & maskIsPartial) == maskIsPartial && (*src & maskIsPartial) == maskIsPartial; line = (uint16_t) std::min(count, maskCount); @@ -97,9 +107,9 @@ struct FileCoverageV2 ofs << std::format(R"()", version) << std::endl; } - static void openDirectory(std::ostream& ofs, const std::string& aDir) + static void openDirectory(std::ostream& ofs, const bool isSolutionPath, const std::filesystem::path& aDir) { - ofs << std::format(R"( )", aDir) << std::endl; + ofs << std::format(R"( )", isSolutionPath ? "true" : "false", aDir.string()) << std::endl; } static void closeDirectory(std::ostream& ofs) diff --git a/Coverage/FileInfo.cpp b/Coverage/FileInfo.cpp index cb93d12..f489804 100644 --- a/Coverage/FileInfo.cpp +++ b/Coverage/FileInfo.cpp @@ -111,7 +111,7 @@ FileLineInfo* FileInfo::LineInfo(size_t lineNumber) if (lineNumber < 0xf00000 - 1 && lineNumber != numberLines) { - if (RuntimeOptions::Instance().isAtLeastLevel(VerboseLevel::Warning)) + if (RuntimeOptionsSingleton::Instance().isAtLeastLevel(VerboseLevel::Warning)) { std::cout << "Warning: line number out of bounds: " << lineNumber << " >= " << numberLines << std::endl; } diff --git a/Coverage/Main.cpp b/Coverage/Main.cpp index 5a8e101..3b4c4db 100644 --- a/Coverage/Main.cpp +++ b/Coverage/Main.cpp @@ -38,6 +38,7 @@ void ShowHelp() std::cout << " helper processes into the main -o output file and delete them," << std::endl; std::cout << " so that you end up with a single coverage file containing both" << std::endl; std::cout << " bitnesses. Only supported for -format native / nativeV2." << std::endl; + std::cout << " -excludeFile: Regexp to exclude file of coverage (sometime you can have template fake file)" << std::endl; std::cout << " -- [name]: Run coverage on the given executable filename" << std::endl; std::cout << "Return code:" << std::endl; std::cout << " 0: Success run" << std::endl; @@ -55,7 +56,7 @@ void ShowHelp() void ParseCommandLine(int argc, const char** argv) { - RuntimeOptions& opts = RuntimeOptions::Instance(); + RuntimeOptions& opts = RuntimeOptionsSingleton::Instance(); LPTSTR cmd = GetCommandLine(); std::string cmdLine = cmd; @@ -146,6 +147,7 @@ void ParseCommandLine(int argc, const char** argv) opts.SolutionPath = t; if (!std::filesystem::exists(opts.SolutionPath)) throw std::exception("The solution path provide is not existing."); + opts.CodePaths.emplace(opts.SolutionPath); } else if (s == "-format") { @@ -197,7 +199,7 @@ void ParseCommandLine(int argc, const char** argv) } std::string t(argv[i]); - opts.CodePaths.push_back(t); + opts.CodePaths.emplace(t); } else if (s == "-w") { @@ -244,6 +246,16 @@ void ParseCommandLine(int argc, const char** argv) opts.Executable = t; break; } + else if( s == "-excludeFile") + { + ++i; + if (i == argc) + { + throw std::exception("Unexpected end of parameters. Expected filter."); + } + + opts.excludeFilter.emplace_back( std::string(argv[i]) ); + } else if (s == "-help") { ShowHelp(); @@ -324,12 +336,13 @@ void ParseCommandLine(int argc, const char** argv) opts.ExecutableArguments = opts.ExecutableArguments.substr(1); */ #ifdef _DEBUG - if (RuntimeOptions::Instance().isAtLeastLevel(VerboseLevel::Trace)) + if (opts.isAtLeastLevel(VerboseLevel::Trace)) { std::cout << "Executable: " << opts.Executable << std::endl; std::cout << "Arguments: " << opts.ExecutableArguments << std::endl; } #endif + return -1; } class UTF8CodePage { @@ -361,7 +374,7 @@ int main(int argc, const char** argv) } #endif - RuntimeOptions& opts = RuntimeOptions::Instance(); + RuntimeOptions& opts = RuntimeOptionsSingleton::Instance(); try { @@ -369,7 +382,7 @@ int main(int argc, const char** argv) } catch (const std::exception& e) { - if (RuntimeOptions::Instance().isAtLeastLevel(VerboseLevel::Error)) + if (opts.isAtLeastLevel(VerboseLevel::Error)) { std::cerr << "Error: " << e.what() << std::endl; } @@ -388,7 +401,7 @@ int main(int argc, const char** argv) { if (opts.Executable.empty()) { - if (RuntimeOptions::Instance().isAtLeastLevel(VerboseLevel::Error)) + if (opts.isAtLeastLevel(VerboseLevel::Error)) { std::cerr << "Error: Missing executable file" << std::endl; } @@ -498,7 +511,7 @@ int main(int argc, const char** argv) { if (!opts.MergedOutput.empty()) { - if (RuntimeOptions::Instance().isAtLeastLevel(VerboseLevel::Info)) + if (opts.isAtLeastLevel(VerboseLevel::Info)) { std::cout << "Merge into " << opts.MergedOutput << std::endl; } @@ -533,7 +546,7 @@ int main(int argc, const char** argv) } catch (const std::exception& e) { - if (RuntimeOptions::Instance().isAtLeastLevel(VerboseLevel::Error)) + if (opts.isAtLeastLevel(VerboseLevel::Error)) { std::cerr << "Error: " << e.what() << std::endl; } diff --git a/Coverage/MergeRunner.h b/Coverage/MergeRunner.h index 2de0bb4..0cd5a17 100644 --- a/Coverage/MergeRunner.h +++ b/Coverage/MergeRunner.h @@ -3,12 +3,24 @@ #include "RuntimeOptions.h" #include +#include #include +struct CoverageResult +{ + virtual ~CoverageResult() = default; + + virtual size_t nbCoveredFile() const = 0; + + virtual size_t nbLineCovered(const std::filesystem::path& path) const = 0; + + virtual size_t nbFolders() const = 0; +}; + class MergeRunner { protected: - RuntimeOptions _options; ///< Copy local of option. + const RuntimeOptions _options; ///< Copy local of option. // Avoid copy constructor MergeRunner(const MergeRunner&) = delete; @@ -28,6 +40,9 @@ class MergeRunner /// Run merge virtual void execute() = 0; + /// Read dict on disk + virtual std::unique_ptr read( const std::filesystem::path& ) const = 0; + // Allow to build good merge runner static std::unique_ptr createMergeRunner(const RuntimeOptions& opts); }; \ No newline at end of file diff --git a/Coverage/MergeRunnerV1.h b/Coverage/MergeRunnerV1.h index 5bd5370..5d981b8 100644 --- a/Coverage/MergeRunnerV1.h +++ b/Coverage/MergeRunnerV1.h @@ -18,7 +18,32 @@ class MergeRunnerV1 : public MergeRunner using DictCoverage = std::map; - DictCoverage makeDictionary(const std::string& filename) + struct Result : public CoverageResult + { + DictCoverage _dict; + + size_t nbCoveredFile() const override + { + return _dict.size(); + } + + size_t nbLineCovered(const std::filesystem::path& path) const override + { + const auto search = _dict.find(path.string()); + if (search != _dict.cend()) + { + return count(search->second.res.begin(), search->second.res.end(), 'c'); + } + return 0ull; + } + + size_t nbFolders() const override + { + return _dict.size(); + } + }; + + DictCoverage makeDictionary(const std::string& filename) const { DictCoverage dictOutput; @@ -99,6 +124,13 @@ class MergeRunnerV1 : public MergeRunner assert(_options.ExportFormat == RuntimeOptions::Native); // Support only this ! } + std::unique_ptr read(const std::filesystem::path& path) const override + { + auto r = std::make_unique(); + r->_dict = makeDictionary(path.string()); + return r; + } + /// Run merge void execute() override { diff --git a/Coverage/MergeRunnerV2.h b/Coverage/MergeRunnerV2.h index acf1bf6..8de21f0 100644 --- a/Coverage/MergeRunnerV2.h +++ b/Coverage/MergeRunnerV2.h @@ -5,8 +5,7 @@ #include "MergeRunner.h" #include -#include -#include +#include #include namespace TestFormat @@ -14,14 +13,57 @@ namespace TestFormat class TestNativeV2; } + + class MergeRunnerV2 : public MergeRunner { public: friend class TestFormat::TestNativeV2; -private: - using CodeCoverage = std::unordered_map; + + struct CodeCoverage + { + using Coverages = std::unordered_map; + + bool _isSolutionFolder = true; + Coverages _coverages; + }; + using DictCoverage = std::unordered_map; + struct Result : public CoverageResult + { + DictCoverage _dict; + + size_t nbLineCovered(const std::filesystem::path& path) const override + { + for(const auto& item : _dict) + { + const auto search = item.second._coverages.find(path.string()); + if (search != item.second._coverages.cend()) + { + return search->second._nbLinesCovered; + } + } + return 0ull; + } + + size_t nbCoveredFile() const override + { + size_t nb = 0; + for(const auto& item : _dict) + { + nb += item.second._coverages.size(); + } + return nb; + } + + size_t nbFolders() const override + { + return _dict.size(); + } + }; + +private: std::string clean(const std::string& content) const { // Need to remove return line (not supported by regex) @@ -29,9 +71,10 @@ class MergeRunnerV2 : public MergeRunner return std::regex_replace(content, PatternClean, ""); } - std::string getDir(const std::string& line) const + std::string getDir(const std::string& line, bool& isSolutionDir) const { - std::regex pattern(R"(")) { - if (!codeCoverage.empty()) + if (!codeCoverage._coverages.empty()) { + codeCoverage._isSolutionFolder = isSolutionDir; dictOutput[currentDir] = codeCoverage; } - codeCoverage.clear(); + codeCoverage._coverages.clear(); currentDir.clear(); } } - if (!codeCoverage.empty()) + if (!codeCoverage._coverages.empty()) { dictOutput[""] = codeCoverage; } @@ -152,27 +199,52 @@ class MergeRunnerV2 : public MergeRunner return dictOutput; } + DictCoverage::iterator findMainDirectory(const DictCoverage::const_iterator& itDirOutput, DictCoverage& dictMerge) + { + // Search is folder exists + auto itDirMerge = dictMerge.find(itDirOutput->first); + + // If we don't found and it's a solutionDir, search if another solution dir exists inside data. + if ( itDirMerge == dictMerge.cend() && itDirOutput->second._isSolutionFolder ) + { + itDirMerge = std::find_if(dictMerge.begin(), dictMerge.end(), [](const auto& data) + { + return data.second._isSolutionFolder; + }); + } + return itDirMerge; + } + void merge(const DictCoverage& dictOutput, DictCoverage& dictMerge) { + // Parsing Output auto itDirOutput = dictOutput.cbegin(); while (itDirOutput != dictOutput.cend()) { - auto itDirMerge = dictMerge.find(itDirOutput->first); - if (itDirMerge != dictMerge.end()) + // Search this file into the merge dict + auto itDirMerge = findMainDirectory(itDirOutput, dictMerge); + if (itDirMerge != dictMerge.cend()) { - auto itFileOutput = itDirOutput->second.cbegin(); - while (itFileOutput != itDirOutput->second.cend()) + auto itFileOutput = itDirOutput->second._coverages.cbegin(); + while (itFileOutput != itDirOutput->second._coverages.cend()) { - auto fileMerge = itDirMerge->second.find(itFileOutput->first); - if (!fileMerge->second.merge(itFileOutput->second)) + auto fileMerge = itDirMerge->second._coverages.find(itFileOutput->first); + if ( fileMerge != itDirMerge->second._coverages.cend() ) + { + if (!fileMerge->second.merge(itFileOutput->second)) + { + // Source is different from both version ? + std::cerr << "Merge warning: impossible to merge " << fileMerge->first << ": size between src/dst is not same." << std::endl; + } + } + else // if is existing into itDirMerge only -> copy { - // Source is different from both version ? - std::cerr << "Merge warning: impossible to merge " << fileMerge->first << ": size between src/dst is not same." << std::endl; + itDirMerge->second._coverages[itFileOutput->first] = itFileOutput->second; } ++itFileOutput; } } - else + else // if is existing into itDirOutput only -> copy { dictMerge[itDirOutput->first] = itDirOutput->second; } @@ -190,6 +262,13 @@ class MergeRunnerV2 : public MergeRunner assert(_options.ExportFormat == RuntimeOptions::NativeV2); // Support only this ! } + std::unique_ptr read( const std::filesystem::path& path ) const override + { + auto result = std::make_unique(); + result->_dict = makeDictionary( path.string() ); + return result; + } + /// Run merge void execute() override { @@ -213,7 +292,7 @@ class MergeRunnerV2 : public MergeRunner // ---- Make merge --------------------------------------------------------------- // Step 1: Parse output files and define a dictionary DictCoverage dictOutput = makeDictionary(_options.OutputFile); - DictCoverage dictMerge = makeDictionary(_options.MergedOutput); + DictCoverage dictMerge = makeDictionary(_options.MergedOutput); // Step 2: Parse merge merge(dictOutput, dictMerge); @@ -228,9 +307,9 @@ class MergeRunnerV2 : public MergeRunner const auto& dirName = directories.first; if (!dirName.empty()) { - FileCoverageV2::openDirectory(ofs, dirName); + FileCoverageV2::openDirectory(ofs, directories.second._isSolutionFolder, dirName); } - for (const auto& cover : directories.second) + for (const auto& cover : directories.second._coverages) { cover.second.write(cover.first, ofs); } diff --git a/Coverage/RuntimeNotifications.cpp b/Coverage/RuntimeNotifications.cpp index 08f639e..6a680bf 100644 --- a/Coverage/RuntimeNotifications.cpp +++ b/Coverage/RuntimeNotifications.cpp @@ -73,7 +73,7 @@ std::string RuntimeNotifications::GetFQN(std::string s) static constexpr char BACKSLASH = '\\'; static constexpr std::string_view parentDir = "..\\"; - auto cp = RuntimeOptions::Instance().SolutionPath; + auto cp = RuntimeOptionsSingleton::Instance().SolutionPath; if (cp.empty()) { return s; } while (s.size() > parentDir.size() && s.starts_with(parentDir)) @@ -112,7 +112,7 @@ std::string RuntimeNotifications::GetFQN(std::string s) void RuntimeNotifications::PrintInvalidNotification(const std::string& notification, const std::string_view information) { - if (RuntimeOptions::Instance().isAtLeastLevel(VerboseLevel::Warning)) + if (RuntimeOptionsSingleton::Instance().isAtLeastLevel(VerboseLevel::Warning)) { std::cout << "WARNING: Invalid path " << notification << ". " << information << std::endl; } @@ -144,7 +144,7 @@ void RuntimeNotifications::Handle(const char* data, const size_t size) } else { - if (RuntimeOptions::Instance().isAtLeastLevel(VerboseLevel::Info)) + if (RuntimeOptionsSingleton::Instance().isAtLeastLevel(VerboseLevel::Info)) { std::cout << "Ignoring folder: " << fullname << std::endl; } @@ -166,7 +166,7 @@ void RuntimeNotifications::Handle(const char* data, const size_t size) } else { - if (RuntimeOptions::Instance().isAtLeastLevel(VerboseLevel::Info)) + if (RuntimeOptionsSingleton::Instance().isAtLeastLevel(VerboseLevel::Info)) { std::cout << "Ignoring file: " << file << std::endl; } @@ -175,13 +175,13 @@ void RuntimeNotifications::Handle(const char* data, const size_t size) } else if (s == "ENABLE CODE ANALYSIS") { - RuntimeOptions::Instance().UseStaticCodeAnalysis = true; + RuntimeOptionsSingleton::Instance().UseStaticCodeAnalysis = true; } else if (s == "DISABLE CODE ANALYSIS") { - RuntimeOptions::Instance().UseStaticCodeAnalysis = false; + RuntimeOptionsSingleton::Instance().UseStaticCodeAnalysis = false; } - else if (RuntimeOptions::Instance().isAtLeastLevel(VerboseLevel::Error)) + else if (RuntimeOptionsSingleton::Instance().isAtLeastLevel(VerboseLevel::Error)) { std::cout << "Unknown option passed to coverage: " << s << std::endl; } diff --git a/Coverage/RuntimeOptions.h b/Coverage/RuntimeOptions.h index 519642b..e6be7d5 100644 --- a/Coverage/RuntimeOptions.h +++ b/Coverage/RuntimeOptions.h @@ -1,22 +1,23 @@ #pragma once -#include +#include #include +#include +#include #include enum class VerboseLevel { - Error = 0x01, + Error = 0x01, Warning = 0x03, - Info = 0x07, - Trace = 0x0F, - None = 0 + Info = 0x07, + Trace = 0x0F, + None = 0 }; struct RuntimeOptions { -private: RuntimeOptions() : UseStaticCodeAnalysis(false), ExportFormat(Native), @@ -24,14 +25,8 @@ struct RuntimeOptions AttachPid(0), ConsolidateAuxiliary(false) {} - -public: - static RuntimeOptions& Instance() - { - static RuntimeOptions instance; - return instance; - } - + virtual ~RuntimeOptions() = default; + VerboseLevel _verboseLevel = VerboseLevel::Trace; bool UseStaticCodeAnalysis; @@ -49,11 +44,12 @@ struct RuntimeOptions std::string MergedOutput; std::string WorkingDirectory; - std::list CodePaths; + std::unordered_set CodePaths; std::string Executable; std::string ExecutableArguments; std::string PackageName = "Program.exe"; std::string SolutionPath; + std::vector excludeFilter; // When Attach is true, the coverage runner will attach (DebugActiveProcess) // to an already-running process identified by AttachPid instead of launching @@ -74,3 +70,18 @@ struct RuntimeOptions bool isAtLeastLevel(const VerboseLevel& level) const { return (static_cast(_verboseLevel) & static_cast(level)) == static_cast(level); } }; + +struct RuntimeOptionsSingleton : public RuntimeOptions +{ +private: + RuntimeOptionsSingleton() = default; + +public: + ~RuntimeOptionsSingleton() override = default; + + static RuntimeOptions& Instance() + { + static RuntimeOptionsSingleton instance; + return instance; + } +}; \ No newline at end of file diff --git a/Coverage/Shared/Shared.vcxitems b/Coverage/Shared/Shared.vcxitems index b022d07..7af08c6 100644 --- a/Coverage/Shared/Shared.vcxitems +++ b/Coverage/Shared/Shared.vcxitems @@ -43,6 +43,7 @@ + diff --git a/Coverage/Shared/Shared.vcxitems.filters b/Coverage/Shared/Shared.vcxitems.filters index 535b217..42e94ba 100644 --- a/Coverage/Shared/Shared.vcxitems.filters +++ b/Coverage/Shared/Shared.vcxitems.filters @@ -12,6 +12,7 @@ + diff --git a/Coverage/Test/CoverageRunnerTest.cpp b/Coverage/Test/CoverageRunnerTest.cpp new file mode 100644 index 0000000..08c3032 --- /dev/null +++ b/Coverage/Test/CoverageRunnerTest.cpp @@ -0,0 +1,56 @@ +#include "CppUnitTest.h" +#include + +#include "CoverageRunner.h" +#include "RuntimeOptions.h" + +#ifndef NOMINMAX +# define NOMINMAX +# include +#endif + +// #pragma warning(disable: 4091) +// #include +// #pragma warning(default: 4091) + +using namespace Microsoft::VisualStudio::CppUnitTestFramework; +// +// namespace Microsoft +// { +// namespace VisualStudio +// { +// namespace CppUnitTestFramework +// { +// template<> static std::wstring ToString(const class FileCoverageV2::LineArray& t) { return L"FileCoverageV2::LineArray"; } +// } +// } +// } + +namespace TestCoverageRunner +{ + TEST_CLASS(TestSourceManager) + { + public: + + TEST_METHOD(checkIsExcluded) + { + SourceManager manager; + + RuntimeOptions options; + + options.excludeFilter.emplace_back("/moc_"); + manager.setupExcludeFilter(options); + + Assert::IsFalse(manager.isExcluded(std::filesystem::path("c:\\My/Path/Without/Issue.cpp"))); + + Assert::IsTrue(manager.isExcluded(std::filesystem::path("c:\\My/Qt/moc_issue.cpp"))); + + Assert::IsFalse(manager.isExcluded(std::filesystem::path("/My/template/predefined C++ types (compiler internal)"))); + + options.excludeFilter.emplace_back("(compiler internal)"); + manager.setupExcludeFilter(options); + + Assert::IsTrue(manager.isExcluded(std::filesystem::path("c:\\My/template/predefined C++ types (compiler internal)"))); + } + }; +} \ No newline at end of file diff --git a/Coverage/Test/FileCallbackInfoTest.cpp b/Coverage/Test/FileCallbackInfoTest.cpp index a4a2af7..9816b90 100644 --- a/Coverage/Test/FileCallbackInfoTest.cpp +++ b/Coverage/Test/FileCallbackInfoTest.cpp @@ -13,8 +13,8 @@ namespace TestFileCallbackInfo public: TestLineInfo() { - auto& options = RuntimeOptions::Instance(); - options.CodePaths.push_back("C:\\proj\\src\\"); + auto& options = RuntimeOptionsSingleton::Instance(); + options.CodePaths.emplace("C:\\proj\\src\\"); // create a test file FileSystem::CreateTestFile("C:\\proj\\src\\srcFile.cpp", "Line_1\nLine_2\nLine_3\nLine_4"); @@ -23,7 +23,7 @@ namespace TestFileCallbackInfo ~TestLineInfo() { - auto& options = RuntimeOptions::Instance(); + auto& options = RuntimeOptionsSingleton::Instance(); options.CodePaths.clear(); FileSystem::DeleteTestFiles(); @@ -94,10 +94,10 @@ namespace TestFileCallbackInfo public: TestWriteReport() { - auto& options = RuntimeOptions::Instance(); - options.CodePaths.push_back("C:\\proj\\src\\"); - options.CodePaths.push_back("C:\\proj\\empty\\"); - options.CodePaths.push_back("C:\\proj\\lib\\"); + auto& options = RuntimeOptionsSingleton::Instance(); + options.CodePaths.emplace("C:\\proj\\src\\"); + options.CodePaths.emplace("C:\\proj\\empty\\"); + options.CodePaths.emplace("C:\\proj\\lib\\"); options.PackageName = "MyPackage.exe"; @@ -120,7 +120,7 @@ namespace TestFileCallbackInfo ~TestWriteReport() { - auto& options = RuntimeOptions::Instance(); + auto& options = RuntimeOptionsSingleton::Instance(); options.CodePaths.clear(); options.PackageName.clear(); @@ -157,7 +157,7 @@ namespace TestFileCallbackInfo const std::string expectReport = R"()""\n" \ R"()""\n" \ - R"( )""\n" \ + R"( )""\n" \ R"( )""\n" \ R"( )""\n" \ R"( BIACwAAAAIACwA==)""\n" \ @@ -167,7 +167,7 @@ namespace TestFileCallbackInfo R"( BoADgA==)""\n" \ R"( )""\n" \ R"( )""\n" \ - R"( )""\n" \ + R"( )""\n" \ R"( )""\n" \ R"( )""\n" \ R"( AIAAgACA)""\n" \ diff --git a/Coverage/Test/MergeRunnerTest.cpp b/Coverage/Test/MergeRunnerTest.cpp new file mode 100644 index 0000000..73914e0 --- /dev/null +++ b/Coverage/Test/MergeRunnerTest.cpp @@ -0,0 +1,87 @@ +#include "CppUnitTest.h" +#include + +#include "MergeRunnerV2.h" +#include "RuntimeOptions.h" + +#ifndef NOMINMAX +# define NOMINMAX +# include +#endif + +// #pragma warning(disable: 4091) +// #include +// #pragma warning(default: 4091) + +using namespace Microsoft::VisualStudio::CppUnitTestFramework; +// +// namespace Microsoft +// { +// namespace VisualStudio +// { +// namespace CppUnitTestFramework +// { +// template<> static std::wstring ToString(const class FileCoverageV2::LineArray& t) { return L"FileCoverageV2::LineArray"; } +// } +// } +// } + +namespace TestMerge +{ + TEST_CLASS(TestMergeRunner) + { + public: + + TEST_METHOD(MergeNativeV2TwoFile) + { + const std::filesystem::path workingDir = std::filesystem::current_path().parent_path().parent_path(); + + const std::filesystem::path output1 = workingDir / "DataTest" / "Test1_UT.tmp.cov"; + const std::filesystem::path output2 = workingDir / "DataTest" / "Test2_UT.tmp.cov"; + const std::filesystem::path merged("./Outputs/Merged.cov"); + std::filesystem::remove_all(merged.parent_path()); + std::filesystem::create_directories(merged.parent_path()); + + Assert::IsFalse( std::filesystem::exists(merged) ); + + RuntimeOptions options; + options.ExportFormat = RuntimeOptions::ExportFormatType::NativeV2; + options.MergedOutput = std::filesystem::absolute(merged).string(); + options.OutputFile = std::filesystem::absolute(output1).string(); + + // Merge on empty file + { + auto merge = MergeRunner::createMergeRunner(options); + try + { + merge->execute(); + } + catch (...) + { + Assert::Fail(); + } + const auto result = merge->read(options.MergedOutput); + Assert::AreEqual(7ull, result->nbCoveredFile()); + } + + Assert::IsTrue(std::filesystem::exists(merged)); + options.OutputFile = output2.string(); + + // Merge with something + { + auto merge = MergeRunner::createMergeRunner(options); + try + { + merge->execute(); + } + catch (...) + { + Assert::Fail(); + } + const auto result = merge->read(options.MergedOutput); + Assert::AreEqual(10ull, result->nbCoveredFile()); + Assert::AreEqual(131ull, result->nbLineCovered(std::filesystem::path("lib/TestM/B.h"))); + } + } + }; +} \ No newline at end of file diff --git a/Coverage/Test/RuntimeNotificationsTest.cpp b/Coverage/Test/RuntimeNotificationsTest.cpp index a55c771..68b540f 100644 --- a/Coverage/Test/RuntimeNotificationsTest.cpp +++ b/Coverage/Test/RuntimeNotificationsTest.cpp @@ -28,13 +28,13 @@ namespace TestRuntimeNotifications TEST_METHOD_INITIALIZE(MethodInit) { // set default SolutionPath - auto& options = RuntimeOptions::Instance(); + auto& options = RuntimeOptionsSingleton::Instance(); options.SolutionPath = "C:\\proj\\sln\\"; } TEST_CLASS_CLEANUP(CleanUp) { - auto& options = RuntimeOptions::Instance(); + auto& options = RuntimeOptionsSingleton::Instance(); options.SolutionPath.clear(); FileSystem::DeleteTestFiles(); @@ -90,7 +90,7 @@ namespace TestRuntimeNotifications TEST_METHOD(IgnoreFileRelativeSolutionPathWithoutLastBackslash) { - RuntimeOptions::Instance().SolutionPath = "C:\\proj\\sln"; + RuntimeOptionsSingleton::Instance().SolutionPath = "C:\\proj\\sln"; static constexpr std::string_view LINE = "IGNORE FILE:..\\lib\\ignoreFile.c "; @@ -114,7 +114,7 @@ namespace TestRuntimeNotifications TEST_METHOD(IgnoreFileWithoutParentDirRelativeSolutionPathWithoutLastBackslash) { - RuntimeOptions::Instance().SolutionPath = "C:\\proj\\sln"; + RuntimeOptionsSingleton::Instance().SolutionPath = "C:\\proj\\sln"; static constexpr std::string_view LINE = "IGNORE FILE: \\dir\\file.c"; @@ -126,7 +126,7 @@ namespace TestRuntimeNotifications TEST_METHOD(IgnoreFileWithoutParentDirStartsWithDirName) { - RuntimeOptions::Instance().SolutionPath = "C:\\proj\\sln"; + RuntimeOptionsSingleton::Instance().SolutionPath = "C:\\proj\\sln"; static constexpr std::string_view LINE = "IGNORE FILE: dir\\file.c"; @@ -138,7 +138,7 @@ namespace TestRuntimeNotifications TEST_METHOD(IgnoreFileRelativeWithoutSolutionPath) { - RuntimeOptions::Instance().SolutionPath.clear(); + RuntimeOptionsSingleton::Instance().SolutionPath.clear(); static constexpr std::string_view LINE = "IGNORE FILE:..\\lib\\ignoreFile.c "; @@ -212,7 +212,7 @@ namespace TestRuntimeNotifications TEST_METHOD(IgnoreFolderRelativeSolutionPathWithoutLastBackslash) { - RuntimeOptions::Instance().SolutionPath = "C:\\proj\\sln"; + RuntimeOptionsSingleton::Instance().SolutionPath = "C:\\proj\\sln"; static constexpr std::string_view LINE = "IGNORE FOLDER: ..\\src\\ignoreFolder1"; @@ -239,7 +239,7 @@ namespace TestRuntimeNotifications TEST_METHOD(IgnoreFolderWithoutParentDirRelativeSolutionPathWithoutLastBackslash) { - RuntimeOptions::Instance().SolutionPath = "C:\\proj\\sln"; + RuntimeOptionsSingleton::Instance().SolutionPath = "C:\\proj\\sln"; static constexpr std::string_view LINE = "IGNORE FOLDER: \\dir"; @@ -252,7 +252,7 @@ namespace TestRuntimeNotifications TEST_METHOD(IgnoreFolderWithoutParentDirStartsWithDirName) { - RuntimeOptions::Instance().SolutionPath = "C:\\proj\\sln"; + RuntimeOptionsSingleton::Instance().SolutionPath = "C:\\proj\\sln"; static constexpr std::string_view LINE = "IGNORE FOLDER: dir"; @@ -265,7 +265,7 @@ namespace TestRuntimeNotifications TEST_METHOD(IgnoreFolderRelativeWithoutSolutionPath) { - RuntimeOptions::Instance().SolutionPath.clear(); + RuntimeOptionsSingleton::Instance().SolutionPath.clear(); static constexpr std::string_view LINE = "IGNORE FOLDER: ..\\src\\ignoreFolder1"; @@ -291,26 +291,26 @@ namespace TestRuntimeNotifications TEST_METHOD(EnableCodeAnalysis) { - RuntimeOptions::Instance().UseStaticCodeAnalysis = false; + RuntimeOptionsSingleton::Instance().UseStaticCodeAnalysis = false; static constexpr std::string_view LINE = "ENABLE CODE ANALYSIS"; RuntimeNotifications notifications; notifications.Handle(LINE.data(), LINE.size()); - Assert::IsTrue(RuntimeOptions::Instance().UseStaticCodeAnalysis); + Assert::IsTrue(RuntimeOptionsSingleton::Instance().UseStaticCodeAnalysis); } TEST_METHOD(DisableCodeAnalysis) { - RuntimeOptions::Instance().UseStaticCodeAnalysis = true; + RuntimeOptionsSingleton::Instance().UseStaticCodeAnalysis = true; static constexpr std::string_view LINE = "DISABLE CODE ANALYSIS"; RuntimeNotifications notifications; notifications.Handle(LINE.data(), LINE.size()); - Assert::IsFalse(RuntimeOptions::Instance().UseStaticCodeAnalysis); + Assert::IsFalse(RuntimeOptionsSingleton::Instance().UseStaticCodeAnalysis); } }; } \ No newline at end of file diff --git a/Coverage/Test/Test.vcxproj b/Coverage/Test/Test.vcxproj index a40dec6..a3bb6cd 100644 --- a/Coverage/Test/Test.vcxproj +++ b/Coverage/Test/Test.vcxproj @@ -151,7 +151,7 @@ true true $(VCInstallDir)UnitTest\include;%(AdditionalIncludeDirectories);.. - NDEBUG;%(PreprocessorDefinitions) + NDEBUG;UNITTEST;%(PreprocessorDefinitions) true stdcpp20 @@ -168,9 +168,11 @@ + + diff --git a/Coverage/Test/nativeV2.cpp b/Coverage/Test/nativeV2.cpp index f1a90b3..13d1533 100644 --- a/Coverage/Test/nativeV2.cpp +++ b/Coverage/Test/nativeV2.cpp @@ -62,7 +62,8 @@ namespace TestFormat FileCoverageV2::writeHeader(ss); if (!dirName.empty()) { - FileCoverageV2::openDirectory(ss, dirName); + const bool isSolutionDir = true; + FileCoverageV2::openDirectory(ss, isSolutionDir, dirName); } merge.write(filename, ss); if (!dirName.empty()) @@ -72,7 +73,7 @@ namespace TestFormat FileCoverageV2::writeFooter(ss); // Create reader - auto options = RuntimeOptions::Instance(); + auto options = RuntimeOptionsSingleton::Instance(); options.MergedOutput = filename; /// Dummy valid value options.OutputFile = filename; /// Dummy valid value options.ExportFormat = RuntimeOptions::ExportFormatType::NativeV2; @@ -82,7 +83,8 @@ namespace TestFormat Assert::AreEqual(EXPECT_DICT_SIZE, dict.size()); // Check dictionary - const auto& saved = dict[dirName][filename]; + const auto& saved = dict[dirName]._coverages[filename]; + Assert::AreEqual(true, dict[dirName]._isSolutionFolder); Assert::AreEqual(reference.size(), saved._code.size()); Assert::AreEqual(reference, saved._code); Assert::AreEqual(merge._nbLinesFile, saved._nbLinesFile); diff --git a/CoverageExt/Cobertura/CoberturaData.cs b/CoverageExt/Cobertura/CoberturaData.cs index ae3f38c..1178c5a 100644 --- a/CoverageExt/Cobertura/CoberturaData.cs +++ b/CoverageExt/Cobertura/CoberturaData.cs @@ -99,7 +99,7 @@ private void PostProcess(string filename) } } - public void Parsing(string filename) + public void Parsing(string filename, string solutionPath) { // Start initializing the data source = null; diff --git a/CoverageExt/Cobertura/CoberturaReportManager.cs b/CoverageExt/Cobertura/CoberturaReportManager.cs index 6d869a0..f4f25a5 100644 --- a/CoverageExt/Cobertura/CoberturaReportManager.cs +++ b/CoverageExt/Cobertura/CoberturaReportManager.cs @@ -82,7 +82,7 @@ private ICoverageData UpdateDataImpl() if (activeCoverageReport == null) { output.WriteLine("Updating coverage results from: {0}", coverageFile); - activeCoverageReport = Load(coverageFile); + activeCoverageReport = Load(coverageFile, folder); activeCoverageFilename = coverageFile; } } @@ -92,7 +92,7 @@ private ICoverageData UpdateDataImpl() return activeCoverageReport; } - private ICoverageData Load(string filename) + private ICoverageData Load(string filename, string solutionPath) { Microsoft.VisualStudio.Shell.ThreadHelper.ThrowIfNotOnUIThread(); @@ -102,7 +102,7 @@ private ICoverageData Load(string filename) try { report = new Cobertura.CoberturaData(); - report.Parsing(filename); + report.Parsing(filename, solutionPath); } catch (Exception e) { diff --git a/CoverageExt/Data/ICoverageData.cs b/CoverageExt/Data/ICoverageData.cs index 0c19687..bb81538 100644 --- a/CoverageExt/Data/ICoverageData.cs +++ b/CoverageExt/Data/ICoverageData.cs @@ -38,6 +38,6 @@ public interface ICoverageData UInt32 nbEntries(); - void Parsing( string filename ); + void Parsing( string filename, string solutionPath); } } diff --git a/CoverageExt/Native/NativeData.cs b/CoverageExt/Native/NativeData.cs index 9c2aca5..39c74c3 100644 --- a/CoverageExt/Native/NativeData.cs +++ b/CoverageExt/Native/NativeData.cs @@ -111,7 +111,7 @@ private void AddOrReplace(string key, FileCoverageData data) } } - public void Parsing(string filename) + public void Parsing( string filename, string solutionPath ) { // Get file date (for modified checks) FileDate = new System.IO.FileInfo(filename).LastWriteTimeUtc; diff --git a/CoverageExt/Native/NativeReportManager.cs b/CoverageExt/Native/NativeReportManager.cs index 92e157b..5b1aa3c 100644 --- a/CoverageExt/Native/NativeReportManager.cs +++ b/CoverageExt/Native/NativeReportManager.cs @@ -84,7 +84,7 @@ private ICoverageData UpdateDataImpl() output.WriteLine("------ Start update Coverage ------"); output.WriteLine("Updating coverage results from: {0}", coverageFile); var watch = System.Diagnostics.Stopwatch.StartNew(); - activeCoverageReport = Load(coverageFile); + activeCoverageReport = Load(coverageFile, folder); activeCoverageFilename = coverageFile; watch.Stop(); output.WriteLine("========== Done in {0} ms with {1} entries ==========", watch.ElapsedMilliseconds, activeCoverageReport.nbEntries()); @@ -101,7 +101,7 @@ public Data.ICoverageData UpdateData() throw new NotImplementedException(); } - virtual public ICoverageData Load(string filename) + virtual public ICoverageData Load(string filename, string solutionPath) { Microsoft.VisualStudio.Shell.ThreadHelper.ThrowIfNotOnUIThread(); @@ -111,7 +111,7 @@ virtual public ICoverageData Load(string filename) try { report = new Native.NativeData(); - report.Parsing(filename); + report.Parsing(filename, solutionPath); } catch (Exception e) { diff --git a/CoverageExt/Native/NativeV2Data.cs b/CoverageExt/Native/NativeV2Data.cs index 7aeb591..f163efe 100644 --- a/CoverageExt/Native/NativeV2Data.cs +++ b/CoverageExt/Native/NativeV2Data.cs @@ -167,7 +167,7 @@ private void ParseFileData(XmlNode item, string dir) } } - void ICoverageData.Parsing(string filename) + void ICoverageData.Parsing(string filename, string solutionPath) { // Get file date (for modified checks) FileDate = new System.IO.FileInfo(filename).LastWriteTimeUtc; @@ -190,8 +190,15 @@ void ICoverageData.Parsing(string filename) { continue; } - + + // Replace by current solutionPath if needed + bool isSolutionPath = dirItem.Attributes["isSolutionPath"].InnerText == "true"; string currentDir = dirItem.Attributes["path"].InnerText; + if ( isSolutionPath ) + { + currentDir = solutionPath; + } + // A directory is valid if (String.IsNullOrEmpty(currentDir) || !System.IO.Path.IsPathRooted(currentDir)) { diff --git a/CoverageExt/Native/NativeV2ReportManager.cs b/CoverageExt/Native/NativeV2ReportManager.cs index ad0e122..8644106 100644 --- a/CoverageExt/Native/NativeV2ReportManager.cs +++ b/CoverageExt/Native/NativeV2ReportManager.cs @@ -15,7 +15,7 @@ public override bool IsValid(Settings instance) && instance.Format == CoverageFormat.NativeV2; } - public override ICoverageData Load(string filename) + public override ICoverageData Load(string filename, string solutionPath) { Microsoft.VisualStudio.Shell.ThreadHelper.ThrowIfNotOnUIThread(); @@ -25,7 +25,7 @@ public override ICoverageData Load(string filename) try { report = new Native.NativeV2Data(); - report.Parsing(filename); + report.Parsing(filename, solutionPath); } catch (Exception e) { diff --git a/CoverageExt/Properties/AssemblyInfo.cs b/CoverageExt/Properties/AssemblyInfo.cs index e38f11d..f341a56 100644 --- a/CoverageExt/Properties/AssemblyInfo.cs +++ b/CoverageExt/Properties/AssemblyInfo.cs @@ -28,8 +28,8 @@ // You can specify all the values or you can default the Revision and Build Numbers // by using the '*' as shown below: -[assembly: AssemblyVersion("2.18.0.0")] -[assembly: AssemblyFileVersion("2.18.0.0")] +[assembly: AssemblyVersion("2.40.0.0")] +[assembly: AssemblyFileVersion("2.40.0.0")] diff --git a/CoverageExt/Resources/Coverage-x64d.exe b/CoverageExt/Resources/Coverage-x64d.exe index e7d94ec..b45d04f 100644 Binary files a/CoverageExt/Resources/Coverage-x64d.exe and b/CoverageExt/Resources/Coverage-x64d.exe differ diff --git a/CoverageExt/Resources/Coverage-x86d.exe b/CoverageExt/Resources/Coverage-x86d.exe index 0914f2f..d76b6c5 100644 Binary files a/CoverageExt/Resources/Coverage-x86d.exe and b/CoverageExt/Resources/Coverage-x86d.exe differ diff --git a/DataTest/Test1_UT.tmp.cov b/DataTest/Test1_UT.tmp.cov new file mode 100644 index 0000000..0f1e74b --- /dev/null +++ b/DataTest/Test1_UT.tmp.cov @@ -0,0 +1,31 @@ + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABgAGAAoABgAGAAAAAAAGAAYAAAAAAAAAAAAGAAAAAAAAAAYAAAAAAAIAAgACAAAAAAACAAIABgAAAAAAAAAAAAYAAAAAAAAAAAAAAAYAAAAGAAAAAAAAAAYAAgAAAAAAAAAGAAAABgAGAAAAAAA== + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgACAAIAAAAAAAAAAAAAAAAAAAAAAAAAAgACAAIAAgACAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAgACAAIAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAIAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAgACAAAAAAAAAAIAAgACAAAAAAAAAAAAAgACAAAAAAACAAAAAAAAAAAAAgACAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgACAAIAAgACAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAAYABgAAAAAAAAAAAAAAAAAAAAYABgAGAAAAAAAAAAAAAAAAAAAAAAAGAAYABgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYABgAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA== + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABgAGAAAABgACAAAAAAAGAAAABgAAAAAAAAAAAAIAAgAAAAIAAAACAAAAAAAAAAAABgAGAAAAAAAGAAAABgAGAAYAAAAGAAYAAAAAAAAAAAAGAAAABgAAAAYABgAAAAAAAAAGAAYAAAAGAAYABgAAAAYABgAAAAAAAAAAAAYACgAAAAYABgAAAAYAAAAGAAAAAAAGAAYAEgAAAAAABgAGAAAAAAAGAAoAAAAAAAYABgAGAAYAAAAGAAYAAAAGAAAABgAOAAAAAAAGAAAADgAAAAYABgAAAAAAAAAAAAYABgAGAAYAAAAHAAYABgAGAAYABgAGAAAAAAAAAAAABgAHAAAABgAAAAYABgAGAAAADgAGAAAAAAAAAAAABgAGAAYABgAAAAAAAAAAAAYABgAGAAYAAAAAAAAAAAACAAAAAAACAAIAAAACAAIAAgA== + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAIAAAACAAIAAgACAAAAAAAAAAAAAgACAAIAAgAAAAAAAAAAAAIAAgACAAIAAAAAAAAAAAACAAIAAAAAAAIAAAAAAAAAAAACAAIAAAACAAIAAAAAAAAAAAACAAIAAAACAAIAAAAAAAAAAAACAAIAAAAAAAIAAAAAAAAAAAACAAIAAgACAAAAAAAAAAAAAgACAAIAAgAAAAAAAAAAAAIAAgACAAIAAAAAAAAAAAACAAIAAgACAAAAAAAAAAAAAgACAAAAAAACAAAAAAAAAAAAAgACAAIAAgAAAAAAAAACAAAAAAACAAIAAAACAAIA= + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABgAKAAAAAAAAAAAABgAGAAYAAAAAAAAAAAAGAAYAAAAGAAAAAAAGAAcABgAGAAAAAAACAAIAAgAAAAAABgAGAAYAAAAAAAYABgAAAAAAAAAGAAYABgAAAAAAAAAAAAAAAAAAAAYABgAAAAIAAAAGAAYABgAGAAAABgAGAAAAAAAAAAAAAgACAAIAAgACAAIAAAAAAAAAAAACAAIAAgACAAAAAAAAAAAABgAGAAYABgAAAAAAAAAAAAYABgAGAAIABgA== + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABgAGAAYABgAGAAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAAAABgAGAAoABgAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABgAGAAYAAAAAAAAAAAAAAAAABgAGAAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAAAAAAAAAAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAAAAAAAAAAAABgAAAAAAAAAAAAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAAAAAAAAAAAAAAAAAAAABgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + + diff --git a/DataTest/Test2_UT.tmp.cov b/DataTest/Test2_UT.tmp.cov new file mode 100644 index 0000000..e1d3564 --- /dev/null +++ b/DataTest/Test2_UT.tmp.cov @@ -0,0 +1,27 @@ + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAAAAoACgAKAAoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABgAGAAYAAAAAAAAABgAGAAYAAAAAAAAABgAGAAYABgAGAAYAAAAAAAAABgAGAAYABgAGAAAAAAAAAAYABgAGAAYABgAAAAAAAAAGAAYABgAGAAYAAAAAAAAABgAGAAYABgAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgACAAIAAgACAAAAAAAAAAYABgAGAAYABgAAAAAAAAAGAAYABgAAAAAAAAAAAAYABgAGAAAAAAAAAAYABgAGAAAAAAAAAAAABgAGAAYAAAAAAAAAAAAGAAYABgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAAYABgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAAYABgAAAAAAAAAGAAYABgAAAAAAAAAGAAYABgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYABgAGAAAAAAAAAAYABgAGAAAAAAAAAAYABgAGAAAAAAAAAAAABgAGAAAAAAAGAAAAAAAAAAAABgAGAAYAAAAAAAAABgAGAAYAAAAAAAAABgAGAAYAAAAAAAAABgAGAAYABgAGAAYAAAAAAAAAAAAGAAYABgAAAAAABgAGAAAAAAAAAAAABgAGAAYAAAAAAAYABgAAAAAAAAAAAAYABgAGAAAAAAAAAAYAAAAAAAYAAAAAAAAAAAAGAAYABgAAAAAAAAAGAAAAAAAGAAAAAAAAAAAAAAAAAAYABgAGAAAAAAAAAAAAAAAGAAYAAAAAAAYAAAAAAAYABgAAAAAABgAAAAAAAAAGAAYAAAAAAAYAAAAAAAAAAAAGAAYABgAAAAAA= + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAgACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAAYABgACAAYAAAAAAAYABgACAAYAAAAAAAAAAAAAAAAAAAAGAAYAAAACAAIAAAAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYAAAAAAAAAAAAGAAYABgAAAAYAAAAAAAAAAAAfAB8AAAAPAAAAGwAXAB8AJwAnAAAAAAAAAAAABgAXAA8AFwAAAAAAAAAAAAYABwAGAAYAAAAAAAAAAAAPAAAAAAAAABMAAAAAAAAAFwAAAAAAAAAAAA8AEwAAABcAAAAAAAYAAAAAAAAAAAAGAAYABgAGAAAABgAGAAYAAAAGAAYABgAGAAAAAAAAAAAABgAGAAAABgAAAAYABgAGAAYABgAGAAAAAAAAAAAABgAAAAAAAAAAAAYABgACAAAABgAGAAYAAAAGAAYABgAAAAYABgAGAAAABgAAAAAAAAAGA + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAIAAgAAAAAAAAAAAAAAAAAAAAIAAgACAAAAAAAAAAAAAAAAAAAAAAACAAIAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAgACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA== + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAAcAAAAGAAYABgAGAAAAAAAAAAAABgAKAAYABgAAAAAAAAAAAAYACgAGAAYAAAAAAAAAAAACAAIAAAAAAAIAAAAAAAAAAAAGAAcAAAAGAAYAAAAAAAAAAAACAAIAAAACAAIAAAAAAAAAAAAGAAYAAAAAAAYAAAAAAAAAAAACAAIAAgACAAAAAAAAAAAAAgACAAIAAgAAAAAAAAAAAAIAAgACAAIAAAAAAAAAAAACAAIAAgACAAAAAAAAAAAABgAGAAAAAAAGAAAAAAAAAAAABgAGAAYABgAAAAAAAAAGAAAAAAAGAAYAAAAGAAYA= + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACgAKAAoACgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABgAGAAYAAAAAAAAABgAGAAYAAAAAAAAABgAGAAYABgAGAAYAAAAAAAAABgAGAAYABgAGAAAAAAAAAAYABgAGAAYABgAAAAAAAAAGAAYABgAGAAYAAAAAAAAABgAGAAYABgAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABgAGAAYABgAGAAAAAAAAAAYABgAGAAYABgAAAAAAAAAAAAYABgAGAAYABgAGAAAAAAAAAAYABgAGAAAAAAAAAAYABgAGAAAAAAAAAAAAAAAGAAYABgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAAYABgAAAAAABgAGAAYAAAAAAAAAAAAGAAYABgAAAAAAAAAAAAAABgAGAAAAAAAGAAAAAAAAAAYABgAGAAAAAAAAAAAAAAAGAAYABgAAAAAAAAAGAAYABgAAAAAABgAGAAYAAAAAAAYABgAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAAYABgAAAAAAAAAGAAYABgAAAAAAAAAGAAYABgAAAAAAAAAAAAAABgAGAAAABgAAAAYABgAAAAAABgAGAAAAAAAAAAAAAAAAAAAABgAGAAYAAAAAAAAABgAGAAYAAAAAAAAABgAGAAYAAAAAAAAABgAGAAYAAAAAAAAAAAAAAAAAAAAGAAYABgAAAAAAAAAAAAYABgAGAAAAAAAGAAYAAAAAAAAAAAAGAAYABgAAAAAABgAGAAAAAAAAAAAABgAGAAYAAAAAAAAABgAAAAAABgAAAAAAAAAAAAYABgAGAAAAAAAAAAYAAAAAAAYAAAAAAAAABgAGAAAAAAAGAAAAAAAAAAAABgAGAAAAAAAGAAAAAAAGAAYAAAAAAAYAAAAAAAAAAAAGAAYABgAAAAAAAAAAAAAAAAAAAAAABgAGAAAAAAAGAAAAAAAAAAAAAAAAAAAAAAAAA + +