/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright 1997-2010 Oracle and/or its affiliates. All rights reserved.
*
* Oracle and Java are registered trademarks of Oracle and/or its affiliates.
* Other names may be trademarks of their respective owners.
*
* The contents of this file are subject to the terms of either the GNU
* General Public License Version 2 only ("GPL") or the Common
* Development and Distribution License("CDDL") (collectively, the
* "License"). You may not use this file except in compliance with the
* License. You can obtain a copy of the License at
* http://www.netbeans.org/cddl-gplv2.html
* or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
* specific language governing permissions and limitations under the
* License. When distributing the software, include this License Header
* Notice in each file and include the License file at
* nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the GPL Version 2 section of the License file that
* accompanied this code. If applicable, add the following below the
* License Header, with the fields enclosed by brackets [] replaced by
* your own identifying information:
* "Portions Copyrighted [year] [name of copyright owner]"
*
* Contributor(s):
*
* The Original Software is NetBeans. The Initial Developer of the Original
* Software is Sun Microsystems, Inc. Portions Copyright 1997-2008 Sun
* Microsystems, Inc. All Rights Reserved.
*
* If you wish your version of this file to be governed by only the CDDL
* or only the GPL Version 2, indicate your decision by adding
* "[Contributor] elects to include this software in this distribution
* under the [CDDL or GPL Version 2] license." If you do not indicate a
* single choice of license, a recipient has the option to distribute
* your version of this file under either the CDDL, the GPL Version 2 or
* to extend the choice of license to its licensees as provided above.
* However, if you add GPL Version 2 code and therefore, elected the GPL
* Version 2 license, then the option applies only if the new code is
* made subject to such option by the copyright holder.
*/
package org.netbeans.modules.ruby.codecoverage;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.Action;
import javax.swing.text.Document;
import org.netbeans.api.extexecution.print.ConvertedLine;
import org.netbeans.api.extexecution.print.LineConvertor;
import org.netbeans.api.project.Project;
import org.netbeans.api.project.SourceGroup;
import org.netbeans.api.project.Sources;
import org.netbeans.api.ruby.platform.RubyInstallation;
import org.netbeans.api.ruby.platform.RubyPlatform;
import org.netbeans.modules.gsf.codecoverage.api.CoverageActionFactory;
import org.netbeans.modules.gsf.codecoverage.api.CoverageManager;
import org.netbeans.modules.gsf.codecoverage.api.CoverageProvider;
import org.netbeans.modules.gsf.codecoverage.api.CoverageProviderHelper;
import org.netbeans.modules.gsf.codecoverage.api.CoverageType;
import org.netbeans.modules.gsf.codecoverage.api.FileCoverageDetails;
import org.netbeans.modules.gsf.codecoverage.api.FileCoverageSummary;
import org.netbeans.modules.ruby.platform.execution.RubyExecutionDescriptor;
import org.netbeans.modules.ruby.platform.gems.GemManager;
import org.netbeans.modules.ruby.spi.project.support.rake.PropertyEvaluator;
import org.openide.DialogDisplayer;
import org.openide.NotifyDescriptor;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileUtil;
import org.openide.modules.InstalledFileLocator;
import org.openide.util.Exceptions;
import org.openide.util.NbBundle;
import org.openide.util.Utilities;
/**
* Code coverage for Ruby, built on top of RCov.
*
* @author Tor Norbye
*/
public final class RubyCoverageProvider implements CoverageProvider {
/**
* Specifies the name of the action that invoking 'Test All' in the code coverage
* bar should run.
*/
private static final String CODE_COVERAGE_TEST_ACTION = "code.coverage.test.action"; //NOI18N
private Map<String, String> hitCounts;
private Map<String, String> fullNames;
private long timestamp;
private Set<String> mimeTypes;
private Project project;
private Boolean enabled;
private Boolean aggregating;
public RubyCoverageProvider(Project project) {
this.project = project;
mimeTypes = new HashSet<String>();
mimeTypes.add(RubyInstallation.RUBY_MIME_TYPE);
// Find out if RCov supports RHTML;
//mimeTypes.add(RubyInstallation.RHTML_MIME_TYPE);
}
public static RubyCoverageProvider get(Project project) {
return project.getLookup().lookup(RubyCoverageProvider.class);
}
public boolean supportsHitCounts() {
return true;
}
public boolean supportsAggregation() {
return true;
}
public synchronized boolean isAggregating() {
if (aggregating == null) {
aggregating = CoverageProviderHelper.isAggregating(project);
}
return aggregating;
}
public synchronized void setAggregating(boolean on) {
if (aggregating != null && on == isAggregating()) {
return;
}
aggregating = on;
CoverageProviderHelper.setAggregating(project, on);
}
public synchronized boolean isEnabled() {
if (enabled == null) {
enabled = CoverageProviderHelper.isEnabled(project);
}
return enabled;
}
public synchronized void setEnabled(boolean on) {
if (enabled != null && on == isEnabled()) {
return;
}
timestamp = 0;
if (on) {
GemManager gemManager = RubyPlatform.gemManagerFor(project);
if (gemManager == null || !gemManager.isGemInstalled("rcov")) {
NotifyDescriptor nd =
new NotifyDescriptor.Message(NbBundle.getMessage(RubyCoverageProvider.class, "RcovNotInstalled"),
NotifyDescriptor.Message.ERROR_MESSAGE);
DialogDisplayer.getDefault().notify(nd);
return;
}
} else {
hitCounts = null;
fullNames = null;
}
enabled = on;
CoverageProviderHelper.setEnabled(project, on);
}
private static List<LineCount> getLineCounts(String lines) {
int size = lines.length() / 5;
List<LineCount> lineCounts = new ArrayList<LineCount>(size);
int start = 0;
int i = start;
int length = lines.length();
int line = 0;
int startLine = -1;
while (i < length) {
char c = lines.charAt(i);
if (c == ':') {
line = Integer.valueOf(lines.substring(start, i));
start = i + 1;
} else if (c == ',') {
int count = Integer.valueOf(lines.substring(start, i));
if (startLine != -1 && startLine < line) {
for (int l = startLine; l <= line; l++) {
lineCounts.add(new LineCount(l, count));
}
startLine = -1;
} else {
lineCounts.add(new LineCount(line, count));
}
start = i + 1;
} else if (c == ' ') {
start = i + 1;
} else if (c == '>') {
startLine = Integer.valueOf(lines.substring(start, i));
start = i + 1;
}
i++;
}
return lineCounts;
}
private static FileCoverageSummary createSummary(Project project, String fileName, List<LineCount> counts) {
// Compute coverage:
int lineCount = 0;
int notExecuted = 0;
int partialCount = 0;
int inferredCount = 0;
for (LineCount lc : counts) {
if (lc.lineno > lineCount) {
lineCount = lc.lineno;
}
if (lc.count == -2) {
notExecuted++;
} else if (lc.count == -1) {
inferredCount++;
}
}
int executedCount = lineCount - notExecuted;
FileObject file;
File f = new File(fileName);
if (f.exists()) {
file = FileUtil.toFileObject(f);
} else {
file = project.getProjectDirectory().getFileObject(fileName.replace('\\', '/'));
}
if (file == null) {
Sources sources = project.getLookup().lookup(Sources.class);
if (sources != null) {
// From RubyBaseProject, downstream
String SOURCES_TYPE_RUBY = "ruby"; // NOI18N
for (SourceGroup sg : sources.getSourceGroups(SOURCES_TYPE_RUBY)) { // NOI18N
FileObject root = sg.getRootFolder();
if (fileName.indexOf('\\') != -1) {
file = root.getFileObject(fileName.replace("\\", "/")); // NOI18N
} else {
file = root.getFileObject(fileName);
}
if (file != null) {
break;
}
}
}
}
FileCoverageSummary result = new FileCoverageSummary(file, fileName, lineCount, executedCount,
inferredCount, partialCount);
return result;
}
public synchronized List<FileCoverageSummary> getResults() {
List<FileCoverageSummary> results = new ArrayList<FileCoverageSummary>();
update();
if (hitCounts == null) {
return null;
}
for (Map.Entry<String, String> entry : hitCounts.entrySet()) {
String fileName = entry.getKey();
if ("fcntl".equals(fileName)) { // NOI18N
// Excluding this at the rcov level doesn't seem to work
continue;
}
List<LineCount> counts = getLineCounts(entry.getValue());
FileCoverageSummary result = createSummary(project, fileName, counts);
results.add(result);
}
return results;
}
public static Action createCoverageAction(Project project) {
InstallRCovAction installRCovAction = new InstallRCovAction(project);
if (!installRCovAction.isEnabled()) {
installRCovAction = null;
}
// PENDING:
// Add other actions here? For example, checkbox to control whether we also show the
// HTML report after running it; include callsites in the report;
// include bogo profiling data; do coverage diff, etc.
return CoverageActionFactory.createCollectorAction(installRCovAction, null);
}
public synchronized void clear() {
File file = getRubyCoverageFile();
if (file.exists()) {
file.delete();
}
file = getNbCoverageFile();
if (file.exists()) {
file.delete();
}
hitCounts = null;
fullNames = null;
timestamp = 0;
}
public synchronized FileCoverageDetails getDetails(FileObject fo, Document doc) {
update();
if (hitCounts == null) {
return null;
}
String path = FileUtil.toFile(fo).getPath();
if (path == null) {
return null;
}
String lines = hitCounts.get(path);
if (lines == null) {
// Happens on some case insensitive file systems
lines = hitCounts.get(path.toLowerCase());
}
if (lines == null) {
String name = fo.getNameExt();
String fullName = fullNames.get(name.toLowerCase());
if (fullName != null && !fullName.equalsIgnoreCase(path)) {
lines = hitCounts.get(fullName);
}
}
if (lines != null) {
List<LineCount> hits = getLineCounts(lines);
int max = 0;
for (LineCount lineCount : hits) {
if (lineCount.lineno > max) {
max = lineCount.lineno;
}
}
int[] result = new int[max + 1];
for (int i = 0; i < max + 1; i++) {
result[i] = -1;
}
for (LineCount lineCount : hits) {
assert lineCount.lineno >= 0;
result[lineCount.lineno] = lineCount.count;
}
return new RubyFileCoverageDetails(fo, result, project, path, hits, timestamp);
}
return null;
}
public Set<String> getMimeTypes() {
return mimeTypes;
}
private File getNbCoverageDir() {
return new File(FileUtil.toFile(project.getProjectDirectory().getFileObject("nbproject")), "private" + File.separator + "coverage"); // NOI18N
}
private File getNbCoverageFile() {
return new File(getNbCoverageDir(), ".nbcoverage"); // NOI18N
}
private File getRubyCoverageFile() {
return new File(getNbCoverageDir(), ".coverage"); // NOI18N
}
public synchronized void notifyProjectOpened() {
CoverageManager.INSTANCE.setEnabled(project, true);
}
public void setAvailable(boolean b) {
// TBD - instead of enabling, should I provide some other way to trigger a version check?
CoverageManager.INSTANCE.setEnabled(project, true);
}
private synchronized void update() {
File rubyCoverage = getRubyCoverageFile();
if (!rubyCoverage.exists()) {
// No recorded data! Done.
return;
}
File nbCoverage = getNbCoverageFile();
// Read & Parse the corresponding data structure into memory
if (nbCoverage.exists() && timestamp < nbCoverage.lastModified()) {
timestamp = nbCoverage.lastModified();
hitCounts = new HashMap<String, String>();
fullNames = new HashMap<String, String>();
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader(nbCoverage));
while (true) {
try {
String file = br.readLine();
String lines = br.readLine();
if (file == null || lines == null) {
break;
}
int last = Math.max(file.lastIndexOf('\\'), file.lastIndexOf('/'));
String base = file;
if (last != -1) {
base = file.substring(last + 1);
}
fullNames.put(base.toLowerCase(), file);
hitCounts.put(file, lines);
} catch (IOException ex) {
Exceptions.printStackTrace(ex);
}
}
} catch (FileNotFoundException ex) {
Exceptions.printStackTrace(ex);
} finally {
if (br != null) {
try {
br.close();
} catch (IOException ex) {
Exceptions.printStackTrace(ex);
}
}
}
}
}
/**
* Wrap the given RubyExecutionDescriptor with a new descriptor that implements code coverage.
* What this will essentially do is create a new descriptor which delegates to rcov to run the original
* Ruby program, and also registers a post-execution hook which will refresh the coverage data
* and then delegate to the original post execution hook.
*
* Unfortunately, it's not as simple as that ;-). First, rcov produces binary data we can't read
* (it's just Marshal.dump'ed binary Ruby data structures). Rather than try to read this data directly
* (which could be error prone, especially in the presence of different versions of interpreters),
* I therefore run the "rcov_wrapper.rb" script, which registers its own shutdown hook with Ruby,
* and then delegates to rcov. In the shutdown hook, run after rcov is finished, it Marshal.load's
* the data back and then writes the data in a plain ASCII format NetBeans can read directly.
* (It would be nice if this was tied more closely into RCov's dumper routines.)
*
* Second, this approach really only works when we're launching Ruby programs directly (e.g. running
* the user's program, or running a file, or running the Rails server, etc.)
*
* If we run Rake, we will only record coverage for the Rake program (and the Rakefile) itself.
* Rake will launch external commands, which we wouldn'be be recording because rake runs Ruby
* directly, not under RCov control. So what we really have to do is trick Rake into delegating
* for us. This is done by the rake_wrapper.rb script. We preload it into rake (-r), and what the
* script does is rewrite the Kernel.system() call. When Rake tries to launch a command, it wil
* eventually wind up calling Kernel.system(), and our customized routine will take apart its arguments,
* insert rcov in the middle as appropriate, and then call the real (aliased) Kernel.system.
* Since this can end up calling many ruby processes, we have to aggregate the data across these
* runs (with rcov --aggrevate) even if the user doesn't want to aggregate data from previous
* runs.
*
* There is one final wrinkle: When you are executing tests under the NetBeans test runner,
* rake is ALREADY running with preloaded routines (nb_test_mediator.rb, nb_test_runner.rb, etc.).
* These don't happily coexist with the rake_wrapper. So in this case we remove this pre-existing
* script from the command line, and instead add it to an environment variable that the rake_wrapper
* will read and delegate to at the end.
*
* Finally, we also add output converters which try to remove from the output any stacktrace lines
* referring to the wrapper scripts, to make test output etc. less confusing for the user.
*
* @param original The original execution descriptor that we want to add code coverage for
* @param isRake Set to true if we are launching rake in this descriptor. Special rules apply.
* @param includeName If non null, tell rcov to explicitly include this file name. This is used to
* override the usual file exclusion rules (which for example excludes test cases by default).
* @return A new RubyExecutionDescriptor which performs the same task as the original parameter,
* but records code coverage as well.
*/
public RubyExecutionDescriptor wrapWithCoverage(final RubyExecutionDescriptor original, boolean isRake, String includeName) {
RubyPlatform platform = original.getPlatform();
File rcov = new File(platform.getInterpreterFile().getParentFile(), "rcov"); // NOI18N
if (!rcov.exists()) {
// Ah, Windows. Goodie.
rcov = new File(platform.getInterpreterFile().getParentFile(), "rcov.bat"); // NOI18N
if (!rcov.exists()) {
rcov = new File(platform.getInterpreterFile().getParentFile(), "rcov.cmd"); // NOI18N
if (!rcov.exists()) {
Logger.getLogger(RubyCoverageProvider.class.getName()).log(Level.WARNING, "Warning: RCov not found at " + rcov.getPath());
return original;
}
}
}
File dir = getNbCoverageDir();
if (!dir.exists()) {
dir.mkdirs();
}
InstalledFileLocator locator = InstalledFileLocator.getDefault();
File script = locator.locate("coverage/rcov_wrapper.rb", "org-netbeans-modules-ruby-codecoverage.jar", false); // NOI18N
assert script != null;
String target = script.getPath();
List<String> args = new ArrayList<String>(20);
File nbCoverage = getNbCoverageFile();
File rubyCoverage = getRubyCoverageFile();
args.add(rubyCoverage.getPath());
args.add(nbCoverage.getPath());
boolean isAggregating = isAggregating();
Map<String, String> additionalEnv = original.getAdditionalEnvironment();
if (!isRake) {
args.add(rcov.getPath());
buildRcovArgs(null, args, isAggregating, includeName, original);
}
// Original interpreter and args etc
args.add(original.getScript());
String initial = original.getInitialArgsPlain();
if (isRake) {
// Copy existing env instead of just adding to it in case it's immutable (such as Collections.emptyMap())
additionalEnv = new HashMap<String, String>(additionalEnv);
File rakeWrapper = locator.locate("coverage/rake_wrapper.rb", "org-netbeans-modules-ruby-codecoverage.jar", false);
assert rakeWrapper != null;
additionalEnv.put("NB_RAKE_WRAPPER", rakeWrapper.getPath()); // NOI18N
additionalEnv.put("NB_RCOV_PATH", rcov.getPath()); // NOI18N
if (initial != null) {
String[] ia = Utilities.parseParameters(initial);
if (ia.length == 2 && "-r".equals(ia[0])) { // NOI18N
additionalEnv.put("NB_DELEGATED_SCRIPT", ia[1]); // NOI18N
initial = "-r \"" + rakeWrapper.getPath() + "\""; // NOI18N
}
}
// Rake can call rcov several times - for example once for each unit / functional / fixture test
// and we want to aggregate these as a single run. Thus, we have to manually simulate the aggregation
// here and always pass aggregate=true on to rake for a single run.
if (!isAggregating) {
clear();
}
//Utilities.parseParameters(target)
StringBuilder rcovArgs = new StringBuilder();
buildRcovArgs(rcovArgs, null, true, includeName, original);
additionalEnv.put("NB_RCOV_ARGS", rcovArgs.toString()); // NOI18N
// Determine if it's the test runner... if so, I have to leave everything alone
// (because it doesn't work to have two -r commands - so instead I've modified
// the nb_testrunner to delegate to the rake_wrapper instead
// If not, we use the rake wrapper as a preload for rake.
}
String[] additionalArgs = original.getAdditionalArgs();
if (additionalArgs != null) {
if (!isRake) {
args.add("--"); // NOI18N
}
for (String arg : additionalArgs) {
args.add(arg);
}
}
additionalArgs = args.toArray(new String[args.size()]);
RubyExecutionDescriptor descriptor = new RubyExecutionDescriptor(original);
descriptor.addAdditionalEnv(additionalEnv);
descriptor.initialArgs(initial);
descriptor.script(target);
descriptor.additionalArgs(additionalArgs);
HideCoverageFramesConvertor hideWrapperConverter = new HideCoverageFramesConvertor();
descriptor.addOutConvertor(hideWrapperConverter);
descriptor.addErrConvertor(hideWrapperConverter);
descriptor.postBuild(new Runnable() {
public void run() {
RubyCoverageProvider.this.update();
CoverageManager.INSTANCE.resultsUpdated(project, RubyCoverageProvider.this);
if (original.getPostBuild() != null) {
original.getPostBuild().run();
}
}
});
return descriptor;
}
public String getTestAllAction() {
if (project != null) {
PropertyEvaluator evaluator = project.getLookup().lookup(PropertyEvaluator.class);
if (evaluator != null) {
String action = evaluator.getProperty(CODE_COVERAGE_TEST_ACTION);
if (action != null) {
return action;
}
}
}
return null;
}
// Remove stacktrace lines that refer to frames in the wrapper scripts (rcov, or rcov_wrapper)
private static class HideCoverageFramesConvertor implements LineConvertor {
public List<ConvertedLine> convert(String line) {
if (line.contains("/ruby/coverage/") || line.contains("/rcov") || // NOI18N
(File.separatorChar == '\\' && (line.contains("\\ruby\\coverage\\") || line.contains("\\rcov")))) { // NOI18N
return Collections.emptyList();
}
return null;
}
}
private static final boolean SKIP_EXCLUSIONS;
private static final boolean RCOV_RAILS;
static {
String exclude = System.getProperty("coverage.exclude");
SKIP_EXCLUSIONS = exclude != null && "true".equals(exclude);
String rails = System.getProperty("coverage.rcov-rails");
RCOV_RAILS = rails == null || "true".equals(rails);
}
private void buildRcovArgs(StringBuilder sb, List<String> args, boolean isAggregating, String includeName, RubyExecutionDescriptor original) {
// Rcov args
String dataFile = getRubyCoverageFile().getPath();
if (isAggregating) {
if (args != null) {
args.add("--aggregate"); // NOI18N
args.add(dataFile);
} else {
sb.append(" --aggregate \""); // NOI18N
sb.append(dataFile);
sb.append("\""); // NOI18N
}
}
if (args != null) {
args.add("--save"); // NOI18N
args.add(dataFile);
} else {
sb.append(" --save \""); // NOI18N
sb.append(dataFile);
sb.append("\""); // NOI18N
}
if (args != null) {
// Collect data only
args.add("--no-html"); // NOI18N
} else {
sb.append(" --no-html"); // NOI18N
}
// Include coverage collection on the current file too, if applicable
// (e.g. collection on test files)
if (includeName == null && original.getFileObject() != null) {
includeName = original.getFileObject().getNameExt();
}
if (includeName != null) {
includeName = includeName.substring(Math.max(includeName.lastIndexOf('\\'), includeName.lastIndexOf('/')) + 1);
if (args != null) {
args.add("--include-file"); // NOI18N
args.add(includeName);
} else {
sb.append(" --include-file \""); // NOI18N
sb.append(includeName);
sb.append("\""); // NOI18N
}
}
StringBuilder exclude = new StringBuilder(100); // NOI18N
// Skip mediator scripts etc.
exclude.append("\\/ruby\\/,/\\\\ruby\\\\/"); // NOI18N
// This shows up when running specs
exclude.append(",\\bfcntl\\b"); // NOI18N
// No need: removed by gem stuff below
//exclude.append(",rcov"); // NOI18N
exclude.append(",/\\bvendor\\//"); // NOI18N
RubyPlatform platform = original.getPlatform();
if (platform != null) {
String home = platform.getHome().getPath();
// Skip stuff in gems and in the libraries - only include stuff in the project
exclude.append(',');
// Escape /'s so we're passing a regex
exclude.append(home.replace("/", "\\/")); // NOI18N
if (File.separatorChar == '\\') {
exclude.append(home.replace('\\', '/').replace("/", "\\/")); // NOI18N
}
if (platform.hasRubyGemsInstalled()) {
GemManager gemManager = platform.getGemManager();
if (gemManager != null) {
String gemHome = gemManager.getGemHome();
if (gemHome != null && !gemHome.startsWith(home)) {
exclude.append(',');
exclude.append(gemHome.replace("/", "\\/")); // NOI18N
if (File.separatorChar == '\\') {
exclude.append(gemHome.replace('\\', '/').replace("/", "\\/")); // NOI18N
}
}
}
}
}
if (SKIP_EXCLUSIONS) {
// Temporary workaround to track down problem
if (args != null) {
args.add("--exclude-only"); // NOI18N
args.add("thisstringdefinitelydoesnotexist"); // NOI18N
} else {
sb.append(" --exclude-only \"thisstringdefinitelydoesnotexist\""); // NOI18N
}
} else if (args != null) {
args.add("--exclude-only"); // NOI18N
args.add(exclude.toString());
} else {
sb.append(" --exclude-only \""); // NOI18N
sb.append(exclude.toString());
sb.append("\""); // NOI18N
}
// If on rails:
if (RCOV_RAILS && project.getClass().getSimpleName().contains("Rails")) { // NOI18N
if (args != null) {
args.add("--rails"); // NOI18N
} else {
sb.append(" --rails"); // NOI18N
}
}
// If unit testing:
//if (args != null) {
// args.add("--test-unit-only"); // NOI18N
//} else {
// sb.append(" --test-unit-only"); // NOI18N
//}
}
private static class RubyFileCoverageDetails implements FileCoverageDetails {
private final int[] hitCounts;
private final String fileName;
private final List<LineCount> lineCounts;
private Project project;
private final long lastUpdated;
private final FileObject fileObject;
public RubyFileCoverageDetails(FileObject fileObject, int[] hitCounts, Project project, String fileName, List<LineCount> lineCounts, long lastUpdated) {
this.fileObject = fileObject;
this.hitCounts = hitCounts;
this.project = project;
this.fileName = fileName;
this.lineCounts = lineCounts;
this.lastUpdated = lastUpdated;
}
public int getLineCount() {
return hitCounts.length;
}
public boolean hasHitCounts() {
return true;
}
public FileCoverageSummary getSummary() {
return createSummary(project, fileName, lineCounts);
}
public CoverageType getType(int lineNo) {
int count = hitCounts[lineNo];
switch (count) {
case -3:
return CoverageType.UNKNOWN;
case -2:
return CoverageType.NOT_COVERED;
case -1:
return CoverageType.INFERRED;
default:
return CoverageType.COVERED;
}
}
public int getHitCount(int lineNo) {
return hitCounts[lineNo];
}
public long lastUpdated() {
return lastUpdated;
}
public FileObject getFile() {
return fileObject;
}
}
private static class LineCount {
private final int lineno;
private final int count;
public LineCount(int lineno, int count) {
this.lineno = lineno;
this.count = count;
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final LineCount other = (LineCount) obj;
if (this.lineno != other.lineno) {
return false;
}
return true;
}
@Override
public int hashCode() {
int hash = 7;
hash = 59 * hash + this.lineno;
return hash;
}
}
}