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