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

Increase Groovy version to 3.0.6 and give condition closures access to the specification instance #1204

Merged
merged 6 commits into from Nov 1, 2020
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.gradle
Expand Up @@ -22,7 +22,7 @@ ext {
minGroovyVersion = "2.5.0"
maxGroovyVersion = "2.9.99"
} else if (variant == 3.0) {
groovyVersion = "3.0.4"
groovyVersion = "3.0.6"
minGroovyVersion = "3.0.0"
maxGroovyVersion = "3.9.99"
} else {
Expand Down
4 changes: 4 additions & 0 deletions docs/extensions.adoc
Expand Up @@ -117,6 +117,10 @@ To make predicates easier to read and write, the following properties are availa
* `env` A map of all environment variables
* `os` Information about the operating system (see `spock.util.environment.OperatingSystem`)
* `jvm` Information about the JVM (see `spock.util.environment.Jvm`)
* `instance` The specification instance if instance fields, shared fields or instance methods are needed.
If this property is used, the whole annotated element cannot be skipped up-front without executing fixtures,
data providers and similar. Instead the whole workflow is followed up to the feature method invocation,
where then the closure is checked and it is decided whether to abort the specific iteration or not.

Using the `os` property, the previous example can be rewritten as:

Expand Down
3 changes: 3 additions & 0 deletions docs/release_notes.adoc
Expand Up @@ -28,6 +28,9 @@ include::include.adoc[]

- Fix https://github.com/spockframework/spock/issues/994[#994] nested closures in argument constraints are not treated as implicit assertions anymore

- `@Requires`, `@IgnoreIf` and `@PendingFeatureIf` can now access instance fields, shared fields and instance methods
by using the `instance.` qualifier inside the condition closure.


== 2.0-M3 (2020-06-11)

Expand Down
Expand Up @@ -58,14 +58,14 @@ public void visitFeatureAnnotation(T annotation, FeatureInfo feature) {
Closure condition = createCondition(annotation);

try {
Object result = evaluateCondition(condition);
Object result = evaluateCondition(condition, feature.getSpec().getReflection());
featureConditionResult(GroovyRuntimeUtil.isTruthy(result), annotation, feature);
} catch (ExtensionException ee) {
if (!(ee.getCause() instanceof MissingPropertyException)) {
throw ee;
}
MissingPropertyException mpe = (MissingPropertyException) ee.getCause();
if (!feature.getDataVariables().contains(mpe.getProperty())) {
if (!"instance".equals(mpe.getProperty()) && !feature.getDataVariables().contains(mpe.getProperty())) {
throw ee;
}
feature.getFeatureMethod().addInterceptor(new IterationCondition(condition, annotation));
Expand All @@ -82,13 +82,23 @@ private Closure createCondition(T annotation) {
}

private static Object evaluateCondition(Closure condition) {
return evaluateCondition(condition, emptyMap());
return evaluateCondition(condition, null, emptyMap(), null);
}

private static Object evaluateCondition(Closure condition, Map<String, Object> dataVariables) {
PreconditionContext context = new PreconditionContext(dataVariables);
condition.setDelegate(context);
condition.setResolveStrategy(Closure.DELEGATE_ONLY);
private static Object evaluateCondition(Closure condition, Object instance,
Map<String, Object> dataVariables) {
return evaluateCondition(condition, instance, dataVariables, null);
}

private static Object evaluateCondition(Closure condition, Object owner) {
return evaluateCondition(condition, null, emptyMap(), owner);
}

private static Object evaluateCondition(Closure condition, Object instance,
Map<String, Object> dataVariables, Object owner) {
PreconditionContext context = new PreconditionContext(instance, dataVariables);
condition = condition.rehydrate(context, owner, null);
condition.setResolveStrategy(Closure.DELEGATE_FIRST);

try {
return condition.call(context);
Expand All @@ -108,7 +118,7 @@ public IterationCondition(Closure condition, T annotation) {

@Override
public void intercept(IMethodInvocation invocation) throws Throwable {
Object result = evaluateCondition(condition, invocation.getIteration().getDataVariables());
Object result = evaluateCondition(condition, invocation.getInstance(), invocation.getIteration().getDataVariables());
iterationConditionResult(GroovyRuntimeUtil.isTruthy(result), annotation, invocation);
invocation.proceed();
}
Expand Down
Expand Up @@ -20,7 +20,6 @@
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.regex.Pattern;

import groovy.lang.MissingPropertyException;
import spock.lang.IgnoreIf;
Expand All @@ -29,36 +28,42 @@
import spock.util.environment.Jvm;
import spock.util.environment.OperatingSystem;

import static java.util.Collections.emptyMap;

/**
* The context (delegate) for a {@link Requires}, {@link IgnoreIf} or {@link PendingFeatureIf} condition.
*/
public class PreconditionContext {
private static final Pattern JAVA_VERSION = Pattern.compile("(\\d+\\.\\d+).*");

private final Object theInstance;
private final Map<String, Object> dataVariables = new HashMap<>();

public PreconditionContext() {
this(null, emptyMap());
}

public PreconditionContext(Map<String, Object> dataVariables) {
this(null, dataVariables);
}

public PreconditionContext(Object instance) {
this(instance, emptyMap());
}

public PreconditionContext(Object instance, Map<String, Object> dataVariables) {
theInstance = instance;
this.dataVariables.putAll(dataVariables);
}

public Object propertyMissing(String propertyName) {
if (dataVariables.containsKey(propertyName)) {
return dataVariables.get(propertyName);
}
if ((theInstance != null) && "instance".equals(propertyName)) {
return theInstance;
}
throw new MissingPropertyException(propertyName, getClass());
}

public void setDataVariable(String name, Object value) {
dataVariables.put(name, value);
}

public void setDataVariables(Map<String, Object> dataVariables) {
this.dataVariables.putAll(dataVariables);
}

/**
* Returns the current JVM's environment variables.
*
Expand Down
Expand Up @@ -70,8 +70,11 @@ private boolean satisfiesCondition(IMethodInvocation invocation, Throwable failu
if (condition == null) {
return true;
}
condition.setDelegate(new RetryConditionContext(invocation, failure));
condition.setResolveStrategy(Closure.DELEGATE_ONLY);
final Closure condition = this.condition.rehydrate(
new RetryConditionContext(invocation.getInstance(), failure),
invocation.getSpec().getReflection(),
null);
condition.setResolveStrategy(Closure.DELEGATE_FIRST);

try {
return GroovyRuntimeUtil.isTruthy(condition.call());
Expand Down
@@ -1,17 +1,15 @@
package org.spockframework.runtime.extension.builtin;

import org.spockframework.runtime.extension.IMethodInvocation;

/**
* The context (delegate) for a {@link spock.lang.Retry} condition.
*/
public class RetryConditionContext {

private final IMethodInvocation invocation;
private final Object instance;
private final Throwable failure;

RetryConditionContext(IMethodInvocation invocation, Throwable failure) {
this.invocation = invocation;
RetryConditionContext(Object instance, Throwable failure) {
this.instance = instance;
this.failure = failure;
}

Expand All @@ -30,7 +28,7 @@ public Throwable getFailure() {
* @return the current {@code Specification} instance
*/
public Object getInstance() {
return invocation.getInstance();
return instance;
}

}
Expand Up @@ -25,8 +25,7 @@

public abstract class MopUtil {
private static final Field ReflectionMetaMethod_method = getDeclaredField(ReflectionMetaMethod.class, "method");
private static final Field CachedField_field = GroovyRuntimeUtil.isGroovy2() ? getDeclaredField(CachedField.class, "field") :
getDeclaredField(CachedField.class, "cachedField");
private static final Field CachedField_field = getDeclaredField(CachedField.class, "field");
leonard84 marked this conversation as resolved.
Show resolved Hide resolved

private static Field getDeclaredField(Class clazz, String name) {
try {
Expand Down
Expand Up @@ -162,4 +162,86 @@ class Bar extends Closure {
ExtensionException ee = thrown()
ee.message == 'Failed to instantiate condition'
}

def "@IgnoreIf provides condition access to Specification instance shared fields"() {
when:
def result = runner.runWithImports("""
class Foo extends Specification {
@Shared
int value
@IgnoreIf({ instance.value == 2 })
def "bar #input"() {
value = input

expect:
true

where:
input << [1, 2, 3]
}
}
""")

then:
result.testsStartedCount == 4
result.testsSucceededCount == 3
result.testsAbortedCount == 1
}

def "@IgnoreIf provides condition access to Specification instance fields"() {
when:
def result = runner.runWithImports("""
class Foo extends Specification {
static int staticValue
int value

def setup() {
value = staticValue
}

@IgnoreIf({ instance.value == 2 })
def "bar #input"() {
staticValue = input

expect:
true

where:
input << [1, 2, 3]
}
}
""")

then:
result.testsStartedCount == 4
result.testsSucceededCount == 3
result.testsAbortedCount == 1
}

def "@IgnoreIf provides condition access to static Specification fields"() {
when:
def result = runner.runWithImports("""
class Foo extends Specification {
static int value = 1

@IgnoreIf({ value == 1 })
def "bar"() {
expect:
false
}

@IgnoreIf({ value != 1 })
def "baz"() {
expect:
true
}
}
""")

then:
result.testsStartedCount == 1
result.testsSkippedCount == 1
result.testsSucceededCount == 1
result.testsFailedCount == 0
}
}
Expand Up @@ -229,4 +229,87 @@ def bar() {
result.testsAbortedCount == 0
result.testsSucceededCount == 2
}

def "@PendingFeatureIf provides condition access to Specification instance shared fields"() {
when:
def result = runner.runWithImports("""
class Foo extends Specification {
@Shared
int value
@PendingFeatureIf({ instance.value == 2 })
def "bar #input"() {
value = input

expect:
input != 3

where:
input << [1, 2, 3]
}
}
""")

then:
result.testsStartedCount == 4
result.testsSucceededCount == 3
result.testsAbortedCount == 1
}

def "@PendingFeatureIf provides condition access to Specification instance fields"() {
when:
def result = runner.runWithImports("""
class Foo extends Specification {
static int staticValue
int value

def setup() {
value = staticValue
}

@PendingFeatureIf({ instance.value == 2 })
def "bar #input"() {
staticValue = input

expect:
input != 3

where:
input << [1, 2, 3]
}
}
""")

then:
result.testsStartedCount == 4
result.testsSucceededCount == 3
result.testsAbortedCount == 1
}

def "@PendingFeatureIf provides condition access to static Specification fields"() {
when:
def result = runner.runWithImports("""
class Foo extends Specification {
static int value = 1

@PendingFeatureIf({ value == 1 })
def "bar"() {
expect:
false
}

@PendingFeatureIf({ value != 1 })
def "baz"() {
expect:
true
}
}
""")

then:
result.testsStartedCount == 2
result.testsFailedCount == 0
result.testsSkippedCount == 0
result.testsAbortedCount == 1
result.testsSucceededCount == 1
}
}