diff --git a/cli/src/main/java/de/pfeufferweb/gitcover/GCOptions.java b/cli/src/main/java/de/pfeufferweb/gitcover/GCOptions.java index 341efa6..ac94f0e 100644 --- a/cli/src/main/java/de/pfeufferweb/gitcover/GCOptions.java +++ b/cli/src/main/java/de/pfeufferweb/gitcover/GCOptions.java @@ -16,8 +16,11 @@ class GCOptions private String ignoreFile = null; private String repository = null; private String reference = null; + private boolean failed = false; + private CoverageTool coverageTool = CoverageTool.COBERTURA; + public void parse(String[] args) { Options options = buildOptions(); @@ -54,6 +57,16 @@ private void parse(String[] args, Options options, CommandLineParser parser) thr printHelp(options); failed = true; } + if(line.hasOption("ct")) + { + switch (line.getOptionValue("ct")){ + case "cobertura": coverageTool = CoverageTool.COBERTURA; + break; + case "jacoco": coverageTool = CoverageTool.JACOCO; + break; + default: break; + } + } } @SuppressWarnings("static-access") @@ -66,6 +79,8 @@ private Options buildOptions() .withDescription("use this to ignore files that have been modified").create("em"); Option excludeAddedOption = OptionBuilder.withLongOpt("exclude-added") .withDescription("use this to ignore files that have been modified").create("ea"); + Option coverageToolOption = OptionBuilder.withLongOpt("coverage-tool") + .withDescription("use this to select between cobertura and jacoco").create("ct"); options.addOption(ignoreFileOption); options.addOption(excludeModifiedOption); options.addOption(excludeAddedOption); diff --git a/cli/src/main/java/de/pfeufferweb/gitcover/GitCover.java b/cli/src/main/java/de/pfeufferweb/gitcover/GitCover.java index e6c990b..9247116 100644 --- a/cli/src/main/java/de/pfeufferweb/gitcover/GitCover.java +++ b/cli/src/main/java/de/pfeufferweb/gitcover/GitCover.java @@ -76,13 +76,13 @@ private void process(GCOptions options) throws Exception out.println("a.ignored {background:#CDF;}"); out.println("a.allCovered {background:#CFC;}"); out.println("a.coverageMissing {background:#FDC;}"); - out.println("a.exp::after {content:\"»\";float:right;}"); + out.println("a.exp::after {content:\"�\";float:right;}"); out.println("a.exp:focus {border-width: 1px 1px 0 1px;border-radius:4px 4px 0 0}"); out.println("a.exp + div {display:none;}"); out.println("a.exp:focus + div {display:block;border-width: 0 1px 1px 1px;border-style:solid; border-radius:0 0 4px 4px;border-color:black;}"); out.println("a.exp:focus::after {content:\"\";}"); out.println("div.exp *{padding:0.3em 10px 0em 10px;}"); - out.println("div.exp table:last-child::after {content:\"«\";float:right;}"); + out.println("div.exp table:last-child::after {content:\"�\";float:right;}"); out.println("div.exp *:first-child {margin-top:0;}"); out.println("tr.notCovered {background: orangered;}"); out.println("tr.covered {background: lightgreen;}"); @@ -91,9 +91,9 @@ private void process(GCOptions options) throws Exception out.println(""); ChangedLines changedLines = createChangedLinesBuilder(options, options.getRepository()).build( options.getReference()); - Coverage coverage = new CoverageBuilder().computeAll(new File(options.getRepository())); + Coverage coverage = new CoberturaCoverageBuilder().computeAll(new File(options.getRepository())); out.println(""); - out.println("

Unittestabdeckung der Änderungen bzgl. Branch " + options.getReference() + "

"); + out.println("

Unittestabdeckung der �nderungen bzgl. Branch " + options.getReference() + "

"); List fileNames = new ArrayList(changedLines.getFileNames()); sort(fileNames); OverallCoverage overall = new OverallCoverage(); @@ -135,7 +135,7 @@ private void process(GCOptions options) throws Exception } out.println(""); } - out.println("Durchschnittliche Abdeckung aller testrelevanten Änderungen: " + overall.getCoverage() + "%"); + out.println("Durchschnittliche Abdeckung aller testrelevanten �nderungen: " + overall.getCoverage() + "%"); out.println(""); out.println(""); } diff --git a/core/src/main/java/de/pfeufferweb/gitcover/ChangedLines.java b/core/src/main/java/de/pfeufferweb/gitcover/ChangedLines.java new file mode 100644 index 0000000..fc343c0 --- /dev/null +++ b/core/src/main/java/de/pfeufferweb/gitcover/ChangedLines.java @@ -0,0 +1,33 @@ +package de.pfeufferweb.gitcover; + +import static java.util.Collections.unmodifiableCollection; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +public class ChangedLines +{ + private final Map> changedLines = new HashMap>(); + + public void addFile(String fileName, Map lines) + { + changedLines.put(fileName, new HashMap(lines)); + } + + public Collection getFileNames() + { + return unmodifiableCollection(changedLines.keySet()); + } + + public Collection getChangedLines(String changedFile) + { + return unmodifiableCollection(changedLines.get(changedFile).keySet()); + } + + public String getLine(String changedFile, int line) + { + return changedLines.get(changedFile).get(line); + } + +} diff --git a/core/src/main/java/de/pfeufferweb/gitcover/ChangedLinesBuilder.java b/core/src/main/java/de/pfeufferweb/gitcover/ChangedLinesBuilder.java new file mode 100644 index 0000000..04f9847 --- /dev/null +++ b/core/src/main/java/de/pfeufferweb/gitcover/ChangedLinesBuilder.java @@ -0,0 +1,125 @@ +package de.pfeufferweb.gitcover; + +import java.io.File; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.diff.DiffEntry; +import org.eclipse.jgit.diff.DiffEntry.ChangeType; +import org.eclipse.jgit.lib.AbbreviatedObjectId; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectLoader; +import org.eclipse.jgit.lib.ObjectReader; +import org.eclipse.jgit.lib.ObjectStream; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.RepositoryBuilder; +import org.eclipse.jgit.treewalk.CanonicalTreeParser; + +import difflib.Delta; +import difflib.DiffUtils; +import difflib.Patch; + +public class ChangedLinesBuilder +{ + private final Repository repository; + private boolean includeModified = true; + private boolean includeAdded = true; + + public ChangedLinesBuilder(String repoFolder) throws Exception + { + this.repository = new RepositoryBuilder().findGitDir(new File(repoFolder)).build(); + } + + public ChangedLines build(String revision) throws Exception + { + ChangedLines changedLines = new ChangedLines(); + Git git = new Git(repository); + + ObjectId headId = repository.resolve("HEAD^{tree}"); + ObjectId oldId = repository.resolve(revision + "^{tree}"); + + ObjectReader reader = repository.newObjectReader(); + + CanonicalTreeParser newTreeIter = new CanonicalTreeParser(); + newTreeIter.reset(reader, headId); + CanonicalTreeParser oldTreeIter = new CanonicalTreeParser(); + oldTreeIter.reset(reader, oldId); + + List diffs = git.diff().setNewTree(newTreeIter).setOldTree(oldTreeIter).call(); + + for (DiffEntry diff : diffs) + { + boolean isRelevantFile = diff.getNewPath().endsWith(".java"); + if (isRelevantFile) + { + if (includeModified && isModified(diff)) + { + Map lines = process(diff); + changedLines.addFile(diff.getNewPath(), lines); + } + else if (includeAdded && isAdded(diff)) + { + Map lines = createLines(load(diff.getNewId())); + changedLines.addFile(diff.getNewPath(), lines); + } + } + } + return changedLines; + } + + public void setIncludeModified(boolean includeModified) + { + this.includeModified = includeModified; + } + + public void setIncludeAdded(boolean includeAdded) + { + this.includeAdded = includeAdded; + } + + private boolean isModified(DiffEntry diff) + { + return diff.getChangeType() == ChangeType.MODIFY; + } + + private boolean isAdded(DiffEntry diff) + { + return diff.getChangeType() == ChangeType.ADD; + } + + private Map createLines(List content) + { + Map lines = new HashMap(); + for (int i = 0; i < content.size(); ++i) + { + lines.put(i + 1, content.get(i)); + } + return lines; + } + + private Map process(DiffEntry diff) throws Exception + { + Patch patch = DiffUtils.diff(load(diff.getOldId()), load(diff.getNewId())); + Map lines = new HashMap(); + for (Delta delta : patch.getDeltas()) + { + int initialPosition = delta.getRevised().getPosition(); + int diffLength = delta.getRevised().getLines().size(); + for (int i = 0; i < diffLength; ++i) + { + lines.put(initialPosition + i + 1, delta.getRevised().getLines().get(i).toString()); + } + } + return lines; + } + + private List load(AbbreviatedObjectId objectId) throws Exception + { + ObjectLoader loader = repository.open(objectId.toObjectId()); + + ObjectStream stream = loader.openStream(); + return new FileLoader(stream).load(); + } +} diff --git a/core/src/main/java/de/pfeufferweb/gitcover/CoberturaCoverageBuilder.java b/core/src/main/java/de/pfeufferweb/gitcover/CoberturaCoverageBuilder.java new file mode 100644 index 0000000..9686761 --- /dev/null +++ b/core/src/main/java/de/pfeufferweb/gitcover/CoberturaCoverageBuilder.java @@ -0,0 +1,108 @@ +package de.pfeufferweb.gitcover; + +import static java.lang.Integer.parseInt; + +import java.io.File; +import java.io.IOException; +import java.io.StringReader; +import java.util.Collection; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpression; +import javax.xml.xpath.XPathFactory; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.filefilter.IOFileFilter; +import org.w3c.dom.Document; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.NodeList; +import org.xml.sax.EntityResolver; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +public class CoberturaCoverageBuilder +{ + public Coverage computeAll(File directory) throws Exception + { + Coverage overallCoverage = new Coverage(); + Collection files = FileUtils.listFiles(directory, new IOFileFilter() + { + + @Override + public boolean accept(File dir, String name) + { + return false; + } + + @Override + public boolean accept(File file) + { + return file.getName().equals("coverage.xml"); + } + }, new IOFileFilter() + { + + @Override + public boolean accept(File dir, String name) + { + return false; + } + + @Override + public boolean accept(File file) + { + return true; + } + }); + for (File file : files) + { + overallCoverage.addAll(compute(file)); + } + return overallCoverage; + } + + public Coverage compute(File file) throws Exception + { + Coverage result = new Coverage(); + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = factory.newDocumentBuilder(); + builder.setEntityResolver(new EntityResolver() + { + @Override + public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException + { + if (systemId.contains("cobertura.sourceforge.net")) + { + return new InputSource(new StringReader("")); + } + else + { + return null; + } + } + }); + Document doc = builder.parse(file); + XPathFactory xPathfactory = XPathFactory.newInstance(); + XPath xpath = xPathfactory.newXPath(); + XPathExpression classExpr = xpath.compile("//class/@filename"); + NodeList foundFileNodes = (NodeList) classExpr.evaluate(doc, XPathConstants.NODESET); + for (int i = 0; i < foundFileNodes.getLength(); ++i) + { + String fileName = foundFileNodes.item(i).getNodeValue(); + result.addFile(fileName); + XPathExpression linesExpr = xpath.compile("//class[@filename='" + fileName + "']//line"); + NodeList foundLineNodes = (NodeList) linesExpr.evaluate(doc, XPathConstants.NODESET); + for (int j = 0; j < foundLineNodes.getLength(); ++j) + { + NamedNodeMap attributes = foundLineNodes.item(j).getAttributes(); + int line = parseInt(attributes.getNamedItem("number").getNodeValue()); + int hits = parseInt(attributes.getNamedItem("hits").getNodeValue()); + result.addLine(fileName, line, hits); + } + } + return result; + } +} diff --git a/core/src/main/java/de/pfeufferweb/gitcover/Coverage.java b/core/src/main/java/de/pfeufferweb/gitcover/Coverage.java new file mode 100644 index 0000000..fec814b --- /dev/null +++ b/core/src/main/java/de/pfeufferweb/gitcover/Coverage.java @@ -0,0 +1,49 @@ +package de.pfeufferweb.gitcover; + +import static java.util.Collections.unmodifiableCollection; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.NoSuchElementException; + +public class Coverage +{ + private final Map> coverage = new HashMap>(); + + void addFile(String fileName) + { + coverage.put(fileName, new HashMap()); + } + + void addLine(String fileName, int line, int hits) + { + coverage.get(fileName).put(line, hits); + } + + public void addAll(Coverage subCoverage) + { + this.coverage.putAll(subCoverage.coverage); + } + + /** + * @throws NoSuchElementException + * whenever there is no coverage for the given file. + */ + public Map getCoverage(String name) + { + for (String candidate : getFileNames()) + { + if (name.endsWith(candidate) || candidate.endsWith(name)) + { + return coverage.get(candidate); + } + } + throw new NoSuchElementException("no coverage found for file " + name); + } + + public Collection getFileNames() + { + return unmodifiableCollection(coverage.keySet()); + } +} diff --git a/core/src/main/java/de/pfeufferweb/gitcover/CoverageTool.java b/core/src/main/java/de/pfeufferweb/gitcover/CoverageTool.java new file mode 100644 index 0000000..bcbf0af --- /dev/null +++ b/core/src/main/java/de/pfeufferweb/gitcover/CoverageTool.java @@ -0,0 +1,9 @@ +package de.pfeufferweb.gitcover; + +/** + * + */ +public enum CoverageTool { + COBERTURA, + JACOCO +} diff --git a/core/src/main/java/de/pfeufferweb/gitcover/FileCoverage.java b/core/src/main/java/de/pfeufferweb/gitcover/FileCoverage.java new file mode 100644 index 0000000..ca9a828 --- /dev/null +++ b/core/src/main/java/de/pfeufferweb/gitcover/FileCoverage.java @@ -0,0 +1,55 @@ +package de.pfeufferweb.gitcover; + +import java.util.List; +import java.util.Map; + +class FileCoverage +{ + final int changesLines; + final int relevantLines; + final int coveredLines; + + private FileCoverage(int changesLines, int relevantLines, int coveredLines) + { + this.changesLines = changesLines; + this.relevantLines = relevantLines; + this.coveredLines = coveredLines; + } + + public static FileCoverage buildFrom(Map lineCoverage, List lines) + { + int changedLines = 0; + int coveredLines = 0; + int relevantLines = 0; + for (int line : lines) + { + ++changedLines; + if (lineCoverage.containsKey(line)) + { + ++relevantLines; + if (lineCoverage.get(line) > 0) + { + ++coveredLines; + } + } + } + return new FileCoverage(changedLines, relevantLines, coveredLines); + } + + boolean completelyCovered() + { + return changesLines == coveredLines; + } + + @Override + public String toString() + { + return changesLines + " Zeile" + (changesLines == 1 ? "" : "n") + " geändert, " + relevantLines + " Zeile" + + (relevantLines == 1 ? "" : "n") + " testrelevant" + ", Testabdeckung: " + getCoverage() + "%"; + } + + int getCoverage() + { + return relevantLines == 0 ? 100 : (100 * coveredLines / relevantLines); + } +} diff --git a/core/src/main/java/de/pfeufferweb/gitcover/FileLoader.java b/core/src/main/java/de/pfeufferweb/gitcover/FileLoader.java new file mode 100644 index 0000000..dfe626f --- /dev/null +++ b/core/src/main/java/de/pfeufferweb/gitcover/FileLoader.java @@ -0,0 +1,30 @@ +package de.pfeufferweb.gitcover; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.List; + +class FileLoader +{ + private final InputStream stream; + + public FileLoader(InputStream stream) + { + this.stream = stream; + } + + List load() throws IOException + { + BufferedReader in = new BufferedReader(new InputStreamReader(stream)); + String line; + List lines = new ArrayList(); + while ((line = in.readLine()) != null) + { + lines.add(line); + } + return lines; + } +} diff --git a/core/src/main/java/de/pfeufferweb/gitcover/JacocoCoverageBuilder.java b/core/src/main/java/de/pfeufferweb/gitcover/JacocoCoverageBuilder.java new file mode 100644 index 0000000..8c3460e --- /dev/null +++ b/core/src/main/java/de/pfeufferweb/gitcover/JacocoCoverageBuilder.java @@ -0,0 +1,107 @@ +package de.pfeufferweb.gitcover; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.filefilter.IOFileFilter; +import org.w3c.dom.Document; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.NodeList; +import org.xml.sax.EntityResolver; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpression; +import javax.xml.xpath.XPathFactory; +import java.io.File; +import java.io.IOException; +import java.io.StringReader; +import java.util.Collection; + +import static java.lang.Integer.parseInt; + +public class JacocoCoverageBuilder +{ + public Coverage computeAll(File directory) throws Exception + { + Coverage overallCoverage = new Coverage(); + Collection files = FileUtils.listFiles(directory, new IOFileFilter() + { + + @Override + public boolean accept(File dir, String name) + { + return false; + } + + @Override + public boolean accept(File file) + { + return file.getName().equals("jacoco.xml"); + } + }, new IOFileFilter() + { + + @Override + public boolean accept(File dir, String name) + { + return false; + } + + @Override + public boolean accept(File file) + { + return true; + } + }); + for (File file : files) + { + overallCoverage.addAll(compute(file)); + } + return overallCoverage; + } + + public Coverage compute(File file) throws Exception + { + Coverage result = new Coverage(); + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = factory.newDocumentBuilder(); + builder.setEntityResolver(new EntityResolver() + { + @Override + public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException + { + if (systemId.contains("JACOCO")) + { + return new InputSource(new StringReader("")); + } + else + { + return null; + } + } + }); + Document doc = builder.parse(file); + XPathFactory xPathfactory = XPathFactory.newInstance(); + XPath xpath = xPathfactory.newXPath(); + XPathExpression classExpr = xpath.compile("//sourcefile/@name"); + NodeList foundFileNodes = (NodeList) classExpr.evaluate(doc, XPathConstants.NODESET); + for (int i = 0; i < foundFileNodes.getLength(); ++i) + { + String fileName = foundFileNodes.item(i).getNodeValue(); + result.addFile(fileName); + XPathExpression linesExpr = xpath.compile("//sourcefile[@name='" + fileName + "']//line"); + NodeList foundLineNodes = (NodeList) linesExpr.evaluate(doc, XPathConstants.NODESET); + for (int j = 0; j < foundLineNodes.getLength(); ++j) + { + NamedNodeMap attributes = foundLineNodes.item(j).getAttributes(); + int line = parseInt(attributes.getNamedItem("nr").getNodeValue()); + int hits = parseInt(attributes.getNamedItem("ci").getNodeValue()); + result.addLine(fileName, line, hits); + } + } + return result; + } +} diff --git a/core/src/main/java/de/pfeufferweb/gitcover/OverallCoverage.java b/core/src/main/java/de/pfeufferweb/gitcover/OverallCoverage.java new file mode 100644 index 0000000..3eb7c56 --- /dev/null +++ b/core/src/main/java/de/pfeufferweb/gitcover/OverallCoverage.java @@ -0,0 +1,18 @@ +package de.pfeufferweb.gitcover; + +class OverallCoverage +{ + private int relevantLines = 0; + private int coveredLines = 0; + + void add(FileCoverage fileCoverage) + { + relevantLines += fileCoverage.relevantLines; + coveredLines += fileCoverage.coveredLines; + } + + public int getCoverage() + { + return relevantLines == 0 ? 100 : (100 * coveredLines / relevantLines); + } +}