Skip to content

Commit

Permalink
Print stack trace for timed-out tests (of timed-out thread)
Browse files Browse the repository at this point in the history
  • Loading branch information
matthiaso committed Jun 19, 2023
1 parent f4edf68 commit 6c12f7a
Show file tree
Hide file tree
Showing 4 changed files with 165 additions and 63 deletions.
Original file line number Diff line number Diff line change
@@ -1,27 +1,22 @@
/*
* Copyright (c) 2010-2017 BSI Business Systems Integration AG.
* Copyright (c) 2010-2023 BSI Business Systems Integration AG.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
* https://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* BSI Business Systems Integration AG - initial API and implementation
*/
package org.eclipse.scout.rt.testing.client.runner.statement;

import java.util.concurrent.TimeUnit;

import org.eclipse.scout.rt.client.context.ClientRunContexts;
import org.eclipse.scout.rt.client.job.ModelJobs;
import org.eclipse.scout.rt.platform.job.IFuture;
import org.eclipse.scout.rt.platform.util.Assertions;
import org.eclipse.scout.rt.platform.util.concurrent.ThreadInterruptedError;
import org.eclipse.scout.rt.platform.util.concurrent.TimedOutError;
import org.eclipse.scout.rt.testing.platform.runner.SafeStatementInvoker;
import org.eclipse.scout.rt.platform.util.concurrent.IRunnable;
import org.eclipse.scout.rt.testing.platform.runner.statement.AbstractTimeoutRunContextStatement;
import org.junit.Test;
import org.junit.runners.model.Statement;
import org.junit.runners.model.TestTimedOutException;

/**
* Statement for executing tests with a timeout (i.e. the annotated test method is expected to complete within the
Expand All @@ -31,35 +26,14 @@
* @see Test#timeout()
* @since 5.1
*/
public class TimeoutClientRunContextStatement extends Statement {

protected final Statement m_next;
private final long m_timeoutMillis;
public class TimeoutClientRunContextStatement extends AbstractTimeoutRunContextStatement {

public TimeoutClientRunContextStatement(final Statement next, final long timeoutMillis) {
m_timeoutMillis = timeoutMillis;
m_next = Assertions.assertNotNull(next, "next statement must not be null");
super(next, timeoutMillis);
}

@Override
public void evaluate() throws Throwable {
final SafeStatementInvoker invoker = new SafeStatementInvoker(m_next);

final IFuture<Void> future = ModelJobs.schedule(invoker, ModelJobs.newInput(ClientRunContexts.copyCurrent()).withName("Running test with support for JUnit timeout"));

try {
if (m_timeoutMillis <= 0) {
future.awaitDone();
}
else {
future.awaitDone(m_timeoutMillis, TimeUnit.MILLISECONDS);
}
}
catch (ThreadInterruptedError | TimedOutError e) { // NOSONAR
future.cancel(true);
throw new TestTimedOutException(m_timeoutMillis, TimeUnit.MILLISECONDS); // JUnit timeout exception
}

invoker.throwOnError();
protected IFuture<Void> createFuture(IRunnable runnable) {
return ModelJobs.schedule(runnable, ModelJobs.newInput(ClientRunContexts.copyCurrent()).withName("Running test with support for JUnit timeout"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* Copyright (c) 2010-2023 BSI Business Systems Integration AG.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* BSI Business Systems Integration AG - initial API and implementation
*/
package org.eclipse.scout.rt.testing.platform.runner.statement;

import java.util.concurrent.TimeUnit;

import org.eclipse.scout.rt.platform.job.IFuture;
import org.eclipse.scout.rt.platform.util.Assertions;
import org.eclipse.scout.rt.platform.util.concurrent.IRunnable;
import org.eclipse.scout.rt.platform.util.concurrent.ThreadInterruptedError;
import org.eclipse.scout.rt.platform.util.concurrent.TimedOutError;
import org.eclipse.scout.rt.testing.platform.runner.SafeStatementInvoker;
import org.junit.runners.model.Statement;
import org.junit.runners.model.TestTimedOutException;

public abstract class AbstractTimeoutRunContextStatement extends Statement {

protected final Statement m_next;
protected final long m_timeoutMillis;

public AbstractTimeoutRunContextStatement(final Statement next, final long timeoutMillis) {
m_next = Assertions.assertNotNull(next, "next statement must not be null");
m_timeoutMillis = timeoutMillis;
}

protected abstract IFuture<Void> createFuture(IRunnable runnable);

@Override
public void evaluate() throws Throwable {
final SafeStatementInvoker invoker = new SafeStatementInvoker(m_next);
class TimeoutFutureRunnable implements IRunnable {

protected Thread m_thread;

@Override
public void run() throws Exception {
try {
m_thread = Thread.currentThread();
invoker.run();
}
finally {
m_thread = null;
}
}

public StackTraceElement[] getStackTrace() {
Thread t = m_thread;
if (t != null) {
return t.getStackTrace();
}
return new StackTraceElement[0];
}
}
final TimeoutFutureRunnable runnable = new TimeoutFutureRunnable();

final IFuture<Void> future = createFuture(runnable);
try {
if (m_timeoutMillis <= 0) {
future.awaitDone();
}
else {
future.awaitDone(m_timeoutMillis, TimeUnit.MILLISECONDS);
}
}
catch (ThreadInterruptedError | TimedOutError e) { // NOSONAR
System.err.println("Test has timed out, cancelling future (if test threw an exception, original exception is still thrown)");
StackTraceElement[] stackTrace = runnable.getStackTrace();
future.cancel(true);
printStackTrace(stackTrace);
invoker.throwOnError(); // throw if test itself has thrown an error
throw new TestTimedOutException(m_timeoutMillis, TimeUnit.MILLISECONDS); // JUnit timeout exception
}

invoker.throwOnError();
}

protected void printStackTrace(StackTraceElement[] stackTrace) {
StringBuilder trace = new StringBuilder();
for (StackTraceElement traceElement : stackTrace) {
if (trace.length() != 0) {
trace.append('\n');
}
trace.append("\tat ");
trace.append(traceElement);
}
if (trace.length() > 0) {
System.err.println("Test timed out\n" + trace);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,29 +1,25 @@
/*
* Copyright (c) 2010-2017 BSI Business Systems Integration AG.
* Copyright (c) 2010-2023 BSI Business Systems Integration AG.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
* https://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* BSI Business Systems Integration AG - initial API and implementation
*/
package org.eclipse.scout.rt.testing.platform.runner.statement;

import java.util.concurrent.TimeUnit;

import org.eclipse.scout.rt.platform.BEANS;
import org.eclipse.scout.rt.platform.context.RunContext;
import org.eclipse.scout.rt.platform.context.RunMonitor;
import org.eclipse.scout.rt.platform.job.IFuture;
import org.eclipse.scout.rt.platform.job.Jobs;
import org.eclipse.scout.rt.platform.transaction.TransactionScope;
import org.eclipse.scout.rt.platform.util.Assertions;
import org.eclipse.scout.rt.platform.util.concurrent.ThreadInterruptedError;
import org.eclipse.scout.rt.platform.util.concurrent.TimedOutError;
import org.eclipse.scout.rt.testing.platform.runner.SafeStatementInvoker;
import org.eclipse.scout.rt.platform.util.concurrent.IRunnable;
import org.junit.Test;
import org.junit.internal.runners.statements.FailOnTimeout;
import org.junit.runners.model.Statement;
import org.junit.runners.model.TestTimedOutException;

/**
* JUnit runs tests with a timeout in a separate thread. This statement is a replace for {@link FailOnTimeout}, and
Expand All @@ -36,31 +32,20 @@
* @see FailOnTimeout
* @since 5.1
*/
public class TimeoutRunContextStatement extends Statement {

private final Statement m_next;
private final long m_timeoutMillis;
public class TimeoutRunContextStatement extends AbstractTimeoutRunContextStatement {

public TimeoutRunContextStatement(final Statement next, final long timeoutMillis) {
m_next = Assertions.assertNotNull(next, "next statement must not be null");
m_timeoutMillis = timeoutMillis;
super(next, timeoutMillis);
}

@Override
public void evaluate() throws Throwable {
final SafeStatementInvoker invoker = new SafeStatementInvoker(m_next);

final IFuture<Void> future = Jobs.schedule(invoker, Jobs.newInput()
.withRunContext(RunContext.CURRENT.get().copy().withTransactionScope(TransactionScope.REQUIRES_NEW)) // Run in new TX, because the same TX is not allowed to be used by multiple threads.
protected IFuture<Void> createFuture(IRunnable runnable) {
RunMonitor newRunMonitor = BEANS.get(RunMonitor.class);
return Jobs.schedule(runnable, Jobs.newInput()
.withRunContext(RunContext.CURRENT.get().copy()
.withRunMonitor(newRunMonitor)
.withParentRunMonitor(RunMonitor.CURRENT.get())
.withTransactionScope(TransactionScope.REQUIRES_NEW)) // Run in new TX, because the same TX is not allowed to be used by multiple threads.
.withName("Running test with support for JUnit timeout"));
try {
future.awaitDone(m_timeoutMillis, TimeUnit.MILLISECONDS);
}
catch (ThreadInterruptedError | TimedOutError e) { // NOSONAR
future.cancel(true);
throw new TestTimedOutException(m_timeoutMillis, TimeUnit.MILLISECONDS); // JUnit timeout exception
}

invoker.throwOnError();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright (c) 2010-2023 BSI Business Systems Integration AG.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* BSI Business Systems Integration AG - initial API and implementation
*/
package org.eclipse.scout.rt.testing.platform.runner.statement;

import java.util.concurrent.TimeUnit;

import org.eclipse.scout.rt.platform.util.SleepUtil;
import org.eclipse.scout.rt.testing.platform.runner.PlatformTestRunner;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.model.Statement;
import org.junit.runners.model.TestTimedOutException;

@RunWith(PlatformTestRunner.class)
public class TimeoutRunContextStatementTest {

@Test
public void testTestTimedOutException() {
Assert.assertThrows(TestTimedOutException.class, () -> new TimeoutRunContextStatement(new Statement() {
@Override
public void evaluate() {
SleepUtil.sleepSafe(5L, TimeUnit.SECONDS);
}
}, TimeUnit.SECONDS.toMillis(1)).evaluate());
}

@Test
public void testOriginalExceptionIsThrownEvenOnTimeout() {
Assert.assertThrows(InterruptedException.class, () -> new TimeoutRunContextStatement(new Statement() {
@Override
public void evaluate() throws InterruptedException {
Thread.sleep(TimeUnit.SECONDS.toMillis(5));
}
}, TimeUnit.SECONDS.toMillis(1)).evaluate());
}
}

0 comments on commit 6c12f7a

Please sign in to comment.