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