Skip to content

Commit

Permalink
Fix SQLiteDatabaseTest foreign key test to match Android behavior
Browse files Browse the repository at this point in the history
SQLiteDatabaseTest.shouldThrowWhenForeignKeysConstraintIsViolated failed if run
on a real Android. It expected that a foreign key exception be thrown. In real
Android, however, no exception was thrown. Only a message is printed in logcat.

Update the test to delete a row that has a reference to another table. This
causes an exception in both real Android and Robolectric. Move this to a
ctesque test to ensure fidelity with real Android.

PiperOrigin-RevId: 386770883
  • Loading branch information
hoisie authored and copybara-robolectric committed Jul 28, 2021
1 parent f4d64f8 commit 19467ec
Show file tree
Hide file tree
Showing 42 changed files with 1,018 additions and 1,476 deletions.
Expand Up @@ -4,6 +4,7 @@
package="org.robolectric.integrationtests.axt">

<uses-sdk
android:minSdkVersion="14"
android:targetSdkVersion="27"/>

<application>
Expand Down
Expand Up @@ -3,13 +3,13 @@
import static com.google.common.truth.Truth.assertThat;

import android.app.Activity;
import androidx.lifecycle.Lifecycle.State;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import androidx.fragment.app.Fragment;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.R;
import androidx.lifecycle.Lifecycle.State;
import androidx.test.core.app.ActivityScenario;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
Expand Down
@@ -0,0 +1,118 @@
package android.database;

import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;

import android.content.ContentValues;
import android.database.sqlite.SQLiteConstraintException;
import android.database.sqlite.SQLiteDatabase;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.base.Ascii;
import com.google.common.base.Throwables;
import java.io.File;
import java.util.Arrays;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.annotation.internal.DoNotInstrument;

/** Compatibility test for {@link android.database.sqlite.SQLiteDatabase} */
@DoNotInstrument
@RunWith(AndroidJUnit4.class)
public class SQLiteDatabaseTest {

private SQLiteDatabase database;
private File databasePath;

@Before
public void setUp() throws Exception {
databasePath = ApplicationProvider.getApplicationContext().getDatabasePath("database.db");
databasePath.getParentFile().mkdirs();

database = SQLiteDatabase.openOrCreateDatabase(databasePath, null);
database.execSQL(
"CREATE TABLE table_name (\n"
+ " id INTEGER PRIMARY KEY AUTOINCREMENT,\n"
+ " first_column VARCHAR(255),\n"
+ " second_column BINARY,\n"
+ " name VARCHAR(255),\n"
+ " big_int INTEGER\n"
+ ");");
}

@After
public void tearDown() throws Exception {
database.close();
assertThat(databasePath.delete()).isTrue();
}

@Test
public void shouldGetBlobFromString() {
String s = "this is a string";
ContentValues values = new ContentValues();
values.put("first_column", s);

database.insert("table_name", null, values);

Cursor data =
database.query("table_name", new String[] {"first_column"}, null, null, null, null, null);
assertThat(data.getCount()).isEqualTo(1);
data.moveToFirst();
byte[] columnBytes = data.getBlob(0);
byte[] expected = Arrays.copyOf(s.getBytes(), s.length() + 1); // include zero-terminal
assertThat(columnBytes).isEqualTo(expected);
}

@Test
public void shouldGetBlobFromNullString() {
ContentValues values = new ContentValues();
values.put("first_column", (String) null);
database.insert("table_name", null, values);

Cursor data =
database.query("table_name", new String[] {"first_column"}, null, null, null, null, null);
assertThat(data.getCount()).isEqualTo(1);
data.moveToFirst();
assertThat(data.getBlob(0)).isEqualTo(null);
}

@Test
public void shouldGetBlobFromEmptyString() {
ContentValues values = new ContentValues();
values.put("first_column", "");
database.insert("table_name", null, values);

Cursor data =
database.query("table_name", new String[] {"first_column"}, null, null, null, null, null);
assertThat(data.getCount()).isEqualTo(1);
data.moveToFirst();
assertThat(data.getBlob(0)).isEqualTo(new byte[] {0});
}

@Test
public void shouldThrowWhenForeignKeysConstraintIsViolated() {
database.execSQL(
"CREATE TABLE artist(\n"
+ " artistid INTEGER PRIMARY KEY, \n"
+ " artistname TEXT\n"
+ ");\n");

database.execSQL(
"CREATE TABLE track(\n"
+ " trackid INTEGER, \n"
+ " trackname TEXT, \n"
+ " trackartist INTEGER,\n"
+ " FOREIGN KEY(trackartist) REFERENCES artist(artistid)\n"
+ ");");

database.execSQL("PRAGMA foreign_keys=ON");
database.execSQL("INSERT into artist (artistid, artistname) VALUES (1, 'Kanye')");
database.execSQL(
"INSERT into track (trackid, trackname, trackartist) VALUES (1, 'Good Life', 1)");
SQLiteConstraintException ex =
assertThrows(SQLiteConstraintException.class, () -> database.execSQL("delete from artist"));
assertThat(Ascii.toLowerCase(Throwables.getStackTraceAsString(ex))).contains("foreign key");
}
}
1 change: 1 addition & 0 deletions integration_tests/multidex/src/test/AndroidManifest.xml
Expand Up @@ -4,6 +4,7 @@
package="org.robolectric.integrationtests.multidex">

<uses-sdk
android:minSdkVersion="14"
android:targetSdkVersion="27"/>

<application />
Expand Down
35 changes: 14 additions & 21 deletions junit/src/main/java/org/robolectric/internal/SandboxTestRunner.java
Expand Up @@ -27,9 +27,6 @@
import org.robolectric.internal.bytecode.InstrumentationConfiguration;
import org.robolectric.internal.bytecode.Interceptor;
import org.robolectric.internal.bytecode.Interceptors;
import org.robolectric.internal.bytecode.InvokeDynamic;
import org.robolectric.internal.bytecode.InvokeDynamicClassInstrumentor;
import org.robolectric.internal.bytecode.OldClassInstrumentor;
import org.robolectric.internal.bytecode.Sandbox;
import org.robolectric.internal.bytecode.SandboxConfig;
import org.robolectric.internal.bytecode.ShadowInfo;
Expand All @@ -51,11 +48,7 @@ public class SandboxTestRunner extends BlockJUnit4ClassRunner {
private static final Injector DEFAULT_INJECTOR = defaultInjector().build();

protected static Injector.Builder defaultInjector() {
return new Injector.Builder()
.bindDefault(ClassInstrumentor.class,
InvokeDynamic.ENABLED
? InvokeDynamicClassInstrumentor.class
: OldClassInstrumentor.class);
return new Injector.Builder();
}

private final ClassInstrumentor classInstrumentor;
Expand Down Expand Up @@ -142,8 +135,7 @@ private static void invokeAfterClass(final Class<?> clazz) throws Throwable {
}
}

protected void afterClass() {
}
protected void afterClass() {}

@Nonnull
protected Sandbox getSandbox(FrameworkMethod method) {
Expand All @@ -152,9 +144,11 @@ protected Sandbox getSandbox(FrameworkMethod method) {
}

/**
* Create an {@link InstrumentationConfiguration} suitable for the provided {@link FrameworkMethod}.
* Create an {@link InstrumentationConfiguration} suitable for the provided {@link
* FrameworkMethod}.
*
* Custom TestRunner subclasses may wish to override this method to provide alternate configuration.
* <p>Custom TestRunner subclasses may wish to override this method to provide alternate
* configuration.
*
* @param method the test method that's about to run
* @return an {@link InstrumentationConfiguration}
Expand Down Expand Up @@ -195,7 +189,8 @@ protected InstrumentationConfiguration createClassLoaderConfig(FrameworkMethod m
return builder.build();
}

private void addInstrumentedPackages(FrameworkMethod method, InstrumentationConfiguration.Builder builder) {
private void addInstrumentedPackages(
FrameworkMethod method, InstrumentationConfiguration.Builder builder) {
SandboxConfig classConfig = getTestClass().getJavaClass().getAnnotation(SandboxConfig.class);
if (classConfig != null) {
for (String pkgName : classConfig.instrumentedPackages()) {
Expand Down Expand Up @@ -227,7 +222,8 @@ protected void configureSandbox(Sandbox sandbox, FrameworkMethod method) {
sandbox.configure(createClassHandler(shadowMap, sandbox), getInterceptors());
}

@Override protected Statement methodBlock(final FrameworkMethod method) {
@Override
protected Statement methodBlock(final FrameworkMethod method) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
Expand Down Expand Up @@ -314,14 +310,12 @@ private void reportPerfStats(PerfStatsCollector perfStatsCollector) {
}
}

protected void beforeTest(Sandbox sandbox, FrameworkMethod method, Method bootstrappedMethod) throws Throwable {
}
protected void beforeTest(Sandbox sandbox, FrameworkMethod method, Method bootstrappedMethod)
throws Throwable {}

protected void afterTest(FrameworkMethod method, Method bootstrappedMethod) {
}
protected void afterTest(FrameworkMethod method, Method bootstrappedMethod) {}

protected void finallyAfterTest(FrameworkMethod method) {
}
protected void finallyAfterTest(FrameworkMethod method) {}

protected HelperTestRunner getHelperTestRunner(Class bootstrappedTestClass) {
try {
Expand Down Expand Up @@ -380,7 +374,6 @@ private long getTimeout(Test annotation) {
}
return annotation.timeout();
}

}

@Nonnull
Expand Down
Expand Up @@ -21,7 +21,6 @@
import org.robolectric.internal.bytecode.ClassNodeProvider;
import org.robolectric.internal.bytecode.InstrumentationConfiguration;
import org.robolectric.internal.bytecode.Interceptors;
import org.robolectric.internal.bytecode.InvokeDynamicClassInstrumentor;
import org.robolectric.util.inject.Injector;

/** Runs Robolectric invokedynamic instrumentation on an android-all jar. */
Expand All @@ -30,10 +29,7 @@ public class JarInstrumentor {
private static final int ONE_MB = 1024 * 1024;
private static final boolean PRINT_FAILED_CLASSES = false;

private static final Injector INJECTOR =
new Injector.Builder()
.bindDefault(ClassInstrumentor.class, InvokeDynamicClassInstrumentor.class)
.build();
private static final Injector INJECTOR = new Injector.Builder().build();

private final ClassInstrumentor classInstrumentor;
private final InstrumentationConfiguration instrumentationConfiguration;
Expand Down
1 change: 1 addition & 0 deletions processor/build.gradle
Expand Up @@ -40,6 +40,7 @@ dependencies {
api "org.ow2.asm:asm-commons:9.0"
api "com.google.guava:guava:27.0.1-jre"
api "com.google.code.gson:gson:2.8.6"
implementation 'com.google.auto:auto-common:1.1.2'

def toolsJar = Jvm.current().getToolsJar()
if (toolsJar != null) {
Expand Down
Expand Up @@ -2,6 +2,7 @@

import static org.robolectric.annotation.processing.validator.ImplementationValidator.METHODS_ALLOWED_TO_BE_PUBLIC;

import com.google.auto.common.AnnotationValues;
import com.sun.source.tree.ImportTree;
import com.sun.source.util.Trees;
import java.util.ArrayList;
Expand Down Expand Up @@ -112,7 +113,7 @@ public Void visitType(TypeElement shadowType, Element parent) {

if (actualType == null
&& !suppressWarnings(shadowType, "robolectric.internal.IgnoreMissingClass")) {
error("@Implements: could not resolve class <" + cv + '>', cv);
error("@Implements: could not resolve class <" + AnnotationValues.toString(cv) + '>', cv);
return null;
}
} else {
Expand Down Expand Up @@ -141,7 +142,8 @@ public Void visitType(TypeElement shadowType, Element parent) {
} else if (typeTP.isEmpty()) {
message.append("Shadow type has type parameters but real type does not");
} else {
message.append("Shadow type must have same type parameters as its real counterpart: expected <");
message.append(
"Shadow type must have same type parameters as its real counterpart: expected <");
helpers.appendParameterList(message, actualType.getTypeParameters());
message.append(">, was <");
helpers.appendParameterList(message, shadowType.getTypeParameters());
Expand Down
Expand Up @@ -11,6 +11,7 @@
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Field;
import java.net.Socket;
import java.time.Duration;
import java.util.Arrays;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -152,6 +153,17 @@ public void fileDescriptorRelease_releaseFdCorrect() throws Throwable {
assertThat(accessor.get(copy)).isEqualTo(42);
}

@Test
public void socketFileDescriptor_returnsNullFileDescriptor() throws Throwable {
FileDescriptor fd =
invokeDynamic(
Socket.class,
"getFileDescriptor$",
void.class,
ClassParameter.from(Socket.class, new Socket()));
assertThat(fd).isNull();
}

@SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"})
private static <T> T invokeDynamic(
Class<?> cls, String methodName, Class<?> returnType, ClassParameter<?>... params)
Expand Down
Expand Up @@ -2,7 +2,6 @@

import static android.database.sqlite.SQLiteDatabase.OPEN_READWRITE;
import static com.google.common.truth.Truth.assertThat;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;

Expand Down Expand Up @@ -840,20 +839,6 @@ public void testRawQueryWithCommonTableExpression() {
}
}

@Test
public void shouldThrowWhenForeignKeysConstraintIsViolated() {
database.execSQL("CREATE TABLE master (master_value INTEGER)");
database.execSQL(
"CREATE TABLE slave (master_value INTEGER REFERENCES" + " master(master_value))");
database.execSQL("PRAGMA foreign_keys=ON");
try {
database.execSQL("INSERT INTO slave(master_value) VALUES (1)");
fail("Foreign key constraint is violated but exception is not thrown");
} catch (SQLiteException e) {
assertThat(e.getCause()).hasMessageThat().contains("foreign");
}
}

@Test
public void shouldBeAbleToBeUsedFromDifferentThread() {
final CountDownLatch sync = new CountDownLatch(1);
Expand Down Expand Up @@ -1002,21 +987,6 @@ public void shouldCorrectlyReturnNullValues() {
assertThat(nullValuesCursor.getBlob(3)).isNull();
}

@Test
public void shouldGetBlobFromString() {
ContentValues values = new ContentValues();
values.put("first_column", "this is a string");
database.insert("table_name", null, values);

Cursor data =
database.query("table_name", new String[] {"first_column"}, null, null, null, null, null);
assertThat(data.getCount()).isEqualTo(1);
data.moveToFirst();
assertThat(data.getBlob(0)).isEqualTo(values.getAsString("first_column").getBytes(UTF_8));
}

/////////////////////

private SQLiteDatabase openOrCreateDatabase(String name) {
return openOrCreateDatabase(ApplicationProvider.getApplicationContext().getDatabasePath(name));
}
Expand Down

0 comments on commit 19467ec

Please sign in to comment.