102030405060708090100110120130140150160170180190201210220230240251261270281290301310320330340350360370380390400410420430440450460470480490500510520530540550560570580590600610620630640650660670680690700710720730740750760770780790800810820830840850860870880890900910920930940950960970980990100010101020103010401050106010701080109011001110112011301140115011601170118011901200121012201230124012501260127012801290130013101320133013401350136013701380139014001410142014301440145014601470148014901500151015201530154015501560157015801590160016101620163016401650166016701680169017001710172017301740175017601770178017901800181018201830184018501860187018801890190019101920193019401950196019701980199020002010202020302040205020602070208020902100211021202130214021502160217021802190220022102220223022402250226022702280229023002310232023302340235023602370238023902400241024202430244024502460247024802490250025102520253025402550256025702580259026002610262026302640265026602670268026902700271027202730274027502760277027802790280028102820283028402850286028702880289029002910292029302940295029602970298029903000301030203030304030503060307030803090310031103120313031403150316031703180319032003210322032303240325032603270328032903300331033203330334033503360337033803390340034103420343034403450346034703480349035003510352035303540355035603570358035903600361036203630364036503660367036803690370037103720373037403750376037703780379038003810382038303840385038603870388038903900391039203930394039503960397039803990400040104020403040404050406040704080409041004110412041304140415041604170418041904200421042204230424042504260427042804290430043104320433043404350436043704380439044004410442044304440 module trial.coverage; import std.algorithm; import std.range; import std.string; import std.stdio; import std.conv; import std.exception; import std.file; import std.path; import std.math; import trial.discovery.code; version(D_Coverage) { shared static this() { import core.runtime; if(exists("coverage")) { writeln("Creating coverage folder..."); rmdirRecurse("coverage"); } auto destination = buildPath("coverage", "raw").asAbsolutePath.array.idup.to!string; mkdirRecurse(destination); dmd_coverSetMerge(false); dmd_coverDestPath(destination); } } double convertLstFiles(string source, string destination, string packagePath, string packageName) { auto htmlPath = buildPath(destination, "html"); if(!source.exists) { return 0; } if(!htmlPath.exists) { htmlPath.mkdirRecurse; } std.file.write(buildPath(htmlPath, "coverage.css"), import("templates/coverage.css")); auto coverageData = dirEntries(buildPath("coverage", "raw"), SpanMode.shallow) .filter!(f => f.name.endsWith(".lst")) .filter!(f => f.isFile) .map!(a => readText(a.name)) .map!(a => a.toCoverageFile(packagePath)).array; std.file.write(buildPath(htmlPath, "coverage-shield.svg"), coverageShield(coverageData.filter!"a.isInCurrentProject".array.coveragePercent.to!int.to!string)); std.file.write(buildPath(htmlPath, "index.html"), coverageData.toHtmlIndex(packageName)); foreach (data; coverageData) { auto htmlFile = data.path.toCoverageHtmlFileName; std.file.write(buildPath(htmlPath, htmlFile), data.toHtml); } return coverageData.coveragePercent; } string toCoverageHtmlFileName(string fileName) { return fileName.replace("/", "-").replace("\\", "-") ~ ".html"; } auto getCoverageSummary(string fileContent) { auto lines = fileContent.splitLines.array; std.algorithm.reverse(lines); return lines .filter!(a => a.indexOf('|') == -1 || a.indexOf('|') > 9) .map!(a => a.strip) .filter!(a => a != ""); } string getFileName(string fileContent) { auto r = fileContent.getCoverageSummary; if(r.empty) { return ""; } auto pos = r.front.lastIndexOf(".d"); return r.front[0..pos + 2]; } double getCoveragePercent(string fileContent) { auto r = fileContent.getCoverageSummary; if(r.empty) { return 100; } auto pos = r.front.lastIndexOf('%'); if(pos == -1) { return 100; } auto pos2 = r.front[0..pos].lastIndexOf(' ') + 1; return r.front[pos2..pos].to!double; } struct LineCoverage { string code; size_t hits; bool hasCode; @disable this(); this(string line) { enforce(line.indexOf("\n") == -1, "You should provide a line"); line = line.strip; auto column = line.indexOf("|"); if(column == -1) { code = line; } else if(column == 0) { code = line[1..$]; } else { hits = line[0..column].strip.to!size_t; hasCode = true; code = line[column + 1..$]; } } } auto toCoverageLines(string fileContent) { return fileContent .splitLines .filter!(a => a.indexOf('|') != -1 && a.indexOf('|') < 10) .map!(a => a.strip) .map!(a => LineCoverage(a)); } struct CoveredFile { string path; bool isInCurrentProject; bool isIgnored; string moduleName; double coveragePercent; LineCoverage[] lines; } bool isIgnored(const string content) { auto firstLine = content.splitter('\n'); if(firstLine.empty) { return false; } auto smallCase = firstLine.front.strip.toLower; auto pieces = smallCase.replace("\t", " ").splitter(' ').filter!(a => a != "").array; if(pieces[0].indexOf("//") == -1 && pieces[0].indexOf("/*") == -1 && pieces[0].indexOf("/+") == -1) { return false; } if(pieces.length == 2) { return pieces[0].indexOf("ignore") != -1 && pieces[1] == "coverage"; } if(pieces.length < 3) { return false; } return pieces[1] == "ignore" && pieces[2] == "coverage"; } bool isPackagePath(string fullPath, string packagePath) { if(fullPath.indexOf("/.trial/") != -1) { return false; } if(fullPath.indexOf("trial_") != -1) { return false; } if(fullPath.indexOf("submodules") != -1) { return false; } if(fullPath.indexOf(packagePath) == 0) { return true; } if(fullPath.replace("\\", "/").indexOf(packagePath) == 0) { return true; } return false; } CoveredFile toCoverageFile(string content, string packagePath) { auto fileName = content.getFileName; auto fullPath = buildNormalizedPath(getcwd, fileName); return CoveredFile( fileName, fullPath.isPackagePath(packagePath), content.isIgnored(), getModuleName(fullPath), getCoveragePercent(content), content.toCoverageLines.array); } string toLineCoverage(T)(LineCoverage line, T index) { return import("templates/coverageColumn.html") .replaceVariable("hasCode", line.hasCode ? "has-code" : "") .replaceVariable("hit", line.hits > 0 ? "hit" : "") .replaceVariable("line", index.to!string) .replaceVariable("hitCount", line.hits.to!string); } string toHtmlCoverage(LineCoverage[] lines) { return lines.enumerate(1).map!(a => a[1].toLineCoverage(a[0])).array.join("").replace("\n", ""); } auto hitLines(LineCoverage[] lines) { return lines.filter!(a => a.hits > 0).array.length; } auto codeLines(LineCoverage[] lines) { return lines.filter!(a => a.hasCode).array.length; } string replaceVariable(const string page, const string key, const string value) pure { return page.replace("{"~key~"}", value); } string wrapToHtml(string content, string title) { return import("templates/page.html").replaceVariable("content", content).replaceVariable("title", title); } string htmlProgress(string percent) { return import("templates/progress.html").replaceVariable("percent", percent); } string coverageHeader(CoveredFile coveredFile) { return import("templates/coverageHeader.html") .replaceVariable("title", coveredFile.moduleName) .replaceVariable("hitLines", coveredFile.lines.hitLines.to!string) .replaceVariable("totalLines", coveredFile.lines.codeLines.to!string) .replaceVariable("coveragePercent", coveredFile.coveragePercent.to!string) .replaceVariable("pathPieces", pathSplitter(coveredFile.path).array.join(`</li><li>`)); } string toHtml(CoveredFile coveredFile) { return wrapToHtml( coverageHeader(coveredFile) ~ import("templates/coverageBody.html") .replaceVariable("lines", coveredFile.lines.toHtmlCoverage) .replaceVariable("code", coveredFile.lines.map!(a => a.code.replace("<", "<").replace(">", ">")).array.join("\n")), coveredFile.moduleName ~ " coverage" ); } string indexTable(string content) { return import("templates/indexTable.html").replaceVariable("content", content); } string ignoredTable(string content) { return import("templates/ignoredTable.html").replaceVariable("content", content); } double coveragePercent(CoveredFile[] coveredFiles) { if(coveredFiles.length == 0) { return 100; } double total = 0; double covered = 0; foreach(file; coveredFiles.filter!"a.isInCurrentProject".filter!"!a.isIgnored") { total += file.lines.map!(a => a.hasCode ? 1 : 0).sum; covered += file.lines.filter!(a => a.hasCode).map!(a => a.hits > 0 ? 1 : 0).sum; } if(total == 0) { return 100; } return round((covered / total) * 10000) / 100; } string toHtmlIndex(CoveredFile[] coveredFiles, string name) { sort!("toUpper(a.path) < toUpper(b.path)", SwapStrategy.stable)(coveredFiles); string content; string table; size_t totalHitLines; size_t totalLines; size_t ignoredLines; int count; foreach(file; coveredFiles.filter!"a.isInCurrentProject".filter!"!a.isIgnored") { auto currentHitLines = file.lines.hitLines; auto currentTotalLines = file.lines.codeLines; table ~= `<tr> <td><a href="` ~ file.path.toCoverageHtmlFileName ~ `">` ~ file.path ~ `</a></td> <td>` ~ file.moduleName ~ `</td> <td>` ~ file.lines.hitLines.to!string ~ `/` ~ currentTotalLines.to!string ~ `</td> <td>` ~ file.coveragePercent.to!string.htmlProgress ~ `</td> </tr>`; totalHitLines += currentHitLines; totalLines += currentTotalLines; count++; } table ~= `<tr> <th colspan="2">Total</td> <th>` ~ totalHitLines.to!string ~ `/` ~ totalLines.to!string ~ `</td> <th>` ~ coveredFiles.coveragePercent.to!string.htmlProgress ~ `</td> </tr>`; content ~= indexHeader(name) ~ table.indexTable; table = ""; foreach(file; coveredFiles.filter!"a.isInCurrentProject".filter!"a.isIgnored") { auto currentTotalLines = file.lines.codeLines; table ~= `<tr> <td><a href="` ~ file.path.toCoverageHtmlFileName ~ `">` ~ file.path ~ `</a></td> <td>` ~ file.moduleName ~ `</td> <td>` ~ currentTotalLines.to!string ~ `/` ~ totalLines.to!string ~ `</td> </tr>`; ignoredLines += currentTotalLines; count++; } table ~= `<tr> <th colspan="2">Total</td> <th>` ~ ignoredLines.to!string ~ `/` ~ totalLines.to!string ~ `</td> </tr>`; content ~= `<h1>Ignored</h1>` ~ table.ignoredTable; table = ""; foreach(file; coveredFiles.filter!"!a.isInCurrentProject") { table ~= `<tr> <td><a href="` ~ file.path.toCoverageHtmlFileName ~ `">` ~ file.path ~ `</a></td> <td>` ~ file.moduleName ~ `</td> <td>` ~ file.lines.hitLines.to!string ~ `/` ~ file.lines.codeLines.to!string ~ `</td> <td>` ~ file.coveragePercent.to!string.htmlProgress ~ `</td> </tr>`; } content ~= `<h1>Dependencies</h1>` ~ table.indexTable; content = `<div class="container">` ~ content ~ `</div>`; return wrapToHtml(content, "Code Coverage report"); } string indexHeader(string name) { return `<h1>` ~ name ~ ` <img src="coverage-shield.svg"></h1>`; } string coverageShield(string percent) { return import("templates/coverage.svg").replace("?%", percent ~ "%"); }
module trial.coverage; import std.algorithm; import std.range; import std.string; import std.stdio; import std.conv; import std.exception; import std.file; import std.path; import std.math; import trial.discovery.code; version(D_Coverage) { shared static this() { import core.runtime; if(exists("coverage")) { writeln("Creating coverage folder..."); rmdirRecurse("coverage"); } auto destination = buildPath("coverage", "raw").asAbsolutePath.array.idup.to!string; mkdirRecurse(destination); dmd_coverSetMerge(false); dmd_coverDestPath(destination); } } double convertLstFiles(string source, string destination, string packagePath, string packageName) { auto htmlPath = buildPath(destination, "html"); if(!source.exists) { return 0; } if(!htmlPath.exists) { htmlPath.mkdirRecurse; } std.file.write(buildPath(htmlPath, "coverage.css"), import("templates/coverage.css")); auto coverageData = dirEntries(buildPath("coverage", "raw"), SpanMode.shallow) .filter!(f => f.name.endsWith(".lst")) .filter!(f => f.isFile) .map!(a => readText(a.name)) .map!(a => a.toCoverageFile(packagePath)).array; std.file.write(buildPath(htmlPath, "coverage-shield.svg"), coverageShield(coverageData.filter!"a.isInCurrentProject".array.coveragePercent.to!int.to!string)); std.file.write(buildPath(htmlPath, "index.html"), coverageData.toHtmlIndex(packageName)); foreach (data; coverageData) { auto htmlFile = data.path.toCoverageHtmlFileName; std.file.write(buildPath(htmlPath, htmlFile), data.toHtml); } return coverageData.coveragePercent; } string toCoverageHtmlFileName(string fileName) { return fileName.replace("/", "-").replace("\\", "-") ~ ".html"; } auto getCoverageSummary(string fileContent) { auto lines = fileContent.splitLines.array; std.algorithm.reverse(lines); return lines .filter!(a => a.indexOf('|') == -1 || a.indexOf('|') > 9) .map!(a => a.strip) .filter!(a => a != ""); } string getFileName(string fileContent) { auto r = fileContent.getCoverageSummary; if(r.empty) { return ""; } auto pos = r.front.lastIndexOf(".d"); return r.front[0..pos + 2]; } double getCoveragePercent(string fileContent) { auto r = fileContent.getCoverageSummary; if(r.empty) { return 100; } auto pos = r.front.lastIndexOf('%'); if(pos == -1) { return 100; } auto pos2 = r.front[0..pos].lastIndexOf(' ') + 1; return r.front[pos2..pos].to!double; } struct LineCoverage { string code; size_t hits; bool hasCode; @disable this(); this(string line) { enforce(line.indexOf("\n") == -1, "You should provide a line"); line = line.strip; auto column = line.indexOf("|"); if(column == -1) { code = line; } else if(column == 0) { code = line[1..$]; } else { hits = line[0..column].strip.to!size_t; hasCode = true; code = line[column + 1..$]; } } } auto toCoverageLines(string fileContent) { return fileContent .splitLines .filter!(a => a.indexOf('|') != -1 && a.indexOf('|') < 10) .map!(a => a.strip) .map!(a => LineCoverage(a)); } struct CoveredFile { string path; bool isInCurrentProject; bool isIgnored; string moduleName; double coveragePercent; LineCoverage[] lines; } bool isIgnored(const string content) { auto firstLine = content.splitter('\n'); if(firstLine.empty) { return false; } auto smallCase = firstLine.front.strip.toLower; auto pieces = smallCase.replace("\t", " ").splitter(' ').filter!(a => a != "").array; if(pieces[0].indexOf("//") == -1 && pieces[0].indexOf("/*") == -1 && pieces[0].indexOf("/+") == -1) { return false; } if(pieces.length == 2) { return pieces[0].indexOf("ignore") != -1 && pieces[1] == "coverage"; } if(pieces.length < 3) { return false; } return pieces[1] == "ignore" && pieces[2] == "coverage"; } bool isPackagePath(string fullPath, string packagePath) { if(fullPath.indexOf("/.trial/") != -1) { return false; } if(fullPath.indexOf("trial_") != -1) { return false; } if(fullPath.indexOf("submodules") != -1) { return false; } if(fullPath.indexOf(packagePath) == 0) { return true; } if(fullPath.replace("\\", "/").indexOf(packagePath) == 0) { return true; } return false; } CoveredFile toCoverageFile(string content, string packagePath) { auto fileName = content.getFileName; auto fullPath = buildNormalizedPath(getcwd, fileName); return CoveredFile( fileName, fullPath.isPackagePath(packagePath), content.isIgnored(), getModuleName(fullPath), getCoveragePercent(content), content.toCoverageLines.array); } string toLineCoverage(T)(LineCoverage line, T index) { return import("templates/coverageColumn.html") .replaceVariable("hasCode", line.hasCode ? "has-code" : "") .replaceVariable("hit", line.hits > 0 ? "hit" : "") .replaceVariable("line", index.to!string) .replaceVariable("hitCount", line.hits.to!string); } string toHtmlCoverage(LineCoverage[] lines) { return lines.enumerate(1).map!(a => a[1].toLineCoverage(a[0])).array.join("").replace("\n", ""); } auto hitLines(LineCoverage[] lines) { return lines.filter!(a => a.hits > 0).array.length; } auto codeLines(LineCoverage[] lines) { return lines.filter!(a => a.hasCode).array.length; } string replaceVariable(const string page, const string key, const string value) pure { return page.replace("{"~key~"}", value); } string wrapToHtml(string content, string title) { return import("templates/page.html").replaceVariable("content", content).replaceVariable("title", title); } string htmlProgress(string percent) { return import("templates/progress.html").replaceVariable("percent", percent); } string coverageHeader(CoveredFile coveredFile) { return import("templates/coverageHeader.html") .replaceVariable("title", coveredFile.moduleName) .replaceVariable("hitLines", coveredFile.lines.hitLines.to!string) .replaceVariable("totalLines", coveredFile.lines.codeLines.to!string) .replaceVariable("coveragePercent", coveredFile.coveragePercent.to!string) .replaceVariable("pathPieces", pathSplitter(coveredFile.path).array.join(`</li><li>`)); } string toHtml(CoveredFile coveredFile) { return wrapToHtml( coverageHeader(coveredFile) ~ import("templates/coverageBody.html") .replaceVariable("lines", coveredFile.lines.toHtmlCoverage) .replaceVariable("code", coveredFile.lines.map!(a => a.code.replace("<", "<").replace(">", ">")).array.join("\n")), coveredFile.moduleName ~ " coverage" ); } string indexTable(string content) { return import("templates/indexTable.html").replaceVariable("content", content); } string ignoredTable(string content) { return import("templates/ignoredTable.html").replaceVariable("content", content); } double coveragePercent(CoveredFile[] coveredFiles) { if(coveredFiles.length == 0) { return 100; } double total = 0; double covered = 0; foreach(file; coveredFiles.filter!"a.isInCurrentProject".filter!"!a.isIgnored") { total += file.lines.map!(a => a.hasCode ? 1 : 0).sum; covered += file.lines.filter!(a => a.hasCode).map!(a => a.hits > 0 ? 1 : 0).sum; } if(total == 0) { return 100; } return round((covered / total) * 10000) / 100; } string toHtmlIndex(CoveredFile[] coveredFiles, string name) { sort!("toUpper(a.path) < toUpper(b.path)", SwapStrategy.stable)(coveredFiles); string content; string table; size_t totalHitLines; size_t totalLines; size_t ignoredLines; int count; foreach(file; coveredFiles.filter!"a.isInCurrentProject".filter!"!a.isIgnored") { auto currentHitLines = file.lines.hitLines; auto currentTotalLines = file.lines.codeLines; table ~= `<tr> <td><a href="` ~ file.path.toCoverageHtmlFileName ~ `">` ~ file.path ~ `</a></td> <td>` ~ file.moduleName ~ `</td> <td>` ~ file.lines.hitLines.to!string ~ `/` ~ currentTotalLines.to!string ~ `</td> <td>` ~ file.coveragePercent.to!string.htmlProgress ~ `</td> </tr>`; totalHitLines += currentHitLines; totalLines += currentTotalLines; count++; } table ~= `<tr> <th colspan="2">Total</td> <th>` ~ totalHitLines.to!string ~ `/` ~ totalLines.to!string ~ `</td> <th>` ~ coveredFiles.coveragePercent.to!string.htmlProgress ~ `</td> </tr>`; content ~= indexHeader(name) ~ table.indexTable; table = ""; foreach(file; coveredFiles.filter!"a.isInCurrentProject".filter!"a.isIgnored") { auto currentTotalLines = file.lines.codeLines; table ~= `<tr> <td><a href="` ~ file.path.toCoverageHtmlFileName ~ `">` ~ file.path ~ `</a></td> <td>` ~ file.moduleName ~ `</td> <td>` ~ currentTotalLines.to!string ~ `/` ~ totalLines.to!string ~ `</td> </tr>`; ignoredLines += currentTotalLines; count++; } table ~= `<tr> <th colspan="2">Total</td> <th>` ~ ignoredLines.to!string ~ `/` ~ totalLines.to!string ~ `</td> </tr>`; content ~= `<h1>Ignored</h1>` ~ table.ignoredTable; table = ""; foreach(file; coveredFiles.filter!"!a.isInCurrentProject") { table ~= `<tr> <td><a href="` ~ file.path.toCoverageHtmlFileName ~ `">` ~ file.path ~ `</a></td> <td>` ~ file.moduleName ~ `</td> <td>` ~ file.lines.hitLines.to!string ~ `/` ~ file.lines.codeLines.to!string ~ `</td> <td>` ~ file.coveragePercent.to!string.htmlProgress ~ `</td> </tr>`; } content ~= `<h1>Dependencies</h1>` ~ table.indexTable; content = `<div class="container">` ~ content ~ `</div>`; return wrapToHtml(content, "Code Coverage report"); } string indexHeader(string name) { return `<h1>` ~ name ~ ` <img src="coverage-shield.svg"></h1>`; } string coverageShield(string percent) { return import("templates/coverage.svg").replace("?%", percent ~ "%"); }