/*
* Copyright (C) 2015 Red Hat, Inc. and/or its affiliates.
*
* 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
*
* http://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.jboss.errai.bus.client.tests;
import com.google.gwt.user.client.Timer;
import org.jboss.errai.bus.client.api.BusErrorCallback;
import org.jboss.errai.bus.client.api.BusLifecycleAdapter;
import org.jboss.errai.bus.client.api.BusLifecycleEvent;
import org.jboss.errai.bus.client.api.BusLifecycleListener;
import org.jboss.errai.bus.client.api.messaging.Message;
import org.jboss.errai.bus.client.api.messaging.MessageCallback;
import org.jboss.errai.bus.client.api.base.MessageBuilder;
import org.jboss.errai.bus.client.api.base.TransportIOException;
import org.jboss.errai.bus.client.api.TransportError;
import org.jboss.errai.bus.client.framework.Wormhole;
import org.jboss.errai.bus.client.tests.support.RecordingBusLifecycleListener;
import org.jboss.errai.bus.client.tests.support.RecordingBusLifecycleListener.EventType;
import org.jboss.errai.bus.client.tests.support.RecordingBusLifecycleListener.RecordedEvent;
import org.jboss.errai.bus.common.AbstractErraiTest;
import java.util.ArrayList;
import java.util.List;
public class LifecycleEventTests extends AbstractErraiTest {
private final RecordingBusLifecycleListener listener = new RecordingBusLifecycleListener();
/**
* Listeners that will be removed from the bus in gwtTearDown(). Tests can add their own listeners.
* This is important because the bus gets
*/
private final List<BusLifecycleListener> listenersToRemove = new ArrayList<BusLifecycleListener>();
/**
* If set non-null by a test, this endpoint URL will be restored during teardown.
*/
private Wormhole.Fixer endpointFixer = null;
@Override
public String getModuleName() {
return "org.jboss.errai.bus.ErraiBusTests";
}
@Override
protected void gwtSetUp() throws Exception {
super.gwtSetUp();
bus.addLifecycleListener(listener);
}
@Override
protected void gwtTearDown() throws Exception {
for (BusLifecycleListener listener : listenersToRemove) {
bus.removeLifecycleListener(listener);
}
if (endpointFixer != null) {
endpointFixer.fix();
}
super.gwtTearDown();
}
public void testNormalFullLifecycle() throws Exception {
final List<EventType> expectedEventTypes = new ArrayList<EventType>();
assertEquals(expectedEventTypes, listener.getEventTypes());
runAfterInit(new Runnable() {
@Override
public void run() {
expectedEventTypes.add(EventType.ASSOCIATING);
expectedEventTypes.add(EventType.ONLINE);
assertEquals(expectedEventTypes, listener.getEventTypes());
bus.stop(true);
expectedEventTypes.add(EventType.OFFLINE);
expectedEventTypes.add(EventType.DISASSOCIATING);
assertEquals(expectedEventTypes, listener.getEventTypes());
finishTest();
}
});
}
public void testRecoverFromExpiredSession() throws Exception {
final List<EventType> expectedEventTypes = new ArrayList<EventType>();
assertEquals(expectedEventTypes, listener.getEventTypes());
runAfterInit(new Runnable() {
@Override
public void run() {
expectedEventTypes.add(EventType.ASSOCIATING);
expectedEventTypes.add(EventType.ONLINE);
assertEquals(expectedEventTypes, listener.getEventTypes());
// simulate session expiration
MessageBuilder.createMessage()
.toSubject("ExpiryService")
.signalling().noErrorHandling().sendNowWith(bus);
expectedEventTypes.add(EventType.OFFLINE);
// currently, session renewal involves a full stop of the bus. this isn't particularly critical
// for end-user applications to see, so if this test fails later by producing [OFFLINE, ONLINE]
// we could consider that a win.
expectedEventTypes.add(EventType.DISASSOCIATING);
expectedEventTypes.add(EventType.ASSOCIATING);
expectedEventTypes.add(EventType.ONLINE);
pollUntilListenerSees(expectedEventTypes);
}
});
}
public void testRecoverFromNetworkError() throws Exception {
final List<EventType> expectedEventTypes = new ArrayList<EventType>();
assertEquals(expectedEventTypes, listener.getEventTypes());
runAfterInit(new Runnable() {
@Override
public void run() {
expectedEventTypes.add(EventType.ASSOCIATING);
expectedEventTypes.add(EventType.ONLINE);
assertEquals(expectedEventTypes, listener.getEventTypes());
// simulate 404 on bus endpoint URL
endpointFixer = Wormhole.changeBusEndpointUrl(bus, "invalid.url");
expectedEventTypes.add(EventType.OFFLINE);
expectedEventTypes.add(EventType.ONLINE);
pollUntilListenerSees(expectedEventTypes);
// wait for failure, then set back to the correct value so bus can recover
new Timer() {
@Override
public void run() {
endpointFixer.fix();
}
}.schedule(5000);
}
});
}
//todo: now must test that bus keeps trying
public void ignoreTestPersistentNetworkError() throws Exception {
final List<EventType> expectedEventTypes = new ArrayList<EventType>();
assertEquals(expectedEventTypes, listener.getEventTypes());
runAfterInit(new Runnable() {
@Override
public void run() {
expectedEventTypes.add(EventType.ASSOCIATING);
expectedEventTypes.add(EventType.ONLINE);
assertEquals(expectedEventTypes, listener.getEventTypes());
// simulate 404 on bus endpoint URL
endpointFixer = Wormhole.changeBusEndpointUrl(bus, "invalid.url");
expectedEventTypes.add(EventType.OFFLINE);
expectedEventTypes.add(EventType.DISASSOCIATING);
pollUntilListenerSees(expectedEventTypes);
}
});
}
public void ignoreTestAppDirectedRecoveryFromPersistentNetworkError() throws Exception {
System.out.println("Begin testAppDirectedRecoveryFromPersistentNetworkError()");
final BusLifecycleListener reattacher = new BusLifecycleAdapter() {
@Override
public void busDisassociating(BusLifecycleEvent e) {
// simulate server back online after extended outage
// (or changing endpoint to fail over to an online server)
endpointFixer.fix();
// Wormhole.changeBusEndpointUrl(bus, originalBusEndpointUrl);
// explicit bus restart (in a timer so it doesn't make other listeners
// see events out of order due to recursive event delivery)
new Timer() {
@Override
public void run() {
bus.init();
}
}.schedule(1);
}
};
bus.addLifecycleListener(reattacher);
listenersToRemove.add(reattacher);
final List<EventType> expectedEventTypes = new ArrayList<EventType>();
assertEquals(expectedEventTypes, listener.getEventTypes());
runAfterInit(new Runnable() {
@Override
public void run() {
expectedEventTypes.add(EventType.ASSOCIATING);
expectedEventTypes.add(EventType.ONLINE);
assertEquals(expectedEventTypes, listener.getEventTypes());
// simulate 404 on bus endpoint URL
endpointFixer = Wormhole.changeBusEndpointUrl(bus, "invalid.url");
expectedEventTypes.add(EventType.OFFLINE);
expectedEventTypes.add(EventType.DISASSOCIATING);
// our lifecycle listener kicks in here
expectedEventTypes.add(EventType.ASSOCIATING);
expectedEventTypes.add(EventType.ONLINE);
pollUntilListenerSees(expectedEventTypes);
System.out.println("End testAppDirectedRecoveryFromPersistentNetworkError()");
}
});
}
/**
* Tests the local message delivery behaviour described in
* {@link BusLifecycleListener#busDisassociating(BusLifecycleEvent)}: when you
* call bus.setInitialized(true), local messages are delivered offline.
*/
public void testLocalDeliveryAfterStoppedBus() throws Exception {
System.out.println("Begin testLocalDeliveryAfterStoppedBus()");
final List<EventType> expectedEventTypes = new ArrayList<EventType>();
assertEquals(expectedEventTypes, listener.getEventTypes());
runAfterInit(new Runnable() {
@Override
public void run() {
expectedEventTypes.add(EventType.ASSOCIATING);
expectedEventTypes.add(EventType.ONLINE);
assertEquals(expectedEventTypes, listener.getEventTypes());
// as of Errai 2.2, subscriptions must be created when bus is online
bus.subscribeLocal("myLocalTestSubject", new MessageCallback() {
@Override
public void callback(Message message) {
finishTest();
}
});
// stop the bus and send disconnect signal to server
bus.stop(true);
expectedEventTypes.add(EventType.OFFLINE);
expectedEventTypes.add(EventType.DISASSOCIATING);
assertEquals(expectedEventTypes, listener.getEventTypes());
MessageBuilder.createMessage("myLocalTestSubject").withValue("cows often say moo")
.errorsHandledBy(new BusErrorCallback() {
@Override
public boolean error(Message message, Throwable throwable) {
throwable.printStackTrace();
fail("Got an error sending local message");
return false;
}
}).sendNowWith(bus);
}
});
}
/**
* Tests the local message delivery behaviour described in
* {@link BusLifecycleListener#busDisassociating(BusLifecycleEvent)}: when you
* do not call bus.setInitialized(true), local message delivery is deferred
* until the bus reconnects.
* <p/>
* <p/>
* NOTE: The contract is no longer true. If a local message can be delivered, it will be, regardless of bus state.
*/
public void ignoreTestLocalDeliveryAfterBusRestarted() throws Exception {
final List<EventType> expectedEventTypes = new ArrayList<EventType>();
// we expect the bus already fired an ASSOCIATING event way before we had a
// chance to observe it (i.e. in its constructor). So we expect the listener's
// log to be empty at this point.
assertEquals(expectedEventTypes, listener.getEventTypes());
runAfterInit(new Runnable() {
private boolean receivedLocalMessage;
@Override
public void run() {
expectedEventTypes.add(EventType.ASSOCIATING);
expectedEventTypes.add(EventType.ONLINE);
assertEquals(expectedEventTypes, listener.getEventTypes());
// as of Errai 2.2, subscriptions must be created when bus is online
bus.subscribeLocal("myLocalTestSubject", new MessageCallback() {
@Override
public void callback(Message message) {
receivedLocalMessage = true;
}
});
// stop the bus and send disconnect signal to server
bus.stop(true);
expectedEventTypes.add(EventType.OFFLINE);
expectedEventTypes.add(EventType.DISASSOCIATING);
assertEquals(expectedEventTypes, listener.getEventTypes());
MessageBuilder.createMessage("myLocalTestSubject").withValue("cows often say moo")
.errorsHandledBy(new BusErrorCallback() {
@Override
public boolean error(Message message, Throwable throwable) {
throwable.printStackTrace();
fail("Got an error sending local message");
return false;
}
}).sendNowWith(bus);
assertFalse("Message delivery should be deferred in this state", receivedLocalMessage);
// reconnect in 500ms
new Timer() {
@Override
public void run() {
assertFalse(receivedLocalMessage);
bus.init();
}
}.schedule(500);
// ensure the local message is received
final Timer t = new Timer() {
@Override
public void run() {
if (receivedLocalMessage) {
finishTest();
}
else {
System.out.println("Still waiting for local message");
// poll again later
schedule(1000);
}
}
};
t.schedule(750);
}
});
}
public void testOfflineEventHasErrorInformation() throws Exception {
final List<EventType> expectedEventTypes = new ArrayList<EventType>();
// we expect the bus already fired an ASSOCIATING event way before we had a
// chance to observe it (i.e. in its constructor). So we expect the listener's
// log to be empty at this point.
assertEquals(expectedEventTypes, listener.getEventTypes());
final RecordingTransportErrorHandler errorHandler = new RecordingTransportErrorHandler();
bus.addTransportErrorHandler(errorHandler);
runAfterInit(new Runnable() {
@Override
public void run() {
// simulate 404 on bus endpoint URL
endpointFixer = Wormhole.changeBusEndpointUrl(bus, "invalid.url");
expectedEventTypes.add(EventType.ASSOCIATING);
expectedEventTypes.add(EventType.ONLINE);
expectedEventTypes.add(EventType.OFFLINE);
pollUntilListenerSees(expectedEventTypes, new Runnable() {
@Override
public void run() {
RecordedEvent recordedEvent = listener.getEvents().get(2);
assertEquals("Picked wrong event from recorder", EventType.OFFLINE, recordedEvent.getType());
BusLifecycleEvent actualEvent = recordedEvent.getEvent();
TransportError error = actualEvent.getReason();
assertNotNull("No error information", error);
assertEquals("Wrong status code", 404, error.getStatusCode());
// this is no longer a reliable test, since the error detection is more sophisticated
// and can encounter a 404 error WITHOUT encountering a GWT RequestBuilder exception.
// This is because we detect problems on both send and receive. Not just receive anymore.
// assertNotNull("Throwable was not provided", error.getException());
// assertEquals("Wrong exception type", TransportIOException.class, error.getException().getClass());
assertNotNull("Request object was not provided", error.getRequest());
assertTrue("Bus should be planning to retry failed connection attempt",
error.getRetryInfo().getDelayUntilNextRetry() >= 0);
assertEquals(0, error.getRetryInfo().getRetryCount());
List<TransportError> transportErrors = errorHandler.getTransportErrors();
assertTrue("No errors were recorded", !transportErrors.isEmpty());
// It's possible for the send and receive channels to both encounter errors at the same time
// in an unpredictable manner, making this assertion unreliable.
// assertTrue("Got too many errors: " + transportErrors, transportErrors.size() <= 2);
assertSame("Lifecycle listener and error handler should see exact same TransportError object",
transportErrors.get(0), error);
}
});
}
});
}
public void ignoreTestDisassociatingEventHasErrorInformation() throws Exception {
final List<EventType> expectedEventTypes = new ArrayList<EventType>();
// we expect the bus already fired an ASSOCIATING event way before we had a
// chance to observe it (i.e. in its constructor). So we expect the listener's
// log to be empty at this point.
assertEquals(expectedEventTypes, listener.getEventTypes());
final RecordingTransportErrorHandler errorHandler = new RecordingTransportErrorHandler();
bus.addTransportErrorHandler(errorHandler);
runAfterInit(new Runnable() {
@Override
public void run() {
// simulate 404 on bus endpoint URL
endpointFixer = Wormhole.changeBusEndpointUrl(bus, "invalid.url");
expectedEventTypes.add(EventType.ONLINE);
expectedEventTypes.add(EventType.OFFLINE);
expectedEventTypes.add(EventType.DISASSOCIATING);
pollUntilListenerSees(expectedEventTypes, new Runnable() {
@Override
public void run() {
RecordedEvent recordedEvent = listener.getEvents().get(2);
assertEquals("Picked wrong event from recorder", EventType.DISASSOCIATING, recordedEvent.getType());
BusLifecycleEvent actualEvent = recordedEvent.getEvent();
TransportError error = actualEvent.getReason();
assertNotNull("No error information", error);
assertEquals("Wrong status code", 404, error.getStatusCode());
assertNotNull("Throwable was not provided", error.getException());
assertEquals("Wrong exception type", TransportIOException.class, error.getException().getClass());
assertNotNull("Request object was not provided", error.getRequest());
assertEquals(-1L, error.getRetryInfo().getDelayUntilNextRetry());
assertTrue("Expected at least a few retries before bus gave up", error.getRetryInfo().getRetryCount() > 2);
List<TransportError> transportErrors = errorHandler.getTransportErrors();
assertSame("Lifecycle listener and error handler should see exact same TransportError object",
transportErrors.get(transportErrors.size() - 1), error);
int expectedRetryCount = 0;
for (TransportError oldError : transportErrors.subList(0, transportErrors.size() - 1)) {
assertTrue(oldError.getRetryInfo().getDelayUntilNextRetry() > 0);
assertEquals(expectedRetryCount, oldError.getRetryInfo().getRetryCount());
expectedRetryCount++;
}
}
});
}
});
}
private void pollUntilListenerSees(final List<EventType> expected) {
pollUntilListenerSees(expected, null);
}
private void pollUntilListenerSees(final List<EventType> expected, final Runnable doAfter) {
final Timer t = new Timer() {
@Override
public void run() {
List<EventType> actual = listener.getEventTypes();
if (expected.equals(actual)) {
if (doAfter != null) {
doAfter.run();
}
finishTest();
}
else {
System.out.println("Lists do not match yet:");
System.out.println(" expected: " + expected);
System.out.println(" actual: " + actual);
// poll again later
schedule(1000);
}
}
};
t.schedule(1000);
}
}