blob: 878777c10fd10aac595ee709989cd8c911981fd3 [file] [log] [blame]
package org.robolectric.shadows;
import static android.os.Build.VERSION_CODES.LOLLIPOP;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static com.google.common.truth.TruthJUnit.assume;
import static org.junit.Assert.fail;
import static org.robolectric.annotation.SQLiteMode.Mode.LEGACY;
import static org.robolectric.shadows.ShadowLegacySQLiteConnection.convertSQLWithLocalizedUnicodeCollator;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteDatatypeMismatchException;
import android.database.sqlite.SQLiteStatement;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.almworks.sqlite4java.SQLiteConnection;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.annotation.Config;
import org.robolectric.annotation.SQLiteMode;
import org.robolectric.shadows.util.SQLiteLibraryLoader;
import org.robolectric.util.ReflectionHelpers;
@RunWith(AndroidJUnit4.class)
@Config(minSdk = LOLLIPOP)
@SQLiteMode(LEGACY) // This test relies on legacy SQLite behavior in Robolectric.
public class ShadowSQLiteConnectionTest {
private SQLiteDatabase database;
private File databasePath;
private long ptr;
private SQLiteConnection conn;
private ShadowLegacySQLiteConnection.Connections connections;
@Before
public void setUp() throws Exception {
if (!SQLiteLibraryLoader.isOsSupported()) {
return;
}
database = createDatabase("database.db");
SQLiteStatement createStatement =
database.compileStatement(
"CREATE TABLE `routine` (`id` INTEGER PRIMARY KEY AUTOINCREMENT , `name` VARCHAR ,"
+ " `lastUsed` INTEGER DEFAULT 0 , UNIQUE (`name`)) ;");
createStatement.execute();
conn = getSQLiteConnection();
}
@After
public void tearDown() {
if (!SQLiteLibraryLoader.isOsSupported()) {
return;
}
database.close();
}
@Test
public void testSqlConversion() {
assume().that(SQLiteLibraryLoader.isOsSupported()).isTrue();
assertThat(convertSQLWithLocalizedUnicodeCollator("select * from `routine`"))
.isEqualTo("select * from `routine`");
assertThat(
convertSQLWithLocalizedUnicodeCollator(
"select * from `routine` order by name \n\r \f collate\f\n\tunicode"
+ "\n, id \n\n\t collate\n\t \n\flocalized"))
.isEqualTo(
"select * from `routine` order by name COLLATE NOCASE\n" + ", id COLLATE NOCASE");
assertThat(
convertSQLWithLocalizedUnicodeCollator(
"select * from `routine` order by name" + " collate localized"))
.isEqualTo("select * from `routine` order by name COLLATE NOCASE");
assertThat(
convertSQLWithLocalizedUnicodeCollator(
"select * from `routine` order by name" + " collate unicode"))
.isEqualTo("select * from `routine` order by name COLLATE NOCASE");
}
@Test
public void testSQLWithLocalizedOrUnicodeCollatorShouldBeSortedAsNoCase() {
assume().that(SQLiteLibraryLoader.isOsSupported()).isTrue();
database.execSQL("insert into routine(name) values ('الصحافة اليدوية')");
database.execSQL("insert into routine(name) values ('Hand press 1')");
database.execSQL("insert into routine(name) values ('hand press 2')");
database.execSQL("insert into routine(name) values ('Hand press 3')");
List<String> expected =
Arrays.asList(
"Hand press" + " 1", "hand press" + " 2", "Hand press" + " 3", "الصحافة" + " اليدوية");
String sqlLocalized = "SELECT `name` FROM `routine` ORDER BY `name` collate localized";
String sqlUnicode = "SELECT `name` FROM `routine` ORDER BY `name` collate unicode";
assertThat(simpleQueryForList(database, sqlLocalized)).isEqualTo(expected);
assertThat(simpleQueryForList(database, sqlUnicode)).isEqualTo(expected);
}
private List<String> simpleQueryForList(SQLiteDatabase db, String sql) {
Cursor cursor = db.rawQuery(sql, new String[0]);
List<String> result = new ArrayList<>();
while (cursor.moveToNext()) {
result.add(cursor.getString(0));
}
cursor.close();
return result;
}
@Test
public void nativeOpen_addsConnectionToPool() {
assume().that(SQLiteLibraryLoader.isOsSupported()).isTrue();
assertThat(conn).isNotNull();
assertWithMessage("open").that(conn.isOpen()).isTrue();
}
@Test
public void nativeClose_closesConnection() {
assume().that(SQLiteLibraryLoader.isOsSupported()).isTrue();
ShadowLegacySQLiteConnection.nativeClose(ptr);
assertWithMessage("open").that(conn.isOpen()).isFalse();
}
@Test
public void reset_closesConnection() {
assume().that(SQLiteLibraryLoader.isOsSupported()).isTrue();
ShadowLegacySQLiteConnection.reset();
assertWithMessage("open").that(conn.isOpen()).isFalse();
}
@Test
public void reset_clearsConnectionCache() {
assume().that(SQLiteLibraryLoader.isOsSupported()).isTrue();
final Map<Long, SQLiteConnection> connectionsMap =
ReflectionHelpers.getField(connections, "connectionsMap");
assertWithMessage("connections before").that(connectionsMap).isNotEmpty();
ShadowLegacySQLiteConnection.reset();
assertWithMessage("connections after").that(connectionsMap).isEmpty();
}
@Test
public void reset_clearsStatementCache() {
assume().that(SQLiteLibraryLoader.isOsSupported()).isTrue();
final Map<Long, SQLiteStatement> statementsMap =
ReflectionHelpers.getField(connections, "statementsMap");
assertWithMessage("statements before").that(statementsMap).isNotEmpty();
ShadowLegacySQLiteConnection.reset();
assertWithMessage("statements after").that(statementsMap).isEmpty();
}
@Test
public void error_resultsInSpecificExceptionWithCause() {
assume().that(SQLiteLibraryLoader.isOsSupported()).isTrue();
try {
database.execSQL("insert into routine(name) values ('Hand press 1')");
ContentValues values = new ContentValues(1);
values.put("rowid", "foo");
database.update("routine", values, "name='Hand press 1'", null);
fail();
} catch (SQLiteDatatypeMismatchException expected) {
assertThat(expected)
.hasCauseThat()
.hasCauseThat()
.isInstanceOf(com.almworks.sqlite4java.SQLiteException.class);
}
}
@Test
public void interruption_doesNotConcurrentlyModifyDatabase() {
assume().that(SQLiteLibraryLoader.isOsSupported()).isTrue();
Thread.currentThread().interrupt();
try {
database.execSQL("insert into routine(name) values ('الصحافة اليدوية')");
} finally {
Thread.interrupted();
}
ShadowLegacySQLiteConnection.reset();
}
@Test
public void test_setUseInMemoryDatabase() {
assume().that(SQLiteLibraryLoader.isOsSupported()).isTrue();
assertThat(conn.isMemoryDatabase()).isFalse();
ShadowSQLiteConnection.setUseInMemoryDatabase(true);
SQLiteDatabase inMemoryDb = createDatabase("in_memory.db");
SQLiteConnection inMemoryConn = getSQLiteConnection();
assertThat(inMemoryConn.isMemoryDatabase()).isTrue();
inMemoryDb.close();
}
@Test
public void cancel_shouldCancelAllStatements() {
assume().that(SQLiteLibraryLoader.isOsSupported()).isTrue();
SQLiteStatement statement1 =
database.compileStatement("insert into routine(name) values ('Hand press 1')");
SQLiteStatement statement2 =
database.compileStatement("insert into routine(name) values ('Hand press 2')");
ShadowLegacySQLiteConnection.nativeCancel(ptr);
// An attempt to execute a statement after a cancellation should be a no-op, unless the
// statement hasn't been cancelled, in which case it will throw a SQLiteInterruptedException.
statement1.execute();
statement2.execute();
}
private SQLiteDatabase createDatabase(String filename) {
databasePath = ApplicationProvider.getApplicationContext().getDatabasePath(filename);
databasePath.getParentFile().mkdirs();
return SQLiteDatabase.openOrCreateDatabase(databasePath.getPath(), null);
}
private SQLiteConnection getSQLiteConnection() {
ptr =
ShadowLegacySQLiteConnection.nativeOpen(
databasePath.getPath(), 0, "test connection", false, false)
.longValue();
connections =
ReflectionHelpers.getStaticField(ShadowLegacySQLiteConnection.class, "CONNECTIONS");
return connections.getConnection(ptr);
}
}