From c6c1768b202ec45363bf4653a74c0965df628c85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonard=20Br=C3=BCnings?= Date: Wed, 24 Jan 2024 18:04:20 +0100 Subject: [PATCH] Enable parameter injection for @TempDir and run as initialization interceptor for fields (#1693) * Enable parameter injection for @TempDir. * Prior to this commit @TempDir values were not available to other field initializers, now they can be used to initialize other fields. * Fixed exception when configured baseDir did not exist, now @TempDir will create the baseDir directory if it is missing. --- docs/extensions.adoc | 6 +- docs/release_notes.adoc | 3 + .../extension/builtin/TempDirExtension.java | 40 ++---- .../extension/builtin/TempDirInterceptor.java | 120 ++++++++++++++---- .../util/IThrowableBiConsumer.java | 27 ++++ .../util/IThrowableFunction.java | 1 + .../src/main/java/spock/lang/TempDir.java | 6 +- .../docs/extension/TempDirDocSpec.groovy | 14 +- .../extension/TempDirExtensionSpec.groovy | 34 ++++- 9 files changed, 196 insertions(+), 55 deletions(-) create mode 100644 spock-core/src/main/java/org/spockframework/util/IThrowableBiConsumer.java diff --git a/docs/extensions.adoc b/docs/extensions.adoc index 35e9709e42..b8f23c03ad 100644 --- a/docs/extensions.adoc +++ b/docs/extensions.adoc @@ -533,7 +533,11 @@ def ignoreMyExceptions In order to generate a temporary directory for test and delete it after test, annotate a member field of type `java.io.File`, `java.nio.file.Path` or untyped using `def` in a spec class (`def` will inject a `Path`). Alternatively, you can annotate a field with a custom type that has a public constructor accepting either `java.io.File` or `java.nio.file.Path` as its single parameter (see <> for an example). -If the annotated field is `@Shared`, the temporary directory will be shared in the corresponding specification, otherwise every feature method and every iteration per parametrized feature method will have their own temporary directories: +If the annotated field is `@Shared`, the temporary directory will be shared in the corresponding specification, otherwise every feature method and every iteration per parametrized feature method will have their own temporary directories. + +Since Spock 2.4, you can also use parameter injection with `@TempDir`. +The types follow the same rules as for field injection. +Valid methods are `setup()`, `setupSpec()`, or any feature methods. [source,groovy,indent=0] ---- diff --git a/docs/release_notes.adoc b/docs/release_notes.adoc index ef8a610dca..4b56107c9b 100644 --- a/docs/release_notes.adoc +++ b/docs/release_notes.adoc @@ -14,6 +14,9 @@ include::include.adoc[] === Misc * Add support for <> +* Add support for parameter injection of `@TempDir` +* Improve `@TempDir` field injection, now it happens before field initialization, so it can be used by other field initializers. +* Fix exception when configured `baseDir` was not existing, now `@TempDir` will create the baseDir directory if it is missing. * Fix bad error message for collection conditions, when one of the operands is `null` == 2.4-M1 (2022-11-30) diff --git a/spock-core/src/main/java/org/spockframework/runtime/extension/builtin/TempDirExtension.java b/spock-core/src/main/java/org/spockframework/runtime/extension/builtin/TempDirExtension.java index 4515f6a527..33c0c1bd42 100644 --- a/spock-core/src/main/java/org/spockframework/runtime/extension/builtin/TempDirExtension.java +++ b/spock-core/src/main/java/org/spockframework/runtime/extension/builtin/TempDirExtension.java @@ -10,6 +10,8 @@ import java.io.File; import java.lang.reflect.Constructor; import java.nio.file.Path; +import java.util.EnumSet; +import java.util.Set; /** * @author dqyuan @@ -17,6 +19,8 @@ */ @Beta public class TempDirExtension implements IAnnotationDrivenExtension { + + private static final Set VALID_METHOD_KINDS = EnumSet.of(MethodKind.SETUP, MethodKind.SETUP_SPEC, MethodKind.FEATURE); private final TempDirConfiguration configuration; public TempDirExtension(TempDirConfiguration configuration) { @@ -25,41 +29,21 @@ public TempDirExtension(TempDirConfiguration configuration) { @Override public void visitFieldAnnotation(TempDir annotation, FieldInfo field) { - Class fieldType = field.getType(); - IThrowableFunction mapper = createPathToFieldTypeMapper(fieldType); - TempDirInterceptor interceptor = new TempDirInterceptor(mapper, field, configuration.baseDir, configuration.keep); + TempDirInterceptor interceptor = TempDirInterceptor.forField(field, configuration.baseDir, configuration.keep); // attach interceptor SpecInfo specInfo = field.getParent(); if (field.isShared()) { - specInfo.getBottomSpec().addInterceptor(interceptor); + specInfo.getBottomSpec().addSharedInitializerInterceptor(interceptor); } else { - for (FeatureInfo featureInfo : specInfo.getBottomSpec().getAllFeatures()) { - featureInfo.addIterationInterceptor(interceptor); - } + specInfo.addInitializerInterceptor(interceptor); } } - private IThrowableFunction createPathToFieldTypeMapper(Class fieldType) { - if (fieldType.isAssignableFrom(Path.class) || Object.class.equals(fieldType)) { - return p -> p; - } - if (fieldType.isAssignableFrom(File.class)) { - return Path::toFile; - } - - try { - return fieldType.getConstructor(Path.class)::newInstance; - } catch (NoSuchMethodException ignore) { - // fall through - } - try { - Constructor constructor = fieldType.getConstructor(File.class); - return path -> constructor.newInstance(path.toFile()); - } catch (NoSuchMethodException ignore) { - // fall through - } - throw new InvalidSpecException("@TempDir can only be used on File, Path, untyped field, " + - "or class that takes Path or File as single constructor argument."); + @Override + public void visitParameterAnnotation(TempDir annotation, ParameterInfo parameter) { + Checks.checkArgument(VALID_METHOD_KINDS.contains(parameter.getParent().getKind()), () -> "@TempDir can only be used on setup, setupSpec or feature method parameters."); + TempDirInterceptor interceptor = TempDirInterceptor.forParameter(parameter, configuration.baseDir, configuration.keep); + parameter.getParent().addInterceptor(interceptor); } } diff --git a/spock-core/src/main/java/org/spockframework/runtime/extension/builtin/TempDirInterceptor.java b/spock-core/src/main/java/org/spockframework/runtime/extension/builtin/TempDirInterceptor.java index a53ffa8b09..650a19bc1b 100644 --- a/spock-core/src/main/java/org/spockframework/runtime/extension/builtin/TempDirInterceptor.java +++ b/spock-core/src/main/java/org/spockframework/runtime/extension/builtin/TempDirInterceptor.java @@ -1,17 +1,26 @@ package org.spockframework.runtime.extension.builtin; -import org.spockframework.runtime.extension.*; +import org.codehaus.groovy.runtime.ResourceGroovyMethods; +import org.spockframework.runtime.InvalidSpecException; +import org.spockframework.runtime.extension.IMethodInterceptor; +import org.spockframework.runtime.extension.IMethodInvocation; +import org.spockframework.runtime.extension.IStore; import org.spockframework.runtime.model.FieldInfo; +import org.spockframework.runtime.model.ParameterInfo; import org.spockframework.util.*; +import java.io.File; import java.io.IOException; -import java.nio.file.*; +import java.lang.reflect.Constructor; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; import java.util.regex.Pattern; -import org.codehaus.groovy.runtime.ResourceGroovyMethods; - import static java.nio.file.FileVisitResult.CONTINUE; +import static org.spockframework.runtime.model.MethodInfo.MISSING_ARGUMENT; /** * @author dqyuan @@ -19,18 +28,24 @@ */ @Beta public class TempDirInterceptor implements IMethodInterceptor { + + private static final IStore.Namespace NAMESPACE = IStore.Namespace.create(TempDirInterceptor.class); private static final String TEMP_DIR_PREFIX = "spock"; private static final Pattern VALID_CHARS = Pattern.compile("[^a-zA-Z0-9_.-]++"); - private final IThrowableFunction pathToFieldMapper; - private final FieldInfo fieldInfo; + private final IThrowableBiConsumer valueSetter; + private final String name; private final Path parentDir; private final boolean keep; - TempDirInterceptor(IThrowableFunction pathToFieldMapper, FieldInfo fieldInfo, - Path parentDir, boolean keep) { - this.pathToFieldMapper = pathToFieldMapper; - this.fieldInfo = fieldInfo; + private TempDirInterceptor( + IThrowableBiConsumer valueSetter, + String name, + Path parentDir, + boolean keep) { + + this.valueSetter = valueSetter; + this.name = name; this.parentDir = parentDir; this.keep = keep; } @@ -50,7 +65,7 @@ private String dirPrefix(IMethodInvocation invocation) { if (invocation.getIteration() != null) { prefix.append('_').append(invocation.getIteration().getIterationIndex()); } - return prefix.append('_').append(fieldInfo.getName()).toString(); + return prefix.append('_').append(name).toString(); } private Path generateTempDir(IMethodInvocation invocation) throws IOException { @@ -58,32 +73,50 @@ private Path generateTempDir(IMethodInvocation invocation) throws IOException { if (parentDir == null) { return Files.createTempDirectory(prefix); } + if (!Files.exists(parentDir)) { + Files.createDirectories(parentDir); + } return Files.createTempDirectory(parentDir, prefix); } protected Path setUp(IMethodInvocation invocation) throws Exception { Path tempPath = generateTempDir(invocation); - fieldInfo.writeValue(invocation.getInstance(), pathToFieldMapper.apply(tempPath)); + valueSetter.accept(invocation, tempPath); return tempPath; } - protected void destroy(Path path) throws IOException { - if (!keep) { - deleteTempDir(path); - } - } - @Override public void intercept(IMethodInvocation invocation) throws Throwable { Path path = setUp(invocation); + invocation.getStore(NAMESPACE).put(path, new TempDirContainer(path, keep)); + invocation.proceed(); + } + + + private static IThrowableFunction createPathToTypeMapper(Class targetType) { + if (targetType.isAssignableFrom(Path.class) || Object.class.equals(targetType)) { + return p -> p; + } + if (targetType.isAssignableFrom(File.class)) { + return Path::toFile; + } + + try { + return targetType.getConstructor(Path.class)::newInstance; + } catch (NoSuchMethodException ignore) { + // fall through + } try { - invocation.proceed(); - } finally { - destroy(path); + Constructor constructor = targetType.getConstructor(File.class); + return path -> constructor.newInstance(path.toFile()); + } catch (NoSuchMethodException ignore) { + // fall through } + throw new InvalidSpecException("@TempDir can only be used on File, Path, untyped field, " + + "or class that takes Path or File as single constructor argument."); } - private void deleteTempDir(Path tempPath) throws IOException { + private static void deleteTempDir(Path tempPath) throws IOException { if (Files.notExists(tempPath)) { return; } @@ -96,7 +129,7 @@ private void deleteTempDir(Path tempPath) throws IOException { ResourceGroovyMethods.deleteDir(tempPath.toFile()); } - private void tryMakeWritable(Path tempPath) throws IOException { + private static void tryMakeWritable(Path tempPath) throws IOException { Files.walkFileTree(tempPath, MakeWritableVisitor.INSTANCE); } @@ -115,4 +148,45 @@ public FileVisitResult postVisitDirectory(Path dir, IOException exc) { return CONTINUE; } } + + static TempDirInterceptor forField(FieldInfo fieldInfo, Path parentDir, boolean keep) { + IThrowableFunction typeMapper = createPathToTypeMapper(fieldInfo.getType()); + return new TempDirInterceptor( + (invocation, path) -> fieldInfo.writeValue(invocation.getInstance(), typeMapper.apply(path)), + fieldInfo.getName(), + parentDir, + keep); + } + + static TempDirInterceptor forParameter(ParameterInfo parameterInfo, Path parentDir, boolean keep) { + IThrowableFunction typeMapper = createPathToTypeMapper(parameterInfo.getReflection().getType()); + return new TempDirInterceptor( + (IMethodInvocation invocation, Path path) -> { + invocation.resolveArgument(parameterInfo.getIndex(), typeMapper.apply(path)); + }, + parameterInfo.getName(), + parentDir, + keep); + } + + static class TempDirContainer implements AutoCloseable { + private final Path path; + private final boolean keep; + + TempDirContainer(Path path, boolean keep) { + this.path = path; + this.keep = keep; + } + + @Override + public void close() { + if (!keep) { + try { + deleteTempDir(path); + } catch (IOException e) { + ExceptionUtil.sneakyThrow(e); + } + } + } + } } diff --git a/spock-core/src/main/java/org/spockframework/util/IThrowableBiConsumer.java b/spock-core/src/main/java/org/spockframework/util/IThrowableBiConsumer.java new file mode 100644 index 0000000000..bccf9d4235 --- /dev/null +++ b/spock-core/src/main/java/org/spockframework/util/IThrowableBiConsumer.java @@ -0,0 +1,27 @@ +/* + * Copyright 2024 the original author or authors. + * + * Licensed 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 + * https://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 org.spockframework.util; + +/** + * A BiConsumer that may throw a checked exception. + * + * @author Leonard Brünings + * @since 2.4 + */ +@FunctionalInterface +public interface IThrowableBiConsumer { + void accept(A a, B b) throws T; +} diff --git a/spock-core/src/main/java/org/spockframework/util/IThrowableFunction.java b/spock-core/src/main/java/org/spockframework/util/IThrowableFunction.java index e2bc69cf10..4ae0d6872b 100644 --- a/spock-core/src/main/java/org/spockframework/util/IThrowableFunction.java +++ b/spock-core/src/main/java/org/spockframework/util/IThrowableFunction.java @@ -21,6 +21,7 @@ * * @author Peter Niederwieser */ +@FunctionalInterface public interface IThrowableFunction { C apply(D value) throws T; } diff --git a/spock-core/src/main/java/spock/lang/TempDir.java b/spock-core/src/main/java/spock/lang/TempDir.java index ba8c37dbb1..6435a99e10 100644 --- a/spock-core/src/main/java/spock/lang/TempDir.java +++ b/spock-core/src/main/java/spock/lang/TempDir.java @@ -36,13 +36,17 @@ * @TempDir * FileSystemFixture fsFixture // will inject an instance of FileSystemFixture with the temp path injected via constructor * + *

+ * Since Spock 2.4, {@code @TempDir} can be used to annotate a method parameter, with the same behavior as the field types. + * Use this if you want to use a temp directory in a single method only. + * Valid methods are {@code setup()}, {@code setupSpec()}, or any feature methods. * * @author dqyuan * @since 2.0 */ @Beta @Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.FIELD) +@Target({ElementType.FIELD, ElementType.PARAMETER}) @ExtensionAnnotation(TempDirExtension.class) public @interface TempDir { } diff --git a/spock-specs/src/test/groovy/org/spockframework/docs/extension/TempDirDocSpec.groovy b/spock-specs/src/test/groovy/org/spockframework/docs/extension/TempDirDocSpec.groovy index 1c10d5b537..2dc99d3591 100644 --- a/spock-specs/src/test/groovy/org/spockframework/docs/extension/TempDirDocSpec.groovy +++ b/spock-specs/src/test/groovy/org/spockframework/docs/extension/TempDirDocSpec.groovy @@ -29,12 +29,24 @@ class TempDirDocSpec extends Specification { @TempDir FileSystemFixture path4 - def demo() { + // Use for parameter injection of a setupSpec method + def setupSpec(@TempDir Path sharedPath) { + assert sharedPath instanceof Path + } + + // Use for parameter injection of a setup method + def setup(@TempDir Path setupPath) { + assert setupPath instanceof Path + } + + // Use for parameter injection of a feature + def demo(@TempDir Path path5) { expect: path1 instanceof Path path2 instanceof File path3 instanceof Path path4 instanceof FileSystemFixture + path5 instanceof Path } // end::example[] diff --git a/spock-specs/src/test/groovy/org/spockframework/smoke/extension/TempDirExtensionSpec.groovy b/spock-specs/src/test/groovy/org/spockframework/smoke/extension/TempDirExtensionSpec.groovy index 90bd8cace1..3b65a8abe8 100644 --- a/spock-specs/src/test/groovy/org/spockframework/smoke/extension/TempDirExtensionSpec.groovy +++ b/spock-specs/src/test/groovy/org/spockframework/smoke/extension/TempDirExtensionSpec.groovy @@ -86,7 +86,7 @@ class TempDirExtensionSpec extends EmbeddedSpecification { def "define temp directory location and keep temp directory using configuration script"() { given: - def userDefinedBase = Paths.get("build") + def userDefinedBase = untypedPath.resolve("build") runner.configurationScript { tempdir { baseDir userDefinedBase @@ -271,6 +271,38 @@ class CustomTempDirSpec extends Specification { } } +class TempDirParameterSpec extends Specification { + @Shared + Path setupSpecParameter + @Shared + Path setupParameter + @Shared + def featureParameter + + def setupSpec(@TempDir Path tempDir) { + setupSpecParameter = tempDir + } + + def setup(@TempDir Path tempDir) { + setupParameter = tempDir + } + + def "test"(@TempDir tempDir) { + given: + featureParameter = tempDir + expect: + tempDir instanceof Path + Files.isDirectory(setupSpecParameter) + Files.isDirectory(setupParameter) + Files.isDirectory(tempDir as Path) + } + + def cleanupSpec() { + assert !Files.exists(setupParameter) + assert !Files.exists(featureParameter as Path) + } +} + class MyFile { File root