Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion src/main/java/org/apache/groovy/util/JavaShell.java
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,6 @@
* additionally written to disk by {@code compileAllTo}). See the {@code javac} documentation
* for the complete list of supported flags.
*/
@Incubating
public class JavaShell {
private static final String MAIN_METHOD_NAME = "main";
private static final URL[] EMPTY_URL_ARRAY = new URL[0];
Expand Down Expand Up @@ -194,6 +193,10 @@ public Map<String, Class<?>> compileAll(final String className, String src) thro

private void doCompile(String className, String src, Iterable<String> options) throws IOException {
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
if (compiler == null) {
throw new IllegalStateException("JavaShell requires a JDK at runtime; "
+ "ToolProvider.getSystemJavaCompiler() returned null (running on a JRE?).");
}
try (BytesJavaFileManager bjfm = new BytesJavaFileManager(compiler.getStandardFileManager(null, locale, charset))) {
StringBuilderWriter out = new StringBuilderWriter();
JavaCompiler.CompilationTask task =
Expand Down Expand Up @@ -240,6 +243,7 @@ private void doCompile(String className, String src, Iterable<String> options) t
* @throws JavaShellCompilationException if the source fails to compile
* @since 6.0.0
*/
@Incubating
public Map<String, Path> compileAllTo(String className, Iterable<String> options,
String src, Path outputDir)
throws IOException {
Expand Down Expand Up @@ -269,6 +273,7 @@ public Map<String, Path> compileAllTo(String className, Iterable<String> options
* @throws JavaShellCompilationException if the source fails to compile
* @since 6.0.0
*/
@Incubating
public Map<String, Path> compileAllTo(String className, String src, Path outputDir)
throws IOException {
return compileAllTo(className, Collections.emptyList(), src, outputDir);
Expand Down
41 changes: 41 additions & 0 deletions src/spec/doc/core-testing-guide.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@

= Testing Guide

ifndef::guide-integrating[]
:guide-integrating: guide-integrating.adoc
endif::[]

== Introduction

The Groovy programming language comes with great support for writing tests. In addition to the language
Expand Down Expand Up @@ -355,6 +359,43 @@ cobertura {
Several output formats can be chosen for Cobertura coverage reports and test code coverage reports can be added to
continuous integration build tasks.

==== JavaShell

The `org.apache.groovy.util.JavaShell` class (covered in detail in the
<<{guide-integrating}#integ-javashell,integration guide>>) is also useful in tests when you need
real Java classes alongside your Groovy code. It avoids two recurring frictions:

* you can declare Java fixture classes _inside_ the test that uses them, instead of adding
`src/test/java/...` files that exist only to support one Groovy test;
* you can capture genuine `javac`-emitted bytecode for use cases such as comparing what the
Groovy compiler produces against what Java produces for an equivalent source — useful when
asserting joint-compilation behaviour, annotation-processing output, or `@CompileStatic`
codegen equivalence.

A short fixture example:

[source,groovy]
----
include::../test/testingguide/JavaShellExampleTests.groovy[tags=javashell_test_fixture,indent=0]
----
<1> compile a Java POJO in-memory; no `.java` file is added to the test source tree
<2> attach `JavaShell`'s class loader to a `GroovyShell` so the test's Groovy code can resolve
the freshly-compiled class
<3> exercise the fixture from Groovy

For a bytecode-equivalence assertion, `compileAllTo` writes the generated `.class` files to a
temporary directory from which the bytes can be read and fed into a bytecode-diff tool of choice
(ASM, `javap` output, etc.). The corresponding Groovy-side bytecode can be obtained through the
`GroovyClassLoader` / `CompilationUnit` paths used elsewhere in Groovy's own test suite.

[source,groovy]
----
include::../test/testingguide/JavaShellExampleTests.groovy[tags=javashell_bytecode_capture,indent=0]
----
<1> compile and write the Java class to a temp directory
<2> read the class bytes back from disk
<3> compare against equivalent Groovy bytes obtained however best suits your test

== Testing with JUnit

Groovy simplifies JUnit testing in the following ways:
Expand Down
55 changes: 55 additions & 0 deletions src/spec/doc/guide-integrating.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,61 @@ Hello, dependency 2!
Hello, dependency 2!
----

[[integ-javashell]]
=== JavaShell

While `GroovyShell` and `GroovyClassLoader` compile Groovy source, `org.apache.groovy.util.JavaShell`
compiles _Java_ source in-memory using the platform `javax.tools.JavaCompiler`. It is useful when a
Groovy or polyglot application needs to compile and load Java classes at runtime: code generation,
plug-in loaders, scripting-style Java endpoints, or mixed-language testing setups.

NOTE: `JavaShell` requires a JDK at runtime, not just a JRE — `ToolProvider.getSystemJavaCompiler()`
returns `null` on a JRE.

The simplest usage compiles a single source string and returns the loaded `Class`:

[source,groovy]
----
include::../test/IntegrationTest.groovy[tags=javashell_compile,indent=0]
----
<1> create a new `JavaShell` (an optional `ClassLoader` argument sets the parent loader)
<2> compile the source; `className` is the fully-qualified binary name and must match the
`package` declaration — see the `JavaShell` javadoc for the full naming rules
<3> invoke the compiled method reflectively

`compileAll` returns _every_ class produced from the source unit — useful when the source declares
auxiliary or nested classes:

[source,groovy]
----
include::../test/IntegrationTest.groovy[tags=javashell_compile_all,indent=0]
----
<1> compile a primary public class together with its package-private helper
<2> the returned map is keyed by binary class name

To additionally persist the generated class files to disk (for tools that consume `.class` files
or to seed a classpath outside the JVM), use `compileAllTo`. It accepts a `Path` output directory
and writes each class to its conventional package subdirectory:

[source,groovy]
----
include::../test/IntegrationTest.groovy[tags=javashell_compile_all_to,indent=0]
----
<1> compile and write to disk in one call; the second argument passes `javac` options (here
`-parameters` to retain formal parameter names)
<2> packaged classes land under `<outputDir>/<package as path>/<ClassName>.class`; default-package
classes land directly under `outputDir`

The compiled classes remain available through `js.getClassLoader()` after `compileAllTo` returns,
so the same instance covers both "compile to memory" and "compile to memory and disk".

When the source fails to compile, `JavaShell` throws `JavaShellCompilationException` carrying the
`javac` diagnostics. See the `JavaShell` javadoc for the full set of overloads (with/without an
`Iterable<String>` options argument) and the expected `className`/`options` formats.

NOTE: `compileAllTo` and its no-options overload are marked `@Incubating` — their signatures may
evolve in a subsequent 6.x release based on usage feedback.

=== CompilationUnit

Ultimately, it is possible to perform more operations during compilation by relying directly on the
Expand Down
55 changes: 55 additions & 0 deletions src/spec/test/IntegrationTest.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,61 @@ try {
} finally {
tmpDir.deleteDir()
}
'''
}

@Test
void testJavaShell() {
assertScript '''// tag::javashell_compile[]
import org.apache.groovy.util.JavaShell

def js = new JavaShell() // <1>
def src = """
package demo;
public class Foo {
public static String greet(String who) { return "hello, " + who; }
}
"""
Class<?> foo = js.compile('demo.Foo', src) // <2>
assert foo.getDeclaredMethod('greet', String).invoke(null, 'world') == 'hello, world' // <3>
// end::javashell_compile[]
'''

assertScript '''// tag::javashell_compile_all[]
import org.apache.groovy.util.JavaShell

def js = new JavaShell()
def src = """
package demo;
public class Box { public static int hidden() { return Helper.value(); } }
class Helper { static int value() { return 42; } }
"""
Map<String, Class<?>> classes = js.compileAll('demo.Box', src) // <1>
assert classes.keySet() == ['demo.Box', 'demo.Helper'] as Set // <2>
assert classes['demo.Box'].getDeclaredMethod('hidden').invoke(null) == 42
// end::javashell_compile_all[]
'''

assertScript '''// tag::javashell_compile_all_to[]
import org.apache.groovy.util.JavaShell
import java.nio.file.Files

def out = Files.createTempDirectory('javashell-')
try {
def js = new JavaShell()
def src = """
package demo;
public class Deep { public static String tag(String label) { return label; } }
"""
def written = js.compileAllTo('demo.Deep', ['-parameters'], src, out) // <1>
def classFile = out.resolve('demo/Deep.class') // <2>
assert written['demo.Deep'] == classFile && Files.exists(classFile)
// classes also remain available through js.classLoader, same as compileAll
assert js.classLoader.loadClass('demo.Deep').getDeclaredMethod('tag', String).invoke(null, 'x') == 'x'
} finally {
out.toFile().deleteDir()
}
// end::javashell_compile_all_to[]
'''
}
}
75 changes: 75 additions & 0 deletions src/spec/test/testingguide/JavaShellExampleTests.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package testingguide

import org.junit.jupiter.api.Test

import static groovy.test.GroovyAssert.assertScript

final class JavaShellExampleTests {

@Test
void testJavaFixture() {
assertScript '''// tag::javashell_test_fixture[]
import org.apache.groovy.util.JavaShell

def js = new JavaShell()
js.compile('fixtures.PojoFixture', """
package fixtures;
public class PojoFixture {
private final String name;
public PojoFixture(String name) { this.name = name; }
public String getName() { return name; }
}
""") // <1>

// share JavaShell\'s class loader so Groovy code under test sees the fixture
def shell = new GroovyShell(js.classLoader) // <2>
shell.evaluate """
def bean = fixtures.PojoFixture.newInstance('Groovy')
assert bean.name == 'Groovy'
""" // <3>
// end::javashell_test_fixture[]
'''
}

@Test
void testBytecodeCapture() {
assertScript '''// tag::javashell_bytecode_capture[]
import org.apache.groovy.util.JavaShell
import java.nio.file.Files

def out = Files.createTempDirectory('bc-')
try {
new JavaShell().compileAllTo('bc.Calc', """
package bc;
public class Calc { public static int add(int a, int b) { return a + b; } }
""", out) // <1>
byte[] javaBytes = Files.readAllBytes(out.resolve('bc/Calc.class')) // <2>
// Feed javaBytes (and bytes for the equivalent Groovy source) into ASM,
// javap, or your bytecode-diff tool of choice for an equivalence assertion. // <3>
assert javaBytes.length > 0
assert javaBytes[0..3] == [(byte)0xCA, (byte)0xFE, (byte)0xBA, (byte)0xBE]
} finally {
out.toFile().deleteDir()
}
// end::javashell_bytecode_capture[]
'''
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1628,7 +1628,7 @@ class Console implements CaretListener, HyperlinkListener, ComponentListener, Fo
Optional<String> optionalPrimaryClassName = findPrimaryClassName(src)
if (optionalPrimaryClassName.isPresent()) {
def js = new JavaShell(Thread.currentThread().contextClassLoader)
js.run(optionalPrimaryClassName.get(), src)
js.run(optionalPrimaryClassName.get(), src, Console.this.scriptArgsArray)
} else {
System.err.println('Initial parsing successful but no public class found. Compile/run will not proceed.')
}
Expand Down
17 changes: 17 additions & 0 deletions subprojects/groovy-console/src/spec/doc/groovy-console.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,23 @@ executed.
* You can turn the System.out capture on and off by selecting `Capture
System.out` from the `Actions` menu

[[GroovyConsole-RunningAsJava]]
=== Running and compiling as Java

The console can also treat the editor contents as Java source rather than Groovy:

* `Script > Run as Java` (shortcut `Alt+R`) compiles the input as Java and invokes its `main`
method, with any program arguments taken from `Script > Set Script Arguments`.
* `Script > Run Selection as Java` does the same for just the highlighted text.
Comment thread
paulk-asert marked this conversation as resolved.
* `Script > Compile as Java` compiles the input without running it — useful as a quick syntax
check.

The source must declare a single top-level public class whose name matches a fully-qualified
binary name in any `package` declaration; the console parses the source to locate that class
and reports if none is found. Compilation and execution go through the
`org.apache.groovy.util.JavaShell` API, so the same `javac` diagnostics surface in the output
area.

[[GroovyConsole-EditingFiles]]
=== Editing Files

Expand Down
Loading