You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by pa...@apache.org on 2023/07/12 03:53:20 UTC
[groovy] branch master updated: GROOVY-11118: Partial JEP 445 compatibility
This is an automated email from the ASF dual-hosted git repository.
paulk pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git
The following commit(s) were added to refs/heads/master by this push:
new 344e7aa3d1 GROOVY-11118: Partial JEP 445 compatibility
344e7aa3d1 is described below
commit 344e7aa3d17e7a38dac54c5a8f2ebd5be3e6a5f3
Author: Paul King <pa...@asert.com.au>
AuthorDate: Tue Jul 4 13:37:21 2023 +1000
GROOVY-11118: Partial JEP 445 compatibility
---
build.gradle | 3 +
src/main/java/groovy/lang/GroovyShell.java | 79 +++++++++-----
.../java/org/codehaus/groovy/ast/ModuleNode.java | 106 +++++++++++++++----
src/spec/doc/core-program-structure.adoc | 113 +++++++++++++++++++--
src/spec/test/ScriptsAndClassesSpecTest.groovy | 53 ++++++++++
src/test/groovy/bugs/Groovy3749Bug.groovy | 77 +++++++++++---
6 files changed, 364 insertions(+), 67 deletions(-)
diff --git a/build.gradle b/build.gradle
index 1fd8de5a24..e31746b1b5 100644
--- a/build.gradle
+++ b/build.gradle
@@ -119,6 +119,9 @@ dependencies {
testImplementation "org.apache.logging.log4j:log4j-core:${versions.log4j2}"
testImplementation "org.slf4j:jcl-over-slf4j:${versions.slf4j}"
testImplementation "com.thoughtworks.qdox:qdox:${versions.qdox}"
+ testImplementation "com.fasterxml.jackson.core:jackson-databind:${versions.jacksonDatabind}"
+ testImplementation "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:${versions.jackson}"
+ testImplementation "com.fasterxml.jackson.core:jackson-annotations:${versions.jackson}"
testFixturesImplementation projects.groovyXml
testFixturesImplementation projects.groovyTest
diff --git a/src/main/java/groovy/lang/GroovyShell.java b/src/main/java/groovy/lang/GroovyShell.java
index 1498857b03..c6bd035ffe 100644
--- a/src/main/java/groovy/lang/GroovyShell.java
+++ b/src/main/java/groovy/lang/GroovyShell.java
@@ -32,6 +32,8 @@ import java.io.IOException;
import java.io.Reader;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
import java.net.URI;
import java.security.PrivilegedAction;
import java.security.PrivilegedActionException;
@@ -40,6 +42,7 @@ import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import static org.codehaus.groovy.control.ResolveVisitor.EMPTY_STRING_ARRAY;
+import static org.codehaus.groovy.runtime.InvokerHelper.EMPTY_ARGS;
import static org.codehaus.groovy.runtime.InvokerHelper.MAIN_METHOD_NAME;
/**
@@ -290,36 +293,62 @@ public class GroovyShell extends GroovyObjectSupport {
}
}
try {
- // let's find a main method
- scriptClass.getMethod(MAIN_METHOD_NAME, String[].class);
- // if that main method exist, invoke it
- return InvokerHelper.invokeMethod(scriptClass, MAIN_METHOD_NAME, new Object[]{args});
- } catch (NoSuchMethodException e) {
- // if it implements Runnable, try to instantiate it
- if (Runnable.class.isAssignableFrom(scriptClass)) {
- return runRunnable(scriptClass, args);
+ // let's find a String[] main method
+ Method stringArrayMain = scriptClass.getMethod(MAIN_METHOD_NAME, String[].class);
+ // if that main method exists, invoke it
+ if (Modifier.isStatic(stringArrayMain.getModifiers())) {
+ return InvokerHelper.invokeStaticMethod(scriptClass, MAIN_METHOD_NAME, new Object[]{args});
+ } else {
+ Object script = InvokerHelper.invokeNoArgumentsConstructorOf(scriptClass);
+ return InvokerHelper.invokeMethod(script, MAIN_METHOD_NAME, args);
}
- GroovyRunnerRegistry runnerRegistry = GroovyRunnerRegistry.getInstance();
- for (GroovyRunner runner : runnerRegistry) {
- if (runner.canRun(scriptClass, this.loader)) {
- return runner.run(scriptClass, this.loader);
- }
+ } catch (NoSuchMethodException ignore) { }
+ try {
+ // let's find an Object main method
+ Method stringArrayMain = scriptClass.getMethod(MAIN_METHOD_NAME, Object.class);
+ // if that main method exists, invoke it
+ if (Modifier.isStatic(stringArrayMain.getModifiers())) {
+ return InvokerHelper.invokeStaticMethod(scriptClass, MAIN_METHOD_NAME, new Object[]{args});
+ } else {
+ Object script = InvokerHelper.invokeNoArgumentsConstructorOf(scriptClass);
+ return InvokerHelper.invokeMethod(script, MAIN_METHOD_NAME, new Object[]{args});
}
- StringBuilder message = new StringBuilder("This script or class could not be run.\n" +
- "It should either:\n" +
- "- have a main method,\n" +
- "- be a JUnit test or extend GroovyTestCase,\n" +
- "- implement the Runnable interface,\n" +
- "- or be compatible with a registered script runner. Known runners:\n");
- if (runnerRegistry.isEmpty()) {
- message.append(" * <none>");
+ } catch (NoSuchMethodException ignore) { }
+ try {
+ // let's find a no-arg main method
+ Method noArgMain = scriptClass.getMethod(MAIN_METHOD_NAME);
+ // if that main method exists, invoke it
+ if (Modifier.isStatic(noArgMain.getModifiers())) {
+ return InvokerHelper.invokeStaticNoArgumentsMethod(scriptClass, MAIN_METHOD_NAME);
} else {
- for (String key : runnerRegistry.keySet()) {
- message.append(" * ").append(key).append("\n");
- }
+ Object script = InvokerHelper.invokeNoArgumentsConstructorOf(scriptClass);
+ return InvokerHelper.invokeMethod(script, MAIN_METHOD_NAME, EMPTY_ARGS);
+ }
+ } catch (NoSuchMethodException ignore) { }
+ // if it implements Runnable, try to instantiate it
+ if (Runnable.class.isAssignableFrom(scriptClass)) {
+ return runRunnable(scriptClass, args);
+ }
+ GroovyRunnerRegistry runnerRegistry = GroovyRunnerRegistry.getInstance();
+ for (GroovyRunner runner : runnerRegistry) {
+ if (runner.canRun(scriptClass, this.loader)) {
+ return runner.run(scriptClass, this.loader);
+ }
+ }
+ StringBuilder message = new StringBuilder("This script or class could not be run.\n" +
+ "It should either:\n" +
+ "- have a main method,\n" +
+ "- be a JUnit test or extend GroovyTestCase,\n" +
+ "- implement the Runnable interface,\n" +
+ "- or be compatible with a registered script runner. Known runners:\n");
+ if (runnerRegistry.isEmpty()) {
+ message.append(" * <none>");
+ } else {
+ for (String key : runnerRegistry.keySet()) {
+ message.append(" * ").append(key).append("\n");
}
- throw new GroovyRuntimeException(message.toString());
}
+ throw new GroovyRuntimeException(message.toString());
}
private static Object runRunnable(Class scriptClass, String[] args) {
diff --git a/src/main/java/org/codehaus/groovy/ast/ModuleNode.java b/src/main/java/org/codehaus/groovy/ast/ModuleNode.java
index 5e22024b79..7d0b44acde 100644
--- a/src/main/java/org/codehaus/groovy/ast/ModuleNode.java
+++ b/src/main/java/org/codehaus/groovy/ast/ModuleNode.java
@@ -18,7 +18,12 @@
*/
package org.codehaus.groovy.ast;
+import org.codehaus.groovy.ast.expr.DeclarationExpression;
+import org.codehaus.groovy.ast.expr.Expression;
+import org.codehaus.groovy.ast.expr.ListExpression;
+import org.codehaus.groovy.ast.expr.VariableExpression;
import org.codehaus.groovy.ast.stmt.BlockStatement;
+import org.codehaus.groovy.ast.stmt.ExpressionStatement;
import org.codehaus.groovy.ast.stmt.Statement;
import org.codehaus.groovy.classgen.GeneratorContext;
import org.codehaus.groovy.control.SourceUnit;
@@ -343,6 +348,49 @@ public class ModuleNode extends ASTNode {
MethodNode existingMain = handleMainMethodIfPresent(methods);
+ boolean hasUncontainedStatements = false;
+ List<FieldNode> fields = new ArrayList<>();
+ // check for uncontained statements (excluding decl statements)
+ for (Statement statement : statementBlock.getStatements()) {
+ if (!(statement instanceof ExpressionStatement)) {
+ hasUncontainedStatements = true;
+ break;
+ }
+ ExpressionStatement es = (ExpressionStatement) statement;
+ Expression expression = es.getExpression();
+ if (!(expression instanceof DeclarationExpression)) {
+ hasUncontainedStatements = true;
+ break;
+ }
+ DeclarationExpression de = (DeclarationExpression) expression;
+ if (de.isMultipleAssignmentDeclaration()) {
+ List<Expression> variables = de.getTupleExpression().getExpressions();
+ if (!(de.getRightExpression() instanceof ListExpression)) break;
+ List<Expression> values = ((ListExpression)de.getRightExpression()).getExpressions();
+ for (int i = 0; i < variables.size(); i++) {
+ VariableExpression var = (VariableExpression) variables.get(i);
+ Expression val = i >= values.size() ? null : values.get(i);
+ fields.add(new FieldNode(var.getName(), var.getModifiers(), var.getType(), null, val));
+ }
+ } else {
+ VariableExpression ve = de.getVariableExpression();
+ fields.add(new FieldNode(ve.getName(), ve.getModifiers(), ve.getType(), null, de.getRightExpression()));
+ }
+ }
+
+ if (existingMain != null && !hasUncontainedStatements) {
+ ClassNode result = new ClassNode(classNode.getName(), 0, ClassHelper.OBJECT_TYPE);
+ result.addAnnotations(existingMain.getAnnotations());
+ result.putNodeMetaData("_SKIPPABLE_ANNOTATIONS", Boolean.TRUE);
+ existingMain.putNodeMetaData("_SKIPPABLE_ANNOTATIONS", Boolean.TRUE);
+// result.getAnnotations().forEach(a -> {
+// // TODO handle AST transform annotations
+// });
+ methods.forEach(result::addMethod);
+ fields.forEach(result::addField);
+ return result;
+ }
+
classNode.addMethod(
new MethodNode(
"main",
@@ -360,12 +408,22 @@ public class ModuleNode extends ASTNode {
)
);
- MethodNode methodNode = new MethodNode("run", ACC_PUBLIC, ClassHelper.OBJECT_TYPE, Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, statementBlock);
- methodNode.setIsScriptBody();
- if (existingMain != null) {
- methodNode.addAnnotations(existingMain.getAnnotations());
+ // we add the run method unless we find a no-arg instance run method
+ // and there are no uncontained statements
+ MethodNode existingRun = hasUncontainedStatements ? null : findRun();
+ if (existingRun == null) {
+ MethodNode methodNode = new MethodNode("run", ACC_PUBLIC, ClassHelper.OBJECT_TYPE, Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, statementBlock);
+ methodNode.setIsScriptBody();
+ if (existingMain != null) {
+ methodNode.addAnnotations(existingMain.getAnnotations());
+ }
+ classNode.addMethod(methodNode);
+ } else {
+ fields.forEach(classNode::addField);
+ classNode.addAnnotations(existingRun.getAnnotations());
+ classNode.putNodeMetaData("_SKIPPABLE_ANNOTATIONS", Boolean.TRUE);
+ existingRun.putNodeMetaData("_SKIPPABLE_ANNOTATIONS", Boolean.TRUE);
}
- classNode.addMethod(methodNode);
classNode.addConstructor(ACC_PUBLIC, Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, new BlockStatement());
@@ -392,34 +450,42 @@ public class ModuleNode extends ASTNode {
return classNode;
}
+ private MethodNode findRun() {
+ for (MethodNode node : methods) {
+ if (node.getName().equals("run") && node.getParameters().length == 0) {
+ return node;
+ }
+ }
+ return null;
+ }
+
/*
* If a main method is provided by user, account for it under run() as scripts generate their own 'main' so they can run.
*/
private MethodNode handleMainMethodIfPresent(final List<MethodNode> methods) {
- boolean found = false;
+ boolean foundInstance = false;
+ boolean foundStatic = false;
MethodNode result = null;
for (Iterator<MethodNode> iter = methods.iterator(); iter.hasNext(); ) {
MethodNode node = iter.next();
- if (node.getName().equals("main")) {
- if (node.isStatic() && node.getParameters().length == 1) {
- boolean retTypeMatches, argTypeMatches;
- ClassNode argType = node.getParameters()[0].getType();
+ if (node.getName().equals("main") && !node.isPrivate()) {
+ int numParams = node.getParameters().length;
+ if (numParams < 2) {
+ ClassNode argType = numParams > 0 ? node.getParameters()[0].getType() : null;
ClassNode retType = node.getReturnType();
- argTypeMatches = (ClassHelper.isObjectType(argType) || argType.getName().contains("String[]"));
- retTypeMatches = (ClassHelper.isPrimitiveVoid(retType) || ClassHelper.isObjectType(retType));
+ boolean argTypeMatches = argType == null || ClassHelper.isObjectType(argType) || argType.getName().contains("String[]");
+ boolean retTypeMatches = ClassHelper.isPrimitiveVoid(retType) || ClassHelper.isObjectType(retType);
if (retTypeMatches && argTypeMatches) {
- if (found) {
+ if ((foundStatic && node.isStatic()) || (foundInstance && !node.isStatic())) {
throw new RuntimeException("Repetitive main method found.");
- } else {
- found = true;
- result = node;
}
- // if script has both loose statements as well as main(), then main() is ignored
- if (statementBlock.isEmpty()) {
- addStatement(node.getCode());
+ if (!foundStatic) { // static trumps instance
+ result = node;
}
- iter.remove();
+
+ if (node.isStatic()) foundStatic = true;
+ else foundInstance = true;
}
}
}
diff --git a/src/spec/doc/core-program-structure.adoc b/src/spec/doc/core-program-structure.adoc
index baded9edce..a4d9c766fd 100644
--- a/src/spec/doc/core-program-structure.adoc
+++ b/src/spec/doc/core-program-structure.adoc
@@ -166,8 +166,12 @@ include::../test/PackageTest.groovy[tags=alias_import,indent=0]
== Scripts versus classes
-=== public static void main vs script
-Groovy supports both scripts and classes. Take the following code for example:
+Groovy supports both scripts and classes. From Groovy 5,
+Groovy also supports https://openjdk.org/jeps/445[JEP 445] compatible scripts.
+
+=== Motivation for scripts
+
+Take the following code for example:
[source,groovy]
.Main.groovy
@@ -187,9 +191,11 @@ Groovy makes it easier, the following code is equivalent:
include::../test/ScriptsAndClassesSpecTest.groovy[tags=groovy_script,indent=0]
----
-A script can be considered as a class without needing to declare it, with some differences.
+A script can be considered as a class without needing to explicitly declare it.
+There are some differences which we'll cover next. First, we'll cover Groovy's
+main `Script` class. Then, we'll cover JEP 445 compatible classes.
-=== Script class
+=== `Script` class
A gapi:groovy.lang.Script[script] is always compiled into a class. The Groovy compiler will compile the class for you,
with the body of the script copied into a `run` method. The previous example is therefore compiled as if it was the
@@ -229,13 +235,17 @@ include::../test/ScriptsAndClassesSpecTest.groovy[tags=multiple_methods_assembly
<2> a method is defined within the script body
<3> and script continues
-This code is internally converted into:
+Statements 1 and 3 are sometimes referred to as "loose" statements.
+They are not contained within an explicit enclosing method or class.
+Loose statements are assembled sequentially into the `run` method.
+
+So, the above code is internally converted into:
[source,groovy]
----
include::../test/ScriptsAndClassesSpecTest.groovy[tags=multiple_methods_assembly_equiv,indent=0]
----
-<1> the `power` method is copied as is into the generated script class
+<1> the `power` method is copied as-is into the generated script class
<2> first statement is copied into the `run` method
<3> second statement is copied into the `run` method
@@ -273,5 +283,94 @@ TIP: Another approach to making a variable visible to all methods, is to use the
<<{core-metaprogramming}#xform-Field,@Field annotation>>.
A variable annotated this way will become a field of the generated script class and,
as for local variables, access won't involve the script `Binding`.
-While not recommended, if you have a local variable or script field with the same name as a binding variable,
+If you have a local variable or script field with the same name as a binding variable,
+we recommend renaming one of them to avoid potential confusion. If that's not possible,
you can use `binding.varName` to access the binding variable.
+
+=== Convenience variations
+
+As mentioned previously, normally, `public static void main` and `run` methods
+are automatically added to your script, so it is normally illegal to add your own versions
+of either of those; you would see a duplicate method compiler error if you tried.
+
+However, there are some exceptions where the above rules don't apply.
+If your script contains _only_ a compatible main method and no other loose statements,
+or _only_ a no-arg `run` instance method (from Groovy 5), then it is allowed.
+In this case, no loose statements (because there aren't any) are collected into the `run` method.
+The method you supplied is used instead of Groovy adding the respective method(s).
+
+This can be useful if you need to add an annotation to the otherwise implicitly added
+`main` or `run` methods as this example shows:
+
+[source,groovy]
+----
+include::../test/ScriptsAndClassesSpecTest.groovy[tags=script_with_explicit_static_main,indent=0]
+----
+
+To be recognised as a convenience variation, as well as having no loose statements,
+the parameter for the `main` method should be:
+
+* untyped as above (`Object` type),
+* or of type `String[]`,
+* or have no arguments (from Groovy 5).
+
+From Groovy 5, a no-arg instance `run` variant is also supported.
+This also allows annotations to be added.
+The `run` variant follows the JEP 445 rules for field declarations
+(hence doesn't need to use the `@Field` annotation)
+as this example involving Jackson JSON serialization shows:
+
+[source,groovy]
+----
+include::../test/ScriptsAndClassesSpecTest.groovy[tags=script_with_explicit_instance_run,indent=0]
+----
+
+The `run` variant is recommended if you need your script to extend the `Script` class
+and have access to the script context and bindings. If you don't have that requirement,
+providing one of the `main` variants will create a JEP 445 compatible class which won't
+extend `Script`. We'll cover JEP 445 compatible scripts in more detail next.
+
+== JEP 445 compatible scripts
+
+From Groovy 5, support has been added for JEP 445 compatible scripts containing
+a `main` method. Such scripts have several differences to normal Groovy `Script` classes:
+
+* they won't have a `public static void main` method added
+* they won't extend the `Script` class and hence won't have access to the script
+context or binding variables
+* allows additional class-level _fields_ and _methods_ to be defined in addition to `main`
+* can't have "loose" statements outside the `main` method (excluding any field definitions)
+
+A simple example might look like:
+
+[source,groovy]
+----
+include::../test/ScriptsAndClassesSpecTest.groovy[tags=jep445_barescript,indent=0]
+----
+
+An example with additional fields and methods might look like:
+
+[source,groovy]
+----
+include::../test/ScriptsAndClassesSpecTest.groovy[tags=jep445_script,indent=0]
+----
+<1> Note that multi-assignment syntax is supported and results in separate field definitions for each component.
+
+=== Differences with Java JEP 445 behavior
+
+There are some differences with Groovy's JEP 445 support and that offered by Java:
+
+* Java supports either a no-arg `main` method or one containing a single `String[]` parameter.
+Groovy also adds support for a single untyped (`Object`) parameter, e.g. `def main(args) { ... }`.
+This addition is known by the Groovy runner but would not be known by the
+Java launch protocol for a JDK supporting JEP 445.
+* Java supports `void` main methods. Groovy also adds support for untyped `def` (`Object`) methods,
+e.g. `def main(...)` as well as `void main(...)`.
+This addition is known by the Groovy runner but would not be known by the Java launch protocol
+for a JDK supporting JEP 445.
+* For static `main` variants, Groovy _promotes_ the no-arg or untyped variants to have the
+standard `public static void main(String[] args)` signature. This is for compatibility
+with versions of Groovy prior to Groovy 5 (where JEP 445 support was added).
+As a consequence, such classes are compatible with the Java launch protocol prior to JEP 445 support.
+* Groovy's runner has been made aware of JEP 445 compatible classes and can run all variations
+for JDK11 and above and without the need for preview mode to be enabled.
diff --git a/src/spec/test/ScriptsAndClassesSpecTest.groovy b/src/spec/test/ScriptsAndClassesSpecTest.groovy
index e36eaf23b8..a97f3f42b2 100644
--- a/src/spec/test/ScriptsAndClassesSpecTest.groovy
+++ b/src/spec/test/ScriptsAndClassesSpecTest.groovy
@@ -48,6 +48,55 @@ class ScriptsAndClassesSpecTest extends GroovyTestCase {
}
// end::groovy_script_equiv[]
'''
+
+ assertScript '''
+ import groovy.transform.CompileStatic
+ // tag::script_with_explicit_static_main[]
+ @CompileStatic
+ static main(args) {
+ println 'Groovy world!'
+ }
+ // end::script_with_explicit_static_main[]
+ '''
+
+ assertScript '''
+ import groovy.transform.*
+ import com.fasterxml.jackson.annotation.*
+ import com.fasterxml.jackson.databind.ObjectMapper
+
+ // tag::script_with_explicit_instance_run[]
+ @JsonIgnoreProperties(["binding"])
+ def run() {
+ var mapper = new ObjectMapper()
+ assert mapper.writeValueAsString(this) == '{"pets":["cat","dog"]}'
+ }
+
+ public pets = ['cat', 'dog']
+ // end::script_with_explicit_instance_run[]
+ '''
+ }
+
+ void testJep445Definition() {
+ runScript '''
+ // tag::jep445_barescript[]
+ void main(args) {
+ println new Date()
+ }
+ // end::jep445_barescript[]
+ '''
+
+ runScript '''
+ // tag::jep445_script[]
+ def main() {
+ assert upper(foo) + lower(bar) == 'FOObar'
+ }
+
+ def upper(s) { s.toUpperCase() }
+
+ def lower = String::toLowerCase
+ def (foo, bar) = ['Foo', 'Bar'] // <1>
+ // end::jep445_script[]
+ '''
}
void testMethodDefinition() {
@@ -103,4 +152,8 @@ class ScriptsAndClassesSpecTest extends GroovyTestCase {
// end::script_with_untyped_variables[]
'''
}
+
+ private static void runScript(String scriptText) {
+ new GroovyShell().run(scriptText, 'ScriptSnippet', [] as String[])
+ }
}
diff --git a/src/test/groovy/bugs/Groovy3749Bug.groovy b/src/test/groovy/bugs/Groovy3749Bug.groovy
index d7cada5ec1..0b3adbdaa9 100644
--- a/src/test/groovy/bugs/Groovy3749Bug.groovy
+++ b/src/test/groovy/bugs/Groovy3749Bug.groovy
@@ -24,48 +24,48 @@ class Groovy3749Bug extends GroovyTestCase {
void testScriptsProvidingStaticMainMethod() {
def scriptStr
- // test various signatures of main()
+ // test various signatures of static main()
scriptStr = """
static main(args) {
throw new RuntimeException('main called')
}
"""
- verifyScriptRun(scriptStr, "RuntimeException")
+ assertScriptFails(scriptStr, "RuntimeException")
scriptStr = """
static def main(args) {
throw new RuntimeException('main called')
}
"""
- verifyScriptRun(scriptStr, "RuntimeException")
+ assertScriptFails(scriptStr, "RuntimeException")
scriptStr = """
static void main(args) {
throw new RuntimeException('main called')
}
"""
- verifyScriptRun(scriptStr, "RuntimeException")
+ assertScriptFails(scriptStr, "RuntimeException")
scriptStr = """
static main(String[] args) {
throw new RuntimeException('main called')
}
"""
- verifyScriptRun(scriptStr, "RuntimeException")
+ assertScriptFails(scriptStr, "RuntimeException")
scriptStr = """
static def main(String[] args) {
throw new RuntimeException('main called')
}
"""
- verifyScriptRun(scriptStr, "RuntimeException")
+ assertScriptFails(scriptStr, "RuntimeException")
scriptStr = """
static void main(String[] args) {
throw new RuntimeException('main called')
}
"""
- verifyScriptRun(scriptStr, "RuntimeException")
+ assertScriptFails(scriptStr, "RuntimeException")
// if both main() and the loose statements are provided, then the loose statements should run and not main
scriptStr = """
@@ -74,20 +74,67 @@ class Groovy3749Bug extends GroovyTestCase {
}
throw new Error()
"""
- verifyScriptRun(scriptStr, "Error")
+ assertScriptFails(scriptStr, "Error")
- assertScript """
- def main(args) {
+ scriptStr = """
+ static void main() {
+ throw new RuntimeException('main called')
+ }
+ """
+ assertScriptFails(scriptStr, "RuntimeException")
+
+ // if param type doesn't match, this main won't execute
+ runScript """
+ static main(Date args) {
throw new RuntimeException('main called')
}
"""
}
- void verifyScriptRun(scriptText, expectedFailure) {
- try{
- assertScript(scriptText)
- }catch(Throwable ex) {
- assertTrue ex.class.name.contains(expectedFailure)
+ void testScriptsProvidingInstanceMainMethod() {
+ def scriptStr
+
+ // test various signatures of instance main()
+ scriptStr = """
+ def main(String[] args) {
+ throw new RuntimeException('main called')
+ }
+ """
+ assertScriptFails(scriptStr, "RuntimeException")
+
+ scriptStr = """
+ void main(args) {
+ throw new RuntimeException('main called')
+ }
+ """
+ assertScriptFails(scriptStr, "RuntimeException")
+
+ scriptStr = """
+ void main() {
+ throw new RuntimeException('main called')
+ }
+ """
+ assertScriptFails(scriptStr, "RuntimeException")
+
+ // if param type doesn't match, this main won't execute
+ runScript """
+ def main(Date args) {
+ throw new RuntimeException('main called')
+ }
+ """
+ }
+
+ static void assertScriptFails(scriptText, expectedFailure) {
+ try {
+ runScript(scriptText)
+ } catch (Throwable ex) {
+ assert ex.class.name.contains(expectedFailure)
+ return
}
+ fail("Expected script to fail with '$expectedFailure' but passed.")
+ }
+
+ private static void runScript(String scriptText) {
+ new GroovyShell().run(scriptText, 'Groovy3749Snippet', [] as String[])
}
}