Skip to content

Commit e648fef

Browse files
committed
attempt to conditionally integrate JaCoCo into JUnit; currently, JUnit runs (does not hang) and reports correct results, but coverage is reported as 0% due to issues with instrumentation.
1 parent 5fe29f9 commit e648fef

3 files changed

Lines changed: 188 additions & 33 deletions

File tree

drjava/src/edu/rice/cs/drjava/model/junit/DefaultJUnitModel.java

Lines changed: 179 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@
3636

3737
package edu.rice.cs.drjava.model.junit;
3838

39+
import edu.rice.cs.drjava.ui.coverage.ReportGenerator;
40+
3941
import java.awt.EventQueue;
4042
import java.io.File;
4143
import java.io.IOException;
@@ -44,6 +46,30 @@
4446
import java.io.FileReader;
4547
import java.rmi.RemoteException;
4648

49+
// For JaCoCo:
50+
import java.io.Writer;
51+
import java.io.BufferedWriter;
52+
import java.io.OutputStreamWriter;
53+
import java.io.FileOutputStream;
54+
import java.io.PrintWriter;
55+
import java.io.StringWriter;
56+
import org.jacoco.core.analysis.Analyzer;
57+
import org.jacoco.core.analysis.CoverageBuilder;
58+
import org.jacoco.core.analysis.IBundleCoverage;
59+
import org.jacoco.core.tools.ExecFileLoader;
60+
import org.jacoco.core.analysis.IClassCoverage;
61+
import org.jacoco.core.analysis.ICounter;
62+
import org.jacoco.core.data.ExecutionDataStore;
63+
import org.jacoco.core.data.SessionInfoStore;
64+
import org.jacoco.core.instr.Instrumenter;
65+
import org.jacoco.core.runtime.IRuntime;
66+
import org.jacoco.core.runtime.LoggerRuntime;
67+
import org.jacoco.core.runtime.RuntimeData;
68+
import org.jacoco.report.DirectorySourceFileLocator;
69+
import org.jacoco.report.FileMultiReportOutput;
70+
import org.jacoco.report.IReportVisitor;
71+
import org.jacoco.report.html.HTMLFormatter;
72+
4773
import java.util.List;
4874
import java.util.LinkedList;
4975
import java.util.ArrayList;
@@ -92,6 +118,8 @@
92118
*/
93119
public class DefaultJUnitModel implements JUnitModel, JUnitModelCallback {
94120

121+
private boolean coverage = false;
122+
95123
/** log for use in debugging */
96124
private static Log _log = new Log("DefaultJUnitModel.txt", false);
97125

@@ -141,6 +169,7 @@ public DefaultJUnitModel(MainJVM jvm, CompilerModel compilerModel, GlobalModel m
141169

142170
//-------------------------- Field Setters --------------------------------//
143171

172+
public void setCoverage(boolean c) { coverage = c; }
144173
public void setForceTestSuffix(boolean b) { _forceTestSuffix = b; }
145174

146175
//------------------------ Simple Predicates ------------------------------//
@@ -312,7 +341,8 @@ private void junitOpenDefDocs(final List<OpenDefinitionsDocument> lod, final boo
312341
*/
313342
private void _rawJUnitOpenDefDocs(List<OpenDefinitionsDocument> lod, final boolean allTests) {
314343
File buildDir = _model.getBuildDirectory();
315-
// Utilities.show("Running JUnit tests. Build directory is " + buildDir);
344+
345+
//Utilities.show("Running JUnit tests. Build directory is " + buildDir);
316346

317347
/** Open java source files */
318348
HashSet<String> openDocFiles = new HashSet<String>();
@@ -328,7 +358,7 @@ private void _rawJUnitOpenDefDocs(List<OpenDefinitionsDocument> lod, final boole
328358
for (OpenDefinitionsDocument doc: lod) /* for all nonEmpty documents in lod */ {
329359
if (doc.isSourceFile()) { // excludes Untitled documents and open non-source files
330360
try {
331-
// System.err.println("Processing " + doc);
361+
//System.err.println("Processing " + doc);
332362
File sourceRoot = doc.getSourceRoot(); // may throw an InvalidPackageException
333363

334364
// doc has valid package name; add it to list of open java source doc files
@@ -348,21 +378,21 @@ private void _rawJUnitOpenDefDocs(List<OpenDefinitionsDocument> lod, final boole
348378

349379
if (! classDirsAndRoots.containsKey(classFileDir)) {
350380
classDirsAndRoots.put(classFileDir, sourceDir);
351-
// System.err.println("Adding " + classFileDir + " with source root " + sourceRoot +
352-
// " to list of class directories");
381+
//System.err.println("Adding " + classFileDir + " with source root " + sourceRoot +
382+
//" to list of class directories");
353383
}
354384
}
355385
catch (InvalidPackageException e) { /* Skip the file, since it doesn't have a valid package */ }
356386
}
357387
}
358388

359-
// System.err.println("classDirs = " + classDirsAndRoots.keySet());
389+
//System.err.println("classDirs = " + classDirsAndRoots.keySet());
360390

361391
/** set of dirs potentially containing test classes */
362392
Set<File> classDirs = classDirsAndRoots.keySet();
363393

364-
// System.err.println("openDocFiles = " + openDocFiles);
365-
394+
//System.err.println("openDocFiles = " + openDocFiles);
395+
366396
/* Names of test classes. */
367397
final ArrayList<String> classNames = new ArrayList<String>();
368398

@@ -374,32 +404,31 @@ private void _rawJUnitOpenDefDocs(List<OpenDefinitionsDocument> lod, final boole
374404

375405
try {
376406
for (File dir: classDirs) { // foreach class file directory
377-
// System.err.println("Examining directory " + dir);
407+
//System.err.println("Examining directory " + dir);
378408

379409
File[] listing = dir.listFiles();
380410

381-
// System.err.println("Directory contains the files: " + Arrays.asList(listing));
411+
//System.err.println("Directory contains the files: " + Arrays.asList(listing));
382412

383413
if (listing != null) { // listFiles may return null if there's an IO error
384414
for (File entry : listing) { /* for each class file in the build directory */
385415

386-
// System.err.println("Examining file " + entry);
416+
//System.err.println("Examining file " + entry);
387417

388418
/* ignore non-class files */
389419
String name = entry.getName();
390420
if (! name.endsWith(".class")) continue;
391421

392422
/* Ignore class names that do not end in "Test" if FORCE_TEST_SUFFIX option is set */
423+
String noExtName = "";
393424
if (_forceTestSuffix) {
394-
String noExtName = name.substring(0, name.length() - 6); // remove ".class" from name
425+
noExtName = name.substring(0, name.length() - 6); // remove ".class" from name
395426
int indexOfLastDot = noExtName.lastIndexOf('.');
396427
String simpleClassName = noExtName.substring(indexOfLastDot + 1);
397-
// System.err.println("Simple class name is " + simpleClassName);
428+
//System.err.println("Simple class name is " + simpleClassName);
398429
if (/*isProject &&*/ ! simpleClassName.endsWith("Test")) continue;
399430
}
400-
401-
// System.err.println("Found test class: " + noExtName);
402-
431+
403432
/* ignore entries that do not correspond to files? Can this happen? */
404433
if (! entry.isFile()) continue;
405434

@@ -429,17 +458,18 @@ public void visitEnd() { }
429458

430459
/** The canonical pathname for the file (including the file name) */
431460
String javaSourceFileName = getCanonicalPath(rootDir) + File.separator + sourceName.value();
432-
// System.err.println("Full java source fileName = " + javaSourceFileName);
461+
462+
//System.err.println("Full java source fileName = " + javaSourceFileName);
433463

434464
/* The index in fileName of the dot preceding the extension ".java", ".dj", ".dj0*, ".dj1", or ".dj2" */
435465
int indexOfExtDot = javaSourceFileName.lastIndexOf('.');
436-
// System.err.println("indexOfExtDot = " + indexOfExtDot);
466+
//System.err.println("indexOfExtDot = " + indexOfExtDot);
437467
if (indexOfExtDot == -1) continue; // RMI stub class files return source file names without extensions
438-
// System.err.println("File found in openDocFiles = " + openDocFiles.contains(sourceFileName));
468+
//System.err.println("File found in openDocFiles = " + openDocFiles.contains(sourceFileName));
439469

440470
/* Determine if this java source file was generated from a language levels file. */
441471
String strippedName = javaSourceFileName.substring(0, indexOfExtDot);
442-
// System.err.println("Stripped name = " + strippedName);
472+
//System.err.println("Stripped name = " + strippedName);
443473

444474
String sourceFileName;
445475

@@ -466,10 +496,80 @@ else if (openDocFiles.contains(strippedName + OptionConstants.OLD_DJ2_FILE_EXTEN
466496
}
467497
}
468498
catch(Exception e) {
469-
// new ScrollableDialog(null, "UnexceptedExceptionThrown", e.toString(), "").show();
499+
//new ScrollableDialog(null, "UnexceptedExceptionThrown", e.toString(), "").show();
470500
throw new UnexpectedException(e); // triggers _junitInterrupted which runs hourglassOff
471501
}
472502

503+
// JaCoCo: Create instrumented versions of class files.
504+
ReportGenerator rg = null;
505+
RuntimeData myData = null;
506+
ArrayList<File> myInstrumentedFiles = null;
507+
IRuntime myRuntime = null;
508+
509+
if (coverage) {
510+
// Prepare to generate a JaCoCo report
511+
try {
512+
rg = new ReportGenerator(_model,
513+
_model.getDocumentNavigator().getSelectedDocuments(),
514+
new File("/tmp/JaCoCo"));
515+
} catch (Exception e) {
516+
throw new UnexpectedException(e);
517+
}
518+
519+
myData = new RuntimeData();
520+
myInstrumentedFiles = new ArrayList<File>();
521+
myRuntime = new LoggerRuntime();
522+
523+
// The Instrumenter creates a modified version of our test target class
524+
// that contains additional probes for execution data recording:
525+
ArrayList<byte[]> instrumenteds = new ArrayList<byte[]>();
526+
for (int i = 0 ; i< files.size() ; i++) {
527+
528+
// Instrument the i-th file
529+
try {
530+
final Instrumenter instr = new Instrumenter(myRuntime);
531+
final byte[] instrumented = instr.instrument(rg.getTargetClass(
532+
files.get(i)), classNames.get(i));
533+
String[] pathParts = files.get(i).getAbsolutePath().split("/");
534+
535+
// Write this instrumented class to a file in /tmp/
536+
FileOutputStream fos = new FileOutputStream("/tmp/" +
537+
pathParts[pathParts.length - 1].substring(0,
538+
pathParts[pathParts.length - 1].length() - 4) + "class");
539+
fos.write(instrumented);
540+
fos.close();
541+
542+
// Keep track of all of the instrumented files
543+
myInstrumentedFiles.add(new File("/tmp/" +
544+
pathParts[pathParts.length - 1]));
545+
instrumenteds.add(instrumented);
546+
547+
} catch (Exception e) {
548+
StringWriter stackTrace = new StringWriter();
549+
e.printStackTrace(new PrintWriter(stackTrace));
550+
Utilities.show("INSTRUMENTATION EXCEPTION: " + stackTrace.toString());
551+
continue;
552+
}
553+
}
554+
555+
// Now we're ready to run our instrumented class and need to startup the
556+
// runtime first:
557+
try {
558+
myRuntime.startup(myData);
559+
} catch (Exception e) {
560+
throw new UnexpectedException(e);
561+
}
562+
563+
_jvm.setWorkingDirectory(new File("/tmp"));
564+
Utilities.show("Set working directory.");
565+
_jvm.restartInterpreterJVM(true);
566+
Utilities.show("Restarted interpreter.");
567+
}
568+
569+
final RuntimeData data = myData;
570+
final ArrayList<File> instrumentedFiles = myInstrumentedFiles;
571+
final IRuntime runtime = myRuntime;
572+
473573
/** Run the junit test suite that has already been set up on the slave JVM */
474574
_testInProgress = true;
475575
// System.err.println("Spawning test thread");
@@ -484,33 +584,82 @@ public void run() {
484584
// _debugger.getPendingRequestManager().classPrepared(e); (which presumably
485585
// deals with preparing the class) on the event thread using invokeLater.
486586
// This, however, doesn't get executed because the event thread is still blocking --> deadlock.
487-
488587
synchronized(_compilerModel.getCompilerLock()) {
489588
// synchronized over _compilerModel to ensure that compilation and junit testing are mutually exclusive.
490589
/** Set up junit test suite on slave JVM; get TestCase classes forming that suite */
491-
List<String> tests = _jvm.findTestClasses(classNames, files).unwrap(null);
492-
// System.err.println("tests = " + tests);
590+
List<String> tests;
591+
if (coverage) {
592+
tests = _jvm.findTestClasses(classNames, instrumentedFiles).unwrap(null);
593+
} else {
594+
tests = _jvm.findTestClasses(classNames, files).unwrap(null);
595+
}
596+
493597
if (tests == null || tests.isEmpty()) {
494598
nonTestCase(allTests, false);
495599
return;
496600
}
497601
}
498602

499603
try {
500-
// Utilities.show("Starting JUnit");
501-
502604
_notifyJUnitStarted();
503-
boolean testsPresent = _jvm.runTestSuite(); // The false return value could be changed to an exception.
504-
if (! testsPresent) throw new RemoteException("No unit test classes were passed to the slave JVM");
605+
// The false return value could be changed to an exception.
606+
boolean testsPresent = _jvm.runTestSuite();
607+
if (! testsPresent) {
608+
throw new RemoteException("No unit test classes were passed to the slave JVM");
609+
}
505610
}
506-
catch(RemoteException e) { // Unit testing aborted; cleanup; hourglassOff already called in junitStarted
611+
catch (RemoteException e) {
612+
// Unit testing aborted; cleanup; hourglassOff already called in junitStarted
507613
_notifyJUnitEnded(); // balances junitStarted()
508614
_testInProgress = false;
509615
}
510616
}
617+
511618
}).start();
512-
}
513-
619+
620+
// At the end of test execution we collect execution data and shutdown
621+
// the runtime:
622+
//highlight(rg, true);
623+
if (coverage) {
624+
Utilities.show("Collecting session info...");
625+
final ExecutionDataStore executionData = new ExecutionDataStore();
626+
final SessionInfoStore sessionInfos = new SessionInfoStore();
627+
myData.collect(executionData, sessionInfos, false);
628+
runtime.shutdown();
629+
630+
// Together with the original class definition we can calculate coverage
631+
// information:
632+
final CoverageBuilder coverageBuilder = new CoverageBuilder();
633+
final Analyzer analyzer = new Analyzer(executionData, coverageBuilder);
634+
635+
try {
636+
for (int i = 0; i< classNames.size(); i++) {
637+
Utilities.show("className = " + classNames.get(i));
638+
analyzer.analyzeClass(rg.getTargetClass(files.get(i)),
639+
classNames.get(i));
640+
}
641+
642+
//printCoverage(coverageBuilder);
643+
644+
// Run the structure analyzer on a single class folder to build up
645+
// the coverage model. The process would be similar if your classes
646+
// were in a jar file. Typically you would create a bundle for each
647+
// class folder and each jar you want in your report. If you have
648+
// more than one bundle you will need to add a grouping node to your
649+
// report
650+
Utilities.show("rg.getSourceDirectory().getName() = " + rg.getSourceDirectory().getName());
651+
final IBundleCoverage bundleCoverage = coverageBuilder.getBundle(
652+
rg.getSourceDirectory().getName());
653+
rg.createReport(bundleCoverage, executionData, sessionInfos);
654+
} catch (Exception e) {
655+
StringWriter stackTrace = new StringWriter();
656+
e.printStackTrace(new PrintWriter(stackTrace));
657+
Utilities.show("ANALYSIS EXCEPTION: " + stackTrace.toString());
658+
}
659+
660+
}
661+
}
662+
514663
//-------------------------------- Helpers --------------------------------//
515664

516665
/** Helper method to notify JUnitModel listeners that JUnit test suite execution has started. */

drjava/src/edu/rice/cs/drjava/model/junit/JUnitModel.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ public interface JUnitModel {
4747

4848

4949
/** set the forceTestSuffix flag that forces class names in projects to end in "Test */
50+
public void setCoverage(boolean c);
5051
public void setForceTestSuffix(boolean b);
5152

5253
//-------------------------- Listener Management --------------------------//

drjava/src/edu/rice/cs/drjava/ui/MainFrame.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5842,9 +5842,14 @@ private void _junitAll() {
58425842
updateStatusField("Running All Open Unit Tests");
58435843
hourglassOn(); // turned off in junitStarted/nonTestCase/_junitInterrupted
58445844
_guiAvailabilityNotifier.junitStarted(); // JUNIT and COMPILER
5845-
try { _model.getJUnitModel().junitAll(); }
5846-
catch(UnexpectedException e) { _junitInterrupted(e); }
5847-
catch(Exception e) { _junitInterrupted(new UnexpectedException(e)); }
5845+
try {
5846+
_model.getJUnitModel().setCoverage(true);
5847+
_model.getJUnitModel().junitAll();
5848+
} catch(UnexpectedException e) {
5849+
_junitInterrupted(e);
5850+
} catch(Exception e) {
5851+
_junitInterrupted(new UnexpectedException(e));
5852+
}
58485853
}
58495854

58505855
// /**

0 commit comments

Comments
 (0)