Add foundation for automated performance tests.
This commit is contained in:
parent
d8cc3c86b4
commit
f92891895e
9 changed files with 226 additions and 2 deletions
|
@ -202,6 +202,12 @@ android {
|
|||
debuggable false
|
||||
matchingFallbacks = ['debug']
|
||||
}
|
||||
mock {
|
||||
initWith debug
|
||||
isDefault false
|
||||
minifyEnabled false
|
||||
matchingFallbacks = ['debug']
|
||||
}
|
||||
}
|
||||
|
||||
productFlavors {
|
||||
|
@ -227,6 +233,15 @@ android {
|
|||
buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl"
|
||||
}
|
||||
|
||||
study {
|
||||
dimension 'distribution'
|
||||
|
||||
applicationIdSuffix ".study"
|
||||
ext.websiteUpdateUrl = "null"
|
||||
buildConfigField "boolean", "PLAY_STORE_DISABLED", "false"
|
||||
buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl"
|
||||
}
|
||||
|
||||
prod {
|
||||
dimension 'environment'
|
||||
|
||||
|
@ -266,6 +281,18 @@ android {
|
|||
}
|
||||
}
|
||||
|
||||
android.variantFilter { variant ->
|
||||
def distribution = variant.getFlavors().get(0).name
|
||||
def environment = variant.getFlavors().get(1).name
|
||||
def buildType = variant.buildType.name
|
||||
|
||||
if (distribution == 'study' && buildType != 'perf' && buildType != 'mock') {
|
||||
variant.setIgnore(true)
|
||||
} else if (distribution != 'study' && buildType == 'mock') {
|
||||
variant.setIgnore(true)
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
abortOnError true
|
||||
baseline file("lint-baseline.xml")
|
||||
|
|
|
@ -6,6 +6,7 @@ import android.content.Context;
|
|||
import androidx.annotation.AnyThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
|
@ -126,6 +127,15 @@ public final class KeyValueStore implements KeyValueReader {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Forces the store to re-fetch all of it's data from the database.
|
||||
* Should only be used for testing!
|
||||
*/
|
||||
@VisibleForTesting
|
||||
synchronized void resetCache() {
|
||||
dataSet = null;
|
||||
initializeIfNecessary();
|
||||
}
|
||||
|
||||
private synchronized void write(@NonNull KeyValueDataSet newDataSet, @NonNull Collection<String> removes) {
|
||||
initializeIfNecessary();
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
package org.thoughtcrime.securesms.keyvalue;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.preference.PreferenceDataStore;
|
||||
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.Wallpaper;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.util.SignalUncaughtExceptionHandler;
|
||||
|
||||
|
@ -70,6 +70,15 @@ public final class SignalStore {
|
|||
proxy().onFirstEverAppLaunch();
|
||||
}
|
||||
|
||||
/**
|
||||
* Forces the store to re-fetch all of it's data from the database.
|
||||
* Should only be used for testing!
|
||||
*/
|
||||
@VisibleForTesting
|
||||
public static void resetCache() {
|
||||
INSTANCE.store.resetCache();
|
||||
}
|
||||
|
||||
public static @NonNull KbsValues kbsValues() {
|
||||
return INSTANCE.kbsValues;
|
||||
}
|
||||
|
|
|
@ -5,6 +5,8 @@ import android.database.Cursor;
|
|||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
|
@ -13,13 +15,14 @@ import org.whispersystems.libsignal.util.guava.Preconditions;
|
|||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public final class SqlUtil {
|
||||
private SqlUtil() {}
|
||||
|
||||
private SqlUtil() {}
|
||||
|
||||
public static boolean tableExists(@NonNull SQLiteDatabase db, @NonNull String table) {
|
||||
try (Cursor cursor = db.rawQuery("SELECT name FROM sqlite_master WHERE type=? AND name=?", new String[] { "table", table })) {
|
||||
|
@ -27,6 +30,28 @@ public final class SqlUtil {
|
|||
}
|
||||
}
|
||||
|
||||
public static @NonNull List<String> getAllTables(@NonNull SQLiteDatabase db) {
|
||||
List<String> tables = new LinkedList<>();
|
||||
|
||||
try (Cursor cursor = db.rawQuery("SELECT name FROM sqlite_master WHERE type=?", new String[] { "table" })) {
|
||||
while (cursor.moveToNext()) {
|
||||
tables.add(cursor.getString(0));
|
||||
}
|
||||
}
|
||||
|
||||
return tables;
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits a multi-statement SQL block into independent statements. It is assumed that there is
|
||||
* only one statement per line, and that each statement is terminated by a semi-colon.
|
||||
*/
|
||||
public static @NonNull List<String> splitStatements(@NonNull String sql) {
|
||||
return Stream.of(Arrays.asList(sql.split(";\n")))
|
||||
.map(String::trim)
|
||||
.toList();
|
||||
}
|
||||
|
||||
public static boolean isEmpty(@NonNull SQLiteDatabase db, @NonNull String table) {
|
||||
try (Cursor cursor = db.rawQuery("SELECT COUNT(*) FROM " + table, null)) {
|
||||
if (cursor.moveToFirst()) {
|
||||
|
|
10
app/src/mock/AndroidManifest.xml
Normal file
10
app/src/mock/AndroidManifest.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="org.thoughtcrime.securesms">
|
||||
|
||||
<application
|
||||
android:name=".MockApplicationContext"
|
||||
tools:replace="android:name" />
|
||||
|
||||
</manifest>
|
|
@ -0,0 +1,101 @@
|
|||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
|
||||
import org.signal.core.util.StreamUtil;
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.KeyValueDatabase;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.SetUtil;
|
||||
import org.thoughtcrime.securesms.util.SqlUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Helper to initialize app state with the right database contents, shared prefs, etc.
|
||||
*/
|
||||
final class MockAppDataInitializer {
|
||||
|
||||
private static final Set<String> IGNORED_TABLES = SetUtil.newHashSet(
|
||||
"sqlite_sequence",
|
||||
"sms_fts",
|
||||
"sms_fts_data",
|
||||
"sms_fts_idx",
|
||||
"sms_fts_docsize",
|
||||
"sms_fts_config",
|
||||
"mms_fts",
|
||||
"mms_fts_data",
|
||||
"mms_fts_idx",
|
||||
"mms_fts_docsize",
|
||||
"mms_fts_config"
|
||||
);
|
||||
|
||||
public static void initialize(@NonNull Application application, @NonNull File sqlDirectory) throws IOException {
|
||||
String localE164 = StreamUtil.readFullyAsString(new FileInputStream(new File(sqlDirectory, "e164.txt"))).trim();
|
||||
String mainSql = StreamUtil.readFullyAsString(new FileInputStream(new File(sqlDirectory, "signal.sql")));
|
||||
String keyValueSql = StreamUtil.readFullyAsString(new FileInputStream(new File(sqlDirectory, "signal-key-value.sql")));
|
||||
|
||||
initializeDatabase(DatabaseFactory.getInstance(application).getRawDatabase(), mainSql);
|
||||
initializeDatabase(KeyValueDatabase.getInstance(application).getSqlCipherDatabase(), keyValueSql);
|
||||
|
||||
initializePreferences(application, localE164);
|
||||
|
||||
SignalStore.resetCache();
|
||||
}
|
||||
|
||||
private static void initializeDatabase(@NonNull SQLiteDatabase db, @NonNull String sql) {
|
||||
db.beginTransaction();
|
||||
try {
|
||||
clearAllTables(db);
|
||||
execStatements(db, sql);
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
private static void clearAllTables(@NonNull SQLiteDatabase db) {
|
||||
List<String> tables = SqlUtil.getAllTables(db);
|
||||
|
||||
for (String table : tables) {
|
||||
if (!IGNORED_TABLES.contains(table)) {
|
||||
db.execSQL("DELETE FROM " + table);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void execStatements(@NonNull SQLiteDatabase db, @NonNull String sql) {
|
||||
List<String> statements = SqlUtil.splitStatements(sql);
|
||||
|
||||
for (String statement : statements) {
|
||||
db.execSQL(statement);
|
||||
}
|
||||
}
|
||||
|
||||
private static void initializePreferences(@NonNull Context context, @NonNull String localE164) {
|
||||
MasterSecret masterSecret = MasterSecretUtil.generateMasterSecret(context, MasterSecretUtil.UNENCRYPTED_PASSPHRASE);
|
||||
|
||||
MasterSecretUtil.generateAsymmetricMasterSecret(context, masterSecret);
|
||||
IdentityKeyUtil.generateIdentityKeys(context);
|
||||
|
||||
TextSecurePreferences.setPromptedPushRegistration(context, true);
|
||||
TextSecurePreferences.setLocalNumber(context, localE164);
|
||||
TextSecurePreferences.setLocalUuid(context, Recipient.external(context, localE164).requireUuid());
|
||||
TextSecurePreferences.setPushRegistered(context, true);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package org.thoughtcrime.securesms;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
public class MockApplicationContext extends ApplicationContext {
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
try {
|
||||
MockAppDataInitializer.initialize(this, new File(getExternalFilesDir(null), "mock-data"));
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException("Failed to initialize mock data!", e);
|
||||
}
|
||||
}
|
||||
}
|
5
app/src/study/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
5
app/src/study/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/core_green"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
|
@ -10,6 +10,7 @@ import org.robolectric.annotation.Config;
|
|||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
@ -114,4 +115,22 @@ public final class SqlUtilTest {
|
|||
public void buildCollectionQuery_none() {
|
||||
SqlUtil.buildCollectionQuery("a", Collections.emptyList());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void splitStatements_singleStatement() {
|
||||
List<String> result = SqlUtil.splitStatements("SELECT * FROM foo;\n");
|
||||
assertEquals(Arrays.asList("SELECT * FROM foo"), result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void splitStatements_twoStatements() {
|
||||
List<String> result = SqlUtil.splitStatements("SELECT * FROM foo;\nSELECT * FROM bar;\n");
|
||||
assertEquals(Arrays.asList("SELECT * FROM foo", "SELECT * FROM bar"), result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void splitStatements_twoStatementsSeparatedByNewLines() {
|
||||
List<String> result = SqlUtil.splitStatements("SELECT * FROM foo;\n\nSELECT * FROM bar;\n");
|
||||
assertEquals(Arrays.asList("SELECT * FROM foo", "SELECT * FROM bar"), result);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue