The implementation is Spock-dependent and is only compiled into the
+ * Spock-supporting build variants (Groovy 3/4/5). On variants without Spock
+ * (e.g. Groovy 6) no provider is registered and {@link java.util.ServiceLoader}
+ * resolution yields no result, in which case the executor reports that the
+ * feature is not supported on the running Groovy version.
+ */
+public interface SpecSupport {
+
+ /**
+ * Compiles and runs the given script as one or more Spock specifications.
+ *
+ * @param code the script source
+ * @return the rendered specification result tree
+ */
+ Object runSpec(String code);
+
+ /**
+ * @return the Spock version available on this build, for reporting in execution info
+ */
+ String spockVersion();
+
+ /**
+ * Renders the AST of the given script at the requested compile phase.
+ *
+ * @param code the script source
+ * @param phase the compile phase name (case-insensitive)
+ * @param isSpock whether the script is a Spock specification
+ * @return the transpiled source
+ */
+ String renderAst(String code, String phase, boolean isSpock);
+}
diff --git a/functions/groovy-executor/src/no-spock-test/groovy/gwc/GFunctionExecutorFallbackTest.groovy b/functions/groovy-executor/src/no-spock-test/groovy/gwc/GFunctionExecutorFallbackTest.groovy
new file mode 100644
index 00000000..9550b730
--- /dev/null
+++ b/functions/groovy-executor/src/no-spock-test/groovy/gwc/GFunctionExecutorFallbackTest.groovy
@@ -0,0 +1,81 @@
+package gwc
+
+import com.google.cloud.functions.HttpRequest
+import com.google.cloud.functions.HttpResponse
+import groovy.json.JsonOutput
+import groovy.json.JsonSlurper
+import org.junit.jupiter.api.Test
+
+import static org.junit.jupiter.api.Assertions.assertEquals
+import static org.junit.jupiter.api.Assertions.assertNull
+import static org.junit.jupiter.api.Assertions.assertTrue
+
+/**
+ * Exercises {@link GFunctionExecutor} on a build WITHOUT Spock (the Groovy 6 variant).
+ * Plain Groovy must run normally; Spock specs and the AST view must report that they
+ * are not supported instead of failing obscurely.
+ */
+class GFunctionExecutorFallbackTest {
+
+ private final GFunctionExecutor executor = new GFunctionExecutor()
+
+ private Map invoke(Map payload) {
+ def reader = new BufferedReader(new StringReader(JsonOutput.toJson(payload)))
+ def output = new StringWriter()
+ def writer = new BufferedWriter(output)
+ def request = [
+ getMethod : { 'POST' },
+ getContentType: { Optional.of('application/json') },
+ getReader : { reader },
+ ] as HttpRequest
+ def response = [
+ appendHeader : { String name, String value -> },
+ setContentType: { String contentType -> },
+ setStatusCode : { int code -> },
+ getWriter : { writer },
+ ] as HttpResponse
+
+ // The executor writes the response and closes the writer (try-with-resources),
+ // which flushes it into `output`.
+ executor.service(request, response)
+ new JsonSlurper().parseText(output.toString()) as Map
+ }
+
+ @Test
+ void 'plain Groovy script returns its result'() {
+ def response = invoke(code: '1 + 1')
+ assertEquals(2, response.result)
+ assertEquals('', response.err)
+ }
+
+ @Test
+ void 'plain Groovy script output is captured'() {
+ def response = invoke(code: "print 'Hello World'")
+ assertEquals('Hello World', response.out)
+ assertEquals('', response.err)
+ }
+
+ @Test
+ void 'Spock specification reports not supported'() {
+ def response = invoke(code: '''
+class ASpec extends Specification {
+ def "hello world"() {
+ expect: true
+ }
+}
+''')
+ assertTrue(
+ response.err.contains('Spock specifications are not supported'),
+ "unexpected err: ${response.err}")
+ assertNull(response.result)
+ }
+
+ @Test
+ void 'AST action reports not supported'() {
+ def response = invoke(code: 'def x = 1', action: 'ast', astPhase: 'CONVERSION')
+ assertTrue(
+ response.err.contains('The AST view is not supported'),
+ "unexpected err: ${response.err}")
+ assertNull(response.result)
+ }
+}
diff --git a/functions/groovy-executor/src/main/java/gwc/spock/AstRenderer.java b/functions/groovy-executor/src/spock/java/gwc/spock/AstRenderer.java
similarity index 100%
rename from functions/groovy-executor/src/main/java/gwc/spock/AstRenderer.java
rename to functions/groovy-executor/src/spock/java/gwc/spock/AstRenderer.java
diff --git a/functions/groovy-executor/src/main/java/gwc/spock/ScriptCompiler.java b/functions/groovy-executor/src/spock/java/gwc/spock/ScriptCompiler.java
similarity index 100%
rename from functions/groovy-executor/src/main/java/gwc/spock/ScriptCompiler.java
rename to functions/groovy-executor/src/spock/java/gwc/spock/ScriptCompiler.java
diff --git a/functions/groovy-executor/src/main/java/gwc/spock/ScriptRunner.java b/functions/groovy-executor/src/spock/java/gwc/spock/ScriptRunner.java
similarity index 100%
rename from functions/groovy-executor/src/main/java/gwc/spock/ScriptRunner.java
rename to functions/groovy-executor/src/spock/java/gwc/spock/ScriptRunner.java
diff --git a/functions/groovy-executor/src/spock/java/gwc/spock/SpockSpecSupport.java b/functions/groovy-executor/src/spock/java/gwc/spock/SpockSpecSupport.java
new file mode 100644
index 00000000..dcb5ddf1
--- /dev/null
+++ b/functions/groovy-executor/src/spock/java/gwc/spock/SpockSpecSupport.java
@@ -0,0 +1,29 @@
+package gwc.spock;
+
+import gwc.spi.SpecSupport;
+import org.spockframework.util.SpockReleaseInfo;
+
+/**
+ * Spock-backed {@link SpecSupport} provider, registered via {@code ServiceLoader}.
+ * Only present on the Spock-supporting build variants (Groovy 3/4/5).
+ */
+public class SpockSpecSupport implements SpecSupport {
+
+ @Override
+ public Object runSpec(String code) {
+ ScriptRunner scriptRunner = new ScriptRunner();
+ // TODO revisit colored output
+ scriptRunner.setDisableColors(true);
+ return scriptRunner.run(code);
+ }
+
+ @Override
+ public String renderAst(String code, String phase, boolean isSpock) {
+ return new AstRenderer().render(code, phase, isSpock);
+ }
+
+ @Override
+ public String spockVersion() {
+ return SpockReleaseInfo.getVersion().toString();
+ }
+}
diff --git a/functions/groovy-executor/src/main/java/gwc/spock/output/Color.java b/functions/groovy-executor/src/spock/java/gwc/spock/output/Color.java
similarity index 100%
rename from functions/groovy-executor/src/main/java/gwc/spock/output/Color.java
rename to functions/groovy-executor/src/spock/java/gwc/spock/output/Color.java
diff --git a/functions/groovy-executor/src/main/java/gwc/spock/output/Theme.java b/functions/groovy-executor/src/spock/java/gwc/spock/output/Theme.java
similarity index 100%
rename from functions/groovy-executor/src/main/java/gwc/spock/output/Theme.java
rename to functions/groovy-executor/src/spock/java/gwc/spock/output/Theme.java
diff --git a/functions/groovy-executor/src/main/java/gwc/spock/output/TreeNode.java b/functions/groovy-executor/src/spock/java/gwc/spock/output/TreeNode.java
similarity index 100%
rename from functions/groovy-executor/src/main/java/gwc/spock/output/TreeNode.java
rename to functions/groovy-executor/src/spock/java/gwc/spock/output/TreeNode.java
diff --git a/functions/groovy-executor/src/main/java/gwc/spock/output/TreePrinter.java b/functions/groovy-executor/src/spock/java/gwc/spock/output/TreePrinter.java
similarity index 100%
rename from functions/groovy-executor/src/main/java/gwc/spock/output/TreePrinter.java
rename to functions/groovy-executor/src/spock/java/gwc/spock/output/TreePrinter.java
diff --git a/functions/groovy-executor/src/main/java/gwc/spock/output/TreePrintingListener.java b/functions/groovy-executor/src/spock/java/gwc/spock/output/TreePrintingListener.java
similarity index 100%
rename from functions/groovy-executor/src/main/java/gwc/spock/output/TreePrintingListener.java
rename to functions/groovy-executor/src/spock/java/gwc/spock/output/TreePrintingListener.java
diff --git a/functions/groovy-executor/src/main/java/gwc/spock/output/package-info.java b/functions/groovy-executor/src/spock/java/gwc/spock/output/package-info.java
similarity index 100%
rename from functions/groovy-executor/src/main/java/gwc/spock/output/package-info.java
rename to functions/groovy-executor/src/spock/java/gwc/spock/output/package-info.java
diff --git a/functions/groovy-executor/src/spock/resources/META-INF/services/gwc.spi.SpecSupport b/functions/groovy-executor/src/spock/resources/META-INF/services/gwc.spi.SpecSupport
new file mode 100644
index 00000000..40ef102a
--- /dev/null
+++ b/functions/groovy-executor/src/spock/resources/META-INF/services/gwc.spi.SpecSupport
@@ -0,0 +1 @@
+gwc.spock.SpockSpecSupport
diff --git a/functions/pom.xml b/functions/pom.xml
index c02b0196..5be8cf8d 100644
--- a/functions/pom.xml
+++ b/functions/pom.xml
@@ -16,6 +16,7 @@