/*******************************************************************************
* Copyright (c) 2007, 2017 Innoopract Informationssysteme GmbH and others.
* 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
*
* Contributors:
* Innoopract Informationssysteme GmbH - initial API and implementation
* EclipseSource - ongoing development
******************************************************************************/
package org.eclipse.rap.rwt.internal.serverpush;
import static org.eclipse.rap.rwt.testfixture.internal.ConcurrencyTestUtil.runInThread;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionBindingListener;
import org.eclipse.rap.rwt.internal.application.ApplicationContextImpl;
import org.eclipse.rap.rwt.internal.lifecycle.PhaseId;
import org.eclipse.rap.rwt.internal.lifecycle.ProcessActionRunner;
import org.eclipse.rap.rwt.internal.service.ContextProvider;
import org.eclipse.rap.rwt.internal.service.ServiceContext;
import org.eclipse.rap.rwt.internal.service.ServiceStore;
import org.eclipse.rap.rwt.internal.service.UISessionImpl;
import org.eclipse.rap.rwt.service.UISession;
import org.eclipse.rap.rwt.testfixture.internal.Fixture;
import org.eclipse.rap.rwt.testfixture.internal.NoOpRunnable;
import org.eclipse.rap.rwt.testfixture.internal.TestRequest;
import org.eclipse.rap.rwt.testfixture.internal.TestResponse;
import org.eclipse.swt.SWTException;
import org.eclipse.swt.widgets.Display;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
public class ServerPushManager_Test {
public static final String SYS_PROP_SLEEP_TIME = "sleepTime";
private static final int SLEEP_TIME;
private static final int REQUEST_WAIT_TIMEOUT = 1000;
private static final Object HANDLE_1 = new Object();
private static final Object HANDLE_2 = new Object();
private static final String RUN_ASYNC_EXEC = "run async exec|";
private static final Runnable EMPTY_RUNNABLE = new NoOpRunnable();
static {
String sleepTimeProp = System.getProperty( SYS_PROP_SLEEP_TIME );
SLEEP_TIME = sleepTimeProp == null ? 50 : Integer.parseInt( sleepTimeProp );
}
private volatile String log = "";
private Display display;
private ServerPushManager manager;
private ServerPushServiceHandler pushServiceHandler;
@Before
public void setUp() {
Fixture.setUp();
log = "";
display = new Display();
manager = ServerPushManager.getInstance();
pushServiceHandler = new ServerPushServiceHandler();
}
@After
public void tearDown() {
Fixture.tearDown();
}
@Test
public void testWakeClient() throws InterruptedException {
final AtomicReference<Throwable> serverPushServiceHandlerThrowable = new AtomicReference<>();
final ServiceContext context = ContextProvider.getContext();
Thread thread = new Thread( new Runnable() {
@Override
public void run() {
ContextProvider.setContext( context );
Fixture.fakeResponseWriter();
ServerPushServiceHandler pushServiceHandler = new ServerPushServiceHandler();
try {
manager.activateServerPushFor( HANDLE_1 );
pushServiceHandler.service( ContextProvider.getRequest(), ContextProvider.getResponse() );
} catch( Throwable thr ) {
serverPushServiceHandlerThrowable.set( thr );
}
}
} );
thread.start();
Thread.sleep( SLEEP_TIME );
if( serverPushServiceHandlerThrowable.get() != null ) {
serverPushServiceHandlerThrowable.get().printStackTrace();
}
assertNull( serverPushServiceHandlerThrowable.get() );
assertTrue( manager.isCallBackRequestBlocked() );
manager.setHasRunnables( true );
manager.wakeClient();
thread.join();
assertFalse( manager.isCallBackRequestBlocked() );
assertFalse( thread.isAlive() );
}
@Test
public void testWaitOnUIThread() throws Exception {
CallBackRequestSimulator callBackRequestSimulator = new CallBackRequestSimulator();
callBackRequestSimulator.sendRequest();
display.wake();
assertTrue( manager.isCallBackRequestBlocked() );
assertFalse( callBackRequestSimulator.exceptionOccured() );
manager.releaseBlockedRequest();
}
@Test
public void testWaitOnBackgroundThread() throws Throwable {
CallBackRequestSimulator callBackRequestSimulator = new CallBackRequestSimulator();
callBackRequestSimulator.sendRequest();
assertTrue( manager.isCallBackRequestBlocked() );
callDisplayWake();
callBackRequestSimulator.waitForRequest();
assertFalse( manager.isCallBackRequestBlocked() );
assertFalse( callBackRequestSimulator.exceptionOccured() );
}
// same test as above, but while UIThread running
@Test
public void testWaitOnBackgroundThreadDuringLifecycle() throws Throwable {
CallBackRequestSimulator callBackRequestSimulator = new CallBackRequestSimulator();
callBackRequestSimulator.sendRequest();
assertTrue( manager.isCallBackRequestBlocked() );
// assume that UIThread is currently running the life cycle
manager.notifyUIThreadStart();
callDisplayWake();
manager.notifyUIThreadEnd();
callBackRequestSimulator.waitForRequest();
assertFalse( callBackRequestSimulator.exceptionOccured() );
assertFalse( manager.isCallBackRequestBlocked() );
}
@Test
public void testAsyncExecWhileLifeCycleIsRunning() {
fakeNewRequest();
Fixture.fakePhase( PhaseId.READ_DATA );
simulateAsyncExecDuringLifeCycle();
Fixture.fakePhase( PhaseId.PROCESS_ACTION );
display.readAndDispatch();
assertTrue( manager.hasRunnables() );
Fixture.executeLifeCycleFromServerThread();
assertEquals( RUN_ASYNC_EXEC, log );
assertFalse( manager.isCallBackRequestBlocked() );
}
@Test
public void testAsyncExecWithBackgroundAndLifeCycleRunnables() throws Throwable {
// test unblocking in case of background addition of runnables
simulateBackgroundAddition( ContextProvider.getContext() );
// test runnables execution during lifecycle with interlocked additions
fakeNewRequest();
Fixture.fakePhase( PhaseId.READ_DATA );
simulateAsyncExecDuringLifeCycle();
Fixture.executeLifeCycleFromServerThread();
assertFalse( manager.isCallBackRequestBlocked() );
assertEquals( RUN_ASYNC_EXEC + RUN_ASYNC_EXEC + RUN_ASYNC_EXEC, log );
}
@Test
public void testCallBackRequestBlocking() throws Exception {
CallBackRequestSimulator callBackRequestSimulator = new CallBackRequestSimulator();
callBackRequestSimulator.sendRequest();
assertFalse( callBackRequestSimulator.exceptionOccured() );
assertTrue( manager.isCallBackRequestBlocked() );
}
@Test
public void testCallBackRequestReleasing() throws Throwable {
ServiceContext context = ContextProvider.getContext();
CallBackRequestSimulator callBackRequestSimulator = new CallBackRequestSimulator( context );
callBackRequestSimulator.sendRequest();
simulateBackgroundAddition( context );
callBackRequestSimulator.waitForRequest();
assertFalse( manager.isCallBackRequestBlocked() );
assertFalse( callBackRequestSimulator.isRequestRunning() );
assertEquals( "", log );
}
@Test
public void testCallBackRequestNotBlockedWhenRunnablesExist() throws Throwable {
ServiceContext context = ContextProvider.getContext();
display.asyncExec( mock( Runnable.class ) );
CallBackRequestSimulator callBackRequestSimulator = new CallBackRequestSimulator( context );
callBackRequestSimulator.sendRequest();
callBackRequestSimulator.waitForRequest();
assertFalse( manager.isCallBackRequestBlocked() );
assertFalse( callBackRequestSimulator.isRequestRunning() );
}
@Test
public void testCallBackRequestIsReleased_onSessionInvalidation() throws Exception {
ServiceContext context = ContextProvider.getContext();
CallBackRequestSimulator callBackRequestSimulator = new CallBackRequestSimulator( context );
callBackRequestSimulator.sendRequest();
context.getUISession().getHttpSession().invalidate();
callBackRequestSimulator.waitForRequest();
assertFalse( manager.isCallBackRequestBlocked() );
assertFalse( callBackRequestSimulator.isRequestRunning() );
assertFalse( callBackRequestSimulator.exceptionOccured() );
}
/*
* See bug 422472: ServerPush request is not released when web application is stopped
*/
@Test
public void testCallBackRequestIsReleased_onApplicationContextDeactivation() throws Exception {
ServiceContext context = ContextProvider.getContext();
CallBackRequestSimulator callBackRequestSimulator = new CallBackRequestSimulator( context );
callBackRequestSimulator.sendRequest();
context.getApplicationContext().deactivate();
callBackRequestSimulator.waitForRequest();
assertFalse( manager.isCallBackRequestBlocked() );
assertFalse( callBackRequestSimulator.isRequestRunning() );
assertFalse( callBackRequestSimulator.exceptionOccured() );
}
@Test
public void testCallBackRequestIsReleasedWhenSessionExpires() {
HttpSession httpSession = ContextProvider.getUISession().getHttpSession();
httpSession.setMaxInactiveInterval( 1 );
HttpSessionBindingListener sessionListener = mock( HttpSessionBindingListener.class );
httpSession.setAttribute( "listener", sessionListener );
manager.setRequestCheckInterval( 10 );
manager.activateServerPushFor( HANDLE_1 );
// must not block
manager.processRequest( ContextProvider.getResponse() );
}
@Test
public void testMultipleCallBackRequests() throws Exception {
manager.setRequestCheckInterval( 20 );
ServiceContext context1 = ContextProvider.getContext();
CallBackRequestSimulator callBackRequestSimulator1 = new CallBackRequestSimulator( context1 );
callBackRequestSimulator1.sendRequest();
ServiceContext context2 = createServiceContext( new TestResponse() );
CallBackRequestSimulator callBackRequestSimulator2 = new CallBackRequestSimulator( context2 );
callBackRequestSimulator2.sendRequest();
callBackRequestSimulator1.waitForRequest();
assertTrue( manager.isCallBackRequestBlocked() );
assertFalse( callBackRequestSimulator1.exceptionOccured() );
assertFalse( callBackRequestSimulator2.exceptionOccured() );
assertFalse( callBackRequestSimulator1.isRequestRunning() );
assertTrue( callBackRequestSimulator2.isRequestRunning() );
}
@Test
public void testCallBackRequestTerminatsWhenConnectionBreaks() throws Exception {
manager.setRequestCheckInterval( 20 );
TestResponse response = new TestResponse() {
@Override
public PrintWriter getWriter() throws IOException {
PrintWriter failingWriter = mock( PrintWriter.class );
when( new Boolean( failingWriter.checkError() ) ).thenReturn( Boolean.TRUE );
return failingWriter;
}
};
ServiceContext context2 = createServiceContext( response );
CallBackRequestSimulator callBackRequestSimulator = new CallBackRequestSimulator( context2 );
callBackRequestSimulator.sendRequest();
Thread.sleep( SLEEP_TIME );
assertFalse( manager.isCallBackRequestBlocked() );
callBackRequestSimulator.waitForRequest();
assertFalse( callBackRequestSimulator.isRequestRunning() );
}
@Test
public void testAsyncExec() throws Throwable {
Throwable[] serverPushServiceHandlerThrowable = { null };
ServiceContext context = ContextProvider.getContext();
// test runnables addition while no server push thread is not blocked
CallBackRequestSimulator callBackRequestSimulator = new CallBackRequestSimulator( context );
callBackRequestSimulator.sendRequest();
manager.notifyUIThreadEnd();
simulateBackgroundAddition( context );
fakeNewRequest();
Fixture.executeLifeCycleFromServerThread();
// let request thread finish off and die
callBackRequestSimulator.requestThread.join( 100 );
assertNull( serverPushServiceHandlerThrowable[ 0 ] );
assertFalse( manager.isCallBackRequestBlocked() );
assertFalse( callBackRequestSimulator.isRequestRunning() );
assertEquals( RUN_ASYNC_EXEC + RUN_ASYNC_EXEC, log );
}
// This test ensures that addSync doesn't cause deadlocks
@Test
public void testSyncExecBlock() throws Exception {
Fixture.fakePhase( PhaseId.PROCESS_ACTION );
final ServiceContext context = ContextProvider.getContext();
// the code in bgRunnable simulates a bg-thread that calls Display#addSync
Runnable bgRunnable = new Runnable() {
@Override
public void run() {
ContextProvider.setContext( context );
Fixture.fakeResponseWriter();
display.syncExec( EMPTY_RUNNABLE );
}
};
// simulate a lot "parallel" bg-threads to provoke multi-threading problems
List<Thread> bgThreads = new ArrayList<Thread>();
for( int i = 0; i < 200; i++ ) {
Thread bgThread = new Thread( bgRunnable, "Test-Bg-Thread " + i );
bgThread.setDaemon( true );
bgThread.start();
display.readAndDispatch();
bgThreads.add( bgThread );
}
// wait (hopefully long enough) until all bg-threads have done their work
// (i.e. called addSync) and make sure all sync-runnables get executed
Thread.sleep( SLEEP_TIME );
while( display.readAndDispatch() ) {
Thread.sleep( SLEEP_TIME );
}
// wait for all bgThreads to terminate
for( int i = 0; i < bgThreads.size(); i++ ) {
Thread bgThread = bgThreads.get( i );
bgThread.join();
display.readAndDispatch();
}
assertFalse( manager.hasRunnables() );
}
// This test ensures that SyncRunnable releases the blocked thread in case of
// an exception
@Test
public void testAddSyncWithExceptionInRunnable() throws Exception {
Fixture.fakePhase( PhaseId.PROCESS_ACTION );
final ServiceContext context = ContextProvider.getContext();
final AtomicReference<SWTException> exceptionInBgThread = new AtomicReference<SWTException>();
// the code in bgRunnable simulates a bg-thread that calls Display#addSync
// and causes an exception in the runnable
Runnable bgRunnable = new Runnable() {
@Override
public void run() {
ContextProvider.setContext( context );
Fixture.fakeResponseWriter();
Runnable causeException = new Runnable() {
@Override
public void run() {
throw new RuntimeException( "Exception in sync-runnable" );
}
};
try {
display.syncExec( causeException );
} catch( SWTException e ) {
exceptionInBgThread.set( e );
}
}
};
Thread bgThread = new Thread( bgRunnable );
bgThread.setDaemon( true );
bgThread.start();
try {
while( !display.readAndDispatch() ) {
Thread.yield();
Thread.sleep( SLEEP_TIME );
}
fail( "Exception from causeException-runnable must end up here" );
} catch( SWTException expected ) {
}
Thread.sleep( SLEEP_TIME );
assertNotNull( exceptionInBgThread.get() );
assertFalse( bgThread.isAlive() );
}
@Test
public void testMustBlockCallBackRequest() {
assertFalse( manager.mustBlockCallBackRequest() );
}
@Test
public void testMustBlockCallBackRequestWhenActive() {
manager.activateServerPushFor( HANDLE_1 );
assertTrue( manager.mustBlockCallBackRequest() );
}
@Test
public void testMustBlockCallBackRequestWhenActiveAndRunnablesPending() {
manager.activateServerPushFor( HANDLE_1 );
manager.setHasRunnables( true );
assertFalse( manager.mustBlockCallBackRequest() );
}
@Test
public void testMustBlockCallBackRequestWhenDeactivatedAndRunnablesPending() {
manager.setHasRunnables( true );
assertFalse( manager.mustBlockCallBackRequest() );
}
@Test
public void testNeedActivationFromDifferentSession() throws Throwable {
// test that on/off switching is managed in session scope
manager.activateServerPushFor( HANDLE_1 );
final AtomicBoolean otherSession = new AtomicBoolean();
Runnable runnable = new Runnable() {
@Override
public void run() {
Fixture.createServiceContext();
new Display();
otherSession.set( ServerPushManager.getInstance().needsActivation() );
}
};
runInThread( runnable );
assertFalse( otherSession.get() );
}
@Test
public void testNeedActivationWithoutActivateCall() {
boolean needsActivation = manager.needsActivation();
assertFalse( needsActivation );
}
@Test
public void testNeedActivationAfterDeactivate() {
manager.deactivateServerPushFor( HANDLE_1 );
assertFalse( manager.needsActivation() );
}
@Test
public void testNeedActivationWithDifferentIds() {
manager.activateServerPushFor( HANDLE_1 );
manager.activateServerPushFor( HANDLE_2 );
assertTrue( manager.needsActivation() );
}
@Test
public void testNeedActivationAfterActivateTwoDeactivateOne() {
manager.activateServerPushFor( HANDLE_1 );
manager.activateServerPushFor( HANDLE_2 );
manager.deactivateServerPushFor( HANDLE_1 );
assertTrue( manager.needsActivation() );
}
@Test
public void testNeedActivateTwice() {
manager.activateServerPushFor( HANDLE_1 );
manager.deactivateServerPushFor( HANDLE_1 );
manager.activateServerPushFor( HANDLE_2 );
assertTrue( manager.needsActivation() );
}
@Test
public void testNeedActivationWithActivateDeactivateAndPendingRunnables() {
manager.activateServerPushFor( HANDLE_1 );
display.asyncExec( EMPTY_RUNNABLE );
manager.deactivateServerPushFor( HANDLE_1 );
assertTrue( manager.needsActivation() );
}
@Test
public void testNeedActivationWithPendingRunnablesDoesntEnableServerPush() {
display.asyncExec( EMPTY_RUNNABLE );
assertFalse( manager.needsActivation() );
}
@Test
public void testIsSessionExpiredWithInfiniteSessionTimeout() {
ContextProvider.getUISession().getHttpSession().setMaxInactiveInterval( -1 );
boolean sessionExpired = ServerPushManager.isSessionExpired( 1, 2 );
assertFalse( sessionExpired );
}
@Test
public void testIsSessionExpiredWhenSessionTimedOut() {
ContextProvider.getUISession().getHttpSession().setMaxInactiveInterval( 10 );
boolean sessionExpired = ServerPushManager.isSessionExpired( 1, 20000 );
assertTrue( sessionExpired );
}
@Test
public void testIsSessionExpiredWhenSessionActive() {
ContextProvider.getUISession().getHttpSession().setMaxInactiveInterval( 10 );
boolean sessionExpired = ServerPushManager.isSessionExpired( 1, 9000 );
assertFalse( sessionExpired );
}
@Test
public void testIsSessionExpiredWhenUISessionInactive() {
UISessionImpl uiSession = ( UISessionImpl )ContextProvider.getUISession();
uiSession.shutdown();
boolean sessionExpired = ServerPushManager.isSessionExpired( 1, 2 );
assertTrue( sessionExpired );
}
@Test
public void testSetHasRunnablesWithoutStateInfo() {
// Service handlers don't have a state info
manager.activateServerPushFor( HANDLE_1 );
Fixture.replaceServiceStore( null );
try {
manager.setHasRunnables( true );
} catch( NullPointerException notExpected ) {
fail();
}
}
@Test
public void testResponseHeaders() throws IOException {
TestResponse response = ( TestResponse )ContextProvider.getResponse();
pushServiceHandler.service( ContextProvider.getRequest(), response );
assertNotNull( response.getHeader( "Cache-Control" ) );
assertNotNull( response.getHeader( "Pragma" ) );
assertNotNull( response.getHeader( "Expires" ) );
}
private void simulateBackgroundAddition( final ServiceContext serviceContext ) throws Throwable {
Runnable runnable = new Runnable() {
@Override
public void run() {
ContextProvider.setContext( serviceContext );
display.asyncExec( new AsyncExecRunnable() );
display.asyncExec( new AsyncExecRunnable() );
}
};
runInThread( runnable );
}
private void simulateAsyncExecDuringLifeCycle() {
ProcessActionRunner.add( new Runnable() {
@Override
public void run() {
Runnable target = new Runnable() {
@Override
public void run() {
display.asyncExec( new AsyncExecRunnable() );
}
};
try {
runInThread( target );
} catch( Throwable e ) {
e.printStackTrace();
}
}
} );
}
private void callDisplayWake() throws Throwable {
Runnable runnable = new Runnable() {
@Override
public void run() {
display.wake();
}
};
runInThread( runnable );
}
private void fakeNewRequest() {
Fixture.fakeNewRequest();
ContextProvider.getUISession().setAttribute( "org.eclipse.swt.display", display );
}
private static ServiceContext createServiceContext( TestResponse response ) {
UISession uiSession = ContextProvider.getContext().getUISession();
TestRequest request = new TestRequest();
ApplicationContextImpl applicationContext = mock( ApplicationContextImpl.class );
ServiceContext serviceContext = new ServiceContext( request, response, applicationContext );
serviceContext.setServiceStore( new ServiceStore() );
serviceContext.setUISession( uiSession );
return serviceContext;
}
private class AsyncExecRunnable implements Runnable {
@Override
public void run() {
log += RUN_ASYNC_EXEC;
}
}
private class CallBackRequestSimulator {
private final ServiceContext serviceContext;
private volatile Thread requestThread;
private volatile Throwable exception;
CallBackRequestSimulator() {
serviceContext = ContextProvider.getContext();
}
CallBackRequestSimulator( ServiceContext serviceContext ) {
this.serviceContext = serviceContext;
}
void sendRequest() throws InterruptedException {
requestThread = new Thread( new Runnable() {
@Override
public void run() {
ContextProvider.setContext( serviceContext );
Fixture.fakeResponseWriter();
try {
manager.activateServerPushFor( HANDLE_1 );
pushServiceHandler.service( ContextProvider.getRequest(),
ContextProvider.getResponse() );
} catch( Throwable thr ) {
exception = thr;
}
}
} );
requestThread.start();
Thread.sleep( SLEEP_TIME );
}
void waitForRequest() throws InterruptedException {
requestThread.join( REQUEST_WAIT_TIMEOUT );
}
boolean isRequestRunning() {
return requestThread.isAlive();
}
boolean exceptionOccured() {
return exception != null;
}
}
}