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.
5 package org.chromium.chrome.test.util.browser.sync;
7 import static org.chromium.base.test.util.ScalableTimeout.scaleTimeout;
9 import android.accounts.Account;
10 import android.content.Context;
11 import android.util.Log;
12 import android.util.Pair;
14 import junit.framework.Assert;
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;
33 import java.util.HashMap;
34 import java.util.Locale;
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;
43 * Utility class for shared sync test functionality.
45 public final class SyncTestUtil {
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";
51 public static final long UI_TIMEOUT_MS = scaleTimeout(20000);
52 public static final int CHECK_INTERVAL_MS = 250;
54 private static final long SYNC_WAIT_TIMEOUT_MS = scaleTimeout(30 * 1000);
55 private static final int SYNC_CHECK_INTERVAL_MS = 250;
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");
63 // Override the default server used for profile sync.
64 // Native switch - chrome_switches::kSyncServiceURL
65 private static final String SYNC_URL = "sync-url";
67 private SyncTestUtil() {
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
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());
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").
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
90 public static Map<Pair<String, String>, String> getAboutInfoStats(String rawJson)
91 throws JSONException {
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"));
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.
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));
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.
135 * @param expectedStats a map of stat names to their expected values
136 * @return whether the stats matched up before the timeout
138 public static boolean pollAboutSyncStats(
139 Context context, final Map<Pair<String, String>, String> expectedStats) {
140 final AboutSyncInfoGetter aboutInfoGetter =
141 new AboutSyncInfoGetter(context);
143 Criteria statChecker = new Criteria() {
145 public boolean isSatisfied() {
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);
157 boolean matched = false;
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.");
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.
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
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) {
185 actualValue = actualValue.toLowerCase(Locale.US).trim();
186 if (!expectedValue.contentEquals(actualValue)) {
194 * Triggers a sync and waits till it is complete.
196 public static void triggerSyncAndWaitForCompletion(final Context context)
197 throws InterruptedException {
198 final long oldSyncTime = getCurrentSyncTime(context);
200 ThreadUtils.runOnUiThreadBlocking(new Runnable() {
203 InvalidationServiceFactory.getForProfile(Profile.getLastUsedProfile())
204 .requestSyncFromNativeChromeForAllTypes();
208 // Wait till lastSyncedTime > oldSyncTime.
209 Assert.assertTrue("Timed out waiting for syncing to complete.",
210 CriteriaHelper.pollForCriteria(new Criteria() {
212 public boolean isSatisfied() {
213 long currentSyncTime = 0;
215 currentSyncTime = getCurrentSyncTime(context);
216 } catch (InterruptedException e) {
217 Log.w(TAG, "Interrupted while getting sync time.", e);
219 return currentSyncTime > oldSyncTime;
221 }, SYNC_WAIT_TIMEOUT_MS, SYNC_CHECK_INTERVAL_MS));
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() {
230 result.set(ProfileSyncService.get(context).getLastSyncedTimeForTest());
234 Assert.assertTrue(s.tryAcquire(SYNC_WAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS));
239 * Waits for a possible async initialization of the sync backend.
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() {
245 public boolean isSatisfied() {
246 return ThreadUtils.runOnUiThreadBlockingNoException(
247 new Callable<Boolean>() {
249 public Boolean call() throws Exception {
250 return ProfileSyncService.get(context)
251 .isSyncInitialized();
256 }, SYNC_WAIT_TIMEOUT_MS, SYNC_CHECK_INTERVAL_MS));
260 * Verifies that the sync status is "READY" and sync is signed in with the account.
262 public static void verifySyncIsSignedIn(Context context, Account account)
263 throws InterruptedException {
264 ensureSyncInitialized(context);
265 triggerSyncAndWaitForCompletion(context);
266 verifySignedInWithAccount(context, account);
270 * Makes sure that sync is enabled with the correct account.
272 public static void verifySignedInWithAccount(Context context, Account account) {
273 if (account == null) return;
276 account.name, ChromeSigninController.get(context).getSignedInAccountName());
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.
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");
292 * Sets up a test Google account on the device with specified auth token types.
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);
305 accountManager.addAccountHolderExplicitly(accountHolder.build());
310 * Sets up a test Google account on the device, that accepts all auth tokens.
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());
323 * Returns whether the sync engine has keep everything synced set to true.
325 public static boolean isSyncEverythingEnabled(final Context context) {
326 final AtomicBoolean result = new AtomicBoolean();
327 ThreadUtils.runOnUiThreadBlocking(new Runnable() {
330 result.set(ProfileSyncService.get(context).hasKeepEverythingSynced());
337 * Verifies that the sync status is "Syncing not enabled" and that sync is signed in with the
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);
345 "Expected sync to be disabled.", pollAboutSyncStats(context, expectedStats));
346 verifySignedInWithAccount(context, account);
350 * Retrieves the sync internals information which is the basis for chrome://sync-internals and
351 * makes the result available in {@link AboutSyncInfoGetter#getAboutInfo()}.
353 * This class has to be run on the main thread, as it accesses the ProfileSyncService.
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;
360 public AboutSyncInfoGetter(Context context) {
361 mContext = context.getApplicationContext();
362 mAboutInfo = new HashMap<Pair<String, String>, String>();
367 String info = ProfileSyncService.get(mContext).getSyncInternalsInfoForTest();
369 mAboutInfo = getAboutInfoStats(info);
370 } catch (JSONException e) {
371 Log.w(TAG, "Unable to parse JSON message: " + info, e);
375 public Map<Pair<String, String>, String> getAboutInfo() {
381 * Helper class used to create a mock account on the device.
383 public static class SyncTestContext extends AdvancedMockContext {
385 public SyncTestContext(Context context) {
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.");
395 return super.getSystemService(name);