Upstream version 5.34.104.0
[platform/framework/web/crosswalk.git] / src / chrome / test / android / javatests / src / org / chromium / chrome / test / util / browser / sync / SyncTestUtil.java
1 // Copyright 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 package org.chromium.chrome.test.util.browser.sync;
6
7 import android.accounts.Account;
8 import android.content.Context;
9 import android.util.Log;
10 import android.util.Pair;
11
12 import junit.framework.Assert;
13
14 import static org.chromium.base.test.util.ScalableTimeout.scaleTimeout;
15
16 import org.chromium.base.CommandLine;
17 import org.chromium.base.ThreadUtils;
18 import org.chromium.base.test.util.AdvancedMockContext;
19 import org.chromium.chrome.browser.sync.ProfileSyncService;
20 import org.chromium.chrome.test.util.TestHttpServerClient;
21 import org.chromium.content.browser.test.util.Criteria;
22 import org.chromium.content.browser.test.util.CriteriaHelper;
23 import org.chromium.sync.signin.AccountManagerHelper;
24 import org.chromium.sync.signin.ChromeSigninController;
25 import org.chromium.sync.test.util.AccountHolder;
26 import org.chromium.sync.test.util.MockAccountManager;
27 import org.json.JSONArray;
28 import org.json.JSONException;
29 import org.json.JSONObject;
30
31 import java.util.HashMap;
32 import java.util.Map;
33 import java.util.concurrent.Callable;
34 import java.util.concurrent.Semaphore;
35 import java.util.concurrent.TimeUnit;
36 import java.util.concurrent.atomic.AtomicBoolean;
37 import java.util.concurrent.atomic.AtomicLong;
38
39 public final class SyncTestUtil {
40
41     public static final String DEFAULT_TEST_ACCOUNT = "test@gmail.com";
42     public static final String DEFAULT_PASSWORD = "myPassword";
43     public static final String CHROME_SYNC_OAUTH2_SCOPE =
44             "oauth2:https://www.googleapis.com/auth/chromesync";
45     public static final String LOGIN_OAUTH2_SCOPE =
46             "oauth2:https://www.google.com/accounts/OAuthLogin";
47     private static final String TAG = "SyncTestUtil";
48
49     public static final long UI_TIMEOUT_MS = scaleTimeout(20000);
50     public static final int CHECK_INTERVAL_MS = 250;
51
52     private static final long SYNC_WAIT_TIMEOUT_MS = scaleTimeout(30 * 1000);
53     private static final int SYNC_CHECK_INTERVAL_MS = 250;
54
55     public static final Pair<String, String> SYNC_SUMMARY_STATUS =
56             newPair("Summary", "Summary");
57     protected static final String UNINITIALIZED = "Uninitialized";
58     protected static final Pair<String, String> USERNAME_STAT =
59             newPair("Identity", "Username");
60
61     // Override the default server used for profile sync.
62     // Native switch - chrome_switches::kSyncServiceURL
63     private static final String SYNC_URL = "sync-url";
64
65     private SyncTestUtil() {
66     }
67
68     /**
69      * Creates a Pair of lowercased and trimmed Strings. Makes it easier to avoid running afoul of
70      * case-sensitive comparison since getAboutInfoStats(), et al, use Pair<String, String> as map
71      * keys.
72      */
73     private static Pair<String, String> newPair(String first, String second) {
74         return Pair.create(first.toLowerCase().trim(), second.toLowerCase().trim());
75     }
76
77     /**
78      * Parses raw JSON into a map with keys Pair<String, String>. The first string in each Pair
79      * corresponds to the title under which a given stat_name/stat_value is situated, and the second
80      * contains the name of the actual stat. For example, a stat named "Syncing" which falls under
81      * "Local State" would be a Pair of newPair("Local State", "Syncing").
82      *
83      * @param rawJson the JSON to parse into a map
84      * @return a map containing a mapping of titles and stat names to stat values
85      * @throws org.json.JSONException
86      */
87     public static Map<Pair<String, String>, String> getAboutInfoStats(String rawJson)
88             throws JSONException {
89
90         // What we get back is what you'd get from chrome.sync.aboutInfo at chrome://sync. This is
91         // a JSON object, and we care about the "details" field in that object. "details" itself has
92         // objects with two fields: data and title. The data field itself contains an array of
93         // objects. These objects contains two fields: stat_name and stat_value. Ultimately these
94         // are the values displayed on the page and the values we care about in this method.
95         Map<Pair<String, String>, String> statLookup = new HashMap<Pair<String, String>, String>();
96         JSONObject aboutInfo = new JSONObject(rawJson);
97         JSONArray detailsArray = aboutInfo.getJSONArray("details");
98         for (int i = 0; i < detailsArray.length(); i++) {
99             JSONObject dataObj = detailsArray.getJSONObject(i);
100             String dataTitle = dataObj.getString("title");
101             JSONArray dataArray = dataObj.getJSONArray("data");
102             for (int j = 0; j < dataArray.length(); j++) {
103                 JSONObject statObj = dataArray.getJSONObject(j);
104                 String statName = statObj.getString("stat_name");
105                 Pair<String, String> key = newPair(dataTitle, statName);
106                 statLookup.put(key, statObj.getString("stat_value"));
107             }
108         }
109
110         return statLookup;
111     }
112
113     /**
114      * Verifies that sync is signed out and its status is "Syncing not enabled".
115      * TODO(mmontgomery): check whether or not this method is necessary. It queries
116      * syncSummaryStatus(), which is a slightly more direct route than via JSON.
117      */
118     public static void verifySyncIsSignedOut(Context context) {
119         Map<Pair<String, String>, String> expectedStats =
120                 new HashMap<Pair<String, String>, String>();
121         expectedStats.put(SYNC_SUMMARY_STATUS, UNINITIALIZED);
122         expectedStats.put(USERNAME_STAT, ""); // Expect an empty username when sync is signed out.
123         Assert.assertTrue("Expected sync to be disabled.",
124                 pollAboutSyncStats(context, expectedStats));
125     }
126
127     /**
128      * Polls the stats on about:sync until timeout or all expected stats match actual stats. The
129      * comparison is case insensitive. *All* stats must match those passed in via expectedStats.
130      *
131      *
132      * @param expectedStats a map of stat names to their expected values
133      * @return whether the stats matched up before the timeout
134      */
135     public static boolean pollAboutSyncStats(
136             Context context, final Map<Pair<String, String>, String> expectedStats) {
137         final AboutSyncInfoGetter aboutInfoGetter =
138                 new AboutSyncInfoGetter(context);
139
140         Criteria statChecker = new Criteria() {
141             @Override
142             public boolean isSatisfied() {
143                 try {
144                     ThreadUtils.runOnUiThreadBlocking(aboutInfoGetter);
145                     Map<Pair<String, String>, String> actualStats = aboutInfoGetter.getAboutInfo();
146                     return areExpectedStatsAmongActual(expectedStats, actualStats);
147                 } catch (Throwable e) {
148                     Log.w(TAG, "Interrupted while attempting to fetch sync internals info.", e);
149                 }
150                 return false;
151             }
152         };
153
154         boolean matched = false;
155         try {
156             matched = CriteriaHelper.pollForCriteria(statChecker, UI_TIMEOUT_MS, CHECK_INTERVAL_MS);
157         } catch (InterruptedException e) {
158             Log.w(TAG, "Interrupted while polling sync internals info.", e);
159             Assert.fail("Interrupted while polling sync internals info.");
160         }
161         return matched;
162     }
163
164     /**
165      * Checks whether the expected map's keys and values are a subset of those in another map. Both
166      * keys and values are compared in a case-insensitive fashion.
167      *
168      * @param expectedStats a map which may be a subset of actualSet
169      * @param actualStats   a map which may be a superset of expectedSet
170      * @return true if all key/value pairs in expectedSet are in actualSet; false otherwise
171      */
172     private static boolean areExpectedStatsAmongActual(
173             Map<Pair<String, String>, String> expectedStats,
174             Map<Pair<String, String>, String> actualStats) {
175         for (Map.Entry<Pair<String, String>, String> statEntry : expectedStats.entrySet()) {
176             // Make stuff lowercase here, at the site of comparison.
177             String expectedValue = statEntry.getValue().toLowerCase().trim();
178             String actualValue = actualStats.get(statEntry.getKey());
179             if (actualValue == null) {
180                 return false;
181             }
182             actualValue = actualValue.toLowerCase().trim();
183             if (!expectedValue.contentEquals(actualValue)) {
184                 return false;
185             }
186         }
187         return true;
188     }
189
190     /**
191      * Triggers a sync and waits till it is complete.
192      */
193     public static void triggerSyncAndWaitForCompletion(final Context context)
194             throws InterruptedException {
195         final long oldSyncTime = getCurrentSyncTime(context);
196         // Request sync.
197         ThreadUtils.runOnUiThreadBlocking(new Runnable() {
198             @Override
199             public void run() {
200                 ProfileSyncService.get(context).requestSyncCycleForTest();
201             }
202         });
203
204         // Wait till lastSyncedTime > oldSyncTime.
205         Assert.assertTrue("Timed out waiting for syncing to complete.",
206                 CriteriaHelper.pollForCriteria(new Criteria() {
207                     @Override
208                     public boolean isSatisfied() {
209                         long currentSyncTime = 0;
210                         try {
211                             currentSyncTime = getCurrentSyncTime(context);
212                         } catch (InterruptedException e) {
213                             Log.w(TAG, "Interrupted while getting sync time.", e);
214                         }
215                         return currentSyncTime > oldSyncTime;
216                     }
217                 }, SYNC_WAIT_TIMEOUT_MS, SYNC_CHECK_INTERVAL_MS));
218     }
219
220     private static long getCurrentSyncTime(final Context context) throws InterruptedException {
221         final Semaphore s = new Semaphore(0);
222         final AtomicLong result = new AtomicLong();
223         ThreadUtils.runOnUiThreadBlocking(new Runnable() {
224             @Override
225             public void run() {
226                 result.set(ProfileSyncService.get(context).getLastSyncedTimeForTest());
227                 s.release();
228             }
229         });
230         Assert.assertTrue(s.tryAcquire(SYNC_WAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS));
231         return result.get();
232     }
233
234     /**
235      * Waits for a possible async initialization of the sync backend.
236      */
237     public static void ensureSyncInitialized(final Context context) throws InterruptedException {
238         Assert.assertTrue("Timed out waiting for syncing to be initialized.",
239                 CriteriaHelper.pollForCriteria(new Criteria() {
240                     @Override
241                     public boolean isSatisfied() {
242                         return ThreadUtils.runOnUiThreadBlockingNoException(
243                                 new Callable<Boolean>() {
244                                     @Override
245                                     public Boolean call() throws Exception {
246                                         return ProfileSyncService.get(context)
247                                                 .isSyncInitialized();
248
249                                     }
250                                 });
251                     }
252                 }, SYNC_WAIT_TIMEOUT_MS, SYNC_CHECK_INTERVAL_MS));
253     }
254
255     /**
256      * Verifies that the sync status is "READY" and sync is signed in with the account.
257      */
258     public static void verifySyncIsSignedIn(Context context, Account account)
259             throws InterruptedException {
260         ensureSyncInitialized(context);
261         triggerSyncAndWaitForCompletion(context);
262         verifySignedInWithAccount(context, account);
263     }
264
265     /**
266      * Makes sure that sync is enabled with the correct account.
267      */
268     public static void verifySignedInWithAccount(Context context, Account account) {
269         if (account == null) return;
270
271         Assert.assertEquals(
272                 account.name, ChromeSigninController.get(context).getSignedInAccountName());
273     }
274
275     /**
276      * Makes sure that the Python sync server was successfully started by checking for a well known
277      * response to a request for the server time. The command line argument for the sync server must
278      * be present in order for this check to be valid.
279      */
280     public static void verifySyncServerIsRunning() {
281         boolean hasSwitch = CommandLine.getInstance().hasSwitch(SYNC_URL);
282         Assert.assertTrue(SYNC_URL + " is a required parameter for the sync tests.", hasSwitch);
283         String syncTimeUrl = CommandLine.getInstance().getSwitchValue(SYNC_URL) + "/time";
284         TestHttpServerClient.checkServerIsUp(syncTimeUrl, "0123456789");
285     }
286
287     /**
288      * Sets up a test Google account on the device.
289      */
290     public static Account setupTestAccount(MockAccountManager accountManager, String accountName,
291                                            String password, String... allowedAuthTokenTypes) {
292         Account account = AccountManagerHelper.createAccountFromName(accountName);
293         AccountHolder.Builder accountHolder =
294                 AccountHolder.create().account(account).password(password);
295         if (allowedAuthTokenTypes != null) {
296             // Auto-allowing provided auth token types
297             for (String authTokenType : allowedAuthTokenTypes) {
298                 accountHolder.hasBeenAccepted(authTokenType, true);
299             }
300         }
301         accountManager.addAccountHolderExplicitly(accountHolder.build());
302         return account;
303     }
304
305     /**
306      * Returns whether the sync engine has keep everything synced set to true.
307      */
308     public static boolean isSyncEverythingEnabled(final Context context) {
309         final AtomicBoolean result = new AtomicBoolean();
310         ThreadUtils.runOnUiThreadBlocking(new Runnable() {
311             @Override
312             public void run() {
313                 result.set(ProfileSyncService.get(context).hasKeepEverythingSynced());
314             }
315         });
316         return result.get();
317     }
318
319     /**
320      * Verifies that the sync status is "Syncing not enabled" and that sync is signed in with the
321      * account.
322      */
323     public static void verifySyncIsDisabled(Context context, Account account) {
324         Map<Pair<String, String>, String> expectedStats =
325                 new HashMap<Pair<String, String>, String>();
326         expectedStats.put(SYNC_SUMMARY_STATUS, UNINITIALIZED);
327         Assert.assertTrue(
328                 "Expected sync to be disabled.", pollAboutSyncStats(context, expectedStats));
329         verifySignedInWithAccount(context, account);
330     }
331
332     public static class AboutSyncInfoGetter implements Runnable {
333         private static final String TAG = "AboutSyncInfoGetter";
334         final Context mContext;
335         Map<Pair<String, String>, String> mAboutInfo;
336
337         public AboutSyncInfoGetter(Context context) {
338             mContext = context.getApplicationContext();
339             mAboutInfo = new HashMap<Pair<String, String>, String>();
340         }
341
342         @Override
343         public void run() {
344             String info = ProfileSyncService.get(mContext).getSyncInternalsInfoForTest();
345             try {
346                 mAboutInfo = getAboutInfoStats(info);
347             } catch (JSONException e) {
348                 Log.w(TAG, "Unable to parse JSON message: " + info, e);
349             }
350         }
351
352         public Map<Pair<String, String>, String> getAboutInfo() {
353             return mAboutInfo;
354         }
355     }
356
357     /**
358      * Helper class used to create a mock account on the device.
359      */
360     public static class SyncTestContext extends AdvancedMockContext {
361
362         public SyncTestContext(Context context) {
363             super(context);
364         }
365
366         @Override
367         public Object getSystemService(String name) {
368             if (Context.ACCOUNT_SERVICE.equals(name)) {
369                 throw new UnsupportedOperationException(
370                         "Sync tests should not use system Account Manager.");
371             }
372             return super.getSystemService(name);
373         }
374     }
375 }