Skip to content

Commit

Permalink
Enable parameter injection for @tempdir and run as initialization int…
Browse files Browse the repository at this point in the history
…erceptor 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.
  • Loading branch information
leonard84 authored Jan 24, 2024
1 parent d49ffef commit c6c1768
Show file tree
Hide file tree
Showing 9 changed files with 196 additions and 55 deletions.
6 changes: 5 additions & 1 deletion docs/extensions.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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 <<utilities.adoc#file-stystem-fixture, FileSystemFixture>> 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]
----
Expand Down
3 changes: 3 additions & 0 deletions docs/release_notes.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ include::include.adoc[]

=== Misc
* Add support for <<extensions.adoc#extension-store,keeping state in extensions>>
* 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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,17 @@
import java.io.File;
import java.lang.reflect.Constructor;
import java.nio.file.Path;
import java.util.EnumSet;
import java.util.Set;

/**
* @author dqyuan
* @since 2.0
*/
@Beta
public class TempDirExtension implements IAnnotationDrivenExtension<TempDir> {

private static final Set<MethodKind> VALID_METHOD_KINDS = EnumSet.of(MethodKind.SETUP, MethodKind.SETUP_SPEC, MethodKind.FEATURE);
private final TempDirConfiguration configuration;

public TempDirExtension(TempDirConfiguration configuration) {
Expand All @@ -25,41 +29,21 @@ public TempDirExtension(TempDirConfiguration configuration) {

@Override
public void visitFieldAnnotation(TempDir annotation, FieldInfo field) {
Class<?> fieldType = field.getType();
IThrowableFunction<Path, ?, Exception> 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<Path, ?, Exception> 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);
}
}
Original file line number Diff line number Diff line change
@@ -1,36 +1,51 @@
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
* @since 2.0
*/
@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<Path, ?, Exception> pathToFieldMapper;
private final FieldInfo fieldInfo;
private final IThrowableBiConsumer<IMethodInvocation, Path, Exception> valueSetter;
private final String name;
private final Path parentDir;
private final boolean keep;

TempDirInterceptor(IThrowableFunction<Path, ?, Exception> pathToFieldMapper, FieldInfo fieldInfo,
Path parentDir, boolean keep) {
this.pathToFieldMapper = pathToFieldMapper;
this.fieldInfo = fieldInfo;
private TempDirInterceptor(
IThrowableBiConsumer<IMethodInvocation, Path, Exception> valueSetter,
String name,
Path parentDir,
boolean keep) {

this.valueSetter = valueSetter;
this.name = name;
this.parentDir = parentDir;
this.keep = keep;
}
Expand All @@ -50,40 +65,58 @@ 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 {
String prefix = dirPrefix(invocation);
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<Path, ?, Exception> 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;
}
Expand All @@ -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);
}

Expand All @@ -115,4 +148,45 @@ public FileVisitResult postVisitDirectory(Path dir, IOException exc) {
return CONTINUE;
}
}

static TempDirInterceptor forField(FieldInfo fieldInfo, Path parentDir, boolean keep) {
IThrowableFunction<Path, ?, Exception> 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<Path, ?, Exception> 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);
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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<A, B, T extends Throwable> {
void accept(A a, B b) throws T;
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
*
* @author Peter Niederwieser
*/
@FunctionalInterface
public interface IThrowableFunction<D, C, T extends Throwable> {
C apply(D value) throws T;
}
6 changes: 5 additions & 1 deletion spock-core/src/main/java/spock/lang/TempDir.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,17 @@
* &#64;TempDir
* FileSystemFixture fsFixture // will inject an instance of FileSystemFixture with the temp path injected via constructor
* </code></pre>
* <p>
* 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 {
}
Original file line number Diff line number Diff line change
Expand Up @@ -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[]
Expand Down
Loading

0 comments on commit c6c1768

Please sign in to comment.