Skip to content

Commit

Permalink
Fix incremental build Java generation. (#16)
Browse files Browse the repository at this point in the history
Translate task is requested to put generated source into a target
directory "main", however dafny actually places it into a directory
callled "main-java" and the translate task performs a rename at the
end to compensate. The problem is this rename can fail, for example
on an incremental build, when the target directory already exists.

Instead keep "main-java" as target and pass this modified target
onto subsequent gradle tasks as new source to be compiled.
  • Loading branch information
hillcg-aws authored Jan 7, 2025
1 parent ffba5a1 commit 969ccc6
Show file tree
Hide file tree
Showing 6 changed files with 88 additions and 39 deletions.
12 changes: 12 additions & 0 deletions examples/java/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
plugins {
id("org.dafny.dafny")
}

repositories {
mavenLocal()
mavenCentral()
}

dafny {
dafnyVersion.set("4.9.1")
}
Empty file.
5 changes: 5 additions & 0 deletions examples/java/src/main/dafny/Foo.dfy
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module Foo {
datatype Bar = Create(baz: string) {
const x: string := baz
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,71 +22,106 @@
*/
class DafnyPluginFunctionalTest {

@Test void canVerify() throws IOException {
@Test
void canVerify() throws IOException {
BuildResult result = GradleRunner.create()
.forwardOutput()
.withPluginClasspath()
.withArguments("build")
.withProjectDir(new File("examples/simple-verify"))
.buildAndFail();
.forwardOutput()
.withPluginClasspath()
.withArguments("build")
.withProjectDir(new File("examples/simple-verify"))
.buildAndFail();

Assertions.assertTrue(result.getOutput().contains(
"examples/simple-verify/src/main/dafny/simple.dfy(2,9): Error: assertion might not hold"));
Assertions.assertTrue(result.getOutput().contains(
"examples/simple-verify/src/main/dafny/nested/simple.dfy(2,9): Error: assertion might not hold"));
}

@Test void canReferenceDependencies() throws IOException {
@Test
void canReferenceDependencies() throws IOException {
GradleRunner.create()
.forwardOutput()
.withPluginClasspath()
.withArguments("clean", "build")
.withProjectDir(new File("examples/multi-project"))
.build();
.forwardOutput()
.withPluginClasspath()
.withArguments("clean", "build")
.withProjectDir(new File("examples/multi-project"))
.build();
}

// Expected to fail because the producer and consumer use different values of --unicode-char
@Test void failsOnIncompatibleDependencies() throws IOException {
// Expected to fail because the producer and consumer use different values of
// --unicode-char
@Test
void failsOnIncompatibleDependencies() throws IOException {
BuildResult result = GradleRunner.create()
.forwardOutput()
.withPluginClasspath()
.withArguments("clean", "build")
.withProjectDir(new File("examples/multi-project-incompatible"))
.buildAndFail();
.forwardOutput()
.withPluginClasspath()
.withArguments("clean", "build")
.withProjectDir(new File("examples/multi-project-incompatible"))
.buildAndFail();
Assertions.assertTrue(result.getOutput().contains(
"--unicode-char is set locally to True, but the library was built with False"));
}

@Test void succeedsWithNoDafnySourceFiles() throws IOException {
@Test
void succeedsWithNoDafnySourceFiles() throws IOException {
GradleRunner.create()
.forwardOutput()
.withPluginClasspath()
.withArguments("clean", "build")
.withProjectDir(new File("examples/no-dafny"))
.build();
.forwardOutput()
.withPluginClasspath()
.withArguments("clean", "build")
.withProjectDir(new File("examples/no-dafny"))
.build();
}

@Test void failsOnWrongDafnyVersion() throws IOException {
@Test
void failsOnWrongDafnyVersion() throws IOException {
BuildResult result = GradleRunner.create()
.forwardOutput()
.withPluginClasspath()
.withArguments("clean", "build")
.withProjectDir(new File("examples/wrong-dafny-version"))
.buildAndFail();
.forwardOutput()
.withPluginClasspath()
.withArguments("clean", "build")
.withProjectDir(new File("examples/wrong-dafny-version"))
.buildAndFail();
Assertions.assertTrue(result.getOutput().contains(
"Incorrect Dafny version: expected 2.3.0, found"));
}

// Regression test: this previously failed because the standard libraries
// end up included in the .doo file,
// so passing `--standard-libraries` again when translating led to duplicate definitions.
// Dafny has addressed this by not including the standard libraries which building .doo files.
@Test void supportsStandardLibraries() throws IOException {
// so passing `--standard-libraries` again when translating led to duplicate
// definitions.
// Dafny has addressed this by not including the standard libraries which
// building .doo files.
@Test
void supportsStandardLibraries() throws IOException {
GradleRunner.create()
.forwardOutput()
.withPluginClasspath()
.withArguments("clean", "build")
.withProjectDir(new File("examples/using-standard-libraries"))
.build();
}

@Test
void convertsToJava() throws IOException {
var dir = new File("examples/java");
// N.B. This path is set in DafnyPlugin.java, then Dafny adds "-java" suffix
var expected = dir.toPath().resolve("build/generated/sources/fromDafny/java/main-java/Foo/Bar.java").toFile();

// Clean build
GradleRunner.create()
.forwardOutput()
.withPluginClasspath()
.withArguments("clean", "translateDafnyToJava")
.withProjectDir(dir)
.build();
Assertions.assertTrue(expected.exists());
var firstTime = expected.lastModified();

// (Force) an incremental re-build, and check the modified time moved
GradleRunner.create()
.forwardOutput()
.withPluginClasspath()
.withArguments("translateDafnyToJava", "--rerun-tasks")
.withProjectDir(dir)
.build();
Assertions.assertTrue(expected.lastModified() > firstTime);
}
}
3 changes: 2 additions & 1 deletion src/main/java/org/dafny/gradle/plugin/DafnyPlugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@ public void apply(Project project) {

// Register the Dafny-generated code to be compiled
JavaPluginExtension javaExt = project.getExtensions().findByType(JavaPluginExtension.class);
javaExt.getSourceSets().findByName("main").getJava().srcDir(translatedJavaDir);
// Annoyingly, Dafny adds "-java" to the supplied directory name
javaExt.getSourceSets().findByName("main").getJava().srcDir(new File(translatedJavaDir.getPath() + "-java"));

// Make sure the Dafny-generated code is generated before compiling
project.getTasks().withType(JavaCompile.class, javaCompile ->
Expand Down
4 changes: 0 additions & 4 deletions src/main/java/org/dafny/gradle/plugin/DafnyTranslateTask.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,5 @@ public void translateToJava() throws IOException, InterruptedException {
args.add(outputDir.getPath());

invokeDafnyCLI(args);

// Annoyingly, Dafny adds "-java" to the directory, so rename it
File outputDirWithJava = new File(outputDir.getParentFile(), outputDir.getName() + "-java");
outputDirWithJava.renameTo(outputDir);
}
}

0 comments on commit 969ccc6

Please sign in to comment.