blob: 7c2d35e4bbdce9dde165584ca1a1ff424f6ca243 [file] [log] [blame]
// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.test.util.browser.sync;
import static org.chromium.base.test.util.ScalableTimeout.scaleTimeout;
import android.accounts.Account;
import android.content.Context;
import android.util.Log;
import android.util.Pair;
import junit.framework.Assert;
import org.chromium.base.CommandLine;
import org.chromium.base.ThreadUtils;
import org.chromium.base.test.util.AdvancedMockContext;
import org.chromium.chrome.browser.invalidation.InvalidationServiceFactory;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.sync.ProfileSyncService;
import org.chromium.chrome.test.util.TestHttpServerClient;
import org.chromium.content.browser.test.util.Criteria;
import org.chromium.content.browser.test.util.CriteriaHelper;
import org.chromium.sync.signin.AccountManagerHelper;
import org.chromium.sync.signin.ChromeSigninController;
import org.chromium.sync.test.util.AccountHolder;
import org.chromium.sync.test.util.MockAccountManager;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
/**
* Utility class for shared sync test functionality.
*/
public final class SyncTestUtil {
public static final String DEFAULT_TEST_ACCOUNT = "test@gmail.com";
public static final String DEFAULT_PASSWORD = "myPassword";
private static final String TAG = "SyncTestUtil";
public static final long UI_TIMEOUT_MS = scaleTimeout(20000);
public static final int CHECK_INTERVAL_MS = 250;
private static final long SYNC_WAIT_TIMEOUT_MS = scaleTimeout(30 * 1000);
private static final int SYNC_CHECK_INTERVAL_MS = 250;
public static final Pair<String, String> SYNC_SUMMARY_STATUS =
newPair("Summary", "Summary");
protected static final String UNINITIALIZED = "Uninitialized";
protected static final Pair<String, String> USERNAME_STAT =
newPair("Identity", "Username");
// Override the default server used for profile sync.
// Native switch - chrome_switches::kSyncServiceURL
private static final String SYNC_URL = "sync-url";
private SyncTestUtil() {
}
/**
* Creates a Pair of lowercased and trimmed Strings. Makes it easier to avoid running afoul of
* case-sensitive comparison since getAboutInfoStats(), et al, use Pair<String, String> as map
* keys.
*/
private static Pair<String, String> newPair(String first, String second) {
return Pair.create(first.toLowerCase(Locale.US).trim(),
second.toLowerCase(Locale.US).trim());
}
/**
* Parses raw JSON into a map with keys Pair<String, String>. The first string in each Pair
* corresponds to the title under which a given stat_name/stat_value is situated, and the second
* contains the name of the actual stat. For example, a stat named "Syncing" which falls under
* "Local State" would be a Pair of newPair("Local State", "Syncing").
*
* @param rawJson the JSON to parse into a map
* @return a map containing a mapping of titles and stat names to stat values
* @throws org.json.JSONException
*/
public static Map<Pair<String, String>, String> getAboutInfoStats(String rawJson)
throws JSONException {
// What we get back is what you'd get from chrome.sync.aboutInfo at chrome://sync. This is
// a JSON object, and we care about the "details" field in that object. "details" itself has
// objects with two fields: data and title. The data field itself contains an array of
// objects. These objects contains two fields: stat_name and stat_value. Ultimately these
// are the values displayed on the page and the values we care about in this method.
Map<Pair<String, String>, String> statLookup = new HashMap<Pair<String, String>, String>();
JSONObject aboutInfo = new JSONObject(rawJson);
JSONArray detailsArray = aboutInfo.getJSONArray("details");
for (int i = 0; i < detailsArray.length(); i++) {
JSONObject dataObj = detailsArray.getJSONObject(i);
String dataTitle = dataObj.getString("title");
JSONArray dataArray = dataObj.getJSONArray("data");
for (int j = 0; j < dataArray.length(); j++) {
JSONObject statObj = dataArray.getJSONObject(j);
String statName = statObj.getString("stat_name");
Pair<String, String> key = newPair(dataTitle, statName);
statLookup.put(key, statObj.getString("stat_value"));
}
}
return statLookup;
}
/**
* Verifies that sync is signed out and its status is "Syncing not enabled".
* TODO(mmontgomery): check whether or not this method is necessary. It queries
* syncSummaryStatus(), which is a slightly more direct route than via JSON.
*/
public static void verifySyncIsSignedOut(Context context) {
Map<Pair<String, String>, String> expectedStats =
new HashMap<Pair<String, String>, String>();
expectedStats.put(SYNC_SUMMARY_STATUS, UNINITIALIZED);
expectedStats.put(USERNAME_STAT, ""); // Expect an empty username when sync is signed out.
Assert.assertTrue("Expected sync to be disabled.",
pollAboutSyncStats(context, expectedStats));
}
/**
* Polls the stats on about:sync until timeout or all expected stats match actual stats. The
* comparison is case insensitive. *All* stats must match those passed in via expectedStats.
*
*
* @param expectedStats a map of stat names to their expected values
* @return whether the stats matched up before the timeout
*/
public static boolean pollAboutSyncStats(
Context context, final Map<Pair<String, String>, String> expectedStats) {
final AboutSyncInfoGetter aboutInfoGetter =
new AboutSyncInfoGetter(context);
Criteria statChecker = new Criteria() {
@Override
public boolean isSatisfied() {
try {
ThreadUtils.runOnUiThreadBlocking(aboutInfoGetter);
Map<Pair<String, String>, String> actualStats = aboutInfoGetter.getAboutInfo();
return areExpectedStatsAmongActual(expectedStats, actualStats);
} catch (Throwable e) {
Log.w(TAG, "Interrupted while attempting to fetch sync internals info.", e);
}
return false;
}
};
boolean matched = false;
try {
matched = CriteriaHelper.pollForCriteria(statChecker, UI_TIMEOUT_MS, CHECK_INTERVAL_MS);
} catch (InterruptedException e) {
Log.w(TAG, "Interrupted while polling sync internals info.", e);
Assert.fail("Interrupted while polling sync internals info.");
}
return matched;
}
/**
* Checks whether the expected map's keys and values are a subset of those in another map. Both
* keys and values are compared in a case-insensitive fashion.
*
* @param expectedStats a map which may be a subset of actualSet
* @param actualStats a map which may be a superset of expectedSet
* @return true if all key/value pairs in expectedSet are in actualSet; false otherwise
*/
private static boolean areExpectedStatsAmongActual(
Map<Pair<String, String>, String> expectedStats,
Map<Pair<String, String>, String> actualStats) {
for (Map.Entry<Pair<String, String>, String> statEntry : expectedStats.entrySet()) {
// Make stuff lowercase here, at the site of comparison.
String expectedValue = statEntry.getValue().toLowerCase(Locale.US).trim();
String actualValue = actualStats.get(statEntry.getKey());
if (actualValue == null) {
return false;
}
actualValue = actualValue.toLowerCase(Locale.US).trim();
if (!expectedValue.contentEquals(actualValue)) {
return false;
}
}
return true;
}
/**
* Triggers a sync and waits till it is complete.
*/
public static void triggerSyncAndWaitForCompletion(final Context context)
throws InterruptedException {
final long oldSyncTime = getCurrentSyncTime(context);
// Request sync.
ThreadUtils.runOnUiThreadBlocking(new Runnable() {
@Override
public void run() {
InvalidationServiceFactory.getForProfile(Profile.getLastUsedProfile())
.requestSyncFromNativeChromeForAllTypes();
}
});
// Wait till lastSyncedTime > oldSyncTime.
Assert.assertTrue("Timed out waiting for syncing to complete.",
CriteriaHelper.pollForCriteria(new Criteria() {
@Override
public boolean isSatisfied() {
long currentSyncTime = 0;
try {
currentSyncTime = getCurrentSyncTime(context);
} catch (InterruptedException e) {
Log.w(TAG, "Interrupted while getting sync time.", e);
}
return currentSyncTime > oldSyncTime;
}
}, SYNC_WAIT_TIMEOUT_MS, SYNC_CHECK_INTERVAL_MS));
}
private static long getCurrentSyncTime(final Context context) throws InterruptedException {
final Semaphore s = new Semaphore(0);
final AtomicLong result = new AtomicLong();
ThreadUtils.runOnUiThreadBlocking(new Runnable() {
@Override
public void run() {
result.set(ProfileSyncService.get(context).getLastSyncedTimeForTest());
s.release();
}
});
Assert.assertTrue(s.tryAcquire(SYNC_WAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS));
return result.get();
}
/**
* Waits for a possible async initialization of the sync backend.
*/
public static void ensureSyncInitialized(final Context context) throws InterruptedException {
Assert.assertTrue("Timed out waiting for syncing to be initialized.",
CriteriaHelper.pollForCriteria(new Criteria() {
@Override
public boolean isSatisfied() {
return ThreadUtils.runOnUiThreadBlockingNoException(
new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
return ProfileSyncService.get(context)
.isSyncInitialized();
}
});
}
}, SYNC_WAIT_TIMEOUT_MS, SYNC_CHECK_INTERVAL_MS));
}
/**
* Verifies that the sync status is "READY" and sync is signed in with the account.
*/
public static void verifySyncIsSignedIn(Context context, Account account)
throws InterruptedException {
ensureSyncInitialized(context);
triggerSyncAndWaitForCompletion(context);
verifySignedInWithAccount(context, account);
}
/**
* Makes sure that sync is enabled with the correct account.
*/
public static void verifySignedInWithAccount(Context context, Account account) {
if (account == null) return;
Assert.assertEquals(
account.name, ChromeSigninController.get(context).getSignedInAccountName());
}
/**
* Makes sure that the Python sync server was successfully started by checking for a well known
* response to a request for the server time. The command line argument for the sync server must
* be present in order for this check to be valid.
*/
public static void verifySyncServerIsRunning() {
boolean hasSwitch = CommandLine.getInstance().hasSwitch(SYNC_URL);
Assert.assertTrue(SYNC_URL + " is a required parameter for the sync tests.", hasSwitch);
String syncTimeUrl = CommandLine.getInstance().getSwitchValue(SYNC_URL) + "/time";
TestHttpServerClient.checkServerIsUp(syncTimeUrl, "0123456789");
}
/**
* Sets up a test Google account on the device with specified auth token types.
*/
public static Account setupTestAccount(MockAccountManager accountManager, String accountName,
String password, String... allowedAuthTokenTypes) {
Account account = AccountManagerHelper.createAccountFromName(accountName);
AccountHolder.Builder accountHolder =
AccountHolder.create().account(account).password(password);
if (allowedAuthTokenTypes != null) {
// Auto-allowing provided auth token types
for (String authTokenType : allowedAuthTokenTypes) {
accountHolder.hasBeenAccepted(authTokenType, true);
}
}
accountManager.addAccountHolderExplicitly(accountHolder.build());
return account;
}
/**
* Sets up a test Google account on the device, that accepts all auth tokens.
*/
public static Account setupTestAccountThatAcceptsAllAuthTokens(
MockAccountManager accountManager,
String accountName, String password) {
Account account = AccountManagerHelper.createAccountFromName(accountName);
AccountHolder.Builder accountHolder =
AccountHolder.create().account(account).password(password).alwaysAccept(true);
accountManager.addAccountHolderExplicitly(accountHolder.build());
return account;
}
/**
* Returns whether the sync engine has keep everything synced set to true.
*/
public static boolean isSyncEverythingEnabled(final Context context) {
final AtomicBoolean result = new AtomicBoolean();
ThreadUtils.runOnUiThreadBlocking(new Runnable() {
@Override
public void run() {
result.set(ProfileSyncService.get(context).hasKeepEverythingSynced());
}
});
return result.get();
}
/**
* Verifies that the sync status is "Syncing not enabled" and that sync is signed in with the
* account.
*/
public static void verifySyncIsDisabled(Context context, Account account) {
Map<Pair<String, String>, String> expectedStats =
new HashMap<Pair<String, String>, String>();
expectedStats.put(SYNC_SUMMARY_STATUS, UNINITIALIZED);
Assert.assertTrue(
"Expected sync to be disabled.", pollAboutSyncStats(context, expectedStats));
verifySignedInWithAccount(context, account);
}
/**
* Retrieves the sync internals information which is the basis for chrome://sync-internals and
* makes the result available in {@link AboutSyncInfoGetter#getAboutInfo()}.
*
* This class has to be run on the main thread, as it accesses the ProfileSyncService.
*/
public static class AboutSyncInfoGetter implements Runnable {
private static final String TAG = "AboutSyncInfoGetter";
final Context mContext;
Map<Pair<String, String>, String> mAboutInfo;
public AboutSyncInfoGetter(Context context) {
mContext = context.getApplicationContext();
mAboutInfo = new HashMap<Pair<String, String>, String>();
}
@Override
public void run() {
String info = ProfileSyncService.get(mContext).getSyncInternalsInfoForTest();
try {
mAboutInfo = getAboutInfoStats(info);
} catch (JSONException e) {
Log.w(TAG, "Unable to parse JSON message: " + info, e);
}
}
public Map<Pair<String, String>, String> getAboutInfo() {
return mAboutInfo;
}
}
/**
* Helper class used to create a mock account on the device.
*/
public static class SyncTestContext extends AdvancedMockContext {
public SyncTestContext(Context context) {
super(context);
}
@Override
public Object getSystemService(String name) {
if (Context.ACCOUNT_SERVICE.equals(name)) {
throw new UnsupportedOperationException(
"Sync tests should not use system Account Manager.");
}
return super.getSystemService(name);
}
}
}