/* * 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.jpa.sync.test.client; import java.sql.Timestamp; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import org.jboss.errai.common.client.api.Caller; import org.jboss.errai.common.client.api.ErrorCallback; import org.jboss.errai.common.client.api.RemoteCallback; import org.jboss.errai.common.client.api.extension.InitVotes; import org.jboss.errai.enterprise.client.cdi.CDIClientBootstrap; import org.jboss.errai.enterprise.client.cdi.api.CDI; import org.jboss.errai.ioc.client.Container; import org.jboss.errai.ioc.client.container.Factory; import org.jboss.errai.ioc.client.container.IOC; import org.jboss.errai.ioc.client.lifecycle.api.StateChange; import org.jboss.errai.jpa.client.local.ErraiEntityManager; import org.jboss.errai.jpa.sync.client.local.ClientSyncManager; import org.jboss.errai.jpa.sync.client.shared.DataSyncService; import org.jboss.errai.jpa.sync.client.shared.DeleteResponse; import org.jboss.errai.jpa.sync.client.shared.IdChangeResponse; import org.jboss.errai.jpa.sync.client.shared.NewRemoteEntityResponse; import org.jboss.errai.jpa.sync.client.shared.SyncRequestOperation; import org.jboss.errai.jpa.sync.client.shared.SyncResponse; import org.jboss.errai.jpa.sync.client.shared.SyncableDataSet; import org.jboss.errai.jpa.sync.client.shared.UpdateResponse; import org.jboss.errai.jpa.sync.test.client.entity.SimpleEntity; import org.jboss.errai.jpa.sync.test.client.ioc.DependentScopedSyncBean; import com.google.gwt.junit.client.GWTTestCase; import com.google.gwt.user.client.Timer; import junit.framework.AssertionFailedError; public class ClientSyncManagerIntegrationTest extends GWTTestCase { private ClientSyncManager csm; private DependentScopedSyncBean syncBean; public native void setRemoteCommunicationEnabled(boolean enabled) /*-{ $wnd.erraiBusRemoteCommunicationEnabled = enabled; }-*/; @Override public String getModuleName() { return "org.jboss.errai.jpa.sync.test.DataSyncTests"; } @Override protected void gwtSetUp() throws Exception { setRemoteCommunicationEnabled(false); InitVotes.setTimeoutMillis(60000); ClientSyncManager.resetInstance(); // Unfortunately, GWTTestCase does not call our inherited module's onModuleLoad() methods // http://code.google.com/p/google-web-toolkit/issues/detail?id=3791 new CDI().__resetSubsystem(); new Container().onModuleLoad(); new CDIClientBootstrap().onModuleLoad(); InitVotes.startInitPolling(); super.gwtSetUp(); csm = IOC.getBeanManager().lookupBean(ClientSyncManager.class).getInstance(); csm.getDesiredStateEm().removeAll(); csm.getExpectedStateEm().removeAll(); } @Override protected void gwtTearDown() throws Exception { assertFalse("ClientSyncManager 'sync in progress' flag got stuck on true", csm.isSyncInProgress()); if (syncBean != null) { IOC.getBeanManager().destroyBean(syncBean); } Container.reset(); IOC.reset(); InitVotes.reset(); setRemoteCommunicationEnabled(true); super.gwtTearDown(); } public void testNewEntityFromServer() { final SimpleEntity newEntity = new SimpleEntity(); newEntity.setString("the string value"); newEntity.setDate(new Timestamp(1234567L)); newEntity.setInteger(9999); SimpleEntity.setId(newEntity, 88L); final List<SyncRequestOperation<SimpleEntity>> expectedClientRequests = new ArrayList<>(); // in this case, the client should make an empty request (both persistence contexts are empty) final List<SyncResponse<SimpleEntity>> fakeServerResponses = new ArrayList<>(); fakeServerResponses.add(new NewRemoteEntityResponse<>(newEntity)); performColdSync(expectedClientRequests, fakeServerResponses); final ErraiEntityManager esem = csm.getExpectedStateEm(); final ErraiEntityManager dsem = csm.getDesiredStateEm(); final SimpleEntity newEntityExpected = esem.find(SimpleEntity.class, newEntity.getId()); final SimpleEntity newEntityDesired = dsem.find(SimpleEntity.class, newEntity.getId()); assertEquals(newEntityExpected.toString(), newEntity.toString()); assertEquals(newEntityDesired.toString(), newEntity.toString()); assertNotSame("Expected State and Desired State instances must be separate", newEntityExpected, newEntityDesired); } public void testIdChangeFromServer() { final SimpleEntity entity = new SimpleEntity(); entity.setString("the string value"); entity.setDate(new Timestamp(1234567L)); entity.setInteger(9999); final ErraiEntityManager esem = csm.getExpectedStateEm(); final ErraiEntityManager dsem = csm.getDesiredStateEm(); final SimpleEntity originalLocalState = dsem.merge(entity); final long originalId = originalLocalState.getId(); dsem.flush(); dsem.detach(originalLocalState); assertNull(esem.find(SimpleEntity.class, originalId)); assertEquals(dsem.find(SimpleEntity.class, originalId).toString(), entity.toString()); // Now change the ID and tell the ClientSyncManager it happened final long newId = originalLocalState.getId() + 100; SimpleEntity.setId(entity, newId); final List<SyncRequestOperation<SimpleEntity>> expectedClientRequests = new ArrayList<>(); expectedClientRequests.add(SyncRequestOperation.created(originalLocalState)); final List<SyncResponse<SimpleEntity>> fakeServerResponses = new ArrayList<>(); fakeServerResponses.add(new IdChangeResponse<>(originalId, entity)); performColdSync(expectedClientRequests, fakeServerResponses); assertNull(esem.find(SimpleEntity.class, originalId)); assertNull(dsem.find(SimpleEntity.class, originalId)); final SimpleEntity changedEntityExpected = esem.find(SimpleEntity.class, newId); final SimpleEntity changedEntityDesired = dsem.find(SimpleEntity.class, newId); assertEquals(changedEntityExpected.toString(), entity.toString()); assertEquals(changedEntityDesired.toString(), entity.toString()); assertNotSame(changedEntityDesired, changedEntityExpected); } // a hybrid of "new entity from server" and "id change from server" // the scenario is that we have a local entity with, say, ID 100 // and the server tells us "here's a new entity. its ID is 100!" // so we have to move our existing entity out of the way before accepting the remote one. public void testNewEntityWithConflictingIdFromServer() { final SimpleEntity newRemote = new SimpleEntity(); newRemote.setString("new entity from server"); newRemote.setDate(new Timestamp(1234567L)); newRemote.setInteger(9999); SimpleEntity.setId(newRemote, 100L); final SimpleEntity existingLocal = new SimpleEntity(); existingLocal.setString("existing local entity"); existingLocal.setDate(new Timestamp(7654321L)); existingLocal.setInteger(8888); SimpleEntity.setId(existingLocal, 100L); final ErraiEntityManager esem = csm.getExpectedStateEm(); final ErraiEntityManager dsem = csm.getDesiredStateEm(); dsem.persist(existingLocal); dsem.flush(); assertNull(esem.find(SimpleEntity.class, existingLocal.getId())); assertEquals(dsem.find(SimpleEntity.class, existingLocal.getId()).toString(), existingLocal.toString()); final List<SyncRequestOperation<SimpleEntity>> expectedClientRequests = new ArrayList<>(); expectedClientRequests.add(SyncRequestOperation.created(existingLocal)); // note that the mock // server will ignore // this for the purpose // of this test final List<SyncResponse<SimpleEntity>> fakeServerResponses = new ArrayList<>(); fakeServerResponses.add(new NewRemoteEntityResponse<>(newRemote)); performColdSync(expectedClientRequests, fakeServerResponses); // now the ID of existingLocal (still managed by dsem) should not be 100 anymore assertFalse("Existing id " + existingLocal.getId() + " should not be the same as new object's ID " + newRemote.getId(), existingLocal.getId() == newRemote.getId()); assertSame(existingLocal, dsem.find(SimpleEntity.class, existingLocal.getId())); assertEquals(newRemote.toString(), dsem.find(SimpleEntity.class, newRemote.getId()).toString()); assertEquals(newRemote.toString(), esem.find(SimpleEntity.class, newRemote.getId()).toString()); } public void testUpdateFromServer() { final SimpleEntity newEntity = new SimpleEntity(); newEntity.setString("the string value"); newEntity.setDate(new Timestamp(1234567L)); newEntity.setInteger(9999); final ErraiEntityManager esem = csm.getExpectedStateEm(); final ErraiEntityManager dsem = csm.getDesiredStateEm(); // persist this as both the "expected state" from the server and the "desired state" on the // client final SimpleEntity originalEntityState = esem.merge(newEntity); esem.flush(); esem.clear(); dsem.persist(originalEntityState); dsem.flush(); dsem.clear(); final List<SyncRequestOperation<SimpleEntity>> expectedClientRequests = new ArrayList<>(); expectedClientRequests.add(SyncRequestOperation.unchanged(originalEntityState)); // now cook up a server response that says something changed SimpleEntity.setId(newEntity, originalEntityState.getId()); newEntity.setString("a new string value"); newEntity.setInteger(110011); final List<SyncResponse<SimpleEntity>> fakeServerResponses = new ArrayList<>(); fakeServerResponses.add(new UpdateResponse<>(newEntity)); performColdSync(expectedClientRequests, fakeServerResponses); final SimpleEntity changedEntityExpected = esem.find(SimpleEntity.class, newEntity.getId()); final SimpleEntity changedEntityDesired = dsem.find(SimpleEntity.class, newEntity.getId()); assertEquals(changedEntityExpected.toString(), newEntity.toString()); assertEquals(changedEntityDesired.toString(), newEntity.toString()); assertNotSame(changedEntityDesired, changedEntityExpected); } public void testDeleteFromServer() { final SimpleEntity newEntity = new SimpleEntity(); newEntity.setString("the string value"); newEntity.setDate(new Timestamp(1234567L)); newEntity.setInteger(9999); final ErraiEntityManager esem = csm.getExpectedStateEm(); final ErraiEntityManager dsem = csm.getDesiredStateEm(); // persist this as both the "expected state" from the server and the "desired state" on the // client final SimpleEntity originalEntityState = esem.merge(newEntity); esem.flush(); esem.clear(); dsem.persist(originalEntityState); dsem.flush(); dsem.clear(); final List<SyncRequestOperation<SimpleEntity>> expectedClientRequests = new ArrayList<>(); expectedClientRequests.add(SyncRequestOperation.unchanged(originalEntityState)); // now cook up a server response that says it got deleted final List<SyncResponse<SimpleEntity>> fakeServerResponses = new ArrayList<>(); fakeServerResponses.add(new DeleteResponse<>(newEntity)); performColdSync(expectedClientRequests, fakeServerResponses); assertNull(esem.find(SimpleEntity.class, newEntity.getId())); assertNull(dsem.find(SimpleEntity.class, newEntity.getId())); // finally, ensure the deleted entity is not stuck in the REMOVED state // (should be NEW or DETACHED; we can verify by trying to merge it) try { esem.merge(newEntity); } catch (final IllegalArgumentException e) { fail("Merging removed entity failed: " + e); } try { dsem.merge(newEntity); } catch (final IllegalArgumentException e) { fail("Merging removed entity failed: " + e); } } public void testUpdateFromClient() { final SimpleEntity entity = new SimpleEntity(); entity.setString("the string value"); entity.setDate(new Timestamp(1234567L)); entity.setInteger(9999); final ErraiEntityManager esem = csm.getExpectedStateEm(); final ErraiEntityManager dsem = csm.getDesiredStateEm(); // first persist the expected state final SimpleEntity originalEntityState = esem.merge(entity); esem.flush(); esem.clear(); // now make a change and persist the desired state SimpleEntity.setId(entity, originalEntityState.getId()); entity.setString("this has been updated"); dsem.persist(entity); dsem.flush(); dsem.clear(); final List<SyncRequestOperation<SimpleEntity>> expectedClientRequests = new ArrayList<>(); expectedClientRequests.add(SyncRequestOperation.updated(entity, originalEntityState)); // the server will respond with confirmation of the update final List<SyncResponse<SimpleEntity>> fakeServerResponses = new ArrayList<>(); fakeServerResponses.add(new UpdateResponse<>(entity)); performColdSync(expectedClientRequests, fakeServerResponses); final SimpleEntity changedEntityExpected = esem.find(SimpleEntity.class, entity.getId()); final SimpleEntity changedEntityDesired = dsem.find(SimpleEntity.class, entity.getId()); assertEquals(changedEntityExpected.toString(), entity.toString()); assertEquals(changedEntityDesired.toString(), entity.toString()); assertNotSame(changedEntityDesired, changedEntityExpected); } public void testDeleteFromClient() { final SimpleEntity entity = new SimpleEntity(); entity.setString("the string value"); entity.setDate(new Timestamp(1234567L)); entity.setInteger(9999); final ErraiEntityManager esem = csm.getExpectedStateEm(); final ErraiEntityManager dsem = csm.getDesiredStateEm(); // first persist the expected state esem.persist(entity); esem.flush(); esem.clear(); final List<SyncRequestOperation<SimpleEntity>> expectedClientRequests = new ArrayList<>(); expectedClientRequests.add(SyncRequestOperation.deleted(entity)); // assuming no conflict, the server deletes the entity and generates the appropriate response final List<SyncResponse<SimpleEntity>> fakeServerResponses = new ArrayList<>(); fakeServerResponses.add(new DeleteResponse<>(entity)); performColdSync(expectedClientRequests, fakeServerResponses); assertNull(esem.find(SimpleEntity.class, entity.getId())); assertNull(dsem.find(SimpleEntity.class, entity.getId())); } public void testConcurrentSyncRequestsRejected() { final SimpleEntity entity = new SimpleEntity(); entity.setString("the string value"); entity.setDate(new Timestamp(1234567L)); entity.setInteger(9999); final ErraiEntityManager esem = csm.getExpectedStateEm(); final ErraiEntityManager dsem = csm.getDesiredStateEm(); // first persist the desired state final SimpleEntity clientEntity = dsem.merge(entity); dsem.flush(); dsem.clear(); final Long originalId = clientEntity.getId(); final List<SyncRequestOperation<SimpleEntity>> expectedClientRequests = new ArrayList<>(); expectedClientRequests.add(SyncRequestOperation.created(clientEntity)); // the server creates the entity with a different ID and notifies us of the change final List<SyncResponse<SimpleEntity>> fakeServerResponses = new ArrayList<>(); SimpleEntity.setId(entity, 1010L); fakeServerResponses.add(new IdChangeResponse<>(originalId, entity)); final Runnable doDuringSync = new Runnable() { boolean alreadyRunning = false; @Override public void run() { if (alreadyRunning) { fail("Detected recursive call to coldSync()"); } alreadyRunning = true; // at this point, ClientSyncManager is in the middle of a coldSync call. for safety, it is // required to fail. try { csm.coldSync("allSimpleEntities", SimpleEntity.class, Collections.<String, Object> emptyMap(), new RemoteCallback<List<SyncResponse<SimpleEntity>>>() { @Override public void callback(final List<SyncResponse<SimpleEntity>> response) { fail("this recursive call to coldSync must not succeed"); } }, new ErrorCallback<List<SyncResponse<SimpleEntity>>>() { @Override public boolean error(final List<SyncResponse<SimpleEntity>> message, final Throwable throwable) { fail("this recursive call to coldSync should have failed synchronously"); throw new AssertionError(); } }); fail("recursive call to coldSync() failed to throw an exception"); } catch (final IllegalStateException ex) { System.out .println("Got expected IllegalStateException. Returning normally so client state assertions can run."); // expected } } }; performColdSync(expectedClientRequests, fakeServerResponses, doDuringSync); // now ensure the results of the original sync request were not harmed assertFalse(csm.isSyncInProgress()); assertEquals(esem.find(SimpleEntity.class, entity.getId()).toString(), entity.toString()); assertEquals(dsem.find(SimpleEntity.class, entity.getId()).toString(), entity.toString()); assertNull(esem.find(SimpleEntity.class, originalId)); assertNull(dsem.find(SimpleEntity.class, originalId)); } public void testDeclarativeSync() { delayTestFinish(45000); final List<SyncResponse<SimpleEntity>> expectedSyncResponses = new ArrayList<>(); final Map<String, Object> parameters = new HashMap<>(); // replace the caller so we can see what the SyncWorker asks its ClientSyncManager to do Factory.maybeUnwrapProxy(csm).dataSyncService = new Caller<DataSyncService>() { @Override public DataSyncService call(final RemoteCallback<?> callback) { return new DataSyncService() { @SuppressWarnings({ "unchecked", "rawtypes" }) @Override public <X> List<SyncResponse<X>> coldSync(final SyncableDataSet<X> dataSet, final List<SyncRequestOperation<X>> actualClientRequests) { System.out.println("Short-circuiting DataSyncService call:"); System.out.println(" dataSet = " + dataSet); System.out.println(" actualClientRequests = " + actualClientRequests); // Don't assert anything here! The timer we start later on will still fire if the test // fails at this point! parameters.putAll(dataSet.getParameters()); final RemoteCallback rawRemoteCallback = callback; rawRemoteCallback.callback(expectedSyncResponses); return null; // this is the Caller stub. it doesn't return the value directly. } }; } @Override public DataSyncService call(final RemoteCallback<?> callback, final ErrorCallback<?> errorCallback) { return call(callback); } @Override public DataSyncService call() { fail("Unexpected use of callback"); return null; // NOTREACHED } }; syncBean = IOC.getBeanManager().lookupBean(DependentScopedSyncBean.class).getInstance(); new Timer() { @Override public void run() { assertEquals(1l, parameters.get("id")); assertEquals("test", parameters.get("string")); assertEquals("literalValue", parameters.get("literal")); // should get back the exact list of sync responses that we returned from our fake // Caller<DataSyncService> above assertNotNull(syncBean.getResponses()); assertSame(expectedSyncResponses, syncBean.getResponses().getResponses()); // we expect 2 sync tasks to have happened (one after a short delay in start() and the first // repeating one after 5s) assertEquals(2, syncBean.getCallbackCount()); finishTest(); } }.schedule(7000); } @SuppressWarnings("unchecked") public void testDeclarativeSyncAndFieldValueChanges() { delayTestFinish(45000); final List<SyncResponse<SimpleEntity>> expectedSyncResponses = new ArrayList<>(); final Map<String, Object> parameters = new HashMap<>(); // replace the caller so we can see what the SyncWorker asks its ClientSyncManager to do csm.dataSyncService = new Caller<DataSyncService>() { @Override public DataSyncService call(final RemoteCallback<?> callback) { return new DataSyncService() { @SuppressWarnings({ "rawtypes" }) @Override public <X> List<SyncResponse<X>> coldSync(final SyncableDataSet<X> dataSet, final List<SyncRequestOperation<X>> actualClientRequests) { System.out.println("Short-circuiting DataSyncService call:"); System.out.println(" dataSet = " + dataSet); System.out.println(" actualClientRequests = " + actualClientRequests); // Don't assert anything here! The timer we start later on will still fire if the test // fails at this point! parameters.putAll(dataSet.getParameters()); final RemoteCallback rawRemoteCallback = callback; rawRemoteCallback.callback(expectedSyncResponses); return null; // this is the Caller stub. it doesn't return the value directly. } }; } @Override public DataSyncService call(final RemoteCallback<?> callback, final ErrorCallback<?> errorCallback) { return call(callback); } @Override public DataSyncService call() { fail("Unexpected use of callback"); return null; // NOTREACHED } }; // Change the field values and fire IOC state change event so the sync worker can update its // query parameters syncBean = IOC.getBeanManager().lookupBean(DependentScopedSyncBean.class).getInstance(); syncBean.setId(1337); syncBean.setName("changed"); final StateChange<DependentScopedSyncBean> changeEvent = IOC.getBeanManager().lookupBean(StateChange.class).getInstance(); changeEvent.fireAsync(syncBean); new Timer() { @Override public void run() { assertEquals(1337l, parameters.get("id")); assertEquals("changed", parameters.get("string")); assertEquals("literalValue", parameters.get("literal")); // should get back the exact list of sync responses that we returned from our fake // Caller<DataSyncService> above assertNotNull(syncBean.getResponses()); assertSame(expectedSyncResponses, syncBean.getResponses().getResponses()); // we expect 2 sync tasks to have happened (one after a short delay in start() and the first // repeating one after 5s) assertEquals(2, syncBean.getCallbackCount()); finishTest(); } }.schedule(7000); } public void testDestructionCallbackStopSyncWorker() { delayTestFinish(45000); final List<SyncResponse<SimpleEntity>> expectedSyncResponses = new ArrayList<>(); // replace the caller so we can see what the SyncWorker asks its ClientSyncManager to do csm.dataSyncService = new Caller<DataSyncService>() { @Override public DataSyncService call(final RemoteCallback<?> callback) { return new DataSyncService() { @SuppressWarnings({ "unchecked", "rawtypes" }) @Override public <X> List<SyncResponse<X>> coldSync(final SyncableDataSet<X> dataSet, final List<SyncRequestOperation<X>> actualClientRequests) { System.out.println("Short-circuiting DataSyncService call:"); System.out.println(" dataSet = " + dataSet); System.out.println(" actualClientRequests = " + actualClientRequests); final RemoteCallback rawRemoteCallback = callback; rawRemoteCallback.callback(expectedSyncResponses); return null; // this is the Caller stub. it doesn't return the value directly. } }; } @Override public DataSyncService call(final RemoteCallback<?> callback, final ErrorCallback<?> errorCallback) { return call(callback); } @Override public DataSyncService call() { fail("Unexpected use of callback"); return null; // NOTREACHED } }; syncBean = IOC.getBeanManager().lookupBean(DependentScopedSyncBean.class).getInstance(); IOC.getBeanManager().destroyBean(syncBean); new Timer() { @Override public void run() { assertEquals(0, syncBean.getCallbackCount()); syncBean = null; finishTest(); } }.schedule(7000); } /** * Calls ClientSyncManager.coldSync() in a way that no actual server communication happens. The * given "fake" server response is returned immediately to the ClientSyncManager's callback * function. * * @param expectedClientRequests * The list of requests that the ClientSyncManager is expected to produce, based on the * current state of its Expected State EntityManager and its Desired State EntityManager. * If the contents of this list do not match the list produced by the ClientSyncManager, * this method will throw an {@link AssertionFailedError}. * @param fakeServerResponses * The list of SyncResponse operations to feed back to ClientSyncManager. The * ClientSyncManager will process this list as if it was returned by the server. */ private <Y> void performColdSync( final List<SyncRequestOperation<Y>> expectedClientRequests, final List<SyncResponse<Y>> fakeServerResponses) { performColdSync(expectedClientRequests, fakeServerResponses, null); } /** * Calls ClientSyncManager.coldSync() in a way that no actual server communication happens. The * given "fake" server response is returned immediately to the ClientSyncManager's callback * function. * * @param expectedClientRequests * The list of requests that the ClientSyncManager is expected to produce, based on the * current state of its Expected State EntityManager and its Desired State EntityManager. * If the contents of this list do not match the list produced by the ClientSyncManager, * this method will throw an {@link AssertionFailedError}. * @param fakeServerResponses * The list of SyncResponse operations to feed back to ClientSyncManager. The * ClientSyncManager will process this list as if it was returned by the server. * @param doDuringSync * If non-null, this runnable is executed after the sync request ops are checked against * the expected ones, but before the fake responses are delivered to the client sync * manager. */ private <Y> void performColdSync( final List<SyncRequestOperation<Y>> expectedClientRequests, final List<SyncResponse<Y>> fakeServerResponses, final Runnable doDuringSync) { csm.dataSyncService = new Caller<DataSyncService>() { @Override public DataSyncService call(final RemoteCallback<?> callback) { return new DataSyncService() { @SuppressWarnings({ "unchecked", "rawtypes" }) @Override public <X> List<SyncResponse<X>> coldSync(final SyncableDataSet<X> dataSet, final List<SyncRequestOperation<X>> actualClientRequests) { final List erasedExpectedClientRequests = expectedClientRequests; assertSyncRequestsEqual(erasedExpectedClientRequests, actualClientRequests); if (doDuringSync != null) { doDuringSync.run(); } final RemoteCallback erasedCallback = callback; erasedCallback.callback(fakeServerResponses); return null; } }; } @Override public DataSyncService call(final RemoteCallback<?> callback, final ErrorCallback<?> errorCallback) { return call(callback); } @Override public DataSyncService call() { fail("Unexpected use of callback"); return null; // NOTREACHED } }; System.out.println("Overrode DataSyncService in ClientSyncManager"); csm.coldSync("allSimpleEntities", SimpleEntity.class, Collections.<String, Object> emptyMap(), new RemoteCallback<List<SyncResponse<SimpleEntity>>>() { @Override public void callback(final List<SyncResponse<SimpleEntity>> response) { System.out.println("Got sync callback"); } }, null); } private static <X> void assertSyncRequestsEqual( final List<SyncRequestOperation<X>> expected, final List<SyncRequestOperation<X>> actual) { assertEquals( "List lengths differ. Expected: " + stringify(expected) + ", actual: " + stringify(actual), expected.size(), actual.size()); for (int i = 0; i < expected.size(); i++) { assertEquals("Ops differ at index " + i, stringify(expected.get(i)), stringify(actual.get(i))); } } private static <X> String stringify(final List<SyncRequestOperation<X>> ops) { final StringBuilder sb = new StringBuilder(500); for (final SyncRequestOperation<X> op : ops) { sb.append(stringify(op)).append(" "); } return sb.toString(); } private static <X> String stringify(final SyncRequestOperation<X> op) { return "[" + op.getType() + ": expected=" + op.getExpectedState() + ", desired=" + op.getEntity() + "]"; } }