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

Add GlobalTimeoutExtension and use it in specs #1986

Merged
merged 6 commits into from
Aug 19, 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
2 changes: 1 addition & 1 deletion .idea/copyright/ASLv2.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 11 additions & 2 deletions docs/extensions.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -311,8 +311,7 @@ def "I fail if I run for more than five seconds"() { ... }
def "I better be quick" { ... }
----

Applying `Timeout` to a spec class has the same effect as applying it to each feature that is not already annotated
with `Timeout`, excluding time spent in fixtures:
Applying `@Timeout` to a spec class has the same effect as applying it to each feature and fixture method that is not already annotated with `@Timeout`:

[source,groovy]
----
Expand All @@ -323,6 +322,11 @@ class TimedSpec extends Specification {

@Timeout(value = 250, unit = MILLISECONDS)
def "I fail much faster"() { ... }

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it be better to convert that example into a real test, to be sure that the example is "working" at least syntactically.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but in a separate PR. I'd like to get this merged soon to hopefully trace the hanging builds we've seen.

def setup() { /* I have 10 seconds */}

@Timeout(value = 25, unit = SECONDS)
def cleanup() { /* I have 25 seconds */ }
}
----

Expand All @@ -337,6 +341,11 @@ the timeout was exceeded. Additionally, thread dumps can be captured and logged
include::{sourcedir}/timeout/TimeoutConfigurationDoc.groovy[tag=example]
----

Since Spock 2.4, you can also configure a global timeout via the configuration file.
This will apply to all features that are not already annotated with `@Timeout`.
Setting the global to non-null will have the same effect as applying `@Timeout` it to all features.
With the flag `applyGlobalTimeoutToFixtures` you can control if the global timeout should also be applied to fixture methods.

=== Retry

The `@Retry` extensions can be used for flaky integration tests, where remote systems can fail sometimes.
Expand Down
4 changes: 3 additions & 1 deletion docs/release_notes.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ include::include.adoc[]

=== Misc

* Types constraints or arguments in interactions can now handle primitive types like `_ as int` spockIssue:1974[]
* Add `globalTimeout` to `@Timeout` extension, to apply a timeout to all features in a specification, configurable via the spock configuration file spockPull:1986[]
* 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[]

== 2.4-M4 (2024-03-21)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* 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.builtin;
AndreasTu marked this conversation as resolved.
Show resolved Hide resolved

import spock.lang.Timeout;

import java.util.stream.Stream;
import java.util.stream.StreamSupport;

import org.spockframework.runtime.extension.IGlobalExtension;
import org.spockframework.runtime.model.FeatureInfo;
import org.spockframework.runtime.model.MethodInfo;
import org.spockframework.runtime.model.SpecInfo;
import org.spockframework.util.Assert;
import org.spockframework.util.Beta;
import org.spockframework.util.Nullable;

/**
* Applies a timeout to every feature and fixture depending on the configuration.
*
* @author Leonard Brünings
* @since 2.4
*/
@Beta
public class GlobalTimeoutExtension implements IGlobalExtension {
private final TimeoutConfiguration timeoutConfiguration;
private @Nullable TimeoutInterceptor timeoutInterceptor;

public GlobalTimeoutExtension(TimeoutConfiguration timeoutConfiguration) {
// TimeoutConfiguration is mutable and will be configured after the extension is created,
// so we need to store a reference to it and delay until the extension is started to create the interceptor.
this.timeoutConfiguration = Assert.notNull(timeoutConfiguration, "timeoutConfiguration is null");
}

@Override
public void start() {
timeoutInterceptor = timeoutConfiguration.globalTimeout == null
? null
: new TimeoutInterceptor(timeoutConfiguration.globalTimeout, timeoutConfiguration);
}

@Override
public void visitSpec(SpecInfo spec) {
if (timeoutInterceptor == null
|| spec.getReflection().isAnnotationPresent(Timeout.class)) {
return;
}

Stream<MethodInfo> features = spec.getAllFeatures().stream()
.map(FeatureInfo::getFeatureMethod);
Stream<MethodInfo> fixtures = timeoutConfiguration.applyGlobalTimeoutToFixtures
? StreamSupport.stream(spec.getAllFixtureMethods().spliterator(), false)
: Stream.empty();

Stream.concat(features, fixtures)
.filter(method -> !method.getReflection().isAnnotationPresent(Timeout.class))
.forEach(method -> method.addInterceptor(timeoutInterceptor));
}
}
Original file line number Diff line number Diff line change
@@ -1,24 +1,25 @@
/*
* Copyright 2023 the original author or authors.
* 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.
* 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.builtin;

import org.spockframework.util.Beta;
import org.spockframework.util.Nullable;

import spock.config.ConfigurationObject;

import java.time.Duration;
import java.util.ArrayList;
import java.util.List;

Expand All @@ -45,6 +46,18 @@
@ConfigurationObject("timeout")
public class TimeoutConfiguration {

/**
* If set to a valid duration, it will apply for all features that do not have a specific timeout set, default null.
* @since 2.4
*/
public @Nullable Duration globalTimeout = null;

/**
* Determines whether the global timeout will be applied to fixtures, default false.
* @since 2.4
*/
public boolean applyGlobalTimeoutToFixtures = false;

/**
* Determines whether thread dumps will be captured and logged on feature timeout or unsuccessful interrupt attempts, default false.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
/*
* Copyright 2009 the original author or authors.
* 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.
* 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.builtin;
Expand All @@ -22,6 +20,8 @@
import org.spockframework.runtime.model.SpecInfo;
import spock.lang.Timeout;

import java.time.Duration;

/**
* @author Peter Niederwieser
*/
Expand All @@ -44,11 +44,11 @@ public void visitSpecAnnotation(Timeout timeout, SpecInfo spec) {

@Override
public void visitFeatureAnnotation(Timeout timeout, FeatureInfo feature) {
feature.getFeatureMethod().addInterceptor(new TimeoutInterceptor(timeout, configuration));
feature.getFeatureMethod().addInterceptor(new TimeoutInterceptor(Duration.ofNanos(timeout.unit().toNanos(timeout.value())), configuration));
}

@Override
public void visitFixtureAnnotation(Timeout timeout, MethodInfo fixtureMethod) {
fixtureMethod.addInterceptor(new TimeoutInterceptor(timeout, configuration));
fixtureMethod.addInterceptor(new TimeoutInterceptor(Duration.ofNanos(timeout.unit().toNanos(timeout.value())), configuration));
}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
/*
* Copyright 2009 the original author or authors.
* 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.
* 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.builtin;
Expand All @@ -20,7 +18,6 @@
import org.spockframework.runtime.extension.IMethodInterceptor;
import org.spockframework.runtime.extension.IMethodInvocation;
import org.spockframework.util.*;
import spock.lang.Timeout;

import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
Expand All @@ -40,14 +37,15 @@
*
* @author Peter Niederwieser
*/

@ThreadSafe
public class TimeoutInterceptor implements IMethodInterceptor {

private final Timeout timeout;
private final Duration timeout;
private final TimeoutConfiguration configuration;
private final JavaProcessThreadDumpCollector threadDumpCollector;

public TimeoutInterceptor(Timeout timeout, TimeoutConfiguration configuration) {
public TimeoutInterceptor(Duration timeout, TimeoutConfiguration configuration) {
Checks.checkArgument(timeout.toNanos() > 0, () -> "timeout must be positive but was " + timeout);
this.timeout = timeout;
this.configuration = configuration;
this.threadDumpCollector = JavaProcessThreadDumpCollector.create(configuration.threadDumpUtilityType);
Expand All @@ -59,13 +57,11 @@ public void intercept(final IMethodInvocation invocation) throws Throwable {
final SynchronousQueue<StackTraceElement[]> sync = new SynchronousQueue<>();
final CountDownLatch startLatch = new CountDownLatch(2);
final String methodName = invocation.getMethod().getName();
final double timeoutSeconds = TimeUtil.toSeconds(timeout.value(), timeout.unit());
final double timeoutSeconds = TimeUtil.toSeconds(timeout);

new Thread(String.format("[spock.lang.Timeout] Watcher for method '%s'", methodName)) {
@Override
public void run() {
ThreadSupport.virtualThreadIfSupported(String.format("[spock.lang.Timeout] Watcher for method '%s'", methodName), () -> {
StackTraceElement[] stackTrace = new StackTraceElement[0];
long waitMillis = timeout.unit().toMillis(timeout.value());
long waitMillis = timeout.toMillis();
boolean synced = false;
long timeoutAt = 0;
int unsuccessfulInterruptAttempts = 0;
Expand All @@ -92,9 +88,8 @@ public void run() {
}
mainThread.interrupt();
}
}
}
}.start();
}).start();

syncWithThread(startLatch, "watcher", methodName);

Expand Down Expand Up @@ -137,7 +132,7 @@ private void logUnsuccessfulInterrupt(String methodName, long now, long timeoutA
System.err.printf(
"[spock.lang.Timeout] Method '%s' has not stopped after timing out %1.2f seconds ago - interrupting. Next try in %1.2f seconds.\n%n",
methodName,
Duration.ofNanos(now - timeoutAt).toMillis() / 1000d,
TimeUtil.toSeconds(Duration.ofNanos(now - timeoutAt)),
waitMillis / 1000d
);

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.util;
AndreasTu marked this conversation as resolved.
Show resolved Hide resolved

import java.lang.reflect.Method;

public class ThreadSupport {
private static final @Nullable Class<?> THREAD_OF_VIRTUAL_CLASS = ReflectionUtil.loadClassIfAvailable("java.lang.Thread$Builder$OfVirtual");
private static final @Nullable Method OF_VIRTUAL = THREAD_OF_VIRTUAL_CLASS != null ? ReflectionUtil.getMethodByName(Thread.class, "ofVirtual") : null;
AndreasTu marked this conversation as resolved.
Show resolved Hide resolved
private static final @Nullable Method THREAD_OF_VIRTUAL_NAME = THREAD_OF_VIRTUAL_CLASS != null ? ReflectionUtil.getMethodBySignature(THREAD_OF_VIRTUAL_CLASS, "name", String.class) : null;
private static final @Nullable Method THREAD_OF_VIRTUAL_UNSTARTED = THREAD_OF_VIRTUAL_CLASS != null ? ReflectionUtil.getMethodBySignature(THREAD_OF_VIRTUAL_CLASS, "unstarted", Runnable.class) : null;

/**
* Creates a virtual thread if supported by the current JVM.
*
* @param name the name of the thread
* @param target the target to run on the thread
* @return the created thread
*/
public static Thread virtualThreadIfSupported(String name, Runnable target) {
if (THREAD_OF_VIRTUAL_CLASS == null) {
return new Thread(target, name);
}
Object builder = ReflectionUtil.invokeMethod(Thread.class, OF_VIRTUAL);
ReflectionUtil.invokeMethod(builder, THREAD_OF_VIRTUAL_NAME, name);
return (Thread) ReflectionUtil.invokeMethod(builder, THREAD_OF_VIRTUAL_UNSTARTED, target);
}
}
Loading
Loading