diff --git a/org.jacoco.agent.rt.test/pom.xml b/org.jacoco.agent.rt.test/pom.xml index 9f2f764e28..0cbfd959a8 100644 --- a/org.jacoco.agent.rt.test/pom.xml +++ b/org.jacoco.agent.rt.test/pom.xml @@ -33,6 +33,10 @@ ${project.groupId} org.jacoco.agent.rt + + ${project.groupId} + org.jacoco.core.test + junit junit diff --git a/org.jacoco.agent.rt.test/src/org/jacoco/agent/rt/internal/output/FileOutputTest.java b/org.jacoco.agent.rt.test/src/org/jacoco/agent/rt/internal/output/FileOutputTest.java index b7de06efda..f10528b790 100644 --- a/org.jacoco.agent.rt.test/src/org/jacoco/agent/rt/internal/output/FileOutputTest.java +++ b/org.jacoco.agent.rt.test/src/org/jacoco/agent/rt/internal/output/FileOutputTest.java @@ -14,12 +14,18 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; +import java.io.InterruptedIOException; +import java.nio.channels.OverlappingFileLockException; import org.jacoco.core.runtime.AgentOptions; import org.jacoco.core.runtime.RuntimeData; +import org.jacoco.core.test.validation.JavaVersion; +import org.junit.AssumptionViolatedException; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; @@ -33,7 +39,7 @@ public class FileOutputTest { public TemporaryFolder folder = new TemporaryFolder(); @Test - public void testCreateDestFileOnStartup() throws Exception { + public void startup_should_create_empty_execfile() throws Exception { File destFile = folder.newFile("jacoco.exec"); AgentOptions options = new AgentOptions(); options.setDestfile(destFile.getAbsolutePath()); @@ -47,7 +53,7 @@ public void testCreateDestFileOnStartup() throws Exception { } @Test - public void testWriteData() throws Exception { + public void writeExecutionData_should_write_execdata() throws Exception { File destFile = folder.newFile("jacoco.exec"); AgentOptions options = new AgentOptions(); options.setDestfile(destFile.getAbsolutePath()); @@ -62,14 +68,69 @@ public void testWriteData() throws Exception { destFile.length() > 0); } - @Test(expected = IOException.class) - public void testInvalidDestFile() throws Exception { + @Test + public void startup_should_throw_IOException_when_execfile_cannot_be_created() + throws Exception { AgentOptions options = new AgentOptions(); options.setDestfile(folder.newFolder("folder").getAbsolutePath()); FileOutput controller = new FileOutput(); - // Startup should fail as the file can not be created: - controller.startup(options, new RuntimeData()); + try { + controller.startup(options, new RuntimeData()); + fail("IOException expected"); + } catch (IOException e) { + // expected + } + } + + @Test + public void startup_should_throw_OverlappingFileLockException_when_execfile_is_permanently_locked() + throws Exception { + if (JavaVersion.current().isBefore("1.6")) { + throw new AssumptionViolatedException( + "OverlappingFileLockException only thrown since Java 1.6"); + } + + File destFile = folder.newFile("jacoco.exec"); + AgentOptions options = new AgentOptions(); + options.setDestfile(destFile.getAbsolutePath()); + FileOutputStream out = new FileOutputStream(destFile); + out.getChannel().lock(); + FileOutput controller = new FileOutput(); + + try { + controller.startup(options, new RuntimeData()); + fail("OverlappingFileLockException expected"); + } catch (OverlappingFileLockException e) { + // expected + } finally { + out.close(); + } + } + + public void startup_should_throw_InterruptedIOException_when_execfile_is_locked_and_thread_is_interrupted() + throws Exception { + if (JavaVersion.current().isBefore("1.6")) { + throw new AssumptionViolatedException( + "OverlappingFileLockException only thrown since Java 1.6"); + } + + File destFile = folder.newFile("jacoco.exec"); + AgentOptions options = new AgentOptions(); + options.setDestfile(destFile.getAbsolutePath()); + FileOutputStream out = new FileOutputStream(destFile); + out.getChannel().lock(); + FileOutput controller = new FileOutput(); + Thread.currentThread().interrupt(); + + try { + controller.startup(options, new RuntimeData()); + fail("InterruptedIOException expected"); + } catch (InterruptedIOException e) { + // expected + } finally { + out.close(); + } } } diff --git a/org.jacoco.agent.rt/src/org/jacoco/agent/rt/internal/output/FileOutput.java b/org.jacoco.agent.rt/src/org/jacoco/agent/rt/internal/output/FileOutput.java index 97afe39a50..872065579e 100644 --- a/org.jacoco.agent.rt/src/org/jacoco/agent/rt/internal/output/FileOutput.java +++ b/org.jacoco.agent.rt/src/org/jacoco/agent/rt/internal/output/FileOutput.java @@ -15,7 +15,10 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; +import java.io.InterruptedIOException; import java.io.OutputStream; +import java.nio.channels.FileChannel; +import java.nio.channels.OverlappingFileLockException; import org.jacoco.core.data.ExecutionDataWriter; import org.jacoco.core.runtime.AgentOptions; @@ -31,6 +34,10 @@ */ public class FileOutput implements IAgentOutput { + private static final int LOCK_RETRY_COUNT = 30; + + private static final long LOCK_RETRY_WAIT_TIME_MS = 100; + private RuntimeData data; private File destFile; @@ -67,8 +74,28 @@ public void shutdown() throws IOException { private OutputStream openFile() throws IOException { final FileOutputStream file = new FileOutputStream(destFile, append); // Avoid concurrent writes from different agents running in parallel: - file.getChannel().lock(); - return file; + final FileChannel fc = file.getChannel(); + int retries = 0; + while (true) { + try { + // An agent from another JVM might have a lock. In this case + // this method blocks until the lock is freed. + fc.lock(); + return file; + } catch (final OverlappingFileLockException e) { + // In the case of multiple class loaders there can be multiple + // JaCoCo runtimes even in the same VM. In this case we get an + // OverlappingFileLockException and retry lock acquisition: + if (retries++ > LOCK_RETRY_COUNT) { + throw e; + } + } + try { + Thread.sleep(LOCK_RETRY_WAIT_TIME_MS); + } catch (final InterruptedException e) { + throw new InterruptedIOException(); + } + } } } diff --git a/org.jacoco.core.test.validation.java5/src/org/jacoco/core/test/validation/java5/EnumSwitchTest.java b/org.jacoco.core.test.validation.java5/src/org/jacoco/core/test/validation/java5/EnumSwitchTest.java index 03a681704d..48ec061469 100644 --- a/org.jacoco.core.test.validation.java5/src/org/jacoco/core/test/validation/java5/EnumSwitchTest.java +++ b/org.jacoco.core.test.validation.java5/src/org/jacoco/core/test/validation/java5/EnumSwitchTest.java @@ -12,6 +12,7 @@ *******************************************************************************/ package org.jacoco.core.test.validation.java5; +import org.jacoco.core.test.validation.JavaVersion; import org.jacoco.core.test.validation.Source.Line; import org.jacoco.core.test.validation.ValidationTestBase; import org.jacoco.core.test.validation.java5.targets.EnumSwitchTarget; @@ -27,7 +28,7 @@ public EnumSwitchTest() { } public void assertSwitch(final Line line) { - if (isJDKCompiler && JAVA_VERSION.isBefore("1.6")) { + if (isJDKCompiler && JavaVersion.current().isBefore("1.6")) { // class that holds "switch map" is not marked as synthetic when // compiling with javac 1.5 assertPartlyCovered(line, 0, 2); diff --git a/org.jacoco.core.test.validation.java5/src/org/jacoco/core/test/validation/java5/FinallyTest.java b/org.jacoco.core.test.validation.java5/src/org/jacoco/core/test/validation/java5/FinallyTest.java index 9f4aa94914..e2d362e0e8 100644 --- a/org.jacoco.core.test.validation.java5/src/org/jacoco/core/test/validation/java5/FinallyTest.java +++ b/org.jacoco.core.test.validation.java5/src/org/jacoco/core/test/validation/java5/FinallyTest.java @@ -24,6 +24,7 @@ import org.jacoco.core.internal.instr.InstrSupport; import org.jacoco.core.test.TargetLoader; +import org.jacoco.core.test.validation.JavaVersion; import org.jacoco.core.test.validation.Source.Line; import org.jacoco.core.test.validation.ValidationTestBase; import org.jacoco.core.test.validation.java5.targets.FinallyTarget; @@ -62,7 +63,7 @@ public void assertFinally(final Line line) { } public void assertTwoRegions1(final Line line) { - if (isJDKCompiler && JAVA_VERSION.isBefore("1.8")) { + if (isJDKCompiler && JavaVersion.current().isBefore("1.8")) { // https://bugs.openjdk.java.net/browse/JDK-7008643 assertPartlyCovered(line); } else { @@ -71,7 +72,7 @@ public void assertTwoRegions1(final Line line) { } public void assertTwoRegionsReturn1(final Line line) { - if (isJDKCompiler && JAVA_VERSION.isBefore("1.8")) { + if (isJDKCompiler && JavaVersion.current().isBefore("1.8")) { // https://bugs.openjdk.java.net/browse/JDK-7008643 assertEmpty(line); } else { @@ -80,7 +81,7 @@ public void assertTwoRegionsReturn1(final Line line) { } public void assertTwoRegionsReturn2(final Line line) { - if (isJDKCompiler && JAVA_VERSION.isBefore("1.8")) { + if (isJDKCompiler && JavaVersion.current().isBefore("1.8")) { // https://bugs.openjdk.java.net/browse/JDK-7008643 assertEmpty(line); } else { @@ -89,7 +90,7 @@ public void assertTwoRegionsReturn2(final Line line) { } public void assertEmptyTry1(final Line line) { - if (isJDKCompiler && JAVA_VERSION.isBefore("1.8")) { + if (isJDKCompiler && JavaVersion.current().isBefore("1.8")) { // compiler bug fixed in javac >= 1.8: assertPartlyCovered(line); } else { @@ -98,7 +99,7 @@ public void assertEmptyTry1(final Line line) { } public void assertEmptyTry2(final Line line) { - if (isJDKCompiler && JAVA_VERSION.isBefore("1.8")) { + if (isJDKCompiler && JavaVersion.current().isBefore("1.8")) { // compiler bug fixed in javac >= 1.8: assertFullyCovered(line); } else { @@ -146,7 +147,7 @@ private void gotos() throws IOException { expected.add("breakStatement.for"); if (isJDKCompiler) { - if (JAVA_VERSION.isBefore("10")) { + if (JavaVersion.current().isBefore("10")) { // https://bugs.openjdk.java.net/browse/JDK-8180141 expected.add("breakStatement.1"); } else { @@ -179,7 +180,7 @@ private void gotos() throws IOException { expected.add("nested.3"); } - if (isJDKCompiler && JAVA_VERSION.isBefore("1.8")) { + if (isJDKCompiler && JavaVersion.current().isBefore("1.8")) { expected.add("emptyTry.2"); } diff --git a/org.jacoco.core.test.validation.java7/src/org/jacoco/core/test/validation/java7/TryWithResourcesTest.java b/org.jacoco.core.test.validation.java7/src/org/jacoco/core/test/validation/java7/TryWithResourcesTest.java index a147282e6c..cab72fd802 100644 --- a/org.jacoco.core.test.validation.java7/src/org/jacoco/core/test/validation/java7/TryWithResourcesTest.java +++ b/org.jacoco.core.test.validation.java7/src/org/jacoco/core/test/validation/java7/TryWithResourcesTest.java @@ -12,6 +12,7 @@ *******************************************************************************/ package org.jacoco.core.test.validation.java7; +import org.jacoco.core.test.validation.JavaVersion; import org.jacoco.core.test.validation.Source.Line; import org.jacoco.core.test.validation.ValidationTestBase; import org.jacoco.core.test.validation.java7.targets.TryWithResourcesTarget; @@ -28,7 +29,7 @@ public TryWithResourcesTest() { public void assertTry(final Line line) { // without filter this line is covered partly: - if (!isJDKCompiler || JAVA_VERSION.isBefore("11")) { + if (!isJDKCompiler || JavaVersion.current().isBefore("11")) { assertFullyCovered(line); } else { assertEmpty(line); @@ -40,7 +41,7 @@ public void assertReturnInBodyClose(final Line line) { if (isJDKCompiler) { // https://bugs.openjdk.java.net/browse/JDK-8134759 // javac 7 and 8 up to 8u92 are affected - if (JAVA_VERSION.isBefore("1.8.0_92")) { + if (JavaVersion.current().isBefore("1.8.0_92")) { assertFullyCovered(line); } else { assertEmpty(line); @@ -61,9 +62,9 @@ public void assertHandwritten(final Line line) { public void assertEmptyClose(final Line line) { if (!isJDKCompiler) { assertPartlyCovered(line, 7, 1); - } else if (JAVA_VERSION.isBefore("8")) { + } else if (JavaVersion.current().isBefore("8")) { assertPartlyCovered(line, 6, 2); - } else if (JAVA_VERSION.isBefore("9")) { + } else if (JavaVersion.current().isBefore("9")) { assertPartlyCovered(line, 2, 2); } else { assertFullyCovered(line); @@ -74,9 +75,9 @@ public void assertThrowInBodyClose(final Line line) { // not filtered if (!isJDKCompiler) { assertNotCovered(line, 6, 0); - } else if (JAVA_VERSION.isBefore("9")) { + } else if (JavaVersion.current().isBefore("9")) { assertNotCovered(line, 4, 0); - } else if (JAVA_VERSION.isBefore("11")) { + } else if (JavaVersion.current().isBefore("11")) { assertNotCovered(line); } else { assertEmpty(line); diff --git a/org.jacoco.core.test.validation.java8/src/org/jacoco/core/test/validation/java8/BadCycleInterfaceTest.java b/org.jacoco.core.test.validation.java8/src/org/jacoco/core/test/validation/java8/BadCycleInterfaceTest.java index ab2ddcb619..a7ff44a938 100644 --- a/org.jacoco.core.test.validation.java8/src/org/jacoco/core/test/validation/java8/BadCycleInterfaceTest.java +++ b/org.jacoco.core.test.validation.java8/src/org/jacoco/core/test/validation/java8/BadCycleInterfaceTest.java @@ -12,6 +12,7 @@ *******************************************************************************/ package org.jacoco.core.test.validation.java8; +import org.jacoco.core.test.validation.JavaVersion; import org.jacoco.core.test.validation.Source.Line; import org.jacoco.core.test.validation.ValidationTestBase; import org.jacoco.core.test.validation.java8.targets.BadCycleInterfaceTarget; @@ -28,7 +29,7 @@ public BadCycleInterfaceTest() throws Exception { @Test public void method_execution_sequence() throws Exception { - if (JAVA_VERSION.isBefore("1.8.0_152")) { + if (JavaVersion.current().isBefore("1.8.0_152")) { assertLogEvents("baseclinit", "childdefaultmethod", "childclinit", "childstaticmethod"); } else { @@ -37,7 +38,7 @@ public void method_execution_sequence() throws Exception { } public void assertBaseClInit(final Line line) { - if (JAVA_VERSION.isBefore("1.8.0_152")) { + if (JavaVersion.current().isBefore("1.8.0_152")) { // Incorrect interpetation of JVMS 5.5 in JDK 8 causes a default // method to be called before the static initializer of an interface // (see JDK-8098557 and JDK-8164302): @@ -51,7 +52,7 @@ public void assertBaseClInit(final Line line) { } public void assertChildDefault(final Line line) throws Exception { - if (JAVA_VERSION.isBefore("1.8.0_152")) { + if (JavaVersion.current().isBefore("1.8.0_152")) { // Incorrect interpetation of JVMS 5.5 in JDK 8 causes a default // method to be called before the static initializer of an interface // (see JDK-8098557 and JDK-8164302): diff --git a/org.jacoco.core.test/src/org/jacoco/core/test/validation/JavaVersion.java b/org.jacoco.core.test/src/org/jacoco/core/test/validation/JavaVersion.java index 8522d7651a..6ecd7aca59 100644 --- a/org.jacoco.core.test/src/org/jacoco/core/test/validation/JavaVersion.java +++ b/org.jacoco.core.test/src/org/jacoco/core/test/validation/JavaVersion.java @@ -64,4 +64,11 @@ public boolean isBefore(final String version) { && this.update < other.update); } + /** + * @return version of the current JVM + */ + public static JavaVersion current() { + return new JavaVersion(System.getProperty("java.version")); + } + } diff --git a/org.jacoco.core.test/src/org/jacoco/core/test/validation/ValidationTestBase.java b/org.jacoco.core.test/src/org/jacoco/core/test/validation/ValidationTestBase.java index 13c7de13bd..8c57e4eee3 100644 --- a/org.jacoco.core.test/src/org/jacoco/core/test/validation/ValidationTestBase.java +++ b/org.jacoco.core.test/src/org/jacoco/core/test/validation/ValidationTestBase.java @@ -41,9 +41,6 @@ public abstract class ValidationTestBase { protected static final boolean isJDKCompiler = Compiler.DETECT.isJDK(); - protected static final JavaVersion JAVA_VERSION = new JavaVersion( - System.getProperty("java.version")); - private static final String[] STATUS_NAME = new String[4]; { diff --git a/org.jacoco.doc/docroot/doc/changes.html b/org.jacoco.doc/docroot/doc/changes.html index 561f54200e..c0f05510c3 100644 --- a/org.jacoco.doc/docroot/doc/changes.html +++ b/org.jacoco.doc/docroot/doc/changes.html @@ -33,6 +33,8 @@

New Features

  • Branch added by the Kotlin compiler version 1.6.0 and above for "unsafe" cast operator is filtered out during generation of report (GitHub #1266).
  • +
  • Improved support for multiple JaCoCo runtimes in the same VM + (GitHub #1057).
  • Fixed bugs