Skip to content

Commit b0b00d1

Browse files
cjosephaJawnnypoo
authored andcommitted
Save eventually issue (#831)
* - Added a failing test for #827 - The eventually queue should be cleared at each test execution but Parse.getEventuallyQueue().clear() crashes, that's why line 1585 is commented * Removed unused imports * Fixed ParseUserTest.testSaveEventuallyWhenSessionIsInvalid() so it reproduce exactly issue #827 * Fixed server response mocking in ParseUserTest.testSaveEventuallyWhenSessionIsInvalid() * Failing test for #827 fails when the deadlock at ParseObject.java:L1604 occurs * * Suggestion of fix for #827 * Improved test for #827 : - Generalize to any server error that is different to CONNECTION_FAILED (not only INVALID_SESSION_TOKEN) - Added isDirty() checks - fixed test tearDown * Working fix suggestion for #827
1 parent 2817393 commit b0b00d1

File tree

5 files changed

+214
-27
lines changed

5 files changed

+214
-27
lines changed

Parse/src/main/java/com/parse/Parse.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,10 @@ public static boolean isLocalDatastoreEnabled() {
248248
* @param configuration The configuration for your application.
249249
*/
250250
public static void initialize(Configuration configuration) {
251+
initialize(configuration, null);
252+
}
253+
254+
static void initialize(Configuration configuration, ParsePlugins parsePlugins) {
251255
if (isInitialized()) {
252256
PLog.w(TAG, "Parse is already initialized");
253257
return;
@@ -256,7 +260,11 @@ public static void initialize(Configuration configuration) {
256260
// isLocalDataStoreEnabled() to perform additional behavior.
257261
isLocalDatastoreEnabled = configuration.localDataStoreEnabled;
258262

259-
ParsePlugins.initialize(configuration.context, configuration);
263+
if (parsePlugins == null) {
264+
ParsePlugins.initialize(configuration.context, configuration);
265+
} else {
266+
ParsePlugins.set(parsePlugins);
267+
}
260268

261269
try {
262270
ParseRESTCommand.server = new URL(configuration.server);
@@ -311,6 +319,8 @@ public Void then(Task<Void> task) throws Exception {
311319
}
312320

313321
static void destroy() {
322+
ParseObject.unregisterParseSubclasses();
323+
314324
ParseEventuallyQueue queue;
315325
synchronized (MUTEX) {
316326
queue = eventuallyQueue;
@@ -322,6 +332,8 @@ static void destroy() {
322332

323333
ParseCorePlugins.getInstance().reset();
324334
ParsePlugins.reset();
335+
336+
setLocalDatastore(null);
325337
}
326338

327339
/**

Parse/src/main/java/com/parse/ParseObject.java

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1420,6 +1420,20 @@ private ParseRESTObjectCommand currentSaveEventuallyCommand(
14201420
final ParseObject.State result, final ParseOperationSet operationsBeforeSave) {
14211421
Task<Void> task = Task.forResult(null);
14221422

1423+
/*
1424+
* If this object is in the offline store, then we need to make sure that we pull in any dirty
1425+
* changes it may have before merging the server data into it.
1426+
*/
1427+
final OfflineStore store = Parse.getLocalDatastore();
1428+
if (store != null) {
1429+
task = task.onSuccessTask(new Continuation<Void, Task<Void>>() {
1430+
@Override
1431+
public Task<Void> then(Task<Void> task) throws Exception {
1432+
return store.fetchLocallyAsync(ParseObject.this).makeVoid();
1433+
}
1434+
});
1435+
}
1436+
14231437
final boolean success = result != null;
14241438
synchronized (mutex) {
14251439
// Find operationsBeforeSave in the queue so that we can remove it and move to the next
@@ -1433,24 +1447,22 @@ private ParseRESTObjectCommand currentSaveEventuallyCommand(
14331447
// Merge the data from the failed save into the next save.
14341448
ParseOperationSet nextOperation = opIterator.next();
14351449
nextOperation.mergeFrom(operationsBeforeSave);
1450+
if (store != null) {
1451+
task = task.continueWithTask(new Continuation<Void, Task<Void>>() {
1452+
@Override
1453+
public Task<Void> then(Task<Void> task) throws Exception {
1454+
if (task.isFaulted()) {
1455+
return Task.forResult(null);
1456+
} else {
1457+
return store.updateDataForObjectAsync(ParseObject.this);
1458+
}
1459+
}
1460+
});
1461+
}
14361462
return task;
14371463
}
14381464
}
14391465

1440-
/*
1441-
* If this object is in the offline store, then we need to make sure that we pull in any dirty
1442-
* changes it may have before merging the server data into it.
1443-
*/
1444-
final OfflineStore store = Parse.getLocalDatastore();
1445-
if (store != null) {
1446-
task = task.onSuccessTask(new Continuation<Void, Task<Void>>() {
1447-
@Override
1448-
public Task<Void> then(Task<Void> task) throws Exception {
1449-
return store.fetchLocallyAsync(ParseObject.this).makeVoid();
1450-
}
1451-
});
1452-
}
1453-
14541466
// fetchLocallyAsync will return an error if this object isn't in the LDS yet and that's ok
14551467
task = task.continueWith(new Continuation<Void, Void>() {
14561468
@Override

Parse/src/test/java/com/parse/ParseInstallationTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ public void testMissingRequiredFieldWhenSaveAsync() throws Exception {
146146
ParseInstallation installation = ParseInstallation.getCurrentInstallation();
147147
assertNotNull(installation);
148148
installation.put("key", "value");
149-
installation.saveAsync(sessionToken, toAwait);
149+
ParseTaskUtils.wait(installation.saveAsync(sessionToken, toAwait));
150150
verify(controller).getAsync();
151151
verify(objController, times(2)).saveAsync(
152152
any(ParseObject.State.class),
@@ -187,7 +187,7 @@ public void testObjectNotFoundWhenSaveAsync() throws Exception {
187187
assertNotNull(installation);
188188
installation.setState(state);
189189
installation.put("key", "value");
190-
installation.saveAsync(sessionToken, toAwait);
190+
ParseTaskUtils.wait(installation.saveAsync(sessionToken, toAwait));
191191

192192
verify(controller).getAsync();
193193
verify(objController, times(2)).saveAsync(

Parse/src/test/java/com/parse/ParseTestUtils.java

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,15 @@
88
*/
99
package com.parse;
1010

11+
import android.content.Context;
12+
1113
import com.parse.http.ParseHttpRequest;
1214
import com.parse.http.ParseHttpResponse;
1315

1416
import org.json.JSONObject;
1517

1618
import java.io.ByteArrayInputStream;
19+
import java.io.File;
1720
import java.io.IOException;
1821

1922
import bolts.Task;
@@ -41,15 +44,51 @@ public static void setTestParseUser() {
4144

4245
public static ParseHttpClient mockParseHttpClientWithResponse(
4346
JSONObject content, int statusCode, String reasonPhrase) throws IOException {
47+
ParseHttpClient client = mock(ParseHttpClient.class);
48+
updateMockParseHttpClientWithResponse(client, content, statusCode, reasonPhrase);
49+
return client;
50+
}
51+
52+
static void updateMockParseHttpClientWithResponse(
53+
ParseHttpClient client, JSONObject content, int statusCode, String reasonPhrase) throws IOException {
4454
byte[] contentBytes = content.toString().getBytes();
4555
ParseHttpResponse response = new ParseHttpResponse.Builder()
46-
.setContent(new ByteArrayInputStream(contentBytes))
47-
.setStatusCode(statusCode)
48-
.setTotalSize(contentBytes.length)
49-
.setContentType("application/json")
50-
.build();
51-
ParseHttpClient client = mock(ParseHttpClient.class);
56+
.setContent(new ByteArrayInputStream(contentBytes))
57+
.setStatusCode(statusCode)
58+
.setTotalSize(contentBytes.length)
59+
.setContentType("application/json")
60+
.build();
5261
when(client.execute(any(ParseHttpRequest.class))).thenReturn(response);
53-
return client;
62+
}
63+
64+
static ParsePlugins mockParsePlugins(Parse.Configuration configuration) {
65+
ParsePlugins parsePlugins = mock(ParsePlugins.class);
66+
when(parsePlugins.applicationId()).thenReturn(configuration.applicationId);
67+
when(parsePlugins.clientKey()).thenReturn(configuration.clientKey);
68+
when(parsePlugins.configuration()).thenReturn(configuration);
69+
Context applicationContext = configuration.context.getApplicationContext();
70+
when(parsePlugins.applicationContext()).thenReturn(applicationContext);
71+
File parseDir = createFileDir(applicationContext.getDir("Parse", Context.MODE_PRIVATE));
72+
when(parsePlugins.installationId())
73+
.thenReturn(
74+
new InstallationId(new File(parseDir, "installationId")));
75+
when(parsePlugins.getParseDir())
76+
.thenReturn(parseDir);
77+
when(parsePlugins.getCacheDir())
78+
.thenReturn(createFileDir(
79+
new File(applicationContext.getCacheDir(), "com.parse")));
80+
when(parsePlugins.getFilesDir())
81+
.thenReturn(createFileDir(
82+
new File(applicationContext.getFilesDir(), "com.parse")));
83+
return parsePlugins;
84+
}
85+
86+
private static File createFileDir(File file) {
87+
if (!file.exists()) {
88+
if (!file.mkdirs()) {
89+
return file;
90+
}
91+
}
92+
return file;
5493
}
5594
}

Parse/src/test/java/com/parse/ParseUserTest.java

Lines changed: 127 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
*/
99
package com.parse;
1010

11+
import android.Manifest;
1112
import android.os.Parcel;
1213

1314
import org.json.JSONObject;
@@ -20,14 +21,20 @@
2021
import org.mockito.ArgumentCaptor;
2122
import org.mockito.Matchers;
2223
import org.robolectric.RobolectricTestRunner;
24+
import org.robolectric.RuntimeEnvironment;
25+
import org.robolectric.Shadows;
2326
import org.robolectric.annotation.Config;
2427

2528
import java.util.Collections;
29+
import java.util.Date;
2630
import java.util.HashMap;
2731
import java.util.Map;
32+
import java.util.concurrent.CountDownLatch;
2833
import java.util.concurrent.Semaphore;
2934
import java.util.concurrent.TimeUnit;
3035

36+
import bolts.Capture;
37+
import bolts.Continuation;
3138
import bolts.Task;
3239

3340
import static org.junit.Assert.assertEquals;
@@ -68,9 +75,19 @@ public void setUp() throws Exception {
6875
@After
6976
public void tearDown() throws Exception {
7077
super.tearDown();
71-
ParseObject.unregisterSubclass(ParseUser.class);
72-
ParseObject.unregisterSubclass(ParseSession.class);
73-
Parse.disableLocalDatastore();
78+
if (ParsePlugins.get() != null) {
79+
ParseCurrentInstallationController installationController =
80+
ParseCorePlugins.getInstance().getCurrentInstallationController();
81+
if (installationController != null) {
82+
installationController.clearFromDisk();
83+
}
84+
ParseCurrentUserController userController =
85+
ParseCorePlugins.getInstance().getCurrentUserController();
86+
if (userController != null) {
87+
userController.clearFromDisk();
88+
}
89+
}
90+
Parse.destroy();
7491
}
7592

7693
@Test
@@ -1541,4 +1558,111 @@ private static void setLazy(ParseUser user) {
15411558
anonymousAuthData.put("anonymousToken", "anonymousTest");
15421559
user.putAuthData(ParseAnonymousUtils.AUTH_TYPE, anonymousAuthData);
15431560
}
1561+
1562+
//region testSaveEventuallyWhenServerError
1563+
1564+
@Test
1565+
public void testSaveEventuallyWhenServerError() throws Exception {
1566+
Shadows.shadowOf(RuntimeEnvironment.application)
1567+
.grantPermissions(Manifest.permission.ACCESS_NETWORK_STATE);
1568+
Parse.Configuration configuration =
1569+
new Parse.Configuration.Builder(RuntimeEnvironment.application)
1570+
.applicationId(BuildConfig.APPLICATION_ID)
1571+
.server("https://api.parse.com/1")
1572+
.enableLocalDataStore()
1573+
.build();
1574+
ParsePlugins plugins = ParseTestUtils.mockParsePlugins(configuration);
1575+
JSONObject mockResponse = new JSONObject();
1576+
mockResponse.put("objectId", "objectId");
1577+
mockResponse.put("email", "[email protected]");
1578+
mockResponse.put("username", "username");
1579+
mockResponse.put("sessionToken", "r:sessionToken");
1580+
mockResponse.put("createdAt", ParseDateFormat.getInstance().format(new Date(1000)));
1581+
mockResponse.put("updatedAt", ParseDateFormat.getInstance().format(new Date(2000)));
1582+
ParseHttpClient restClient = ParseTestUtils.mockParseHttpClientWithResponse(
1583+
mockResponse,200, "OK");
1584+
when(plugins.restClient())
1585+
.thenReturn(restClient);
1586+
Parse.initialize(configuration, plugins);
1587+
1588+
ParseUser user = ParseUser.logIn("username", "password");
1589+
assertFalse(user.isDirty());
1590+
1591+
user.put("field", "data");
1592+
assertTrue(user.isDirty());
1593+
1594+
mockResponse = new JSONObject();
1595+
mockResponse.put("updatedAt", ParseDateFormat.getInstance().format(new Date(3000)));
1596+
ParseTestUtils.updateMockParseHttpClientWithResponse(
1597+
restClient, mockResponse, 200, "OK");
1598+
1599+
final CountDownLatch saveCountDown1 = new CountDownLatch(1);
1600+
final Capture<Exception> exceptionCapture = new Capture<>();
1601+
user.saveInBackground().continueWith(new Continuation<Void, Void>() {
1602+
@Override
1603+
public Void then(Task<Void> task) throws Exception {
1604+
exceptionCapture.set(task.getError());
1605+
saveCountDown1.countDown();
1606+
return null;
1607+
}
1608+
});
1609+
assertTrue(saveCountDown1.await(5, TimeUnit.SECONDS));
1610+
assertNull(exceptionCapture.get());
1611+
assertFalse(user.isDirty());
1612+
1613+
user.put("field", "other data");
1614+
assertTrue(user.isDirty());
1615+
1616+
mockResponse = new JSONObject();
1617+
mockResponse.put("error", "Save is not allowed");
1618+
mockResponse.put("code", 141);
1619+
ParseTestUtils.updateMockParseHttpClientWithResponse(
1620+
restClient, mockResponse, 400, "Bad Request");
1621+
1622+
final CountDownLatch saveEventuallyCountDown = new CountDownLatch(1);
1623+
user.saveEventually().continueWith(new Continuation<Void, Void>() {
1624+
@Override
1625+
public Void then(Task<Void> task) throws Exception {
1626+
exceptionCapture.set(task.getError());
1627+
saveEventuallyCountDown.countDown();
1628+
return null;
1629+
}
1630+
});
1631+
assertTrue(saveEventuallyCountDown.await(5, TimeUnit.SECONDS));
1632+
assertTrue(exceptionCapture.get() instanceof ParseException);
1633+
assertEquals(ParseException.SCRIPT_ERROR, ((ParseException)exceptionCapture.get()).getCode());
1634+
assertEquals("Save is not allowed", exceptionCapture.get().getMessage());
1635+
assertTrue(user.isDirty());
1636+
1637+
// Simulate reboot
1638+
Parse.destroy();
1639+
Parse.initialize(configuration, plugins);
1640+
1641+
user = ParseUser.getCurrentUser();
1642+
assertTrue(user.isDirty());
1643+
1644+
assertEquals("other data", user.get("field"));
1645+
user.put("field", "another data");
1646+
1647+
mockResponse = new JSONObject();
1648+
mockResponse.put("updatedAt", ParseDateFormat.getInstance().format(new Date(4000)));
1649+
ParseTestUtils.updateMockParseHttpClientWithResponse(
1650+
restClient, mockResponse, 200, "OK");
1651+
1652+
final CountDownLatch saveCountDown2 = new CountDownLatch(1);
1653+
user.saveInBackground().continueWith(new Continuation<Void, Void>() {
1654+
@Override
1655+
public Void then(Task<Void> task) throws Exception {
1656+
exceptionCapture.set(task.getError());
1657+
saveCountDown2.countDown();
1658+
return null;
1659+
}
1660+
});
1661+
1662+
assertTrue(saveCountDown2.await(5, TimeUnit.SECONDS));
1663+
assertNull(exceptionCapture.get());
1664+
assertFalse(user.isDirty());
1665+
}
1666+
1667+
//endregion
15441668
}

0 commit comments

Comments
 (0)