Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make IStore available via ISpecificationContext #2064

Merged
merged 4 commits into from
Dec 28, 2024
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
3 changes: 3 additions & 0 deletions docs/extensions.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1399,6 +1399,9 @@ The `org.spockframework.runtime.IRunListener` can be registered via `SpecInfo.ad
The `org.spockframework.runtime.extension.IBlockListener` can be registered on a feature via, `FeatureInfo.addBlockListener(IBlockListener)` and will receive notifications about the progress of the feature.

It will be called once when entering a block (`blockEntered`) and once when exiting a block (`blockExited`).
Both methods receive the `BlockInfo` of the block that is entered or exited.
They also receive the current `Specification` instance which gives access to the `ISpecificationContext` to get the current `IterationInfo` or retrieve an `IStore`.
While this gives extensive access to the current state of the test run, it should be used responsibly as it can lead to surprising results if abused.

When an exception is thrown in a block, the `blockExited` will not be called for that block.
The failed block will be part of the `ErrorContext` in `ErrorInfo` that is passed to `IRunListener.error(ErrorInfo)`.
Expand Down
1 change: 1 addition & 0 deletions docs/release_notes.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ include::include.adoc[]
** Built-in extensions have been updated to use this new interface where applicable.
* Add best-effort error reporting for interactions on final methods when using the `byte-buddy` mock maker spockIssue:2039[]
* Add support for `@FailsWith` to assert an exception message spockIssue:2039[]
* Add support for accessing the `IStore` via `ISpecificationContext` spockPull:2064[]
* Improve `@Timeout` extension will now use virtual threads if available spockPull:1986[]
* Improve mock argument matching, types constraints or arguments in interactions can now handle primitive types like `_ as int` spockIssue:1974[]
* Improve `verifyEach` to accept an optional second index parameter for the assertion block closure spockPull:2043[]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -422,9 +422,9 @@ private void addBlockListeners(Block block) {
if (!blockType.isSupportingBlockListeners()) return;

// SpockRuntime.callBlockEntered(getSpecificationContext(), blockMetadataIndex)
MethodCallExpression blockEnteredCall = createBlockListenerCall(block, blockType, nodeCache.SpockRuntime_CallBlockEntered);
MethodCallExpression blockEnteredCall = createBlockListenerCall(block, nodeCache.SpockRuntime_CallBlockEntered);
// SpockRuntime.callBlockExited(getSpecificationContext(), blockMetadataIndex)
MethodCallExpression blockExitedCall = createBlockListenerCall(block, blockType, nodeCache.SpockRuntime_CallBlockExited);
MethodCallExpression blockExitedCall = createBlockListenerCall(block, nodeCache.SpockRuntime_CallBlockExited);

block.getAst().add(0, new ExpressionStatement(blockEnteredCall));
if (blockType == BlockParseInfo.CLEANUP) {
Expand Down Expand Up @@ -455,13 +455,13 @@ private IfStatement ifThrowableIsNotNull(Statement statement) {
);
}

private MethodCallExpression createBlockListenerCall(Block block, BlockParseInfo blockType, MethodNode blockListenerMethod) {
private MethodCallExpression createBlockListenerCall(Block block, MethodNode blockListenerMethod) {
if (block.getBlockMetaDataIndex() < 0) throw new SpockException("Block metadata index not set: " + block);
return createDirectMethodCall(
new ClassExpression(nodeCache.SpockRuntime),
blockListenerMethod,
new ArgumentListExpression(
getSpecificationContext(),
VariableExpression.THIS_EXPRESSION,
new ConstantExpression(block.getBlockMetaDataIndex(), true)
));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package org.spockframework.lang;

import org.spockframework.mock.IThreadAwareMockController;
import org.spockframework.runtime.extension.IStoreProvider;
import org.spockframework.runtime.model.BlockInfo;
import org.spockframework.runtime.model.FeatureInfo;
import org.spockframework.runtime.model.SpecInfo;
Expand All @@ -24,7 +25,7 @@
import org.spockframework.util.Nullable;

@Beta
public interface ISpecificationContext {
public interface ISpecificationContext extends IStoreProvider {
@Nullable
SpecInfo getCurrentSpec();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ SpockExecutionContext createSpecInstance(SpockExecutionContext context, boolean

context = context.withChildStoreProvider().withCurrentInstance(instance);
getSpecificationContext(context).setCurrentSpec(context.getSpec());
getSpecificationContext(context).pushStoreProvider(context.getStoreProvider());
if (shared) {
context = context.withSharedInstance(instance);
}
Expand Down Expand Up @@ -188,13 +189,15 @@ public void runFeature(SpockExecutionContext context, Runnable feature) {
throw new InternalSpockError("Invalid state, feature is executed although it should have been skipped");
}
getSpecificationContext(context).setCurrentFeature(currentFeature);
getSpecificationContext(context).pushStoreProvider(context.getStoreProvider());

supervisor.beforeFeature(currentFeature);
invoke(context, this, createMethodInfoForDoRunFeature(context, feature));
supervisor.afterFeature(currentFeature);

runCloseContextStoreProvider(context, MethodKind.CLEANUP);
getSpecificationContext(context).setCurrentFeature(null);
getSpecificationContext(context).popStoreProvider();
}

private MethodInfo createMethodInfoForDoRunFeature(SpockExecutionContext context, Runnable feature) {
Expand All @@ -216,13 +219,15 @@ void runIteration(SpockExecutionContext context, IterationInfo iterationInfo, Ru

context = context.withCurrentIteration(iterationInfo);
getSpecificationContext(context).setCurrentIteration(iterationInfo);
getSpecificationContext(context).pushStoreProvider(context.getStoreProvider());

supervisor.beforeIteration(iterationInfo);
invoke(context, this, createMethodInfoForDoRunIteration(context, runnable));
supervisor.afterIteration(iterationInfo);
runCloseContextStoreProvider(context, MethodKind.CLEANUP);

getSpecificationContext(context).setCurrentIteration(null); // TODO check if we really need to null here
getSpecificationContext(context).popStoreProvider();
}

IterationInfo createIterationInfo(SpockExecutionContext context, int iterationIndex, Object[] args, int estimatedNumIterations) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,15 @@
import org.spockframework.mock.IMockController;
import org.spockframework.mock.IThreadAwareMockController;
import org.spockframework.mock.runtime.MockController;
import org.spockframework.runtime.extension.IStore;
import org.spockframework.runtime.extension.IStoreProvider;
import org.spockframework.runtime.model.*;
import org.spockframework.util.Nullable;
import spock.lang.Specification;

import java.util.ArrayDeque;
import java.util.Deque;

public class SpecificationContext implements ISpecificationContext {
private volatile SpecInfo currentSpec;
private volatile FeatureInfo currentFeature;
Expand All @@ -18,6 +23,7 @@ public class SpecificationContext implements ISpecificationContext {
private volatile Specification sharedInstance;

private volatile Throwable thrownException;
private final Deque<IStoreProvider> storeProvider = new ArrayDeque<>(3); // spec, feature, iteration

private final MockController mockController = new MockController();

Expand Down Expand Up @@ -107,4 +113,16 @@ public IThreadAwareMockController getThreadAwareMockController() {
return mockController;
}

@Override
public IStore getStore(IStore.Namespace namespace) {
return storeProvider.getLast().getStore(namespace);
}

public void pushStoreProvider(IStoreProvider storeProvider) {
this.storeProvider.push(storeProvider);
}

public void popStoreProvider() {
this.storeProvider.pop();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@
import org.hamcrest.Matcher;
import org.hamcrest.collection.IsIterableContainingInAnyOrder;
import org.opentest4j.MultipleFailuresError;
import spock.lang.Specification;

import org.spockframework.lang.ISpecificationContext;
import org.spockframework.runtime.extension.IBlockListener;
import org.spockframework.runtime.model.BlockInfo;
import org.spockframework.runtime.model.ExpressionInfo;
Expand Down Expand Up @@ -231,11 +234,12 @@ public static Object[] despreadList(Object[] args, Object[] spreads, int[] posit

public static final String CALL_BLOCK_ENTERED = "callBlockEntered";

public static void callBlockEntered(SpecificationContext context, int blockInfoIndex) {
public static void callBlockEntered(Specification specification, int blockInfoIndex) {
SpecificationContext context = (SpecificationContext) specification.getSpecificationContext();
IterationInfo currentIteration = context.getCurrentIteration();
BlockInfo blockInfo = context.getCurrentFeature().getBlocks().get(blockInfoIndex);
context.setCurrentBlock(blockInfo);
notifyBlockListener(currentIteration, blockListener -> blockListener.blockEntered(currentIteration, blockInfo));
notifyBlockListener(currentIteration, blockListener -> blockListener.blockEntered(specification, blockInfo));
}

private static void notifyBlockListener(IterationInfo currentIteration, Consumer<IBlockListener> consumer) {
Expand All @@ -246,10 +250,11 @@ private static void notifyBlockListener(IterationInfo currentIteration, Consumer

public static final String CALL_BLOCK_EXITED = "callBlockExited";

public static void callBlockExited(SpecificationContext context, int blockInfoIndex) {
public static void callBlockExited(Specification specification, int blockInfoIndex) {
SpecificationContext context = (SpecificationContext) specification.getSpecificationContext();
IterationInfo currentIteration = context.getCurrentIteration();
BlockInfo blockInfo = context.getCurrentFeature().getBlocks().get(blockInfoIndex);
notifyBlockListener(currentIteration, blockListener -> blockListener.blockExited(currentIteration, blockInfo));
notifyBlockListener(currentIteration, blockListener -> blockListener.blockExited(specification, blockInfo));
context.setCurrentBlock(null);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import org.junit.platform.engine.support.store.NamespacedHierarchicalStore;
import org.spockframework.runtime.extension.IStore;
import org.spockframework.runtime.extension.IStoreProvider;
import org.spockframework.util.Nullable;

import java.util.Objects;
Expand All @@ -25,7 +26,7 @@
* @author Leonard Brünings
* @since 2.4
*/
public class StoreProvider implements AutoCloseable {
public class StoreProvider implements AutoCloseable, IStoreProvider {
private static final NamespacedHierarchicalStore.CloseAction<IStore.Namespace> CLOSE_ACTION = (IStore.Namespace namespace, Object key, Object value) -> {
if (value instanceof AutoCloseable) {
((AutoCloseable) value).close();
Expand All @@ -36,7 +37,7 @@ public class StoreProvider implements AutoCloseable {
@Nullable
private final StoreProvider parent;

private StoreProvider(NamespacedHierarchicalStore<IStore.Namespace> backend, StoreProvider parent) {
private StoreProvider(NamespacedHierarchicalStore<IStore.Namespace> backend, @Nullable StoreProvider parent) {
this.backend = Objects.requireNonNull(backend);
this.parent = parent;
}
Expand All @@ -49,6 +50,7 @@ public StoreProvider createChildStoreProvider() {
return new StoreProvider(newBackendStore(backend), this);
}

@Override
public NamespacedExtensionStore getStore(IStore.Namespace namespace) {
return new NamespacedExtensionStore(backend,
() -> parent == null ? null : parent.getStore(namespace),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

package org.spockframework.runtime.extension;

import spock.lang.Specification;

import org.spockframework.runtime.model.BlockInfo;
import org.spockframework.runtime.model.ErrorInfo;
import org.spockframework.runtime.model.IterationInfo;
Expand Down Expand Up @@ -44,7 +46,7 @@
/**
* Called when a block is entered.
*/
default void blockEntered(IterationInfo iterationInfo, BlockInfo blockInfo) {}
default <S extends Specification> void blockEntered(S specificationInstance, BlockInfo blockInfo) {}

Check warning on line 49 in spock-core/src/main/java/org/spockframework/runtime/extension/IBlockListener.java

View check run for this annotation

Codecov / codecov/patch

spock-core/src/main/java/org/spockframework/runtime/extension/IBlockListener.java#L49

Added line #L49 was not covered by tests

/**
* Called when a block is exited.
Expand All @@ -53,5 +55,5 @@
* The block that was active will be available in the {@link org.spockframework.runtime.model.IErrorContext}
* and can be observed via {@link org.spockframework.runtime.IRunListener#error(ErrorInfo)}.
*/
default void blockExited(IterationInfo iterationInfo, BlockInfo blockInfo) {}
default <S extends Specification> void blockExited(S specificationInstance, BlockInfo blockInfo) {}

Check warning on line 58 in spock-core/src/main/java/org/spockframework/runtime/extension/IBlockListener.java

View check run for this annotation

Codecov / codecov/patch

spock-core/src/main/java/org/spockframework/runtime/extension/IBlockListener.java#L58

Added line #L58 was not covered by tests
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
/**
* @author Peter Niederwieser
*/
public interface IMethodInvocation {
public interface IMethodInvocation extends IStoreProvider {
/**
* Returns the specification which this method invocation belongs to.
*
Expand Down Expand Up @@ -98,22 +98,6 @@ public interface IMethodInvocation {
*/
Object[] getArguments();

/**
* Get the {@link IStore} for the supplied {@linkplain IStore.Namespace namespace}.
*
* <p>A store is bound to its context lifecycle. When a
* context lifecycle ends it closes its associated store. All stored values
* that are instances of {@link AutoCloseable} are
* notified by invoking their {@code close()} methods.
*
* @param namespace the {@code Namespace} to get the store for; never {@code null}
* @return the store in which to put and get objects for other invocations
* working in the same namespace; never {@code null}
* @since 2.4
*/
@Beta
IStore getStore(IStore.Namespace namespace);

/**
* Sets the arguments for this method invocation.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* 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.runtime.extension;

import org.spockframework.util.Beta;

/**
* Provides access to the {@link IStore} for a given {@linkplain IStore.Namespace namespace}.
* @since 2.4
*/
@Beta
public interface IStoreProvider {

/**
* Get the {@link IStore} for the supplied {@linkplain IStore.Namespace namespace}.
* <p>
* A store is bound to its context lifecycle. When a
* context lifecycle ends it closes its associated store. All stored values
* that are instances of {@link AutoCloseable} are
* notified by invoking their {@code close()} methods.
*
* @param namespace the {@code Namespace} to get the store for; never {@code null}
* @return the store in which to put and get objects for other invocations
* working in the same namespace; never {@code null}
*/
@Beta
IStore getStore(IStore.Namespace namespace);
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ class BlockListenerSpec extends Specification {

def setup() {
specificationContext.currentIteration.feature.addBlockListener([
blockEntered: { IterationInfo i, BlockInfo b ->
blockEntered: { Specification i, BlockInfo b ->
blocks << b
},
blockExited: { IterationInfo i, BlockInfo b ->
blockExited: { Specification i, BlockInfo b ->
exitBlocks << b
}] as IBlockListener)
}
Expand Down
Loading
Loading