diff options
Diffstat (limited to 'tests')
173 files changed, 24589 insertions, 0 deletions
diff --git a/tests/instrumented/src/AndroidManifest.xml b/tests/instrumented/src/AndroidManifest.xml new file mode 100644 index 0000000..e1ba8b8 --- /dev/null +++ b/tests/instrumented/src/AndroidManifest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2021 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<manifest + xmlns:android="http://schemas.android.com/apk/res/android" + package="com.google.android.enterprise.connectedapps"> + + <uses-sdk + android:minSdkVersion="21" + android:targetSdkVersion="28"/> + + <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" /> + + <application> + <uses-library android:name="android.test.runner" /> + <service android:name="com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector_Service" android:exported="false" /> + </application> + + <instrumentation android:name="com.google.android.apps.common.testing.testrunner.Google3InstrumentationTestRunner" + android:targetPackage="com.google.android.enterprise.connectedapps" + android:label="Connected Apps SDK test"/> +</manifest> diff --git a/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/tests/AvailabilityListenerTest.java b/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/tests/AvailabilityListenerTest.java new file mode 100644 index 0000000..f35f3b5 --- /dev/null +++ b/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/tests/AvailabilityListenerTest.java @@ -0,0 +1,102 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.instrumented.tests; + +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.assertFutureHasException; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assume.assumeTrue; + +import android.app.Application; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.AvailabilityListener; +import com.google.android.enterprise.connectedapps.TestAvailabilityListener; +import com.google.android.enterprise.connectedapps.exceptions.UnavailableProfileException; +import com.google.android.enterprise.connectedapps.instrumented.utils.InstrumentedTestUtilities; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileType; +import com.google.common.util.concurrent.ListenableFuture; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for {@link AvailabilityListener}. */ +@RunWith(JUnit4.class) +public class AvailabilityListenerTest { + private static final Application context = ApplicationProvider.getApplicationContext(); + + private final TestProfileConnector connector = TestProfileConnector.create(context); + private final InstrumentedTestUtilities utilities = + new InstrumentedTestUtilities(context, connector); + private final ProfileTestCrossProfileType type = ProfileTestCrossProfileType.create(connector); + + @Before + public void setup() { + utilities.ensureReadyForCrossProfileCalls(); + } + + @After + public void teardown() { + utilities.ensureNoWorkProfile(); + } + + @Test + public void workProfileTurnedOff_availabilityListenerFires() throws InterruptedException { + assumeTrue( + "Tests can only turn work profile on/off after O", VERSION.SDK_INT >= VERSION_CODES.P); + + utilities.ensureWorkProfileTurnedOn(); + TestAvailabilityListener availabilityListener = new TestAvailabilityListener(); + connector.registerAvailabilityListener(availabilityListener); + + utilities.turnOffWorkProfileAndWait(); + + assertThat(availabilityListener.awaitAvailabilityChange()).isGreaterThan(0); + assertThat(connector.isAvailable()).isFalse(); + } + + @Test + public void workProfileTurnedOn_availabilityListenerFires() throws InterruptedException { + assumeTrue( + "Tests can only turn work profile on/off after O", VERSION.SDK_INT >= VERSION_CODES.P); + + utilities.ensureWorkProfileTurnedOff(); + TestAvailabilityListener availabilityListener = new TestAvailabilityListener(); + connector.registerAvailabilityListener(availabilityListener); + + utilities.turnOnWorkProfileAndWait(); + + assertThat(availabilityListener.awaitAvailabilityChange()).isGreaterThan(0); + assertThat(connector.isAvailable()).isTrue(); + } + + @Test + public void temporaryConnectionError_inProgressCall_availabilityListenerFires() + throws InterruptedException { + utilities.ensureWorkProfileTurnedOn(); + TestAvailabilityListener availabilityListener = new TestAvailabilityListener(); + connector.registerAvailabilityListener(availabilityListener); + + ListenableFuture<Void> unusedFuture = type.other().killApp(); + + assertFutureHasException(unusedFuture, UnavailableProfileException.class); + assertThat(availabilityListener.awaitAvailabilityChange()).isGreaterThan(0); + assertThat(connector.isAvailable()).isTrue(); + } +} diff --git a/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/tests/BothProfilesTest.java b/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/tests/BothProfilesTest.java new file mode 100644 index 0000000..b90ae24 --- /dev/null +++ b/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/tests/BothProfilesTest.java @@ -0,0 +1,98 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.instrumented.tests; + +import static com.google.common.truth.Truth.assertThat; + +import android.app.Application; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.Profile; +import com.google.android.enterprise.connectedapps.instrumented.utils.BlockingStringCallbackListenerMulti; +import com.google.android.enterprise.connectedapps.instrumented.utils.InstrumentedTestUtilities; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileTypeWhichNeedsContext; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests regarding calling a method on both profiles. */ +@RunWith(JUnit4.class) +public class BothProfilesTest { + private static final int FIVE_SECONDS = 5000; + private static final String STRING = "String"; + + private static final Application context = ApplicationProvider.getApplicationContext(); + + private final TestProfileConnector connector = TestProfileConnector.create(context); + private final InstrumentedTestUtilities utilities = + new InstrumentedTestUtilities(context, connector); + + private final ProfileTestCrossProfileTypeWhichNeedsContext type = + ProfileTestCrossProfileTypeWhichNeedsContext.create(connector); + + @Before + public void setup() { + utilities.ensureReadyForCrossProfileCalls(); + } + + /** This test could not be covered by Robolectric. */ + @Test + public void both_synchronous_timesOutOnWorkProfile_timeoutNotEnforcedOnSynchronousCalls() { + utilities.manuallyConnectAndWait(); + + Map<Profile, String> result = + type.both() + .timeout(FIVE_SECONDS) + .identityStringMethodWhichDelays10SecondsOnWorkProfile(STRING); + + assertThat(result).containsKey(connector.utils().getPersonalProfile()); + assertThat(result).containsKey(connector.utils().getWorkProfile()); + } + + /** This test could not be covered by Robolectric. */ + @Test + public void both_async_timesOutOnWorkProfile_onlyIncludesPersonalProfile() + throws InterruptedException { + + BlockingStringCallbackListenerMulti callbackListener = + new BlockingStringCallbackListenerMulti(); + + type.both() + .timeout(FIVE_SECONDS) + .asyncIdentityStringMethodWhichDelays10SecondsOnWorkProfile(STRING, callbackListener); + Map<Profile, String> result = callbackListener.await(); + + assertThat(result).containsKey(connector.utils().getPersonalProfile()); + assertThat(result).doesNotContainKey(connector.utils().getWorkProfile()); + } + + /** This test could not be covered by Robolectric. */ + @Test + public void both_future_timesOutOnWorkProfile_onlyIncludesPersonalProfile() + throws InterruptedException, ExecutionException { + Map<Profile, String> result = + type.both() + .timeout(FIVE_SECONDS) + .futureIdentityStringMethodWhichDelays10SecondsOnWorkProfile(STRING) + .get(); + + assertThat(result).containsKey(connector.utils().getPersonalProfile()); + assertThat(result).doesNotContainKey(connector.utils().getWorkProfile()); + } +} diff --git a/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/tests/ConnectTest.java b/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/tests/ConnectTest.java new file mode 100644 index 0000000..0ab9c44 --- /dev/null +++ b/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/tests/ConnectTest.java @@ -0,0 +1,112 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.instrumented.tests; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import android.app.Application; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.exceptions.UnavailableProfileException; +import com.google.android.enterprise.connectedapps.instrumented.utils.InstrumentedTestUtilities; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** + * Tests regarding manual connections. + * + * <p>These must be instrumented tests as they require multiple threads. + * + * <p>Tests for manual connections when not installed in the other profile are in {@link + * NotInstalledInOtherUserTest}. + */ +@RunWith(JUnit4.class) +public class ConnectTest { + private static final Application context = ApplicationProvider.getApplicationContext(); + + private final TestProfileConnector connector = TestProfileConnector.create(context); + private final InstrumentedTestUtilities utilities = + new InstrumentedTestUtilities(context, connector); + + @Before + public void setup() { + utilities.ensureReadyForCrossProfileCalls(); + } + + @Test + public void connect_connects() throws Exception { + utilities.ensureReadyForCrossProfileCalls(); + + connector.connect(); + + assertThat(connector.isConnected()).isTrue(); + } + + @Test + public void connect_startsManuallyManagingConnection() throws Exception { + utilities.ensureReadyForCrossProfileCalls(); + + connector.connect(); + + assertThat(connector.isManuallyManagingConnection()).isTrue(); + } + + @Test + public void connect_otherProfileNotAvailable_throwsUnavailableProfileException() { + utilities.ensureNoWorkProfile(); + + assertThrows(UnavailableProfileException.class, connector::connect); + } + + @Test + public void connect_otherProfileNotAvailable_doesNotConnect() { + utilities.ensureNoWorkProfile(); + + connectIgnoreExceptions(); + + assertThat(connector.isConnected()).isFalse(); + } + + @Test + public void connect_otherProfileNotAvailable_doesNotStartManuallyManagingConnection() { + utilities.ensureNoWorkProfile(); + + connectIgnoreExceptions(); + + assertThat(connector.isManuallyManagingConnection()).isFalse(); + } + + @Test + public void connect_alreadyConnected_returns() throws UnavailableProfileException { + utilities.ensureReadyForCrossProfileCalls(); + connector.connect(); + + connector.connect(); + + assertThat(connector.isConnected()).isTrue(); + } + + private void connectIgnoreExceptions() { + try { + connector.connect(); + } catch (UnavailableProfileException ignored) { + // Ignore + } + } +} diff --git a/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/tests/HappyPathEndToEndTest.java b/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/tests/HappyPathEndToEndTest.java new file mode 100644 index 0000000..de542d1 --- /dev/null +++ b/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/tests/HappyPathEndToEndTest.java @@ -0,0 +1,152 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.instrumented.tests; + +import static com.google.common.truth.Truth.assertThat; + +import android.app.Application; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.exceptions.UnavailableProfileException; +import com.google.android.enterprise.connectedapps.instrumented.utils.BlockingExceptionCallbackListener; +import com.google.android.enterprise.connectedapps.instrumented.utils.BlockingStringCallbackListener; +import com.google.android.enterprise.connectedapps.instrumented.utils.InstrumentedTestUtilities; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileType; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileTypeWhichNeedsContext; +import com.google.android.enterprise.connectedapps.testing.BlockingPoll; +import java.util.concurrent.ExecutionException; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** + * Tests for high level behaviour running on a correctly configured device (with a managed profile + * with the app installed in both sides, granted INTERACT_ACROSS_USERS). + * + * <p>This tests that each type of call works in both directions. + */ +@RunWith(JUnit4.class) +public class HappyPathEndToEndTest { + private static final Application context = ApplicationProvider.getApplicationContext(); + + private static final String STRING = "String"; + + private final TestProfileConnector connector = TestProfileConnector.create(context); + private final InstrumentedTestUtilities utilities = + new InstrumentedTestUtilities(context, connector); + private final ProfileTestCrossProfileType type = ProfileTestCrossProfileType.create(connector); + private final ProfileTestCrossProfileTypeWhichNeedsContext typeWithContext = + ProfileTestCrossProfileTypeWhichNeedsContext.create(connector); + + @Before + public void setup() { + utilities.ensureReadyForCrossProfileCalls(); + } + + @After + public void teardown() { + connector.stopManualConnectionManagement(); + utilities.waitForDisconnected(); + } + + @Test + public void isAvailable_isTrue() { + assertThat(connector.isAvailable()).isTrue(); + } + + @Test + public void isConnected_isFalse() { + connector.stopManualConnectionManagement(); + utilities.waitForDisconnected(); + + assertThat(connector.isConnected()).isFalse(); + } + + @Test + public void isConnected_hasConnected_isTrue() { + utilities.manuallyConnectAndWait(); + + assertThat(connector.isConnected()).isTrue(); + } + + @Test + public void synchronousMethod_resultIsCorrect() throws UnavailableProfileException { + utilities.manuallyConnectAndWait(); + + assertThat(type.other().identityStringMethod(STRING)).isEqualTo(STRING); + } + + @Test + public void futureMethod_resultIsCorrect() throws InterruptedException, ExecutionException { + assertThat(type.other().listenableFutureIdentityStringMethod(STRING).get()).isEqualTo(STRING); + } + + @Test + public void asyncMethod_resultIsCorrect() throws InterruptedException { + BlockingStringCallbackListener stringCallbackListener = new BlockingStringCallbackListener(); + + type.other() + .asyncIdentityStringMethod( + STRING, stringCallbackListener, new BlockingExceptionCallbackListener()); + + assertThat(stringCallbackListener.await()).isEqualTo(STRING); + } + + @Test + public void synchronousMethod_fromOtherProfile_resultIsCorrect() + throws UnavailableProfileException { + utilities.manuallyConnectAndWait(); + typeWithContext.other().connectToOtherProfile(); + BlockingPoll.poll( + () -> { + try { + return typeWithContext.other().isConnectedToOtherProfile(); + } catch (UnavailableProfileException e) { + return false; + } + }, + /* pollFrequency= */ 100, + /* timeoutMillis= */ 10000); + + assertThat(typeWithContext.other().methodWhichCallsIdentityStringMethodOnOtherProfile(STRING)) + .isEqualTo(STRING); + } + + @Test + public void asyncMethod_fromOtherProfile_resultIsCorrect() throws InterruptedException { + BlockingStringCallbackListener stringCallbackListener = new BlockingStringCallbackListener(); + + typeWithContext + .other() + .asyncMethodWhichCallsIdentityStringMethodOnOtherProfile( + STRING, stringCallbackListener, new BlockingExceptionCallbackListener()); + + assertThat(stringCallbackListener.await()).isEqualTo(STRING); + } + + @Test + public void futureMethod_fromOtherProfile_resultIsCorrect() + throws ExecutionException, InterruptedException { + assertThat( + typeWithContext + .other() + .listenableFutureMethodWhichCallsIdentityStringMethodOnOtherProfile(STRING) + .get()) + .isEqualTo(STRING); + } +} diff --git a/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/tests/InstrumentedTestUtilitiesTest.java b/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/tests/InstrumentedTestUtilitiesTest.java new file mode 100644 index 0000000..7a84852 --- /dev/null +++ b/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/tests/InstrumentedTestUtilitiesTest.java @@ -0,0 +1,97 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.instrumented.tests; + +import static com.google.common.truth.Truth.assertThat; + +import android.app.Application; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.AvailabilityListener; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import com.google.android.enterprise.connectedapps.testing.InstrumentedTestUtilities; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for {@link AvailabilityListener}. */ +@RunWith(JUnit4.class) +public class InstrumentedTestUtilitiesTest { + + private static final Application context = ApplicationProvider.getApplicationContext(); + + private final TestProfileConnector connector = TestProfileConnector.create(context); + private final InstrumentedTestUtilities utilities = + new InstrumentedTestUtilities(context, connector); + + @Test + public void isAvailable_ensureReadyForCrossProfileCalls_isTrue() { + utilities.ensureReadyForCrossProfileCalls(); + + assertThat(connector.isAvailable()).isTrue(); + } + + @Test + public void isAvailable_ensureNoWorkProfile_isFalse() { + utilities.ensureReadyForCrossProfileCalls(); + + utilities.ensureNoWorkProfile(); + + assertThat(connector.isAvailable()).isFalse(); + } + + @Test + public void hasWorkProfile_createdWorkProfile_isTrue() { + utilities.ensureWorkProfileExists(); + + assertThat(utilities.hasWorkProfile()).isTrue(); + } + + @Test + public void hasWorkProfile_removedWorkProfile_isFalse() { + utilities.ensureNoWorkProfile(); + + assertThat(utilities.hasWorkProfile()).isFalse(); + } + + @Test + public void getWorkProfileUserId_createdWorkProfile_isNotZero() { + utilities.ensureWorkProfileExists(); + + assertThat(utilities.getWorkProfileUserId()).isNotEqualTo(0); + } + + @Test + public void isConnected_waitForConnected_isTrue() { + utilities.ensureReadyForCrossProfileCalls(); + + connector.startConnecting(); + utilities.waitForConnected(); + + assertThat(connector.isConnected()).isTrue(); + } + + @Test + public void isConnected_waitForDisconnected_isFalse() { + utilities.ensureReadyForCrossProfileCalls(); + connector.startConnecting(); + utilities.waitForConnected(); + + connector.stopManualConnectionManagement(); + utilities.waitForDisconnected(); + + assertThat(connector.isConnected()).isFalse(); + } +} diff --git a/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/tests/MessageSizeTest.java b/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/tests/MessageSizeTest.java new file mode 100644 index 0000000..25be188 --- /dev/null +++ b/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/tests/MessageSizeTest.java @@ -0,0 +1,103 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.instrumented.tests; + +import static com.google.android.enterprise.connectedapps.StringUtilities.randomString; +import static com.google.common.truth.Truth.assertThat; + +import android.app.Application; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.TestExceptionCallbackListener; +import com.google.android.enterprise.connectedapps.exceptions.UnavailableProfileException; +import com.google.android.enterprise.connectedapps.instrumented.utils.BlockingStringCallbackListener; +import com.google.android.enterprise.connectedapps.instrumented.utils.InstrumentedTestUtilities; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileType; +import java.util.concurrent.ExecutionException; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for passing large messages across profiles. */ +@RunWith(JUnit4.class) +public class MessageSizeTest { + private static final Application context = ApplicationProvider.getApplicationContext(); + + private static final String SMALL_STRING = "String"; + private static final String LARGE_STRING = randomString(1500000); // 3Mb + + private final TestProfileConnector connector = TestProfileConnector.create(context); + private final InstrumentedTestUtilities utilities = + new InstrumentedTestUtilities(context, connector); + private final ProfileTestCrossProfileType type = ProfileTestCrossProfileType.create(connector); + + private final BlockingStringCallbackListener stringCallbackListener = + new BlockingStringCallbackListener(); + private final TestExceptionCallbackListener exceptionCallbackListener = + new TestExceptionCallbackListener(); + + @Before + public void setup() { + utilities.ensureReadyForCrossProfileCalls(); + } + + @Test + public void synchronous_smallMessage_sends() throws UnavailableProfileException { + utilities.manuallyConnectAndWait(); + + assertThat(type.other().identityStringMethod(SMALL_STRING)).isEqualTo(SMALL_STRING); + } + + @Test + public void synchronous_largeMessage_sends() throws UnavailableProfileException { + utilities.manuallyConnectAndWait(); + + // We can't use the asserts which compare Strings because of b/158998985 + assertThat(type.other().identityStringMethod(LARGE_STRING).equals(LARGE_STRING)).isTrue(); + } + + @Test + public void async_smallMessage_sends() throws InterruptedException { + type.other() + .asyncIdentityStringMethod(SMALL_STRING, stringCallbackListener, exceptionCallbackListener); + + assertThat(stringCallbackListener.await()).isEqualTo(SMALL_STRING); + } + + @Test + public void async_largeMessage_sends() throws InterruptedException { + type.other() + .asyncIdentityStringMethod(LARGE_STRING, stringCallbackListener, exceptionCallbackListener); + + // We can't use the asserts which compare Strings because of b/158998985 + assertThat(stringCallbackListener.await().equals(LARGE_STRING)).isTrue(); + } + + @Test + public void future_smallMessage_sends() throws ExecutionException, InterruptedException { + String result = type.other().listenableFutureIdentityStringMethod(SMALL_STRING).get(); + + assertThat(result).isEqualTo(SMALL_STRING); + } + + @Test + public void future_largeMessage_sends() throws ExecutionException, InterruptedException { + String result = type.other().listenableFutureIdentityStringMethod(LARGE_STRING).get(); + + assertThat(result).isEqualTo(LARGE_STRING); + } +} diff --git a/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/tests/NotInstalledInOtherUserTest.java b/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/tests/NotInstalledInOtherUserTest.java new file mode 100644 index 0000000..b3c4b38 --- /dev/null +++ b/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/tests/NotInstalledInOtherUserTest.java @@ -0,0 +1,63 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.instrumented.tests; + +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.assertFutureHasException; +import static org.junit.Assert.assertThrows; + +import android.app.Application; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.exceptions.UnavailableProfileException; +import com.google.android.enterprise.connectedapps.instrumented.utils.InstrumentedTestUtilities; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileType; +import com.google.common.util.concurrent.ListenableFuture; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** + * Tests how the SDK behaves when running on a device with a work profile but without the + * app installed. + */ +@RunWith(JUnit4.class) +public class NotInstalledInOtherUserTest { + + private static final String STRING = "String"; + + private static final Application context = ApplicationProvider.getApplicationContext(); + + private final TestProfileConnector connector = TestProfileConnector.create(context); + private final ProfileTestCrossProfileType type = ProfileTestCrossProfileType.create(connector); + private final InstrumentedTestUtilities utilities = + new InstrumentedTestUtilities(context, connector); + + @Test + public void asyncCall_notInstalledInOtherProfile_failsFast() { + utilities.ensureWorkProfileExistsWithoutTestApp(); + + ListenableFuture<String> future = type.other().listenableFutureIdentityStringMethod(STRING); + + assertFutureHasException(future, UnavailableProfileException.class); + } + + @Test + public void connect_notInstalledInOtherProfile_failsFast() { + utilities.ensureWorkProfileExistsWithoutTestApp(); + + assertThrows(UnavailableProfileException.class, connector::connect); + } +} diff --git a/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/tests/NotReallySerializableTest.java b/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/tests/NotReallySerializableTest.java new file mode 100644 index 0000000..abb5133 --- /dev/null +++ b/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/tests/NotReallySerializableTest.java @@ -0,0 +1,85 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.instrumented.tests; + +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.assertFutureHasException; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import android.app.Application; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.exceptions.ProfileRuntimeException; +import com.google.android.enterprise.connectedapps.exceptions.UnavailableProfileException; +import com.google.android.enterprise.connectedapps.instrumented.utils.BlockingExceptionCallbackListener; +import com.google.android.enterprise.connectedapps.instrumented.utils.InstrumentedTestUtilities; +import com.google.android.enterprise.connectedapps.testapp.NotReallySerializableObject; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileType; +import com.google.common.util.concurrent.ListenableFuture; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** + * Tests regarding types which claim to be Serializable but are not. + * + * <p>This requires instrumented tests as there is no way to force the serialization in Robolectric + * tests. + */ +@RunWith(JUnit4.class) +public class NotReallySerializableTest { + private static final Application context = ApplicationProvider.getApplicationContext(); + + private final TestProfileConnector connector = TestProfileConnector.create(context); + private final InstrumentedTestUtilities utilities = + new InstrumentedTestUtilities(context, connector); + + private final ProfileTestCrossProfileType type = ProfileTestCrossProfileType.create(connector); + private final BlockingExceptionCallbackListener exceptionCallbackListener = + new BlockingExceptionCallbackListener(); + + @Before + public void setup() { + utilities.ensureReadyForCrossProfileCalls(); + } + + @Test + public void + synchronous_serializableObjectIsNotReallySerializable_throwsProfileRuntimeException() { + utilities.manuallyConnectAndWait(); + + assertThrows( + ProfileRuntimeException.class, + () -> type.other().returnNotReallySerializableObjectMethod()); + } + + @Test + public void asyncMethod_serializableObjectIsNotReallySerializable_throwsException() + throws InterruptedException { + type.other().asyncGetNotReallySerializableObjectMethod(object -> {}, exceptionCallbackListener); + + assertThat(exceptionCallbackListener.await()).isInstanceOf(UnavailableProfileException.class); + } + + @Test + public void future_serializableObjectIsNotReallySerializable_throwsException() { + ListenableFuture<NotReallySerializableObject> future = + type.other().futureGetNotReallySerializableObjectMethod(); + + assertFutureHasException(future, UnavailableProfileException.class); + } +} diff --git a/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/tests/SecondUserTest.java b/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/tests/SecondUserTest.java new file mode 100644 index 0000000..b882aa6 --- /dev/null +++ b/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/tests/SecondUserTest.java @@ -0,0 +1,111 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.instrumented.tests; + +import static com.google.common.truth.Truth.assertThat; + +import android.app.Application; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.exceptions.UnavailableProfileException; +import com.google.android.enterprise.connectedapps.instrumented.utils.InstrumentedTestUtilities; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileTypeWhichNeedsContext; +import com.google.android.enterprise.connectedapps.testing.BlockingPoll; +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests regarding how the SDK behaves when running on a device with a second user. */ +@RunWith(JUnit4.class) +public class SecondUserTest { + private static final Application context = ApplicationProvider.getApplicationContext(); + + private final TestProfileConnector connector = TestProfileConnector.create(context); + private final InstrumentedTestUtilities utilities = + new InstrumentedTestUtilities(context, connector); + + private final ProfileTestCrossProfileTypeWhichNeedsContext type = + ProfileTestCrossProfileTypeWhichNeedsContext.create(connector); + + @After + public void teardown() { + utilities.ensureNoWorkProfile(); + } + + @Test + public void isAvailable_noWorkProfile_hasSecondUser_isFalse() { + int secondUserId = utilities.createUser("SecondUser"); + + try { + utilities.startUser(secondUserId); + utilities.installInUser(secondUserId); + utilities.grantInteractAcrossUsers(); + + assertThat(connector.isAvailable()).isFalse(); + } finally { + utilities.removeUser(secondUserId); + } + } + + @Test + public void call_hasWorkProfile_hasSecondUser_executesOnWorkProfile() + throws UnavailableProfileException { + utilities.ensureReadyForCrossProfileCalls(); + utilities.manuallyConnectAndWait(); + int secondUserId = utilities.createUser("SecondUser"); + + try { + utilities.startUser(secondUserId); + utilities.installInUser(secondUserId); + utilities.grantInteractAcrossUsers(); + + assertThat(type.other().getUserId()).isEqualTo(utilities.getWorkProfileUserId()); + } finally { + utilities.removeUser(secondUserId); + } + } + + @Test + public void call_hasWorkProfile_hasSecondUser_fromWorkProfile_executesOnThisUser() + throws UnavailableProfileException { + utilities.ensureReadyForCrossProfileCalls(); + utilities.manuallyConnectAndWait(); + int secondUserId = utilities.createUser("SecondUser"); + + try { + utilities.startUser(secondUserId); + utilities.installInUser(secondUserId); + utilities.grantInteractAcrossUsers(); + + type.other().connectToOtherProfile(); + BlockingPoll.poll( + () -> { + try { + return type.other().isConnectedToOtherProfile(); + } catch (UnavailableProfileException e) { + return false; + } + }, + /* pollFrequency= */ 100, + /* timeoutMillis= */ 10000); + + assertThat(type.other().getOtherUserId()).isEqualTo(0); + } finally { + utilities.removeUser(secondUserId); + } + } +} diff --git a/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/utils/BlockingBroadcastReceiver.java b/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/utils/BlockingBroadcastReceiver.java new file mode 100644 index 0000000..28baf55 --- /dev/null +++ b/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/utils/BlockingBroadcastReceiver.java @@ -0,0 +1,88 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.instrumented.utils; + +import static java.util.Collections.singleton; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import java.util.Collection; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; + +/** A {@link BroadcastReceiver} which can block until a broadcast is received. */ +public class BlockingBroadcastReceiver extends BroadcastReceiver { + private static final int DEFAULT_TIMEOUT_SECONDS = 30; + + private final BlockingQueue<Intent> blockingQueue; + private final Collection<String> expectedActions; + private final Context context; + + public BlockingBroadcastReceiver(Context context, Collection<String> expectedActions) { + this.context = context; + this.expectedActions = expectedActions; + blockingQueue = new ArrayBlockingQueue<>(/* capacity= */ 10); + } + + public BlockingBroadcastReceiver(Context context, String expectedAction) { + this(context, singleton(expectedAction)); + } + + @Override + public void onReceive(Context context, Intent intent) { + if (expectedActions.contains(intent.getAction())) { + blockingQueue.add(intent); + } + } + + /** Call before making the call which should trigger the broadcast. */ + public void register() { + for (String expectedAction : expectedActions) { + context.registerReceiver(this, new IntentFilter(expectedAction)); + } + } + + /** + * Wait until the broadcast and return the received broadcast intent. {@code null} is returned if + * no broadcast with expected action is received within 10 seconds. + */ + public Intent awaitForBroadcast() { + return awaitForBroadcast(DEFAULT_TIMEOUT_SECONDS * 1000); + } + + /** + * Wait until the broadcast and return the received broadcast intent. {@code null} is returned if + * no broadcast with expected action is received within the given timeout. + */ + public Intent awaitForBroadcast(long timeoutMillis) { + try { + return blockingQueue.poll(timeoutMillis, MILLISECONDS); + } catch (InterruptedException e) { + throw new AssertionError("Awaiting broadcast interrupted", e); + } + } + + public void unregisterQuietly() { + try { + context.unregisterReceiver(this); + } catch (RuntimeException ex) { + // ignore issues unregistering + } + } +} diff --git a/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/utils/BlockingCallbackListener.java b/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/utils/BlockingCallbackListener.java new file mode 100644 index 0000000..6963fdd --- /dev/null +++ b/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/utils/BlockingCallbackListener.java @@ -0,0 +1,39 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.instrumented.utils; + +import java.util.concurrent.CountDownLatch; + +/** + * Base class for callback listeners which can block until a result is received. + * + * <p>To use, extend this class passing {@code E} as the type of value being received, and call + * {@link #receive(Object)} when the callback completes. + */ +public abstract class BlockingCallbackListener<E> { + private E callbackValue; + private final CountDownLatch latch = new CountDownLatch(1); + + public E await() throws InterruptedException { + latch.await(); + return callbackValue; + } + + protected void receive(E value) { + callbackValue = value; + latch.countDown(); + } +} diff --git a/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/utils/BlockingExceptionCallbackListener.java b/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/utils/BlockingExceptionCallbackListener.java new file mode 100644 index 0000000..dcd8c03 --- /dev/null +++ b/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/utils/BlockingExceptionCallbackListener.java @@ -0,0 +1,27 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.instrumented.utils; + +import com.google.android.enterprise.connectedapps.ExceptionCallback; + +/** An {@link ExceptionCallback} which can block for a result. */ +public class BlockingExceptionCallbackListener extends BlockingCallbackListener<Throwable> + implements ExceptionCallback { + @Override + public void onException(Throwable throwable) { + receive(throwable); + } +} diff --git a/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/utils/BlockingStringCallbackListener.java b/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/utils/BlockingStringCallbackListener.java new file mode 100644 index 0000000..ba24e68 --- /dev/null +++ b/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/utils/BlockingStringCallbackListener.java @@ -0,0 +1,27 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.instrumented.utils; + +import com.google.android.enterprise.connectedapps.testapp.TestStringCallbackListener; + +/** A {@link TestStringCallbackListener} which can block for a result. */ +public class BlockingStringCallbackListener extends BlockingCallbackListener<String> + implements TestStringCallbackListener { + @Override + public void stringCallback(String s) { + receive(s); + } +} diff --git a/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/utils/BlockingStringCallbackListenerMulti.java b/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/utils/BlockingStringCallbackListenerMulti.java new file mode 100644 index 0000000..b7231c4 --- /dev/null +++ b/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/utils/BlockingStringCallbackListenerMulti.java @@ -0,0 +1,29 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.instrumented.utils; + +import com.google.android.enterprise.connectedapps.Profile; +import com.google.android.enterprise.connectedapps.testapp.TestStringCallbackListener_Multi; +import java.util.Map; + +public class BlockingStringCallbackListenerMulti + extends BlockingCallbackListener<Map<Profile, String>> + implements TestStringCallbackListener_Multi { + @Override + public void stringCallback(Map<Profile, String> s) { + receive(s); + } +} diff --git a/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/utils/InstrumentedTestUtilities.java b/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/utils/InstrumentedTestUtilities.java new file mode 100644 index 0000000..e76ee9c --- /dev/null +++ b/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/utils/InstrumentedTestUtilities.java @@ -0,0 +1,353 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.instrumented.utils; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import android.content.Context; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.os.ParcelFileDescriptor; +import android.os.UserHandle; +import androidx.test.platform.app.InstrumentationRegistry; +import com.google.android.enterprise.connectedapps.ProfileConnector; +import com.google.android.enterprise.connectedapps.SharedTestUtilities; +import com.google.android.enterprise.connectedapps.instrumented.utils.ServiceCall.Parameter; +import com.google.android.enterprise.connectedapps.testing.ProfileAvailabilityPoll; +import java.io.FileInputStream; +import java.io.InputStream; +import java.util.NoSuchElementException; +import java.util.Scanner; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Wrapper around {@link + * com.google.android.enterprise.connectedapps.testing.InstrumentedTestUtilities} which adds + * features needed only by the SDK. + */ +public class InstrumentedTestUtilities { + + private final ProfileConnector connector; + private final Context context; + private final com.google.android.enterprise.connectedapps.testing.InstrumentedTestUtilities + instrumentedTestUtilities; + + private static final int R_REQUEST_QUIET_MODE_ENABLED_ID = 72; + private static final int REQUEST_QUIET_MODE_ENABLED_ID = 58; + + private static final String USER_ID_KEY = "USER_ID"; + private static final Parameter USER_ID_PARAMETER = new Parameter(USER_ID_KEY); + + private static final ServiceCall R_TURN_OFF_WORK_PROFILE_COMMAND = + new ServiceCall("user", R_REQUEST_QUIET_MODE_ENABLED_ID) + .setUser(1000) // user 1000 has packageName "android" + .addStringParam("android") // callingPackage + .addBooleanParam(true) // enableQuietMode + .addIntParam(USER_ID_PARAMETER) // userId + .addIntParam(0) // target + .addIntParam(0); // flags + + private static final ServiceCall TURN_OFF_WORK_PROFILE_COMMAND = + new ServiceCall("user", REQUEST_QUIET_MODE_ENABLED_ID) + .setUser(1000) // user 1000 has packageName "android" + .addStringParam("android") // callingPackage + .addBooleanParam(true) // enableQuietMode + .addIntParam(USER_ID_PARAMETER) // userId + .addIntParam(0); // target + + private static final ServiceCall R_TURN_ON_WORK_PROFILE_COMMAND = + new ServiceCall("user", R_REQUEST_QUIET_MODE_ENABLED_ID) + .setUser(1000) // user 1000 has packageName "android" + .addStringParam("android") // callingPackage + .addBooleanParam(false) // enableQuietMode + .addIntParam(USER_ID_PARAMETER) // userId + .addIntParam(0) // target + .addIntParam(0); // flags + + private static final ServiceCall TURN_ON_WORK_PROFILE_COMMAND = + new ServiceCall("user", REQUEST_QUIET_MODE_ENABLED_ID) + .setUser(1000) // user 1000 has packageName "android" + .addStringParam("android") // callingPackage + .addBooleanParam(false) // enableQuietMode + .addIntParam(USER_ID_PARAMETER) // userId + .addIntParam(0); // target + + public InstrumentedTestUtilities(Context context, ProfileConnector connector) { + this.context = context; + this.connector = connector; + this.instrumentedTestUtilities = + new com.google.android.enterprise.connectedapps.testing.InstrumentedTestUtilities( + context, connector); + } + + /** + * See {@link + * com.google.android.enterprise.connectedapps.testing.InstrumentedTestUtilities#hasWorkProfile()}. + */ + public boolean hasWorkProfile() { + return instrumentedTestUtilities.hasWorkProfile(); + } + + /** + * See {@link + * com.google.android.enterprise.connectedapps.testing.InstrumentedTestUtilities#getWorkProfileUserId()}. + */ + public int getWorkProfileUserId() { + return instrumentedTestUtilities.getWorkProfileUserId(); + } + + private UserHandle getWorkProfileUserHandle() { + return SharedTestUtilities.getUserHandleForUserId(getWorkProfileUserId()); + } + + /** + * See {@link + * com.google.android.enterprise.connectedapps.testing.InstrumentedTestUtilities#ensureReadyForCrossProfileCalls()}. + */ + public void ensureReadyForCrossProfileCalls() { + instrumentedTestUtilities.ensureReadyForCrossProfileCalls(); + ensureWorkProfileTurnedOn(); + } + + /** + * See {@link + * com.google.android.enterprise.connectedapps.testing.InstrumentedTestUtilities#ensureNoWorkProfile()}. + */ + public void ensureNoWorkProfile() { + instrumentedTestUtilities.ensureNoWorkProfile(); + } + + public void removeUser(int userId) { + runCommandWithOutput("pm remove-user " + userId); + } + + public void installInUser(int userId) { + runCommandWithOutput( + "cmd package install-existing --user " + userId + " " + context.getPackageName()); + } + + /** + * Grant the {@code INTERACT_ACROSS_USERS} permission if this app declares it. + * + * <p>This is required before cross-profile interaction will work. + */ + public void grantInteractAcrossUsers() { + // TODO(scottjonathan): Support INTERACT_ACROSS_PROFILES in these tests. + runCommandWithOutput( + "pm grant " + context.getPackageName() + " android.permission.INTERACT_ACROSS_USERS"); + } + + /** + * See {@link + * com.google.android.enterprise.connectedapps.testing.InstrumentedTestUtilities#ensureWorkProfileExists()} + */ + public void ensureWorkProfileExists() { + instrumentedTestUtilities.ensureWorkProfileExists(); + } + + /** + * Create a work profile but do not install the test app. + * + * <p>This means that, as there is no profile owner, it will not be recognised as a work profile + * by the SDK when running on that profile. + */ + public void ensureWorkProfileExistsWithoutTestApp() { + if (hasWorkProfile()) { + if (!userHasPackageInstalled(getWorkProfileUserId(), context.getPackageName())) { + return; + } + + // TODO(162219825): Try to remove the package + + throw new IllegalStateException( + "There is already a work profile on the device with user id " + + getWorkProfileUserId() + + "."); + } + runCommandWithOutput("pm create-user --profileOf 0 --managed TestProfile123"); + int workProfileUserId = getWorkProfileUserId(); + startUser(workProfileUserId); + } + + private static boolean userHasPackageInstalled(int userId, String packageName) { + String expectedPackageLine = "package:" + packageName; + String[] installedPackages = + runCommandWithOutput("pm list packages --user " + userId).split("\n"); + for (String installedPackage : installedPackages) { + if (installedPackage.equals(expectedPackageLine)) { + return true; + } + } + return false; + } + + /** Ensure that the work profile is running. */ + public void ensureWorkProfileTurnedOn() { + turnOnWorkProfileAndWait(); + } + + /** Ensure that the work profile is not running. */ + public void ensureWorkProfileTurnedOff() { + turnOffWorkProfileAndWait(); + } + + /** + * Turn off the work profile and block until it has been turned off. + * + * <p>This uses {@link ServiceCall} and so is only guaranteed to work correctly on AOSP. + * + * @see #turnOffWorkProfile() + */ + public void turnOffWorkProfileAndWait() { + turnOffWorkProfile(); + + ProfileAvailabilityPoll.blockUntilProfileNotAvailable(context, getWorkProfileUserHandle()); + } + + // TODO(160147511): Remove use of service calls for versions after R + /** + * Turn off the work profile + * + * <p>This uses {@link ServiceCall} and so is only guaranteed to work correctly on AOSP. + * + * @see #turnOffWorkProfileAndWait() + */ + public void turnOffWorkProfile() { + if (VERSION.SDK_INT == VERSION_CODES.R) { + runCommandWithOutput( + R_TURN_OFF_WORK_PROFILE_COMMAND + .prepare() + .setInt(USER_ID_KEY, getWorkProfileUserId()) + .getCommand()); + } else if (VERSION.SDK_INT == VERSION_CODES.Q || VERSION.SDK_INT == VERSION_CODES.P) { + runCommandWithOutput( + TURN_OFF_WORK_PROFILE_COMMAND + .prepare() + .setInt(USER_ID_KEY, getWorkProfileUserId()) + .getCommand()); + } else { + throw new IllegalStateException("Cannot turn off work on this version of android"); + } + } + + /** + * Turn on the work profile and block until it has been turned on. + * + * <p>This uses {@link ServiceCall} and so is only guaranteed to work correctly on AOSP. + * + * @see #turnOnWorkProfile() + */ + public void turnOnWorkProfileAndWait() { + if (connector.isAvailable()) { + return; // Already on + } + + turnOnWorkProfile(); + + ProfileAvailabilityPoll.blockUntilProfileRunningAndUnlocked( + context, getWorkProfileUserHandle()); + } + + // TODO(160147511): Remove use of service calls for versions after R + /** + * Turn on the work profile and block until it has been turned on. + * + * <p>This uses {@link ServiceCall} and so is only guaranteed to work correctly on AOSP. + * + * @see #turnOnWorkProfileAndWait() + */ + public void turnOnWorkProfile() { + if (VERSION.SDK_INT == VERSION_CODES.R) { + runCommandWithOutput( + R_TURN_ON_WORK_PROFILE_COMMAND + .prepare() + .setInt(USER_ID_KEY, getWorkProfileUserId()) + .getCommand()); + } else if (VERSION.SDK_INT == VERSION_CODES.Q || VERSION.SDK_INT == VERSION_CODES.P) { + runCommandWithOutput( + TURN_ON_WORK_PROFILE_COMMAND + .prepare() + .setInt(USER_ID_KEY, getWorkProfileUserId()) + .getCommand()); + } else { + throw new IllegalStateException("Cannot turn on work on this version of android"); + } + } + + /** + * See {@link + * com.google.android.enterprise.connectedapps.testing.InstrumentedTestUtilities#waitForDisconnected()}. + */ + public void waitForDisconnected() { + instrumentedTestUtilities.waitForDisconnected(); + } + + /** + * See {@link + * com.google.android.enterprise.connectedapps.testing.InstrumentedTestUtilities#waitForConnected()}. + */ + public void waitForConnected() { + instrumentedTestUtilities.waitForConnected(); + } + + /** + * Manually call {@link ProfileConnector#startConnecting()} and wait for connection to be + * complete. + */ + public void manuallyConnectAndWait() { + connector.startConnecting(); + waitForConnected(); + } + + private static final Pattern CREATE_USER_PATTERN = + Pattern.compile("Success: created user id (\\d+)"); + + public int createUser(String username) { + String output = runCommandWithOutput("pm create-user " + username); + + Matcher userMatcher = CREATE_USER_PATTERN.matcher(output); + if (userMatcher.find()) { + return Integer.parseInt(userMatcher.group(1)); + } + + throw new IllegalStateException("Could not create user. Output: " + output); + } + + public void startUser(int userId) { + UserHandle userHandle = SharedTestUtilities.getUserHandleForUserId(userId); + InstrumentedTestUtilities.runCommandWithOutput("am start-user " + userId); + ProfileAvailabilityPoll.blockUntilProfileRunningAndUnlocked(context, userHandle); + } + + private static String runCommandWithOutput(String command) { + ParcelFileDescriptor p = runCommand(command); + + InputStream inputStream = new FileInputStream(p.getFileDescriptor()); + + try (Scanner scanner = new Scanner(inputStream, UTF_8.name())) { + return scanner.useDelimiter("\\A").next(); + } catch (NoSuchElementException e) { + return ""; + } + } + + private static ParcelFileDescriptor runCommand(String command) { + return InstrumentationRegistry.getInstrumentation() + .getUiAutomation() + .executeShellCommand(command); + } +} diff --git a/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/utils/ServiceCall.java b/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/utils/ServiceCall.java new file mode 100644 index 0000000..ae6e479 --- /dev/null +++ b/tests/instrumented/src/main/java/com/google/android/enterprise/connectedapps/instrumented/utils/ServiceCall.java @@ -0,0 +1,201 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.instrumented.utils; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** + * Class used when building a service call command to be used on the shell. + * + * <p>These commands are likely to break in future android versions so should be replaced as soon as + * possible. + */ +class ServiceCall { + + enum DataType { + STRING, + INT, + BOOLEAN + } + + /** This reflects a parameter which is passed in to complete the service call command */ + static class Parameter { + + final String name; + + public Parameter(String name) { + this.name = name; + } + + String placeholder() { + return "{{" + name + "}}"; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Parameter)) { + return false; + } + Parameter parameter = (Parameter) o; + return Objects.equals(name, parameter.name); + } + + @Override + public int hashCode() { + return Objects.hashCode(name); + } + } + + static class PreparedServiceCall { + private final ServiceCall serviceCall; + private final Map<Parameter, String> setParameters = new HashMap<>(); + + private PreparedServiceCall(ServiceCall serviceCall) { + this.serviceCall = serviceCall; + } + + public PreparedServiceCall setString(String key, String value) { + Parameter keyParameter = new Parameter(key); + if (!serviceCall.parameters.containsKey(keyParameter)) { + throw new IllegalStateException("No such key " + key); + } + if (serviceCall.parameters.get(keyParameter) != DataType.STRING) { + throw new IllegalStateException(key + " is not a String"); + } + setParameters.put(keyParameter, value); + return this; + } + + public PreparedServiceCall setInt(String key, int value) { + Parameter keyParameter = new Parameter(key); + if (!serviceCall.parameters.containsKey(keyParameter)) { + throw new IllegalStateException( + "No such key " + key + " valid keys " + serviceCall.parameters.keySet()); + } + if (serviceCall.parameters.get(keyParameter) != DataType.INT) { + throw new IllegalStateException(key + " is not an int"); + } + setParameters.put(keyParameter, Integer.toString(value)); + return this; + } + + public PreparedServiceCall setBoolean(String key, boolean value) { + Parameter keyParameter = new Parameter(key); + if (!serviceCall.parameters.containsKey(keyParameter)) { + throw new IllegalStateException("No such key " + key); + } + if (serviceCall.parameters.get(keyParameter) != DataType.BOOLEAN) { + throw new IllegalStateException(key + " is not a boolean"); + } + setParameters.put(keyParameter, value ? "1" : "0"); + return this; + } + + public String getCommand() { + Set<Parameter> parametersToSet = new HashSet<>(serviceCall.parameters.keySet()); + parametersToSet.removeAll(setParameters.keySet()); + + if (!parametersToSet.isEmpty()) { + throw new IllegalStateException("Unset parameters: " + parametersToSet); + } + + String command = serviceCall.getCommandUnchecked(); + for (Map.Entry<Parameter, String> entry : setParameters.entrySet()) { + command = command.replace(entry.getKey().placeholder(), entry.getValue()); + } + + return command; + } + } + + private Integer user; + private final String serviceName; + private final int methodId; + private final Map<Parameter, DataType> parameters = new HashMap<>(); + + private final StringBuilder commandBuilder = new StringBuilder(); + + ServiceCall(String serviceName, int methodId) { + this.serviceName = serviceName; + this.methodId = methodId; + } + + ServiceCall addStringParam(String value) { + commandBuilder.append(" s16 ").append(value); + return this; + } + + ServiceCall addStringParam(Parameter value) { + commandBuilder.append(" s16 ").append(value.placeholder()); + parameters.put(value, DataType.STRING); + return this; + } + + ServiceCall addIntParam(int value) { + commandBuilder.append(" i32 ").append(value); + return this; + } + + ServiceCall addIntParam(Parameter value) { + commandBuilder.append(" i32 ").append(value.placeholder()); + parameters.put(value, DataType.INT); + return this; + } + + ServiceCall addBooleanParam(boolean value) { + return addIntParam(value ? 1 : 0); + } + + ServiceCall addBooleanParam(Parameter value) { + commandBuilder.append(" i32 ").append(value.placeholder()); + parameters.put(value, DataType.BOOLEAN); + return this; + } + + ServiceCall setUser(int user) { + this.user = user; + return this; + } + + PreparedServiceCall prepare() { + return new PreparedServiceCall(this); + } + + String getCommand() { + if (!parameters.isEmpty()) { + throw new IllegalStateException("This ServiceCall requires parameters, use #prepare"); + } + + return getCommandUnchecked(); + } + + private String getCommandUnchecked() { + String cmd = "service call " + serviceName + " " + methodId + commandBuilder; + + if (user != null) { + cmd = "su " + user + " " + cmd; + } + + return cmd; + } +} diff --git a/tests/processor/src/main/AndroidManifest.xml b/tests/processor/src/main/AndroidManifest.xml new file mode 100644 index 0000000..0669799 --- /dev/null +++ b/tests/processor/src/main/AndroidManifest.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2021 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<manifest + xmlns:android="http://schemas.android.com/apk/res/android" package="com.google.android.enterprise.connectedapps.testing"> + <uses-sdk + android:minSdkVersion="14" + android:targetSdkVersion="28"/> +</manifest> diff --git a/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/AlwaysThrowsTest.java b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/AlwaysThrowsTest.java new file mode 100644 index 0000000..08e4e2a --- /dev/null +++ b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/AlwaysThrowsTest.java @@ -0,0 +1,89 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.NOTES_PACKAGE; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesCrossProfileType; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesProvider; +import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.Compiler.javac; + +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationPrinter; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationStrings; +import com.google.testing.compile.Compilation; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class AlwaysThrowsTest { + + private final AnnotationPrinter annotationPrinter; + + public AlwaysThrowsTest(AnnotationPrinter annotationPrinter) { + this.annotationPrinter = annotationPrinter; + } + + @Parameters(name = "{0}") + public static Iterable<AnnotationStrings> getAnnotationPrinters() { + return AnnotationFinder.annotationStrings(); + } + + @Test + public void compile_generatesAlwaysThrowsClass() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesProvider(annotationPrinter), + annotatedNotesCrossProfileType(annotationPrinter)); + + assertThat(compilation).generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_AlwaysThrows"); + } + + @Test + public void compile_alwaysThrowsClassImplementsSingleSender() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesProvider(annotationPrinter), + annotatedNotesCrossProfileType(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_AlwaysThrows") + .contentsAsUtf8String() + .contains( + "class ProfileNotesType_AlwaysThrows implements" + " ProfileNotesType_SingleSender"); + } + + @Test + public void compile_alwaysThrowsClassHasConstructorTakingErrorMessage() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesProvider(annotationPrinter), + annotatedNotesCrossProfileType(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_AlwaysThrows") + .contentsAsUtf8String() + .contains("public ProfileNotesType_AlwaysThrows(String errorMessage)"); + } +} diff --git a/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/BundlerTest.java b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/BundlerTest.java new file mode 100644 index 0000000..ca62a9d --- /dev/null +++ b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/BundlerTest.java @@ -0,0 +1,73 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.NOTES_PACKAGE; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesCrossProfileType; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesProvider; +import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.Compiler.javac; + +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationPrinter; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationStrings; +import com.google.testing.compile.Compilation; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class BundlerTest { + + private final AnnotationPrinter annotationPrinter; + + public BundlerTest(AnnotationPrinter annotationPrinter) { + this.annotationPrinter = annotationPrinter; + } + + @Parameters(name = "{0}") + public static Iterable<AnnotationStrings> getAnnotationPrinters() { + return AnnotationFinder.annotationStrings(); + } + + @Test + public void generatesBundlerClass() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesProvider(annotationPrinter), + annotatedNotesCrossProfileType(annotationPrinter)); + + assertThat(compilation).generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_Bundler"); + } + + @Test + public void bundlerClassImplementsBundler() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesProvider(annotationPrinter), + annotatedNotesCrossProfileType(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_Bundler") + .contentsAsUtf8String() + .contains("ProfileNotesType_Bundler implements Bundler"); + } +} diff --git a/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileCallbackSupportedParameterTypeTest.java b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileCallbackSupportedParameterTypeTest.java new file mode 100644 index 0000000..136fa36 --- /dev/null +++ b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileCallbackSupportedParameterTypeTest.java @@ -0,0 +1,120 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.NOTES_PACKAGE; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.PARCELABLE_OBJECT; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.SERIALIZABLE_OBJECT; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.combineParameters; +import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.Compiler.javac; + +import com.google.android.enterprise.connectedapps.annotations.CrossProfileCallback; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationPrinter; +import com.google.testing.compile.Compilation; +import com.google.testing.compile.JavaFileObjects; +import java.util.Arrays; +import javax.tools.JavaFileObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +/** + * Test supported parameter types for methods on an {@link CrossProfileCallback} annotated + * interface. + * + * <p>This tests a single parameter of each supported type. Multiple parameters and unsupported + * types are tested in {@link CrossProfileCallbackTest}. + */ +@RunWith(Parameterized.class) +public class CrossProfileCallbackSupportedParameterTypeTest { + + @Parameters(name = "{0} for {1}") + public static Iterable<Object[]> data() { + String[] types = { + "String", + "String[]", + "byte", + "Byte", + "short", + "Short", + "int", + "Integer", + "long", + "Long", + "float", + "Float", + "double", + "Double", + "char", + "Character", + "boolean", + "Boolean", + "ParcelableObject", + "ParcelableObject[]", + "java.util.List<ParcelableObject>", + "java.util.List<ParcelableObject[]>", + "java.util.List<java.util.List<ParcelableObject>>", + "java.util.List<SerializableObject>", + "java.util.List<SerializableObject[]>", + "java.util.List<java.util.List<SerializableObject>>", + "java.util.List<String>", + "java.util.List<String[]>", + "java.util.Map<String, String>", + "java.util.Set<String>", + "java.util.Collection<String>", + "com.google.protos.connectedappssdk.TestProtoOuterClass.TestProto", + "com.google.protos.connectedappssdk.TestProtoOuterClass.TestProto[]", + "java.util.List<com.google.protos.connectedappssdk.TestProtoOuterClass.TestProto>", + "com.google.common.collect.ImmutableMap<String, String>", + "android.util.Pair<String, Integer>", + "com.google.common.base.Optional<ParcelableObject>" + }; + return combineParameters(AnnotationFinder.annotationStrings(), Arrays.asList(types)); + } + + private final AnnotationPrinter annotationPrinter; + + private final String type; + + public CrossProfileCallbackSupportedParameterTypeTest( + AnnotationPrinter annotationPrinter, String type) { + this.annotationPrinter = annotationPrinter; + this.type = type; + } + + @Test + public void crossProfileCallbackInterfaceWithSupportedParameterType_compiles() { + JavaFileObject callbackInterface = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".InstallationListener", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileCallbackQualifiedName() + ";", + annotationPrinter.crossProfileCallbackAsAnnotation(), + "public interface InstallationListener {", + " void installationComplete(" + type + " param);", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(callbackInterface, PARCELABLE_OBJECT, SERIALIZABLE_OBJECT); + + assertThat(compilation).succeededWithoutWarnings(); + } +} diff --git a/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileCallbackTest.java b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileCallbackTest.java new file mode 100644 index 0000000..adc1c3d --- /dev/null +++ b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileCallbackTest.java @@ -0,0 +1,847 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.CUSTOM_PROFILE_CONNECTOR_QUALIFIED_NAME; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.CUSTOM_WRAPPER; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.GENERIC_PARCELABLE_OBJECT; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.GENERIC_SERIALIZABLE_OBJECT; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.NOTES_PACKAGE; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.PARCELABLE_CUSTOM_WRAPPER; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.PROFILE_CONNECTOR_QUALIFIED_NAME; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.UNSUPPORTED_TYPE; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.UNSUPPORTED_TYPE_NAME; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesProvider; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.formatErrorMessage; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.installationListener; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.installationListenerWithListStringParam; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.installationListenerWithStringParam; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.notesCrossProfileTypeWhichUsesInstallationListener; +import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.Compiler.javac; + +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationStrings; +import com.google.testing.compile.Compilation; +import com.google.testing.compile.JavaFileObjects; +import javax.tools.JavaFileObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class CrossProfileCallbackTest { + + private static final String UNSUPPORTED_PARAMETER_TYPE_ERROR = + "cannot be used by parameters of methods on interfaces annotated" + + " @CROSS_PROFILE_CALLBACK_ANNOTATION"; + private static final String CALLBACK_INTERFACE_DEFAULT_PACKAGE_ERROR = + "Interfaces annotated @CROSS_PROFILE_CALLBACK_ANNOTATION must not be in the default package"; + private static final String NOT_INTERFACE_ERROR = + "Only interfaces may be annotated @CROSS_PROFILE_CALLBACK_ANNOTATION"; + private static final String NO_METHODS_ERROR = + "Interfaces annotated @CROSS_PROFILE_CALLBACK_ANNOTATION must have at least one method"; + private static final String NOT_ONE_METHOD_ERROR = + "Interfaces annotated @CROSS_PROFILE_CALLBACK_ANNOTATION(simple=true) must have exactly one" + + " method"; + private static final String DEFAULT_METHOD_ERROR = + "Interfaces annotated @CROSS_PROFILE_CALLBACK_ANNOTATION must have no default methods"; + private static final String STATIC_METHOD_ERROR = + "Interfaces annotated @CROSS_PROFILE_CALLBACK_ANNOTATION must have no static methods"; + private static final String NOT_VOID_ERROR = + "Methods on interfaces annotated @CROSS_PROFILE_CALLBACK_ANNOTATION must return void"; + private static final String GENERIC_CALLBACK_INTERFACE_ERROR = + "Interfaces annotated @CROSS_PROFILE_CALLBACK_ANNOTATION can not be generic"; + private static final String MORE_THAN_ONE_PARAMETER_ERROR = + "Methods on interfaces annotated @CROSS_PROFILE_CALLBACK_ANNOTATION(simple=true) can only" + + " take a single parameter"; + + private final AnnotationStrings annotationStrings; + + public CrossProfileCallbackTest(AnnotationStrings annotationStrings) { + this.annotationStrings = annotationStrings; + } + + @Parameters(name = "{0}") + public static Iterable<AnnotationStrings> getAnnotationPrinters() { + return AnnotationFinder.annotationStrings(); + } + + @Test + public void crossProfileCallbackInterface_compiles() { + JavaFileObject callbackInterface = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".InstallationListener", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileCallbackQualifiedName() + ";", + annotationStrings.crossProfileCallbackAsAnnotation(), + "public interface InstallationListener {", + " void installationComplete(int state);", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(callbackInterface); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void crossProfileCallbackInterface_defaultPackage_hasError() { + JavaFileObject callbackInterface = + JavaFileObjects.forSourceLines( + "InstallationListener", + "import " + annotationStrings.crossProfileCallbackQualifiedName() + ";", + annotationStrings.crossProfileCallbackAsAnnotation(), + "public interface InstallationListener {", + " void installationComplete(int state);", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(callbackInterface); + + assertThat(compilation) + .hadErrorContaining( + formatErrorMessage(CALLBACK_INTERFACE_DEFAULT_PACKAGE_ERROR, annotationStrings)) + .inFile(callbackInterface); + } + + @Test + public void crossProfileCallbackInterface_notInterface_hasError() { + JavaFileObject callbackInterface = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".InstallationListener", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileCallbackQualifiedName() + ";", + annotationStrings.crossProfileCallbackAsAnnotation(), + "public abstract class InstallationListener {", + " abstract void installationComplete(int state);", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(callbackInterface); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(NOT_INTERFACE_ERROR, annotationStrings)) + .inFile(callbackInterface); + } + + @Test + public void crossProfileCallbackInterface_noMethods_hasError() { + JavaFileObject callbackInterface = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".InstallationListener", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileCallbackQualifiedName() + ";", + annotationStrings.crossProfileCallbackAsAnnotation(), + "public interface InstallationListener {", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(callbackInterface); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(NO_METHODS_ERROR, annotationStrings)) + .inFile(callbackInterface); + } + + @Test + public void crossProfileCallbackInterface_simple_moreThanOneMethod_hasError() { + JavaFileObject callbackInterface = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".InstallationListener", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileCallbackQualifiedName() + ";", + annotationStrings.crossProfileCallbackAsAnnotation("simple=true"), + "public interface InstallationListener {", + " abstract void installationComplete(int state);", + " abstract void secondMethod(String s);", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(callbackInterface); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(NOT_ONE_METHOD_ERROR, annotationStrings)) + .inFile(callbackInterface); + } + + @Test + public void crossProfileCallbackInterface_moreThanOneMethod_compiles() { + JavaFileObject callbackInterface = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".InstallationListener", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileCallbackQualifiedName() + ";", + annotationStrings.crossProfileCallbackAsAnnotation(), + "public interface InstallationListener {", + " abstract void installationComplete(int state);", + " abstract void secondMethod(String s);", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(callbackInterface); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void crossProfileCallbackInterface_defaultMethod_hasError() { + JavaFileObject callbackInterface = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".InstallationListener", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileCallbackQualifiedName() + ";", + annotationStrings.crossProfileCallbackAsAnnotation(), + "public interface InstallationListener {", + "default void defaultMethod() {};", + " void installationComplete(int state);", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(callbackInterface); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(DEFAULT_METHOD_ERROR, annotationStrings)) + .inFile(callbackInterface); + } + + @Test + public void crossProfileCallbackInterface_staticMethod_hasError() { + JavaFileObject callbackInterface = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".InstallationListener", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileCallbackQualifiedName() + ";", + annotationStrings.crossProfileCallbackAsAnnotation(), + "public interface InstallationListener {", + "static void staticMethod() {};", + " void installationComplete(int state);", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(callbackInterface); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(STATIC_METHOD_ERROR, annotationStrings)) + .inFile(callbackInterface); + } + + @Test + public void crossProfileCallbackInterface_nonVoidReturnType_hasError() { + JavaFileObject callbackInterface = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".InstallationListener", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileCallbackQualifiedName() + ";", + annotationStrings.crossProfileCallbackAsAnnotation(), + "public interface InstallationListener {", + " int installationComplete(int state);", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(callbackInterface); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(NOT_VOID_ERROR, annotationStrings)) + .inFile(callbackInterface); + } + + @Test + public void multipleSupportedParameters_simple_hasError() { + JavaFileObject callbackInterface = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".InstallationListener", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileCallbackQualifiedName() + ";", + annotationStrings.crossProfileCallbackAsAnnotation("simple=true"), + "public interface InstallationListener {", + " void installationComplete(String s, String t);", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(callbackInterface); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(MORE_THAN_ONE_PARAMETER_ERROR, annotationStrings)) + .inFile(callbackInterface); + } + + @Test + public void multipleSupportedParameters_notSimple_compiles() { + JavaFileObject callbackInterface = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".InstallationListener", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileCallbackQualifiedName() + ";", + annotationStrings.crossProfileCallbackAsAnnotation(), + "public interface InstallationListener {", + " void installationComplete(String s, String t);", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(callbackInterface); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void crossProfileCallbackInterfaceMethodWithUnsupportedParameterType_notUsed_compiles() { + // Cross-profile callbacks are only evaluated in the context of a Cross-profile Type + JavaFileObject callbackInterface = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".InstallationListener", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileCallbackQualifiedName() + ";", + annotationStrings.crossProfileCallbackAsAnnotation(), + "public interface InstallationListener {", + " void installationComplete(" + UNSUPPORTED_TYPE_NAME + " s);", + "}"); + + Compilation compilation = + javac().withProcessors(new Processor()).compile(callbackInterface, UNSUPPORTED_TYPE); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void + crossProfileCallbackInterfaceMethodWithUnsupportedParameterTypeInGeneric_notUsed_compiles() { + JavaFileObject callbackInterface = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".InstallationListener", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileCallbackQualifiedName() + ";", + annotationStrings.crossProfileCallbackAsAnnotation(), + "public interface InstallationListener {", + " void installationComplete(java.util.List<" + UNSUPPORTED_TYPE_NAME + "> s);", + "}"); + + Compilation compilation = + javac().withProcessors(new Processor()).compile(callbackInterface, UNSUPPORTED_TYPE); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void + crossProfileCallbackInterfaceMethodWithUnsupportedParameterTypeInGeneric_isUsed_hasError() { + JavaFileObject callbackInterface = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".InstallationListener", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileCallbackQualifiedName() + ";", + annotationStrings.crossProfileCallbackAsAnnotation(), + "public interface InstallationListener {", + " void installationComplete(java.util.List<" + UNSUPPORTED_TYPE_NAME + "> s);", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + callbackInterface, + UNSUPPORTED_TYPE, + notesCrossProfileTypeWhichUsesInstallationListener(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(UNSUPPORTED_PARAMETER_TYPE_ERROR, annotationStrings)) + .inFile(callbackInterface); + } + + @Test + public void crossProfileCallbackInterfaceMethodWithContextParameterTypeInGeneric_hasError() { + JavaFileObject callbackInterface = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".InstallationListener", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileCallbackQualifiedName() + ";", + "import android.content.Context;", + annotationStrings.crossProfileCallbackAsAnnotation(), + "public interface InstallationListener {", + " void installationComplete(java.util.List<Context> s);", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + callbackInterface, + UNSUPPORTED_TYPE, + notesCrossProfileTypeWhichUsesInstallationListener(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(UNSUPPORTED_PARAMETER_TYPE_ERROR, annotationStrings)) + .inFile(callbackInterface); + } + + @Test + public void genericCrossProfileCallbackMethod_hasError() { + JavaFileObject callbackInterface = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".InstallationListener", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileCallbackQualifiedName() + ";", + annotationStrings.crossProfileCallbackAsAnnotation(), + "public interface InstallationListener<R> {", + " void installationComplete(String s);", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(callbackInterface); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(GENERIC_CALLBACK_INTERFACE_ERROR, annotationStrings)) + .inFile(callbackInterface); + } + + @Test + public void + crossProfileCallbackInterfaceWithUnsupportedParameterTypeInGenericParcelable_compiles() { + JavaFileObject callbackInterface = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".InstallationListener", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileCallbackQualifiedName() + ";", + annotationStrings.crossProfileCallbackAsAnnotation(), + "public interface InstallationListener {", + " public void installationComplete(GenericParcelableObject<" + + UNSUPPORTED_TYPE_NAME + + "> s);", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(callbackInterface, UNSUPPORTED_TYPE, GENERIC_PARCELABLE_OBJECT); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void + crossProfileCallbackInterfaceWithUnsupportedParameterTypeInGenericSerializable_compiles() { + JavaFileObject callbackInterface = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".InstallationListener", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileCallbackQualifiedName() + ";", + annotationStrings.crossProfileCallbackAsAnnotation(), + "public interface InstallationListener {", + " public void installationComplete(GenericSerializableObject<" + + UNSUPPORTED_TYPE_NAME + + "> s);", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(callbackInterface, UNSUPPORTED_TYPE, GENERIC_SERIALIZABLE_OBJECT); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void + crossProfileCallbackInterface_hasCrossProfileCallbackInterfaceParameter_notUsed_compiles() { + JavaFileObject otherCallbackInterface = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".OtherCallbackInterface", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileCallbackQualifiedName() + ";", + annotationStrings.crossProfileCallbackAsAnnotation(), + "public interface OtherCallbackInterface {", + " void installationComplete(String s);", + "}"); + + JavaFileObject installationListener = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".InstallationListener", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileCallbackQualifiedName() + ";", + annotationStrings.crossProfileCallbackAsAnnotation(), + "public interface InstallationListener {", + " void installationComplete(OtherCallbackInterface s);", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(installationListener, otherCallbackInterface); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void + crossProfileCallbackInterface_hasCrossProfileCallbackInterfaceParameter_isUsed_hasError() { + JavaFileObject otherCallbackInterface = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".OtherCallbackInterface", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileCallbackQualifiedName() + ";", + annotationStrings.crossProfileCallbackAsAnnotation(), + "public interface OtherCallbackInterface {", + " void installationComplete(String s);", + "}"); + + JavaFileObject installationListener = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".InstallationListener", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileCallbackQualifiedName() + ";", + annotationStrings.crossProfileCallbackAsAnnotation(), + "public interface InstallationListener {", + " void installationComplete(OtherCallbackInterface s);", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + installationListener, + otherCallbackInterface, + notesCrossProfileTypeWhichUsesInstallationListener(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(UNSUPPORTED_PARAMETER_TYPE_ERROR, annotationStrings)) + .inFile(installationListener); + } + + @Test + public void crossProfileCallbackInterface_generatesMultiClass() { + Compilation compilation = + javac().withProcessors(new Processor()).compile(installationListener(annotationStrings)); + + assertThat(compilation) + .generatedSourceFile("com.google.android.enterprise.notes.InstallationListener_Multi"); + } + + @Test + public void crossProfileCallbackInterface_multiClassIncludesNoArgsMethod() { + JavaFileObject installationListener = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".InstallationListener", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileCallbackQualifiedName() + ";", + annotationStrings.crossProfileCallbackAsAnnotation(), + "public interface InstallationListener {", + " void installationComplete();", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(installationListener); + + assertThat(compilation) + .generatedSourceFile("com.google.android.enterprise.notes.InstallationListener_Multi") + .contentsAsUtf8String() + .contains("void installationComplete();"); + } + + @Test + public void crossProfileCallbackInterface_multiClassIncludesSingleArgMethod() { + JavaFileObject installationListener = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".InstallationListener", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileCallbackQualifiedName() + ";", + annotationStrings.crossProfileCallbackAsAnnotation(), + "public interface InstallationListener {", + " void installationComplete(String s);", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(installationListener); + + assertThat(compilation) + .generatedSourceFile("com.google.android.enterprise.notes.InstallationListener_Multi") + .contentsAsUtf8String() + .contains("void installationComplete(Map<Profile, String> s);"); + } + + @Test + public void crossProfileCallbackInterface_generatesReceiverClass() { + Compilation compilation = + javac().withProcessors(new Processor()).compile(installationListener(annotationStrings)); + + assertThat(compilation) + .generatedSourceFile( + "com.google.android.enterprise.notes.Profile_InstallationListener_Receiver"); + } + + @Test + public void crossProfileCallbackInterface_receiverClassImplementsOriginalInterface() { + Compilation compilation = + javac().withProcessors(new Processor()).compile(installationListener(annotationStrings)); + + assertThat(compilation) + .generatedSourceFile( + "com.google.android.enterprise.notes.Profile_InstallationListener_Receiver") + .contentsAsUtf8String() + .contains("implements InstallationListener"); + } + + @Test + public void crossProfileCallbackInterface_generatesSenderClass() { + Compilation compilation = + javac().withProcessors(new Processor()).compile(installationListener(annotationStrings)); + + assertThat(compilation) + .generatedSourceFile( + "com.google.android.enterprise.notes.Profile_InstallationListener_Sender"); + } + + @Test + public void crossProfileCallbackInterface_senderClassImplementsLocalCallback() { + Compilation compilation = + javac().withProcessors(new Processor()).compile(installationListener(annotationStrings)); + + assertThat(compilation) + .generatedSourceFile( + "com.google.android.enterprise.notes.Profile_InstallationListener_Sender") + .contentsAsUtf8String() + .contains("implements LocalCallback"); + } + + @Test + public void crossProfileCallbackInterface_generatesMultiMergerInputClass() { + Compilation compilation = + javac().withProcessors(new Processor()).compile(installationListener(annotationStrings)); + + assertThat(compilation) + .generatedSourceFile( + "com.google.android.enterprise.notes.Profile_InstallationListener_MultiMergerInput"); + } + + @Test + public void crossProfileCallbackInterface_multiMergerInputClassImplementsInterface() { + Compilation compilation = + javac().withProcessors(new Processor()).compile(installationListener(annotationStrings)); + + assertThat(compilation) + .generatedSourceFile( + "com.google.android.enterprise.notes.Profile_InstallationListener_MultiMergerInput") + .contentsAsUtf8String() + .contains("implements InstallationListener"); + } + + @Test + public void crossProfileCallbackInterface_generatesMultiMergerResultClass() { + Compilation compilation = + javac().withProcessors(new Processor()).compile(installationListener(annotationStrings)); + + assertThat(compilation) + .generatedSourceFile( + "com.google.android.enterprise.notes.Profile_InstallationListener_MultiMergerResult"); + } + + @Test + public void + crossProfileCallbackInterface_multiMergerResultClassImplementsCompleteListenerInterface() { + Compilation compilation = + javac().withProcessors(new Processor()).compile(installationListener(annotationStrings)); + + assertThat(compilation) + .generatedSourceFile( + "com.google.android.enterprise.notes.Profile_InstallationListener_MultiMergerResult") + .contentsAsUtf8String() + .contains("implements CrossProfileCallbackMultiMergerCompleteListener<Void>"); + } + + @Test + public void crossProfileCallbackInterface_withArgumentCompilesSuccessfully() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void doInstall(InstallationListener l) {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + notesType, + annotatedNotesProvider(annotationStrings), + installationListener(annotationStrings)); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void crossProfileCallbackInterface_argumentTypeIsIncludedInBundler() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void doInstall(InstallationListener l) {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + notesType, + annotatedNotesProvider(annotationStrings), + installationListenerWithStringParam(annotationStrings)); + + assertThat(compilation) + .generatedSourceFile("com.google.android.enterprise.notes.ProfileNotesType_Bundler") + .contentsAsUtf8String() + .contains(".writeString"); + } + + @Test + public void + crossProfileCallbackInterfaceWithMultipleMethods_allArgumentTypesAreIncludedInBundler() { + JavaFileObject installationListener = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".InstallationListener", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileCallbackQualifiedName() + ";", + annotationStrings.crossProfileCallbackAsAnnotation(), + "public interface InstallationListener {", + " void installationComplete(String s, Float f);", + " void installationFailed(Boolean b, Byte p);", + "}"); + + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void doInstall(InstallationListener l) {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationStrings), installationListener); + + assertThat(compilation) + .generatedSourceFile("com.google.android.enterprise.notes.ProfileNotesType_Bundler") + .contentsAsUtf8String() + .contains(".writeString"); + + assertThat(compilation) + .generatedSourceFile("com.google.android.enterprise.notes.ProfileNotesType_Bundler") + .contentsAsUtf8String() + .contains(".writeFloat"); + + assertThat(compilation) + .generatedSourceFile("com.google.android.enterprise.notes.ProfileNotesType_Bundler") + .contentsAsUtf8String() + .contains(".writeInt"); // used for Boolean + + assertThat(compilation) + .generatedSourceFile("com.google.android.enterprise.notes.ProfileNotesType_Bundler") + .contentsAsUtf8String() + .contains(".writeByte"); + } + + @Test + public void crossProfileCallbackInterface_wrappedArgumentTypeIsIncludedInBundler() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void doInstall(InstallationListener l) {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + notesType, + annotatedNotesProvider(annotationStrings), + installationListenerWithListStringParam(annotationStrings)); + + assertThat(compilation) + .generatedSourceFile("com.google.android.enterprise.notes.ProfileNotesType_Bundler") + .contentsAsUtf8String() + .contains("ParcelableList"); + } + + @Test + public void + crossProfileCallbackInterfaceMethodWithCustomParcelableWrapperParameterType_compiles() { + JavaFileObject callbackInterface = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".InstallationListener", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileCallbackQualifiedName() + ";", + annotationStrings.crossProfileCallbackAsAnnotation(), + "public interface InstallationListener {", + " void installationComplete(CustomWrapper s);", + "}"); + + JavaFileObject crossProfileType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + annotationStrings.crossProfileAsAnnotation( + "connector=CrossProfileConnectorWhichSupportsCustomWrapper.class"), + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void install(InstallationListener l) {", + " }", + "}"); + + JavaFileObject connector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".CrossProfileConnectorWhichSupportsCustomWrapper", + "package " + NOTES_PACKAGE + ";", + "import " + PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "import " + CUSTOM_PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "@CustomProfileConnector(parcelableWrappers={ParcelableCustomWrapper.class})", + "public interface CrossProfileConnectorWhichSupportsCustomWrapper extends" + + " ProfileConnector {", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + callbackInterface, + CUSTOM_WRAPPER, + PARCELABLE_CUSTOM_WRAPPER, + crossProfileType, + connector); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(UNSUPPORTED_PARAMETER_TYPE_ERROR, annotationStrings)) + .inFile(callbackInterface); + } + + @Test + public void interfaceMarkedSimple_isSimple_compiles() { + JavaFileObject callbackInterface = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".InstallationListener", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileCallbackQualifiedName() + ";", + annotationStrings.crossProfileCallbackAsAnnotation("simple=true"), + "public interface InstallationListener {", + " void installationComplete(String s);", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(callbackInterface); + + assertThat(compilation).succeededWithoutWarnings(); + } +} diff --git a/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileProviderClassTest.java b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileProviderClassTest.java new file mode 100644 index 0000000..08c19a2 --- /dev/null +++ b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileProviderClassTest.java @@ -0,0 +1,321 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.ANNOTATED_NOTES_CONNECTOR; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.NOTES_PACKAGE; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesCrossProfileType; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.formatErrorMessage; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.staticType; +import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.Compiler.javac; + +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationStrings; +import com.google.testing.compile.Compilation; +import com.google.testing.compile.JavaFileObjects; +import javax.tools.JavaFileObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class CrossProfileProviderClassTest { + + private static final String INVALID_CONSTRUCTORS_ERROR = + "Provider classes must have a single public constructor which takes either a single Context" + + " argument or no arguments"; + private static final String PROVIDER_CLASS_DIFFERENT_CONNECTOR_ERROR = + "All @CROSS_PROFILE_ANNOTATION types provided by a provider class must use the same" + + " ProfileConnector"; + private static final String STATICTYPES_ERROR = + "@CROSS_PROFILE_ANNOTATION classes referenced in @CROSS_PROFILE_PROVIDER_ANNOTATION" + + " staticTypes annotations must not have non-static @CROSS_PROFILE_ANNOTATION annotated" + + " methods"; + + private final AnnotationStrings annotationStrings; + + public CrossProfileProviderClassTest(AnnotationStrings annotationStrings) { + this.annotationStrings = annotationStrings; + } + + @Parameters(name = "{0}") + public static Iterable<AnnotationStrings> getAnnotationPrinters() { + return AnnotationFinder.annotationStrings(); + } + + @Test + public void hasACustomNoArgsConstructor_compiles() { + JavaFileObject providerClass = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesProvider", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileProviderQualifiedName() + ";", + "public final class NotesProvider {", + " public NotesProvider() {", + " }", + annotationStrings.crossProfileProviderAsAnnotation(), + " public NotesType provideNotesType() {", + " return new NotesType();", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(providerClass, annotatedNotesCrossProfileType(annotationStrings)); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void hasNonPublicNoArgsConstructor_hasError() { + JavaFileObject providerClass = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesProvider", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileProviderQualifiedName() + ";", + "public final class NotesProvider {", + " NotesProvider() {", + " }", + annotationStrings.crossProfileProviderAsAnnotation(), + " public NotesType provideNotesType() {", + " return new NotesType();", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(providerClass, annotatedNotesCrossProfileType(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(INVALID_CONSTRUCTORS_ERROR, annotationStrings)) + .inFile(providerClass); + } + + @Test + public void hasNoNoArgsConstructor_hasError() { + JavaFileObject providerClass = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesProvider", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileProviderQualifiedName() + ";", + "public final class NotesProvider {", + " public NotesProvider(String p) {", + " }", + annotationStrings.crossProfileProviderAsAnnotation(), + " public NotesType provideNotesType() {", + " return new NotesType();", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(providerClass, annotatedNotesCrossProfileType(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(INVALID_CONSTRUCTORS_ERROR, annotationStrings)) + .inFile(providerClass); + } + + @Test + public void hasPublicConstructorTakingContext_compiles() { + JavaFileObject providerClass = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesProvider", + "package " + NOTES_PACKAGE + ";", + "import android.content.Context;", + "import " + annotationStrings.crossProfileProviderQualifiedName() + ";", + "public final class NotesProvider {", + " public NotesProvider(Context c) {", + " }", + annotationStrings.crossProfileProviderAsAnnotation(), + " public NotesType provideNotesType() {", + " return new NotesType();", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(providerClass, annotatedNotesCrossProfileType(annotationStrings)); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void hasMoreThanOnePublicConstructor_errors() { + JavaFileObject providerClass = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesProvider", + "package " + NOTES_PACKAGE + ";", + "import android.content.Context;", + "import " + annotationStrings.crossProfileProviderQualifiedName() + ";", + "public final class NotesProvider {", + " public NotesProvider(Context c) {", + " }", + " public NotesProvider() {", + " }", + annotationStrings.crossProfileProviderAsAnnotation(), + " public NotesType provideNotesType() {", + " return new NotesType();", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(providerClass, annotatedNotesCrossProfileType(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(INVALID_CONSTRUCTORS_ERROR, annotationStrings)) + .inFile(providerClass); + } + + @Test + public void hasCrossProfileTypesWithDifferentConnectors_hasError() { + JavaFileObject providerClass = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesProvider", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileProviderQualifiedName() + ";", + "public final class NotesProvider {", + annotationStrings.crossProfileProviderAsAnnotation(), + " public NotesType provideNotesType() {", + " return new NotesType();", + " }", + annotationStrings.crossProfileProviderAsAnnotation(), + " public NotesType2 provideNotesType2() {", + " return new NotesType2();", + " }", + "}"); + + JavaFileObject notesTypeWithCrossProfileConnector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "import com.google.android.enterprise.connectedapps.CrossProfileConnector;", + annotationStrings.crossProfileAsAnnotation("connector=CrossProfileConnector.class"), + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes() {", + " }", + "}"); + + JavaFileObject notesType2WithNotesConnector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType2", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + annotationStrings.crossProfileAsAnnotation("connector=NotesConnector.class"), + "public final class NotesType2 {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes() {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + providerClass, + notesTypeWithCrossProfileConnector, + notesType2WithNotesConnector, + ANNOTATED_NOTES_CONNECTOR); + + assertThat(compilation) + .hadErrorContaining( + formatErrorMessage(PROVIDER_CLASS_DIFFERENT_CONNECTOR_ERROR, annotationStrings)) + .inFile(providerClass); + } + + @Test + public void staticTypes_onlyReferencesStaticTypes_compiles() { + JavaFileObject notesProvider = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesProvider", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileProviderQualifiedName() + ";", + "import com.google.android.enterprise.connectedapps.CrossProfileConnector;", + annotationStrings.crossProfileProviderAsAnnotation("staticTypes={StaticType.class}"), + "public final class NotesProvider {", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesProvider, staticType(annotationStrings)); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void staticTypes_referencesNonStaticType_hasError() { + JavaFileObject notesProvider = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesProvider", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileProviderQualifiedName() + ";", + "import com.google.android.enterprise.connectedapps.CrossProfileConnector;", + annotationStrings.crossProfileProviderAsAnnotation("staticTypes={NotesType.class}"), + "public final class NotesProvider {", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesProvider, annotatedNotesCrossProfileType(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(STATICTYPES_ERROR, annotationStrings)) + .inFile(notesProvider); + } + + @Test + public void staticProvidedClass_usedTypeIsIncludedInBundler() { + JavaFileObject notesProvider = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesProvider", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileProviderQualifiedName() + ";", + "import com.google.android.enterprise.connectedapps.CrossProfileConnector;", + annotationStrings.crossProfileProviderAsAnnotation("staticTypes={StaticType.class}"), + "public final class NotesProvider {", + "}"); + JavaFileObject staticType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".StaticType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class StaticType {", + annotationStrings.crossProfileAsAnnotation(), + " public static void refreshNotes(String param) {", + " }", + "}"); + + Compilation compilation = + javac().withProcessors(new Processor()).compile(notesProvider, staticType); + + assertThat(compilation) + .generatedSourceFile("com.google.android.enterprise.notes.ProfileStaticType_Bundler") + .contentsAsUtf8String() + .contains("parcel.writeString("); + } +} diff --git a/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileProviderTest.java b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileProviderTest.java new file mode 100644 index 0000000..9e37596 --- /dev/null +++ b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileProviderTest.java @@ -0,0 +1,337 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.NOTES_PACKAGE; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesCrossProfileType; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.formatErrorMessage; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.staticType; +import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.Compiler.javac; + +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationStrings; +import com.google.testing.compile.Compilation; +import com.google.testing.compile.JavaFileObjects; +import javax.tools.JavaFileObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class CrossProfileProviderTest { + + private static final String MULTIPLE_PROVIDERS_ERROR = "has been provided more than once"; + private static final String PROVIDING_NON_CROSS_PROFILE_TYPE_ERROR = + "Methods annotated @CROSS_PROFILE_PROVIDER_ANNOTATION must only return" + + " @CROSS_PROFILE_ANNOTATION annotated types"; + private static final String PROVIDER_INCORRECT_ARGS_ERROR = + "Methods annotated @CROSS_PROFILE_PROVIDER_ANNOTATION can only take a single Context" + + " argument, or no-args"; + private static final String STATIC_PROVIDER_ERROR = + "Methods annotated @CROSS_PROFILE_PROVIDER_ANNOTATION can not be static"; + private static final String METHOD_STATICTYPES_ERROR = + "@CROSS_PROFILE_PROVIDER_ANNOTATION annotations on methods can not specify staticTypes"; + + private final AnnotationStrings annotationStrings; + + public CrossProfileProviderTest(AnnotationStrings annotationStrings) { + this.annotationStrings = annotationStrings; + } + + @Parameters(name = "{0}") + public static Iterable<AnnotationStrings> getAnnotationPrinters() { + return AnnotationFinder.annotationStrings(); + } + + @Test + public void providesAValidCrossProfileType_compiles() { + final JavaFileObject validNotesProvider = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesProvider", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileProviderQualifiedName() + ";", + "public final class NotesProvider {", + annotationStrings.crossProfileProviderAsAnnotation(), + " public NotesType provideNotesType() {", + " return new NotesType();", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(validNotesProvider, annotatedNotesCrossProfileType(annotationStrings)); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void providesANotCrossProfileType_hasError() { + final JavaFileObject stringProvider = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesProvider", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileProviderQualifiedName() + ";", + "public final class NotesProvider {", + annotationStrings.crossProfileProviderAsAnnotation(), + " public NotesType provideNotesType() {", + " return new NotesType();", + " }", + annotationStrings.crossProfileProviderAsAnnotation(), + " public String provideString() {", + " return \"Test\";", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(stringProvider, annotatedNotesCrossProfileType(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining( + formatErrorMessage(PROVIDING_NON_CROSS_PROFILE_TYPE_ERROR, annotationStrings)) + .inFile(stringProvider); + } + + @Test + public void takesContextArgument_compiles() { + final JavaFileObject validNotesProvider = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesProvider", + "package " + NOTES_PACKAGE + ";", + "import android.content.Context;", + "import " + annotationStrings.crossProfileProviderQualifiedName() + ";", + "public final class NotesProvider {", + annotationStrings.crossProfileProviderAsAnnotation(), + " public NotesType provideNotesType(Context context) {", + " return new NotesType();", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(validNotesProvider, annotatedNotesCrossProfileType(annotationStrings)); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void takesNonContextArgument_hasError() { + final JavaFileObject notesProviderTakesNonContextArgument = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesProvider", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileProviderQualifiedName() + ";", + "public final class NotesProvider {", + annotationStrings.crossProfileProviderAsAnnotation(), + " public NotesType provideNotesType(String s) {", + " return new NotesType();", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + notesProviderTakesNonContextArgument, + annotatedNotesCrossProfileType(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(PROVIDER_INCORRECT_ARGS_ERROR, annotationStrings)) + .inFile(notesProviderTakesNonContextArgument); + } + + @Test + public void takesMultipleContextArguments_hasError() { + final JavaFileObject notesProviderTakesMultipleContextArguments = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesProvider", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileProviderQualifiedName() + ";", + "public final class NotesProvider {", + annotationStrings.crossProfileProviderAsAnnotation(), + " public NotesType provideNotesType(Context c1, Context c2) {", + " return new NotesType();", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + notesProviderTakesMultipleContextArguments, + annotatedNotesCrossProfileType(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(PROVIDER_INCORRECT_ARGS_ERROR, annotationStrings)) + .inFile(notesProviderTakesMultipleContextArguments); + } + + @Test + public void isStaticMethod_hasError() { + final JavaFileObject staticNotesProvider = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesProvider", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileProviderQualifiedName() + ";", + "public final class NotesProvider {", + annotationStrings.crossProfileProviderAsAnnotation(), + " public static NotesType provideNotesType() {", + " return new NotesType();", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(staticNotesProvider, annotatedNotesCrossProfileType(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(STATIC_PROVIDER_ERROR, annotationStrings)) + .inFile(staticNotesProvider); + } + + @Test + public void providesSameTypeTwice_hasError() { + final JavaFileObject staticNotesProvider = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesProvider", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileProviderQualifiedName() + ";", + "public final class NotesProvider {", + annotationStrings.crossProfileProviderAsAnnotation(), + " public NotesType provideNotesType() {", + " return new NotesType();", + " }", + annotationStrings.crossProfileProviderAsAnnotation(), + " public NotesType provideNotesType2() {", + " return new NotesType();", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(staticNotesProvider, annotatedNotesCrossProfileType(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(MULTIPLE_PROVIDERS_ERROR, annotationStrings)) + .inFile(staticNotesProvider); + } + + @Test + public void providesSameTypeTwiceInDifferentProviders_hasError() { + final JavaFileObject notesProvider1 = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesProvider1", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileProviderQualifiedName() + ";", + "public final class NotesProvider1 {", + annotationStrings.crossProfileProviderAsAnnotation(), + " public NotesType provideNotesType() {", + " return new NotesType();", + " }", + "}"); + final JavaFileObject notesProvider2 = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesProvider2", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileProviderQualifiedName() + ";", + "public final class NotesProvider2 {", + annotationStrings.crossProfileProviderAsAnnotation(), + " public NotesType provideNotesType() {", + " return new NotesType();", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + notesProvider1, notesProvider2, annotatedNotesCrossProfileType(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(MULTIPLE_PROVIDERS_ERROR, annotationStrings)) + .inFile(notesProvider1); + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(MULTIPLE_PROVIDERS_ERROR, annotationStrings)) + .inFile(notesProvider2); + } + + @Test + public void providesSameTypeTwiceInStaticAndNonStaticProviders_hasError() { + final JavaFileObject staticNotesProvider = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".StaticNotesProvider", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileProviderQualifiedName() + ";", + annotationStrings.crossProfileProviderAsAnnotation("staticTypes=StaticType.class"), + "public final class StaticNotesProvider {", + "}"); + final JavaFileObject nonStaticNotesProvider = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NonStaticNotesProvider", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileProviderQualifiedName() + ";", + "public final class NonStaticNotesProvider {", + annotationStrings.crossProfileProviderAsAnnotation(), + " public StaticType provideNotesType() {", + " return null;", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(staticNotesProvider, nonStaticNotesProvider, staticType(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(MULTIPLE_PROVIDERS_ERROR, annotationStrings)) + .inFile(staticNotesProvider); + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(MULTIPLE_PROVIDERS_ERROR, annotationStrings)) + .inFile(nonStaticNotesProvider); + } + + @Test + public void specifyStaticTypesOnMethodAnnotation_hasError() { + JavaFileObject notesProvider = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesProvider", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileProviderQualifiedName() + ";", + "import com.google.android.enterprise.connectedapps.CrossProfileConnector;", + "public final class NotesProvider {", + annotationStrings.crossProfileProviderAsAnnotation("staticTypes=NotesType.class"), + " public NotesType provideNotesType() {", + " return new NotesType();", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesProvider, annotatedNotesCrossProfileType(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(METHOD_STATICTYPES_ERROR, annotationStrings)) + .inFile(notesProvider); + } +} diff --git a/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileSupportedParameterTypeTest.java b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileSupportedParameterTypeTest.java new file mode 100644 index 0000000..0b4f7f1 --- /dev/null +++ b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileSupportedParameterTypeTest.java @@ -0,0 +1,131 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.NOTES_PACKAGE; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.PARCELABLE_OBJECT; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.SERIALIZABLE_OBJECT; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesProvider; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.combineParameters; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.installationListener; +import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.Compiler.javac; + +import com.google.android.enterprise.connectedapps.annotations.CrossProfile; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationPrinter; +import com.google.testing.compile.Compilation; +import com.google.testing.compile.JavaFileObjects; +import java.util.Arrays; +import javax.tools.JavaFileObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +/** + * Test supported parameter types for {@link CrossProfile} annotated methods. + * + * <p>This tests a single parameter of each supported type. Multiple parameters and unsupported + * types are tested in {@link CrossProfileTest}. + */ +@RunWith(Parameterized.class) +public class CrossProfileSupportedParameterTypeTest { + + @Parameters(name = "{0} for {1}") + public static Iterable<Object[]> data() { + String[] types = { + "String", + "String[]", + "byte", + "Byte", + "short", + "Short", + "int", + "Integer", + "long", + "Long", + "float", + "Float", + "double", + "Double", + "char", + "Character", + "boolean", + "Boolean", + "ParcelableObject", + "ParcelableObject[]", + "java.util.List<ParcelableObject>", + "java.util.List<ParcelableObject[]>", + "java.util.List<java.util.List<ParcelableObject>>", + "SerializableObject", + "SerializableObject[]", + "java.util.List<SerializableObject>", + "java.util.List<SerializableObject[]>", + "java.util.List<java.util.List<SerializableObject>>", + "java.util.List<String>", + "java.util.List<String[]>", + "java.util.Map<String, String>", + "java.util.Set<String>", + "java.util.Collection<String>", + "java.util.Optional<String>", + "com.google.protos.connectedappssdk.TestProtoOuterClass.TestProto", + "com.google.protos.connectedappssdk.TestProtoOuterClass.TestProto[]", + "java.util.List<com.google.protos.connectedappssdk.TestProtoOuterClass.TestProto>", + "InstallationListener", + "com.google.common.collect.ImmutableMap<String, String>", + "android.util.Pair<String, Integer>", + "android.graphics.Bitmap", + "android.content.Context" + }; + return combineParameters(AnnotationFinder.annotationStrings(), Arrays.asList(types)); + } + + private final AnnotationPrinter annotationPrinter; + + private final String type; + + public CrossProfileSupportedParameterTypeTest(AnnotationPrinter annotationPrinter, String type) { + this.annotationPrinter = annotationPrinter; + this.type = type; + } + + @Test + public void crossProfileMethodWithSupportedParameterType_compiles() { + JavaFileObject crossProfileMethodWithSupportedParameterType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationPrinter.crossProfileAsAnnotation(), + " public void refreshNotes(" + type + " a) {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + crossProfileMethodWithSupportedParameterType, + annotatedNotesProvider(annotationPrinter), + PARCELABLE_OBJECT, + SERIALIZABLE_OBJECT, + installationListener(annotationPrinter)); + + assertThat(compilation).succeededWithoutWarnings(); + } +} diff --git a/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileSupportedReturnTypeTest.java b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileSupportedReturnTypeTest.java new file mode 100644 index 0000000..9335557 --- /dev/null +++ b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileSupportedReturnTypeTest.java @@ -0,0 +1,170 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.NOTES_PACKAGE; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.PARCELABLE_OBJECT; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.SERIALIZABLE_OBJECT; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesProvider; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.combineParameters; +import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.Compiler.javac; + +import com.google.android.enterprise.connectedapps.annotations.CrossProfile; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationPrinter; +import com.google.testing.compile.Compilation; +import com.google.testing.compile.JavaFileObjects; +import java.util.Arrays; +import javax.tools.JavaFileObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +/** + * Test supported return types for {@link CrossProfile} annotated methods. + * + * <p>This does not test the {@code void} return type as that does not return anything. This is + * tested in {@link CrossProfileTest}. + */ +@RunWith(Parameterized.class) +public class CrossProfileSupportedReturnTypeTest { + + @Parameters(name = "{0} for {1}") + public static Iterable<Object[]> data() { + TypeWithReturnValue[] typesWithReturnValues = + new TypeWithReturnValue[] { + TypeWithReturnValue.referenceType("Void"), + TypeWithReturnValue.referenceType("String"), + TypeWithReturnValue.referenceType("String[]"), + TypeWithReturnValue.primitiveType("byte", "0"), + TypeWithReturnValue.referenceType("Byte"), + TypeWithReturnValue.primitiveType("short", "0"), + TypeWithReturnValue.referenceType("Short"), + TypeWithReturnValue.primitiveType("int", "0"), + TypeWithReturnValue.referenceType("Integer"), + TypeWithReturnValue.primitiveType("long", "0"), + TypeWithReturnValue.referenceType("Long"), + TypeWithReturnValue.primitiveType("float", "0"), + TypeWithReturnValue.referenceType("Float"), + TypeWithReturnValue.primitiveType("double", "0"), + TypeWithReturnValue.referenceType("Double"), + TypeWithReturnValue.primitiveType("char", "'a'"), + TypeWithReturnValue.referenceType("Character"), + TypeWithReturnValue.primitiveType("boolean", "false"), + TypeWithReturnValue.referenceType("Boolean"), + TypeWithReturnValue.referenceType("ParcelableObject"), + TypeWithReturnValue.referenceType("ParcelableObject[]"), + TypeWithReturnValue.referenceType("java.util.List<ParcelableObject>"), + TypeWithReturnValue.referenceType("java.util.List<ParcelableObject[]>"), + TypeWithReturnValue.referenceType("java.util.List<java.util.List<ParcelableObject>>"), + TypeWithReturnValue.referenceType("SerializableObject"), + TypeWithReturnValue.referenceType("SerializableObject[]"), + TypeWithReturnValue.referenceType("java.util.List<SerializableObject>"), + TypeWithReturnValue.referenceType("java.util.List<SerializableObject[]>"), + TypeWithReturnValue.referenceType("java.util.List<java.util.List<SerializableObject>>"), + TypeWithReturnValue.referenceType("java.util.List<String>"), + TypeWithReturnValue.referenceType("java.util.List<String[]>"), + TypeWithReturnValue.referenceType("java.util.Map<String, String>"), + TypeWithReturnValue.referenceType("java.util.Set<String>"), + TypeWithReturnValue.referenceType("java.util.Collection<String>"), + TypeWithReturnValue.referenceType("java.util.Optional<String>"), + TypeWithReturnValue.referenceType( + "com.google.protos.connectedappssdk.TestProtoOuterClass.TestProto"), + TypeWithReturnValue.referenceType( + "com.google.protos.connectedappssdk.TestProtoOuterClass.TestProto[]"), + TypeWithReturnValue.referenceType( + "java.util.List<com.google.protos.connectedappssdk.TestProtoOuterClass.TestProto>"), + TypeWithReturnValue.referenceType( + "com.google.common.util.concurrent.ListenableFuture<String>"), + TypeWithReturnValue.referenceType( + "com.google.common.util.concurrent.ListenableFuture<String[]>"), + TypeWithReturnValue.referenceType( + "com.google.common.util.concurrent.ListenableFuture<com.google.protos.connectedappssdk.TestProtoOuterClass.TestProto>"), + TypeWithReturnValue.referenceType( + "com.google.common.util.concurrent.ListenableFuture<java.util.List<String>>"), + TypeWithReturnValue.referenceType( + "com.google.common.collect.ImmutableMap<String, String>"), + TypeWithReturnValue.referenceType("android.util.Pair<String, Integer>"), + TypeWithReturnValue.referenceType("com.google.common.base.Optional<ParcelableObject>"), + TypeWithReturnValue.referenceType("android.graphics.Bitmap"), + }; + return combineParameters( + AnnotationFinder.annotationStrings(), Arrays.asList(typesWithReturnValues)); + } + + private final AnnotationPrinter annotationPrinter; + + private final TypeWithReturnValue supportedReturnType; + + public CrossProfileSupportedReturnTypeTest( + AnnotationPrinter annotationPrinter, TypeWithReturnValue supportedReturnType) { + this.annotationPrinter = annotationPrinter; + this.supportedReturnType = supportedReturnType; + } + + @Test + public void crossProfileMethodWithSupportedReturnType_compiles() { + JavaFileObject crossProfileMethodWithSupportedReturnType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationPrinter.crossProfileAsAnnotation(), + " public " + supportedReturnType.type + " refreshNotes() {", + " return " + supportedReturnType.returnValue + ";", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + crossProfileMethodWithSupportedReturnType, + annotatedNotesProvider(annotationPrinter), + PARCELABLE_OBJECT, + SERIALIZABLE_OBJECT); + + assertThat(compilation).succeededWithoutWarnings(); + } + + private static class TypeWithReturnValue { + + final String type; + + final String returnValue; + + static TypeWithReturnValue primitiveType(String type, String returnValue) { + return new TypeWithReturnValue(type, returnValue); + } + + static TypeWithReturnValue referenceType(String type) { + return new TypeWithReturnValue(type, "null"); + } + + private TypeWithReturnValue(String type, String returnValue) { + this.type = type; + this.returnValue = returnValue; + } + + @Override + public String toString() { + return type; + } + } +} diff --git a/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileTest.java b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileTest.java new file mode 100644 index 0000000..eecccac --- /dev/null +++ b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileTest.java @@ -0,0 +1,1698 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.CROSS_PROFILE_QUALIFIED_NAME; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.CUSTOM_PROFILE_CONNECTOR_QUALIFIED_NAME; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.CUSTOM_WRAPPER; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.GENERIC_PARCELABLE_OBJECT; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.GENERIC_SERIALIZABLE_OBJECT; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.INSTALLATION_LISTENER_NAME; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.NOTES_PACKAGE; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.PARCELABLE_CUSTOM_WRAPPER; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.PROFILE_CONNECTOR_QUALIFIED_NAME; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.SIMPLE_FUTURE; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.SIMPLE_FUTURE_WRAPPER; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.UNSUPPORTED_TYPE; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.UNSUPPORTED_TYPE_NAME; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesCrossProfileType; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesProvider; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.formatErrorMessage; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.installationListener; +import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.Compiler.javac; + +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationStrings; +import com.google.testing.compile.Compilation; +import com.google.testing.compile.JavaFileObjects; +import javax.tools.JavaFileObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class CrossProfileTest { + + private static final String UNSUPPORTED_RETURN_TYPE_ERROR = + "cannot be returned by methods annotated @CROSS_PROFILE_ANNOTATION"; + private static final String UNSUPPORTED_PARAMETER_TYPE_ERROR = + "cannot be used by parameters of methods annotated @CROSS_PROFILE_ANNOTATION"; + private static final String MULTIPLE_ASYNC_CALLBACK_PARAMETERS_ERROR = + "Methods annotated @CROSS_PROFILE_ANNOTATION can have a maximum of one parameter of a type" + + " annotated @CROSS_PROFILE_CALLBACK_ANNOTATION"; + private static final String NON_VOID_CALLBACK_ERROR = + "Methods annotated @CROSS_PROFILE_ANNOTATION which take a parameter type annotated" + + " @CROSS_PROFILE_CALLBACK_ANNOTATION must return void"; + private static final String METHOD_ISSTATIC_ERROR = + "@CROSS_PROFILE_ANNOTATION annotations on methods can not specify isStatic"; + private static final String METHOD_CONNECTOR_ERROR = + "@CROSS_PROFILE_ANNOTATION annotations on methods can not specify a connector"; + private static final String METHOD_PARCELABLE_WRAPPERS_ERROR = + "@CROSS_PROFILE_ANNOTATION annotations on methods can not specify parcelable wrappers"; + private static final String METHOD_CLASSNAME_ERROR = + "@CROSS_PROFILE_ANNOTATION annotations on methods can not specify a profile class name"; + private static final String INVALID_TIMEOUT_MILLIS = "timeoutMillis must be positive"; + private static final String ASYNC_DECLARED_EXCEPTION_ERROR = + "Asynchronous methods annotated @CROSS_PROFILE_ANNOTATION cannot declare exceptions"; + private static final String PARCELABLE_WRAPPER_ANNOTATION_ERROR = + "Parcelable Wrappers must be annotated @CustomParcelableWrapper"; + private static final String FUTURE_WRAPPER_ANNOTATION_ERROR = + "Future Wrappers must be annotated @CustomFutureWrapper"; + + private final AnnotationStrings annotationStrings; + + public CrossProfileTest(AnnotationStrings annotationStrings) { + this.annotationStrings = annotationStrings; + } + + @Parameters(name = "{0}") + public static Iterable<AnnotationStrings> getAnnotationPrinters() { + return AnnotationFinder.annotationStrings(); + } + + @Test + public void validCrossProfileAnnotation_compiles() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesProvider(annotationStrings), + annotatedNotesCrossProfileType(annotationStrings)); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void staticCrossProfileMethod_compiles() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public static void refreshNotes() {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationStrings)); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void crossProfileMethodWithUnsupportedReturnType_hasError() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public " + UNSUPPORTED_TYPE_NAME + " refreshNotes() {", + " return null;", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationStrings), UNSUPPORTED_TYPE); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(UNSUPPORTED_RETURN_TYPE_ERROR, annotationStrings)) + .inFile(notesType); + } + + @Test + public void crossProfileMethodWithContextReturnType_hasError() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "import android.content.Context;", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public Context refreshNotes() {", + " return null;", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationStrings), UNSUPPORTED_TYPE); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(UNSUPPORTED_RETURN_TYPE_ERROR, annotationStrings)) + .inFile(notesType); + } + + @Test + public void crossProfileMethodWithUnsupportedReturnTypeArray_hasError() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public " + UNSUPPORTED_TYPE_NAME + "[] refreshNotes() {", + " return null;", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationStrings), UNSUPPORTED_TYPE); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(UNSUPPORTED_RETURN_TYPE_ERROR, annotationStrings)) + .inFile(notesType); + } + + @Test + public void crossProfileMethodWithUnsupportedReturnTypeInGeneric_hasError() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public java.util.List<" + UNSUPPORTED_TYPE_NAME + "> refreshNotes() {", + " return null;", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationStrings), UNSUPPORTED_TYPE); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(UNSUPPORTED_RETURN_TYPE_ERROR, annotationStrings)) + .inFile(notesType); + } + + @Test + public void crossProfileMethodWithContextReturnTypeInGeneric_hasError() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "import android.content.Context;", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public java.util.List<Context> refreshNotes() {", + " return null;", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationStrings), UNSUPPORTED_TYPE); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(UNSUPPORTED_RETURN_TYPE_ERROR, annotationStrings)) + .inFile(notesType); + } + + @Test + public void crossProfileMethodWithUnsupportedReturnTypeInGenericParcelable_compiles() { + // Parcelables take responsibility for their generics so we don't validate them + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public GenericParcelableObject<" + UNSUPPORTED_TYPE_NAME + "> refreshNotes() {", + " return null;", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + notesType, + annotatedNotesProvider(annotationStrings), + UNSUPPORTED_TYPE, + GENERIC_PARCELABLE_OBJECT); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void crossProfileMethodWithUnsupportedReturnTypeInGenericSerializable_compiles() { + // Serializables take responsibility for their generics so we don't validate them + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public GenericSerializableObject<" + UNSUPPORTED_TYPE_NAME + "> refreshNotes() {", + " return null;", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + notesType, + annotatedNotesProvider(annotationStrings), + UNSUPPORTED_TYPE, + GENERIC_SERIALIZABLE_OBJECT); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void crossProfileMethodWithVoidReturnType_compiles() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes() {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationStrings)); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void crossProfileMethodWithCrossProfileCallbackInterfaceReturnType_hasError() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public " + INSTALLATION_LISTENER_NAME + " refreshNotes() {", + " return null;", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + notesType, + annotatedNotesProvider(annotationStrings), + installationListener(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(UNSUPPORTED_RETURN_TYPE_ERROR, annotationStrings)) + .inFile(notesType); + } + + @Test + public void crossProfileMethodWithCrossProfileCallbackInterfaceArrayReturnType_hasError() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public " + INSTALLATION_LISTENER_NAME + "[] refreshNotes() {", + " return null;", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + notesType, + annotatedNotesProvider(annotationStrings), + installationListener(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(UNSUPPORTED_RETURN_TYPE_ERROR, annotationStrings)) + .inFile(notesType); + } + + @Test + public void crossProfileMethodWithMultipleSupportedParameters_compiles() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes(String s, String t) {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationStrings)); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void crossProfileMethodWithUnsupportedParameterType_hasError() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes(" + UNSUPPORTED_TYPE_NAME + " s) {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationStrings), UNSUPPORTED_TYPE); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(UNSUPPORTED_PARAMETER_TYPE_ERROR, annotationStrings)) + .inFile(notesType); + } + + @Test + public void crossProfileMethodWithUnsupportedArrayParameterType_hasError() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes(" + UNSUPPORTED_TYPE_NAME + "[] s) {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationStrings), UNSUPPORTED_TYPE); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(UNSUPPORTED_PARAMETER_TYPE_ERROR, annotationStrings)) + .inFile(notesType); + } + + @Test + public void crossProfileMethodWithUnsupportedParameterTypeInGeneric_hasError() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes(java.util.List<" + UNSUPPORTED_TYPE_NAME + "> s) {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationStrings), UNSUPPORTED_TYPE); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(UNSUPPORTED_PARAMETER_TYPE_ERROR, annotationStrings)) + .inFile(notesType); + } + + @Test + public void crossProfileMethodWithCrossProfileCallbackListenerInGeneric_hasError() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes(java.util.List<" + INSTALLATION_LISTENER_NAME + "> s) {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + notesType, + annotatedNotesProvider(annotationStrings), + installationListener(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(UNSUPPORTED_PARAMETER_TYPE_ERROR, annotationStrings)) + .inFile(notesType); + } + + @Test + public void crossProfileMethodWithCrossProfileCallbackListenerInArray_hasError() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes(" + INSTALLATION_LISTENER_NAME + "[] s) {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + notesType, + annotatedNotesProvider(annotationStrings), + installationListener(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(UNSUPPORTED_PARAMETER_TYPE_ERROR, annotationStrings)) + .inFile(notesType); + } + + @Test + public void crossProfileMethodWithMultipleCrossProfileCallbackListenerParameters_hasError() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes(" + + INSTALLATION_LISTENER_NAME + + " a, " + + INSTALLATION_LISTENER_NAME + + " b) {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + notesType, + annotatedNotesProvider(annotationStrings), + installationListener(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining( + formatErrorMessage(MULTIPLE_ASYNC_CALLBACK_PARAMETERS_ERROR, annotationStrings)) + .inFile(notesType); + } + + @Test + public void crossProfileMethodWithUnsupportedParameterTypeInGenericParcelable_compiles() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes(GenericParcelableObject<" + + UNSUPPORTED_TYPE_NAME + + "> s) {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + notesType, + annotatedNotesProvider(annotationStrings), + UNSUPPORTED_TYPE, + GENERIC_PARCELABLE_OBJECT); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void crossProfileMethodWithUnsupportedParameterTypeInGenericSerializable_compiles() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes(GenericSerializableObject<" + + UNSUPPORTED_TYPE_NAME + + "> s) {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + notesType, + annotatedNotesProvider(annotationStrings), + UNSUPPORTED_TYPE, + GENERIC_SERIALIZABLE_OBJECT); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void crossProfileMethodWithCrossProfileCallbackListenerInGenericParcelable_compiles() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes(GenericParcelableObject<" + + INSTALLATION_LISTENER_NAME + + "> s) {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + notesType, + annotatedNotesProvider(annotationStrings), + installationListener(annotationStrings), + GENERIC_PARCELABLE_OBJECT); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void crossProfileMethodWithCrossProfileCallbackListenerInGenericSerializable_compiles() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes(GenericSerializableObject<" + + INSTALLATION_LISTENER_NAME + + "> s) {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + notesType, + annotatedNotesProvider(annotationStrings), + installationListener(annotationStrings), + GENERIC_SERIALIZABLE_OBJECT); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void crossProfileMethodWithListenableFutureParameter_hasError() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "import com.google.common.util.concurrent.ListenableFuture;", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes(ListenableFuture<String> s) {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(UNSUPPORTED_PARAMETER_TYPE_ERROR, annotationStrings)) + .inFile(notesType); + } + + @Test + public void crossProfileMethodWithListenableFutureArrayParameter_hasError() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "import com.google.common.util.concurrent.ListenableFuture;", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes(ListenableFuture<String>[] s) {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(UNSUPPORTED_PARAMETER_TYPE_ERROR, annotationStrings)) + .inFile(notesType); + } + + @Test + public void crossProfileMethodWithListenableFutureInParcelableWrapperParameter_hasError() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "import com.google.common.util.concurrent.ListenableFuture;", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes(java.util.List<ListenableFuture<String>> s) {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(UNSUPPORTED_PARAMETER_TYPE_ERROR, annotationStrings)) + .inFile(notesType); + } + + @Test + public void crossProfileMethodWithListenableFutureInParcelableWrapperReturnType_hasError() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "import com.google.common.util.concurrent.ListenableFuture;", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public java.util.List<ListenableFuture<String>> refreshNotes() {", + " return null;", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(UNSUPPORTED_RETURN_TYPE_ERROR, annotationStrings)) + .inFile(notesType); + } + + @Test + public void crossProfileMethodWithListenableFutureReturnWithUnsupportedGenericType_hasError() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "import com.google.common.util.concurrent.ListenableFuture;", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public ListenableFuture<" + UNSUPPORTED_TYPE_NAME + "> refreshNotes() {", + " return null;", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationStrings), UNSUPPORTED_TYPE); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(UNSUPPORTED_RETURN_TYPE_ERROR, annotationStrings)) + .inFile(notesType); + } + + @Test + public void crossProfileMethodWithNonVoidReturnAndCrossProfileCallbackParameter_hasError() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "import com.google.common.util.concurrent.ListenableFuture;", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public String refreshNotes(" + INSTALLATION_LISTENER_NAME + " a) {", + " return null;", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + notesType, + annotatedNotesProvider(annotationStrings), + installationListener(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(NON_VOID_CALLBACK_ERROR, annotationStrings)) + .inFile(notesType); + } + + @Test + public void crossProfileMethodWithListenableFutureInGenericParcelable_compiles() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "import com.google.common.util.concurrent.ListenableFuture;", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes(GenericParcelableObject<ListenableFuture<String>> s) {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + notesType, annotatedNotesProvider(annotationStrings), GENERIC_PARCELABLE_OBJECT); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void crossProfileMethodWithListenableFutureInGenericSerializable_compiles() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "import com.google.common.util.concurrent.ListenableFuture;", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes(GenericSerializableObject<ListenableFuture<String>> s) {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + notesType, annotatedNotesProvider(annotationStrings), GENERIC_SERIALIZABLE_OBJECT); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void crossProfileMethodWithGenericArrayParameter_hasError() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes(java.util.Collection<String>[] s) {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(UNSUPPORTED_PARAMETER_TYPE_ERROR, annotationStrings)) + .inFile(notesType); + } + + @Test + public void crossProfileMethodWithGenericArrayReturnType_hasError() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public java.util.Collection<String>[] refreshNotes() {", + " return null;", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(UNSUPPORTED_RETURN_TYPE_ERROR, annotationStrings)) + .inFile(notesType); + } + + @Test + public void crossProfileMethodWithPrimitiveArrayParameterType_hasError() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes(int[] i) {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(UNSUPPORTED_PARAMETER_TYPE_ERROR, annotationStrings)) + .inFile(notesType); + } + + @Test + public void crossProfileMethodWithPrimitiveArrayReturnType_hasError() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public int[] refreshNotes() {", + " return null;", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(UNSUPPORTED_RETURN_TYPE_ERROR, annotationStrings)) + .inFile(notesType); + } + + @Test + public void crossProfileMethodWithMultiDimensionalArrayParameterType_hasError() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes(java.lang.String[][] s) {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(UNSUPPORTED_PARAMETER_TYPE_ERROR, annotationStrings)) + .inFile(notesType); + } + + @Test + public void crossProfileMethodWithMultiDimensionalArrayReturnType_hasError() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public java.lang.String[][] refreshNotes() {", + " return null;", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(UNSUPPORTED_RETURN_TYPE_ERROR, annotationStrings)) + .inFile(notesType); + } + + @Test + public void specifyConnectorOnMethodAnnotation_hasError() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "import com.google.android.enterprise.connectedapps.CrossProfileConnector;", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation("connector=CrossProfileConnector.class"), + " public void refreshNotes() {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(METHOD_CONNECTOR_ERROR, annotationStrings)) + .inFile(notesType); + } + + @Test + public void specifyParcelableWrappersOnMethodAnnotation_hasError() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation( + "parcelableWrappers=ParcelableCustomWrapper.class"), + " public void refreshNotes() {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + notesType, + annotatedNotesProvider(annotationStrings), + PARCELABLE_CUSTOM_WRAPPER, + CUSTOM_WRAPPER); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(METHOD_PARCELABLE_WRAPPERS_ERROR, annotationStrings)) + .inFile(notesType); + } + + @Test + public void specifyProfileClassNameOnMethodAnnotation_hasError() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation( + "profileClassName=\"" + NOTES_PACKAGE + ".ProfileNotes\""), + " public void refreshNotes() {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(METHOD_CLASSNAME_ERROR, annotationStrings)) + .inFile(notesType); + } + + @Test + public void crossProfileInterface_works() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesInterface", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public interface NotesInterface {", + annotationStrings.crossProfileAsAnnotation(), + " void refreshNotes();", + "}"); + JavaFileObject providerClass = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesProvider", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileProviderQualifiedName() + ";", + "public final class NotesProvider {", + annotationStrings.crossProfileProviderAsAnnotation(), + " public NotesInterface provideNotesInterface() {", + " return null;", + " }", + "}"); + + Compilation compilation = + javac().withProcessors(new Processor()).compile(notesType, providerClass); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void crossProfile_specifiesValidTimeoutMillisAndAlsoOnType_compiles() { + JavaFileObject crossProfileType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + annotationStrings.crossProfileAsAnnotation("timeoutMillis=30"), + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation("timeoutMillis=10"), + " public void refreshNotes() {", + " }", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(crossProfileType); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void crossProfile_specifiesValidTimeoutMillis_compiles() { + JavaFileObject crossProfileType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation("timeoutMillis=10"), + " public void refreshNotes() {", + " }", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(crossProfileType); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void crossProfile_specifiesNegativeTimeoutMillis_hasError() { + JavaFileObject crossProfileType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation("timeoutMillis=-10"), + " public void refreshNotes() {", + " }", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(crossProfileType); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(INVALID_TIMEOUT_MILLIS, annotationStrings)) + .inFile(crossProfileType); + } + + @Test + public void crossProfileType_specifiesZeroTimeoutMillis_hasError() { + JavaFileObject crossProfileType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation("timeoutMillis=0"), + " public void refreshNotes() {", + " }", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(crossProfileType); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(INVALID_TIMEOUT_MILLIS, annotationStrings)) + .inFile(crossProfileType); + } + + @Test + public void crossProfileMethod_synchronous_declaresException_compiles() { + JavaFileObject crossProfileType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "import java.io.IOException;", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes() throws IOException {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(crossProfileType, installationListener(annotationStrings)); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void crossProfileMethod_async_declaresException_hasError() { + JavaFileObject crossProfileType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "import java.io.IOException;", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes(" + + INSTALLATION_LISTENER_NAME + + " callback) throws IOException {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(crossProfileType, installationListener(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(ASYNC_DECLARED_EXCEPTION_ERROR, annotationStrings)) + .inFile(crossProfileType); + } + + @Test + public void crossProfileMethod_future_declaresException_hasError() { + JavaFileObject crossProfileType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "import java.io.IOException;", + "import com.google.common.util.concurrent.ListenableFuture;", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public ListenableFuture<String> refreshNotes() throws IOException {", + " return null;", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(crossProfileType, installationListener(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(ASYNC_DECLARED_EXCEPTION_ERROR, annotationStrings)) + .inFile(crossProfileType); + } + + @Test + public void crossProfileMethod_returnsCustomParcelableType_compiles() { + JavaFileObject crossProfileType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + annotationStrings.crossProfileAsAnnotation( + "connector=CrossProfileConnectorWhichSupportsCustomWrapper.class"), + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public CustomWrapper<String> refreshNotes() {", + " return null;", + " }", + "}"); + + JavaFileObject connector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".CrossProfileConnectorWhichSupportsCustomWrapper", + "package " + NOTES_PACKAGE + ";", + "import " + PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "import " + CUSTOM_PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "@CustomProfileConnector(parcelableWrappers={ParcelableCustomWrapper.class})", + "public interface CrossProfileConnectorWhichSupportsCustomWrapper extends" + + " ProfileConnector {", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(crossProfileType, connector, CUSTOM_WRAPPER, PARCELABLE_CUSTOM_WRAPPER); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void crossProfileMethod_takesCustomParcelableTypeArgument_compiles() { + JavaFileObject crossProfileType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + annotationStrings.crossProfileAsAnnotation( + "connector=CrossProfileConnectorWhichSupportsCustomWrapper.class"), + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes(CustomWrapper<String> a) {", + " }", + "}"); + + JavaFileObject connector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".CrossProfileConnectorWhichSupportsCustomWrapper", + "package " + NOTES_PACKAGE + ";", + "import " + PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "import " + CUSTOM_PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "@CustomProfileConnector(parcelableWrappers={ParcelableCustomWrapper.class})", + "public interface CrossProfileConnectorWhichSupportsCustomWrapper extends" + + " ProfileConnector {", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(crossProfileType, connector, CUSTOM_WRAPPER, PARCELABLE_CUSTOM_WRAPPER); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void crossProfileMethod_usesCustomParcelableTypeFromDifferentConnector_hasError() { + JavaFileObject crossProfileType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + annotationStrings.crossProfileAsAnnotation( + "connector=CrossProfileConnectorWhichSupportsCustomWrapper.class"), + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public CustomWrapper<String> refreshNotes() {", + " return null;", + " }", + "}"); + + JavaFileObject secondCrossProfileType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType2", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType2 {", + annotationStrings.crossProfileAsAnnotation(), + " public CustomWrapper<String> refreshNotes() {", + " return null;", + " }", + "}"); + + JavaFileObject connector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".CrossProfileConnectorWhichSupportsCustomWrapper", + "package " + NOTES_PACKAGE + ";", + "import " + PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "import " + CUSTOM_PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "@CustomProfileConnector(parcelableWrappers={ParcelableCustomWrapper.class})", + "public interface CrossProfileConnectorWhichSupportsCustomWrapper extends" + + " ProfileConnector {", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + crossProfileType, + secondCrossProfileType, + connector, + CUSTOM_WRAPPER, + PARCELABLE_CUSTOM_WRAPPER); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(UNSUPPORTED_RETURN_TYPE_ERROR, annotationStrings)) + .inFile(secondCrossProfileType); + } + + @Test + public void crossProfileMethod_returnsCustomParcelableTypeForCrossProfileType_compiles() { + JavaFileObject crossProfileType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + annotationStrings.crossProfileAsAnnotation( + "parcelableWrappers=ParcelableCustomWrapper.class"), + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public CustomWrapper<String> refreshNotes() {", + " return null;", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(crossProfileType, CUSTOM_WRAPPER, PARCELABLE_CUSTOM_WRAPPER); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void crossProfileMethod_takesCustomParcelableTypeForCrossProfileTypeAsArgument_compiles() { + JavaFileObject crossProfileType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + annotationStrings.crossProfileAsAnnotation( + "parcelableWrappers=ParcelableCustomWrapper.class"), + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes(CustomWrapper<String> a) {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(crossProfileType, CUSTOM_WRAPPER, PARCELABLE_CUSTOM_WRAPPER); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void crossProfileType_includesNonParcelableWrapperInParcelableWrappers_hasError() { + JavaFileObject crossProfileType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + annotationStrings.crossProfileAsAnnotation("parcelableWrappers=String.class"), + "public final class NotesType {", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(crossProfileType); + + assertThat(compilation) + .hadErrorContaining( + formatErrorMessage(PARCELABLE_WRAPPER_ANNOTATION_ERROR, annotationStrings)) + .inFile(crossProfileType); + } + + @Test + public void crossProfileMethod_returnsCustomFutureType_compiles() { + JavaFileObject crossProfileType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + annotationStrings.crossProfileAsAnnotation( + "connector=CrossProfileConnectorWhichSupportsSimpleFuture.class"), + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public SimpleFuture<String> refreshNotes() {", + " return null;", + " }", + "}"); + + JavaFileObject connector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".CrossProfileConnectorWhichSupportsSimpleFuture", + "package " + NOTES_PACKAGE + ";", + "import " + PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "import " + CUSTOM_PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "@CustomProfileConnector(futureWrappers={SimpleFutureWrapper.class})", + "public interface CrossProfileConnectorWhichSupportsSimpleFuture extends" + + " ProfileConnector {", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(crossProfileType, connector, SIMPLE_FUTURE, SIMPLE_FUTURE_WRAPPER); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void crossProfileMethod_usesCustomFutureTypeFromDifferentConnector_hasError() { + JavaFileObject crossProfileType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + annotationStrings.crossProfileAsAnnotation( + "connector=CrossProfileConnectorWhichSupportsSimpleFuture.class"), + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public SimpleFuture<String> refreshNotes() {", + " return null;", + " }", + "}"); + + JavaFileObject secondCrossProfileType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType2", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType2 {", + annotationStrings.crossProfileAsAnnotation(), + " public SimpleFuture<String> refreshNotes() {", + " return null;", + " }", + "}"); + + JavaFileObject connector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".CrossProfileConnectorWhichSupportsSimpleFuture", + "package " + NOTES_PACKAGE + ";", + "import " + PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "import " + CUSTOM_PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "@CustomProfileConnector(futureWrappers={SimpleFutureWrapper.class})", + "public interface CrossProfileConnectorWhichSupportsSimpleFuture extends" + + " ProfileConnector {", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + crossProfileType, + secondCrossProfileType, + connector, + SIMPLE_FUTURE, + SIMPLE_FUTURE_WRAPPER); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(UNSUPPORTED_RETURN_TYPE_ERROR, annotationStrings)) + .inFile(secondCrossProfileType); + } + + @Test + public void crossProfileMethod_usesCustomFutureTypeImportedFromDifferentConnector_compiles() { + JavaFileObject crossProfileType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + CROSS_PROFILE_QUALIFIED_NAME + ";", + "@CrossProfile(connector=CrossProfileConnectorWithImport.class)", + "public final class NotesType {", + " @CrossProfile", + " public SimpleFuture<String> refreshNotes() {", + " return null;", + " }", + "}"); + JavaFileObject connector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".CrossProfileConnectorWhichSupportsSimpleFuture", + "package " + NOTES_PACKAGE + ";", + "import " + PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "import " + CUSTOM_PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "@CustomProfileConnector(futureWrappers={SimpleFutureWrapper.class})", + "public interface CrossProfileConnectorWhichSupportsSimpleFuture extends" + + " ProfileConnector {", + "}"); + JavaFileObject connectorWithImport = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".CrossProfileConnectorWithImport", + "package " + NOTES_PACKAGE + ";", + "import " + PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "import " + CUSTOM_PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "@CustomProfileConnector(imports=CrossProfileConnectorWhichSupportsSimpleFuture.class)", + "public interface CrossProfileConnectorWithImport extends" + + " ProfileConnector {", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + crossProfileType, + connector, + connectorWithImport, + SIMPLE_FUTURE, + SIMPLE_FUTURE_WRAPPER); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void crossProfileMethod_usesCustomFutureTypeImportedIndirectly_compiles() { + JavaFileObject crossProfileType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + CROSS_PROFILE_QUALIFIED_NAME + ";", + "@CrossProfile(connector=CrossProfileConnectorWithImportOfImport.class)", + "public final class NotesType {", + " @CrossProfile", + " public SimpleFuture<String> refreshNotes() {", + " return null;", + " }", + "}"); + JavaFileObject connector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".CrossProfileConnectorWhichSupportsSimpleFuture", + "package " + NOTES_PACKAGE + ";", + "import " + PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "import " + CUSTOM_PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "@CustomProfileConnector(futureWrappers={SimpleFutureWrapper.class})", + "public interface CrossProfileConnectorWhichSupportsSimpleFuture extends" + + " ProfileConnector {", + "}"); + JavaFileObject connectorWithImport = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".CrossProfileConnectorWithImport", + "package " + NOTES_PACKAGE + ";", + "import " + PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "import " + CUSTOM_PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "@CustomProfileConnector(imports=CrossProfileConnectorWhichSupportsSimpleFuture.class)", + "public interface CrossProfileConnectorWithImport extends" + + " ProfileConnector {", + "}"); + JavaFileObject connectorWithImportOfImport = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".CrossProfileConnectorWithImportOfImport", + "package " + NOTES_PACKAGE + ";", + "import " + PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "import " + CUSTOM_PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "@CustomProfileConnector(imports=CrossProfileConnectorWithImport.class)", + "public interface CrossProfileConnectorWithImportOfImport extends" + + " ProfileConnector {", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + crossProfileType, + connector, + connectorWithImport, + connectorWithImportOfImport, + SIMPLE_FUTURE, + SIMPLE_FUTURE_WRAPPER); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void crossProfileMethod_usesCustomFutureTypeForCrossProfileType_compiles() { + JavaFileObject crossProfileType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + annotationStrings.crossProfileAsAnnotation("futureWrappers=SimpleFutureWrapper.class"), + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public SimpleFuture<String> refreshNotes() {", + " return null;", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + crossProfileType, + SIMPLE_FUTURE, + SIMPLE_FUTURE_WRAPPER, + annotatedNotesProvider(annotationStrings)); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void crossProfileType_includesNonFutureWrapperInFutureWrappers_hasError() { + JavaFileObject crossProfileType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + annotationStrings.crossProfileAsAnnotation("futureWrappers=String.class"), + "public final class NotesType {", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(crossProfileType); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(FUTURE_WRAPPER_ANNOTATION_ERROR, annotationStrings)) + .inFile(crossProfileType); + } + + @Test + public void specifyIsStaticOnMethodAnnotation_hasError() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation("isStatic=true"), + " public void refreshNotes() {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(METHOD_ISSTATIC_ERROR, annotationStrings)) + .inFile(notesType); + } +} diff --git a/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileTestTest.java b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileTestTest.java new file mode 100644 index 0000000..0050a6e --- /dev/null +++ b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileTestTest.java @@ -0,0 +1,225 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.CUSTOM_PROFILE_CONNECTOR_QUALIFIED_NAME; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.NOTES_PACKAGE; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.PROFILE_CONNECTOR_QUALIFIED_NAME; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesConfigurationWithNotesProvider; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesProvider; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.formatErrorMessage; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.notesTypeWithDefaultConnector; +import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.Compiler.javac; + +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationStrings; +import com.google.testing.compile.Compilation; +import com.google.testing.compile.JavaFileObjects; +import javax.tools.JavaFileObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class CrossProfileTestTest { + + private static final String NOT_A_CONFIGURATION_ERROR = + "Configurations referenced in a @CROSS_PROFILE_TEST_ANNOTATION annotation must be annotated" + + " @CROSS_PROFILE_CONFIGURATION_ANNOTATION or @CROSS_PROFILE_CONFIGURATIONS_ANNOTATION"; + + private final AnnotationStrings annotationStrings; + + public CrossProfileTestTest(AnnotationStrings annotationStrings) { + this.annotationStrings = annotationStrings; + } + + @Parameters(name = "{0}") + public static Iterable<AnnotationStrings> getAnnotationPrinters() { + return AnnotationFinder.annotationStrings(); + } + + @Test + public void crossProfileTest_compiles() { + JavaFileObject crossProfileTest = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesTest", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileTestQualifiedName() + ";", + annotationStrings.crossProfileTestAsAnnotation( + "configuration=NotesConfiguration.class"), + "public final class NotesTest {", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + crossProfileTest, + annotatedNotesConfigurationWithNotesProvider(annotationStrings), + annotatedNotesProvider(annotationStrings), + notesTypeWithDefaultConnector(annotationStrings)); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void crossProfileTest_referencesNonConfigurationClass_hasError() { + JavaFileObject crossProfileTest = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesTest", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileTestQualifiedName() + ";", + annotationStrings.crossProfileTestAsAnnotation("configuration=String.class"), + "public final class NotesTest {", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(crossProfileTest); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(NOT_A_CONFIGURATION_ERROR, annotationStrings)) + .inFile(crossProfileTest); + } + + @Test + public void crossProfileTest_generatesFakeConnector() { + JavaFileObject crossProfileTest = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesTest", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileTestQualifiedName() + ";", + annotationStrings.crossProfileTestAsAnnotation( + "configuration=NotesConfiguration.class"), + "public final class NotesTest {", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + crossProfileTest, + annotatedNotesConfigurationWithNotesProvider(annotationStrings), + annotatedNotesProvider(annotationStrings), + notesTypeWithDefaultConnector(annotationStrings)); + + assertThat(compilation) + .generatedSourceFile( + "com.google.android.enterprise.connectedapps.FakeCrossProfileConnector"); + } + + @Test + public void crossProfileTest_fakeConnectorImplementsConnector() { + JavaFileObject crossProfileTest = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesTest", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileTestQualifiedName() + ";", + annotationStrings.crossProfileTestAsAnnotation( + "configuration=NotesConfiguration.class"), + "public final class NotesTest {", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + crossProfileTest, + annotatedNotesConfigurationWithNotesProvider(annotationStrings), + annotatedNotesProvider(annotationStrings), + notesTypeWithDefaultConnector(annotationStrings)); + + assertThat(compilation) + .generatedSourceFile( + "com.google.android.enterprise.connectedapps.FakeCrossProfileConnector") + .contentsAsUtf8String() + .contains("implements CrossProfileConnector"); + } + + @Test + public void + crossProfileTest_connectorDoesNotSpecifyPrimaryProfile_fakeConnectorHasConstructorToSpecifyProfile() { + JavaFileObject crossProfileTest = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesTest", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileTestQualifiedName() + ";", + annotationStrings.crossProfileTestAsAnnotation( + "configuration=NotesConfiguration.class"), + "public final class NotesTest {", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + crossProfileTest, + annotatedNotesConfigurationWithNotesProvider(annotationStrings), + annotatedNotesProvider(annotationStrings), + notesTypeWithDefaultConnector(annotationStrings)); + + // We can't assert multi-line methods, so we check just that we have a constructer with an + // additional param + assertThat(compilation) + .generatedSourceFile( + "com.google.android.enterprise.connectedapps.FakeCrossProfileConnector") + .contentsAsUtf8String() + .contains("public FakeCrossProfileConnector(Context context,"); + } + + @Test + public void + crossProfileTest_connectorSpecifiesPrimaryProfile_fakeConnectorDoesNotHaveConstructorToSpecifyProfile() { + JavaFileObject crossProfileTest = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesTest", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileTestQualifiedName() + ";", + annotationStrings.crossProfileTestAsAnnotation( + "configuration=NotesConfiguration.class"), + "public final class NotesTest {", + "}"); + JavaFileObject notesConfiguration = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConfiguration", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileConfigurationQualifiedName() + ";", + "import com.google.android.enterprise.connectedapps.CrossProfileConnector;", + annotationStrings.crossProfileConfigurationAsAnnotation( + "connector=NotesConnector.class"), + "public abstract class NotesConfiguration {", + "}"); + JavaFileObject notesConnector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector", + "package " + NOTES_PACKAGE + ";", + "import " + PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "import " + CUSTOM_PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "@CustomProfileConnector(primaryProfile=CustomProfileConnector.ProfileType.WORK)", + "public interface NotesConnector extends ProfileConnector {", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(crossProfileTest, notesConfiguration, notesConnector); + + assertThat(compilation) + .generatedSourceFile("com.google.android.enterprise.notes.FakeNotesConnector") + .contentsAsUtf8String() + .doesNotContain("CustomProfileConnector.ProfileType primaryProfile"); + } +} diff --git a/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileTypeTest.java b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileTypeTest.java new file mode 100644 index 0000000..cb692ff --- /dev/null +++ b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileTypeTest.java @@ -0,0 +1,363 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.ANNOTATED_NOTES_CONNECTOR; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.NOTES_PACKAGE; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.PROFILE_CONNECTOR_QUALIFIED_NAME; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesCrossProfileType; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesProvider; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.formatErrorMessage; +import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.Compiler.javac; + +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationPrinter; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationStrings; +import com.google.testing.compile.Compilation; +import com.google.testing.compile.JavaFileObjects; +import javax.tools.JavaFileObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class CrossProfileTypeTest { + + private static JavaFileObject secondAnnotatedNotesProvider(AnnotationPrinter annotationPrinter) { + return JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesProvider2", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileProviderQualifiedName() + ";", + "public final class NotesProvider2 {", + annotationPrinter.crossProfileProviderAsAnnotation(), + " public NotesType provideNotesType() {", + " return new NotesType();", + " }", + "}"); + } + + private static final String MULTIPLE_PROVIDERS_ERROR = "has been provided more than once"; + private static final String DEFAULT_PACKAGE_ERROR = + "@CROSS_PROFILE_ANNOTATION types must not be in the default package"; + private static final String NON_PUBLIC_CLASS_ERROR = + "@CROSS_PROFILE_ANNOTATION types must be public"; + private static final String CONNECTOR_MUST_EXTEND_CONNECTOR = + "Interfaces specified as a connector must extend ProfileConnector"; + private static final String INVALID_TIMEOUT_MILLIS = "timeoutMillis must be positive"; + private static final String CONNECTOR_MUST_BE_INTERFACE = "Connectors must be interfaces"; + private static final String NOT_STATIC_ERROR = + "Types annotated @CROSS_PROFILE_ANNOTATION(isStatic=true) must not contain any non-static" + + " methods annotated @CROSS_PROFILE_ANNOTATION"; + + private final AnnotationStrings annotationStrings; + + public CrossProfileTypeTest(AnnotationStrings annotationStrings) { + this.annotationStrings = annotationStrings; + } + + @Parameters(name = "{0}") + public static Iterable<AnnotationStrings> getAnnotationNames() { + return AnnotationFinder.annotationStrings(); + } + + @Test + public void crossProfileType_inDefaultPackage_hasError() { + JavaFileObject crossProfileTypeInDefaultPackage = + JavaFileObjects.forSourceLines( + "NotesType", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes() {", + " }", + "}"); + + Compilation compilation = + javac().withProcessors(new Processor()).compile(crossProfileTypeInDefaultPackage); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(DEFAULT_PACKAGE_ERROR, annotationStrings)) + .inFile(crossProfileTypeInDefaultPackage); + } + + @Test + public void crossProfileType_inNonPublicClass_hasError() { + JavaFileObject crossProfileTypeInNonPublicClass = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes() {", + " }", + "}"); + + Compilation compilation = + javac().withProcessors(new Processor()).compile(crossProfileTypeInNonPublicClass); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(NON_PUBLIC_CLASS_ERROR, annotationStrings)) + .inFile(crossProfileTypeInNonPublicClass); + } + + @Test + public void crossProfileType_doesNotSpecifyConnector_compiles() { + JavaFileObject crossProfileTypeWithoutConnector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + annotationStrings.crossProfileAsAnnotation(), + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes() {", + " }", + "}"); + + Compilation compilation = + javac().withProcessors(new Processor()).compile(crossProfileTypeWithoutConnector); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void crossProfileType_specifiesValidConnector_compiles() { + JavaFileObject crossProfileTypeWithValidConnector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + annotationStrings.crossProfileAsAnnotation("connector=NotesConnector.class"), + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes() {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(crossProfileTypeWithValidConnector, ANNOTATED_NOTES_CONNECTOR); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void crossProfileType_specifiesValidTimeoutMillis_compiles() { + JavaFileObject crossProfileType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + annotationStrings.crossProfileAsAnnotation("timeoutMillis=10"), + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes() {", + " }", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(crossProfileType); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void crossProfileType_specifiesNegativeTimeoutMillis_hasError() { + JavaFileObject crossProfileType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + annotationStrings.crossProfileAsAnnotation("timeoutMillis=-10"), + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes() {", + " }", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(crossProfileType); + + assertThat(compilation).hadErrorContaining(INVALID_TIMEOUT_MILLIS).inFile(crossProfileType); + } + + @Test + public void crossProfileType_specifiesZeroTimeoutMillis_hasError() { + JavaFileObject crossProfileType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + annotationStrings.crossProfileAsAnnotation("timeoutMillis=0"), + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes() {", + " }", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(crossProfileType); + + assertThat(compilation).hadErrorContaining(INVALID_TIMEOUT_MILLIS).inFile(crossProfileType); + } + + @Test + public void crossProfileType_specifiesNotInterfaceConnector_hasError() { + JavaFileObject crossProfileType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + annotationStrings.crossProfileAsAnnotation("connector=NotesConnector.class"), + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes() {", + " }", + "}"); + JavaFileObject notesConnector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector", + "package " + NOTES_PACKAGE + ";", + "public class NotesConnector {", + "}"); + + Compilation compilation = + javac().withProcessors(new Processor()).compile(crossProfileType, notesConnector); + + assertThat(compilation) + .hadErrorContaining(CONNECTOR_MUST_BE_INTERFACE) + .inFile(crossProfileType); + } + + @Test + public void crossProfileType_specifiesConnectorNotExtendingProfileConnector_hasError() { + JavaFileObject crossProfileType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + annotationStrings.crossProfileAsAnnotation("connector=NotesConnector.class"), + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes() {", + " }", + "}"); + JavaFileObject notesConnector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector", + "package " + NOTES_PACKAGE + ";", + "import " + PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "public interface NotesConnector {", + "}"); + + Compilation compilation = + javac().withProcessors(new Processor()).compile(crossProfileType, notesConnector); + + assertThat(compilation) + .hadErrorContaining(CONNECTOR_MUST_EXTEND_CONNECTOR) + .inFile(crossProfileType); + } + + @Test + public void multipleCrossProfileProviders_hasError() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesCrossProfileType(annotationStrings), + annotatedNotesProvider(annotationStrings), + secondAnnotatedNotesProvider(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(MULTIPLE_PROVIDERS_ERROR) + .inFile(annotatedNotesProvider(annotationStrings)); + assertThat(compilation) + .hadErrorContaining(MULTIPLE_PROVIDERS_ERROR) + .inFile(secondAnnotatedNotesProvider(annotationStrings)); + } + + @Test + public void specifiesAlternativeProfileClassName_generatesCorrectClass() { + JavaFileObject crossProfileType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + annotationStrings.crossProfileAsAnnotation( + "profileClassName=\"" + NOTES_PACKAGE + ".CrossProfileNotes\""), + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes() {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(crossProfileType, annotatedNotesProvider(annotationStrings)); + + assertThat(compilation).generatedSourceFile(NOTES_PACKAGE + ".CrossProfileNotes"); + } + + @Test + public void isStaticContainsNoNonStaticMethods_compiles() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "import com.google.android.enterprise.connectedapps.CrossProfileConnector;", + annotationStrings.crossProfileAsAnnotation("isStatic=true"), + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public static void refreshNotes() {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationStrings)); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void isStaticContainsNonStaticMethods_hasError() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "import com.google.android.enterprise.connectedapps.CrossProfileConnector;", + annotationStrings.crossProfileAsAnnotation("isStatic=true"), + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes() {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationStrings)); + + assertThat(compilation) + .hadErrorContaining(formatErrorMessage(NOT_STATIC_ERROR, annotationStrings)) + .inFile(notesType); + } +} diff --git a/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CustomFutureWrapperTest.java b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CustomFutureWrapperTest.java new file mode 100644 index 0000000..e5f6008 --- /dev/null +++ b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CustomFutureWrapperTest.java @@ -0,0 +1,855 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.NOTES_PACKAGE; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.SIMPLE_FUTURE; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.SIMPLE_FUTURE_WRAPPER; +import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.Compiler.javac; + +import com.google.testing.compile.Compilation; +import com.google.testing.compile.JavaFileObjects; +import javax.tools.JavaFileObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class CustomFutureWrapperTest { + + private static final String DOES_NOT_EXTEND_FUTURE_WRAPPER_ERROR = + "Classes annotated @CustomFutureWrapper must extend FutureWrapper"; + private static final String INCORRECT_CREATE_METHOD_ERROR = + "Classes annotated @CustomFutureWrapper must have a create method which returns an instance" + + " of the class and takes a Bundler and BundlerType argument"; + private static final String INCORRECT_GET_FUTURE_METHOD_ERROR = + "Classes annotated @CustomFutureWrapper must have a getFuture method which returns an" + + " instance of the wrapped future and takes no arguments"; + private static final String INCORRECT_RESOLVE_CALLBACK_WHEN_FUTURE_IS_SET_METHOD_ERROR = + "Classes annotated @CustomFutureWrapper must have a writeFutureResult method" + + " which returns void and takes as arguments an instance of the wrapped future and a" + + " FutureResultWriter"; + private static final String INCORRECT_GROUP_RESULTS_METHOD_ERROR = + "Classes annotated @CustomFutureWrapper must have a groupResults method which returns an" + + " instance of the wrapped future containing a map from Profile to the wrapped future" + + " type, and takes as an argument a map from Profile to an instance of the wrapped" + + " future"; + private static final String MUST_HAVE_ONE_TYPE_PARAMETER_ERROR = + "Classes annotated @CustomFutureWrapper must have a single type parameter"; + + static final JavaFileObject SIMPLE_FUTURE_WRAPPER_IS_NOT_GENERIC = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".SimpleFutureWrapper", + "package " + NOTES_PACKAGE + ";", + "import com.google.android.enterprise.connectedapps.FutureWrapper;", + "import com.google.android.enterprise.connectedapps.internal.FutureResultWriter;", + "import com.google.android.enterprise.connectedapps.Profile;", + "import com.google.android.enterprise.connectedapps.internal.Bundler;", + "import com.google.android.enterprise.connectedapps.internal.BundlerType;", + "import" + + " com.google.android.enterprise.connectedapps.internal.CrossProfileCallbackMultiMerger;", + "import java.util.Map;", + "@com.google.android.enterprise.connectedapps.annotations.CustomFutureWrapper(", + "originalType = SimpleFuture.class)", + "public final class SimpleFutureWrapper extends FutureWrapper<String> {", + "private final SimpleFuture<String> future = new SimpleFuture<>();", + "public static SimpleFutureWrapper create(Bundler bundler, BundlerType" + + " bundlerType) {", + "return new SimpleFutureWrapper(bundler, bundlerType);", + "}", + "private SimpleFutureWrapper(Bundler bundler, BundlerType bundlerType) {", + "super(bundler, bundlerType);", + "}", + "public SimpleFuture<String> getFuture() {", + "return future;", + "}", + "@Override", + "public void onResult(String result) {", + "future.set(result);", + "}", + "@Override", + "public void onException(Throwable throwable) {", + "future.setException(throwable);", + "}", + "public static void writeFutureResult(", + "SimpleFuture<String> future,", + "FutureResultWriter<E> resultWriter) {", + "future.setCallback(", + "(value) -> {", + "resultWriter.onSuccess(value);", + "},", + "(exception) -> {", + "resultWriter.onFailure(exception);", + "});", + "}", + "public static SimpleFuture<Map<Profile, String>> groupResults(", + "Map<Profile, SimpleFuture<String>> results) {", + "SimpleFuture<Map<Profile, String>> m = new SimpleFuture<>();", + "CrossProfileCallbackMultiMerger<String> merger =", + "new CrossProfileCallbackMultiMerger<>(results.size(), m::set);", + "for (Map.Entry<Profile, SimpleFuture<String>> result : results.entrySet()) {", + "result", + ".getValue()", + ".setCallback(", + "(value) -> {", + "merger.onResult(result.getKey(), value);", + "},", + "(throwable) -> {", + "merger.missingResult(result.getKey());", + "});", + "}", + "return m;", + "}", + "}"); + + static final JavaFileObject SIMPLE_FUTURE_WRAPPER_MULTIPLE_TYPE_PARAMETERS = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".SimpleFutureWrapper", + "package " + NOTES_PACKAGE + ";", + "import com.google.android.enterprise.connectedapps.FutureWrapper;", + "import com.google.android.enterprise.connectedapps.internal.FutureResultWriter;", + "import com.google.android.enterprise.connectedapps.Profile;", + "import com.google.android.enterprise.connectedapps.internal.Bundler;", + "import com.google.android.enterprise.connectedapps.internal.BundlerType;", + "import" + + " com.google.android.enterprise.connectedapps.internal.CrossProfileCallbackMultiMerger;", + "import java.util.Map;", + "@com.google.android.enterprise.connectedapps.annotations.CustomFutureWrapper(", + "originalType = SimpleFuture.class)", + "public final class SimpleFutureWrapper<E, R> extends FutureWrapper<E> {", + "private final SimpleFuture<E> future = new SimpleFuture<>();", + "public static <E> SimpleFutureWrapper<E, String> create(Bundler bundler, BundlerType" + + " bundlerType) {", + "return new SimpleFutureWrapper<E, String>(bundler, bundlerType);", + "}", + "private SimpleFutureWrapper(Bundler bundler, BundlerType bundlerType) {", + "super(bundler, bundlerType);", + "}", + "public SimpleFuture<E> getFuture() {", + "return future;", + "}", + "@Override", + "public void onResult(E result) {", + "future.set(result);", + "}", + "@Override", + "public void onException(Throwable throwable) {", + "future.setException(throwable);", + "}", + "public static <E> void writeFutureResult(", + "SimpleFuture<E> future,", + "FutureResultWriter<E> resultWriter) {", + "future.setCallback(", + "(value) -> {", + "resultWriter.onSuccess(value);", + "},", + "(exception) -> {", + "resultWriter.onFailure(exception);", + "});", + "}", + "public static <E> SimpleFuture<Map<Profile, E>> groupResults(", + "Map<Profile, SimpleFuture<E>> results) {", + "SimpleFuture<Map<Profile, E>> m = new SimpleFuture<>();", + "CrossProfileCallbackMultiMerger<E> merger =", + "new CrossProfileCallbackMultiMerger<>(results.size(), m::set);", + "for (Map.Entry<Profile, SimpleFuture<E>> result : results.entrySet()) {", + "result", + ".getValue()", + ".setCallback(", + "(value) -> {", + "merger.onResult(result.getKey(), value);", + "},", + "(throwable) -> {", + "merger.missingResult(result.getKey());", + "});", + "}", + "return m;", + "}", + "}"); + + private static final JavaFileObject SIMPLE_FUTURE_WRAPPER_DOES_NOT_EXTEND_FUTURE_WRAPPER = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".SimpleFutureWrapper", + "package " + NOTES_PACKAGE + ";", + "import com.google.android.enterprise.connectedapps.FutureWrapper;", + "import com.google.android.enterprise.connectedapps.internal.FutureResultWriter;", + "import com.google.android.enterprise.connectedapps.Profile;", + "import com.google.android.enterprise.connectedapps.internal.Bundler;", + "import com.google.android.enterprise.connectedapps.internal.BundlerType;", + "import" + + " com.google.android.enterprise.connectedapps.internal.CrossProfileCallbackMultiMerger;", + "import java.util.Map;", + "@com.google.android.enterprise.connectedapps.annotations.CustomFutureWrapper(", + "originalType = SimpleFuture.class)", + "public final class SimpleFutureWrapper<E> {", + "private final SimpleFuture<E> future = new SimpleFuture<>();", + "public static <E> SimpleFutureWrapper<E> create(Bundler bundler, BundlerType" + + " bundlerType) {", + "return new SimpleFutureWrapper<>(bundler, bundlerType);", + "}", + "private SimpleFutureWrapper(Bundler bundler, BundlerType bundlerType) {", + "}", + "public SimpleFuture<E> getFuture() {", + "return future;", + "}", + "public void onResult(E result) {", + "future.set(result);", + "}", + "public void onException(Throwable throwable) {", + "future.setException(throwable);", + "}", + "public static <E> void writeFutureResult(", + "SimpleFuture<E> future,", + "FutureResultWriter<E> resultWriter) {", + "future.setCallback(", + "(value) -> {", + "resultWriter.onSuccess(value);", + "},", + "(exception) -> {", + "resultWriter.onFailure(exception);", + "});", + "}", + "public static <E> SimpleFuture<Map<Profile, E>> groupResults(", + "Map<Profile, SimpleFuture<E>> results) {", + "SimpleFuture<Map<Profile, E>> m = new SimpleFuture<>();", + "CrossProfileCallbackMultiMerger<E> merger =", + "new CrossProfileCallbackMultiMerger<>(results.size(), m::set);", + "for (Map.Entry<Profile, SimpleFuture<E>> result : results.entrySet()) {", + "result", + ".getValue()", + ".setCallback(", + "(value) -> {", + "merger.onResult(result.getKey(), value);", + "},", + "(throwable) -> {", + "merger.missingResult(result.getKey());", + "});", + "}", + "return m;", + "}", + "}"); + + private static final JavaFileObject SIMPLE_FUTURE_WRAPPER_NO_CREATE_METHOD = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".SimpleFutureWrapper", + "package " + NOTES_PACKAGE + ";", + "import com.google.android.enterprise.connectedapps.FutureWrapper;", + "import com.google.android.enterprise.connectedapps.internal.FutureResultWriter;", + "import com.google.android.enterprise.connectedapps.Profile;", + "import com.google.android.enterprise.connectedapps.internal.Bundler;", + "import com.google.android.enterprise.connectedapps.internal.BundlerType;", + "import" + + " com.google.android.enterprise.connectedapps.internal.CrossProfileCallbackMultiMerger;", + "import java.util.Map;", + "@com.google.android.enterprise.connectedapps.annotations.CustomFutureWrapper(", + "originalType = SimpleFuture.class)", + "public final class SimpleFutureWrapper<E> extends FutureWrapper<E> {", + "private final SimpleFuture<E> future = new SimpleFuture<>();", + "private SimpleFutureWrapper(Bundler bundler, BundlerType bundlerType) {", + "super(bundler, bundlerType);", + "}", + "public SimpleFuture<E> getFuture() {", + "return future;", + "}", + "@Override", + "public void onResult(E result) {", + "future.set(result);", + "}", + "@Override", + "public void onException(Throwable throwable) {", + "future.setException(throwable);", + "}", + "public static <E> void writeFutureResult(", + "SimpleFuture<E> future,", + "FutureResultWriter<E> resultWriter) {", + "future.setCallback(", + "(value) -> {", + "resultWriter.onSuccess(value);", + "},", + "(exception) -> {", + "resultWriter.onFailure(exception);", + "});", + "}", + "public static <E> SimpleFuture<Map<Profile, E>> groupResults(", + "Map<Profile, SimpleFuture<E>> results) {", + "SimpleFuture<Map<Profile, E>> m = new SimpleFuture<>();", + "CrossProfileCallbackMultiMerger<E> merger =", + "new CrossProfileCallbackMultiMerger<>(results.size(), m::set);", + "for (Map.Entry<Profile, SimpleFuture<E>> result : results.entrySet()) {", + "result", + ".getValue()", + ".setCallback(", + "(value) -> {", + "merger.onResult(result.getKey(), value);", + "},", + "(throwable) -> {", + "merger.missingResult(result.getKey());", + "});", + "}", + "return m;", + "}", + "}"); + + private static final JavaFileObject SIMPLE_FUTURE_WRAPPER_INCORRECT_CREATE_METHOD = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".SimpleFutureWrapper", + "package " + NOTES_PACKAGE + ";", + "import com.google.android.enterprise.connectedapps.FutureWrapper;", + "import com.google.android.enterprise.connectedapps.internal.FutureResultWriter;", + "import com.google.android.enterprise.connectedapps.Profile;", + "import com.google.android.enterprise.connectedapps.internal.Bundler;", + "import com.google.android.enterprise.connectedapps.internal.BundlerType;", + "import" + + " com.google.android.enterprise.connectedapps.internal.CrossProfileCallbackMultiMerger;", + "import java.util.Map;", + "@com.google.android.enterprise.connectedapps.annotations.CustomFutureWrapper(", + "originalType = SimpleFuture.class)", + "public final class SimpleFutureWrapper<E> extends FutureWrapper<E> {", + "private final SimpleFuture<E> future = new SimpleFuture<>();", + "public static <E> SimpleFutureWrapper<E> create(BundlerType bundlerType) {", + "return null;", + "}", + "private SimpleFutureWrapper(Bundler bundler, BundlerType bundlerType) {", + "super(bundler, bundlerType);", + "}", + "public SimpleFuture<E> getFuture() {", + "return future;", + "}", + "@Override", + "public void onResult(E result) {", + "future.set(result);", + "}", + "@Override", + "public void onException(Throwable throwable) {", + "future.setException(throwable);", + "}", + "public static <E> void writeFutureResult(", + "SimpleFuture<E> future,", + "FutureResultWriter<E> resultWriter) {", + "future.setCallback(", + "(value) -> {", + "resultWriter.onSuccess(value);", + "},", + "(exception) -> {", + "resultWriter.onFailure(exception);", + "});", + "}", + "public static <E> SimpleFuture<Map<Profile, E>> groupResults(", + "Map<Profile, SimpleFuture<E>> results) {", + "SimpleFuture<Map<Profile, E>> m = new SimpleFuture<>();", + "CrossProfileCallbackMultiMerger<E> merger =", + "new CrossProfileCallbackMultiMerger<>(results.size(), m::set);", + "for (Map.Entry<Profile, SimpleFuture<E>> result : results.entrySet()) {", + "result", + ".getValue()", + ".setCallback(", + "(value) -> {", + "merger.onResult(result.getKey(), value);", + "},", + "(throwable) -> {", + "merger.missingResult(result.getKey());", + "});", + "}", + "return m;", + "}", + "}"); + + private static final JavaFileObject SIMPLE_FUTURE_WRAPPER_NO_GET_FUTURE_METHOD = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".SimpleFutureWrapper", + "package " + NOTES_PACKAGE + ";", + "import com.google.android.enterprise.connectedapps.FutureWrapper;", + "import com.google.android.enterprise.connectedapps.internal.FutureResultWriter;", + "import com.google.android.enterprise.connectedapps.Profile;", + "import com.google.android.enterprise.connectedapps.internal.Bundler;", + "import com.google.android.enterprise.connectedapps.internal.BundlerType;", + "import" + + " com.google.android.enterprise.connectedapps.internal.CrossProfileCallbackMultiMerger;", + "import java.util.Map;", + "@com.google.android.enterprise.connectedapps.annotations.CustomFutureWrapper(", + "originalType = SimpleFuture.class)", + "public final class SimpleFutureWrapper<E> extends FutureWrapper<E> {", + "private final SimpleFuture<E> future = new SimpleFuture<>();", + "public static <E> SimpleFutureWrapper<E> create(Bundler bundler, BundlerType" + + " bundlerType) {", + "return new SimpleFutureWrapper<>(bundler, bundlerType);", + "}", + "private SimpleFutureWrapper(Bundler bundler, BundlerType bundlerType) {", + "super(bundler, bundlerType);", + "}", + "@Override", + "public void onResult(E result) {", + "future.set(result);", + "}", + "@Override", + "public void onException(Throwable throwable) {", + "future.setException(throwable);", + "}", + "public static <E> void writeFutureResult(", + "SimpleFuture<E> future,", + "FutureResultWriter<E> resultWriter) {", + "future.setCallback(", + "(value) -> {", + "resultWriter.onSuccess(value);", + "},", + "(exception) -> {", + "resultWriter.onFailure(exception);", + "});", + "}", + "public static <E> SimpleFuture<Map<Profile, E>> groupResults(", + "Map<Profile, SimpleFuture<E>> results) {", + "SimpleFuture<Map<Profile, E>> m = new SimpleFuture<>();", + "CrossProfileCallbackMultiMerger<E> merger =", + "new CrossProfileCallbackMultiMerger<>(results.size(), m::set);", + "for (Map.Entry<Profile, SimpleFuture<E>> result : results.entrySet()) {", + "result", + ".getValue()", + ".setCallback(", + "(value) -> {", + "merger.onResult(result.getKey(), value);", + "},", + "(throwable) -> {", + "merger.missingResult(result.getKey());", + "});", + "}", + "return m;", + "}", + "}"); + + private static final JavaFileObject SIMPLE_FUTURE_WRAPPER_INCORRECT_GET_FUTURE_METHOD = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".SimpleFutureWrapper", + "package " + NOTES_PACKAGE + ";", + "import com.google.android.enterprise.connectedapps.FutureWrapper;", + "import com.google.android.enterprise.connectedapps.internal.FutureResultWriter;", + "import com.google.android.enterprise.connectedapps.Profile;", + "import com.google.android.enterprise.connectedapps.internal.Bundler;", + "import com.google.android.enterprise.connectedapps.internal.BundlerType;", + "import" + + " com.google.android.enterprise.connectedapps.internal.CrossProfileCallbackMultiMerger;", + "import java.util.Map;", + "@com.google.android.enterprise.connectedapps.annotations.CustomFutureWrapper(", + "originalType = SimpleFuture.class)", + "public final class SimpleFutureWrapper<E> extends FutureWrapper<E> {", + "private final SimpleFuture<E> future = new SimpleFuture<>();", + "public static <E> SimpleFutureWrapper<E> create(Bundler bundler, BundlerType" + + " bundlerType) {", + "return new SimpleFutureWrapper<>(bundler, bundlerType);", + "}", + "private SimpleFutureWrapper(Bundler bundler, BundlerType bundlerType) {", + "super(bundler, bundlerType);", + "}", + "public SimpleFuture<E> getFuture(String s) {", + "return future;", + "}", + "@Override", + "public void onResult(E result) {", + "future.set(result);", + "}", + "@Override", + "public void onException(Throwable throwable) {", + "future.setException(throwable);", + "}", + "public static <E> void writeFutureResult(", + "SimpleFuture<E> future,", + "FutureResultWriter<E> resultWriter) {", + "future.setCallback(", + "(value) -> {", + "resultWriter.onSuccess(value);", + "},", + "(exception) -> {", + "resultWriter.onFailure(exception);", + "});", + "}", + "public static <E> SimpleFuture<Map<Profile, E>> groupResults(", + "Map<Profile, SimpleFuture<E>> results) {", + "SimpleFuture<Map<Profile, E>> m = new SimpleFuture<>();", + "CrossProfileCallbackMultiMerger<E> merger =", + "new CrossProfileCallbackMultiMerger<>(results.size(), m::set);", + "for (Map.Entry<Profile, SimpleFuture<E>> result : results.entrySet()) {", + "result", + ".getValue()", + ".setCallback(", + "(value) -> {", + "merger.onResult(result.getKey(), value);", + "},", + "(throwable) -> {", + "merger.missingResult(result.getKey());", + "});", + "}", + "return m;", + "}", + "}"); + + private static final JavaFileObject SIMPLE_FUTURE_WRAPPER_NO_RESOLVE_METHOD = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".SimpleFutureWrapper", + "package " + NOTES_PACKAGE + ";", + "import com.google.android.enterprise.connectedapps.FutureWrapper;", + "import com.google.android.enterprise.connectedapps.internal.FutureResultWriter;", + "import com.google.android.enterprise.connectedapps.Profile;", + "import com.google.android.enterprise.connectedapps.internal.Bundler;", + "import com.google.android.enterprise.connectedapps.internal.BundlerType;", + "import" + + " com.google.android.enterprise.connectedapps.internal.CrossProfileCallbackMultiMerger;", + "import java.util.Map;", + "@com.google.android.enterprise.connectedapps.annotations.CustomFutureWrapper(", + "originalType = SimpleFuture.class)", + "public final class SimpleFutureWrapper<E> extends FutureWrapper<E> {", + "private final SimpleFuture<E> future = new SimpleFuture<>();", + "public static <E> SimpleFutureWrapper<E> create(Bundler bundler, BundlerType" + + " bundlerType) {", + "return new SimpleFutureWrapper<>(bundler, bundlerType);", + "}", + "private SimpleFutureWrapper(Bundler bundler, BundlerType bundlerType) {", + "super(bundler, bundlerType);", + "}", + "public SimpleFuture<E> getFuture() {", + "return future;", + "}", + "@Override", + "public void onResult(E result) {", + "future.set(result);", + "}", + "@Override", + "public void onException(Throwable throwable) {", + "future.setException(throwable);", + "}", + "public static <E> SimpleFuture<Map<Profile, E>> groupResults(", + "Map<Profile, SimpleFuture<E>> results) {", + "SimpleFuture<Map<Profile, E>> m = new SimpleFuture<>();", + "CrossProfileCallbackMultiMerger<E> merger =", + "new CrossProfileCallbackMultiMerger<>(results.size(), m::set);", + "for (Map.Entry<Profile, SimpleFuture<E>> result : results.entrySet()) {", + "result", + ".getValue()", + ".setCallback(", + "(value) -> {", + "merger.onResult(result.getKey(), value);", + "},", + "(throwable) -> {", + "merger.missingResult(result.getKey());", + "});", + "}", + "return m;", + "}", + "}"); + + private static final JavaFileObject SIMPLE_FUTURE_WRAPPER_INCORRECT_RESOLVE_METHOD = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".SimpleFutureWrapper", + "package " + NOTES_PACKAGE + ";", + "import com.google.android.enterprise.connectedapps.FutureWrapper;", + "import com.google.android.enterprise.connectedapps.internal.FutureResultWriter;", + "import com.google.android.enterprise.connectedapps.Profile;", + "import com.google.android.enterprise.connectedapps.internal.Bundler;", + "import com.google.android.enterprise.connectedapps.internal.BundlerType;", + "import" + + " com.google.android.enterprise.connectedapps.internal.CrossProfileCallbackMultiMerger;", + "import java.util.Map;", + "@com.google.android.enterprise.connectedapps.annotations.CustomFutureWrapper(", + "originalType = SimpleFuture.class)", + "public final class SimpleFutureWrapper<E> extends FutureWrapper<E> {", + "private final SimpleFuture<E> future = new SimpleFuture<>();", + "public static <E> SimpleFutureWrapper<E> create(Bundler bundler, BundlerType" + + " bundlerType) {", + "return new SimpleFutureWrapper<>(bundler, bundlerType);", + "}", + "private SimpleFutureWrapper(Bundler bundler, BundlerType bundlerType) {", + "super(bundler, bundlerType);", + "}", + "public SimpleFuture<E> getFuture() {", + "return future;", + "}", + "@Override", + "public void onResult(E result) {", + "future.set(result);", + "}", + "@Override", + "public void onException(Throwable throwable) {", + "future.setException(throwable);", + "}", + "public static <E> void writeFutureResult(", + "String s,", + "SimpleFuture<E> future,", + "FutureResultWriter<E> resultWriter) {", + "future.setCallback(", + "(value) -> {", + "resultWriter.onSuccess(value);", + "},", + "(exception) -> {", + "resultWriter.onFailure(exception);", + "});", + "}", + "public static <E> SimpleFuture<Map<Profile, E>> groupResults(", + "Map<Profile, SimpleFuture<E>> results) {", + "SimpleFuture<Map<Profile, E>> m = new SimpleFuture<>();", + "CrossProfileCallbackMultiMerger<E> merger =", + "new CrossProfileCallbackMultiMerger<>(results.size(), m::set);", + "for (Map.Entry<Profile, SimpleFuture<E>> result : results.entrySet()) {", + "result", + ".getValue()", + ".setCallback(", + "(value) -> {", + "merger.onResult(result.getKey(), value);", + "},", + "(throwable) -> {", + "merger.missingResult(result.getKey());", + "});", + "}", + "return m;", + "}", + "}"); + + private static final JavaFileObject SIMPLE_FUTURE_WRAPPER_NO_GROUP_METHOD = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".SimpleFutureWrapper", + "package " + NOTES_PACKAGE + ";", + "import com.google.android.enterprise.connectedapps.FutureWrapper;", + "import com.google.android.enterprise.connectedapps.internal.FutureResultWriter;", + "import com.google.android.enterprise.connectedapps.Profile;", + "import com.google.android.enterprise.connectedapps.internal.Bundler;", + "import com.google.android.enterprise.connectedapps.internal.BundlerType;", + "import" + + " com.google.android.enterprise.connectedapps.internal.CrossProfileCallbackMultiMerger;", + "import java.util.Map;", + "@com.google.android.enterprise.connectedapps.annotations.CustomFutureWrapper(", + "originalType = SimpleFuture.class)", + "public final class SimpleFutureWrapper<E> extends FutureWrapper<E> {", + "private final SimpleFuture<E> future = new SimpleFuture<>();", + "public static <E> SimpleFutureWrapper<E> create(Bundler bundler, BundlerType" + + " bundlerType) {", + "return new SimpleFutureWrapper<>(bundler, bundlerType);", + "}", + "private SimpleFutureWrapper(Bundler bundler, BundlerType bundlerType) {", + "super(bundler, bundlerType);", + "}", + "public SimpleFuture<E> getFuture() {", + "return future;", + "}", + "@Override", + "public void onResult(E result) {", + "future.set(result);", + "}", + "@Override", + "public void onException(Throwable throwable) {", + "future.setException(throwable);", + "}", + "public static <E> void writeFutureResult(", + "SimpleFuture<E> future,", + "FutureResultWriter<E> resultWriter) {", + "future.setCallback(", + "(value) -> {", + "resultWriter.onSuccess(value);", + "},", + "(exception) -> {", + "resultWriter.onFailure(exception);", + "});", + "}", + "}"); + + private static final JavaFileObject SIMPLE_FUTURE_WRAPPER_INCORRECT_GROUP_METHOD = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".SimpleFutureWrapper", + "package " + NOTES_PACKAGE + ";", + "import com.google.android.enterprise.connectedapps.FutureWrapper;", + "import com.google.android.enterprise.connectedapps.internal.FutureResultWriter;", + "import com.google.android.enterprise.connectedapps.Profile;", + "import com.google.android.enterprise.connectedapps.internal.Bundler;", + "import com.google.android.enterprise.connectedapps.internal.BundlerType;", + "import" + + " com.google.android.enterprise.connectedapps.internal.CrossProfileCallbackMultiMerger;", + "import java.util.Map;", + "@com.google.android.enterprise.connectedapps.annotations.CustomFutureWrapper(", + "originalType = SimpleFuture.class)", + "public final class SimpleFutureWrapper<E> extends FutureWrapper<E> {", + "private final SimpleFuture<E> future = new SimpleFuture<>();", + "public static <E> SimpleFutureWrapper<E> create(Bundler bundler, BundlerType" + + " bundlerType) {", + "return new SimpleFutureWrapper<>(bundler, bundlerType);", + "}", + "private SimpleFutureWrapper(Bundler bundler, BundlerType bundlerType) {", + "super(bundler, bundlerType);", + "}", + "public SimpleFuture<E> getFuture() {", + "return future;", + "}", + "@Override", + "public void onResult(E result) {", + "future.set(result);", + "}", + "@Override", + "public void onException(Throwable throwable) {", + "future.setException(throwable);", + "}", + "public static <E> void writeFutureResult(", + "SimpleFuture<E> future,", + "FutureResultWriter<E> resultWriter) {", + "future.setCallback(", + "(value) -> {", + "resultWriter.onSuccess(value);", + "},", + "(exception) -> {", + "resultWriter.onFailure(exception);", + "});", + "}", + "public static <E> SimpleFuture<Map<Profile, E>> groupResults(", + "Map<String, SimpleFuture<E>> results) {", + "return null;", + "}", + "}"); + + @Test + public void validFutureWrapperAnnotation_compiles() { + Compilation compilation = + javac().withProcessors(new Processor()).compile(SIMPLE_FUTURE_WRAPPER, SIMPLE_FUTURE); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void futureWrapperAnnotation_isNotGeneric_hasError() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(SIMPLE_FUTURE_WRAPPER_IS_NOT_GENERIC, SIMPLE_FUTURE); + + assertThat(compilation) + .hadErrorContaining(MUST_HAVE_ONE_TYPE_PARAMETER_ERROR) + .inFile(SIMPLE_FUTURE_WRAPPER_IS_NOT_GENERIC); + } + + @Test + public void futureWrapperAnnotation_hasMultipleTypeParameters_hasError() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(SIMPLE_FUTURE_WRAPPER_MULTIPLE_TYPE_PARAMETERS, SIMPLE_FUTURE); + + assertThat(compilation) + .hadErrorContaining(MUST_HAVE_ONE_TYPE_PARAMETER_ERROR) + .inFile(SIMPLE_FUTURE_WRAPPER_MULTIPLE_TYPE_PARAMETERS); + } + + @Test + public void futureWrapperAnnotation_doesNotExtendFutureWrapper_hasError() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(SIMPLE_FUTURE_WRAPPER_DOES_NOT_EXTEND_FUTURE_WRAPPER, SIMPLE_FUTURE); + + assertThat(compilation) + .hadErrorContaining(DOES_NOT_EXTEND_FUTURE_WRAPPER_ERROR) + .inFile(SIMPLE_FUTURE_WRAPPER_DOES_NOT_EXTEND_FUTURE_WRAPPER); + } + + @Test + public void futureWrapperAnnotation_noCreateMethod_hasError() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(SIMPLE_FUTURE_WRAPPER_NO_CREATE_METHOD, SIMPLE_FUTURE); + + assertThat(compilation) + .hadErrorContaining(INCORRECT_CREATE_METHOD_ERROR) + .inFile(SIMPLE_FUTURE_WRAPPER_NO_CREATE_METHOD); + } + + @Test + public void futureWrapperAnnotation_incorrectCreateMethod_hasError() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(SIMPLE_FUTURE_WRAPPER_INCORRECT_CREATE_METHOD, SIMPLE_FUTURE); + + assertThat(compilation) + .hadErrorContaining(INCORRECT_CREATE_METHOD_ERROR) + .inFile(SIMPLE_FUTURE_WRAPPER_INCORRECT_CREATE_METHOD); + } + + @Test + public void futureWrapperAnnotation_noGetFutureMethod_hasError() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(SIMPLE_FUTURE_WRAPPER_NO_GET_FUTURE_METHOD, SIMPLE_FUTURE); + + assertThat(compilation) + .hadErrorContaining(INCORRECT_GET_FUTURE_METHOD_ERROR) + .inFile(SIMPLE_FUTURE_WRAPPER_NO_GET_FUTURE_METHOD); + } + + @Test + public void futureWrapperAnnotation_incorrectGetFutureMethod_hasError() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(SIMPLE_FUTURE_WRAPPER_INCORRECT_GET_FUTURE_METHOD, SIMPLE_FUTURE); + + assertThat(compilation) + .hadErrorContaining(INCORRECT_GET_FUTURE_METHOD_ERROR) + .inFile(SIMPLE_FUTURE_WRAPPER_INCORRECT_GET_FUTURE_METHOD); + } + + @Test + public void futureWrapperAnnotation_noResolveFutureMethod_hasError() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(SIMPLE_FUTURE_WRAPPER_NO_RESOLVE_METHOD, SIMPLE_FUTURE); + + assertThat(compilation) + .hadErrorContaining(INCORRECT_RESOLVE_CALLBACK_WHEN_FUTURE_IS_SET_METHOD_ERROR) + .inFile(SIMPLE_FUTURE_WRAPPER_NO_RESOLVE_METHOD); + } + + @Test + public void futureWrapperAnnotation_incorrectResolveFutureMethod_hasError() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(SIMPLE_FUTURE_WRAPPER_INCORRECT_RESOLVE_METHOD, SIMPLE_FUTURE); + + assertThat(compilation) + .hadErrorContaining(INCORRECT_RESOLVE_CALLBACK_WHEN_FUTURE_IS_SET_METHOD_ERROR) + .inFile(SIMPLE_FUTURE_WRAPPER_INCORRECT_RESOLVE_METHOD); + } + + @Test + public void futureWrapperAnnotation_noGroupResultsMethod_hasError() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(SIMPLE_FUTURE_WRAPPER_NO_GROUP_METHOD, SIMPLE_FUTURE); + + assertThat(compilation) + .hadErrorContaining(INCORRECT_GROUP_RESULTS_METHOD_ERROR) + .inFile(SIMPLE_FUTURE_WRAPPER_NO_GROUP_METHOD); + } + + @Test + public void futureWrapperAnnotation_incorrectGroupResultsMethod_hasError() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(SIMPLE_FUTURE_WRAPPER_INCORRECT_GROUP_METHOD, SIMPLE_FUTURE); + + assertThat(compilation) + .hadErrorContaining(INCORRECT_GROUP_RESULTS_METHOD_ERROR) + .inFile(SIMPLE_FUTURE_WRAPPER_INCORRECT_GROUP_METHOD); + } +} diff --git a/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CustomParcelableWrapperTest.java b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CustomParcelableWrapperTest.java new file mode 100644 index 0000000..4c4b4ee --- /dev/null +++ b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CustomParcelableWrapperTest.java @@ -0,0 +1,799 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.CUSTOM_WRAPPER; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.NOTES_PACKAGE; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.PARCELABLE_CUSTOM_WRAPPER; +import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.Compiler.javac; + +import com.google.testing.compile.Compilation; +import com.google.testing.compile.JavaFileObjects; +import javax.tools.JavaFileObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class CustomParcelableWrapperTest { + + private static final String NOT_PARCELABLE_ERROR = + "Classes annotated @CustomParcelableWrapper must implement Parcelable"; + private static final String INCORRECT_OF_METHOD = + "Classes annotated @CustomParcelableWrapper must have a static 'of' method which takes a" + + " Bundler, a BundlerType, and an instance of the wrapped type as arguments and returns" + + " an instance of the parcelable wrapper"; + private static final String INCORRECT_GET_METHOD = + "Classes annotated @CustomParcelableWrapper must have a static 'get' method which takes no" + + " arguments and returns an instance of the wrapped class"; + private static final String INCORRECT_PARCELABLE_IMPLEMENTATION = + "Classes annotated @CustomParcelableWrapper must correctly implement Parcelable"; + + static final JavaFileObject PARCELABLE_CUSTOM_WRAPPER_NOT_GENERIC = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".ParcelableCustomWrapper", + "package " + NOTES_PACKAGE + ";", + "import android.os.Parcel;", + "import android.os.Parcelable;", + "import com.google.android.enterprise.connectedapps.annotations.CustomParcelableWrapper;", + "import com.google.android.enterprise.connectedapps.internal.Bundler;", + "import com.google.android.enterprise.connectedapps.internal.BundlerType;", + "@CustomParcelableWrapper(originalType = CustomWrapper.class)", + "public class ParcelableCustomWrapper implements Parcelable {", + "private static final int NULL = -1;", + "private static final int NOT_NULL = 1;", + "public static ParcelableCustomWrapper of(", + "Bundler bundler, BundlerType type, CustomWrapper customWrapper) {", + "return null;", + "}", + "public CustomWrapper get() {", + "return null;", + "}", + "private ParcelableCustomWrapper(Parcel in) {", + "}", + "@Override", + "public void writeToParcel(Parcel dest, int flags) {", + "}", + "@Override", + "public int describeContents() {", + "return 0;", + "}", + "@SuppressWarnings(\"rawtypes\")", + "public static final Creator<ParcelableCustomWrapper> CREATOR =", + "new Creator<ParcelableCustomWrapper>() {", + "@Override", + "public ParcelableCustomWrapper createFromParcel(Parcel in) {", + "return new ParcelableCustomWrapper(in);", + "}", + "@Override", + "public ParcelableCustomWrapper[] newArray(int size) {", + "return new ParcelableCustomWrapper[size];", + "}", + "};", + "}"); + + static final JavaFileObject PARCELABLE_CUSTOM_WRAPPER_DOES_NOT_IMPLEMENT_PARCELABLE = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".ParcelableCustomWrapper", + "package " + NOTES_PACKAGE + ";", + "import android.os.Parcel;", + "import android.os.Parcelable;", + "import com.google.android.enterprise.connectedapps.annotations.CustomParcelableWrapper;", + "import com.google.android.enterprise.connectedapps.internal.Bundler;", + "import com.google.android.enterprise.connectedapps.internal.BundlerType;", + "import com.google.android.enterprise.connectedapps.testapp.CustomWrapper;", + "@CustomParcelableWrapper(originalType = CustomWrapper.class)", + "public class ParcelableCustomWrapper<E> {", + "private static final int NULL = -1;", + "private static final int NOT_NULL = 1;", + "private final Bundler bundler;", + "private final BundlerType type;", + "private final CustomWrapper<E> customWrapper;", + "public static <F> ParcelableCustomWrapper<F> of(", + "Bundler bundler, BundlerType type, CustomWrapper<F> customWrapper) {", + "return new ParcelableCustomWrapper<>(bundler, type, customWrapper);", + "}", + "public CustomWrapper<E> get() {", + "return customWrapper;", + "}", + "private ParcelableCustomWrapper(Bundler bundler, BundlerType type, CustomWrapper<E>" + + " customWrapper) {", + "if (bundler == null || type == null) {", + "throw new NullPointerException();", + "}", + "this.bundler = bundler;", + "this.type = type;", + "this.customWrapper = customWrapper;", + "}", + "private ParcelableCustomWrapper(Parcel in) {", + "bundler = in.readParcelable(Bundler.class.getClassLoader());", + "int presentValue = in.readInt();", + "if (presentValue == NULL) {", + "type = null;", + "customWrapper = null;", + "return;", + "}", + "type = (BundlerType) in.readParcelable(Bundler.class.getClassLoader());", + "BundlerType valueType = type.typeArguments().get(0);", + "@SuppressWarnings(\"unchecked\")", + "E value = (E) bundler.readFromParcel(in, valueType);", + "customWrapper = new CustomWrapper<>(value);", + "}", + "@Override", + "public void writeToParcel(Parcel dest, int flags) {", + "dest.writeParcelable(bundler, flags);", + "if (customWrapper == null) {", + "dest.writeInt(NULL);", + "return;", + "}", + "dest.writeInt(NOT_NULL);", + "dest.writeParcelable(type, flags);", + "BundlerType valueType = type.typeArguments().get(0);", + "bundler.writeToParcel(dest, customWrapper.value(), valueType, flags);", + "}", + "@Override", + "public int describeContents() {", + "return 0;", + "}", + "@SuppressWarnings(\"rawtypes\")", + "public static final Creator<ParcelableCustomWrapper> CREATOR =", + "new Creator<ParcelableCustomWrapper>() {", + "@Override", + "public ParcelableCustomWrapper createFromParcel(Parcel in) {", + "return new ParcelableCustomWrapper(in);", + "}", + "@Override", + "public ParcelableCustomWrapper[] newArray(int size) {", + "return new ParcelableCustomWrapper[size];", + "}", + "};", + "}"); + + static final JavaFileObject PARCELABLE_CUSTOM_WRAPPER_NO_OF_METHOD = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".ParcelableCustomWrapper", + "package " + NOTES_PACKAGE + ";", + "import android.os.Parcel;", + "import android.os.Parcelable;", + "import com.google.android.enterprise.connectedapps.annotations.CustomParcelableWrapper;", + "import com.google.android.enterprise.connectedapps.internal.Bundler;", + "import com.google.android.enterprise.connectedapps.internal.BundlerType;", + "import com.google.android.enterprise.connectedapps.testapp.CustomWrapper;", + "@CustomParcelableWrapper(originalType = CustomWrapper.class)", + "public class ParcelableCustomWrapper<E> implements Parcelable {", + "private static final int NULL = -1;", + "private static final int NOT_NULL = 1;", + "private final Bundler bundler;", + "private final BundlerType type;", + "private final CustomWrapper<E> customWrapper;", + "public CustomWrapper<E> get() {", + "return customWrapper;", + "}", + "private ParcelableCustomWrapper(Bundler bundler, BundlerType type, CustomWrapper<E>" + + " customWrapper) {", + "if (bundler == null || type == null) {", + "throw new NullPointerException();", + "}", + "this.bundler = bundler;", + "this.type = type;", + "this.customWrapper = customWrapper;", + "}", + "private ParcelableCustomWrapper(Parcel in) {", + "bundler = in.readParcelable(Bundler.class.getClassLoader());", + "int presentValue = in.readInt();", + "if (presentValue == NULL) {", + "type = null;", + "customWrapper = null;", + "return;", + "}", + "type = (BundlerType) in.readParcelable(Bundler.class.getClassLoader());", + "BundlerType valueType = type.typeArguments().get(0);", + "@SuppressWarnings(\"unchecked\")", + "E value = (E) bundler.readFromParcel(in, valueType);", + "customWrapper = new CustomWrapper<>(value);", + "}", + "@Override", + "public void writeToParcel(Parcel dest, int flags) {", + "dest.writeParcelable(bundler, flags);", + "if (customWrapper == null) {", + "dest.writeInt(NULL);", + "return;", + "}", + "dest.writeInt(NOT_NULL);", + "dest.writeParcelable(type, flags);", + "BundlerType valueType = type.typeArguments().get(0);", + "bundler.writeToParcel(dest, customWrapper.value(), valueType, flags);", + "}", + "@Override", + "public int describeContents() {", + "return 0;", + "}", + "@SuppressWarnings(\"rawtypes\")", + "public static final Creator<ParcelableCustomWrapper> CREATOR =", + "new Creator<ParcelableCustomWrapper>() {", + "@Override", + "public ParcelableCustomWrapper createFromParcel(Parcel in) {", + "return new ParcelableCustomWrapper(in);", + "}", + "@Override", + "public ParcelableCustomWrapper[] newArray(int size) {", + "return new ParcelableCustomWrapper[size];", + "}", + "};", + "}"); + + static final JavaFileObject PARCELABLE_CUSTOM_WRAPPER_OF_METHOD_WRONG_ARGUMENTS = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".ParcelableCustomWrapper", + "package " + NOTES_PACKAGE + ";", + "import android.os.Parcel;", + "import android.os.Parcelable;", + "import com.google.android.enterprise.connectedapps.annotations.CustomParcelableWrapper;", + "import com.google.android.enterprise.connectedapps.internal.Bundler;", + "import com.google.android.enterprise.connectedapps.internal.BundlerType;", + "import com.google.android.enterprise.connectedapps.testapp.CustomWrapper;", + "@CustomParcelableWrapper(originalType = CustomWrapper.class)", + "public class ParcelableCustomWrapper<E> implements Parcelable {", + "private static final int NULL = -1;", + "private static final int NOT_NULL = 1;", + "private final Bundler bundler;", + "private final BundlerType type;", + "private final CustomWrapper<E> customWrapper;", + "public static <F> ParcelableCustomWrapper<F> of(", + "Bundler bundler, CustomWrapper<F> customWrapper) {", + "return null;", + "}", + "public CustomWrapper<E> get() {", + "return customWrapper;", + "}", + "private ParcelableCustomWrapper(Bundler bundler, BundlerType type, CustomWrapper<E>" + + " customWrapper) {", + "if (bundler == null || type == null) {", + "throw new NullPointerException();", + "}", + "this.bundler = bundler;", + "this.type = type;", + "this.customWrapper = customWrapper;", + "}", + "private ParcelableCustomWrapper(Parcel in) {", + "bundler = in.readParcelable(Bundler.class.getClassLoader());", + "int presentValue = in.readInt();", + "if (presentValue == NULL) {", + "type = null;", + "customWrapper = null;", + "return;", + "}", + "type = (BundlerType) in.readParcelable(Bundler.class.getClassLoader());", + "BundlerType valueType = type.typeArguments().get(0);", + "@SuppressWarnings(\"unchecked\")", + "E value = (E) bundler.readFromParcel(in, valueType);", + "customWrapper = new CustomWrapper<>(value);", + "}", + "@Override", + "public void writeToParcel(Parcel dest, int flags) {", + "dest.writeParcelable(bundler, flags);", + "if (customWrapper == null) {", + "dest.writeInt(NULL);", + "return;", + "}", + "dest.writeInt(NOT_NULL);", + "dest.writeParcelable(type, flags);", + "BundlerType valueType = type.typeArguments().get(0);", + "bundler.writeToParcel(dest, customWrapper.value(), valueType, flags);", + "}", + "@Override", + "public int describeContents() {", + "return 0;", + "}", + "@SuppressWarnings(\"rawtypes\")", + "public static final Creator<ParcelableCustomWrapper> CREATOR =", + "new Creator<ParcelableCustomWrapper>() {", + "@Override", + "public ParcelableCustomWrapper createFromParcel(Parcel in) {", + "return new ParcelableCustomWrapper(in);", + "}", + "@Override", + "public ParcelableCustomWrapper[] newArray(int size) {", + "return new ParcelableCustomWrapper[size];", + "}", + "};", + "}"); + + static final JavaFileObject PARCELABLE_CUSTOM_WRAPPER_OF_METHOD_WRONG_RETURN_TYPE = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".ParcelableCustomWrapper", + "package " + NOTES_PACKAGE + ";", + "import android.os.Parcel;", + "import android.os.Parcelable;", + "import com.google.android.enterprise.connectedapps.annotations.CustomParcelableWrapper;", + "import com.google.android.enterprise.connectedapps.internal.Bundler;", + "import com.google.android.enterprise.connectedapps.internal.BundlerType;", + "import com.google.android.enterprise.connectedapps.testapp.CustomWrapper;", + "@CustomParcelableWrapper(originalType = CustomWrapper.class)", + "public class ParcelableCustomWrapper<E> implements Parcelable {", + "private static final int NULL = -1;", + "private static final int NOT_NULL = 1;", + "private final Bundler bundler;", + "private final BundlerType type;", + "private final CustomWrapper<E> customWrapper;", + "public static<F> String of(", + "Bundler bundler, BundlerType bundlerType, CustomWrapper<F> customWrapper) {", + "return null;", + "}", + "public CustomWrapper<E> get() {", + "return customWrapper;", + "}", + "private ParcelableCustomWrapper(Bundler bundler, BundlerType type, CustomWrapper<E>" + + " customWrapper) {", + "if (bundler == null || type == null) {", + "throw new NullPointerException();", + "}", + "this.bundler = bundler;", + "this.type = type;", + "this.customWrapper = customWrapper;", + "}", + "private ParcelableCustomWrapper(Parcel in) {", + "bundler = in.readParcelable(Bundler.class.getClassLoader());", + "int presentValue = in.readInt();", + "if (presentValue == NULL) {", + "type = null;", + "customWrapper = null;", + "return;", + "}", + "type = (BundlerType) in.readParcelable(Bundler.class.getClassLoader());", + "BundlerType valueType = type.typeArguments().get(0);", + "@SuppressWarnings(\"unchecked\")", + "E value = (E) bundler.readFromParcel(in, valueType);", + "customWrapper = new CustomWrapper<>(value);", + "}", + "@Override", + "public void writeToParcel(Parcel dest, int flags) {", + "dest.writeParcelable(bundler, flags);", + "if (customWrapper == null) {", + "dest.writeInt(NULL);", + "return;", + "}", + "dest.writeInt(NOT_NULL);", + "dest.writeParcelable(type, flags);", + "BundlerType valueType = type.typeArguments().get(0);", + "bundler.writeToParcel(dest, customWrapper.value(), valueType, flags);", + "}", + "@Override", + "public int describeContents() {", + "return 0;", + "}", + "@SuppressWarnings(\"rawtypes\")", + "public static final Creator<ParcelableCustomWrapper> CREATOR =", + "new Creator<ParcelableCustomWrapper>() {", + "@Override", + "public ParcelableCustomWrapper createFromParcel(Parcel in) {", + "return new ParcelableCustomWrapper(in);", + "}", + "@Override", + "public ParcelableCustomWrapper[] newArray(int size) {", + "return new ParcelableCustomWrapper[size];", + "}", + "};", + "}"); + + static final JavaFileObject PARCELABLE_CUSTOM_WRAPPER_NO_GET_METHOD = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".ParcelableCustomWrapper", + "package " + NOTES_PACKAGE + ";", + "import android.os.Parcel;", + "import android.os.Parcelable;", + "import com.google.android.enterprise.connectedapps.annotations.CustomParcelableWrapper;", + "import com.google.android.enterprise.connectedapps.internal.Bundler;", + "import com.google.android.enterprise.connectedapps.internal.BundlerType;", + "import com.google.android.enterprise.connectedapps.testapp.CustomWrapper;", + "@CustomParcelableWrapper(originalType = CustomWrapper.class)", + "public class ParcelableCustomWrapper<E> implements Parcelable {", + "private static final int NULL = -1;", + "private static final int NOT_NULL = 1;", + "private final Bundler bundler;", + "private final BundlerType type;", + "private final CustomWrapper<E> customWrapper;", + "public static <F> ParcelableCustomWrapper<F> of(", + "Bundler bundler, BundlerType type, CustomWrapper<F> customWrapper) {", + "return new ParcelableCustomWrapper<>(bundler, type, customWrapper);", + "}", + "private ParcelableCustomWrapper(Bundler bundler, BundlerType type, CustomWrapper<E>" + + " customWrapper) {", + "if (bundler == null || type == null) {", + "throw new NullPointerException();", + "}", + "this.bundler = bundler;", + "this.type = type;", + "this.customWrapper = customWrapper;", + "}", + "private ParcelableCustomWrapper(Parcel in) {", + "bundler = in.readParcelable(Bundler.class.getClassLoader());", + "int presentValue = in.readInt();", + "if (presentValue == NULL) {", + "type = null;", + "customWrapper = null;", + "return;", + "}", + "type = (BundlerType) in.readParcelable(Bundler.class.getClassLoader());", + "BundlerType valueType = type.typeArguments().get(0);", + "@SuppressWarnings(\"unchecked\")", + "E value = (E) bundler.readFromParcel(in, valueType);", + "customWrapper = new CustomWrapper<>(value);", + "}", + "@Override", + "public void writeToParcel(Parcel dest, int flags) {", + "dest.writeParcelable(bundler, flags);", + "if (customWrapper == null) {", + "dest.writeInt(NULL);", + "return;", + "}", + "dest.writeInt(NOT_NULL);", + "dest.writeParcelable(type, flags);", + "BundlerType valueType = type.typeArguments().get(0);", + "bundler.writeToParcel(dest, customWrapper.value(), valueType, flags);", + "}", + "@Override", + "public int describeContents() {", + "return 0;", + "}", + "@SuppressWarnings(\"rawtypes\")", + "public static final Creator<ParcelableCustomWrapper> CREATOR =", + "new Creator<ParcelableCustomWrapper>() {", + "@Override", + "public ParcelableCustomWrapper createFromParcel(Parcel in) {", + "return new ParcelableCustomWrapper(in);", + "}", + "@Override", + "public ParcelableCustomWrapper[] newArray(int size) {", + "return new ParcelableCustomWrapper[size];", + "}", + "};", + "}"); + + static final JavaFileObject PARCELABLE_CUSTOM_WRAPPER_GET_METHOD_WRONG_RETURN_TYPE = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".ParcelableCustomWrapper", + "package " + NOTES_PACKAGE + ";", + "import android.os.Parcel;", + "import android.os.Parcelable;", + "import com.google.android.enterprise.connectedapps.annotations.CustomParcelableWrapper;", + "import com.google.android.enterprise.connectedapps.internal.Bundler;", + "import com.google.android.enterprise.connectedapps.internal.BundlerType;", + "import com.google.android.enterprise.connectedapps.testapp.CustomWrapper;", + "@CustomParcelableWrapper(originalType = CustomWrapper.class)", + "public class ParcelableCustomWrapper<E> implements Parcelable {", + "private static final int NULL = -1;", + "private static final int NOT_NULL = 1;", + "private final Bundler bundler;", + "private final BundlerType type;", + "private final CustomWrapper<E> customWrapper;", + "public static <F> ParcelableCustomWrapper<F> of(", + "Bundler bundler, BundlerType type, CustomWrapper<F> customWrapper) {", + "return new ParcelableCustomWrapper<>(bundler, type, customWrapper);", + "}", + "public String get() {", + "return null;", + "}", + "private ParcelableCustomWrapper(Bundler bundler, BundlerType type, CustomWrapper<E>" + + " customWrapper) {", + "if (bundler == null || type == null) {", + "throw new NullPointerException();", + "}", + "this.bundler = bundler;", + "this.type = type;", + "this.customWrapper = customWrapper;", + "}", + "private ParcelableCustomWrapper(Parcel in) {", + "bundler = in.readParcelable(Bundler.class.getClassLoader());", + "int presentValue = in.readInt();", + "if (presentValue == NULL) {", + "type = null;", + "customWrapper = null;", + "return;", + "}", + "type = (BundlerType) in.readParcelable(Bundler.class.getClassLoader());", + "BundlerType valueType = type.typeArguments().get(0);", + "@SuppressWarnings(\"unchecked\")", + "E value = (E) bundler.readFromParcel(in, valueType);", + "customWrapper = new CustomWrapper<>(value);", + "}", + "@Override", + "public void writeToParcel(Parcel dest, int flags) {", + "dest.writeParcelable(bundler, flags);", + "if (customWrapper == null) {", + "dest.writeInt(NULL);", + "return;", + "}", + "dest.writeInt(NOT_NULL);", + "dest.writeParcelable(type, flags);", + "BundlerType valueType = type.typeArguments().get(0);", + "bundler.writeToParcel(dest, customWrapper.value(), valueType, flags);", + "}", + "@Override", + "public int describeContents() {", + "return 0;", + "}", + "@SuppressWarnings(\"rawtypes\")", + "public static final Creator<ParcelableCustomWrapper> CREATOR =", + "new Creator<ParcelableCustomWrapper>() {", + "@Override", + "public ParcelableCustomWrapper createFromParcel(Parcel in) {", + "return new ParcelableCustomWrapper(in);", + "}", + "@Override", + "public ParcelableCustomWrapper[] newArray(int size) {", + "return new ParcelableCustomWrapper[size];", + "}", + "};", + "}"); + + static final JavaFileObject PARCELABLE_CUSTOM_WRAPPER_NO_CREATOR = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".ParcelableCustomWrapper", + "package " + NOTES_PACKAGE + ";", + "import android.os.Parcel;", + "import android.os.Parcelable;", + "import com.google.android.enterprise.connectedapps.annotations.CustomParcelableWrapper;", + "import com.google.android.enterprise.connectedapps.internal.Bundler;", + "import com.google.android.enterprise.connectedapps.internal.BundlerType;", + "import com.google.android.enterprise.connectedapps.testapp.CustomWrapper;", + "@CustomParcelableWrapper(originalType = CustomWrapper.class)", + "public class ParcelableCustomWrapper<E> implements Parcelable {", + "private static final int NULL = -1;", + "private static final int NOT_NULL = 1;", + "private final Bundler bundler;", + "private final BundlerType type;", + "private final CustomWrapper<E> customWrapper;", + "public static <F> ParcelableCustomWrapper<F> of(", + "Bundler bundler, BundlerType type, CustomWrapper<F> customWrapper) {", + "return new ParcelableCustomWrapper<>(bundler, type, customWrapper);", + "}", + "public CustomWrapper<E> get() {", + "return customWrapper;", + "}", + "private ParcelableCustomWrapper(Bundler bundler, BundlerType type, CustomWrapper<E>" + + " customWrapper) {", + "if (bundler == null || type == null) {", + "throw new NullPointerException();", + "}", + "this.bundler = bundler;", + "this.type = type;", + "this.customWrapper = customWrapper;", + "}", + "private ParcelableCustomWrapper(Parcel in) {", + "bundler = in.readParcelable(Bundler.class.getClassLoader());", + "int presentValue = in.readInt();", + "if (presentValue == NULL) {", + "type = null;", + "customWrapper = null;", + "return;", + "}", + "type = (BundlerType) in.readParcelable(Bundler.class.getClassLoader());", + "BundlerType valueType = type.typeArguments().get(0);", + "@SuppressWarnings(\"unchecked\")", + "E value = (E) bundler.readFromParcel(in, valueType);", + "customWrapper = new CustomWrapper<>(value);", + "}", + "@Override", + "public void writeToParcel(Parcel dest, int flags) {", + "dest.writeParcelable(bundler, flags);", + "if (customWrapper == null) {", + "dest.writeInt(NULL);", + "return;", + "}", + "dest.writeInt(NOT_NULL);", + "dest.writeParcelable(type, flags);", + "BundlerType valueType = type.typeArguments().get(0);", + "bundler.writeToParcel(dest, customWrapper.value(), valueType, flags);", + "}", + "@Override", + "public int describeContents() {", + "return 0;", + "}", + "}"); + + static final JavaFileObject PARCELABLE_CUSTOM_WRAPPER_INCORRECT_CREATOR = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".ParcelableCustomWrapper", + "package " + NOTES_PACKAGE + ";", + "import android.os.Parcel;", + "import android.os.Parcelable;", + "import com.google.android.enterprise.connectedapps.annotations.CustomParcelableWrapper;", + "import com.google.android.enterprise.connectedapps.internal.Bundler;", + "import com.google.android.enterprise.connectedapps.internal.BundlerType;", + "import com.google.android.enterprise.connectedapps.testapp.CustomWrapper;", + "@CustomParcelableWrapper(originalType = CustomWrapper.class)", + "public class ParcelableCustomWrapper<E> implements Parcelable {", + "private static final int NULL = -1;", + "private static final int NOT_NULL = 1;", + "private final Bundler bundler;", + "private final BundlerType type;", + "private final CustomWrapper<E> customWrapper;", + "public static <F> ParcelableCustomWrapper<F> of(", + "Bundler bundler, BundlerType type, CustomWrapper<F> customWrapper) {", + "return new ParcelableCustomWrapper<>(bundler, type, customWrapper);", + "}", + "public CustomWrapper<E> get() {", + "return customWrapper;", + "}", + "private ParcelableCustomWrapper(Bundler bundler, BundlerType type, CustomWrapper<E>" + + " customWrapper) {", + "if (bundler == null || type == null) {", + "throw new NullPointerException();", + "}", + "this.bundler = bundler;", + "this.type = type;", + "this.customWrapper = customWrapper;", + "}", + "private ParcelableCustomWrapper(Parcel in) {", + "bundler = in.readParcelable(Bundler.class.getClassLoader());", + "int presentValue = in.readInt();", + "if (presentValue == NULL) {", + "type = null;", + "customWrapper = null;", + "return;", + "}", + "type = (BundlerType) in.readParcelable(Bundler.class.getClassLoader());", + "BundlerType valueType = type.typeArguments().get(0);", + "@SuppressWarnings(\"unchecked\")", + "E value = (E) bundler.readFromParcel(in, valueType);", + "customWrapper = new CustomWrapper<>(value);", + "}", + "@Override", + "public void writeToParcel(Parcel dest, int flags) {", + "dest.writeParcelable(bundler, flags);", + "if (customWrapper == null) {", + "dest.writeInt(NULL);", + "return;", + "}", + "dest.writeInt(NOT_NULL);", + "dest.writeParcelable(type, flags);", + "BundlerType valueType = type.typeArguments().get(0);", + "bundler.writeToParcel(dest, customWrapper.value(), valueType, flags);", + "}", + "@Override", + "public int describeContents() {", + "return 0;", + "}", + "@SuppressWarnings(\"rawtypes\")", + "public static final Creator<String> CREATOR =", + "new Creator<String>() {", + "@Override", + "public String createFromParcel(Parcel in) {", + "return null;", + "}", + "@Override", + "public String[] newArray(int size) {", + "return new String[size];", + "}", + "};", + "}"); + + @Test + public void validParcelableWrapperAnnotation_compiles() { + Compilation compilation = + javac().withProcessors(new Processor()).compile(CUSTOM_WRAPPER, PARCELABLE_CUSTOM_WRAPPER); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void validParcelableWrapperAnnotation_notGeneric_compiles() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(CUSTOM_WRAPPER, PARCELABLE_CUSTOM_WRAPPER_NOT_GENERIC); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void parcelableWrapper_doesNotImplementParcelable_hasError() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(CUSTOM_WRAPPER, PARCELABLE_CUSTOM_WRAPPER_DOES_NOT_IMPLEMENT_PARCELABLE); + + assertThat(compilation) + .hadErrorContaining(NOT_PARCELABLE_ERROR) + .inFile(PARCELABLE_CUSTOM_WRAPPER_DOES_NOT_IMPLEMENT_PARCELABLE); + } + + @Test + public void parcelableWrapper_hasNoOfMethod_hasError() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(CUSTOM_WRAPPER, PARCELABLE_CUSTOM_WRAPPER_NO_OF_METHOD); + + assertThat(compilation) + .hadErrorContaining(INCORRECT_OF_METHOD) + .inFile(PARCELABLE_CUSTOM_WRAPPER_NO_OF_METHOD); + } + + @Test + public void parcelableWrapper_ofMethodWrongReturnType_hasError() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(CUSTOM_WRAPPER, PARCELABLE_CUSTOM_WRAPPER_OF_METHOD_WRONG_RETURN_TYPE); + + assertThat(compilation) + .hadErrorContaining(INCORRECT_OF_METHOD) + .inFile(PARCELABLE_CUSTOM_WRAPPER_OF_METHOD_WRONG_RETURN_TYPE); + } + + @Test + public void parcelableWrapper_ofMethodWrongArguments_hasError() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(CUSTOM_WRAPPER, PARCELABLE_CUSTOM_WRAPPER_OF_METHOD_WRONG_ARGUMENTS); + + assertThat(compilation) + .hadErrorContaining(INCORRECT_OF_METHOD) + .inFile(PARCELABLE_CUSTOM_WRAPPER_OF_METHOD_WRONG_ARGUMENTS); + } + + @Test + public void parcelableWrapper_hasNoGetMethod_hasError() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(CUSTOM_WRAPPER, PARCELABLE_CUSTOM_WRAPPER_NO_GET_METHOD); + + assertThat(compilation) + .hadErrorContaining(INCORRECT_GET_METHOD) + .inFile(PARCELABLE_CUSTOM_WRAPPER_NO_GET_METHOD); + } + + @Test + public void parcelableWrapper_getMethodWrongReturnType_hasError() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(CUSTOM_WRAPPER, PARCELABLE_CUSTOM_WRAPPER_GET_METHOD_WRONG_RETURN_TYPE); + + assertThat(compilation) + .hadErrorContaining(INCORRECT_GET_METHOD) + .inFile(PARCELABLE_CUSTOM_WRAPPER_GET_METHOD_WRONG_RETURN_TYPE); + } + + @Test + public void parcelableWrapper_noCreator_hasError() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(CUSTOM_WRAPPER, PARCELABLE_CUSTOM_WRAPPER_NO_CREATOR); + + assertThat(compilation) + .hadErrorContaining(INCORRECT_PARCELABLE_IMPLEMENTATION) + .inFile(PARCELABLE_CUSTOM_WRAPPER_NO_CREATOR); + } + + @Test + public void parcelableWrapper_incorrectCreator_hasError() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(CUSTOM_WRAPPER, PARCELABLE_CUSTOM_WRAPPER_INCORRECT_CREATOR); + + assertThat(compilation) + .hadErrorContaining(INCORRECT_PARCELABLE_IMPLEMENTATION) + .inFile(PARCELABLE_CUSTOM_WRAPPER_INCORRECT_CREATOR); + } +} diff --git a/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CustomProfileConnectorTest.java b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CustomProfileConnectorTest.java new file mode 100644 index 0000000..6498559 --- /dev/null +++ b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CustomProfileConnectorTest.java @@ -0,0 +1,160 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.CUSTOM_PROFILE_CONNECTOR_QUALIFIED_NAME; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.NOTES_PACKAGE; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.PROFILE_CONNECTOR_QUALIFIED_NAME; +import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.Compiler.javac; + +import com.google.testing.compile.Compilation; +import com.google.testing.compile.JavaFileObjects; +import javax.tools.JavaFileObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class CustomProfileConnectorTest { + + private static final String NOT_INTERFACE_ERROR = + "@CustomProfileConnector must only be applied to interfaces"; + private static final String NOT_IMPLEMENTING_CONNECTOR_ERROR = + "Interfaces annotated with @CustomProfileConnector must extend ProfileConnector"; + private static final String PARCELABLE_WRAPPER_ANNOTATION_ERROR = + "Parcelable Wrappers must be annotated @CustomParcelableWrapper"; + private static final String FUTURE_WRAPPER_ANNOTATION_ERROR = + "Future Wrappers must be annotated @CustomFutureWrapper"; + private static final String IMPORTS_NOT_CONNECTOR_ERROR = + "Classes included in includes= must be annotated @CustomProfileConnector"; + + @Test + public void isNotInterface_hasError() { + final JavaFileObject connector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector", + "package " + NOTES_PACKAGE + ";", + "import " + CUSTOM_PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "import " + PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "@CustomProfileConnector", + "public class NotesConnector {", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(connector); + + assertThat(compilation).hadErrorContaining(NOT_INTERFACE_ERROR).inFile(connector); + } + + @Test + public void doesNotExtendProfileConnector_hasError() { + final JavaFileObject notImplementingBaseConnector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector", + "package " + NOTES_PACKAGE + ";", + "import " + CUSTOM_PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "@CustomProfileConnector", + "public interface NotesConnector {", + "}"); + + Compilation compilation = + javac().withProcessors(new Processor()).compile(notImplementingBaseConnector); + + assertThat(compilation) + .hadErrorContaining(NOT_IMPLEMENTING_CONNECTOR_ERROR) + .inFile(notImplementingBaseConnector); + } + + @Test + public void includesNonParcelableWrapperInParcelableWrappers_hasError() { + final JavaFileObject connector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector", + "package " + NOTES_PACKAGE + ";", + "import " + CUSTOM_PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "import " + PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "@CustomProfileConnector(parcelableWrappers=String.class)", + "public interface NotesConnector extends ProfileConnector {", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(connector); + + assertThat(compilation) + .hadErrorContaining(PARCELABLE_WRAPPER_ANNOTATION_ERROR) + .inFile(connector); + } + + @Test + public void includesNonFutureWrapperInFutureWrappers_hasError() { + final JavaFileObject connector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector", + "package " + NOTES_PACKAGE + ";", + "import " + CUSTOM_PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "import " + PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "@CustomProfileConnector(futureWrappers=String.class)", + "public interface NotesConnector extends ProfileConnector {", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(connector); + + assertThat(compilation).hadErrorContaining(FUTURE_WRAPPER_ANNOTATION_ERROR).inFile(connector); + } + + @Test + public void imports_containsNonProfileConnector_hasError() { + final JavaFileObject connector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector", + "package " + NOTES_PACKAGE + ";", + "import " + CUSTOM_PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "import " + PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "@CustomProfileConnector(imports=String.class)", + "public interface NotesConnector extends ProfileConnector {", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(connector); + + assertThat(compilation).hadErrorContaining(IMPORTS_NOT_CONNECTOR_ERROR).inFile(connector); + } + + @Test + public void imports_containsProfileConnector_compiles() { + final JavaFileObject connector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector", + "package " + NOTES_PACKAGE + ";", + "import " + CUSTOM_PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "import " + PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "@CustomProfileConnector(imports=NotesConnector2.class)", + "public interface NotesConnector extends ProfileConnector {", + "}"); + final JavaFileObject connector2 = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector2", + "package " + NOTES_PACKAGE + ";", + "import " + CUSTOM_PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "import " + PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "@CustomProfileConnector", + "public interface NotesConnector2 extends ProfileConnector {", + "}"); + + Compilation compilation = + javac().withProcessors(new Processor()).compile(connector, connector2); + + assertThat(compilation).succeededWithoutWarnings(); + } +} diff --git a/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CustomUserConnectorTest.java b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CustomUserConnectorTest.java new file mode 100644 index 0000000..ba7a554 --- /dev/null +++ b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CustomUserConnectorTest.java @@ -0,0 +1,160 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.CUSTOM_USER_CONNECTOR_QUALIFIED_NAME; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.NOTES_PACKAGE; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.USER_CONNECTOR_QUALIFIED_NAME; +import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.Compiler.javac; + +import com.google.testing.compile.Compilation; +import com.google.testing.compile.JavaFileObjects; +import javax.tools.JavaFileObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class CustomUserConnectorTest { + + private static final String NOT_INTERFACE_ERROR = + "@CustomUserConnector must only be applied to interfaces"; + private static final String NOT_IMPLEMENTING_CONNECTOR_ERROR = + "Interfaces annotated with @CustomUserConnector must extend UserConnector"; + private static final String PARCELABLE_WRAPPER_ANNOTATION_ERROR = + "Parcelable Wrappers must be annotated @CustomParcelableWrapper"; + private static final String FUTURE_WRAPPER_ANNOTATION_ERROR = + "Future Wrappers must be annotated @CustomFutureWrapper"; + private static final String IMPORTS_NOT_CONNECTOR_ERROR = + "Classes included in includes= must be annotated @CustomUserConnector"; + + @Test + public void isNotInterface_hasError() { + final JavaFileObject connector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector", + "package " + NOTES_PACKAGE + ";", + "import " + CUSTOM_USER_CONNECTOR_QUALIFIED_NAME + ";", + "import " + USER_CONNECTOR_QUALIFIED_NAME + ";", + "@CustomUserConnector", + "public class NotesConnector {", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(connector); + + assertThat(compilation).hadErrorContaining(NOT_INTERFACE_ERROR).inFile(connector); + } + + @Test + public void doesNotExtendUserConnector_hasError() { + final JavaFileObject notImplementingBaseConnector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector", + "package " + NOTES_PACKAGE + ";", + "import " + CUSTOM_USER_CONNECTOR_QUALIFIED_NAME + ";", + "@CustomUserConnector", + "public interface NotesConnector {", + "}"); + + Compilation compilation = + javac().withProcessors(new Processor()).compile(notImplementingBaseConnector); + + assertThat(compilation) + .hadErrorContaining(NOT_IMPLEMENTING_CONNECTOR_ERROR) + .inFile(notImplementingBaseConnector); + } + + @Test + public void includesNonParcelableWrapperInParcelableWrappers_hasError() { + final JavaFileObject connector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector", + "package " + NOTES_PACKAGE + ";", + "import " + CUSTOM_USER_CONNECTOR_QUALIFIED_NAME + ";", + "import " + USER_CONNECTOR_QUALIFIED_NAME + ";", + "@CustomUserConnector(parcelableWrappers=String.class)", + "public interface NotesConnector extends UserConnector {", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(connector); + + assertThat(compilation) + .hadErrorContaining(PARCELABLE_WRAPPER_ANNOTATION_ERROR) + .inFile(connector); + } + + @Test + public void includesNonFutureWrapperInFutureWrappers_hasError() { + final JavaFileObject connector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector", + "package " + NOTES_PACKAGE + ";", + "import " + CUSTOM_USER_CONNECTOR_QUALIFIED_NAME + ";", + "import " + USER_CONNECTOR_QUALIFIED_NAME + ";", + "@CustomUserConnector(futureWrappers=String.class)", + "public interface NotesConnector extends UserConnector {", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(connector); + + assertThat(compilation).hadErrorContaining(FUTURE_WRAPPER_ANNOTATION_ERROR).inFile(connector); + } + + @Test + public void imports_containsNonUserConnector_hasError() { + final JavaFileObject connector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector", + "package " + NOTES_PACKAGE + ";", + "import " + CUSTOM_USER_CONNECTOR_QUALIFIED_NAME + ";", + "import " + USER_CONNECTOR_QUALIFIED_NAME + ";", + "@CustomUserConnector(imports=String.class)", + "public interface NotesConnector extends UserConnector {", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(connector); + + assertThat(compilation).hadErrorContaining(IMPORTS_NOT_CONNECTOR_ERROR).inFile(connector); + } + + @Test + public void imports_containsUserConnector_compiles() { + final JavaFileObject connector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector", + "package " + NOTES_PACKAGE + ";", + "import " + CUSTOM_USER_CONNECTOR_QUALIFIED_NAME + ";", + "import " + USER_CONNECTOR_QUALIFIED_NAME + ";", + "@CustomUserConnector(imports=NotesConnector2.class)", + "public interface NotesConnector extends UserConnector {", + "}"); + final JavaFileObject connector2 = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector2", + "package " + NOTES_PACKAGE + ";", + "import " + CUSTOM_USER_CONNECTOR_QUALIFIED_NAME + ";", + "import " + USER_CONNECTOR_QUALIFIED_NAME + ";", + "@CustomUserConnector", + "public interface NotesConnector2 extends UserConnector {", + "}"); + + Compilation compilation = + javac().withProcessors(new Processor()).compile(connector, connector2); + + assertThat(compilation).succeededWithoutWarnings(); + } +} diff --git a/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/DefaultProfileClassTest.java b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/DefaultProfileClassTest.java new file mode 100644 index 0000000..4bc9e35 --- /dev/null +++ b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/DefaultProfileClassTest.java @@ -0,0 +1,73 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.NOTES_PACKAGE; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesCrossProfileType; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesProvider; +import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.Compiler.javac; + +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationPrinter; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationStrings; +import com.google.testing.compile.Compilation; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class DefaultProfileClassTest { + + private final AnnotationPrinter annotationPrinter; + + public DefaultProfileClassTest(AnnotationPrinter annotationPrinter) { + this.annotationPrinter = annotationPrinter; + } + + @Parameters(name = "{0}") + public static Iterable<AnnotationStrings> getAnnotationPrinters() { + return AnnotationFinder.annotationStrings(); + } + + @Test + public void compile_generatesDefaultProfileClass() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesProvider(annotationPrinter), + annotatedNotesCrossProfileType(annotationPrinter)); + + assertThat(compilation).generatedSourceFile(NOTES_PACKAGE + ".DefaultProfileNotesType"); + } + + @Test + public void compile_defaultProfileClassImplementsProfileInterface() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesProvider(annotationPrinter), + annotatedNotesCrossProfileType(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".DefaultProfileNotesType") + .contentsAsUtf8String() + .contains("class DefaultProfileNotesType implements ProfileNotesType"); + } +} diff --git a/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/DispatcherTest.java b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/DispatcherTest.java new file mode 100644 index 0000000..1ce6023 --- /dev/null +++ b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/DispatcherTest.java @@ -0,0 +1,110 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.CUSTOM_PROFILE_CONNECTOR_QUALIFIED_NAME; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.NOTES_PACKAGE; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.PROFILE_CONNECTOR_QUALIFIED_NAME; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesConfigurationWithNotesProvider; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesCrossProfileType; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesProvider; +import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.Compiler.javac; + +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationPrinter; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationStrings; +import com.google.testing.compile.Compilation; +import com.google.testing.compile.JavaFileObjects; +import javax.tools.JavaFileObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class DispatcherTest { + + private final AnnotationPrinter annotationPrinter; + + public DispatcherTest(AnnotationPrinter annotationPrinter) { + this.annotationPrinter = annotationPrinter; + } + + @Parameters(name = "{0}") + public static Iterable<AnnotationStrings> getAnnotationPrinters() { + return AnnotationFinder.annotationStrings(); + } + + @Test + public void generatesDispatcherClass() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesConfigurationWithNotesProvider(annotationPrinter), + annotatedNotesProvider(annotationPrinter), + annotatedNotesCrossProfileType(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile( + "com.google.android.enterprise.connectedapps.CrossProfileConnector_Service_Dispatcher"); + } + + @Test + public void specifiedClassName_generatesSpecifiedClassNameDispatcher() { + JavaFileObject notesConfiguration = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConfiguration", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileConfigurationQualifiedName() + ";", + annotationPrinter.crossProfileConfigurationAsAnnotation( + "providers=NotesProvider.class"), + "public abstract class NotesConfiguration {", + "}"); + JavaFileObject crossProfileType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileQualifiedName() + ";", + annotationPrinter.crossProfileAsAnnotation("connector=NotesConnector.class"), + "public final class NotesType {", + annotationPrinter.crossProfileAsAnnotation(), + " public void refreshNotes() {", + " }", + "}"); + JavaFileObject notesConnector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector", + "package " + NOTES_PACKAGE + ";", + "import " + CUSTOM_PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "import " + PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "@CustomProfileConnector(serviceClassName=\"com.google.android.CustomConnector\")", + "public interface NotesConnector extends ProfileConnector {", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + notesConfiguration, + annotatedNotesProvider(annotationPrinter), + notesConnector, + crossProfileType); + + assertThat(compilation).generatedSourceFile("com.google.android.CustomConnector_Dispatcher"); + } +} diff --git a/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/GeneratedProfileConnectorTest.java b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/GeneratedProfileConnectorTest.java new file mode 100644 index 0000000..63b8630 --- /dev/null +++ b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/GeneratedProfileConnectorTest.java @@ -0,0 +1,152 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.GENERATED_PROFILE_CONNECTOR_QUALIFIED_NAME; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.NOTES_PACKAGE; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.PROFILE_CONNECTOR_QUALIFIED_NAME; +import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.Compiler.javac; + +import com.google.testing.compile.Compilation; +import com.google.testing.compile.JavaFileObjects; +import javax.tools.JavaFileObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class GeneratedProfileConnectorTest { + + private static final String NOT_INTERFACE_ERROR = + "@GeneratedProfileConnector must only be applied to interfaces"; + private static final String NOT_IMPLEMENTING_CONNECTOR_ERROR = + "Interfaces annotated with @GeneratedProfileConnector must extend ProfileConnector"; + private static final String ADDITIONAL_METHODS_ERROR = + "Interfaces annotated with @GeneratedProfileConnector can not declare non-static methods"; + + @Test + public void isNotInterface_hasError() { + JavaFileObject connector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector", + "package " + NOTES_PACKAGE + ";", + "import " + GENERATED_PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "import " + PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "@GeneratedProfileConnector", + "public class NotesConnector implements ProfileConnector {", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(connector); + + assertThat(compilation).hadErrorContaining(NOT_INTERFACE_ERROR).inFile(connector); + } + + @Test + public void doesNotExtendProfileConnector_hasError() { + JavaFileObject connector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector", + "package " + NOTES_PACKAGE + ";", + "import " + GENERATED_PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "@GeneratedProfileConnector", + "public interface NotesConnector {", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(connector); + + assertThat(compilation) + .hadErrorContaining(NOT_IMPLEMENTING_CONNECTOR_ERROR) + .inFile(connector); + } + + @Test + public void addsAdditionalMethod_hasError() { + JavaFileObject connector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector", + "package " + NOTES_PACKAGE + ";", + "import " + GENERATED_PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "import " + PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "@GeneratedProfileConnector", + "public interface NotesConnector extends ProfileConnector {", + " String getString();", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(connector); + + assertThat(compilation) + .hadErrorContaining(ADDITIONAL_METHODS_ERROR) + .inFile(connector); + } + + @Test + public void generatedProfileConnector_compiles() { + JavaFileObject connector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector", + "package " + NOTES_PACKAGE + ";", + "import " + GENERATED_PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "import " + PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "@GeneratedProfileConnector", + "public interface NotesConnector extends ProfileConnector {", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(connector); + + assertThat(compilation).generatedSourceFile(NOTES_PACKAGE + ".GeneratedNotesConnector"); + } + + @Test + public void generatedProfileConnector_extendsAbstractProfileConnector() { + JavaFileObject connector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector", + "package " + NOTES_PACKAGE + ";", + "import " + GENERATED_PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "import " + PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "@GeneratedProfileConnector", + "public interface NotesConnector extends ProfileConnector {", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(connector); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".GeneratedNotesConnector") + .contentsAsUtf8String() + .contains("final class GeneratedNotesConnector extends AbstractProfileConnector"); + } + + @Test + public void generatedProfileConnector_implementsConnector() { + JavaFileObject connector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector", + "package " + NOTES_PACKAGE + ";", + "import " + GENERATED_PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "import " + PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "@GeneratedProfileConnector", + "public interface NotesConnector extends ProfileConnector {", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(connector); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".GeneratedNotesConnector") + .contentsAsUtf8String() + .contains("implements NotesConnector"); + } +} diff --git a/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/GeneratedUserConnectorTest.java b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/GeneratedUserConnectorTest.java new file mode 100644 index 0000000..a7fe1f2 --- /dev/null +++ b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/GeneratedUserConnectorTest.java @@ -0,0 +1,152 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.GENERATED_USER_CONNECTOR_QUALIFIED_NAME; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.NOTES_PACKAGE; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.USER_CONNECTOR_QUALIFIED_NAME; +import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.Compiler.javac; + +import com.google.testing.compile.Compilation; +import com.google.testing.compile.JavaFileObjects; +import javax.tools.JavaFileObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class GeneratedUserConnectorTest { + + private static final String NOT_INTERFACE_ERROR = + "@GeneratedUserConnector must only be applied to interfaces"; + private static final String NOT_IMPLEMENTING_CONNECTOR_ERROR = + "Interfaces annotated with @GeneratedUserConnector must extend UserConnector"; + private static final String ADDITIONAL_METHODS_ERROR = + "Interfaces annotated with @GeneratedUserConnector can not declare non-static methods"; + + @Test + public void isNotInterface_hasError() { + JavaFileObject connector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector", + "package " + NOTES_PACKAGE + ";", + "import " + GENERATED_USER_CONNECTOR_QUALIFIED_NAME + ";", + "import " + USER_CONNECTOR_QUALIFIED_NAME + ";", + "@GeneratedUserConnector", + "public class NotesConnector implements UserConnector {", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(connector); + + assertThat(compilation).hadErrorContaining(NOT_INTERFACE_ERROR).inFile(connector); + } + + @Test + public void doesNotExtendProfileConnector_hasError() { + JavaFileObject connector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector", + "package " + NOTES_PACKAGE + ";", + "import " + GENERATED_USER_CONNECTOR_QUALIFIED_NAME + ";", + "@GeneratedUserConnector", + "public interface NotesConnector {", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(connector); + + assertThat(compilation) + .hadErrorContaining(NOT_IMPLEMENTING_CONNECTOR_ERROR) + .inFile(connector); + } + + @Test + public void addsAdditionalMethod_hasError() { + JavaFileObject connector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector", + "package " + NOTES_PACKAGE + ";", + "import " + GENERATED_USER_CONNECTOR_QUALIFIED_NAME + ";", + "import " + USER_CONNECTOR_QUALIFIED_NAME + ";", + "@GeneratedUserConnector", + "public interface NotesConnector extends UserConnector {", + " String getString();", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(connector); + + assertThat(compilation) + .hadErrorContaining(ADDITIONAL_METHODS_ERROR) + .inFile(connector); + } + + @Test + public void generatedUserConnector_compiles() { + JavaFileObject connector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector", + "package " + NOTES_PACKAGE + ";", + "import " + GENERATED_USER_CONNECTOR_QUALIFIED_NAME + ";", + "import " + USER_CONNECTOR_QUALIFIED_NAME + ";", + "@GeneratedUserConnector", + "public interface NotesConnector extends UserConnector {", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(connector); + + assertThat(compilation).generatedSourceFile(NOTES_PACKAGE + ".GeneratedNotesConnector"); + } + + @Test + public void generatedUserConnector_extendsAbstractUserConnector() { + JavaFileObject connector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector", + "package " + NOTES_PACKAGE + ";", + "import " + GENERATED_USER_CONNECTOR_QUALIFIED_NAME + ";", + "import " + USER_CONNECTOR_QUALIFIED_NAME + ";", + "@GeneratedUserConnector", + "public interface NotesConnector extends UserConnector {", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(connector); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".GeneratedNotesConnector") + .contentsAsUtf8String() + .contains("final class GeneratedNotesConnector extends AbstractUserConnector"); + } + + @Test + public void generatedUserConnector_implementsConnector() { + JavaFileObject connector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector", + "package " + NOTES_PACKAGE + ";", + "import " + GENERATED_USER_CONNECTOR_QUALIFIED_NAME + ";", + "import " + USER_CONNECTOR_QUALIFIED_NAME + ";", + "@GeneratedUserConnector", + "public interface NotesConnector extends UserConnector {", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(connector); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".GeneratedNotesConnector") + .contentsAsUtf8String() + .contains("implements NotesConnector"); + } +} diff --git a/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/IfAvailableTest.java b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/IfAvailableTest.java new file mode 100644 index 0000000..59de5e9 --- /dev/null +++ b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/IfAvailableTest.java @@ -0,0 +1,192 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.NOTES_PACKAGE; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesCrossProfileType; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesProvider; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.installationListener; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.installationListenerWithStringParam; +import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.Compiler.javac; + +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationPrinter; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationStrings; +import com.google.testing.compile.Compilation; +import com.google.testing.compile.JavaFileObjects; +import javax.tools.JavaFileObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class IfAvailableTest { + + private final AnnotationPrinter annotationPrinter; + + public IfAvailableTest(AnnotationPrinter annotationPrinter) { + this.annotationPrinter = annotationPrinter; + } + + @Parameters(name = "{0}") + public static Iterable<AnnotationStrings> getAnnotationPrinters() { + return AnnotationFinder.annotationStrings(); + } + + @Test + public void compile_generatesIfAvailableClass() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesProvider(annotationPrinter), + annotatedNotesCrossProfileType(annotationPrinter)); + + assertThat(compilation).generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_IfAvailable"); + } + + @Test + public void compile_synchronousVoidMethod_ifAvailableClassHasMethod() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationPrinter.crossProfileAsAnnotation(), + " public void refreshNotes() {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_IfAvailable") + .contentsAsUtf8String() + .contains("void refreshNotes()"); + } + + @Test + public void compile_synchronousNotVoidMethod_ifAvailableClassHasMethod() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationPrinter.crossProfileAsAnnotation(), + " public int refreshNotes() {", + " return 0;", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_IfAvailable") + .contentsAsUtf8String() + .contains("int refreshNotes(int defaultValue)"); + } + + @Test + public void compile_callbackVoidMethod_ifAvailableClassHasMethod() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationPrinter.crossProfileAsAnnotation(), + " public void refreshNotes(InstallationListener listener) {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + notesType, + annotatedNotesProvider(annotationPrinter), + installationListener(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_IfAvailable") + .contentsAsUtf8String() + .contains("void refreshNotes(InstallationListener listener)"); + } + + @Test + public void compile_callbackNotVoidMethod_ifAvailableClassHasMethod() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationPrinter.crossProfileAsAnnotation(), + " public void refreshNotes(String s, InstallationListener listener) {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + notesType, + annotatedNotesProvider(annotationPrinter), + installationListenerWithStringParam(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_IfAvailable") + .contentsAsUtf8String() + .contains( + "void refreshNotes(String s, InstallationListener listener, String defaultValue)"); + } + + @Test + public void compile_futureMethod_ifAvailableClassHasMethod() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileQualifiedName() + ";", + "import com.google.common.util.concurrent.ListenableFuture;", + "public final class NotesType {", + annotationPrinter.crossProfileAsAnnotation(), + " public ListenableFuture<String> refreshNotes() {", + " return null;", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_IfAvailable") + .contentsAsUtf8String() + .contains("ListenableFuture<String> refreshNotes(String defaultValue)"); + } +} diff --git a/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/InterfaceTest.java b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/InterfaceTest.java new file mode 100644 index 0000000..e59a306 --- /dev/null +++ b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/InterfaceTest.java @@ -0,0 +1,560 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.NOTES_PACKAGE; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesCrossProfileType; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesProvider; +import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.Compiler.javac; + +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationPrinter; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationStrings; +import com.google.testing.compile.Compilation; +import com.google.testing.compile.JavaFileObjects; +import javax.tools.JavaFileObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class InterfaceTest { + + private final AnnotationPrinter annotationPrinter; + + public InterfaceTest(AnnotationPrinter annotationPrinter) { + this.annotationPrinter = annotationPrinter; + } + + @Parameters(name = "{0}") + public static Iterable<AnnotationStrings> getAnnotationPrinters() { + return AnnotationFinder.annotationStrings(); + } + + @Test + public void compile_generatesSingleSenderInterface() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesCrossProfileType(annotationPrinter), + annotatedNotesProvider(annotationPrinter)); + + assertThat(compilation).generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_SingleSender"); + } + + @Test + public void compile_singleAnnotatedMethod_singleSenderInterfaceHasMethod() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationPrinter.crossProfileAsAnnotation(), + " public void refreshNotes() {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_SingleSender") + .contentsAsUtf8String() + .contains("void refreshNotes()"); + } + + @Test + public void compile_multipleAnnotatedMethods_singleSenderInterfaceHasAllMethods() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationPrinter.crossProfileAsAnnotation(), + " public void refreshNotes() {", + " }", + annotationPrinter.crossProfileAsAnnotation(), + " public int anotherMethod(String s) {", + " return 0;", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_SingleSender") + .contentsAsUtf8String() + .contains("void refreshNotes()"); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_SingleSender") + .contentsAsUtf8String() + .contains("int anotherMethod(String s)"); + } + + @Test + public void compile_multipleMethods_singleSenderInterfaceDoesNotHaveUnannotatedMethods() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationPrinter.crossProfileAsAnnotation(), + " public void refreshNotes() {", + " }", + " public int anotherMethod(String s) {", + " return 0;", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_SingleSender") + .contentsAsUtf8String() + .doesNotContain("anotherMethod"); + } + + @Test + public void compile_generatesSingleSenderCanThrowInterface() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesCrossProfileType(annotationPrinter), + annotatedNotesProvider(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_SingleSenderCanThrow"); + } + + @Test + public void compile_singleAnnotatedMethod_singleSenderCanThrowInterfaceHasMethod() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationPrinter.crossProfileAsAnnotation(), + " public void refreshNotes() {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_SingleSenderCanThrow") + .contentsAsUtf8String() + .contains("void refreshNotes() throws UnavailableProfileException"); + } + + @Test + public void compile_multipleAnnotatedMethods_singleSenderCanThrowInterfaceHasAllMethods() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationPrinter.crossProfileAsAnnotation(), + " public void refreshNotes() {", + " }", + annotationPrinter.crossProfileAsAnnotation(), + " public int anotherMethod(String s) {", + " return 0;", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_SingleSenderCanThrow") + .contentsAsUtf8String() + .contains("void refreshNotes() throws UnavailableProfileException"); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_SingleSenderCanThrow") + .contentsAsUtf8String() + .contains("int anotherMethod(String s) throws UnavailableProfileException"); + } + + @Test + public void compile_multipleMethods_singleSenderCanThrowInterfaceDoesNotHaveUnannotatedMethods() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationPrinter.crossProfileAsAnnotation(), + " public void refreshNotes() {", + " }", + " public int anotherMethod(String s) {", + " return 0;", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_SingleSenderCanThrow") + .contentsAsUtf8String() + .doesNotContain("anotherMethod"); + } + + @Test + public void compile_singleSenderCanThrowInterfaceHasIfAvailableMethod() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationPrinter.crossProfileAsAnnotation(), + " public void refreshNotes() {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_SingleSenderCanThrow") + .contentsAsUtf8String() + .contains("ProfileNotesType_IfAvailable ifAvailable()"); + } + + @Test + public void compile_generatesMultipleSenderInterface() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesCrossProfileType(annotationPrinter), + annotatedNotesProvider(annotationPrinter)); + + assertThat(compilation).generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_MultipleSender"); + } + + @Test + public void compile_singleVoidAnnotatedMethod_multipleSenderInterfaceHasMethod() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationPrinter.crossProfileAsAnnotation(), + " public void refreshNotes() {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_MultipleSender") + .contentsAsUtf8String() + .contains("void refreshNotes()"); + } + + @Test + public void compile_singlePrimitiveAnnotatedMethod_multipleSenderInterfaceHasMethod() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationPrinter.crossProfileAsAnnotation(), + " public int refreshNotes() {", + " return 0;", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_MultipleSender") + .contentsAsUtf8String() + .contains("Map<Profile, Integer> refreshNotes()"); + } + + @Test + public void compile_singleObjectAnnotatedMethod_multipleSenderInterfaceHasMethod() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationPrinter.crossProfileAsAnnotation(), + " public String refreshNotes() {", + " return null;", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_MultipleSender") + .contentsAsUtf8String() + .contains("Map<Profile, String> refreshNotes()"); + } + + @Test + public void compile_multipleAnnotatedMethods_multipleSenderInterfaceHasAllMethods() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationPrinter.crossProfileAsAnnotation(), + " public void refreshNotes() {", + " }", + annotationPrinter.crossProfileAsAnnotation(), + " public int anotherMethod(String s) {", + " return 0;", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_MultipleSender") + .contentsAsUtf8String() + .contains("void refreshNotes()"); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_MultipleSender") + .contentsAsUtf8String() + .contains("Map<Profile, Integer> anotherMethod(String s)"); + } + + @Test + public void compile_multipleMethods_multipleSenderInterfaceDoesNotHaveUnannotatedMethods() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationPrinter.crossProfileAsAnnotation(), + " public void refreshNotes() {", + " }", + " public int anotherMethod(String s) {", + " return 0;", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_MultipleSender") + .contentsAsUtf8String() + .doesNotContain("anotherMethod"); + } + + @Test + public void compile_synchronousMethodWithDeclaredException_singleSenderInterfaceHasMethod() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileQualifiedName() + ";", + "import java.io.IOException;", + "public final class NotesType {", + annotationPrinter.crossProfileAsAnnotation(), + " public void refreshNotes() throws IOException {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_SingleSender") + .contentsAsUtf8String() + .contains("refreshNotes() throws IOException"); + } + + @Test + public void + compile_synchronousMethodWithMultipleDeclaredExceptions_singleSenderInterfaceHasMethod() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileQualifiedName() + ";", + "import java.io.IOException;", + "import java.sql.SQLException;", + "public final class NotesType {", + annotationPrinter.crossProfileAsAnnotation(), + " public void refreshNotes() throws IOException, SQLException {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationPrinter)); + + // Order is not predictable + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_SingleSender") + .contentsAsUtf8String() + .contains("refreshNotes() throws "); + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_SingleSender") + .contentsAsUtf8String() + .contains("SQLException"); + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_SingleSender") + .contentsAsUtf8String() + .contains("IOException"); + } + + @Test + public void + compile_synchronousMethodWithDeclaredException_singleSenderCanThrowInterfaceHasMethod() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileQualifiedName() + ";", + "import java.io.IOException;", + "public final class NotesType {", + annotationPrinter.crossProfileAsAnnotation(), + " public void refreshNotes() throws IOException {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_SingleSender") + .contentsAsUtf8String() + .contains("refreshNotes() throws IOException"); + } + + @Test + public void + compile_synchronousMethodWithMultipleDeclaredExceptions_singleSenderCanThrowInterfaceHasMethod() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileQualifiedName() + ";", + "import java.io.IOException;", + "import java.sql.SQLException;", + "public final class NotesType {", + annotationPrinter.crossProfileAsAnnotation(), + " public void refreshNotes() throws IOException, SQLException {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationPrinter)); + + // Order is not predictable + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_SingleSender") + .contentsAsUtf8String() + .contains("refreshNotes() throws "); + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_SingleSender") + .contentsAsUtf8String() + .contains("SQLException"); + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_SingleSender") + .contentsAsUtf8String() + .contains("IOException"); + } + + @Test + public void + compile_synchronousMethodWithDeclaredException_multipleSenderInterfaceDoesNotHaveMethod() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileQualifiedName() + ";", + "import java.io.IOException;", + "public final class NotesType {", + annotationPrinter.crossProfileAsAnnotation(), + " public void refreshNotes() throws IOException {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_MultipleSender") + .contentsAsUtf8String() + .doesNotContain("refreshNotes()"); + } +} diff --git a/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/InternalCrossProfileTypeTest.java b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/InternalCrossProfileTypeTest.java new file mode 100644 index 0000000..d0af193 --- /dev/null +++ b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/InternalCrossProfileTypeTest.java @@ -0,0 +1,103 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.NOTES_PACKAGE; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesCrossProfileType; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesProvider; +import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.Compiler.javac; + +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationPrinter; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationStrings; +import com.google.testing.compile.Compilation; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class InternalCrossProfileTypeTest { + + private final AnnotationPrinter annotationPrinter; + + public InternalCrossProfileTypeTest(AnnotationPrinter annotationPrinter) { + this.annotationPrinter = annotationPrinter; + } + + @Parameters(name = "{0}") + public static Iterable<AnnotationStrings> getAnnotationPrinters() { + return AnnotationFinder.annotationStrings(); + } + + @Test + public void generatesInternalClass() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesProvider(annotationPrinter), + annotatedNotesCrossProfileType(annotationPrinter)); + + assertThat(compilation).generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_Internal"); + } + + @Test + public void internalClassHasPrivateConstructor() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesProvider(annotationPrinter), + annotatedNotesCrossProfileType(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_Internal") + .contentsAsUtf8String() + .contains("private ProfileNotesType_Internal() {"); + } + + @Test + public void internalClassHasPublicCallMethod() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesProvider(annotationPrinter), + annotatedNotesCrossProfileType(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_Internal") + .contentsAsUtf8String() + .contains("public Parcel call(Context context, int methodIdentifier, Parcel params,"); + } + + @Test + public void internalClassHasInstanceMethod() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesProvider(annotationPrinter), + annotatedNotesCrossProfileType(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_Internal") + .contentsAsUtf8String() + .contains("static ProfileNotesType_Internal instance()"); + } +} diff --git a/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/InternalProviderClassTest.java b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/InternalProviderClassTest.java new file mode 100644 index 0000000..2afee1b --- /dev/null +++ b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/InternalProviderClassTest.java @@ -0,0 +1,145 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.NOTES_PACKAGE; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesCrossProfileType; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesProvider; +import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.Compiler.javac; + +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationPrinter; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationStrings; +import com.google.testing.compile.Compilation; +import com.google.testing.compile.JavaFileObjects; +import javax.tools.JavaFileObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class InternalProviderClassTest { + + private final AnnotationPrinter annotationPrinter; + + public InternalProviderClassTest(AnnotationPrinter annotationPrinter) { + this.annotationPrinter = annotationPrinter; + } + + @Parameters(name = "{0}") + public static Iterable<AnnotationStrings> getAnnotationPrinters() { + return AnnotationFinder.annotationStrings(); + } + + @Test + public void generatesInternalProviderClass() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesProvider(annotationPrinter), + annotatedNotesCrossProfileType(annotationPrinter)); + + assertThat(compilation).generatedSourceFile(NOTES_PACKAGE + ".Profile_NotesProvider_Internal"); + } + + @Test + public void internalProviderClassHasPrivateConstructor() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesProvider(annotationPrinter), + annotatedNotesCrossProfileType(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".Profile_NotesProvider_Internal") + .contentsAsUtf8String() + .contains("private Profile_NotesProvider_Internal() {"); + } + + @Test + public void internalProviderClassHasPublicCallMethod() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesProvider(annotationPrinter), + annotatedNotesCrossProfileType(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".Profile_NotesProvider_Internal") + .contentsAsUtf8String() + .contains( + "public Parcel call(Context context, long crossProfileTypeIdentifier, int" + + " methodIdentifier,"); + } + + @Test + public void internalProviderClassHasInstanceMethod() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesProvider(annotationPrinter), + annotatedNotesCrossProfileType(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".Profile_NotesProvider_Internal") + .contentsAsUtf8String() + .contains("public static Profile_NotesProvider_Internal instance()"); + } + + @Test + public void internalProviderClassHasProviderClassGetter() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesProvider(annotationPrinter), + annotatedNotesCrossProfileType(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".Profile_NotesProvider_Internal") + .contentsAsUtf8String() + .contains("public NotesProvider providerClass(Context context)"); + } + + @Test + public void providerClassHasField_compiles() { + final JavaFileObject providerClass = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesProvider", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileProviderQualifiedName() + ";", + "public final class NotesProvider {", + " int test = 3;", + annotationPrinter.crossProfileProviderAsAnnotation(), + " public NotesType provideNotesType() {", + " return new NotesType();", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(providerClass, annotatedNotesCrossProfileType(annotationPrinter)); + + assertThat(compilation).succeededWithoutWarnings(); + } +} diff --git a/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/OtherProfileTest.java b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/OtherProfileTest.java new file mode 100644 index 0000000..8d38369 --- /dev/null +++ b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/OtherProfileTest.java @@ -0,0 +1,90 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.NOTES_PACKAGE; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesCrossProfileType; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesProvider; +import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.Compiler.javac; + +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationPrinter; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationStrings; +import com.google.testing.compile.Compilation; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class OtherProfileTest { + + private final AnnotationPrinter annotationPrinter; + + public OtherProfileTest(AnnotationPrinter annotationPrinter) { + this.annotationPrinter = annotationPrinter; + } + + @Parameters(name = "{0}") + public static Iterable<AnnotationStrings> getAnnotationPrinters() { + return AnnotationFinder.annotationStrings(); + } + + @Test + public void compile_generatesOtherProfileClass() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesProvider(annotationPrinter), + annotatedNotesCrossProfileType(annotationPrinter)); + + assertThat(compilation).generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_OtherProfile"); + } + + @Test + public void compile_otherProfileClassImplementsSingleSender() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesProvider(annotationPrinter), + annotatedNotesCrossProfileType(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_OtherProfile") + .contentsAsUtf8String() + .contains( + "class ProfileNotesType_OtherProfile implements" + " ProfileNotesType_SingleSender"); + } + + @Test + public void compile_otherProfileClassHasConstructorTakingConnector() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesProvider(annotationPrinter), + annotatedNotesCrossProfileType(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_OtherProfile") + .contentsAsUtf8String() + .contains( + "public ProfileNotesType_OtherProfile(ProfileConnector connector)"); + } +} diff --git a/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProcessorCrossProfileConfigurationTest.java b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProcessorCrossProfileConfigurationTest.java new file mode 100644 index 0000000..49b797e --- /dev/null +++ b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProcessorCrossProfileConfigurationTest.java @@ -0,0 +1,463 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.ANNOTATED_NOTES_CONNECTOR; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.NOTES_PACKAGE; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedDifferentCrossProfileType; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedDifferentProvider; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesCrossProfileType; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesProvider; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.formatErrorMessage; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.notesTypeWithDefaultConnector; +import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.Compiler.javac; + +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationStrings; +import com.google.testing.compile.Compilation; +import com.google.testing.compile.JavaFileObjects; +import javax.tools.JavaFileObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class ProcessorCrossProfileConfigurationTest { + + private static final String NOT_A_PROVIDER_CLASS_ERROR = + "All classes specified in 'providers' must be provider classes"; + private static final String CONFIGURATION_DIFFERENT_CONNECTOR_ERROR = + "All @CROSS_PROFILE_ANNOTATION types specified in the same configuration must use the same" + + " ProfileConnector"; + private static final String INCORRECT_SERVICE_CLASS = + "The class specified by serviceClass must match the serviceClassName given by the" + + " ProfileConnector"; + private static final String CONNECTOR_MUST_BE_INTERFACE = "Connectors must be interfaces"; + private static final String CONNECTOR_MUST_EXTEND_CONNECTOR = + "Interfaces specified as a connector must extend ProfileConnector"; + + private final AnnotationStrings annotationStrings; + + public ProcessorCrossProfileConfigurationTest(AnnotationStrings annotationStrings) { + this.annotationStrings = annotationStrings; + } + + @Parameters(name = "{0}") + public static Iterable<AnnotationStrings> getAnnotationPrinters() { + return AnnotationFinder.annotationStrings(); + } + + @Test + public void multipleConfigurations_compiles() { + final JavaFileObject configuration1 = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConfiguration", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileConfigurationQualifiedName() + ";", + "import com.google.android.enterprise.connectedapps.CrossProfileConnector;", + annotationStrings.crossProfileConfigurationAsAnnotation( + "connector=CrossProfileConnector.class"), + "public abstract class NotesConfiguration {", + "}"); + final JavaFileObject configuration2 = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConfiguration2", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileConfigurationQualifiedName() + ";", + annotationStrings.crossProfileConfigurationAsAnnotation( + "connector=NotesConnector.class"), + "public abstract class NotesConfiguration2 {", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(configuration1, configuration2, ANNOTATED_NOTES_CONNECTOR); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void providersContainsNoProviders_compiles() { + final JavaFileObject configuration = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConfiguration", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileConfigurationQualifiedName() + ";", + "import com.google.android.enterprise.connectedapps.CrossProfileConnector;", + annotationStrings.crossProfileConfigurationAsAnnotation( + "connector=CrossProfileConnector.class"), + "public abstract class NotesConfiguration {", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(configuration); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void providersContainsNoProvidersAndNoConnector_generatesCrossProfileConnectorService() { + final JavaFileObject configuration = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConfiguration", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileConfigurationQualifiedName() + ";", + annotationStrings.crossProfileConfigurationAsAnnotation(), + "public abstract class NotesConfiguration {", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(configuration); + + assertThat(compilation) + .generatedSourceFile( + "com.google.android.enterprise.connectedapps.CrossProfileConnector_Service"); + } + + @Test + public void + providersContainsProviderWithoutConnectorAndNoConnector_generatesCrossProfileConnectorService() { + JavaFileObject configuration = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConfiguration", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileConfigurationQualifiedName() + ";", + annotationStrings.crossProfileConfigurationAsAnnotation( + "providers=NotesProvider.class"), + "public abstract class NotesConfiguration {", + "}"); + JavaFileObject noConnectorProvider = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesProvider", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileProviderQualifiedName() + ";", + "public final class NotesProvider {", + annotationStrings.crossProfileProviderAsAnnotation(), + " public NotesType provideNotesType() {", + " return new NotesType();", + " }", + "}"); + JavaFileObject notesTypeWithoutConnector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes() {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(configuration, noConnectorProvider, notesTypeWithoutConnector); + + assertThat(compilation) + .generatedSourceFile( + "com.google.android.enterprise.connectedapps.CrossProfileConnector_Service"); + } + + @Test + public void providersContainsSingleValidProvider_compiles() { + JavaFileObject configuration = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConfiguration", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileConfigurationQualifiedName() + ";", + annotationStrings.crossProfileConfigurationAsAnnotation( + "providers=NotesProvider.class"), + "public abstract class NotesConfiguration {", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + configuration, + annotatedNotesProvider(annotationStrings), + annotatedNotesCrossProfileType(annotationStrings)); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void providersContainsMultipleValidProviders_compiles() { + JavaFileObject configuration = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConfiguration", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileConfigurationQualifiedName() + ";", + annotationStrings.crossProfileConfigurationAsAnnotation( + "providers={NotesProvider.class, DifferentProvider.class}"), + "public abstract class NotesConfiguration {", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + configuration, + annotatedNotesProvider(annotationStrings), + annotatedNotesCrossProfileType(annotationStrings), + annotatedDifferentCrossProfileType(annotationStrings), + annotatedDifferentProvider(annotationStrings)); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void providersContainsNonProvider_hasError() { + JavaFileObject configuration = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConfiguration", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileConfigurationQualifiedName() + ";", + annotationStrings.crossProfileConfigurationAsAnnotation("providers={String.class}"), + "public abstract class NotesConfiguration {", + "}"); + + Compilation compilation = javac().withProcessors(new Processor()).compile(configuration); + + assertThat(compilation).hadErrorContaining(NOT_A_PROVIDER_CLASS_ERROR).inFile(configuration); + } + + @Test + public void isNotAbstract_compiles() { + JavaFileObject notAbstractConfiguration = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConfiguration", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileConfigurationQualifiedName() + ";", + "import com.google.android.enterprise.connectedapps.CrossProfileConnector;", + annotationStrings.crossProfileConfigurationAsAnnotation( + "connector=CrossProfileConnector.class"), + "public class NotesConfiguration {", + "}"); + + Compilation compilation = + javac().withProcessors(new Processor()).compile(notAbstractConfiguration); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void hasCrossProfileTypeWithDifferentConnectors_hasError() { + JavaFileObject configurationClassWithDifferentConnectors = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConfiguration", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileConfigurationQualifiedName() + ";", + annotationStrings.crossProfileConfigurationAsAnnotation( + "providers={NotesProvider.class, NotesProvider2.class}"), + "abstract class NotesConfiguration {", + "}"); + JavaFileObject providerClassWithDefaultConnector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesProvider", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileProviderQualifiedName() + ";", + "public final class NotesProvider {", + annotationStrings.crossProfileProviderAsAnnotation(), + " public NotesType provideNotesType() {", + " return new NotesType();", + " }", + "}"); + JavaFileObject providerClassWithNotesConnector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesProvider2", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileProviderQualifiedName() + ";", + "public final class NotesProvider2 {", + annotationStrings.crossProfileProviderAsAnnotation(), + " public NotesType2 provideNotesType2() {", + " return new NotesType2();", + " }", + "}"); + JavaFileObject notesTypeWithCrossProfileConnector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + "import com.google.android.enterprise.connectedapps.CrossProfileConnector;", + annotationStrings.crossProfileAsAnnotation("connector=CrossProfileConnector.class"), + "public final class NotesType {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes() {", + " }", + "}"); + JavaFileObject notesType2WithNotesConnector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType2", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileQualifiedName() + ";", + annotationStrings.crossProfileAsAnnotation("connector=NotesConnector.class"), + "public final class NotesType2 {", + annotationStrings.crossProfileAsAnnotation(), + " public void refreshNotes() {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + configurationClassWithDifferentConnectors, + providerClassWithDefaultConnector, + providerClassWithNotesConnector, + notesTypeWithCrossProfileConnector, + notesType2WithNotesConnector, + ANNOTATED_NOTES_CONNECTOR); + + assertThat(compilation) + .hadErrorContaining( + formatErrorMessage(CONFIGURATION_DIFFERENT_CONNECTOR_ERROR, annotationStrings)) + .inFile(configurationClassWithDifferentConnectors); + } + + @Test + public void correctServiceClass_compiles() { + JavaFileObject configuration = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConfiguration", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileConfigurationQualifiedName() + ";", + "import com.google.android.enterprise.connectedapps.CrossProfileConnector;", + annotationStrings.crossProfileConfigurationAsAnnotation( + "serviceClass=com.google.android.enterprise.connectedapps.CrossProfileConnector_Service.class," + + " providers=NotesProvider.class, connector=CrossProfileConnector.class"), + "public abstract class NotesConfiguration {", + "}"); + JavaFileObject serviceClass = + JavaFileObjects.forSourceLines( + "com.google.android.enterprise.connectedapps.CrossProfileConnector_Service", + "package com.google.android.enterprise.connectedapps;", + "import " + annotationStrings.crossProfileConfigurationQualifiedName() + ";", + "import android.app.Service;", + "import android.content.Intent;", + "import android.os.Binder;", + "public final class CrossProfileConnector_Service extends Service {", + " @Override", + " public Binder onBind(Intent intent) {", + " return null;", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + configuration, + serviceClass, + annotatedNotesProvider(annotationStrings), + notesTypeWithDefaultConnector(annotationStrings)); + + assertThat(compilation).succeededWithoutWarnings(); + } + + @Test + public void incorrectlyNamedServiceClass_hasError() { + JavaFileObject configuration = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConfiguration", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileConfigurationQualifiedName() + ";", + "import com.google.android.enterprise.connectedapps.CrossProfileConnector;", + annotationStrings.crossProfileConfigurationAsAnnotation( + "serviceClass=com.google.android.enterprise.connectedapps.CrossProfileConnector_Service2.class," + + " providers=NotesProvider.class, connector=CrossProfileConnector.class"), + "public abstract class NotesConfiguration {", + "}"); + JavaFileObject serviceClass = + JavaFileObjects.forSourceLines( + "com.google.android.enterprise.connectedapps.CrossProfileConnector_Service2", + "package com.google.android.enterprise.connectedapps;", + "import " + annotationStrings.crossProfileConfigurationQualifiedName() + ";", + "import android.app.Service;", + "import android.content.Intent;", + "import android.os.Binder;", + "public final class CrossProfileConnector_Service2 extends Service {", + " @Override", + " public Binder onBind(Intent intent) {", + " return null;", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + configuration, + serviceClass, + annotatedNotesProvider(annotationStrings), + notesTypeWithDefaultConnector(annotationStrings)); + + assertThat(compilation).hadErrorContaining(INCORRECT_SERVICE_CLASS).inFile(configuration); + } + + @Test + public void specifiesNotInterfaceConnector_hasError() { + final JavaFileObject configuration = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConfiguration", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileConfigurationQualifiedName() + ";", + annotationStrings.crossProfileConfigurationAsAnnotation( + "connector=NotesConnector.class"), + "public abstract class NotesConfiguration {", + "}"); + + JavaFileObject notesConnector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector", + "package " + NOTES_PACKAGE + ";", + "public class NotesConnector {", + "}"); + + Compilation compilation = + javac().withProcessors(new Processor()).compile(configuration, notesConnector); + + assertThat(compilation).hadErrorContaining(CONNECTOR_MUST_BE_INTERFACE).inFile(configuration); + } + + @Test + public void specifiesConnectorNotExtendingProfileConnector_hasError() { + final JavaFileObject configuration = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConfiguration", + "package " + NOTES_PACKAGE + ";", + "import " + annotationStrings.crossProfileConfigurationQualifiedName() + ";", + annotationStrings.crossProfileConfigurationAsAnnotation( + "connector=NotesConnector.class"), + "public abstract class NotesConfiguration {", + "}"); + + JavaFileObject notesConnector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector", + "package " + NOTES_PACKAGE + ";", + "public interface NotesConnector {", + "}"); + + Compilation compilation = + javac().withProcessors(new Processor()).compile(configuration, notesConnector); + + assertThat(compilation) + .hadErrorContaining(CONNECTOR_MUST_EXTEND_CONNECTOR) + .inFile(configuration); + } +} diff --git a/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProcessorCurrentProfileTest.java b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProcessorCurrentProfileTest.java new file mode 100644 index 0000000..b009f2f --- /dev/null +++ b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProcessorCurrentProfileTest.java @@ -0,0 +1,90 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.NOTES_PACKAGE; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesCrossProfileType; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesProvider; +import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.Compiler.javac; + +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationPrinter; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationStrings; +import com.google.testing.compile.Compilation; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class ProcessorCurrentProfileTest { + + private final AnnotationPrinter annotationPrinter; + + public ProcessorCurrentProfileTest(AnnotationPrinter annotationPrinter) { + this.annotationPrinter = annotationPrinter; + } + + @Parameters(name = "{0}") + public static Iterable<AnnotationStrings> getAnnotationPrinters() { + return AnnotationFinder.annotationStrings(); + } + + @Test + public void compile_generatesCurrentProfileClass() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesProvider(annotationPrinter), + annotatedNotesCrossProfileType(annotationPrinter)); + + assertThat(compilation).generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_CurrentProfile"); + } + + @Test + public void compile_currentProfileClassImplementsSingleSender() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesProvider(annotationPrinter), + annotatedNotesCrossProfileType(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_CurrentProfile") + .contentsAsUtf8String() + .contains( + "class ProfileNotesType_CurrentProfile implements" + " ProfileNotesType_SingleSender"); + } + + @Test + public void compile_currentProfileClassHasConstructorTakingCrossProfileType() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesProvider(annotationPrinter), + annotatedNotesCrossProfileType(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_CurrentProfile") + .contentsAsUtf8String() + .contains( + "public ProfileNotesType_CurrentProfile(Context context, NotesType crossProfileType)"); + } +} diff --git a/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProcessorMultipleProfilesTest.java b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProcessorMultipleProfilesTest.java new file mode 100644 index 0000000..2f732d1 --- /dev/null +++ b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProcessorMultipleProfilesTest.java @@ -0,0 +1,96 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.NOTES_PACKAGE; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesCrossProfileType; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesProvider; +import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.Compiler.javac; + +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationPrinter; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationStrings; +import com.google.testing.compile.Compilation; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class ProcessorMultipleProfilesTest { + + private final AnnotationPrinter annotationPrinter; + + public ProcessorMultipleProfilesTest(AnnotationPrinter annotationPrinter) { + this.annotationPrinter = annotationPrinter; + } + + @Parameters(name = "{0}") + public static Iterable<AnnotationStrings> getAnnotationPrinters() { + return AnnotationFinder.annotationStrings(); + } + + @Test + public void compile_generatesMultipleProfilesClass() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesProvider(annotationPrinter), + annotatedNotesCrossProfileType(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_MultipleProfiles"); + } + + @Test + public void compile_multipleProfilesClassImplementsMultipleSender() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesProvider(annotationPrinter), + annotatedNotesCrossProfileType(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_MultipleProfiles") + .contentsAsUtf8String() + .contains( + "class ProfileNotesType_MultipleProfiles implements" + + " ProfileNotesType_MultipleSender"); + } + + @Test + public void compile_multipleProfilesClassHasConstructorTakingSenders() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesProvider(annotationPrinter), + annotatedNotesCrossProfileType(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_MultipleProfiles") + .contentsAsUtf8String() + .contains("public ProfileNotesType_MultipleProfiles("); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType_MultipleProfiles") + .contentsAsUtf8String() + .contains("Map<Profile, ProfileNotesType_SingleSenderCanThrow>" + " senders) {"); + } +} diff --git a/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProfileInterfaceTest.java b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProfileInterfaceTest.java new file mode 100644 index 0000000..7922750 --- /dev/null +++ b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProfileInterfaceTest.java @@ -0,0 +1,225 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.CUSTOM_PROFILE_CONNECTOR_QUALIFIED_NAME; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.NOTES_PACKAGE; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.PROFILE_CONNECTOR_QUALIFIED_NAME; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesCrossProfileType; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesProvider; +import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.Compiler.javac; + +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationPrinter; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationStrings; +import com.google.testing.compile.Compilation; +import com.google.testing.compile.JavaFileObjects; +import javax.tools.JavaFileObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class ProfileInterfaceTest { + + private final AnnotationPrinter annotationPrinter; + + public ProfileInterfaceTest(AnnotationPrinter annotationPrinter) { + this.annotationPrinter = annotationPrinter; + } + + @Parameters(name = "{0}") + public static Iterable<AnnotationStrings> getAnnotationPrinters() { + return AnnotationFinder.annotationStrings(); + } + + @Test + public void compile_generatesProfileInterface() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesProvider(annotationPrinter), + annotatedNotesCrossProfileType(annotationPrinter)); + + assertThat(compilation).generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType"); + } + + @Test + public void compile_profileInterfaceHasCreateMethod() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesProvider(annotationPrinter), + annotatedNotesCrossProfileType(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType") + .contentsAsUtf8String() + .contains("static ProfileNotesType create(ProfileConnector connector)"); + } + + @Test + public void compile_profileInterfaceContainsExpectedMethods() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesProvider(annotationPrinter), + annotatedNotesCrossProfileType(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType") + .contentsAsUtf8String() + .contains("ProfileNotesType_SingleSender current()"); + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType") + .contentsAsUtf8String() + .contains("ProfileNotesType_SingleSenderCanThrow other()"); + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType") + .contentsAsUtf8String() + // We ignore the "profile" argument as it gets moved onto another line by the processor + .contains("ProfileNotesType_SingleSenderCanThrow profile("); + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType") + .contentsAsUtf8String() + .contains("ProfileNotesType_MultipleSender both()"); + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType") + .contentsAsUtf8String() + .contains("ProfileNotesType_MultipleSender profiles("); + } + + @Test + public void + compile_withoutPrimaryProfile_profileInterfaceDoesNotContainPrimarySecondaryMethods() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileQualifiedName() + ";", + annotationPrinter.crossProfileAsAnnotation("connector=NotesConnector.class"), + "public final class NotesType {", + annotationPrinter.crossProfileAsAnnotation(), + " public void refreshNotes() {", + " }", + "}"); + JavaFileObject notesConnector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector", + "package " + NOTES_PACKAGE + ";", + "import " + PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "public interface NotesConnector extends ProfileConnector {", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationPrinter), notesConnector); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType") + .contentsAsUtf8String() + .doesNotContain("primary()"); + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType") + .contentsAsUtf8String() + .doesNotContain("secondary()"); + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType") + .contentsAsUtf8String() + .doesNotContain("suppliers()"); + } + + @Test + public void compile_withPrimaryProfile_profileInterfaceDoesContainPrimarySecondaryMethods() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileQualifiedName() + ";", + annotationPrinter.crossProfileAsAnnotation("connector=NotesConnector.class"), + "public final class NotesType {", + annotationPrinter.crossProfileAsAnnotation(), + " public void refreshNotes() {", + " }", + "}"); + JavaFileObject notesConnector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector", + "package " + NOTES_PACKAGE + ";", + "import " + CUSTOM_PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "import " + PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "@CustomProfileConnector(primaryProfile=CustomProfileConnector.ProfileType.WORK)", + "public interface NotesConnector extends ProfileConnector {", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationPrinter), notesConnector); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType") + .contentsAsUtf8String() + .contains("ProfileNotesType_SingleSenderCanThrow primary()"); + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType") + .contentsAsUtf8String() + .contains("ProfileNotesType_SingleSenderCanThrow secondary()"); + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType") + .contentsAsUtf8String() + .contains("ProfileNotesType_MultipleSender suppliers()"); + } + + @Test + public void compile_withoutConnector_profileInterfaceDoesContainPrimarySecondaryMethods() { + JavaFileObject notesType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileQualifiedName() + ";", + annotationPrinter.crossProfileAsAnnotation(), + "public final class NotesType {", + annotationPrinter.crossProfileAsAnnotation(), + " public void refreshNotes() {", + " }", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile(notesType, annotatedNotesProvider(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType") + .contentsAsUtf8String() + .contains("ProfileNotesType_SingleSenderCanThrow primary()"); + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType") + .contentsAsUtf8String() + .contains("ProfileNotesType_SingleSenderCanThrow secondary()"); + assertThat(compilation) + .generatedSourceFile(NOTES_PACKAGE + ".ProfileNotesType") + .contentsAsUtf8String() + .contains("ProfileNotesType_MultipleSender suppliers()"); + } +} diff --git a/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ServiceTest.java b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ServiceTest.java new file mode 100644 index 0000000..b1ea7f0 --- /dev/null +++ b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ServiceTest.java @@ -0,0 +1,165 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.CUSTOM_PROFILE_CONNECTOR_QUALIFIED_NAME; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.NOTES_PACKAGE; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.PROFILE_CONNECTOR_QUALIFIED_NAME; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesConfigurationWithNotesProvider; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesCrossProfileType; +import static com.google.android.enterprise.connectedapps.processor.TestUtilities.annotatedNotesProvider; +import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.Compiler.javac; + +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationPrinter; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationStrings; +import com.google.testing.compile.Compilation; +import com.google.testing.compile.JavaFileObjects; +import javax.tools.JavaFileObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class ServiceTest { + + private final AnnotationPrinter annotationPrinter; + + public ServiceTest(AnnotationPrinter annotationPrinter) { + this.annotationPrinter = annotationPrinter; + } + + @Parameters(name = "{0}") + public static Iterable<AnnotationStrings> getAnnotationPrinters() { + return AnnotationFinder.annotationStrings(); + } + + @Test + public void generatesServiceClass() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesConfigurationWithNotesProvider(annotationPrinter), + annotatedNotesProvider(annotationPrinter), + annotatedNotesCrossProfileType(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile( + "com.google.android.enterprise.connectedapps.CrossProfileConnector_Service"); + } + + @Test + public void serviceClassExtendsService() { + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + annotatedNotesConfigurationWithNotesProvider(annotationPrinter), + annotatedNotesProvider(annotationPrinter), + annotatedNotesCrossProfileType(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile( + "com.google.android.enterprise.connectedapps.CrossProfileConnector_Service") + .contentsAsUtf8String() + .contains("CrossProfileConnector_Service extends Service"); + } + + @Test + public void serviceClass_specifiedAlternativeClass_extendsAlternativeServiceClass() { + JavaFileObject serviceBaseClass = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".ServiceBaseClass", + "package " + NOTES_PACKAGE + ";", + "import android.app.Service;", + "public abstract class ServiceBaseClass extends Service {", + "}"); + JavaFileObject notesConfiguration = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConfiguration", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileConfigurationQualifiedName() + ";", + annotationPrinter.crossProfileConfigurationAsAnnotation( + "serviceSuperclass=ServiceBaseClass.class, providers=NotesProvider.class"), + "public abstract class NotesConfiguration {", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + notesConfiguration, + serviceBaseClass, + annotatedNotesProvider(annotationPrinter), + annotatedNotesCrossProfileType(annotationPrinter)); + + assertThat(compilation) + .generatedSourceFile( + "com.google.android.enterprise.connectedapps.CrossProfileConnector_Service") + .contentsAsUtf8String() + .contains("CrossProfileConnector_Service extends ServiceBaseClass"); + } + + @Test + public void serviceClass_specifiedClassName_generatesSpecifiedClassName() { + JavaFileObject notesConfiguration = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConfiguration", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileConfigurationQualifiedName() + ";", + annotationPrinter.crossProfileConfigurationAsAnnotation( + "providers=NotesProvider.class"), + "public abstract class NotesConfiguration {", + "}"); + JavaFileObject crossProfileType = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileQualifiedName() + ";", + annotationPrinter.crossProfileAsAnnotation("connector=NotesConnector.class"), + "public final class NotesType {", + annotationPrinter.crossProfileAsAnnotation(), + " public void refreshNotes() {", + " }", + "}"); + JavaFileObject notesConnector = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector", + "package " + NOTES_PACKAGE + ";", + "import " + CUSTOM_PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "import " + PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "@CustomProfileConnector(serviceClassName=\"com.google.android.CustomConnector\")", + "public interface NotesConnector extends ProfileConnector {", + "}"); + + Compilation compilation = + javac() + .withProcessors(new Processor()) + .compile( + notesConfiguration, + annotatedNotesProvider(annotationPrinter), + notesConnector, + crossProfileType); + + assertThat(compilation) + .generatedSourceFile("com.google.android.CustomConnector") + .contentsAsUtf8String() + .contains("CustomConnector extends Service"); + } +} diff --git a/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/TestUtilities.java b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/TestUtilities.java new file mode 100644 index 0000000..b1c05bf --- /dev/null +++ b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/TestUtilities.java @@ -0,0 +1,599 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationNames; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationPrinter; +import com.google.testing.compile.JavaFileObjects; +import java.util.ArrayList; +import java.util.List; +import javax.tools.JavaFileObject; + +final class TestUtilities { + + public static final String CROSS_PROFILE_QUALIFIED_NAME = + "com.google.android.enterprise.connectedapps.annotations.CrossProfile"; + public static final String CUSTOM_PROFILE_CONNECTOR_QUALIFIED_NAME = + "com.google.android.enterprise.connectedapps.annotations.CustomProfileConnector"; + public static final String CUSTOM_USER_CONNECTOR_QUALIFIED_NAME = + "com.google.android.enterprise.connectedapps.annotations.CustomUserConnector"; + public static final String GENERATED_PROFILE_CONNECTOR_QUALIFIED_NAME = + "com.google.android.enterprise.connectedapps.annotations.GeneratedProfileConnector"; + public static final String GENERATED_USER_CONNECTOR_QUALIFIED_NAME = + "com.google.android.enterprise.connectedapps.annotations.GeneratedUserConnector"; + public static final String PROFILE_CONNECTOR_QUALIFIED_NAME = + "com.google.android.enterprise.connectedapps.ProfileConnector"; + public static final String USER_CONNECTOR_QUALIFIED_NAME = + "com.google.android.enterprise.connectedapps.UserConnector"; + public static final String NOTES_PACKAGE = "com.google.android.enterprise.notes"; + + public static final String UNSUPPORTED_TYPE_NAME = "UnsupportedType"; + public static final JavaFileObject UNSUPPORTED_TYPE = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".UnsupportedType", + "package " + NOTES_PACKAGE + ";", + "public final class UnsupportedType {}"); + + public static final JavaFileObject ANNOTATED_NOTES_CONNECTOR = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConnector", + "package " + NOTES_PACKAGE + ";", + "import " + PROFILE_CONNECTOR_QUALIFIED_NAME + ";", + "public interface NotesConnector extends ProfileConnector {", + "}"); + + public static final String INSTALLATION_LISTENER_NAME = "InstallationListener"; + + public static final JavaFileObject PARCELABLE_OBJECT = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".ParcelableObject", + "package " + NOTES_PACKAGE + ";", + "import android.os.Parcel;", + "import android.os.Parcelable;", + "import java.util.Objects;", + "public final class ParcelableObject implements Parcelable {", + "@SuppressWarnings(\"rawtypes\")", + "public static final Parcelable.Creator CREATOR =", + "new Parcelable.Creator() {", + "@Override", + "public ParcelableObject createFromParcel(Parcel in) {", + "return new ParcelableObject(in);", + "}", + "@Override", + "public ParcelableObject[] newArray(int size) {", + "return new ParcelableObject[size];", + "}", + "};", + "private String value;", + "public String value() {", + "return value;", + "}", + "public ParcelableObject(Parcel in) {", + "this(in.readString());", + "}", + "public ParcelableObject(String value) {", + "this.value = value;", + "}", + "@Override", + "public int describeContents() {", + "return 0;", + "}", + "@Override", + "public void writeToParcel(Parcel dest, int flags) {", + "dest.writeString(value);", + "}", + "@Override", + "public boolean equals(Object o) {", + "if (this == o) {", + "return true;", + "}", + "if (o == null || getClass() != o.getClass()) {", + "return false;", + "}", + "ParcelableObject that = (ParcelableObject) o;", + "return value.equals(that.value);", + "}", + "@Override", + "public int hashCode() {", + "return Objects.hash(value);", + "}", + "}"); + + public static final JavaFileObject GENERIC_PARCELABLE_OBJECT = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".GenericParcelableObject", + "package " + NOTES_PACKAGE + ";", + "import android.os.Parcel;", + "import android.os.Parcelable;", + "import java.util.Objects;", + "public final class GenericParcelableObject<E> implements Parcelable {", + "@SuppressWarnings(\"rawtypes\")", + "public static final Parcelable.Creator CREATOR =", + "new Parcelable.Creator() {", + "@Override", + "public GenericParcelableObject createFromParcel(Parcel in) {", + "return new GenericParcelableObject(in);", + "}", + "@Override", + "public GenericParcelableObject[] newArray(int size) {", + "return new GenericParcelableObject[size];", + "}", + "};", + "private String value;", + "public String value() {", + "return value;", + "}", + "public GenericParcelableObject(Parcel in) {", + "this(in.readString());", + "}", + "public GenericParcelableObject(String value) {", + "this.value = value;", + "}", + "@Override", + "public int describeContents() {", + "return 0;", + "}", + "@Override", + "public void writeToParcel(Parcel dest, int flags) {", + "dest.writeString(value);", + "}", + "@Override", + "public boolean equals(Object o) {", + "if (this == o) {", + "return true;", + "}", + "if (o == null || getClass() != o.getClass()) {", + "return false;", + "}", + "GenericParcelableObject that = (GenericParcelableObject) o;", + "return value.equals(that.value);", + "}", + "@Override", + "public int hashCode() {", + "return Objects.hash(value);", + "}", + "}"); + + public static final JavaFileObject SERIALIZABLE_OBJECT = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".SerializableObject", + "package " + NOTES_PACKAGE + ";", + "import java.io.Serializable;", + "import java.util.Objects;", + "public final class SerializableObject implements Serializable {", + "private final String value;", + "public String value() {", + "return value;", + "}", + "public SerializableObject(String value) {", + "this.value = value;", + "}", + "@Override", + "public boolean equals(Object o) {", + "if (this == o) {", + "return true;", + "}", + "if (o == null || getClass() != o.getClass()) {", + "return false;", + "}", + "SerializableObject that = (SerializableObject) o;", + "return Objects.equals(value, that.value);", + "}", + "@Override", + "public int hashCode() {", + "return Objects.hash(value);", + "}", + "}"); + + public static final JavaFileObject GENERIC_SERIALIZABLE_OBJECT = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".GenericSerializableObject", + "package " + NOTES_PACKAGE + ";", + "import java.io.Serializable;", + "import java.util.Objects;", + "public class GenericSerializableObject<R> implements Serializable {", + "private final String value;", + "public String value() {", + "return value;", + "}", + "public GenericSerializableObject(String value) {", + "this.value = value;", + "}", + "@Override", + "public boolean equals(Object o) {", + "if (this == o) {", + "return true;", + "}", + "if (o == null || getClass() != o.getClass()) {", + "return false;", + "}", + "GenericSerializableObject that = (GenericSerializableObject) o;", + "return Objects.equals(value, that.value);", + "}", + "@Override", + "public int hashCode() {", + "return Objects.hash(value);", + "}", + "}"); + + static final JavaFileObject CUSTOM_WRAPPER = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".CustomWrapper", + "package " + NOTES_PACKAGE + ";", + "import java.util.Objects;", + "public class CustomWrapper<F> {", + "private F value;", + "public CustomWrapper(F value) {", + "this.value = value;", + "}", + "public F value() {", + "return value;", + "}", + "@Override", + "public boolean equals(Object o) {", + "if (this == o) {", + "return true;", + "}", + "if (o == null || getClass() != o.getClass()) {", + "return false;", + "}", + "CustomWrapper that = (CustomWrapper) o;", + "return Objects.equals(value, that.value);", + "}", + "@Override", + "public int hashCode() {", + "return Objects.hash(value);", + "}", + "}"); + + static final JavaFileObject PARCELABLE_CUSTOM_WRAPPER = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".ParcelableCustomWrapper", + "package " + NOTES_PACKAGE + ";", + "import android.os.Parcel;", + "import android.os.Parcelable;", + "import com.google.android.enterprise.connectedapps.annotations.CustomParcelableWrapper;", + "import com.google.android.enterprise.connectedapps.internal.Bundler;", + "import com.google.android.enterprise.connectedapps.internal.BundlerType;", + "@CustomParcelableWrapper(originalType = CustomWrapper.class)", + "public class ParcelableCustomWrapper<E> implements Parcelable {", + "private static final int NULL = -1;", + "private static final int NOT_NULL = 1;", + "private final Bundler bundler;", + "private final BundlerType type;", + "private final CustomWrapper<E> customWrapper;", + "public static <F> ParcelableCustomWrapper<F> of(", + "Bundler bundler, BundlerType type, CustomWrapper<F> customWrapper) {", + "return new ParcelableCustomWrapper<>(bundler, type, customWrapper);", + "}", + "public CustomWrapper<E> get() {", + "return customWrapper;", + "}", + "private ParcelableCustomWrapper(Bundler bundler, BundlerType type, CustomWrapper<E>" + + " customWrapper) {", + "if (bundler == null || type == null) {", + "throw new NullPointerException();", + "}", + "this.bundler = bundler;", + "this.type = type;", + "this.customWrapper = customWrapper;", + "}", + "private ParcelableCustomWrapper(Parcel in) {", + "bundler = in.readParcelable(Bundler.class.getClassLoader());", + "int presentValue = in.readInt();", + "if (presentValue == NULL) {", + "type = null;", + "customWrapper = null;", + "return;", + "}", + "type = (BundlerType) in.readParcelable(Bundler.class.getClassLoader());", + "BundlerType valueType = type.typeArguments().get(0);", + "@SuppressWarnings(\"unchecked\")", + "E value = (E) bundler.readFromParcel(in, valueType);", + "customWrapper = new CustomWrapper<>(value);", + "}", + "@Override", + "public void writeToParcel(Parcel dest, int flags) {", + "dest.writeParcelable(bundler, flags);", + "if (customWrapper == null) {", + "dest.writeInt(NULL);", + "return;", + "}", + "dest.writeInt(NOT_NULL);", + "dest.writeParcelable(type, flags);", + "BundlerType valueType = type.typeArguments().get(0);", + "bundler.writeToParcel(dest, customWrapper.value(), valueType, flags);", + "}", + "@Override", + "public int describeContents() {", + "return 0;", + "}", + "@SuppressWarnings(\"rawtypes\")", + "public static final Creator<ParcelableCustomWrapper> CREATOR =", + "new Creator<ParcelableCustomWrapper>() {", + "@Override", + "public ParcelableCustomWrapper createFromParcel(Parcel in) {", + "return new ParcelableCustomWrapper(in);", + "}", + "@Override", + "public ParcelableCustomWrapper[] newArray(int size) {", + "return new ParcelableCustomWrapper[size];", + "}", + "};", + "}"); + + static final JavaFileObject SIMPLE_FUTURE = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".SimpleFuture", + "package " + NOTES_PACKAGE + ";", + "import java.util.concurrent.CountDownLatch;", + "import java.util.function.Consumer;", + "public class SimpleFuture<E> {", + "private E value;", + "private Throwable thrown;", + "private final CountDownLatch countDownLatch = new CountDownLatch(1);", + "private Consumer<E> callback;", + "private Consumer<Throwable> exceptionCallback;", + "public void set(E value) {", + "this.value = value;", + "countDownLatch.countDown();", + "if (callback != null) {", + "callback.accept(value);", + "}", + "}", + "public void setException(Throwable t) {", + "this.thrown = t;", + "countDownLatch.countDown();", + "if (exceptionCallback != null) {", + "exceptionCallback.accept(thrown);", + "}", + "}", + "public E get() {", + "try {", + "countDownLatch.await();", + "} catch (InterruptedException e) {", + "return null;", + "}", + "if (thrown != null) {", + "throw new RuntimeException(thrown);", + "}", + "return value;", + "}", + "public void setCallback(Consumer<E> callback, Consumer<Throwable> exceptionCallback) {", + "if (value != null) {", + "callback.accept(value);", + "} else if (thrown != null) {", + "exceptionCallback.accept(thrown);", + "} else {", + "this.callback = callback;", + "this.exceptionCallback = exceptionCallback;", + "}", + "}", + "}"); + + static final JavaFileObject SIMPLE_FUTURE_WRAPPER = + JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".SimpleFutureWrapper", + "package " + NOTES_PACKAGE + ";", + "import com.google.android.enterprise.connectedapps.FutureWrapper;", + "import com.google.android.enterprise.connectedapps.internal.FutureResultWriter;", + "import com.google.android.enterprise.connectedapps.Profile;", + "import com.google.android.enterprise.connectedapps.internal.Bundler;", + "import com.google.android.enterprise.connectedapps.internal.BundlerType;", + "import" + + " com.google.android.enterprise.connectedapps.internal.CrossProfileCallbackMultiMerger;", + "import java.util.Map;", + "@com.google.android.enterprise.connectedapps.annotations.CustomFutureWrapper(", + "originalType = SimpleFuture.class)", + "public final class SimpleFutureWrapper<E> extends FutureWrapper<E> {", + "private final SimpleFuture<E> future = new SimpleFuture<>();", + "public static <E> SimpleFutureWrapper<E> create(Bundler bundler, BundlerType" + + " bundlerType) {", + "return new SimpleFutureWrapper<>(bundler, bundlerType);", + "}", + "private SimpleFutureWrapper(Bundler bundler, BundlerType bundlerType) {", + "super(bundler, bundlerType);", + "}", + "public SimpleFuture<E> getFuture() {", + "return future;", + "}", + "@Override", + "public void onResult(E result) {", + "future.set(result);", + "}", + "@Override", + "public void onException(Throwable throwable) {", + "future.setException(throwable);", + "}", + "public static <E> void writeFutureResult(", + "SimpleFuture<E> future,", + "FutureResultWriter<E> resultWriter) {", + "future.setCallback(", + "(value) -> {", + "resultWriter.onSuccess(value);", + "},", + "(exception) -> {", + "resultWriter.onFailure(exception);", + "});", + "}", + "public static <E> SimpleFuture<Map<Profile, E>> groupResults(", + "Map<Profile, SimpleFuture<E>> results) {", + "SimpleFuture<Map<Profile, E>> m = new SimpleFuture<>();", + "CrossProfileCallbackMultiMerger<E> merger =", + "new CrossProfileCallbackMultiMerger<>(results.size(), m::set);", + "for (Map.Entry<Profile, SimpleFuture<E>> result : results.entrySet()) {", + "result", + ".getValue()", + ".setCallback(", + "(value) -> {", + "merger.onResult(result.getKey(), value);", + "},", + "(throwable) -> {", + "merger.missingResult(result.getKey());", + "});", + "}", + "return m;", + "}", + "}"); + + public static JavaFileObject annotatedNotesCrossProfileType(AnnotationPrinter annotationPrinter) { + return JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationPrinter.crossProfileAsAnnotation(), + " public void refreshNotes() {", + " }", + "}"); + } + + public static JavaFileObject notesCrossProfileTypeWhichUsesInstallationListener( + AnnotationPrinter annotationPrinter) { + return JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileQualifiedName() + ";", + "public final class NotesType {", + annotationPrinter.crossProfileAsAnnotation(), + " public void install(InstallationListener l) {", + " }", + "}"); + } + + public static JavaFileObject annotatedNotesProvider(AnnotationPrinter annotationPrinter) { + return JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesProvider", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileProviderQualifiedName() + ";", + "public final class NotesProvider {", + annotationPrinter.crossProfileProviderAsAnnotation(), + " public NotesType provideNotesType() {", + " return new NotesType();", + " }", + "}"); + } + + public static JavaFileObject installationListener(AnnotationPrinter annotationPrinter) { + return JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".InstallationListener", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileCallbackQualifiedName() + ";", + annotationPrinter.crossProfileCallbackAsAnnotation(), + "public interface InstallationListener {", + " void installationComplete();", + "}"); + } + + public static JavaFileObject installationListenerWithStringParam( + AnnotationPrinter annotationPrinter) { + return JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".InstallationListener", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileCallbackQualifiedName() + ";", + annotationPrinter.crossProfileCallbackAsAnnotation(), + "public interface InstallationListener {", + " void installationComplete(String s);", + "}"); + } + + public static JavaFileObject installationListenerWithListStringParam( + AnnotationPrinter annotationPrinter) { + return JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".InstallationListener", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileCallbackQualifiedName() + ";", + annotationPrinter.crossProfileCallbackAsAnnotation(), + "public interface InstallationListener {", + " void installationComplete(java.util.List<String> s);", + "}"); + } + + public static JavaFileObject staticType(AnnotationPrinter annotationPrinter) { + return JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".StaticType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileQualifiedName() + ";", + "public final class StaticType {", + annotationPrinter.crossProfileAsAnnotation(), + " public static void refreshNotes() {", + " }", + "}"); + } + + public static JavaFileObject annotatedDifferentCrossProfileType( + AnnotationPrinter annotationPrinter) { + return JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".DifferentType", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileQualifiedName() + ";", + "public final class DifferentType {", + annotationPrinter.crossProfileAsAnnotation(), + " public void differentMethod() {", + " }", + "}"); + } + + public static JavaFileObject annotatedDifferentProvider(AnnotationPrinter annotationPrinter) { + return JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".DifferentProvider", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileProviderQualifiedName() + ";", + "public final class DifferentProvider {", + annotationPrinter.crossProfileProviderAsAnnotation(), + " public DifferentType provideDifferentType() {", + " return new DifferentType();", + " }", + "}"); + } + + public static JavaFileObject notesTypeWithDefaultConnector(AnnotationPrinter annotationPrinter) { + return annotatedNotesCrossProfileType(annotationPrinter); + } + + public static JavaFileObject annotatedNotesConfigurationWithNotesProvider( + AnnotationPrinter annotationPrinter) { + return JavaFileObjects.forSourceLines( + NOTES_PACKAGE + ".NotesConfiguration", + "package " + NOTES_PACKAGE + ";", + "import " + annotationPrinter.crossProfileConfigurationQualifiedName() + ";", + "import com.google.android.enterprise.connectedapps.CrossProfileConnector;", + annotationPrinter.crossProfileConfigurationAsAnnotation( + "providers=NotesProvider.class, connector=CrossProfileConnector.class"), + "public abstract class NotesConfiguration {", + "}"); + } + + /** Combines two iterables into an iterable of all possible pairs. */ + public static Iterable<Object[]> combineParameters( + Iterable<?> parameters1, Iterable<?> parameters2) { + List<Object[]> testParameters = new ArrayList<>(); + + for (Object parameter1 : parameters1) { + for (Object parameter2 : parameters2) { + testParameters.add(new Object[] {parameter1, parameter2}); + } + } + + return testParameters; + } + + public static String formatErrorMessage(String errorMessage, AnnotationNames annotationNames) { + return ValidationMessageFormatter.forAnnotations(annotationNames).format(errorMessage); + } + + private TestUtilities() {} +} diff --git a/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ValidationMessageFormatterTest.java b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ValidationMessageFormatterTest.java new file mode 100644 index 0000000..d2198a1 --- /dev/null +++ b/tests/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ValidationMessageFormatterTest.java @@ -0,0 +1,57 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.processor; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationNames; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class ValidationMessageFormatterTest { + + private static final String ERROR_MESSAGE = + "Methods annotated @CROSS_PROFILE_ANNOTATION should also be annotated" + + " @CROSS_PROFILE_CALLBACK_ANNOTATION(simple=true) and" + + " @CROSS_PROFILE_PROVIDER_ANNOTATION and @CROSS_PROFILE_CONFIGURATION_ANNOTATION and" + + " @CROSS_PROFILE_CONFIGURATIONS_ANNOTATION and @CROSS_PROFILE_TEST_ANNOTATION"; + + @Test + public void crossProfileAnnotationNames_formatsCorrectly() { + AnnotationNames annotationNames = AnnotationFinder.crossProfileAnnotationNames(); + + assertThat(ValidationMessageFormatter.forAnnotations(annotationNames).format(ERROR_MESSAGE)) + .isEqualTo( + "Methods annotated @CrossProfile should also be annotated" + + " @CrossProfileCallback(simple=true) and @CrossProfileProvider and" + + " @CrossProfileConfiguration and @CrossProfileConfigurations and" + + " @CrossProfileTest"); + } + + @Test + public void crossUserAnnotationNames_formatsCorrectly() { + AnnotationNames annotationNames = AnnotationFinder.crossUserAnnotationNames(); + + assertThat(ValidationMessageFormatter.forAnnotations(annotationNames).format(ERROR_MESSAGE)) + .isEqualTo( + "Methods annotated @CrossUser should also be annotated @CrossUserCallback(simple=true)" + + " and @CrossUserProvider and @CrossUserConfiguration and" + + " @CrossUserConfigurations and @CrossUserTest"); + } +} diff --git a/tests/processor/src/main/proto/connectedappssdk/TestProto.proto b/tests/processor/src/main/proto/connectedappssdk/TestProto.proto new file mode 100644 index 0000000..0753f5f --- /dev/null +++ b/tests/processor/src/main/proto/connectedappssdk/TestProto.proto @@ -0,0 +1,22 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +syntax = "proto2"; + +package connectedappssdk; + +message TestProto { + optional string text = 1; +} diff --git a/tests/robotests/src/test/AndroidManifest.xml b/tests/robotests/src/test/AndroidManifest.xml new file mode 100644 index 0000000..3ac0c30 --- /dev/null +++ b/tests/robotests/src/test/AndroidManifest.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2021 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<manifest + xmlns:android="http://schemas.android.com/apk/res/android" + package="com.google.android.enterprise.connectedapps"> + + <uses-sdk + android:minSdkVersion="14" + android:targetSdkVersion="28"/> + + <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" /> + + <application> + </application> +</manifest>
\ No newline at end of file diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/ConnectedAppsUtilsManagedPersonalProfileTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/ConnectedAppsUtilsManagedPersonalProfileTest.java new file mode 100644 index 0000000..b87995e --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/ConnectedAppsUtilsManagedPersonalProfileTest.java @@ -0,0 +1,90 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import static com.google.common.truth.Truth.assertThat; + +import android.app.Application; +import android.app.Service; +import android.os.Build.VERSION_CODES; +import android.os.IBinder; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.testapp.configuration.TestApplication; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Tests for {@link ConnectedAppsUtils} when running on a personal profile which has management. + * + * <p>This is on a device which has only one profile. + * + * <p>This should behave as if running on a personal profile, not a work profile. + */ +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.O) +public class ConnectedAppsUtilsManagedPersonalProfileTest { + + private final Application context = ApplicationProvider.getApplicationContext(); + private final ConnectedAppsUtils connectedAppsUtils = new ConnectedAppsUtilsImpl(context); + private final RobolectricTestUtilities testUtilities = + new RobolectricTestUtilities(context, /* scheduledExecutorService= */ null); + + @Before + public void setUp() { + Service profileAwareService = Robolectric.setupService(TestApplication.getService()); + testUtilities.initTests(); + IBinder binder = profileAwareService.onBind(/* intent= */ null); + testUtilities.setBinding(binder, CrossProfileConnector.class.getName()); + } + + @Test + public void getPersonalProfile_runningOnPersonalProfileWithManagement_returnsCurrentProfile() { + testUtilities.setRunningOnPersonalProfile(); + testUtilities.setHasProfileOwner(); + + assertThat(connectedAppsUtils.getPersonalProfile()) + .isEqualTo(connectedAppsUtils.getCurrentProfile()); + } + + @Test + public void getWorkProfile_runningOnPersonalProfileWithManagement_returnsDifferentToCurrent() { + testUtilities.setRunningOnPersonalProfile(); + testUtilities.setHasProfileOwner(); + + assertThat(connectedAppsUtils.getWorkProfile()) + .isNotEqualTo(connectedAppsUtils.getCurrentProfile()); + } + + @Test + public void runningOnWork_runningOnPersonalProfileWithManagement_returnsFalse() { + testUtilities.setRunningOnPersonalProfile(); + testUtilities.setHasProfileOwner(); + + assertThat(connectedAppsUtils.runningOnWork()).isFalse(); + } + + @Test + public void runningOnPersonal_runningOnPersonalProfileWithManagement_returnsTrue() { + testUtilities.setRunningOnPersonalProfile(); + testUtilities.setHasProfileOwner(); + + assertThat(connectedAppsUtils.runningOnPersonal()).isTrue(); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/ConnectedAppsUtilsTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/ConnectedAppsUtilsTest.java new file mode 100644 index 0000000..6692067 --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/ConnectedAppsUtilsTest.java @@ -0,0 +1,183 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import static com.google.common.truth.Truth.assertThat; + +import android.app.Application; +import android.app.Service; +import android.os.Build.VERSION_CODES; +import android.os.IBinder; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.testapp.configuration.TestApplication; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.O) +public class ConnectedAppsUtilsTest { + + private final Application context = ApplicationProvider.getApplicationContext(); + private final ConnectedAppsUtils connectedAppsUtils = new ConnectedAppsUtilsImpl(context); + private final TestScheduledExecutorService scheduledExecutorService = + new TestScheduledExecutorService(); + private final TestProfileConnector testProfileConnector = + TestProfileConnector.create(context, scheduledExecutorService); + private final RobolectricTestUtilities testUtilities = + new RobolectricTestUtilities(testProfileConnector, scheduledExecutorService); + + @Before + public void setUp() { + Service profileAwareService = Robolectric.setupService(TestApplication.getService()); + testUtilities.initTests(); + IBinder binder = profileAwareService.onBind(/* intent= */ null); + testUtilities.setBinding(binder, CrossProfileConnector.class.getName()); + testUtilities.createWorkUser(); + testUtilities.turnOnWorkProfile(); + testUtilities.setRunningOnPersonalProfile(); + testUtilities.startConnectingAndWait(); + } + + @Test + public void getPersonalProfile_runningOnPersonalProfile_returnsSameAsCurrentProfile() { + testUtilities.setRunningOnPersonalProfile(); + + assertThat(connectedAppsUtils.getPersonalProfile()) + .isEqualTo(connectedAppsUtils.getCurrentProfile()); + } + + @Test + public void getPersonalProfile_runningOnWorkProfile_returnsDifferentToCurrentProfile() { + testUtilities.setRunningOnWorkProfile(); + + assertThat(connectedAppsUtils.getPersonalProfile()) + .isNotEqualTo(connectedAppsUtils.getCurrentProfile()); + } + + @Test + public void getWorkProfile_runningOnWorkProfile_returnsSameAsCurrentProfile() { + testUtilities.setRunningOnWorkProfile(); + + assertThat(connectedAppsUtils.getWorkProfile()) + .isEqualTo(connectedAppsUtils.getCurrentProfile()); + } + + @Test + public void getWorkProfile_runningOnPersonalProfile_returnsDifferentToCurrentProfile() { + testUtilities.setRunningOnPersonalProfile(); + + assertThat(connectedAppsUtils.getWorkProfile()) + .isNotEqualTo(connectedAppsUtils.getCurrentProfile()); + } + + @Test + public void getPrimaryProfile_noPrimaryProfileSet_returnsNull() { + assertThat(connectedAppsUtils.getPrimaryProfile()).isNull(); + } + + @Test + public void getSecondaryProfile_noPrimaryProfileSet_returnsNull() { + assertThat(connectedAppsUtils.getSecondaryProfile()).isNull(); + } + + @Test + public void getPrimaryProfile_primaryProfileIsPersonal_runningOnPersonal_returnsCurrent() { + testUtilities.setRunningOnPersonalProfile(); + ConnectedAppsUtils utils = + new ConnectedAppsUtilsImpl(context, connectedAppsUtils.getPersonalProfile()); + + assertThat(utils.getPrimaryProfile()).isEqualTo(connectedAppsUtils.getCurrentProfile()); + } + + @Test + public void + getSecondaryProfile_primaryProfileIsPersonal_runningOnPersonal_returnsDifferentToCurrent() { + testUtilities.setRunningOnPersonalProfile(); + ConnectedAppsUtils utils = + new ConnectedAppsUtilsImpl(context, connectedAppsUtils.getPersonalProfile()); + + assertThat(utils.getSecondaryProfile()).isNotEqualTo(connectedAppsUtils.getCurrentProfile()); + } + + @Test + public void getPrimaryProfile_primaryProfileIsWork_runningOnPersonal_returnsDifferentToCurrent() { + testUtilities.setRunningOnPersonalProfile(); + ConnectedAppsUtils utils = + new ConnectedAppsUtilsImpl(context, connectedAppsUtils.getWorkProfile()); + + assertThat(utils.getPrimaryProfile()).isNotEqualTo(connectedAppsUtils.getCurrentProfile()); + } + + @Test + public void getSecondaryProfile_primaryProfileIsWork_runningOnPersonal_returnsCurrent() { + ConnectedAppsUtils utils = + new ConnectedAppsUtilsImpl(context, connectedAppsUtils.getWorkProfile()); + testUtilities.setRunningOnPersonalProfile(); + + assertThat(utils.getSecondaryProfile()).isEqualTo(connectedAppsUtils.getCurrentProfile()); + } + + @Test + public void getPrimaryProfile_primaryProfileIsWork_runningOnWork_returnsCurrent() { + testUtilities.setRunningOnWorkProfile(); + ConnectedAppsUtils utils = + new ConnectedAppsUtilsImpl(context, connectedAppsUtils.getWorkProfile()); + + assertThat(utils.getPrimaryProfile()).isEqualTo(connectedAppsUtils.getCurrentProfile()); + } + + @Test + public void getSecondaryProfile_primaryProfileIsWork_runningOnWork_returnsDifferentToCurrent() { + testUtilities.setRunningOnWorkProfile(); + ConnectedAppsUtils utils = + new ConnectedAppsUtilsImpl(context, connectedAppsUtils.getWorkProfile()); + + assertThat(utils.getSecondaryProfile()).isNotEqualTo(connectedAppsUtils.getCurrentProfile()); + } + + @Test + public void runningOnWork_runningOnPersonal_returnsFalse() { + testUtilities.setRunningOnPersonalProfile(); + + assertThat(connectedAppsUtils.runningOnWork()).isFalse(); + } + + @Test + public void runningOnWork_runningOnWork_returnsTrue() { + testUtilities.setRunningOnWorkProfile(); + + assertThat(connectedAppsUtils.runningOnWork()).isTrue(); + } + + @Test + public void runningOnPersonal_runningOnPersonal_returnsTrue() { + testUtilities.setRunningOnPersonalProfile(); + + assertThat(connectedAppsUtils.runningOnPersonal()).isTrue(); + } + + @Test + public void runningOnPersonal_runningOnWork_returnsFalse() { + testUtilities.setRunningOnWorkProfile(); + + assertThat(connectedAppsUtils.runningOnPersonal()).isFalse(); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/ConnectedAppsUtilsUnsupportedTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/ConnectedAppsUtilsUnsupportedTest.java new file mode 100644 index 0000000..8df07b6 --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/ConnectedAppsUtilsUnsupportedTest.java @@ -0,0 +1,74 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import static com.google.common.truth.Truth.assertThat; + +import android.app.Application; +import android.os.Build.VERSION_CODES; +import androidx.test.core.app.ApplicationProvider; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** Tests for the {@link ConnectedAppsUtils} class running on unsupported Android versions. */ +@RunWith(RobolectricTestRunner.class) +@Config(maxSdk = VERSION_CODES.N_MR1) +public class ConnectedAppsUtilsUnsupportedTest { + private final Application context = ApplicationProvider.getApplicationContext(); + private final ConnectedAppsUtils connectedAppsUtils = new ConnectedAppsUtilsImpl(context); + + @Test + public void getCurrentProfile_returnsProfile() { + assertThat(connectedAppsUtils.getCurrentProfile()).isNotNull(); + } + + @Test + public void getOtherProfile_returnsProfile() { + assertThat(connectedAppsUtils.getOtherProfile()).isNotNull(); + } + + @Test + public void getWorkProfile_returnsProfile() { + assertThat(connectedAppsUtils.getWorkProfile()).isNotNull(); + } + + @Test + public void getPersonalProfile_returnsProfile() { + assertThat(connectedAppsUtils.getPersonalProfile()).isNotNull(); + } + + @Test + public void getPrimaryProfile_returnsNull() { + assertThat(connectedAppsUtils.getPrimaryProfile()).isNull(); + } + + @Test + public void getSecondaryProfile_returnsNull() { + assertThat(connectedAppsUtils.getSecondaryProfile()).isNull(); + } + + @Test + public void runningOnPersonal_returnsFalse() { + assertThat(connectedAppsUtils.runningOnPersonal()).isFalse(); + } + + @Test + public void runningOnWork_returnsFalse() { + assertThat(connectedAppsUtils.runningOnWork()).isFalse(); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/CrossProfileSenderTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/CrossProfileSenderTest.java new file mode 100644 index 0000000..a8f14d8 --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/CrossProfileSenderTest.java @@ -0,0 +1,499 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import static com.google.android.enterprise.connectedapps.RobolectricTestUtilities.TEST_CONNECTOR_CLASS_NAME; +import static com.google.android.enterprise.connectedapps.RobolectricTestUtilities.TEST_SERVICE_CLASS_NAME; +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.INTERACT_ACROSS_USERS; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; +import static org.robolectric.Shadows.shadowOf; +import static org.robolectric.annotation.LooperMode.Mode.LEGACY; + +import android.app.Application; +import android.app.admin.DevicePolicyManager; +import android.content.ComponentName; +import android.os.Build.VERSION_CODES; +import android.os.Parcel; +import android.os.UserHandle; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.annotations.AvailabilityRestrictions; +import com.google.android.enterprise.connectedapps.exceptions.UnavailableProfileException; +import com.google.common.collect.ImmutableList; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.annotation.LooperMode; + +@LooperMode(LEGACY) +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.O) +public class CrossProfileSenderTest { + + private final Application context = ApplicationProvider.getApplicationContext(); + private final DevicePolicyManager devicePolicyManager = + context.getSystemService(DevicePolicyManager.class); + private final TestService testService = new TestService(); + + private CrossProfileSender sender; + private final TestConnectionListener connectionListener = new TestConnectionListener(); + private final TestAvailabilityListener availabilityListener = new TestAvailabilityListener(); + private final TestScheduledExecutorService scheduledExecutorService = + new TestScheduledExecutorService(); + private final RobolectricTestUtilities testUtilities = + new RobolectricTestUtilities(context, scheduledExecutorService); + + @Before + public void setUp() { + testUtilities.initTests(); + sender = + new CrossProfileSender( + context, + TEST_SERVICE_CLASS_NAME, + new DefaultProfileBinder(), + connectionListener, + availabilityListener, + scheduledExecutorService, + AvailabilityRestrictions.DEFAULT); + sender.beginMonitoringAvailabilityChanges(); + + testUtilities.setBinding(testService, TEST_CONNECTOR_CLASS_NAME); + testUtilities.createWorkUser(); + testUtilities.turnOnWorkProfile(); + testUtilities.setRunningOnPersonalProfile(); + testUtilities.setRequestsPermissions(INTERACT_ACROSS_USERS); + testUtilities.grantPermissions(INTERACT_ACROSS_USERS); + } + + @Test + public void construct_nullContext_throwsNullPointerException() { + assertThrows( + NullPointerException.class, + () -> + new CrossProfileSender( + /* context= */ null, + TEST_SERVICE_CLASS_NAME, + new DefaultProfileBinder(), + connectionListener, + availabilityListener, + scheduledExecutorService, + AvailabilityRestrictions.DEFAULT)); + } + + @Test + public void construct_nullConnectedAppsServiceClassName_throwsNullPointerException() { + assertThrows( + NullPointerException.class, + () -> + new CrossProfileSender( + context, + /* connectedAppsServiceClassName= */ null, + new DefaultProfileBinder(), + connectionListener, + availabilityListener, + scheduledExecutorService, + AvailabilityRestrictions.DEFAULT)); + } + + @Test + public void construct_nullConnectionListener_throwsNullPointerException() { + assertThrows( + NullPointerException.class, + () -> + new CrossProfileSender( + context, + TEST_SERVICE_CLASS_NAME, + new DefaultProfileBinder(), + /* connectionListener= */ null, + availabilityListener, + scheduledExecutorService, + AvailabilityRestrictions.DEFAULT)); + } + + @Test + public void construct_nullAvailabilityListener_throwsNullPointerException() { + assertThrows( + NullPointerException.class, + () -> + new CrossProfileSender( + context, + TEST_SERVICE_CLASS_NAME, + new DefaultProfileBinder(), + connectionListener, + /* availabilityListener= */ null, + scheduledExecutorService, + AvailabilityRestrictions.DEFAULT)); + } + + @Test + public void construct_nullBindingConfig_throwsNullPointerException() { + assertThrows( + NullPointerException.class, + () -> + new CrossProfileSender( + context, + TEST_SERVICE_CLASS_NAME, + /* binder= */ null, + connectionListener, + availabilityListener, + scheduledExecutorService, + AvailabilityRestrictions.DEFAULT)); + } + + @Test + public void construct_nullTimeoutExecutor_throwsNullPointerException() { + assertThrows( + NullPointerException.class, + () -> + new CrossProfileSender( + context, + TEST_SERVICE_CLASS_NAME, + new DefaultProfileBinder(), + connectionListener, + availabilityListener, + /* scheduledExecutorService= */ null, + AvailabilityRestrictions.DEFAULT)); + } + + @Test + public void construct_nullAvailabilityRestrictions_throwsNullPointerException() { + assertThrows( + NullPointerException.class, + () -> + new CrossProfileSender( + context, + TEST_SERVICE_CLASS_NAME, + new DefaultProfileBinder(), + connectionListener, + availabilityListener, + scheduledExecutorService, + /* availabilityRestrictions= */ null)); + } + + // Other manuallyBind tests are covered in Instrumented ConnectTest because Robolectric doesn't + // handle the multiple threads very well + @Test + public void manuallyBind_callingFromUIThread_throwsIllegalStateException() { + assertThrows(IllegalStateException.class, sender::manuallyBind); + } + + @Test + public void startManuallyBinding_otherProfileIsNotAvailable_doesNotbind() { + testUtilities.turnOffWorkProfile(); + sender.startManuallyBinding(); + + assertThat(sender.isBound()).isFalse(); + } + + @Test + public void startManuallyBinding_bindingIsNotPossible_doesNotCallConnectionListener() { + testUtilities.turnOffWorkProfile(); + + sender.startManuallyBinding(); + + assertThat(connectionListener.connectionChangedCount()).isEqualTo(0); + } + + @Test + public void startManuallyBinding_otherProfileIsAvailable_binds() { + testUtilities.turnOnWorkProfile(); + sender.startManuallyBinding(); + + assertThat(sender.isBound()).isTrue(); + } + + @Test + public void startManuallyBinding_binds_callsConnectionListener() { + testUtilities.turnOnWorkProfile(); + sender.startManuallyBinding(); + testUtilities.advanceTimeBySeconds(1); + + assertThat(connectionListener.connectionChangedCount()).isEqualTo(1); + } + + @Test + public void startManuallyBinding_otherProfileBecomesAvailable_binds() { + testUtilities.turnOffWorkProfile(); + sender.startManuallyBinding(); + + testUtilities.turnOnWorkProfile(); + + assertThat(sender.isBound()).isTrue(); + } + + @Test + public void startManuallyBinding_otherProfileBecomesAvailable_callsConnectionListener() { + testUtilities.turnOffWorkProfile(); + sender.startManuallyBinding(); + + testUtilities.turnOnWorkProfile(); + + assertThat(connectionListener.connectionChangedCount()).isEqualTo(1); + } + + @Test + public void startManuallyBinding_profileBecomesUnavailable_unbinds() { + sender.startManuallyBinding(); + testUtilities.advanceTimeBySeconds(10); + + testUtilities.turnOffWorkProfile(); + + assertThat(sender.isBound()).isFalse(); + } + + @Test + public void startManuallyBinding_profileBecomesUnavailable_callsConnectionListener() { + sender.startManuallyBinding(); + testUtilities.advanceTimeBySeconds(10); + connectionListener.resetConnectionChangedCount(); + + testUtilities.turnOffWorkProfile(); + + assertThat(connectionListener.connectionChangedCount()).isEqualTo(1); + } + + @Test + public void startManuallyBinding_profileBecomesAvailableAgain_rebinds() { + sender.startManuallyBinding(); + testUtilities.advanceTimeBySeconds(10); + testUtilities.turnOffWorkProfile(); + + testUtilities.turnOnWorkProfile(); + + assertThat(sender.isBound()).isTrue(); + } + + @Test + public void startManuallyBinding_profileBecomesAvailableAgain_callsConnectionListener() { + sender.startManuallyBinding(); + testUtilities.advanceTimeBySeconds(10); + testUtilities.turnOffWorkProfile(); + connectionListener.resetConnectionChangedCount(); + + testUtilities.turnOnWorkProfile(); + + assertThat(connectionListener.connectionChangedCount()).isEqualTo(1); + } + + @Test + public void unbind_isNotBound() { + sender.startManuallyBinding(); + + sender.unbind(); + + assertThat(sender.isBound()).isFalse(); + } + + @Test + public void unbind_callsConnectionListener() { + sender.startManuallyBinding(); + testUtilities.advanceTimeBySeconds(1); + connectionListener.resetConnectionChangedCount(); + + sender.unbind(); + testUtilities.advanceTimeBySeconds(1); + + assertThat(connectionListener.connectionChangedCount()).isEqualTo(1); + } + + @Test + public void unbind_profileBecomesAvailable_doesNotBind() { + testUtilities.turnOffWorkProfile(); + sender.startManuallyBinding(); + sender.unbind(); + + testUtilities.turnOnWorkProfile(); + + assertThat(sender.isBound()).isFalse(); + } + + @Test + public void bind_bindingFromPersonalProfile_binds() { + testUtilities.setRunningOnPersonalProfile(); + sender.startManuallyBinding(); + + assertThat(sender.isBound()).isTrue(); + } + + @Test + public void bind_bindingFromWorkProfile_binds() { + testUtilities.setRunningOnWorkProfile(); + sender.startManuallyBinding(); + + assertThat(sender.isBound()).isTrue(); + } + + @Test + public void call_isNotBound_throwsUnavailableProfileException() { + int crossProfileTypeIdentifier = 1; + int methodIdentifier = 0; + Parcel params = Parcel.obtain(); + sender.unbind(); + + assertThrows( + UnavailableProfileException.class, + () -> sender.call(crossProfileTypeIdentifier, methodIdentifier, params)); + } + + @Test + public void call_isBound_callsMethod() throws UnavailableProfileException { + int crossProfileTypeIdentifier = 1; + int methodIdentifier = 0; + Parcel params = Parcel.obtain(); + params.writeString("value"); + sender.startManuallyBinding(); + + sender.call(crossProfileTypeIdentifier, methodIdentifier, params); + + assertThat(testService.lastCall().getCrossProfileTypeIdentifier()) + .isEqualTo(crossProfileTypeIdentifier); + assertThat(testService.lastCall().getMethodIdentifier()).isEqualTo(methodIdentifier); + assertThat(testService.lastCall().getParams().readString()).isEqualTo("value"); + } + + @Test + public void call_isBound_returnsResponse() throws UnavailableProfileException { + int crossProfileTypeIdentifier = 1; + int methodIdentifier = 0; + Parcel params = Parcel.obtain(); + Parcel expectedResponseParcel = Parcel.obtain(); + expectedResponseParcel.writeInt(0); // No error + expectedResponseParcel.writeString("value"); + testService.setResponseParcel(expectedResponseParcel); + sender.startManuallyBinding(); + + Parcel actualResponseParcel = sender.call(crossProfileTypeIdentifier, methodIdentifier, params); + + assertThat(actualResponseParcel.readString()).isEqualTo("value"); + } + + @Test + public void bind_usingDpcBinding_otherProfileIsAvailable_binds() { + initWithDpcBinding(); + testUtilities.turnOnWorkProfile(); + sender.startManuallyBinding(); + + assertThat(sender.isBound()).isTrue(); + } + + @Test + public void bind_usingDpcBinding_binds_callsConnectionListener() { + initWithDpcBinding(); + testUtilities.turnOnWorkProfile(); + sender.startManuallyBinding(); + testUtilities.advanceTimeBySeconds(1); + + assertThat(connectionListener.connectionChangedCount()).isEqualTo(1); + } + + @Test + public void bind_usingDpcBinding_otherProfileDoesNotExist_doesNotBind() { + initWithDpcBinding(); + shadowOf(devicePolicyManager).setBindDeviceAdminTargetUsers(ImmutableList.of()); + + sender.startManuallyBinding(); + testUtilities.advanceTimeBySeconds(10); + + assertThat(sender.isBound()).isFalse(); + } + + @Test + public void bind_usingDpcBinding_otherProfileIsCreated_binds() { + initWithDpcBinding(); + shadowOf(devicePolicyManager).setBindDeviceAdminTargetUsers(ImmutableList.of()); + sender.startManuallyBinding(); + testUtilities.advanceTimeBySeconds(10); + + shadowOf(devicePolicyManager) + .setBindDeviceAdminTargetUsers(ImmutableList.of(getWorkUserHandle())); + testUtilities.turnOnWorkProfile(); + + assertThat(sender.isBound()).isTrue(); + } + + @Test + public void bind_usingDpcBinding_otherProfileBecomesAvailable_binds() { + initWithDpcBinding(); + testUtilities.turnOffWorkProfile(); + sender.startManuallyBinding(); + testUtilities.advanceTimeBySeconds(10); + + testUtilities.turnOnWorkProfile(); + + assertThat(sender.isBound()).isTrue(); + } + + @Test + public void bind_usingDpcBinding_otherProfileBecomesAvailable_callsConnectionListener() { + initWithDpcBinding(); + testUtilities.turnOffWorkProfile(); + sender.startManuallyBinding(); + testUtilities.advanceTimeBySeconds(10); + + testUtilities.turnOnWorkProfile(); + + assertThat(connectionListener.connectionChangedCount()).isEqualTo(1); + } + + @Test + public void workProfileBecomesAvailable_callsAvailabilityListener() { + testUtilities.turnOffWorkProfile(); + availabilityListener.reset(); + + testUtilities.turnOnWorkProfile(); + + assertThat(availabilityListener.availabilityChangedCount()).isEqualTo(1); + } + + @Test + public void workProfileBecomesUnavailable_callsAvailabilityListener() { + testUtilities.turnOnWorkProfile(); + availabilityListener.reset(); + + testUtilities.turnOffWorkProfile(); + + assertThat(availabilityListener.availabilityChangedCount()).isEqualTo(1); + } + + private void initWithDpcBinding() { + shadowOf(devicePolicyManager) + .setBindDeviceAdminTargetUsers(ImmutableList.of(getWorkUserHandle())); + + ComponentName deviceAdminReceiver = new ComponentName("A", "B"); + + testUtilities.initTests(); + sender = + new CrossProfileSender( + context, + TEST_SERVICE_CLASS_NAME, + new DpcProfileBinder(deviceAdminReceiver), + connectionListener, + availabilityListener, + scheduledExecutorService, + AvailabilityRestrictions.DEFAULT); + + testUtilities.setBinding(testService, TEST_CONNECTOR_CLASS_NAME); + testUtilities.createWorkUser(); + testUtilities.turnOnWorkProfile(); + testUtilities.setRunningOnPersonalProfile(); + } + + private static UserHandle getWorkUserHandle() { + return SharedTestUtilities.getUserHandleForUserId(10); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/DpcProfileBinderTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/DpcProfileBinderTest.java new file mode 100644 index 0000000..2a62d46 --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/DpcProfileBinderTest.java @@ -0,0 +1,31 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import static org.junit.Assert.assertThrows; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class DpcProfileBinderTest { + + @Test + public void construct_nullDeviceAdminReceiver_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> new DpcProfileBinder(null)); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/PermissionsTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/PermissionsTest.java new file mode 100644 index 0000000..9083b6c --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/PermissionsTest.java @@ -0,0 +1,106 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import static android.Manifest.permission.INTERACT_ACROSS_PROFILES; +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.INTERACT_ACROSS_USERS; +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.INTERACT_ACROSS_USERS_FULL; +import static com.google.common.truth.Truth.assertThat; + +import android.app.Application; +import android.os.Build.VERSION_CODES; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.O) +public class PermissionsTest { + + private final Application context = ApplicationProvider.getApplicationContext(); + private final TestScheduledExecutorService scheduledExecutorService = + new TestScheduledExecutorService(); + private final TestProfileConnector connector = + TestProfileConnector.create(context, scheduledExecutorService); + private final Permissions permissions = connector.permissions(); + private final RobolectricTestUtilities testUtilities = + new RobolectricTestUtilities(connector, scheduledExecutorService); + + @Test + public void canMakeCrossProfileCalls_defaultProfileBinder_doesntDeclareAnyPermissions_isFalse() { + testUtilities.setRequestsPermissions(); + + assertThat(permissions.canMakeCrossProfileCalls()).isFalse(); + } + + @Test + public void + canMakeCrossProfileCalls_defaultProfileBinder_interactAcrossUsersNotGranted_isFalse() { + testUtilities.setRequestsPermissions(INTERACT_ACROSS_USERS); + testUtilities.denyPermissions(INTERACT_ACROSS_USERS); + + assertThat(permissions.canMakeCrossProfileCalls()).isFalse(); + } + + @Test + public void + canMakeCrossProfileCalls_defaultProfileBinder_interactAcrossUsersFullNotGranted_isFalse() { + testUtilities.setRequestsPermissions(INTERACT_ACROSS_USERS_FULL); + testUtilities.denyPermissions(INTERACT_ACROSS_USERS_FULL); + + assertThat(permissions.canMakeCrossProfileCalls()).isFalse(); + } + + @Test + public void + canMakeCrossProfileCalls_defaultProfileBinder_interactAcrossProfilesNotGranted_isFalse() { + testUtilities.setRequestsPermissions(INTERACT_ACROSS_PROFILES); + testUtilities.denyPermissions(INTERACT_ACROSS_PROFILES); + + assertThat(permissions.canMakeCrossProfileCalls()).isFalse(); + } + + @Test + public void canMakeCrossProfileCalls_defaultProfileBinder_interactAcrossUsersGranted_isTrue() { + testUtilities.setRequestsPermissions(INTERACT_ACROSS_USERS); + testUtilities.grantPermissions(INTERACT_ACROSS_USERS); + + assertThat(permissions.canMakeCrossProfileCalls()).isTrue(); + } + + @Test + public void + canMakeCrossProfileCalls_defaultProfileBinder_interactAcrossUsersFullGranted_isTrue() { + testUtilities.setRequestsPermissions(INTERACT_ACROSS_USERS_FULL); + testUtilities.grantPermissions(INTERACT_ACROSS_USERS_FULL); + + assertThat(permissions.canMakeCrossProfileCalls()).isTrue(); + } + + @Test + @Ignore // TODO(161541780): Enable this test when building against a supported version of + // Robolectric + public void canMakeCrossProfileCalls_defaultProfileBinder_interactAcrossProfilesGranted_isTrue() { + testUtilities.setRequestsPermissions(INTERACT_ACROSS_PROFILES); + testUtilities.grantPermissions(INTERACT_ACROSS_PROFILES); + + assertThat(permissions.canMakeCrossProfileCalls()).isTrue(); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/PermissionsUnsupportedTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/PermissionsUnsupportedTest.java new file mode 100644 index 0000000..b6b1758 --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/PermissionsUnsupportedTest.java @@ -0,0 +1,41 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import static com.google.common.truth.Truth.assertThat; + +import android.app.Application; +import android.os.Build.VERSION_CODES; +import androidx.test.core.app.ApplicationProvider; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** Tests for the {@link Permissions} class running on unsupported Android versions. */ +@RunWith(RobolectricTestRunner.class) +@Config(maxSdk = VERSION_CODES.N_MR1) +public class PermissionsUnsupportedTest { + + private final Application context = ApplicationProvider.getApplicationContext(); + private final ConnectionBinder binder = new DefaultProfileBinder(); + private final Permissions permissions = new PermissionsImpl(context, binder); + + @Test + public void canMakeCrossProfileCalls_unsupportedVersion_returnsFalse() { + assertThat(permissions.canMakeCrossProfileCalls()).isFalse(); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/ProfileConnectorTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/ProfileConnectorTest.java new file mode 100644 index 0000000..a6b5b17 --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/ProfileConnectorTest.java @@ -0,0 +1,254 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import static com.google.android.enterprise.connectedapps.RobolectricTestUtilities.TEST_CONNECTOR_CLASS_NAME; +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.INTERACT_ACROSS_USERS; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; +import static org.robolectric.Shadows.shadowOf; +import static org.robolectric.annotation.LooperMode.Mode.LEGACY; + +import android.app.Application; +import android.app.Service; +import android.os.Build.VERSION_CODES; +import android.os.IBinder; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.annotations.CustomProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.configuration.TestApplication; +import com.google.android.enterprise.connectedapps.testapp.connector.DirectBootAwareConnector; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnectorWithCustomServiceClass; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.annotation.LooperMode; + +/** Tests for the {@link CustomProfileConnector} class. */ +@LooperMode(LEGACY) +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.O) +public class ProfileConnectorTest { + + private final Application context = ApplicationProvider.getApplicationContext(); + private final TestConnectionListener connectionListener = new TestConnectionListener(); + private final TestScheduledExecutorService scheduledExecutorService = + new TestScheduledExecutorService(); + private final TestProfileConnector testProfileConnector = + TestProfileConnector.create(context, scheduledExecutorService); + private final RobolectricTestUtilities testUtilities = + new RobolectricTestUtilities(testProfileConnector, scheduledExecutorService); + private final DirectBootAwareConnector directBootAwareConnector = + DirectBootAwareConnector.create(context); + + @Before + public void setUp() { + Service profileAwareService = Robolectric.setupService(TestApplication.getService()); + testUtilities.initTests(); + IBinder binder = profileAwareService.onBind(/* intent= */ null); + testUtilities.setBinding(binder, TEST_CONNECTOR_CLASS_NAME); + testUtilities.setRunningOnPersonalProfile(); + testUtilities.createWorkUser(); + testUtilities.turnOnWorkProfile(); + testUtilities.setRequestsPermissions(INTERACT_ACROSS_USERS); + testUtilities.grantPermissions(INTERACT_ACROSS_USERS); + } + + @Test + public void construct_nullConnector_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> TestProfileConnector.create(null)); + } + + // Other connect tests are covered in Instrumented ConnectTest because Robolectric doesn't + // handle the multiple threads very well + @Test + public void connect_callingFromUIThread_throwsIllegalStateException() { + assertThrows(IllegalStateException.class, testProfileConnector::connect); + } + + @Test + public void startConnecting_fromPersonalProfile_binds() { + testUtilities.setRunningOnPersonalProfile(); + testUtilities.startConnectingAndWait(); + + assertThat(testProfileConnector.isConnected()).isTrue(); + } + + @Test + public void startConnecting_fromWorkProfile_binds() { + testUtilities.setRunningOnWorkProfile(); + testUtilities.startConnectingAndWait(); + + assertThat(testProfileConnector.isConnected()).isTrue(); + } + + @Test + public void disconnect_fromPersonalProfile_doesNotBind() { + testUtilities.setRunningOnPersonalProfile(); + testUtilities.disconnect(); + + assertThat(testProfileConnector.isConnected()).isFalse(); + } + + @Test + public void disconnect_fromWorkProfile_doesNotBind() { + testUtilities.setRunningOnWorkProfile(); + testUtilities.disconnect(); + + assertThat(testProfileConnector.isConnected()).isFalse(); + } + + @Test + public void disconnect_isBound_unbinds() { + testUtilities.startConnectingAndWait(); + + testUtilities.disconnect(); + + assertThat(testProfileConnector.isConnected()).isFalse(); + } + + @Test + public void startConnecting_callsConnectionListener() { + testProfileConnector.registerConnectionListener(connectionListener); + testUtilities.startConnectingAndWait(); + + assertThat(connectionListener.connectionChangedCount()).isEqualTo(1); + } + + @Test + public void startConnecting_doesNotCallUnregisteredConnectionListener() { + testProfileConnector.registerConnectionListener(connectionListener); + testProfileConnector.unregisterConnectionListener(connectionListener); + testUtilities.startConnectingAndWait(); + + assertThat(connectionListener.connectionChangedCount()).isEqualTo(0); + } + + @Test + public void disconnect_callsConnectionListener() { + testProfileConnector.registerConnectionListener(connectionListener); + testUtilities.startConnectingAndWait(); + connectionListener.resetConnectionChangedCount(); + + testUtilities.disconnect(); + + assertThat(connectionListener.connectionChangedCount()).isEqualTo(1); + } + + @Test + public void bindingDies_callsConnectionListener() { + testProfileConnector.registerConnectionListener(connectionListener); + testUtilities.startConnectingAndWait(); + connectionListener.resetConnectionChangedCount(); + + testUtilities.turnOffWorkProfile(); + + assertThat(connectionListener.connectionChangedCount()).isEqualTo(1); + } + + @Test + public void startConnecting_profileConnectorWithCustomServiceClass() { + TestProfileConnectorWithCustomServiceClass.create(context, scheduledExecutorService) + .startConnecting(); + testUtilities.advanceTimeBySeconds(1); // Allow connection + + assertThat(shadowOf(context).getNextStartedService().getComponent().getClassName()) + .isEqualTo("com.google.CustomServiceClass"); + } + + @Test + public void isAvailable_workProfileIsTurnedOn_returnsTrue() { + testUtilities.setRunningOnPersonalProfile(); + testUtilities.createWorkUser(); + testUtilities.turnOnWorkProfile(); + + assertThat(testProfileConnector.isAvailable()).isTrue(); + } + + @Test + public void isAvailable_workProfileIsTurnedOff_returnsFalse() { + testUtilities.setRunningOnPersonalProfile(); + testUtilities.createWorkUser(); + testUtilities.turnOffWorkProfile(); + + assertThat(testProfileConnector.isAvailable()).isFalse(); + } + + @Test + public void isAvailable_runningOnWorkProfile_returnsTrue() { + testUtilities.createWorkUser(); + testUtilities.turnOnWorkProfile(); + testUtilities.setRunningOnWorkProfile(); + + assertThat(testProfileConnector.isAvailable()).isTrue(); + } + + @Test + public void isAvailable_defaultAvailabilityRestrictions_isNotUnlocked_returnsFalse() { + testUtilities.createWorkUser(); + testUtilities.turnOnWorkProfileWithoutUnlocking(); + + assertThat(testProfileConnector.isAvailable()).isFalse(); + } + + @Test + public void isAvailable_defaultAvailabilityRestrictions_isUnlocked_returnsTrue() { + testUtilities.createWorkUser(); + testUtilities.turnOnWorkProfile(); + + assertThat(testProfileConnector.isAvailable()).isTrue(); + } + + @Test + public void isAvailable_directBootAwareAvailabilityRestrictions_isNotUnlocked_returnsTrue() { + testUtilities.createWorkUser(); + testUtilities.turnOnWorkProfileWithoutUnlocking(); + + assertThat(directBootAwareConnector.isAvailable()).isTrue(); + } + + @Test + public void isAvailable_directBootAwareAvailabilityRestrictions_isUnlocked_returnsTrue() { + testUtilities.createWorkUser(); + testUtilities.turnOnWorkProfile(); + + assertThat(directBootAwareConnector.isAvailable()).isTrue(); + } + + @Test + public void isManuallyManagingConnection_returnsFalse() { + assertThat(testProfileConnector.isManuallyManagingConnection()).isFalse(); + } + + @Test + public void isManuallyManagingConnection_hasManuallyConnected_returnsTrue() { + testUtilities.startConnectingAndWait(); + + assertThat(testProfileConnector.isManuallyManagingConnection()).isTrue(); + } + + @Test + public void isManuallyManagingConnection_hasCalledStopManualConnectionManagement_returnsFalse() { + testUtilities.startConnectingAndWait(); + + testProfileConnector.stopManualConnectionManagement(); + + assertThat(testProfileConnector.isManuallyManagingConnection()).isFalse(); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/ProfileConnectorUnsupportedTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/ProfileConnectorUnsupportedTest.java new file mode 100644 index 0000000..d6bb9ca --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/ProfileConnectorUnsupportedTest.java @@ -0,0 +1,93 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import android.app.Application; +import android.os.Build.VERSION_CODES; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.annotations.CustomProfileConnector; +import com.google.android.enterprise.connectedapps.exceptions.UnavailableProfileException; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** Tests for the {@link CustomProfileConnector} class running on unsupported Android versions. */ +@RunWith(RobolectricTestRunner.class) +@Config(maxSdk = VERSION_CODES.N_MR1) +public class ProfileConnectorUnsupportedTest { + private final Application context = ApplicationProvider.getApplicationContext(); + private final TestProfileConnector testProfileConnector = TestProfileConnector.create(context); + + @Test + public void startConnecting_doesNotCrash() { + testProfileConnector.startConnecting(); + } + + @Test + public void connect_throwsUnavailableProfileException() { + assertThrows(UnavailableProfileException.class, testProfileConnector::connect); + } + + @Test + public void isAvailable_returnsFalse() { + assertThat(testProfileConnector.isAvailable()).isFalse(); + } + + @Test + public void isConnected_returnsFalse() { + assertThat(testProfileConnector.isConnected()).isFalse(); + } + + @Test + public void stopManualConnectionManagement_doesNotCrash() { + testProfileConnector.stopManualConnectionManagement(); + } + + @Test + public void crossProfileSender_returnsNull() { + assertThat(testProfileConnector.crossProfileSender()).isNull(); + } + + @Test + public void registerConnectionListener_doesNotCrash() { + testProfileConnector.registerConnectionListener(() -> {}); + } + + @Test + public void unregisterConnectionListener_doesNotCrash() { + testProfileConnector.unregisterConnectionListener(() -> {}); + } + + @Test + public void registerAvailabilityListener_doesNotCrash() { + testProfileConnector.registerAvailabilityListener(() -> {}); + } + + @Test + public void unregisterAvailabilityListener_doesNotCrash() { + testProfileConnector.unregisterAvailabilityListener(() -> {}); + } + + @Test + public void utils_returnsInstance() { + assertThat(testProfileConnector.utils()).isNotNull(); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/ProfileTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/ProfileTest.java new file mode 100644 index 0000000..6c087fb --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/ProfileTest.java @@ -0,0 +1,76 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import static com.google.common.truth.Truth.assertThat; + +import android.app.Application; +import android.os.Build.VERSION_CODES; +import androidx.test.core.app.ApplicationProvider; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.O) +public class ProfileTest { + + private final Application context = ApplicationProvider.getApplicationContext(); + private final ConnectedAppsUtils connectedAppsUtils = new ConnectedAppsUtilsImpl(context); + + @Test + public void isCurrent_currentProfile_returnsTrue() { + Profile identifier = connectedAppsUtils.getCurrentProfile(); + + assertThat(identifier.isCurrent()).isTrue(); + } + + @Test + public void isCurrent_notCurrent_returnsFalse() { + Profile identifier = connectedAppsUtils.getOtherProfile(); + + assertThat(identifier.isCurrent()).isFalse(); + } + + @Test + public void isOther_otherProfile_returnsTrue() { + Profile identifier = connectedAppsUtils.getOtherProfile(); + + assertThat(identifier.isOther()).isTrue(); + } + + @Test + public void isOther_notOtherProfile_returnsFalse() { + Profile identifier = connectedAppsUtils.getCurrentProfile(); + + assertThat(identifier.isOther()).isFalse(); + } + + @Test + public void fromInt_intFromCurrentProfile_equalsCurrentProfile() { + Profile identifier = connectedAppsUtils.getCurrentProfile(); + + assertThat(Profile.fromInt(identifier.asInt())).isEqualTo(identifier); + } + + @Test + public void fromInt_intFromOtherProfile_equalsOtherProfile() { + Profile identifier = connectedAppsUtils.getCurrentProfile(); + + assertThat(Profile.fromInt(identifier.asInt())).isEqualTo(identifier); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/RobolectricTestUtilities.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/RobolectricTestUtilities.java new file mode 100644 index 0000000..ae14549 --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/RobolectricTestUtilities.java @@ -0,0 +1,362 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import static android.Manifest.permission.INTERACT_ACROSS_PROFILES; +import static android.os.Looper.getMainLooper; +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.INTERACT_ACROSS_USERS; +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.INTERACT_ACROSS_USERS_FULL; +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.robolectric.Shadows.shadowOf; + +import android.app.Application; +import android.app.admin.DevicePolicyManager; +import android.content.ComponentName; +import android.content.Intent; +import android.content.ServiceConnection; +import android.content.pm.ApplicationInfo; +import android.content.pm.CrossProfileApps; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.os.IBinder; +import android.os.UserHandle; +import android.os.UserManager; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.types.TestCrossProfileType; +import com.google.common.util.concurrent.FluentFuture; +import com.google.common.util.concurrent.ListenableFuture; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicReference; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.robolectric.Robolectric; +import org.robolectric.shadow.api.Shadow; +import org.robolectric.shadows.ShadowContextImpl; +import org.robolectric.shadows.ShadowProcess; +import org.robolectric.shadows.ShadowUserManager.UserState; + +public class RobolectricTestUtilities { + + private static final int PERSONAL_PROFILE_USER_ID = 0; + private static final int WORK_PROFILE_USER_ID = 10; + + /* Matches UserHandle#PER_USER_RANGE */ + private static final int PER_USER_RANGE = 100000; + + private final UserHandle personalProfileUserHandle = + SharedTestUtilities.getUserHandleForUserId(PERSONAL_PROFILE_USER_ID); + private final UserHandle workProfileUserHandle = + SharedTestUtilities.getUserHandleForUserId(WORK_PROFILE_USER_ID); + private static final int WORK_UID = PER_USER_RANGE * WORK_PROFILE_USER_ID; + private static final int PERSONAL_UID = PER_USER_RANGE * PERSONAL_PROFILE_USER_ID; + private final Application context; + private final DevicePolicyManager devicePolicyManager; + private final UserManager userManager; + private final CrossProfileApps crossProfileApps; + private final ShadowContextImpl shadowContext; + private final PackageManager packageManager; + private final ComponentName profileOwnerComponentName = new ComponentName("profileowner", ""); + private final PackageInfo profileOwnerPackage = new PackageInfo(); + private final TestScheduledExecutorService scheduledExecutorService; + + public static final String TEST_CONNECTOR_CLASS_NAME = TestProfileConnector.class.getName(); + public static final String TEST_SERVICE_CLASS_NAME = TEST_CONNECTOR_CLASS_NAME + "_Service"; + + // These permissions should persist across profiles + private boolean hasGrantedInteractAcrossProfiles = false; + private boolean hasGrantedInteractAcrossUsers = false; + private boolean hasGrantedInteractAcrossUsersFull = false; + + private @Nullable ProfileConnector connector; + + public RobolectricTestUtilities( + ProfileConnector connector, TestScheduledExecutorService scheduledExecutorService) { + this((Application) connector.applicationContext(), scheduledExecutorService); + this.connector = connector; + } + + public RobolectricTestUtilities( + Application context, TestScheduledExecutorService scheduledExecutorService) { + this.context = context; + devicePolicyManager = context.getSystemService(DevicePolicyManager.class); + userManager = context.getSystemService(UserManager.class); + crossProfileApps = context.getSystemService(CrossProfileApps.class); + packageManager = context.getPackageManager(); + shadowContext = Shadow.extract(context.getBaseContext()); + this.scheduledExecutorService = scheduledExecutorService; + + profileOwnerPackage.applicationInfo = new ApplicationInfo(); + profileOwnerPackage.packageName = profileOwnerComponentName.getPackageName(); + } + + public void initTests() { + TestCrossProfileType.voidMethodCalls = 0; + CrossProfileSDKUtilities.clearCache(); + createPersonalUser(); + } + + public void startConnectingAndWait() { + connector.startConnecting(); + advanceTimeBySeconds(1); + } + + public void disconnect() { + connector.stopManualConnectionManagement(); + advanceTimeBySeconds(31); // Give time to timeout connection + } + + public void createPersonalUser() { + shadowOf(userManager).addUser(PERSONAL_PROFILE_USER_ID, "Personal Profile", /* flags= */ 0); + shadowOf(userManager) + .addProfile(PERSONAL_PROFILE_USER_ID, PERSONAL_PROFILE_USER_ID, "Personal Profile", 0); + shadowOf(userManager).setUserState(personalProfileUserHandle, UserState.STATE_RUNNING_UNLOCKED); + } + + public void createWorkUser() { + shadowOf(userManager).addUser(WORK_PROFILE_USER_ID, "Work Profile", /* flags= */ 0); + shadowOf(userManager) + .addProfile(PERSONAL_PROFILE_USER_ID, WORK_PROFILE_USER_ID, "Work Profile", 0); + shadowOf(userManager).addProfile(WORK_PROFILE_USER_ID, WORK_PROFILE_USER_ID, "Work Profile", 0); + shadowOf(userManager) + .addProfile(WORK_PROFILE_USER_ID, PERSONAL_PROFILE_USER_ID, "Personal Profile", 0); + } + + public void turnOnWorkProfileWithoutUnlocking() { + shadowOf(userManager).setUserState(workProfileUserHandle, UserState.STATE_RUNNING_LOCKED); + tryAddTargetUserProfile(workProfileUserHandle); + Intent intent = new Intent(); + intent.setAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE); + context.sendBroadcast(intent); + shadowOf(getMainLooper()).idle(); + } + + public void turnOnWorkProfile() { + shadowOf(userManager).setUserState(workProfileUserHandle, UserState.STATE_RUNNING_UNLOCKED); + tryAddTargetUserProfile(workProfileUserHandle); + Intent intent = new Intent(); + intent.setAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE); + context.sendBroadcast(intent); + intent = new Intent(); + intent.setAction(Intent.ACTION_USER_UNLOCKED); + context.sendBroadcast(intent); + advanceTimeBySeconds(10); + } + + public void turnOffWorkProfile() { + shadowOf(userManager).setUserState(workProfileUserHandle, UserState.STATE_SHUTDOWN); + removeTargetUserProfile(workProfileUserHandle); + simulateDisconnectingServiceConnection(); + Intent intent = new Intent(); + intent.setAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE); + context.sendBroadcast(intent); + advanceTimeBySeconds(10); + } + + private void tryAddTargetUserProfile(UserHandle userHandle) { + try { + addTargetUserProfile(userHandle); + } catch (IllegalArgumentException e) { + // This is thrown if we are running on that profile + } + } + + private void addTargetUserProfile(UserHandle userHandle) { + if (VERSION.SDK_INT < VERSION_CODES.P) { + return; + } + shadowOf(crossProfileApps).addTargetUserProfile(userHandle); + } + + private void tryRemoveTargetUserProfile(UserHandle userHandle) { + try { + removeTargetUserProfile(userHandle); + } catch (IllegalArgumentException e) { + // This is thrown if we are running on that profile + } + } + + private void removeTargetUserProfile(UserHandle userHandle) { + if (VERSION.SDK_INT < VERSION_CODES.P) { + return; + } + shadowOf(crossProfileApps).removeTargetUserProfile(userHandle); + } + + public void simulateDisconnectingServiceConnection() { + ServiceConnection serviceConnection = getServiceConnection(); + if (serviceConnection == null) { + return; + } + serviceConnection.onServiceDisconnected(new ComponentName("", "")); + } + + private ServiceConnection getServiceConnection() { + if (getBoundServiceConnections().isEmpty()) { + return null; + } + return getBoundServiceConnections().get(0); + } + + private List<ServiceConnection> getBoundServiceConnections() { + return shadowOf(context).getBoundServiceConnections(); + } + + public void setRunningOnPersonalProfile() { + shadowContext.setUserId(PERSONAL_PROFILE_USER_ID); + shadowOf(context.getPackageManager()).removePackage(profileOwnerPackage.packageName); + shadowOf(devicePolicyManager).setProfileOwner(null); + shadowOf(userManager).setManagedProfile(false); + setUid(PERSONAL_UID); + shadowOf(context.getPackageManager()).setPackagesForUid(PERSONAL_UID, context.getPackageName()); + tryRemoveTargetUserProfile(personalProfileUserHandle); + tryAddTargetUserProfile(workProfileUserHandle); + regrantPermissions(); + } + + public void setRunningOnWorkProfile() { + shadowContext.setUserId(WORK_PROFILE_USER_ID); + setHasProfileOwner(); + shadowOf(userManager).setManagedProfile(true); + tryRemoveTargetUserProfile(workProfileUserHandle); + setUid(WORK_UID); + shadowOf(context.getPackageManager()).setPackagesForUid(WORK_UID, context.getPackageName()); + shadowOf(userManager).setUserState(personalProfileUserHandle, UserState.STATE_RUNNING_UNLOCKED); + addTargetUserProfile(personalProfileUserHandle); + regrantPermissions(); + } + + public void setHasProfileOwner() { + shadowOf(context.getPackageManager()).installPackage(profileOwnerPackage); + shadowOf(devicePolicyManager).setProfileOwner(profileOwnerComponentName); + } + + private void setUid(int uid) { + ShadowProcess.setUid(uid); + // This is needed for CrossProfileApps but causes issues for < P + if (VERSION.SDK_INT >= VERSION_CODES.P) { + shadowOf(context.getPackageManager()).setNameForUid(uid, context.getPackageName()); + } + } + + public void setBinding(IBinder binder, String connectorClassQualifiedName) { + ComponentName serviceClassComponentName = + new ComponentName(context.getPackageName(), connectorClassQualifiedName + "_Service"); + Intent bindIntent = new Intent(); + bindIntent.setComponent(serviceClassComponentName); + + ICrossProfileService.Stub actualServiceStub = (ICrossProfileService.Stub) binder; + + shadowOf(context) + .setComponentNameAndServiceForBindServiceForIntent( + bindIntent, serviceClassComponentName, actualServiceStub); + } + + public void setRequestsPermissions(String... permissions) { + PackageInfo packageInfo = new PackageInfo(); + packageInfo.packageName = context.getPackageName(); + packageInfo.requestedPermissions = permissions; + shadowOf(packageManager).installPackage(packageInfo); + } + + public void grantPermissions(String... permissions) { + for (String permission : permissions) { + if (permission.equals(INTERACT_ACROSS_USERS)) { + hasGrantedInteractAcrossUsers = true; + } + if (permission.equals(INTERACT_ACROSS_USERS_FULL)) { + hasGrantedInteractAcrossUsersFull = true; + } + if (permission.equals(INTERACT_ACROSS_PROFILES)) { + hasGrantedInteractAcrossProfiles = true; + } + } + shadowOf(context).grantPermissions(permissions); + } + + public void denyPermissions(String... permissions) { + for (String permission : permissions) { + if (permission.equals(INTERACT_ACROSS_USERS)) { + hasGrantedInteractAcrossUsers = false; + } + if (permission.equals(INTERACT_ACROSS_USERS_FULL)) { + hasGrantedInteractAcrossUsersFull = false; + } + if (permission.equals(INTERACT_ACROSS_PROFILES)) { + hasGrantedInteractAcrossProfiles = false; + } + } + shadowOf(context).denyPermissions(permissions); + } + + private void regrantPermissions() { + if (hasGrantedInteractAcrossProfiles) { + grantPermissions(INTERACT_ACROSS_PROFILES); + } else { + denyPermissions(INTERACT_ACROSS_PROFILES); + } + if (hasGrantedInteractAcrossUsers) { + grantPermissions(INTERACT_ACROSS_USERS); + } else { + denyPermissions(INTERACT_ACROSS_USERS); + } + if (hasGrantedInteractAcrossUsersFull) { + grantPermissions(INTERACT_ACROSS_USERS_FULL); + } else { + denyPermissions(INTERACT_ACROSS_USERS_FULL); + } + } + + public @Nullable Throwable assertFutureHasException( + ListenableFuture<?> future, Class<? extends Throwable> throwable) { + AtomicReference<Throwable> thrown = new AtomicReference<>(); + try { + FluentFuture.from(future) + .catching( + throwable, + t -> { + // Expected + thrown.set(t); + return null; + }, + directExecutor()) + .get(); + } catch (InterruptedException | ExecutionException e) { + throw new AssertionError("Unhandled exception", e); + } + + assertThat(thrown.get()).isNotNull(); + return thrown.get(); + } + + public void advanceTimeBySeconds(int intervalSeconds) { + for (int i = 0; i < intervalSeconds; i++) { + if (scheduledExecutorService != null) { + try { + scheduledExecutorService.advanceTimeBy(1, SECONDS); + } catch (Exception e) { + throw new IllegalStateException("Error advancing time", e); + } + + } + Robolectric.getForegroundThreadScheduler().advanceBy(1, SECONDS); + shadowOf(getMainLooper()).idle(); + } + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/TestICrossProfileCallback.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/TestICrossProfileCallback.java new file mode 100644 index 0000000..461cc9a --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/TestICrossProfileCallback.java @@ -0,0 +1,63 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import android.os.IBinder; +import android.os.Parcel; +import android.os.RemoteException; + +/** + * An implementation of {@link ICrossProfileCallback} which just redirects call to a given {@link + * LocalCallback}. + * + * <p>This does not support preparing results, it only supports results which fit into a single + * block. + */ +public class TestICrossProfileCallback implements ICrossProfileCallback { + private final LocalCallback localCallback; + + public TestICrossProfileCallback(LocalCallback localCallback) { + this.localCallback = localCallback; + } + + @Override + public void prepareResult(long callId, int blockId, int numBytes, byte[] params) + throws RemoteException {} + + @Override + public void onResult(long callId, int blockId, int methodIdentifier, byte[] params) + throws RemoteException { + Parcel p = Parcel.obtain(); // Recycled in this method + p.unmarshall(params, 0, params.length); + p.setDataPosition(0); + localCallback.onResult(methodIdentifier, p); + p.recycle(); + } + + @Override + public void onException(long callId, int blockId, byte[] params) throws RemoteException { + Parcel p = Parcel.obtain(); // Recycled in this method + p.unmarshall(params, 0, params.length); + p.setDataPosition(0); + localCallback.onException(p); + p.recycle(); + } + + @Override + public IBinder asBinder() { + return null; + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/TestScheduledExecutorService.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/TestScheduledExecutorService.java new file mode 100644 index 0000000..f237e66 --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/TestScheduledExecutorService.java @@ -0,0 +1,179 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import java.util.List; +import java.util.Queue; +import java.util.concurrent.AbstractExecutorService; +import java.util.concurrent.Callable; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.Delayed; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +/** + * A test {@link ScheduledExecutorService} which supports only the methods used by the Connected + * Apps SDK. + * + * <p>Use {@link #advanceTimeBy(long, TimeUnit)} for progress time. Everything is executed on + * the calling thread. + */ +public class TestScheduledExecutorService extends AbstractExecutorService implements ScheduledExecutorService { + + private long millisPast = 0; + private final Queue<SimpleScheduledFuture<?>> executeQueue = new ConcurrentLinkedQueue<>(); + public TestScheduledExecutorService() {} + + @Override + public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) { + return schedule((Callable<Void>) () -> { + command.run(); + return null; + }, delay, unit); + } + + @Override + public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit) { + if (executeQueue.isEmpty()) { + millisPast = 0; + } + SimpleScheduledFuture<V> future = + new SimpleScheduledFuture<>(callable, millisPast + unit.toMillis(delay)); + executeQueue.add(future); + return future; + } + + @Override + public ScheduledFuture<?> scheduleAtFixedRate( + Runnable command, long initialDelay, long period, TimeUnit unit) { + throw new UnsupportedOperationException(); + } + + @Override + public ScheduledFuture<?> scheduleWithFixedDelay( + Runnable command, long initialDelay, long delay, TimeUnit unit) { + throw new UnsupportedOperationException(); + } + + @Override + public void shutdown() { + throw new UnsupportedOperationException(); + } + + @Override + public List<Runnable> shutdownNow() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isShutdown() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isTerminated() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean awaitTermination(long timeout, TimeUnit unit) { + throw new UnsupportedOperationException(); + } + + @Override + public void execute(Runnable command) { + command.run(); + } + + public void advanceTimeBy(long timeout, TimeUnit unit) throws Exception { + advanceTimeByMillis(unit.toMillis(timeout)); + } + + private void advanceTimeByMillis(long timeoutMillis) throws Exception { + millisPast += timeoutMillis; + while (!executeQueue.isEmpty() && executeQueue.peek().getDelay(MILLISECONDS) <= millisPast) { + executeQueue.remove().complete(); + } + } + + private static class SimpleScheduledFuture<T> implements ScheduledFuture<T> { + + private final Callable<T> callable; + private final long timeoutMillis; + private boolean isCancelled = false; + private boolean isDone = false; + private T value; + + public SimpleScheduledFuture(Callable<T> callable, long timeoutMillis) { + this.callable = callable; + this.timeoutMillis = timeoutMillis; + } + + public void complete() throws Exception { + if (isDone || isCancelled()) { + return; + } + + isDone = true; + value = callable.call(); + } + + @Override + public long getDelay(TimeUnit unit) { + return unit.convert(timeoutMillis, MILLISECONDS); + } + + @Override + public int compareTo(Delayed o) { + return 0; + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + isCancelled = true; + return true; + } + + @Override + public boolean isCancelled() { + return isCancelled; + } + + @Override + public boolean isDone() { + return isDone; + } + + @Override + public T get() { + if (!isDone) { + throw new IllegalStateException("Not executed yet"); + } + return value; + } + + @Override + public T get(long timeout, TimeUnit unit) { + if (!isDone) { + throw new IllegalStateException("Not executed yet"); + } + return value; + } + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/TestService.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/TestService.java new file mode 100644 index 0000000..cb11f3d --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/TestService.java @@ -0,0 +1,102 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import android.os.Parcel; +import android.os.RemoteException; +import com.google.android.enterprise.connectedapps.internal.ByteUtilities; +import com.google.auto.value.AutoValue; +import org.checkerframework.checker.nullness.qual.Nullable; + +public class TestService extends ICrossProfileService.Stub { + + @AutoValue + abstract static class LoggedCrossProfileMethodCall { + abstract long getCrossProfileTypeIdentifier(); + + abstract long getMethodIdentifier(); + + abstract Parcel getParams(); + + @Nullable + abstract ICrossProfileCallback callback(); + + static LoggedCrossProfileMethodCall create( + long crossProfileTypeIdentifier, + long methodIdentifier, + Parcel params, + ICrossProfileCallback callback) { + return new AutoValue_TestService_LoggedCrossProfileMethodCall( + crossProfileTypeIdentifier, methodIdentifier, params, callback); + } + } + + private LoggedCrossProfileMethodCall lastCall; + private Parcel responseParcel = Parcel.obtain(); // Recycled in #setResponseParcel + + LoggedCrossProfileMethodCall lastCall() { + return lastCall; + } + + /** + * Set the parcel to be returned from a call to this service. + * + * <p>The previously set parcel will be recycled. + */ + void setResponseParcel(Parcel responseParcel) { + this.responseParcel.recycle(); + this.responseParcel = responseParcel; + } + + @Override + public void prepareCall(long callId, int blockId, int numBytes, byte[] paramsBytes) {} + + @Override + public byte[] call( + long callId, + int blockId, + long crossProfileTypeIdentifier, + int methodIdentifier, + byte[] paramsBytes, + ICrossProfileCallback callback) + throws RemoteException { + + Parcel parcel = Parcel.obtain(); // Recycled by this method on next call + parcel.unmarshall(paramsBytes, 0, paramsBytes.length); + parcel.setDataPosition(0); + + if (lastCall != null) { + lastCall.getParams().recycle(); + } + + lastCall = + LoggedCrossProfileMethodCall.create( + crossProfileTypeIdentifier, methodIdentifier, parcel, callback); + + byte[] parcelBytes = responseParcel.marshall(); + return prepareResponse(parcelBytes); + } + + private static byte[] prepareResponse(byte[] parcelBytes) { + // This doesn't deal with large responses. + return ByteUtilities.joinByteArrays(new byte[] {0}, parcelBytes); + } + + @Override + public byte[] fetchResponse(long callId, int blockId) { + return null; + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/TestStringCrossProfileCallback.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/TestStringCrossProfileCallback.java new file mode 100644 index 0000000..a88c222 --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/TestStringCrossProfileCallback.java @@ -0,0 +1,57 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import android.os.IBinder; +import android.os.Parcel; +import android.os.RemoteException; +import com.google.android.enterprise.connectedapps.internal.ParcelUtilities; + +public class TestStringCrossProfileCallback implements ICrossProfileCallback { + + public int lastReceivedMethodIdentifier = -1; + public String lastReceivedMethodParam; + public Throwable lastReceivedException; + + @Override + public void prepareResult(long callId, int blockId, int numBytes, byte[] params) {} + + @Override + public void onResult(long callId, int blockId, int methodIdentifier, byte[] paramsBytes) + throws RemoteException { + lastReceivedMethodIdentifier = methodIdentifier; + Parcel parcel = Parcel.obtain(); // Recycled in this method + parcel.unmarshall(paramsBytes, 0, paramsBytes.length); + parcel.setDataPosition(0); + lastReceivedMethodParam = parcel.readString(); + parcel.recycle(); + } + + @Override + public void onException(long callId, int blockId, byte[] paramsBytes) throws RemoteException { + Parcel parcel = Parcel.obtain(); // Recycled in this method + parcel.unmarshall(paramsBytes, 0, paramsBytes.length); + parcel.setDataPosition(0); + + lastReceivedException = ParcelUtilities.readThrowableFromParcel(parcel); + parcel.recycle(); + } + + @Override + public IBinder asBinder() { + return null; + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/internal/ByteUtilitiesTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/internal/ByteUtilitiesTest.java new file mode 100644 index 0000000..4e0774e --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/internal/ByteUtilitiesTest.java @@ -0,0 +1,50 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.internal; + +import static com.google.common.truth.Truth.assertThat; + +import android.os.Build.VERSION_CODES; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.O) +public class ByteUtilitiesTest { + + private static final byte[] FIRST_ARRAY = new byte[] {1, 2}; + private static final byte[] SECOND_ARRAY = new byte[] {3, 4, 5}; + + @Test + public void joinByteArrays_resultIsMerged() { + // This is the merge of FIRST_ARRAY and SECOND_ARRAY + byte[] expectedResult = new byte[] {1, 2, 3, 4, 5}; + + assertThat(ByteUtilities.joinByteArrays(FIRST_ARRAY, SECOND_ARRAY)).isEqualTo(expectedResult); + } + + @Test + public void joinByteArrays_emptyFirstArray_equalsSecondArray() { + assertThat(ByteUtilities.joinByteArrays(new byte[0], SECOND_ARRAY)).isEqualTo(SECOND_ARRAY); + } + + @Test + public void joinByteArrays_emptySecondArray_equalsFirstArray() { + assertThat(ByteUtilities.joinByteArrays(FIRST_ARRAY, new byte[0])).isEqualTo(FIRST_ARRAY); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/internal/CrossProfileCallbackMultiMergerTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/internal/CrossProfileCallbackMultiMergerTest.java new file mode 100644 index 0000000..b7dbc95 --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/internal/CrossProfileCallbackMultiMergerTest.java @@ -0,0 +1,197 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.internal; + +import static com.google.common.truth.Truth.assertThat; + +import android.os.Build.VERSION_CODES; +import com.google.android.enterprise.connectedapps.Profile; +import com.google.android.enterprise.connectedapps.internal.CrossProfileCallbackMultiMerger.CrossProfileCallbackMultiMergerCompleteListener; +import java.util.Map; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.O) +public class CrossProfileCallbackMultiMergerTest { + + static class TestStringListener + implements CrossProfileCallbackMultiMergerCompleteListener<String> { + int timesResultsPosted = 0; + Map<Profile, String> results; + + @Override + public void onResult(Map<Profile, String> results) { + timesResultsPosted++; + this.results = results; + } + } + + private final Profile profile0 = Profile.fromInt(0); + private final Profile profile1 = Profile.fromInt(1); + private final Profile profile2 = Profile.fromInt(2); + private static final String STRING = "String"; + + private final TestStringListener stringListener = new TestStringListener(); + + @Test + public void onResult_expectedResultsNotReached_doesNotReportResult() { + int expectedResults = 2; + CrossProfileCallbackMultiMerger<String> merger = + new CrossProfileCallbackMultiMerger<>(expectedResults, stringListener); + + merger.onResult(profile0, STRING); + + assertThat(stringListener.timesResultsPosted).isEqualTo(0); + } + + @Test + public void onResult_expectedResultsReached_doesReportResult() { + int expectedResults = 2; + CrossProfileCallbackMultiMerger<String> merger = + new CrossProfileCallbackMultiMerger<>(expectedResults, stringListener); + merger.onResult(profile0, STRING); + + merger.onResult(profile1, STRING); + + assertThat(stringListener.timesResultsPosted).isEqualTo(1); + } + + @Test + public void onResult_reportsCorrectResults() { + int expectedResults = 2; + CrossProfileCallbackMultiMerger<String> merger = + new CrossProfileCallbackMultiMerger<>(expectedResults, stringListener); + merger.onResult(profile0, STRING); + + merger.onResult(profile1, STRING); + + assertThat(stringListener.results.get(profile0)).isEqualTo(STRING); + assertThat(stringListener.results.get(profile1)).isEqualTo(STRING); + } + + @Test + public void onResult_sameProfileReportsMultipleTimes_ignored() { + int expectedResults = 2; + CrossProfileCallbackMultiMerger<String> merger = + new CrossProfileCallbackMultiMerger<>(expectedResults, stringListener); + merger.onResult(profile0, STRING); + + merger.onResult(profile0, STRING); + + assertThat(stringListener.timesResultsPosted).isEqualTo(0); + } + + @Test + public void onResult_newResult_resultAlreadyReported_ignored() { + int expectedResults = 2; + CrossProfileCallbackMultiMerger<String> merger = + new CrossProfileCallbackMultiMerger<>(expectedResults, stringListener); + merger.onResult(profile0, STRING); + merger.onResult(profile1, STRING); + + merger.onResult(profile2, STRING); + + assertThat(stringListener.timesResultsPosted).isEqualTo(1); + assertThat(stringListener.results).doesNotContainKey(profile2); + } + + @Test + public void onResult_previousResultMissing_expectedResultsReached_doesReportResult() { + int expectedResults = 2; + CrossProfileCallbackMultiMerger<String> merger = + new CrossProfileCallbackMultiMerger<>(expectedResults, stringListener); + merger.missingResult(profile0); + + merger.onResult(profile1, STRING); + + assertThat(stringListener.timesResultsPosted).isEqualTo(1); + } + + @Test + public void missingResult_allResultsMissing_doesReportResult() { + int expectedResults = 2; + CrossProfileCallbackMultiMerger<String> merger = + new CrossProfileCallbackMultiMerger<>(expectedResults, stringListener); + merger.missingResult(profile0); + + merger.missingResult(profile1); + + assertThat(stringListener.timesResultsPosted).isEqualTo(1); + } + + @Test + public void missingResult_expectedResultsReached_doesReportResult() { + int expectedResults = 2; + CrossProfileCallbackMultiMerger<String> merger = + new CrossProfileCallbackMultiMerger<>(expectedResults, stringListener); + merger.onResult(profile1, STRING); + + merger.missingResult(profile0); + + assertThat(stringListener.timesResultsPosted).isEqualTo(1); + } + + @Test + public void missingResult_expectedResultsNotReached_doesNotReportResult() { + int expectedResults = 2; + CrossProfileCallbackMultiMerger<String> merger = + new CrossProfileCallbackMultiMerger<>(expectedResults, stringListener); + + merger.missingResult(profile0); + + assertThat(stringListener.timesResultsPosted).isEqualTo(0); + } + + @Test + public void missingResult_resultAlreadyPosted_doesNotRecord() { + int expectedResults = 2; + CrossProfileCallbackMultiMerger<String> merger = + new CrossProfileCallbackMultiMerger<>(expectedResults, stringListener); + merger.onResult(profile0, STRING); + + merger.missingResult(profile0); + merger.onResult(profile1, STRING); + + assertThat(stringListener.results.get(profile0)).isEqualTo(STRING); + } + + @Test + public void onResult_resultAlreadyPosted_doesNotRecord() { + int expectedResults = 2; + CrossProfileCallbackMultiMerger<String> merger = + new CrossProfileCallbackMultiMerger<>(expectedResults, stringListener); + merger.missingResult(profile0); + + merger.onResult(profile0, STRING); + merger.onResult(profile1, STRING); + + assertThat(stringListener.results).doesNotContainKey(profile0); + } + + @Test + public void construct_noExpectedResults_reportsResultImmediately() { + int expectedResults = 0; + + CrossProfileCallbackMultiMerger<String> ignoredMerger = + new CrossProfileCallbackMultiMerger<>(expectedResults, stringListener); + + assertThat(stringListener.timesResultsPosted).isEqualTo(1); + assertThat(stringListener.results).isEmpty(); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/internal/ParcelCallSenderTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/internal/ParcelCallSenderTest.java new file mode 100644 index 0000000..dd39fa1 --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/internal/ParcelCallSenderTest.java @@ -0,0 +1,125 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.internal; + +import static com.google.android.enterprise.connectedapps.StringUtilities.randomString; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import android.os.Parcel; +import android.os.RemoteException; +import android.os.TransactionTooLargeException; +import com.google.android.enterprise.connectedapps.exceptions.UnavailableProfileException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class ParcelCallSenderTest { + + static class TestParcelCallSender extends ParcelCallSender { + + int failPrepareCalls = 0; + int failCalls = 0; + int failFetchResponse = 0; + + private final ParcelCallReceiver parcelCallReceiver = new ParcelCallReceiver(); + + @Override + void prepareCall(long callId, int blockId, int totalBytes, byte[] bytes) + throws RemoteException { + if (failPrepareCalls-- > 0) { + throw new TransactionTooLargeException(); + } + + parcelCallReceiver.prepareCall(callId, blockId, totalBytes, bytes); + } + + @Override + byte[] call(long callId, int blockId, byte[] bytes) throws RemoteException { + if (failCalls-- > 0) { + throw new TransactionTooLargeException(); + } + + return parcelCallReceiver.prepareResponse( + callId, parcelCallReceiver.getPreparedCall(callId, blockId, bytes)); + } + + @Override + byte[] fetchResponse(long callId, int blockId) throws RemoteException { + if (failFetchResponse-- > 0) { + throw new TransactionTooLargeException(); + } + + return parcelCallReceiver.getPreparedResponse(callId, blockId); + } + } + + private final TestParcelCallSender parcelCallSender = new TestParcelCallSender(); + private static final String LARGE_STRING = randomString(1500000); // 3Mb + private static final Parcel LARGE_PARCEL = Parcel.obtain(); + + static { + LARGE_PARCEL.writeString(LARGE_STRING); + } + + @Test + public void makeParcelCall_prepareCallHasError_retriesUntilSuccess() + throws UnavailableProfileException { + parcelCallSender.failPrepareCalls = 5; + + assertThat(parcelCallSender.makeParcelCall(LARGE_PARCEL).readString()).isEqualTo(LARGE_STRING); + } + + @Test + public void makeParcelCall_prepareCallHasError_failsAfter10Retries() { + parcelCallSender.failPrepareCalls = 11; + + assertThrows( + UnavailableProfileException.class, () -> parcelCallSender.makeParcelCall(LARGE_PARCEL)); + } + + @Test + public void makeParcelCall_callHasError_retriesUntilSuccess() throws UnavailableProfileException { + parcelCallSender.failCalls = 5; + + assertThat(parcelCallSender.makeParcelCall(LARGE_PARCEL).readString()).isEqualTo(LARGE_STRING); + } + + @Test + public void makeParcelCall_callHasError_failsAfter10Retries() { + parcelCallSender.failCalls = 11; + + assertThrows( + UnavailableProfileException.class, () -> parcelCallSender.makeParcelCall(LARGE_PARCEL)); + } + + @Test + public void makeParcelCall_fetchResponseHasError_retriesUntilSuccess() + throws UnavailableProfileException { + parcelCallSender.failFetchResponse = 5; + + assertThat(parcelCallSender.makeParcelCall(LARGE_PARCEL).readString()).isEqualTo(LARGE_STRING); + } + + @Test + public void makeParcelCall_fetchResponseHasError_failsAfter10Retries() { + parcelCallSender.failFetchResponse = 11; + + assertThrows( + UnavailableProfileException.class, () -> parcelCallSender.makeParcelCall(LARGE_PARCEL)); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/AutomaticConnectionManagementTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/AutomaticConnectionManagementTest.java new file mode 100644 index 0000000..3f43c16 --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/AutomaticConnectionManagementTest.java @@ -0,0 +1,239 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.robotests; + +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.INTERACT_ACROSS_USERS; +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.assertFutureDoesNotHaveException; +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.assertFutureHasException; +import static com.google.common.truth.Truth.assertThat; + +import android.app.Application; +import android.app.Service; +import android.os.Build.VERSION_CODES; +import android.os.IBinder; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.RobolectricTestUtilities; +import com.google.android.enterprise.connectedapps.TestExceptionCallbackListener; +import com.google.android.enterprise.connectedapps.TestScheduledExecutorService; +import com.google.android.enterprise.connectedapps.TestVoidCallbackListenerImpl; +import com.google.android.enterprise.connectedapps.exceptions.UnavailableProfileException; +import com.google.android.enterprise.connectedapps.testapp.configuration.TestApplication; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileType; +import com.google.common.util.concurrent.ListenableFuture; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import org.robolectric.annotation.LooperMode; + +@LooperMode(LooperMode.Mode.LEGACY) +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.O) +public class AutomaticConnectionManagementTest { + + private final Application context = ApplicationProvider.getApplicationContext(); + private final TestVoidCallbackListenerImpl testVoidCallbackListener = + new TestVoidCallbackListenerImpl(); + private final TestExceptionCallbackListener testExceptionCallbackListener = + new TestExceptionCallbackListener(); + private final TestScheduledExecutorService scheduledExecutorService = + new TestScheduledExecutorService(); + private final TestProfileConnector testProfileConnector = + TestProfileConnector.create(context, scheduledExecutorService); + private final RobolectricTestUtilities testUtilities = + new RobolectricTestUtilities(testProfileConnector, scheduledExecutorService); + private final ProfileTestCrossProfileType profileTestCrossProfileType = + ProfileTestCrossProfileType.create(testProfileConnector); + + @Before + public void setUp() { + Service profileAwareService = Robolectric.setupService(TestApplication.getService()); + testUtilities.initTests(); + IBinder binder = profileAwareService.onBind(/* intent= */ null); + testUtilities.setBinding(binder, RobolectricTestUtilities.TEST_CONNECTOR_CLASS_NAME); + testUtilities.createWorkUser(); + testUtilities.turnOnWorkProfile(); + testUtilities.setRunningOnPersonalProfile(); + testUtilities.setRequestsPermissions(INTERACT_ACROSS_USERS); + testUtilities.grantPermissions(INTERACT_ACROSS_USERS); + testProfileConnector.stopManualConnectionManagement(); + } + + @Test + public void lessThanThirtySecondsWithNoCalls_doesNotDisconnect() { + profileTestCrossProfileType + .other() + .asyncVoidMethod(testVoidCallbackListener, testExceptionCallbackListener); + + testUtilities.advanceTimeBySeconds(29); + + assertThat(testProfileConnector.isConnected()).isTrue(); + } + + @Test + public void thirtySecondsWithNoCalls_disconnects() { + profileTestCrossProfileType + .other() + .asyncVoidMethod(testVoidCallbackListener, testExceptionCallbackListener); + + testUtilities.advanceTimeBySeconds(31); + + assertThat(testProfileConnector.isConnected()).isFalse(); + } + + @Test + public void moreThanThirtySecondsWithNoCalls_manualManagementStarted_doesNotDisconnect() { + profileTestCrossProfileType + .other() + .asyncVoidMethod(testVoidCallbackListener, testExceptionCallbackListener); + testUtilities.advanceTimeBySeconds(29); + testUtilities.startConnectingAndWait(); + + testUtilities.advanceTimeBySeconds(31); + + assertThat(testProfileConnector.isConnected()).isTrue(); + } + + @Test + public void lessThanThirtySecondsWithNoCalls_previousCallsWereChained_doesNotDisconnect() { + profileTestCrossProfileType + .other() + .asyncVoidMethod(testVoidCallbackListener, testExceptionCallbackListener); + testUtilities.advanceTimeBySeconds(29); + profileTestCrossProfileType + .other() + .asyncVoidMethod(testVoidCallbackListener, testExceptionCallbackListener); + + testUtilities.advanceTimeBySeconds(29); + + assertThat(testProfileConnector.isConnected()).isTrue(); + } + + @Test + public void thirtySecondsWithNoCalls_previousCallsWereChained_disconnects() { + profileTestCrossProfileType + .other() + .asyncVoidMethod(testVoidCallbackListener, testExceptionCallbackListener); + testUtilities.advanceTimeBySeconds(29); + profileTestCrossProfileType + .other() + .asyncVoidMethod(testVoidCallbackListener, testExceptionCallbackListener); + + testUtilities.advanceTimeBySeconds(31); + + assertThat(testProfileConnector.isConnected()).isFalse(); + } + + @Test + public void callWhichTakesALongTime_doesNotDisconnectDuringCall() { + profileTestCrossProfileType + .other() + .asyncVoidMethodWithNonBlockingDelayWith50SecondTimeout( + testVoidCallbackListener, /* secondsDelay= */ 40, testExceptionCallbackListener); + + testUtilities.advanceTimeBySeconds(31); + + assertThat(testProfileConnector.isConnected()).isTrue(); + } + + @Test + public void lessThanThirtySecondsAfterCallWhichTakesALongTime_doesNotDisconnect() { + profileTestCrossProfileType + .other() + .asyncVoidMethodWithNonBlockingDelayWith50SecondTimeout( + testVoidCallbackListener, /* secondsDelay= */ 40, testExceptionCallbackListener); + + testUtilities.advanceTimeBySeconds(69); + + assertThat(testProfileConnector.isConnected()).isTrue(); + } + + @Test + public void thirtySecondsAfterCallWhichTakesALongTime_disconnects() { + profileTestCrossProfileType + .other() + .asyncVoidMethodWithNonBlockingDelayWith50SecondTimeout( + testVoidCallbackListener, /* secondsDelay= */ 40, testExceptionCallbackListener); + + testUtilities.advanceTimeBySeconds(70); + + assertThat(testProfileConnector.isConnected()).isFalse(); + } + + @Test + public void newCall_afterDisconnection_reconnects() { + profileTestCrossProfileType + .other() + .asyncVoidMethod(testVoidCallbackListener, testExceptionCallbackListener); + testUtilities.advanceTimeBySeconds(31); + + profileTestCrossProfileType + .other() + .asyncVoidMethod(testVoidCallbackListener, testExceptionCallbackListener); + testUtilities.advanceTimeBySeconds(1); + + assertThat(testProfileConnector.isConnected()).isTrue(); + } + + @Test + public void stopManualConnectionManagement_lessThan30SecondsLater_doesNotDisconnect() { + testUtilities.startConnectingAndWait(); + testProfileConnector.stopManualConnectionManagement(); + + testUtilities.advanceTimeBySeconds(29); + + assertThat(testProfileConnector.isConnected()).isTrue(); + } + + @Test + public void stopManualConnectionManagement_moreThan30SecondsLater_disconnects() { + testUtilities.startConnectingAndWait(); + testProfileConnector.stopManualConnectionManagement(); + + testUtilities.advanceTimeBySeconds(29); + + assertThat(testProfileConnector.isConnected()).isTrue(); + } + + @Test + public void asyncCall_doesNotHavePermission_failsImmediately() { + testUtilities.denyPermissions(INTERACT_ACROSS_USERS); + + ListenableFuture<Void> future = + profileTestCrossProfileType.other().listenableFutureVoidMethod(); + testUtilities.advanceTimeBySeconds(1); + + assertFutureHasException(future, UnavailableProfileException.class); + } + + @Test + public void asyncCall_getsPermissionAfterPreviousFailure_doesNotFail() { + testUtilities.denyPermissions(INTERACT_ACROSS_USERS); + ListenableFuture<Void> unusedFuture = + profileTestCrossProfileType.other().listenableFutureVoidMethod(); + testUtilities.advanceTimeBySeconds(5); + testUtilities.grantPermissions(INTERACT_ACROSS_USERS); + + ListenableFuture<Void> future = + profileTestCrossProfileType.other().listenableFutureVoidMethod(); + + assertFutureDoesNotHaveException(future, UnavailableProfileException.class); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/AvailabilityListenerTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/AvailabilityListenerTest.java new file mode 100644 index 0000000..d999d22 --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/AvailabilityListenerTest.java @@ -0,0 +1,112 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.robotests; + +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.INTERACT_ACROSS_USERS; +import static com.google.common.truth.Truth.assertThat; + +import android.app.Application; +import android.app.Service; +import android.os.Build.VERSION_CODES; +import android.os.IBinder; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.RobolectricTestUtilities; +import com.google.android.enterprise.connectedapps.TestAvailabilityListener; +import com.google.android.enterprise.connectedapps.TestScheduledExecutorService; +import com.google.android.enterprise.connectedapps.testapp.configuration.TestApplication; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileType; +import com.google.common.util.concurrent.ListenableFuture; +import java.util.concurrent.ExecutionException; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.O) +public class AvailabilityListenerTest { + + private final Application context = ApplicationProvider.getApplicationContext(); + private final TestScheduledExecutorService scheduledExecutorService = + new TestScheduledExecutorService(); + private final TestProfileConnector testProfileConnector = + TestProfileConnector.create(context, scheduledExecutorService); + private final RobolectricTestUtilities testUtilities = + new RobolectricTestUtilities(testProfileConnector, scheduledExecutorService); + private final ProfileTestCrossProfileType type = + ProfileTestCrossProfileType.create(testProfileConnector); + + @Before + public void setUp() { + Service profileAwareService = Robolectric.setupService(TestApplication.getService()); + testUtilities.initTests(); + IBinder binder = profileAwareService.onBind(/* intent= */ null); + testUtilities.setBinding(binder, RobolectricTestUtilities.TEST_CONNECTOR_CLASS_NAME); + testUtilities.createWorkUser(); + testUtilities.turnOnWorkProfile(); + testUtilities.setRunningOnPersonalProfile(); + testUtilities.setRequestsPermissions(INTERACT_ACROSS_USERS); + testUtilities.grantPermissions(INTERACT_ACROSS_USERS); + testProfileConnector.stopManualConnectionManagement(); + } + + @Test + public void successfulCall_availabilityListenerDoesNotFire() + throws InterruptedException, ExecutionException { + testUtilities.turnOnWorkProfile(); + TestAvailabilityListener availabilityListener = new TestAvailabilityListener(); + testProfileConnector.registerAvailabilityListener(availabilityListener); + + ListenableFuture<Void> unusedFuture = type.other().listenableFutureVoidMethod(); + testUtilities.advanceTimeBySeconds(1); + unusedFuture.get(); + + assertThat(availabilityListener.availabilityChangedCount()).isEqualTo(0); + assertThat(testProfileConnector.isAvailable()).isTrue(); + } + + @Test + public void temporaryConnectionError_inProgressCall_availabilityListenerFires() { + testUtilities.turnOnWorkProfile(); + TestAvailabilityListener availabilityListener = new TestAvailabilityListener(); + testProfileConnector.registerAvailabilityListener(availabilityListener); + + ListenableFuture<Void> unusedFuture = + type.other().listenableFutureVoidMethodWithNonBlockingDelay(/* secondsDelay= */ 5); + testUtilities.simulateDisconnectingServiceConnection(); + testUtilities.advanceTimeBySeconds(1); + + assertThat(availabilityListener.availabilityChangedCount()).isGreaterThan(0); + assertThat(testProfileConnector.isAvailable()).isTrue(); + } + + @Test + public void temporaryConnectionError_noInProgressCall_availabilityListenerDoesNotFire() { + testUtilities.turnOnWorkProfile(); + testUtilities.startConnectingAndWait(); + TestAvailabilityListener availabilityListener = new TestAvailabilityListener(); + testProfileConnector.registerAvailabilityListener(availabilityListener); + + testUtilities.simulateDisconnectingServiceConnection(); + testUtilities.advanceTimeBySeconds(1); + + assertThat(availabilityListener.availabilityChangedCount()).isEqualTo(0); + assertThat(testProfileConnector.isAvailable()).isTrue(); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/BothProfilesAsyncTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/BothProfilesAsyncTest.java new file mode 100644 index 0000000..cb89aad --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/BothProfilesAsyncTest.java @@ -0,0 +1,223 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.robotests; + +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.INTERACT_ACROSS_USERS; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; +import static org.robolectric.annotation.LooperMode.Mode.LEGACY; + +import android.app.Application; +import android.app.Service; +import android.os.Build.VERSION_CODES; +import android.os.IBinder; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.Profile; +import com.google.android.enterprise.connectedapps.RobolectricTestUtilities; +import com.google.android.enterprise.connectedapps.TestBooleanCallbackListenerMultiImpl; +import com.google.android.enterprise.connectedapps.TestScheduledExecutorService; +import com.google.android.enterprise.connectedapps.TestStringCallbackListenerMultiImpl; +import com.google.android.enterprise.connectedapps.TestVoidCallbackListenerMultiImpl; +import com.google.android.enterprise.connectedapps.testapp.CustomRuntimeException; +import com.google.android.enterprise.connectedapps.testapp.configuration.TestApplication; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileType; +import com.google.android.enterprise.connectedapps.testapp.types.TestCrossProfileType; +import java.util.concurrent.ExecutionException; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.annotation.LooperMode; + +@LooperMode(LEGACY) +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.O) +public class BothProfilesAsyncTest { + private static final String STRING = "String"; + + private final Application context = ApplicationProvider.getApplicationContext(); + private final TestScheduledExecutorService scheduledExecutorService = + new TestScheduledExecutorService(); + private final TestProfileConnector testProfileConnector = + TestProfileConnector.create(context, scheduledExecutorService); + private final RobolectricTestUtilities testUtilities = + new RobolectricTestUtilities(testProfileConnector, scheduledExecutorService); + private final ProfileTestCrossProfileType profileTestCrossProfileType = + ProfileTestCrossProfileType.create(testProfileConnector); + + private final Profile currentProfileIdentifier = testProfileConnector.utils().getCurrentProfile(); + private final Profile otherProfileIdentifier = testProfileConnector.utils().getOtherProfile(); + + private final TestVoidCallbackListenerMultiImpl voidCallback = + new TestVoidCallbackListenerMultiImpl(); + private final TestStringCallbackListenerMultiImpl stringCallback = + new TestStringCallbackListenerMultiImpl(); + private final TestBooleanCallbackListenerMultiImpl booleanCallback = + new TestBooleanCallbackListenerMultiImpl(); + + @Before + public void setUp() { + Service profileAwareService = Robolectric.setupService(TestApplication.getService()); + testUtilities.initTests(); + IBinder binder = profileAwareService.onBind(/* intent= */ null); + testUtilities.setBinding(binder, RobolectricTestUtilities.TEST_CONNECTOR_CLASS_NAME); + testUtilities.createWorkUser(); + testUtilities.turnOnWorkProfile(); + testUtilities.setRunningOnPersonalProfile(); + testUtilities.setRequestsPermissions(INTERACT_ACROSS_USERS); + testUtilities.grantPermissions(INTERACT_ACROSS_USERS); + testProfileConnector.stopManualConnectionManagement(); + } + + @Test + public void both_async_canBind_calledOnBothProfiles() + throws ExecutionException, InterruptedException { + testUtilities.turnOnWorkProfile(); + + profileTestCrossProfileType.both().asyncVoidMethod(voidCallback); + + // This calls on the same profile because of robolectric + assertThat(TestCrossProfileType.voidMethodCalls).isEqualTo(2); + } + + @Test + public void both_async_canBind_resultContainsBothProfilesResults() + throws ExecutionException, InterruptedException { + testUtilities.turnOnWorkProfile(); + + profileTestCrossProfileType.both().asyncIdentityStringMethod(STRING, stringCallback); + + assertThat(stringCallback.stringCallbackValues.get(currentProfileIdentifier)).isEqualTo(STRING); + assertThat(stringCallback.stringCallbackValues.get(otherProfileIdentifier)).isEqualTo(STRING); + } + + @Test // This behaviour is expected right now but will change + public void both_async_blockingMethod_blocks() { + testUtilities.turnOnWorkProfile(); + + profileTestCrossProfileType + .both() + .asyncVoidMethodWithDelay(voidCallback, /* secondsDelay= */ 5); + + assertThat(voidCallback.callbackMethodCalls).isEqualTo(1); + } + + @Test + public void both_async_nonblockingMethod_doesNotBlock() { + testUtilities.turnOnWorkProfile(); + + profileTestCrossProfileType + .both() + .asyncVoidMethodWithNonBlockingDelay(voidCallback, /* secondsDelay= */ 5); + + assertThat(voidCallback.callbackMethodCalls).isEqualTo(0); + } + + @Test + public void both_async_nonblockingMethod_doesCallback() { + testUtilities.turnOnWorkProfile(); + + profileTestCrossProfileType + .both() + .asyncVoidMethodWithNonBlockingDelay(voidCallback, /* secondsDelay= */ 5); + testUtilities.advanceTimeBySeconds(10); + + assertThat(voidCallback.callbackMethodCalls).isEqualTo(1); + } + + @Test + public void both_async_canNotBind_calledOnOnlyCurrentProfile() { + testUtilities.turnOffWorkProfile(); + profileTestCrossProfileType.both().asyncVoidMethod(voidCallback); + + // This calls on the same profile because of robolectric + assertThat(TestCrossProfileType.voidMethodCalls).isEqualTo(1); + } + + @Test + public void both_async_canNotBind_resultContainsOnlyCurrentProfilesResult() { + testUtilities.turnOffWorkProfile(); + + profileTestCrossProfileType.both().asyncIdentityStringMethod(STRING, stringCallback); + + assertThat(stringCallback.stringCallbackValues.get(currentProfileIdentifier)).isEqualTo(STRING); + assertThat(stringCallback.stringCallbackValues.get(otherProfileIdentifier)).isEqualTo(null); + } + + @Test + public void both_async_connectionDropsDuringCall_resultContainsOnlyCurrentProfilesResult() { + testUtilities.turnOnWorkProfile(); + profileTestCrossProfileType + .both() + .asyncIdentityStringMethodWithNonBlockingDelay( + STRING, stringCallback, /* secondsDelay= */ 5); + testUtilities.advanceTimeBySeconds(2); + + testUtilities.turnOffWorkProfile(); + testUtilities.advanceTimeBySeconds(3); + + assertThat(stringCallback.stringCallbackValues).containsKey(currentProfileIdentifier); + assertThat(stringCallback.stringCallbackValues).doesNotContainKey(otherProfileIdentifier); + } + + @Test + public void both_async_timeoutSet_doesTimeout() { + profileTestCrossProfileType + .both() + .asyncIdentityStringMethodWithNonBlockingDelayWith3SecondTimeout( + STRING, stringCallback, /* secondsDelay= */ 5); + + testUtilities.advanceTimeBySeconds(6); + + assertThat(stringCallback.stringCallbackValues.get(currentProfileIdentifier)).isEqualTo(STRING); + assertThat(stringCallback.stringCallbackValues).doesNotContainKey(otherProfileIdentifier); + } + + @Test + public void both_async_timeoutSetByCaller_doesTimeout() { + profileTestCrossProfileType + .both() + .timeout(3000) + .asyncIdentityStringMethodWithNonBlockingDelay( + STRING, stringCallback, /* secondsDelay= */ 5); + + testUtilities.advanceTimeBySeconds(6); + + assertThat(stringCallback.stringCallbackValues.get(currentProfileIdentifier)).isEqualTo(STRING); + assertThat(stringCallback.stringCallbackValues).doesNotContainKey(otherProfileIdentifier); + } + + @Test + public void both_async_throwsRuntimeException_exceptionThrownOnCurrentProfileIsThrown() { + assertThrows( + CustomRuntimeException.class, + () -> + profileTestCrossProfileType + .both() + .asyncStringMethodWhichThrowsRuntimeException(stringCallback)); + } + + @Test + public void both_async_contextArgument_works() { + profileTestCrossProfileType.both().asyncIsContextArgumentPassed(booleanCallback); + + assertThat(booleanCallback.booleanCallbackValues.get(currentProfileIdentifier)).isTrue(); + assertThat(booleanCallback.booleanCallbackValues.get(otherProfileIdentifier)).isTrue(); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/BothProfilesListenableFutureTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/BothProfilesListenableFutureTest.java new file mode 100644 index 0000000..78b70c6 --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/BothProfilesListenableFutureTest.java @@ -0,0 +1,278 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.robotests; + +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.INTERACT_ACROSS_USERS; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; +import static org.robolectric.annotation.LooperMode.Mode.LEGACY; + +import android.app.Application; +import android.app.Service; +import android.os.Build.VERSION_CODES; +import android.os.IBinder; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.Profile; +import com.google.android.enterprise.connectedapps.RobolectricTestUtilities; +import com.google.android.enterprise.connectedapps.TestScheduledExecutorService; +import com.google.android.enterprise.connectedapps.testapp.CustomRuntimeException; +import com.google.android.enterprise.connectedapps.testapp.configuration.TestApplication; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileType; +import com.google.android.enterprise.connectedapps.testapp.types.TestCrossProfileType; +import com.google.common.util.concurrent.ListenableFuture; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.annotation.LooperMode; + +@LooperMode(LEGACY) +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.O) +public class BothProfilesListenableFutureTest { + private static final String STRING = "String"; + + private final Application context = ApplicationProvider.getApplicationContext(); + private final TestScheduledExecutorService scheduledExecutorService = + new TestScheduledExecutorService(); + private final TestProfileConnector testProfileConnector = + TestProfileConnector.create(context, scheduledExecutorService); + private final RobolectricTestUtilities testUtilities = + new RobolectricTestUtilities(testProfileConnector, scheduledExecutorService); + + private final Profile currentProfileIdentifier = testProfileConnector.utils().getCurrentProfile(); + private final Profile otherProfileIdentifier = testProfileConnector.utils().getOtherProfile(); + + private final ProfileTestCrossProfileType profileTestCrossProfileType = + ProfileTestCrossProfileType.create(testProfileConnector); + + @Before + public void setUp() { + Service profileAwareService = Robolectric.setupService(TestApplication.getService()); + testUtilities.initTests(); + IBinder binder = profileAwareService.onBind(/* intent= */ null); + testUtilities.setBinding(binder, RobolectricTestUtilities.TEST_CONNECTOR_CLASS_NAME); + testUtilities.createWorkUser(); + testUtilities.turnOnWorkProfile(); + testUtilities.setRunningOnPersonalProfile(); + testUtilities.setRequestsPermissions(INTERACT_ACROSS_USERS); + testUtilities.grantPermissions(INTERACT_ACROSS_USERS); + testProfileConnector.stopManualConnectionManagement(); + } + + @Test + public void both_listenableFuture_canBind_calledOnBothProfiles() + throws ExecutionException, InterruptedException { + testUtilities.turnOnWorkProfile(); + + profileTestCrossProfileType.both().listenableFutureVoidMethod().get(); + + // This calls on the same profile because of robolectric + assertThat(TestCrossProfileType.voidMethodCalls).isEqualTo(2); + } + + @Test + public void both_listenableFuture_canBind_resultContainsBothProfilesResults() + throws ExecutionException, InterruptedException { + testUtilities.turnOnWorkProfile(); + + Map<Profile, String> results = + profileTestCrossProfileType.both().listenableFutureIdentityStringMethod(STRING).get(); + + assertThat(results.get(currentProfileIdentifier)).isEqualTo(STRING); + assertThat(results.get(otherProfileIdentifier)).isEqualTo(STRING); + } + + @Test // This behaviour is expected right now but will change + public void both_listenableFuture_blockingMethod_blocks() { + testUtilities.turnOnWorkProfile(); + + ListenableFuture<Map<Profile, Void>> future = + profileTestCrossProfileType + .both() + .listenableFutureVoidMethodWithDelay(/* secondsDelay= */ 5); + + assertThat(future.isDone()).isTrue(); + } + + @Test + public void both_listenableFuture_nonblockingMethod_doesNotBlock() { + testUtilities.turnOnWorkProfile(); + + ListenableFuture<Map<Profile, Void>> future = + profileTestCrossProfileType + .both() + .listenableFutureVoidMethodWithNonBlockingDelay(/* secondsDelay= */ 5); + + assertThat(future.isDone()).isFalse(); + } + + @Test + public void both_listenableFuture_nonblockingMethod_doesCallback() { + testUtilities.turnOnWorkProfile(); + + ListenableFuture<Map<Profile, Void>> future = + profileTestCrossProfileType + .both() + .listenableFutureVoidMethodWithNonBlockingDelay(/* secondsDelay= */ 5); + testUtilities.advanceTimeBySeconds(10); + + assertThat(future.isDone()).isTrue(); + } + + @Test + public void both_listenableFuture_canNotBind_calledOnOnlyCurrentProfile() + throws ExecutionException, InterruptedException { + testUtilities.turnOffWorkProfile(); + + profileTestCrossProfileType.both().listenableFutureVoidMethod().get(); + + // This calls on the same profile because of robolectric + assertThat(TestCrossProfileType.voidMethodCalls).isEqualTo(1); + } + + @Test + public void both_listenableFuture_canNotBind_resultContainsOnlyCurrentProfilesResult() + throws ExecutionException, InterruptedException { + testUtilities.turnOffWorkProfile(); + + Map<Profile, String> results = + profileTestCrossProfileType.both().listenableFutureIdentityStringMethod(STRING).get(); + + assertThat(results.get(currentProfileIdentifier)).isEqualTo(STRING); + assertThat(results.get(otherProfileIdentifier)).isEqualTo(null); + } + + @Test + public void both_listenableFuture_isBound_becomesUnbound_calledOnBothProfiles() throws Exception { + testUtilities.turnOnWorkProfile(); + ListenableFuture<Map<Profile, Void>> unusedFuture = + profileTestCrossProfileType + .both() + .listenableFutureVoidMethodWithNonBlockingDelay(/* secondsDelay= */ 5); + + // Because of the way Robolectric currently works - the method is guaranteed to have executed + // before the work profile is turned off. This may change with later changes to the SDK so + // this test will be updated. + testUtilities.turnOffWorkProfile(); + + // This calls on the same profile because of robolectric + assertThat(TestCrossProfileType.voidMethodCalls).isEqualTo(2); + } + + @Test + public void both_listenableFuture_isBound_becomesUnbound_callbackFires() { + testUtilities.turnOnWorkProfile(); + ListenableFuture<Map<Profile, Void>> future = + profileTestCrossProfileType + .both() + .listenableFutureVoidMethodWithNonBlockingDelay(/* secondsDelay= */ 5); + + testUtilities.turnOffWorkProfile(); + + assertThat(future.isDone()).isTrue(); + } + + @Test + public void both_listenableFuture_profilesWithExceptionsAreNotIncludedInResults() + throws ExecutionException, InterruptedException { + ListenableFuture<Map<Profile, Void>> future = + profileTestCrossProfileType + .both() + .listenableFutureVoidMethodWhichSetsIllegalStateException(); + + assertThat(future.get()).hasSize(0); + } + + @Test + public void + both_listenableFuture_connectionDropsDuringCall_resultContainsOnlyCurrentProfilesResult() + throws ExecutionException, InterruptedException { + ListenableFuture<Map<Profile, String>> future = + profileTestCrossProfileType + .both() + .listenableFutureIdentityStringMethodWithNonBlockingDelay( + STRING, /* secondsDelay= */ 5); + testUtilities.advanceTimeBySeconds(2); + + testUtilities.turnOffWorkProfile(); + + Map<Profile, String> results = future.get(); + + assertThat(results).containsKey(currentProfileIdentifier); + assertThat(results).doesNotContainKey(otherProfileIdentifier); + } + + @Test + public void both_listenableFuture_timeoutSet_doesTimeout() + throws ExecutionException, InterruptedException { + ListenableFuture<Map<Profile, String>> future = + profileTestCrossProfileType + .both() + .listenableFutureIdentityStringMethodWithNonBlockingDelayWith3SecondTimeout( + STRING, /* secondsDelay= */ 5); + + testUtilities.advanceTimeBySeconds(6); + + Map<Profile, String> results = future.get(); + assertThat(results.get(currentProfileIdentifier)).isEqualTo(STRING); + assertThat(results).doesNotContainKey(otherProfileIdentifier); + } + + @Test + public void both_listenableFuture_timeoutSetByCaller_doesTimeout() + throws ExecutionException, InterruptedException { + ListenableFuture<Map<Profile, String>> future = + profileTestCrossProfileType + .both() + .timeout(3000) + .listenableFutureIdentityStringMethodWithNonBlockingDelay( + STRING, /* secondsDelay= */ 5); + + testUtilities.advanceTimeBySeconds(6); + + Map<Profile, String> results = future.get(); + assertThat(results.get(currentProfileIdentifier)).isEqualTo(STRING); + assertThat(results).doesNotContainKey(otherProfileIdentifier); + } + + @Test + public void + both_listenableFuture_throwsRuntimeException_exceptionThrownOnCurrentProfileIsThrown() { + assertThrows( + CustomRuntimeException.class, + () -> + profileTestCrossProfileType + .both() + .listenableFutureVoidMethodWhichThrowsRuntimeException()); + } + + @Test + public void both_listenableFuture_contextArgument_works() throws Exception { + ListenableFuture<Map<Profile, Boolean>> resultFuture = + profileTestCrossProfileType.both().futureIsContextArgumentPassed(); + + Map<Profile, Boolean> result = resultFuture.get(); + + assertThat(result.get(currentProfileIdentifier)).isTrue(); + assertThat(result.get(otherProfileIdentifier)).isTrue(); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/BothProfilesManualAsyncTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/BothProfilesManualAsyncTest.java new file mode 100644 index 0000000..66b7ef1 --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/BothProfilesManualAsyncTest.java @@ -0,0 +1,212 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.robotests; + +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.INTERACT_ACROSS_USERS; +import static com.google.common.truth.Truth.assertThat; +import static org.robolectric.annotation.LooperMode.Mode.LEGACY; + +import android.app.Application; +import android.app.Service; +import android.os.Build.VERSION_CODES; +import android.os.IBinder; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.Profile; +import com.google.android.enterprise.connectedapps.RobolectricTestUtilities; +import com.google.android.enterprise.connectedapps.TestScheduledExecutorService; +import com.google.android.enterprise.connectedapps.TestStringCallbackListenerMultiImpl; +import com.google.android.enterprise.connectedapps.TestVoidCallbackListenerMultiImpl; +import com.google.android.enterprise.connectedapps.testapp.configuration.TestApplication; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileType; +import com.google.android.enterprise.connectedapps.testapp.types.TestCrossProfileType; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.annotation.LooperMode; + +@LooperMode(LEGACY) +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.O) +public class BothProfilesManualAsyncTest { + private static final String STRING = "String"; + + private final Application context = ApplicationProvider.getApplicationContext(); + private final TestScheduledExecutorService scheduledExecutorService = + new TestScheduledExecutorService(); + private final TestProfileConnector testProfileConnector = + TestProfileConnector.create(context, scheduledExecutorService); + private final RobolectricTestUtilities testUtilities = + new RobolectricTestUtilities(testProfileConnector, scheduledExecutorService); + private final ProfileTestCrossProfileType profileTestCrossProfileType = + ProfileTestCrossProfileType.create(testProfileConnector); + + private final Profile currentProfileIdentifier = testProfileConnector.utils().getCurrentProfile(); + private final Profile otherProfileIdentifier = testProfileConnector.utils().getOtherProfile(); + + private final TestStringCallbackListenerMultiImpl stringCallback = + new TestStringCallbackListenerMultiImpl(); + private final TestVoidCallbackListenerMultiImpl voidCallback = + new TestVoidCallbackListenerMultiImpl(); + + @Before + public void setUp() { + Service profileAwareService = Robolectric.setupService(TestApplication.getService()); + testUtilities.initTests(); + IBinder binder = profileAwareService.onBind(/* intent= */ null); + testUtilities.setBinding(binder, RobolectricTestUtilities.TEST_CONNECTOR_CLASS_NAME); + testUtilities.createWorkUser(); + testUtilities.turnOnWorkProfile(); + testUtilities.setRunningOnPersonalProfile(); + testUtilities.setRequestsPermissions(INTERACT_ACROSS_USERS); + testUtilities.grantPermissions(INTERACT_ACROSS_USERS); + testUtilities.startConnectingAndWait(); + } + + @Test + public void both_async_manualConnection_isBound_calledOnBothProfiles() { + testUtilities.turnOnWorkProfile(); + testUtilities.startConnectingAndWait(); + + profileTestCrossProfileType.both().asyncVoidMethod(voidCallback); + + // This calls on the same profile because of robolectric + assertThat(TestCrossProfileType.voidMethodCalls).isEqualTo(2); + } + + @Test + public void both_async_manualConnection_isBound_resultContainsBothProfilesResults() { + testUtilities.turnOnWorkProfile(); + testUtilities.startConnectingAndWait(); + + profileTestCrossProfileType.both().asyncIdentityStringMethod(STRING, stringCallback); + + assertThat(stringCallback.stringCallbackValues.get(currentProfileIdentifier)).isEqualTo(STRING); + assertThat(stringCallback.stringCallbackValues.get(otherProfileIdentifier)).isEqualTo(STRING); + } + + @Test // This behaviour is expected right now but will change + public void both_async_manualConnection_isBound_blockingMethod_blocks() { + testUtilities.turnOnWorkProfile(); + testUtilities.startConnectingAndWait(); + + profileTestCrossProfileType + .both() + .asyncVoidMethodWithDelay(voidCallback, /* secondsDelay= */ 5); + + assertThat(voidCallback.callbackMethodCalls).isEqualTo(1); + } + + @Test + public void both_async_manualConnection_isBound_nonblockingMethod_doesNotBlock() { + testUtilities.turnOnWorkProfile(); + testUtilities.startConnectingAndWait(); + + profileTestCrossProfileType + .both() + .asyncVoidMethodWithNonBlockingDelay(voidCallback, /* secondsDelay= */ 5); + + assertThat(voidCallback.callbackMethodCalls).isEqualTo(0); + } + + @Test + public void both_async_manualConnection_isBound_nonblockingMethod_doesCallback() { + testUtilities.turnOnWorkProfile(); + testUtilities.startConnectingAndWait(); + + profileTestCrossProfileType + .both() + .asyncVoidMethodWithNonBlockingDelay(voidCallback, /* secondsDelay= */ 5); + testUtilities.advanceTimeBySeconds(10); + + assertThat(voidCallback.callbackMethodCalls).isEqualTo(1); + } + + @Test + public void both_async_manualConnection_isNotBound_calledOnOnlyCurrentProfile() { + testUtilities.startConnectingAndWait(); + testUtilities.turnOffWorkProfile(); + + profileTestCrossProfileType.both().asyncVoidMethod(voidCallback); + + // This calls on the same profile because of robolectric + assertThat(TestCrossProfileType.voidMethodCalls).isEqualTo(1); + } + + @Test + @Ignore // Will be supported when async methods are supported with exceptions + public void both_async_manualConnection_isNotBound_resultContainsOnlyCurrentProfilesResult() { + testUtilities.turnOffWorkProfile(); + + profileTestCrossProfileType.both().asyncIdentityStringMethod(STRING, stringCallback); + + assertThat(stringCallback.stringCallbackValues.get(currentProfileIdentifier)).isEqualTo(STRING); + assertThat(stringCallback.stringCallbackValues.get(otherProfileIdentifier)).isEqualTo(null); + } + + @Test + public void both_async_manualConnection_isBound_becomesUnbound_calledOnBothProfiles() { + testUtilities.turnOnWorkProfile(); + testUtilities.startConnectingAndWait(); + profileTestCrossProfileType + .both() + .asyncVoidMethodWithNonBlockingDelay(voidCallback, /* secondsDelay= */ 5); + + // Because of the way Robolectric currently works - the method is guaranteed to have executed + // before the work profile is turned off. This may change with later changes to the SDK so + // this test will be updated. + testUtilities.turnOffWorkProfile(); + testUtilities.advanceTimeBySeconds(5); // Complete local call + + // This calls on the same profile because of robolectric + assertThat(TestCrossProfileType.voidMethodCalls).isEqualTo(2); + } + + @Test + public void both_async_manualConnection_isBound_becomesUnbound_callbackFires() { + testUtilities.turnOnWorkProfile(); + testUtilities.startConnectingAndWait(); + profileTestCrossProfileType + .both() + .asyncVoidMethodWithNonBlockingDelay(voidCallback, /* secondsDelay= */ 5); + + testUtilities.turnOffWorkProfile(); + testUtilities.advanceTimeBySeconds(5); // Complete local call + + assertThat(voidCallback.callbackMethodCalls).isEqualTo(1); + } + + @Test + public void + both_async_manualConnection_connectionDropsDuringCall_resultContainsOnlyCurrentProfilesResult() { + testUtilities.turnOnWorkProfile(); + testUtilities.startConnectingAndWait(); + profileTestCrossProfileType + .both() + .asyncIdentityStringMethodWithNonBlockingDelay( + STRING, stringCallback, /* secondsDelay= */ 5); + + testUtilities.turnOffWorkProfile(); + testUtilities.advanceTimeBySeconds(5); // Complete local call + + assertThat(stringCallback.stringCallbackValues).containsKey(currentProfileIdentifier); + assertThat(stringCallback.stringCallbackValues).doesNotContainKey(otherProfileIdentifier); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/BothProfilesManualListenableFutureTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/BothProfilesManualListenableFutureTest.java new file mode 100644 index 0000000..a3ca7eb --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/BothProfilesManualListenableFutureTest.java @@ -0,0 +1,234 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.robotests; + +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.INTERACT_ACROSS_USERS; +import static com.google.common.truth.Truth.assertThat; +import static org.robolectric.annotation.LooperMode.Mode.LEGACY; + +import android.app.Application; +import android.app.Service; +import android.os.Build.VERSION_CODES; +import android.os.IBinder; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.Profile; +import com.google.android.enterprise.connectedapps.RobolectricTestUtilities; +import com.google.android.enterprise.connectedapps.TestScheduledExecutorService; +import com.google.android.enterprise.connectedapps.testapp.configuration.TestApplication; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileType; +import com.google.android.enterprise.connectedapps.testapp.types.TestCrossProfileType; +import com.google.common.util.concurrent.ListenableFuture; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.annotation.LooperMode; + +@LooperMode(LEGACY) +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.O) +public class BothProfilesManualListenableFutureTest { + private static final String STRING = "String"; + + private final Application context = ApplicationProvider.getApplicationContext(); + private final TestScheduledExecutorService scheduledExecutorService = + new TestScheduledExecutorService(); + private final TestProfileConnector testProfileConnector = + TestProfileConnector.create(context, scheduledExecutorService); + private final RobolectricTestUtilities testUtilities = + new RobolectricTestUtilities(testProfileConnector, scheduledExecutorService); + private final Profile currentProfileIdentifier = testProfileConnector.utils().getCurrentProfile(); + private final Profile otherProfileIdentifier = testProfileConnector.utils().getOtherProfile(); + private final ProfileTestCrossProfileType profileTestCrossProfileType = + ProfileTestCrossProfileType.create(testProfileConnector); + + @Before + public void setUp() { + Service profileAwareService = Robolectric.setupService(TestApplication.getService()); + testUtilities.initTests(); + IBinder binder = profileAwareService.onBind(/* intent= */ null); + testUtilities.setBinding(binder, RobolectricTestUtilities.TEST_CONNECTOR_CLASS_NAME); + testUtilities.createWorkUser(); + testUtilities.turnOnWorkProfile(); + testUtilities.setRunningOnPersonalProfile(); + testUtilities.setRequestsPermissions(INTERACT_ACROSS_USERS); + testUtilities.grantPermissions(INTERACT_ACROSS_USERS); + testUtilities.startConnectingAndWait(); + } + + @Test + public void both_listenableFuture_manualConnection_isBound_calledOnBothProfiles() + throws ExecutionException, InterruptedException { + testUtilities.startConnectingAndWait(); + testUtilities.turnOnWorkProfile(); + testUtilities.startConnectingAndWait(); + + profileTestCrossProfileType.both().listenableFutureVoidMethod().get(); + + // This calls on the same profile because of robolectric + assertThat(TestCrossProfileType.voidMethodCalls).isEqualTo(2); + } + + @Test + public void both_listenableFuture_manualConnection_isBound_resultContainsBothProfilesResults() + throws ExecutionException, InterruptedException { + testUtilities.startConnectingAndWait(); + testUtilities.turnOnWorkProfile(); + testUtilities.startConnectingAndWait(); + + Map<Profile, String> results = + profileTestCrossProfileType.both().listenableFutureIdentityStringMethod(STRING).get(); + + assertThat(results.get(currentProfileIdentifier)).isEqualTo(STRING); + assertThat(results.get(otherProfileIdentifier)).isEqualTo(STRING); + } + + @Test // This behaviour is expected right now but will change + public void both_listenableFuture_manualConnection_isBound_blockingMethod_blocks() { + testUtilities.startConnectingAndWait(); + testUtilities.turnOnWorkProfile(); + testUtilities.startConnectingAndWait(); + + ListenableFuture<Map<Profile, Void>> future = + profileTestCrossProfileType + .both() + .listenableFutureVoidMethodWithDelay(/* secondsDelay= */ 5); + + assertThat(future.isDone()).isTrue(); + } + + @Test + public void both_listenableFuture_manualConnection_isBound_nonblockingMethod_doesNotBlock() { + testUtilities.turnOnWorkProfile(); + testUtilities.startConnectingAndWait(); + + ListenableFuture<Map<Profile, Void>> future = + profileTestCrossProfileType + .both() + .listenableFutureVoidMethodWithNonBlockingDelay(/* secondsDelay= */ 5); + + assertThat(future.isDone()).isFalse(); + } + + @Test + public void both_listenableFuture_manualConnection_isBound_nonblockingMethod_doesCallback() { + testUtilities.turnOnWorkProfile(); + testUtilities.startConnectingAndWait(); + + ListenableFuture<Map<Profile, Void>> future = + profileTestCrossProfileType + .both() + .listenableFutureVoidMethodWithNonBlockingDelay(/* secondsDelay= */ 5); + testUtilities.advanceTimeBySeconds(10); + + assertThat(future.isDone()).isTrue(); + } + + @Test + public void both_listenableFuture_manualConnection_isNotBound_calledOnOnlyCurrentProfile() + throws ExecutionException, InterruptedException { + testUtilities.startConnectingAndWait(); + testUtilities.turnOffWorkProfile(); + + profileTestCrossProfileType.both().listenableFutureVoidMethod().get(); + + // This calls on the same profile because of robolectric + assertThat(TestCrossProfileType.voidMethodCalls).isEqualTo(1); + } + + @Test + public void + both_listenableFuture_manualConnection_isNotBound_resultContainsOnlyCurrentProfilesResult() + throws ExecutionException, InterruptedException { + testUtilities.startConnectingAndWait(); + testUtilities.turnOffWorkProfile(); + + Map<Profile, String> results = + profileTestCrossProfileType.both().listenableFutureIdentityStringMethod(STRING).get(); + + assertThat(results.get(currentProfileIdentifier)).isEqualTo(STRING); + assertThat(results.get(otherProfileIdentifier)).isEqualTo(null); + } + + @Test + public void both_listenableFuture_manualConnection_isBound_becomesUnbound_calledOnBothProfiles() { + testUtilities.turnOnWorkProfile(); + testUtilities.startConnectingAndWait(); + ListenableFuture<Map<Profile, Void>> unusedFuture = + profileTestCrossProfileType + .both() + .listenableFutureVoidMethodWithNonBlockingDelay(/* secondsDelay= */ 5); + + // Because of the way Robolectric currently works - the method is guaranteed to have executed + // before the work profile is turned off. This may change with later changes to the SDK so + // this test will be updated. + testUtilities.turnOffWorkProfile(); + + // This calls on the same profile because of robolectric + assertThat(TestCrossProfileType.voidMethodCalls).isEqualTo(2); + } + + @Test + public void both_listenableFuture_manualConnection_isBound_becomesUnbound_callbackFires() { + testUtilities.turnOnWorkProfile(); + testUtilities.startConnectingAndWait(); + ListenableFuture<Map<Profile, Void>> future = + profileTestCrossProfileType + .both() + .listenableFutureVoidMethodWithNonBlockingDelay(/* secondsDelay= */ 5); + + testUtilities.turnOffWorkProfile(); + + assertThat(future.isDone()).isTrue(); + } + + @Test + public void both_listenableFuture_manualConnection_profilesWithExceptionsAreNotIncludedInResults() + throws ExecutionException, InterruptedException { + testUtilities.startConnectingAndWait(); + ListenableFuture<Map<Profile, Void>> future = + profileTestCrossProfileType + .both() + .listenableFutureVoidMethodWhichSetsIllegalStateException(); + + assertThat(future.get()).isEmpty(); + } + + @Test + public void + both_listenableFuture_manualConnection_connectionDropsDuringCall_resultContainsOnlyCurrentProfilesResult() + throws ExecutionException, InterruptedException { + testUtilities.startConnectingAndWait(); + ListenableFuture<Map<Profile, String>> future = + profileTestCrossProfileType + .both() + .listenableFutureIdentityStringMethodWithNonBlockingDelay( + STRING, /* secondsDelay= */ 5); + testUtilities.advanceTimeBySeconds(2); + + testUtilities.turnOffWorkProfile(); + + Map<Profile, String> results = future.get(); + + assertThat(results).containsKey(currentProfileIdentifier); + assertThat(results).doesNotContainKey(otherProfileIdentifier); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/BothProfilesSynchronousTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/BothProfilesSynchronousTest.java new file mode 100644 index 0000000..20ee748 --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/BothProfilesSynchronousTest.java @@ -0,0 +1,121 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.robotests; + +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.INTERACT_ACROSS_USERS; +import static com.google.common.truth.Truth.assertThat; +import static junit.framework.TestCase.fail; +import static org.robolectric.annotation.LooperMode.Mode.LEGACY; + +import android.app.Application; +import android.app.Service; +import android.os.Build.VERSION_CODES; +import android.os.IBinder; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.Profile; +import com.google.android.enterprise.connectedapps.RobolectricTestUtilities; +import com.google.android.enterprise.connectedapps.TestScheduledExecutorService; +import com.google.android.enterprise.connectedapps.exceptions.ProfileRuntimeException; +import com.google.android.enterprise.connectedapps.testapp.CustomRuntimeException; +import com.google.android.enterprise.connectedapps.testapp.configuration.TestApplication; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileType; +import java.util.Map; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.annotation.LooperMode; + +@LooperMode(LEGACY) +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.O) +public class BothProfilesSynchronousTest { + + private static final String STRING = "String"; + + private final Application context = ApplicationProvider.getApplicationContext(); + private final TestScheduledExecutorService scheduledExecutorService = + new TestScheduledExecutorService(); + private final TestProfileConnector testProfileConnector = + TestProfileConnector.create(context, scheduledExecutorService); + private final RobolectricTestUtilities testUtilities = + new RobolectricTestUtilities(testProfileConnector, scheduledExecutorService); + private final ProfileTestCrossProfileType profileTestCrossProfileType = + ProfileTestCrossProfileType.create(testProfileConnector); + + private final Profile currentProfileIdentifier = testProfileConnector.utils().getCurrentProfile(); + private final Profile otherProfileIdentifier = testProfileConnector.utils().getOtherProfile(); + + @Before + public void setUp() { + Service profileAwareService = Robolectric.setupService(TestApplication.getService()); + testUtilities.initTests(); + IBinder binder = profileAwareService.onBind(/* intent= */ null); + testUtilities.setBinding(binder, RobolectricTestUtilities.TEST_CONNECTOR_CLASS_NAME); + testUtilities.createWorkUser(); + testUtilities.turnOnWorkProfile(); + testUtilities.setRunningOnPersonalProfile(); + testUtilities.setRequestsPermissions(INTERACT_ACROSS_USERS); + testUtilities.grantPermissions(INTERACT_ACROSS_USERS); + testUtilities.startConnectingAndWait(); + } + + @Test + public void both_synchronous_isBound_resultContainsBothProfileResults() { + testUtilities.turnOnWorkProfile(); + testUtilities.startConnectingAndWait(); + + Map<Profile, String> result = profileTestCrossProfileType.both().identityStringMethod(STRING); + + assertThat(result.get(currentProfileIdentifier)).isEqualTo(STRING); + assertThat(result.get(otherProfileIdentifier)).isEqualTo(STRING); + } + + @Test + public void both_synchronous_isNotBound_resultOnlyContainsCurrentProfileResult() { + testUtilities.turnOffWorkProfile(); + + Map<Profile, String> result = profileTestCrossProfileType.both().identityStringMethod(STRING); + + assertThat(result.get(currentProfileIdentifier)).isEqualTo(STRING); + assertThat(result).doesNotContainKey(otherProfileIdentifier); + } + + @Test + public void both_synchronous_throwsRuntimeException_exceptionThrownOnCurrentProfileIsThrown() { + // Since the exception is thrown on both sides, which is thrown first is not deterministic. + // This test just confirms one of the two is thrown + try { + profileTestCrossProfileType.both().methodWhichThrowsRuntimeException(); + fail(); + } catch (CustomRuntimeException expected) { + + } catch (ProfileRuntimeException expected) { + assertThat(expected).hasCauseThat().isInstanceOf(CustomRuntimeException.class); + } + } + + @Test + public void both_synchronous_contextArgument_works() { + Map<Profile, Boolean> result = profileTestCrossProfileType.both().isContextArgumentPassed(); + + assertThat(result.get(currentProfileIdentifier)).isTrue(); + assertThat(result.get(otherProfileIdentifier)).isTrue(); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/CrossProfileCallbackReceiverTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/CrossProfileCallbackReceiverTest.java new file mode 100644 index 0000000..b4118b1 --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/CrossProfileCallbackReceiverTest.java @@ -0,0 +1,51 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.robotests; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.android.enterprise.connectedapps.TestStringCrossProfileCallback; +import com.google.android.enterprise.connectedapps.internal.Bundler; +import com.google.android.enterprise.connectedapps.testapp.Profile_TestStringCallbackListener_Receiver; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileType_Bundler; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class CrossProfileCallbackReceiverTest { + + private static final String STRING = "String"; + + private final TestStringCrossProfileCallback callback = new TestStringCrossProfileCallback(); + private final Bundler bundler = new ProfileTestCrossProfileType_Bundler(); + private final Profile_TestStringCallbackListener_Receiver receiver = + new Profile_TestStringCallbackListener_Receiver(callback, bundler); + + @Test + public void asyncCallbackListenerReceiver_calls() { + receiver.stringCallback(STRING); + + assertThat(callback.lastReceivedMethodIdentifier).isNotEqualTo(-1); // Has been called + } + + @Test + public void asyncCallbackListenerReceiver_bundlesParams() { + receiver.stringCallback(STRING); + + assertThat(callback.lastReceivedMethodParam).isEqualTo(STRING); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/CrossProfileCallbackSenderTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/CrossProfileCallbackSenderTest.java new file mode 100644 index 0000000..5bd46b2 --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/CrossProfileCallbackSenderTest.java @@ -0,0 +1,67 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.robotests; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.android.enterprise.connectedapps.TestExceptionCallbackListener; +import com.google.android.enterprise.connectedapps.TestICrossProfileCallback; +import com.google.android.enterprise.connectedapps.TestStringCallbackListenerImpl; +import com.google.android.enterprise.connectedapps.internal.Bundler; +import com.google.android.enterprise.connectedapps.testapp.Profile_TestStringCallbackListener_Receiver; +import com.google.android.enterprise.connectedapps.testapp.Profile_TestStringCallbackListener_Sender; +import com.google.android.enterprise.connectedapps.testapp.TestStringCallbackListener; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileType_Bundler; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +/** + * Test the generated _Sender class for {@link TestStringCallbackListener}. + * + * <p>This tests indirectly by creating a _Receiver instance (tested in {@link + * CrossProfileCallbackReceiverTest}) and confirming that calls are passed through. This is because + * the {@code methodIdentifier} is unpredictable. + */ +@RunWith(RobolectricTestRunner.class) +public class CrossProfileCallbackSenderTest { + + private static final String STRING = "String"; + + private final TestExceptionCallbackListener exceptionCallback = + new TestExceptionCallbackListener(); + private final TestStringCallbackListenerImpl callback = new TestStringCallbackListenerImpl(); + private final Bundler bundler = new ProfileTestCrossProfileType_Bundler(); + private final Profile_TestStringCallbackListener_Sender sender = + new Profile_TestStringCallbackListener_Sender(callback, exceptionCallback, bundler); + private final Profile_TestStringCallbackListener_Receiver receiver = + new Profile_TestStringCallbackListener_Receiver( + new TestICrossProfileCallback(sender), bundler); + + @Test + public void asyncCallbackSender_routesCalls() { + receiver.stringCallback(STRING); + + assertThat(callback.callbackMethodCalls).isEqualTo(1); + } + + @Test + public void internalCallbackSender_unbundlesParams() { + receiver.stringCallback(STRING); + + assertThat(callback.stringCallbackValue).isEqualTo(STRING); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/CrossProfileInterfaceTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/CrossProfileInterfaceTest.java new file mode 100644 index 0000000..eb44ea0 --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/CrossProfileInterfaceTest.java @@ -0,0 +1,86 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.robotests; + +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.INTERACT_ACROSS_USERS; +import static com.google.common.truth.Truth.assertThat; +import static org.robolectric.annotation.LooperMode.Mode.LEGACY; + +import android.app.Application; +import android.app.Service; +import android.os.Build.VERSION_CODES; +import android.os.IBinder; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.Profile; +import com.google.android.enterprise.connectedapps.RobolectricTestUtilities; +import com.google.android.enterprise.connectedapps.TestScheduledExecutorService; +import com.google.android.enterprise.connectedapps.testapp.configuration.TestApplication; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileInterface; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.annotation.LooperMode; + +@LooperMode(LEGACY) +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.O) +public class CrossProfileInterfaceTest { + private static final String STRING = "String"; + private static final List<String> listOfString = Collections.singletonList(STRING); + + private final Application context = ApplicationProvider.getApplicationContext(); + private final TestScheduledExecutorService scheduledExecutorService = + new TestScheduledExecutorService(); + private final TestProfileConnector testProfileConnector = + TestProfileConnector.create(context, scheduledExecutorService); + private final RobolectricTestUtilities testUtilities = + new RobolectricTestUtilities(testProfileConnector, scheduledExecutorService); + private final ProfileTestCrossProfileInterface profileTestCrossProfileInterface = + ProfileTestCrossProfileInterface.create(testProfileConnector); + + private final Profile currentProfileIdentifier = testProfileConnector.utils().getCurrentProfile(); + private final Profile otherProfileIdentifier = testProfileConnector.utils().getOtherProfile(); + + @Before + public void setUp() { + Service profileAwareService = Robolectric.setupService(TestApplication.getService()); + testUtilities.initTests(); + IBinder binder = profileAwareService.onBind(/* intent= */ null); + testUtilities.setBinding(binder, RobolectricTestUtilities.TEST_CONNECTOR_CLASS_NAME); + testUtilities.createWorkUser(); + testUtilities.turnOnWorkProfile(); + testUtilities.setRunningOnPersonalProfile(); + testUtilities.setRequestsPermissions(INTERACT_ACROSS_USERS); + testUtilities.grantPermissions(INTERACT_ACROSS_USERS); + testUtilities.startConnectingAndWait(); + } + + @Test + public void crossProfileInterface_both_getsBothResults() { + Map<Profile, List<String>> results = + profileTestCrossProfileInterface.both().identityListOfStringMethod(listOfString); + + assertThat(results.get(currentProfileIdentifier)).isEqualTo(listOfString); + assertThat(results.get(otherProfileIdentifier)).isEqualTo(listOfString); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/CrossProfileServiceTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/CrossProfileServiceTest.java new file mode 100644 index 0000000..f9af23b --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/CrossProfileServiceTest.java @@ -0,0 +1,112 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.robotests; + +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.INTERACT_ACROSS_USERS; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; +import static org.robolectric.Shadows.shadowOf; +import static org.robolectric.annotation.LooperMode.Mode.LEGACY; + +import android.app.Application; +import android.app.Service; +import android.os.Build.VERSION_CODES; +import android.os.IBinder; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.RobolectricTestUtilities; +import com.google.android.enterprise.connectedapps.TestScheduledExecutorService; +import com.google.android.enterprise.connectedapps.exceptions.ProfileRuntimeException; +import com.google.android.enterprise.connectedapps.exceptions.UnavailableProfileException; +import com.google.android.enterprise.connectedapps.testapp.configuration.TestApplication; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileType; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.annotation.LooperMode; +import org.robolectric.shadows.ShadowBinder; + +@LooperMode(LEGACY) +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.O) +public class CrossProfileServiceTest { + private final Application context = ApplicationProvider.getApplicationContext(); + private final TestScheduledExecutorService scheduledExecutorService = + new TestScheduledExecutorService(); + private final TestProfileConnector testProfileConnector = + TestProfileConnector.create(context, scheduledExecutorService); + private final RobolectricTestUtilities testUtilities = + new RobolectricTestUtilities(testProfileConnector, scheduledExecutorService); + private final ProfileTestCrossProfileType profileTestCrossProfileType = + ProfileTestCrossProfileType.create(testProfileConnector); + private static final String DIFFERENT_PACKAGE_NAME = "com.different.package"; + + @Before + public void setUp() { + Service profileAwareService = Robolectric.setupService(TestApplication.getService()); + testUtilities.initTests(); + IBinder binder = profileAwareService.onBind(/* intent= */ null); + testUtilities.setBinding(binder, RobolectricTestUtilities.TEST_CONNECTOR_CLASS_NAME); + testUtilities.createWorkUser(); + testUtilities.turnOnWorkProfile(); + testUtilities.setRunningOnPersonalProfile(); + testUtilities.setRequestsPermissions(INTERACT_ACROSS_USERS); + testUtilities.grantPermissions(INTERACT_ACROSS_USERS); + testUtilities.startConnectingAndWait(); + } + + @After + public void tearDown() { + ShadowBinder.reset(); + } + + @Test + public void crossProfileMethodCall_doesNotThrowException() throws UnavailableProfileException { + testUtilities.startConnectingAndWait(); + + profileTestCrossProfileType.other().voidMethod(); + } + + @Test + public void crossProfileMethodCall_multiplePackagesForUid_doesNotThrowException() + throws UnavailableProfileException { + ShadowBinder.setCallingUid(10); + shadowOf(context.getPackageManager()) + .setPackagesForUid(10, DIFFERENT_PACKAGE_NAME, context.getPackageName()); + testUtilities.startConnectingAndWait(); + + profileTestCrossProfileType.other().voidMethod(); + } + + @Test + public void crossProfileMethodCall_callingFromInvalidPackage_throwsWrappedIllegalStateException() + throws UnavailableProfileException { + testUtilities.startConnectingAndWait(); + ShadowBinder.setCallingUid(10); + shadowOf(context.getPackageManager()).setPackagesForUid(10, DIFFERENT_PACKAGE_NAME); + + try { + profileTestCrossProfileType.other().voidMethod(); + fail(); + } catch (ProfileRuntimeException expected) { + assertThat(expected).hasCauseThat().isInstanceOf(IllegalStateException.class); + } + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/CrossProfileTypeProfileTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/CrossProfileTypeProfileTest.java new file mode 100644 index 0000000..3adf174 --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/CrossProfileTypeProfileTest.java @@ -0,0 +1,553 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.robotests; + +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.INTERACT_ACROSS_USERS; +import static com.google.common.truth.Truth.assertThat; +import static junit.framework.TestCase.fail; +import static org.junit.Assert.assertThrows; +import static org.robolectric.annotation.LooperMode.Mode.LEGACY; + +import android.app.Application; +import android.app.Service; +import android.os.Build.VERSION_CODES; +import android.os.IBinder; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.CrossProfileConnector; +import com.google.android.enterprise.connectedapps.Profile; +import com.google.android.enterprise.connectedapps.RobolectricTestUtilities; +import com.google.android.enterprise.connectedapps.TestExceptionCallbackListener; +import com.google.android.enterprise.connectedapps.TestScheduledExecutorService; +import com.google.android.enterprise.connectedapps.TestStringCallbackListenerImpl; +import com.google.android.enterprise.connectedapps.exceptions.ProfileRuntimeException; +import com.google.android.enterprise.connectedapps.exceptions.UnavailableProfileException; +import com.google.android.enterprise.connectedapps.testapp.CustomRuntimeException; +import com.google.android.enterprise.connectedapps.testapp.TestStringCallbackListener; +import com.google.android.enterprise.connectedapps.testapp.configuration.TestApplication; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileType; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileTypeWhichDoesNotSpecifyConnector; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.annotation.LooperMode; + +@LooperMode(LEGACY) +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.O) +public class CrossProfileTypeProfileTest { + + private static final String STRING = "String"; + + private final Application context = ApplicationProvider.getApplicationContext(); + private final TestScheduledExecutorService scheduledExecutorService = + new TestScheduledExecutorService(); + private final TestProfileConnector testProfileConnector = + TestProfileConnector.create(context, scheduledExecutorService); + private final RobolectricTestUtilities testUtilities = + new RobolectricTestUtilities(testProfileConnector, scheduledExecutorService); + private final ProfileTestCrossProfileType profileTestCrossProfileType = + ProfileTestCrossProfileType.create(testProfileConnector); + private final TestStringCallbackListener stringCallbackListener = + new TestStringCallbackListenerImpl(); + private final TestExceptionCallbackListener exceptionCallbackListener = + new TestExceptionCallbackListener(); + + @Before + public void setUp() { + Service profileAwareService = Robolectric.setupService(TestApplication.getService()); + testUtilities.initTests(); + IBinder binder = profileAwareService.onBind(/* intent= */ null); + testUtilities.setBinding(binder, RobolectricTestUtilities.TEST_CONNECTOR_CLASS_NAME); + testUtilities.createWorkUser(); + testUtilities.turnOnWorkProfile(); + testUtilities.setRunningOnPersonalProfile(); + testUtilities.setRequestsPermissions(INTERACT_ACROSS_USERS); + testUtilities.grantPermissions(INTERACT_ACROSS_USERS); + testUtilities.startConnectingAndWait(); + } + + @Test + public void profile_withIdentifierForCurrentProfile_runsOnCurrentProfile() + throws UnavailableProfileException { + testUtilities.turnOffWorkProfile(); + + Profile currentProfileIdentifier = testProfileConnector.utils().getCurrentProfile(); + + // If this runs on the other profile, an exception will be thrown. + profileTestCrossProfileType.profile(currentProfileIdentifier).voidMethod(); + } + + @Test + public void profile_withIdentifierForOtherProfile_runsOnOtherProfile() { + testUtilities.turnOffWorkProfile(); + + Profile otherProfileIdentifier = testProfileConnector.utils().getOtherProfile(); + + // If this runs on the other profile, an exception will be thrown. + assertThrows( + UnavailableProfileException.class, + () -> profileTestCrossProfileType.profile(otherProfileIdentifier).voidMethod()); + } + + @Test + public void work_calledFromWorkProfile_runsOnCurrentProfile() throws UnavailableProfileException { + testUtilities.turnOffWorkProfile(); + testUtilities.setRunningOnWorkProfile(); + + // If this runs on the other profile, an exception will be thrown. + profileTestCrossProfileType.work().voidMethod(); + } + + @Test + public void work_calledFromPersonalProfile_runsOnOtherProfile() { + testUtilities.turnOffWorkProfile(); + testUtilities.setRunningOnPersonalProfile(); + + // If this runs on the other profile, an exception will be thrown. + assertThrows( + UnavailableProfileException.class, () -> profileTestCrossProfileType.work().voidMethod()); + } + + @Test + public void personal_calledFromWorkProfile_runsOnOtherProfile() { + testUtilities.turnOffWorkProfile(); + testUtilities.setRunningOnWorkProfile(); + + // If this runs on the other profile, an exception will be thrown. + assertThrows( + UnavailableProfileException.class, + () -> profileTestCrossProfileType.personal().voidMethod()); + } + + @Test + public void personal_calledFromPersonalProfile_runsOnCurrentProfile() + throws UnavailableProfileException { + testUtilities.turnOffWorkProfile(); + testUtilities.setRunningOnPersonalProfile(); + + // If this runs on the other profile, an exception will be thrown. + profileTestCrossProfileType.personal().voidMethod(); + } + + @Test + public void primary_calledFromPrimaryProfile_runsOnCurrentProfile() + throws UnavailableProfileException { + // The primary profile is defined as work in TestProfileConnector + testUtilities.turnOffWorkProfile(); + testUtilities.setRunningOnWorkProfile(); + + // If this runs on the other profile, an exception will be thrown. + profileTestCrossProfileType.primary().voidMethod(); + } + + @Test + public void primary_calledFromSecondaryProfile_runsOnOtherProfile() + throws UnavailableProfileException { + // The primary profile is defined as work in TestProfileConnector + testUtilities.turnOffWorkProfile(); + testUtilities.setRunningOnPersonalProfile(); + + // If this runs on the other profile, an exception will be thrown. + assertThrows( + UnavailableProfileException.class, + () -> profileTestCrossProfileType.primary().voidMethod()); + } + + @Test + public void secondary_calledFromSecondaryProfile_runsOnCurrentProfile() + throws UnavailableProfileException { + // The primary profile is defined as work in TestProfileConnector + testUtilities.turnOffWorkProfile(); + testUtilities.setRunningOnPersonalProfile(); + + // If this runs on the other profile, an exception will be thrown. + profileTestCrossProfileType.secondary().voidMethod(); + } + + @Test + public void secondary_calledFromPrimaryProfile_runsOnOtherProfile() + throws UnavailableProfileException { + // The primary profile is defined as work in TestProfileConnector + testUtilities.turnOffWorkProfile(); + testUtilities.setRunningOnWorkProfile(); + + // If this runs on the other profile, an exception will be thrown. + assertThrows( + UnavailableProfileException.class, + () -> profileTestCrossProfileType.secondary().voidMethod()); + } + + @Test + public void primary_calledOnTypeWithoutConnector_connectorHasPrimary_works() + throws UnavailableProfileException { + ProfileTestCrossProfileTypeWhichDoesNotSpecifyConnector type = + ProfileTestCrossProfileTypeWhichDoesNotSpecifyConnector.create(testProfileConnector); + + assertThat(type.primary().identityStringMethod(STRING)).isEqualTo(STRING); + } + + @Test + public void secondary_calledOnTypeWithoutConnector_connectorHasPrimary_works() + throws UnavailableProfileException { + ProfileTestCrossProfileTypeWhichDoesNotSpecifyConnector type = + ProfileTestCrossProfileTypeWhichDoesNotSpecifyConnector.create(testProfileConnector); + + assertThat(type.secondary().identityStringMethod(STRING)).isEqualTo(STRING); + } + + @Test + public void suppliers_calledOnTypeWithoutConnector_connectorHasPrimary_works() { + ProfileTestCrossProfileTypeWhichDoesNotSpecifyConnector type = + ProfileTestCrossProfileTypeWhichDoesNotSpecifyConnector.create(testProfileConnector); + + assertThat(type.suppliers().identityStringMethod(STRING)).isNotEmpty(); + } + + @Test + public void + primary_calledOnTypeWithoutConnector_connectorDoesNotHavePrimary_throwsIllegalStateException() { + CrossProfileConnector crossProfileConnector = CrossProfileConnector.builder(context).build(); + ProfileTestCrossProfileTypeWhichDoesNotSpecifyConnector type = + ProfileTestCrossProfileTypeWhichDoesNotSpecifyConnector.create(crossProfileConnector); + + assertThrows(IllegalStateException.class, () -> type.primary().identityStringMethod(STRING)); + } + + @Test + public void + secondary_calledOnTypeWithoutConnector_connectorDoesNotHavePrimary_throwsIllegalStateException() { + CrossProfileConnector crossProfileConnector = CrossProfileConnector.builder(context).build(); + ProfileTestCrossProfileTypeWhichDoesNotSpecifyConnector type = + ProfileTestCrossProfileTypeWhichDoesNotSpecifyConnector.create(crossProfileConnector); + + assertThrows(IllegalStateException.class, () -> type.secondary().identityStringMethod(STRING)); + } + + @Test + public void + suppliers_calledOnTypeWithoutConnector_connectorDoesNotHavePrimary_throwsIllegalStateException() { + CrossProfileConnector crossProfileConnector = CrossProfileConnector.builder(context).build(); + ProfileTestCrossProfileTypeWhichDoesNotSpecifyConnector type = + ProfileTestCrossProfileTypeWhichDoesNotSpecifyConnector.create(crossProfileConnector); + + assertThrows(IllegalStateException.class, () -> type.suppliers().identityStringMethod(STRING)); + } + + @Test + public void personal_synchronous_runningOnWork_throwsException_exceptionIsWrapped() + throws UnavailableProfileException { + testUtilities.setRunningOnWorkProfile(); + + try { + profileTestCrossProfileType.personal().methodWhichThrowsRuntimeException(); + fail(); + } catch (ProfileRuntimeException expected) { + assertThat(expected).hasCauseThat().isInstanceOf(CustomRuntimeException.class); + } + } + + @Test + public void work_synchronous_runningOnPersonal_throwsException_exceptionIsWrapped() + throws UnavailableProfileException { + testUtilities.setRunningOnPersonalProfile(); + + try { + profileTestCrossProfileType.work().methodWhichThrowsRuntimeException(); + fail(); + } catch (ProfileRuntimeException expected) { + assertThat(expected).hasCauseThat().isInstanceOf(CustomRuntimeException.class); + } + } + + @Test + public void primary_synchronous_runningOnSecondaryProfile_throwsException_exceptionIsWrapped() + throws UnavailableProfileException { + testUtilities.setRunningOnPersonalProfile(); // Work profile is primary for TestProfileConnector + + try { + profileTestCrossProfileType.primary().methodWhichThrowsRuntimeException(); + fail(); + } catch (ProfileRuntimeException expected) { + assertThat(expected).hasCauseThat().isInstanceOf(CustomRuntimeException.class); + } + } + + @Test + public void secondary_synchronous_runningOnPrimaryProfile_throwsException_exceptionIsWrapped() + throws UnavailableProfileException { + testUtilities.setRunningOnWorkProfile(); // Work profile is primary for TestProfileConnector + + try { + profileTestCrossProfileType.secondary().methodWhichThrowsRuntimeException(); + fail(); + } catch (ProfileRuntimeException expected) { + assertThat(expected).hasCauseThat().isInstanceOf(CustomRuntimeException.class); + } + } + + @Test + public void personal_synchronous_runningOnPersonal_throwsException_exceptionIsNotWrapped() { + testUtilities.setRunningOnPersonalProfile(); + + assertThrows( + CustomRuntimeException.class, + () -> profileTestCrossProfileType.personal().methodWhichThrowsRuntimeException()); + } + + @Test + public void work_synchronous_runningOnWork_throwsException_exceptionIsNotWrapped() { + testUtilities.setRunningOnWorkProfile(); + + assertThrows( + CustomRuntimeException.class, + () -> profileTestCrossProfileType.work().methodWhichThrowsRuntimeException()); + } + + @Test + public void primary_synchronous_runningOnPrimaryProfile_throwsException_exceptionIsNotWrapped() { + testUtilities.setRunningOnWorkProfile(); // Work profile is primary for TestProfileConnector + + assertThrows( + CustomRuntimeException.class, + () -> profileTestCrossProfileType.primary().methodWhichThrowsRuntimeException()); + } + + @Test + public void + secondary_synchronous_runningOnSecondaryProfile_throwsException_exceptionIsNotWrapped() { + testUtilities.setRunningOnPersonalProfile(); // Work profile is primary for TestProfileConnector + + assertThrows( + CustomRuntimeException.class, + () -> profileTestCrossProfileType.secondary().methodWhichThrowsRuntimeException()); + } + + @Test + public void personal_async_runningOnWork_throwsException_exceptionIsWrapped() { + testUtilities.setRunningOnWorkProfile(); + + try { + profileTestCrossProfileType + .personal() + .asyncStringMethodWhichThrowsRuntimeException( + stringCallbackListener, exceptionCallbackListener); + fail(); + } catch (ProfileRuntimeException expected) { + assertThat(expected).hasCauseThat().isInstanceOf(CustomRuntimeException.class); + } + } + + @Test + public void work_async_runningOnPersonal_throwsException_exceptionIsWrapped() { + testUtilities.setRunningOnPersonalProfile(); + + try { + profileTestCrossProfileType + .work() + .asyncStringMethodWhichThrowsRuntimeException( + stringCallbackListener, exceptionCallbackListener); + fail(); + } catch (ProfileRuntimeException expected) { + assertThat(expected).hasCauseThat().isInstanceOf(CustomRuntimeException.class); + } + } + + @Test + public void primary_async_runningOnSecondaryProfile_throwsException_exceptionIsWrapped() { + testUtilities.setRunningOnPersonalProfile(); // Work profile is primary for TestProfileConnector + + try { + profileTestCrossProfileType + .primary() + .asyncStringMethodWhichThrowsRuntimeException( + stringCallbackListener, exceptionCallbackListener); + fail(); + } catch (ProfileRuntimeException expected) { + assertThat(expected).hasCauseThat().isInstanceOf(CustomRuntimeException.class); + } + } + + @Test + public void secondary_async_runningOnPrimaryProfile_throwsException_exceptionIsWrapped() { + testUtilities.setRunningOnWorkProfile(); // Work profile is primary for TestProfileConnector + + try { + profileTestCrossProfileType + .secondary() + .asyncStringMethodWhichThrowsRuntimeException( + stringCallbackListener, exceptionCallbackListener); + fail(); + } catch (ProfileRuntimeException expected) { + assertThat(expected).hasCauseThat().isInstanceOf(CustomRuntimeException.class); + } + } + + @Test + public void personal_async_runningOnPersonal_throwsException_exceptionIsNotWrapped() { + testUtilities.setRunningOnPersonalProfile(); + + assertThrows( + CustomRuntimeException.class, + () -> + profileTestCrossProfileType + .personal() + .asyncStringMethodWhichThrowsRuntimeException( + stringCallbackListener, exceptionCallbackListener)); + } + + @Test + public void work_async_runningOnWork_throwsException_exceptionIsNotWrapped() { + testUtilities.setRunningOnWorkProfile(); + + assertThrows( + CustomRuntimeException.class, + () -> + profileTestCrossProfileType + .work() + .asyncStringMethodWhichThrowsRuntimeException( + stringCallbackListener, exceptionCallbackListener)); + } + + @Test + public void primary_async_runningOnPrimaryProfile_throwsException_exceptionIsNotWrapped() { + testUtilities.setRunningOnWorkProfile(); // Work profile is primary for TestProfileConnector + + assertThrows( + CustomRuntimeException.class, + () -> + profileTestCrossProfileType + .primary() + .asyncStringMethodWhichThrowsRuntimeException( + stringCallbackListener, exceptionCallbackListener)); + } + + @Test + public void secondary_async_runningOnSecondaryProfile_throwsException_exceptionIsNotWrapped() { + testUtilities.setRunningOnPersonalProfile(); // Work profile is primary for TestProfileConnector + + assertThrows( + CustomRuntimeException.class, + () -> + profileTestCrossProfileType + .secondary() + .asyncStringMethodWhichThrowsRuntimeException( + stringCallbackListener, exceptionCallbackListener)); + } + + @Test + public void personal_future_runningOnWork_throwsException_wrapsInProfileRuntimeException() { + testUtilities.setRunningOnWorkProfile(); + + try { + profileTestCrossProfileType + .personal() + .listenableFutureVoidMethodWhichThrowsRuntimeException(); + fail(); + } catch (ProfileRuntimeException expected) { + assertThat(expected).hasCauseThat().isInstanceOf(CustomRuntimeException.class); + } + } + + @Test + public void work_future_runningOnPersonal_throwsException_exceptionIsWrapped() { + testUtilities.setRunningOnPersonalProfile(); + + try { + profileTestCrossProfileType.work().listenableFutureVoidMethodWhichThrowsRuntimeException(); + fail(); + } catch (ProfileRuntimeException expected) { + assertThat(expected).hasCauseThat().isInstanceOf(CustomRuntimeException.class); + } + } + + @Test + public void primary_future_runningOnSecondaryProfile_throwsException_exceptionIsWrapped() { + testUtilities.setRunningOnPersonalProfile(); // Work profile is primary for TestProfileConnector + + try { + profileTestCrossProfileType.primary().listenableFutureVoidMethodWhichThrowsRuntimeException(); + fail(); + } catch (ProfileRuntimeException expected) { + assertThat(expected).hasCauseThat().isInstanceOf(CustomRuntimeException.class); + } + } + + @Test + public void secondary_future_runningOnPrimaryProfile_throwsException_exceptionIsWrapped() { + testUtilities.setRunningOnWorkProfile(); // Work profile is primary for TestProfileConnector + + try { + profileTestCrossProfileType + .secondary() + .listenableFutureVoidMethodWhichThrowsRuntimeException(); + fail(); + } catch (ProfileRuntimeException expected) { + assertThat(expected).hasCauseThat().isInstanceOf(CustomRuntimeException.class); + } + } + + @Test + public void personal_future_runningOnPersonal_throwsException_exceptionIsNotWrapped() { + testUtilities.setRunningOnPersonalProfile(); + + assertThrows( + CustomRuntimeException.class, + () -> + profileTestCrossProfileType + .personal() + .listenableFutureVoidMethodWhichThrowsRuntimeException()); + } + + @Test + public void work_future_runningOnWork_throwsException_exceptionIsNotWrapped() { + testUtilities.setRunningOnWorkProfile(); + + assertThrows( + CustomRuntimeException.class, + () -> + profileTestCrossProfileType + .work() + .listenableFutureVoidMethodWhichThrowsRuntimeException()); + } + + @Test + public void primary_future_runningOnPrimaryProfile_throwsException_exceptionIsNotWrapped() { + testUtilities.setRunningOnWorkProfile(); // Work profile is primary for TestProfileConnector + + assertThrows( + CustomRuntimeException.class, + () -> + profileTestCrossProfileType + .primary() + .listenableFutureVoidMethodWhichThrowsRuntimeException()); + } + + @Test + public void secondary_future_runningOnSecondaryProfile_throwsException_exceptionIsNotWrapped() { + testUtilities.setRunningOnPersonalProfile(); // Work profile is primary for TestProfileConnector + + assertThrows( + CustomRuntimeException.class, + () -> + profileTestCrossProfileType + .secondary() + .listenableFutureVoidMethodWhichThrowsRuntimeException()); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/CrossProfileTypeUnsupportedTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/CrossProfileTypeUnsupportedTest.java new file mode 100644 index 0000000..b21524f --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/CrossProfileTypeUnsupportedTest.java @@ -0,0 +1,301 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.robotests; + +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.assertFutureHasException; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import android.app.Application; +import android.os.Build.VERSION_CODES; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.TestExceptionCallbackListener; +import com.google.android.enterprise.connectedapps.TestStringCallbackListenerImpl; +import com.google.android.enterprise.connectedapps.TestVoidCallbackListenerMultiImpl; +import com.google.android.enterprise.connectedapps.exceptions.UnavailableProfileException; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileType; +import com.google.android.enterprise.connectedapps.testapp.types.TestCrossProfileType; +import com.google.common.util.concurrent.ListenableFuture; +import java.util.concurrent.ExecutionException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** Tests for using cross-profile types on unsupported Android versions. */ +@RunWith(RobolectricTestRunner.class) +@Config(maxSdk = VERSION_CODES.N_MR1) +public class CrossProfileTypeUnsupportedTest { + + private static final String STRING = "String"; + + private final Application context = ApplicationProvider.getApplicationContext(); + private final TestProfileConnector testProfileConnector = TestProfileConnector.create(context); + private final ProfileTestCrossProfileType profileTestCrossProfileType = + ProfileTestCrossProfileType.create(testProfileConnector); + private final TestStringCallbackListenerImpl testStringCallbackListener = + new TestStringCallbackListenerImpl(); + private final TestVoidCallbackListenerMultiImpl testVoidCallbackListenerMulti = + new TestVoidCallbackListenerMultiImpl(); + private final TestExceptionCallbackListener testExceptionCallbackListener = + new TestExceptionCallbackListener(); + + @Test + public void current_synchronous_works() { + assertThat(profileTestCrossProfileType.current().identityStringMethod(STRING)) + .isEqualTo(STRING); + } + + @Test + public void current_async_works() { + profileTestCrossProfileType + .current() + .asyncIdentityStringMethod(STRING, testStringCallbackListener); + + assertThat(testStringCallbackListener.stringCallbackValue).isEqualTo(STRING); + } + + @Test + public void current_future_works() throws ExecutionException, InterruptedException { + assertThat( + profileTestCrossProfileType + .current() + .listenableFutureIdentityStringMethod(STRING) + .get()) + .isEqualTo(STRING); + } + + @Test + public void other_synchronous_throwsUnavailableProfileException() { + assertThrows( + UnavailableProfileException.class, + () -> profileTestCrossProfileType.other().identityStringMethod(STRING)); + } + + @Test + public void other_async_passesUnavailableProfileException() { + profileTestCrossProfileType + .other() + .asyncIdentityStringMethod( + STRING, testStringCallbackListener, testExceptionCallbackListener); + + assertThat(testExceptionCallbackListener.lastException) + .isInstanceOf(UnavailableProfileException.class); + } + + @Test + public void other_future_passesUnavailableProfileException() { + ListenableFuture<String> future = + profileTestCrossProfileType.other().listenableFutureIdentityStringMethod(STRING); + + assertFutureHasException(future, UnavailableProfileException.class); + } + + @Test + public void work_synchronous_throwsUnavailableProfileException() { + assertThrows( + UnavailableProfileException.class, + () -> profileTestCrossProfileType.work().identityStringMethod(STRING)); + } + + @Test + public void work_async_passesUnavailableProfileException() { + profileTestCrossProfileType + .work() + .asyncIdentityStringMethod( + STRING, testStringCallbackListener, testExceptionCallbackListener); + + assertThat(testExceptionCallbackListener.lastException) + .isInstanceOf(UnavailableProfileException.class); + } + + @Test + public void work_future_passesUnavailableProfileException() { + ListenableFuture<String> future = + profileTestCrossProfileType.work().listenableFutureIdentityStringMethod(STRING); + + assertFutureHasException(future, UnavailableProfileException.class); + } + + @Test + public void personal_synchronous_throwsUnavailableProfileException() { + assertThrows( + UnavailableProfileException.class, + () -> profileTestCrossProfileType.personal().identityStringMethod(STRING)); + } + + @Test + public void personal_async_passesUnavailableProfileException() { + profileTestCrossProfileType + .personal() + .asyncIdentityStringMethod( + STRING, testStringCallbackListener, testExceptionCallbackListener); + + assertThat(testExceptionCallbackListener.lastException) + .isInstanceOf(UnavailableProfileException.class); + } + + @Test + public void personal_future_passesUnavailableProfileException() { + ListenableFuture<String> future = + profileTestCrossProfileType.personal().listenableFutureIdentityStringMethod(STRING); + + assertFutureHasException(future, UnavailableProfileException.class); + } + + @Test + public void primary_synchronous_throwsUnavailableProfileException() { + assertThrows( + UnavailableProfileException.class, + () -> profileTestCrossProfileType.primary().identityStringMethod(STRING)); + } + + @Test + public void primary_async_passesUnavailableProfileException() { + profileTestCrossProfileType + .primary() + .asyncIdentityStringMethod( + STRING, testStringCallbackListener, testExceptionCallbackListener); + + assertThat(testExceptionCallbackListener.lastException) + .isInstanceOf(UnavailableProfileException.class); + } + + @Test + public void primary_future_passesUnavailableProfileException() { + ListenableFuture<String> future = + profileTestCrossProfileType.primary().listenableFutureIdentityStringMethod(STRING); + + assertFutureHasException(future, UnavailableProfileException.class); + } + + @Test + public void secondary_synchronous_throwsUnavailableProfileException() { + assertThrows( + UnavailableProfileException.class, + () -> profileTestCrossProfileType.secondary().identityStringMethod(STRING)); + } + + @Test + public void secondary_async_passesUnavailableProfileException() { + profileTestCrossProfileType + .secondary() + .asyncIdentityStringMethod( + STRING, testStringCallbackListener, testExceptionCallbackListener); + + assertThat(testExceptionCallbackListener.lastException) + .isInstanceOf(UnavailableProfileException.class); + } + + @Test + public void secondary_future_passesUnavailableProfileException() { + ListenableFuture<String> future = + profileTestCrossProfileType.secondary().listenableFutureIdentityStringMethod(STRING); + + assertFutureHasException(future, UnavailableProfileException.class); + } + + @Test + public void profiles_synchronous_callsNothing() { + TestCrossProfileType.voidMethodCalls = 0; + + profileTestCrossProfileType + .profiles(testProfileConnector.utils().getCurrentProfile()) + .voidMethod(); + + assertThat(TestCrossProfileType.voidMethodCalls).isEqualTo(0); + } + + @Test + public void profiles_async_callsNothing() { + TestCrossProfileType.voidMethodCalls = 0; + + profileTestCrossProfileType + .profiles(testProfileConnector.utils().getCurrentProfile()) + .asyncVoidMethod(testVoidCallbackListenerMulti); + + assertThat(TestCrossProfileType.voidMethodCalls).isEqualTo(0); + } + + @Test + public void profiles_future_callsNothing() throws ExecutionException, InterruptedException { + TestCrossProfileType.voidMethodCalls = 0; + + profileTestCrossProfileType + .profiles(testProfileConnector.utils().getCurrentProfile()) + .listenableFutureVoidMethod() + .get(); + + assertThat(TestCrossProfileType.voidMethodCalls).isEqualTo(0); + } + + @Test + public void both_synchronous_callsCurrentProfileOnce() { + TestCrossProfileType.voidMethodCalls = 0; + + profileTestCrossProfileType.both().voidMethod(); + + assertThat(TestCrossProfileType.voidMethodCalls).isEqualTo(1); + } + + @Test + public void both_async_callsCurrentProfileOnce() { + TestCrossProfileType.voidMethodCalls = 0; + + profileTestCrossProfileType.both().asyncVoidMethod(testVoidCallbackListenerMulti); + + assertThat(TestCrossProfileType.voidMethodCalls).isEqualTo(1); + } + + @Test + public void both_future_callsCurrentProfileOnce() + throws ExecutionException, InterruptedException { + TestCrossProfileType.voidMethodCalls = 0; + + profileTestCrossProfileType.both().listenableFutureVoidMethod().get(); + + assertThat(TestCrossProfileType.voidMethodCalls).isEqualTo(1); + } + + @Test + public void suppliers_synchronous_callsCurrentProfileOnce() { + TestCrossProfileType.voidMethodCalls = 0; + + profileTestCrossProfileType.suppliers().voidMethod(); + + assertThat(TestCrossProfileType.voidMethodCalls).isEqualTo(1); + } + + @Test + public void suppliers_async_callsCurrentProfileOnce() { + TestCrossProfileType.voidMethodCalls = 0; + + profileTestCrossProfileType.suppliers().asyncVoidMethod(testVoidCallbackListenerMulti); + + assertThat(TestCrossProfileType.voidMethodCalls).isEqualTo(1); + } + + @Test + public void suppliers_future_callsCurrentProfileOnce() + throws ExecutionException, InterruptedException { + TestCrossProfileType.voidMethodCalls = 0; + + profileTestCrossProfileType.suppliers().listenableFutureVoidMethod().get(); + + assertThat(TestCrossProfileType.voidMethodCalls).isEqualTo(1); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/CrossUserTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/CrossUserTest.java new file mode 100644 index 0000000..10b7d1f --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/CrossUserTest.java @@ -0,0 +1,81 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.robotests; + +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.INTERACT_ACROSS_USERS; +import static com.google.common.truth.Truth.assertThat; + +import android.app.Application; +import android.app.Service; +import android.os.Build.VERSION_CODES; +import android.os.IBinder; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.RobolectricTestUtilities; +import com.google.android.enterprise.connectedapps.TestScheduledExecutorService; +import com.google.android.enterprise.connectedapps.testapp.crossuser.ProfileTestCrossUserType; +import com.google.android.enterprise.connectedapps.testapp.crossuser.TestCrossUserConfiguration; +import com.google.android.enterprise.connectedapps.testapp.crossuser.TestCrossUserConnector; +import com.google.android.enterprise.connectedapps.testapp.crossuser.TestCrossUserStringCallbackListenerImpl; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.O) +public class CrossUserTest { + + private static final String STRING = "String"; + + private final Application context = ApplicationProvider.getApplicationContext(); + private final TestScheduledExecutorService scheduledExecutorService = + new TestScheduledExecutorService(); + private final TestCrossUserConnector testCrossUserConnector = + TestCrossUserConnector.create(context, scheduledExecutorService); + private final RobolectricTestUtilities testUtilities = + new RobolectricTestUtilities(testCrossUserConnector, scheduledExecutorService); + private final ProfileTestCrossUserType profileCrossUserType = + ProfileTestCrossUserType.create(testCrossUserConnector); + private final TestCrossUserStringCallbackListenerImpl crossUserStringCallback = + new TestCrossUserStringCallbackListenerImpl(); + + @Before + public void setUp() { + Service profileAwareService = Robolectric.setupService(TestCrossUserConfiguration.getService()); + testUtilities.initTests(); + IBinder binder = profileAwareService.onBind(/* intent= */ null); + testUtilities.setBinding(binder, TestCrossUserConnector.class.getName()); + testUtilities.createWorkUser(); + testUtilities.turnOnWorkProfile(); + testUtilities.setRunningOnPersonalProfile(); + testUtilities.setRequestsPermissions(INTERACT_ACROSS_USERS); + testUtilities.grantPermissions(INTERACT_ACROSS_USERS); + testCrossUserConnector.stopManualConnectionManagement(); + } + + @Test + // This test covers all CrossUser annotations + public void passArgumentToCallback_works() { + testUtilities.turnOnWorkProfile(); + testUtilities.startConnectingAndWait(); + + profileCrossUserType.current().passString(STRING, crossUserStringCallback); + + assertThat(crossUserStringCallback.stringCallbackValue).isEqualTo(STRING); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/CurrentProfileTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/CurrentProfileTest.java new file mode 100644 index 0000000..7693a54 --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/CurrentProfileTest.java @@ -0,0 +1,413 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.robotests; + +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.INTERACT_ACROSS_USERS; +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.assertFutureDoesNotHaveException; +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.assertFutureHasException; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import android.app.Application; +import android.app.Service; +import android.os.Build.VERSION_CODES; +import android.os.IBinder; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.CrossProfileConnector; +import com.google.android.enterprise.connectedapps.NonSimpleCallbackListenerImpl; +import com.google.android.enterprise.connectedapps.RobolectricTestUtilities; +import com.google.android.enterprise.connectedapps.TestBooleanCallbackListenerImpl; +import com.google.android.enterprise.connectedapps.TestNotReallySerializableObjectCallbackListenerImpl; +import com.google.android.enterprise.connectedapps.TestScheduledExecutorService; +import com.google.android.enterprise.connectedapps.TestStringCallbackListenerImpl; +import com.google.android.enterprise.connectedapps.TestVoidCallbackListenerImpl; +import com.google.android.enterprise.connectedapps.exceptions.UnavailableProfileException; +import com.google.android.enterprise.connectedapps.testapp.CustomRuntimeException; +import com.google.android.enterprise.connectedapps.testapp.NotReallySerializableObject; +import com.google.android.enterprise.connectedapps.testapp.ParcelableObject; +import com.google.android.enterprise.connectedapps.testapp.configuration.TestApplication; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileType; +import com.google.android.enterprise.connectedapps.testapp.types.TestCrossProfileType; +import com.google.common.util.concurrent.ListenableFuture; +import java.io.IOException; +import java.sql.SQLException; +import java.util.concurrent.ExecutionException; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.O) +public class CurrentProfileTest { + + private static final String STRING = "String"; + private static final String STRING2 = "String2"; + private static final ParcelableObject PARCELABLE_OBJECT = new ParcelableObject(""); + private static final NotReallySerializableObject NOT_REALLY_SERIALIZABLE_OBJECT = + new NotReallySerializableObject(PARCELABLE_OBJECT); + + private final Application context = ApplicationProvider.getApplicationContext(); + private final TestStringCallbackListenerImpl stringCallbackListener = + new TestStringCallbackListenerImpl(); + private final TestBooleanCallbackListenerImpl booleanCallbackListener = + new TestBooleanCallbackListenerImpl(); + private final TestVoidCallbackListenerImpl voidCallbackListener = + new TestVoidCallbackListenerImpl(); + private final TestScheduledExecutorService scheduledExecutorService = + new TestScheduledExecutorService(); + private final TestProfileConnector testProfileConnector = + TestProfileConnector.create(context, scheduledExecutorService); + private final RobolectricTestUtilities testUtilities = + new RobolectricTestUtilities(testProfileConnector, scheduledExecutorService); + private final ProfileTestCrossProfileType profileTestCrossProfileType = + ProfileTestCrossProfileType.create(testProfileConnector); + private final TestNotReallySerializableObjectCallbackListenerImpl + notReallySerializableObjectCallbackListener = + new TestNotReallySerializableObjectCallbackListenerImpl(); + private final NonSimpleCallbackListenerImpl nonSimpleCallbackListener = + new NonSimpleCallbackListenerImpl(); + + @Before + public void setUp() { + Service profileAwareService = Robolectric.setupService(TestApplication.getService()); + testUtilities.initTests(); + IBinder binder = profileAwareService.onBind(/* intent= */ null); + testUtilities.setBinding(binder, CrossProfileConnector.class.getName()); + testUtilities.createWorkUser(); + testUtilities.turnOnWorkProfile(); + testUtilities.setRunningOnPersonalProfile(); + testUtilities.setRequestsPermissions(INTERACT_ACROSS_USERS); + testUtilities.grantPermissions(INTERACT_ACROSS_USERS); + testUtilities.startConnectingAndWait(); + } + + @Test + public void current_isBound_callsMethod() throws UnavailableProfileException { + testUtilities.turnOnWorkProfile(); + testUtilities.startConnectingAndWait(); + + assertThat(profileTestCrossProfileType.current().identityStringMethod(STRING)) + .isEqualTo(STRING); + } + + @Test + public void current_isNotBound_callsMethod() { + testUtilities.turnOffWorkProfile(); + + assertThat(profileTestCrossProfileType.current().identityStringMethod(STRING)) + .isEqualTo(STRING); + } + + @Test + public void current_async_isBound_callsMethod() { + testUtilities.turnOnWorkProfile(); + testUtilities.startConnectingAndWait(); + + profileTestCrossProfileType.current().asyncVoidMethod(voidCallbackListener); + + assertThat(TestCrossProfileType.voidMethodCalls).isEqualTo(1); + } + + @Test + public void current_synchronous_isBound_automaticConnectionManagement_callsMethod() { + testUtilities.turnOnWorkProfile(); + testProfileConnector.stopManualConnectionManagement(); + ListenableFuture<Void> ignored = + profileTestCrossProfileType.other().listenableFutureVoidMethod(); // Causes it to bind + + assertThat(profileTestCrossProfileType.current().identityStringMethod(STRING)) + .isEqualTo(STRING); + } + + @Test + public void current_async_isNotBound_callsMethod() { + testUtilities.turnOffWorkProfile(); + + profileTestCrossProfileType.current().asyncVoidMethod(voidCallbackListener); + + assertThat(TestCrossProfileType.voidMethodCalls).isEqualTo(1); + } + + @Test // This behaviour is expected right now but will change + public void current_async_blockingMethod_blocks() { + profileTestCrossProfileType + .current() + .asyncVoidMethodWithDelay(voidCallbackListener, /* secondsDelay= */ 5); + + assertThat(voidCallbackListener.callbackMethodCalls).isEqualTo(1); + } + + @Test + public void current_async_firesCallback() { + profileTestCrossProfileType.current().asyncVoidMethod(voidCallbackListener); + + assertThat(voidCallbackListener.callbackMethodCalls).isEqualTo(1); + } + + @Test + public void current_async_passesParametersCorrectly() { + profileTestCrossProfileType.current().asyncIdentityStringMethod(STRING, stringCallbackListener); + + assertThat(stringCallbackListener.stringCallbackValue).isEqualTo(STRING); + } + + @Test + public void current_async_nonblockingMethod_doesNotBlock() { + profileTestCrossProfileType + .current() + .asyncVoidMethodWithNonBlockingDelay(voidCallbackListener, /* secondsDelay= */ 5); + + assertThat(voidCallbackListener.callbackMethodCalls).isEqualTo(0); + } + + @Test + public void current_async_nonblockingMethod_doesCallback() { + profileTestCrossProfileType + .current() + .asyncVoidMethodWithNonBlockingDelay(voidCallbackListener, /* secondsDelay= */ 5); + testUtilities.advanceTimeBySeconds(10); + + assertThat(voidCallbackListener.callbackMethodCalls).isEqualTo(1); + } + + @Test + public void current_listenableFuture_isBound_callsMethod() + throws ExecutionException, InterruptedException { + testUtilities.turnOnWorkProfile(); + testUtilities.startConnectingAndWait(); + + profileTestCrossProfileType.current().listenableFutureVoidMethod().get(); + + assertThat(TestCrossProfileType.voidMethodCalls).isEqualTo(1); + } + + @Test + public void current_listenableFuture_isNotBound_callsMethod() + throws ExecutionException, InterruptedException { + testUtilities.turnOffWorkProfile(); + + profileTestCrossProfileType.current().listenableFutureVoidMethod().get(); + + assertThat(TestCrossProfileType.voidMethodCalls).isEqualTo(1); + } + + @Test // This behaviour is expected right now but will change + public void current_listenableFuture_blockingMethod_blocks() { + ListenableFuture<Void> voidFuture = + profileTestCrossProfileType + .current() + .listenableFutureVoidMethodWithDelay(/* secondsDelay= */ 5); + + assertThat(voidFuture.isDone()).isTrue(); + } + + @Test + public void current_listenableFuture_setsFuture() + throws ExecutionException, InterruptedException { + + // This would throw an exception if it wasn't set + assertThat(profileTestCrossProfileType.current().listenableFutureVoidMethod().get()).isNull(); + } + + @Test + public void current_listenableFuture_setsException_isSet() { + assertFutureHasException( + profileTestCrossProfileType + .current() + .listenableFutureVoidMethodWhichSetsIllegalStateException(), + IllegalStateException.class); + } + + @Test + public void current_listenableFuture_passesParametersCorrectly() + throws ExecutionException, InterruptedException { + ListenableFuture<String> future = + profileTestCrossProfileType.current().listenableFutureIdentityStringMethod(STRING); + + assertThat(future.get()).isEqualTo(STRING); + } + + @Test + public void current_listenableFuture_nonblockingMethod_doesNotBlock() { + ListenableFuture<Void> future = + profileTestCrossProfileType + .current() + .listenableFutureVoidMethodWithNonBlockingDelay(/* secondsDelay= */ 5); + + assertThat(future.isDone()).isFalse(); + } + + @Test + public void current_listenableFuture_nonblockingMethod_doesCallback() { + ListenableFuture<Void> future = + profileTestCrossProfileType + .current() + .listenableFutureVoidMethodWithNonBlockingDelay(/* secondsDelay= */ 5); + testUtilities.advanceTimeBySeconds(10); + + assertThat(future.isDone()).isTrue(); + } + + @Test + public void current_listenableFuture_doesNotTimeout() { + ListenableFuture<Void> future = + profileTestCrossProfileType + .current() + .listenableFutureMethodWhichNeverSetsTheValueWith5SecondTimeout(); + testUtilities.advanceTimeBySeconds(10); + + assertFutureDoesNotHaveException(future, UnavailableProfileException.class); + } + + @Test + public void current_async_doesNotTimeout() { + profileTestCrossProfileType + .current() + .asyncMethodWhichNeverCallsBackWith5SecondTimeout(stringCallbackListener); + testUtilities.advanceTimeBySeconds(10); + + assertThat(stringCallbackListener.callbackMethodCalls).isEqualTo(0); + } + + @Test + public void current_serializableObjectIsNotReallySerializable_works() { + assertThat( + profileTestCrossProfileType.current() + .identityNotReallySerializableObjectMethod(NOT_REALLY_SERIALIZABLE_OBJECT)) + .isEqualTo(NOT_REALLY_SERIALIZABLE_OBJECT); + } + + @Test + public void current_async_serializableObjectIsNotReallySerializable_works() { + profileTestCrossProfileType.current() + .asyncGetNotReallySerializableObjectMethod(notReallySerializableObjectCallbackListener); + + assertThat(notReallySerializableObjectCallbackListener.notReallySerializableObjectCallbackValue) + .isNotNull(); + } + + @Test + public void current_future_serializableObjectIsNotReallySerializable_works() + throws ExecutionException, InterruptedException { + ListenableFuture<NotReallySerializableObject> future = + profileTestCrossProfileType.current().futureGetNotReallySerializableObjectMethod(); + + assertThat(future.get()).isNotNull(); + } + + @Test + public void current_synchronous_throwsException_throwsOriginalException() { + assertThrows( + CustomRuntimeException.class, + () -> profileTestCrossProfileType.current().methodWhichThrowsRuntimeException()); + } + + @Test + public void current_async_throwsException_throwsOriginalException() { + assertThrows( + CustomRuntimeException.class, + () -> + profileTestCrossProfileType + .current() + .asyncStringMethodWhichThrowsRuntimeException(stringCallbackListener)); + } + + @Test + public void current_future_throwsException_throwsOriginalException() { + assertThrows( + CustomRuntimeException.class, + () -> + profileTestCrossProfileType + .current() + .listenableFutureVoidMethodWhichThrowsRuntimeException()); + } + + @Test + public void current_synchronous_contextArgument_works() { + assertThat(profileTestCrossProfileType.current().isContextArgumentPassed()).isTrue(); + } + + @Test + public void current_async_contextArgument_works() { + profileTestCrossProfileType.current().asyncIsContextArgumentPassed(booleanCallbackListener); + + assertThat(booleanCallbackListener.booleanCallbackValue).isTrue(); + } + + @Test + public void current_future_contextArgument_works() throws Exception { + ListenableFuture<Boolean> result = + profileTestCrossProfileType.current().futureIsContextArgumentPassed(); + + assertThat(result.get()).isTrue(); + } + + @Test + public void current_synchronous_declaresButDoesNotThrowException_works() throws Exception { + assertThat( + profileTestCrossProfileType + .current() + .identityStringMethodDeclaresButDoesNotThrowIOException(STRING)) + .isEqualTo(STRING); + } + + @Test + public void current_synchronous_throwsException_works() { + assertThrows( + IOException.class, + () -> + profileTestCrossProfileType + .current() + .identityStringMethodThrowsIOException(STRING)); + } + + @Test + public void current_synchronous_declaresMultipleExceptions_throwsException_works() { + assertThrows( + SQLException.class, + () -> + profileTestCrossProfileType + .current() + .identityStringMethodDeclaresIOExceptionThrowsSQLException(STRING)); + } + + @Test + public void current_async_nonSimpleCallback_works() { + nonSimpleCallbackListener.callbackMethodCalls = 0; + profileTestCrossProfileType + .current() + .asyncMethodWithNonSimpleCallback(nonSimpleCallbackListener, STRING, STRING2); + + assertThat(nonSimpleCallbackListener.callbackMethodCalls).isEqualTo(1); + assertThat(nonSimpleCallbackListener.string1CallbackValue).isEqualTo(STRING); + assertThat(nonSimpleCallbackListener.string2CallbackValue).isEqualTo(STRING2); + } + + @Test + public void current_async_nonSimpleCallback_secondMethod_works() { + profileTestCrossProfileType + .current() + .asyncMethodWithNonSimpleCallbackCallsSecondMethod( + nonSimpleCallbackListener, STRING, STRING2); + + assertThat(nonSimpleCallbackListener.string3CallbackValue).isEqualTo(STRING); + assertThat(nonSimpleCallbackListener.string4CallbackValue).isEqualTo(STRING2); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/IfAvailableTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/IfAvailableTest.java new file mode 100644 index 0000000..6106125 --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/IfAvailableTest.java @@ -0,0 +1,187 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.robotests; + +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.INTERACT_ACROSS_USERS; +import static com.google.common.truth.Truth.assertThat; +import static org.robolectric.Shadows.shadowOf; + +import android.app.Application; +import android.app.Service; +import android.os.Build.VERSION_CODES; +import android.os.IBinder; +import android.os.Looper; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.RobolectricTestUtilities; +import com.google.android.enterprise.connectedapps.TestScheduledExecutorService; +import com.google.android.enterprise.connectedapps.TestStringCallbackListenerImpl; +import com.google.android.enterprise.connectedapps.TestVoidCallbackListenerImpl; +import com.google.android.enterprise.connectedapps.testapp.configuration.TestApplication; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileType; +import com.google.common.util.concurrent.ListenableFuture; +import java.util.concurrent.ExecutionException; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** Tests for _IfAvailable classes */ +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.O) +public class IfAvailableTest { + + private static final String STRING1 = "String1"; + private static final String STRING2 = "String2"; + + private final Application context = ApplicationProvider.getApplicationContext(); + private final TestScheduledExecutorService scheduledExecutorService = + new TestScheduledExecutorService(); + private final TestProfileConnector testProfileConnector = + TestProfileConnector.create(context, scheduledExecutorService); + private final RobolectricTestUtilities testUtilities = + new RobolectricTestUtilities(testProfileConnector, scheduledExecutorService); + private final TestStringCallbackListenerImpl stringCallbackListener = + new TestStringCallbackListenerImpl(); + private final TestVoidCallbackListenerImpl voidCallbackListener = + new TestVoidCallbackListenerImpl(); + + private final ProfileTestCrossProfileType profileTestCrossProfileType = + ProfileTestCrossProfileType.create(testProfileConnector); + + @Before + public void setUp() { + Service profileAwareService = Robolectric.setupService(TestApplication.getService()); + testUtilities.initTests(); + IBinder binder = profileAwareService.onBind(/* intent= */ null); + testUtilities.setBinding(binder, RobolectricTestUtilities.TEST_CONNECTOR_CLASS_NAME); + testUtilities.createWorkUser(); + testUtilities.turnOnWorkProfile(); + testUtilities.setRunningOnPersonalProfile(); + testUtilities.setRequestsPermissions(INTERACT_ACROSS_USERS); + testUtilities.grantPermissions(INTERACT_ACROSS_USERS); + } + + @Test + public void synchronous_notConnected_returnsDefaultValue() { + testUtilities.disconnect(); + + assertThat( + profileTestCrossProfileType + .other() + .ifAvailable() + .identityStringMethod(STRING1, /* defaultValue= */ STRING2)) + .isEqualTo(STRING2); + } + + @Test + public void synchronous_connected_makesCall() throws Exception { + testUtilities.startConnectingAndWait(); + + assertThat(profileTestCrossProfileType.other().identityStringMethod(STRING1)) + .isEqualTo(STRING1); + } + + @Test + public void synchronousVoid_notConnected_doesNotThrowException() { + testUtilities.disconnect(); + + profileTestCrossProfileType.other().ifAvailable().voidMethod(); + } + + @Test + public void synchronousVoid_connected_doesNotThrowException() { + testUtilities.startConnectingAndWait(); + + profileTestCrossProfileType.other().ifAvailable().voidMethod(); + } + + @Test + public void callback_notAvailable_returnsDefaultValue() { + testUtilities.turnOffWorkProfile(); + + profileTestCrossProfileType + .other() + .ifAvailable() + .asyncIdentityStringMethod(STRING1, stringCallbackListener, /* defaultValue= */ STRING2); + + assertThat(stringCallbackListener.stringCallbackValue).isEqualTo(STRING2); + } + + @Test + public void callback_available_makesCall() { + testUtilities.turnOnWorkProfile(); + + profileTestCrossProfileType + .other() + .ifAvailable() + .asyncIdentityStringMethod(STRING1, stringCallbackListener, /* defaultValue= */ STRING2); + shadowOf(Looper.getMainLooper()).idle(); + + assertThat(stringCallbackListener.stringCallbackValue).isEqualTo(STRING1); + } + + @Test + public void voidCallback_notAvailable_callsBack() { + testUtilities.turnOffWorkProfile(); + + profileTestCrossProfileType.other().ifAvailable().asyncVoidMethod(voidCallbackListener); + shadowOf(Looper.getMainLooper()).idle(); + + assertThat(voidCallbackListener.callbackMethodCalls).isEqualTo(1); + } + + @Test + public void voidCallback_available_callsBack() { + testUtilities.turnOnWorkProfile(); + + profileTestCrossProfileType.other().ifAvailable().asyncVoidMethod(voidCallbackListener); + shadowOf(Looper.getMainLooper()).idle(); + + assertThat(voidCallbackListener.callbackMethodCalls).isEqualTo(1); + } + + @Test + public void future_notAvailable_setsDefaultValue() + throws ExecutionException, InterruptedException { + testUtilities.turnOffWorkProfile(); + + ListenableFuture<String> future = + profileTestCrossProfileType + .other() + .ifAvailable() + .listenableFutureIdentityStringMethod(STRING1, STRING2); + shadowOf(Looper.getMainLooper()).idle(); + + assertThat(future.get()).isEqualTo(STRING2); + } + + @Test + public void future_available_setsCorrectValue() throws ExecutionException, InterruptedException { + testUtilities.turnOnWorkProfile(); + + ListenableFuture<String> future = + profileTestCrossProfileType + .other() + .ifAvailable() + .listenableFutureIdentityStringMethod(STRING1, STRING2); + shadowOf(Looper.getMainLooper()).idle(); + + assertThat(future.get()).isEqualTo(STRING1); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/ManualConnectionManagementTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/ManualConnectionManagementTest.java new file mode 100644 index 0000000..bb1845e --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/ManualConnectionManagementTest.java @@ -0,0 +1,82 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.robotests; + +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.INTERACT_ACROSS_USERS; +import static com.google.common.truth.Truth.assertThat; + +import android.app.Application; +import android.app.Service; +import android.os.Build.VERSION_CODES; +import android.os.IBinder; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.RobolectricTestUtilities; +import com.google.android.enterprise.connectedapps.TestScheduledExecutorService; +import com.google.android.enterprise.connectedapps.testapp.configuration.TestApplication; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.O) +public class ManualConnectionManagementTest { + + private final Application context = ApplicationProvider.getApplicationContext(); + private final TestScheduledExecutorService scheduledExecutorService = + new TestScheduledExecutorService(); + private final TestProfileConnector testProfileConnector = + TestProfileConnector.create(context, scheduledExecutorService); + private final RobolectricTestUtilities testUtilities = + new RobolectricTestUtilities(testProfileConnector, scheduledExecutorService); + + @Before + public void setUp() { + Service profileAwareService = Robolectric.setupService(TestApplication.getService()); + testUtilities.initTests(); + IBinder binder = profileAwareService.onBind(/* intent= */ null); + testUtilities.setBinding(binder, RobolectricTestUtilities.TEST_CONNECTOR_CLASS_NAME); + testUtilities.createWorkUser(); + testUtilities.turnOnWorkProfile(); + testUtilities.setRunningOnPersonalProfile(); + testUtilities.setRequestsPermissions(INTERACT_ACROSS_USERS); + testUtilities.grantPermissions(INTERACT_ACROSS_USERS); + } + + @Test + public void connect_doesNotHavePermission_doesNotConnect() throws Exception { + testUtilities.denyPermissions(INTERACT_ACROSS_USERS); + + testUtilities.startConnectingAndWait(); + testUtilities.advanceTimeBySeconds(60); + + assertThat(testProfileConnector.isConnected()).isFalse(); + } + + @Test + public void connect_getsPermissionAfterStartingConnecting_connects() throws Exception { + testUtilities.denyPermissions(INTERACT_ACROSS_USERS); + testUtilities.startConnectingAndWait(); + + testUtilities.grantPermissions(INTERACT_ACROSS_USERS); + testUtilities.advanceTimeBySeconds(5); + + assertThat(testProfileConnector.isConnected()).isTrue(); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/MessageSizeTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/MessageSizeTest.java new file mode 100644 index 0000000..759d0c2 --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/MessageSizeTest.java @@ -0,0 +1,130 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.robotests; + +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.INTERACT_ACROSS_USERS; +import static com.google.android.enterprise.connectedapps.StringUtilities.randomString; +import static com.google.common.truth.Truth.assertThat; + +import android.app.Application; +import android.app.Service; +import android.os.Build.VERSION_CODES; +import android.os.IBinder; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.RobolectricTestUtilities; +import com.google.android.enterprise.connectedapps.TestExceptionCallbackListener; +import com.google.android.enterprise.connectedapps.TestScheduledExecutorService; +import com.google.android.enterprise.connectedapps.TestStringCallbackListenerImpl; +import com.google.android.enterprise.connectedapps.exceptions.UnavailableProfileException; +import com.google.android.enterprise.connectedapps.testapp.configuration.TestApplication; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileType; +import java.util.concurrent.ExecutionException; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** Tests for large messages */ +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.O) +public class MessageSizeTest { + + private static final String SMALL_STRING = "String"; + private static final String LARGE_STRING = randomString(1500000); // 3Mb + + private final Application context = ApplicationProvider.getApplicationContext(); + private final TestScheduledExecutorService scheduledExecutorService = + new TestScheduledExecutorService(); + private final TestProfileConnector testProfileConnector = + TestProfileConnector.create(context, scheduledExecutorService); + private final RobolectricTestUtilities testUtilities = + new RobolectricTestUtilities(testProfileConnector, scheduledExecutorService); + + private final ProfileTestCrossProfileType profileTestCrossProfileType = + ProfileTestCrossProfileType.create(testProfileConnector); + + private final TestStringCallbackListenerImpl stringCallbackListener = + new TestStringCallbackListenerImpl(); + private final TestExceptionCallbackListener exceptionCallbackListener = + new TestExceptionCallbackListener(); + + @Before + public void setUp() { + Service profileAwareService = Robolectric.setupService(TestApplication.getService()); + testUtilities.initTests(); + IBinder binder = profileAwareService.onBind(/* intent= */ null); + testUtilities.setBinding(binder, RobolectricTestUtilities.TEST_CONNECTOR_CLASS_NAME); + testUtilities.createWorkUser(); + testUtilities.turnOnWorkProfile(); + testUtilities.setRunningOnPersonalProfile(); + testUtilities.setRequestsPermissions(INTERACT_ACROSS_USERS); + testUtilities.grantPermissions(INTERACT_ACROSS_USERS); + testUtilities.startConnectingAndWait(); + } + + @Test + public void synchronous_smallMessage_sends() throws UnavailableProfileException { + assertThat(profileTestCrossProfileType.other().identityStringMethod(SMALL_STRING)) + .isEqualTo(SMALL_STRING); + } + + @Test + public void synchronous_largeMessage_sends() throws UnavailableProfileException { + assertThat(profileTestCrossProfileType.other().identityStringMethod(LARGE_STRING)) + .isEqualTo(LARGE_STRING); + } + + @Test + public void async_smallMessage_sends() { + profileTestCrossProfileType + .other() + .asyncIdentityStringMethod(SMALL_STRING, stringCallbackListener, exceptionCallbackListener); + + assertThat(stringCallbackListener.stringCallbackValue).isEqualTo(SMALL_STRING); + } + + @Test + public void async_largeMessage_sends() { + profileTestCrossProfileType + .other() + .asyncIdentityStringMethod(LARGE_STRING, stringCallbackListener, exceptionCallbackListener); + + assertThat(stringCallbackListener.stringCallbackValue).isEqualTo(LARGE_STRING); + } + + @Test + public void future_smallMessage_sends() throws ExecutionException, InterruptedException { + assertThat( + profileTestCrossProfileType + .other() + .listenableFutureIdentityStringMethod(SMALL_STRING) + .get()) + .isEqualTo(SMALL_STRING); + } + + @Test + public void future_largeMessage_sends() throws ExecutionException, InterruptedException { + assertThat( + profileTestCrossProfileType + .other() + .listenableFutureIdentityStringMethod(LARGE_STRING) + .get()) + .isEqualTo(LARGE_STRING); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/OtherProfileAsyncTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/OtherProfileAsyncTest.java new file mode 100644 index 0000000..92cd481 --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/OtherProfileAsyncTest.java @@ -0,0 +1,342 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.robotests; + +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.INTERACT_ACROSS_USERS; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; +import static org.robolectric.annotation.LooperMode.Mode.LEGACY; + +import android.app.Application; +import android.app.Service; +import android.os.Build.VERSION_CODES; +import android.os.IBinder; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.NonSimpleCallbackListenerImpl; +import com.google.android.enterprise.connectedapps.RobolectricTestUtilities; +import com.google.android.enterprise.connectedapps.TestBooleanCallbackListenerImpl; +import com.google.android.enterprise.connectedapps.TestExceptionCallbackListener; +import com.google.android.enterprise.connectedapps.TestScheduledExecutorService; +import com.google.android.enterprise.connectedapps.TestStringCallbackListenerImpl; +import com.google.android.enterprise.connectedapps.TestVoidCallbackListenerImpl; +import com.google.android.enterprise.connectedapps.exceptions.ProfileRuntimeException; +import com.google.android.enterprise.connectedapps.exceptions.UnavailableProfileException; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.interfaces.CrossProfileAnnotation; +import com.google.android.enterprise.connectedapps.testapp.CustomRuntimeException; +import com.google.android.enterprise.connectedapps.testapp.configuration.TestApplication; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileType; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileTypeWhichNeedsContext; +import com.google.android.enterprise.connectedapps.testapp.types.TestCrossProfileType; +import java.util.concurrent.TimeUnit; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.annotation.LooperMode; + +@LooperMode(LEGACY) +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.O) +public class OtherProfileAsyncTest { + + private static final String STRING = "String"; + private static final String STRING2 = "String2"; + + private final Application context = ApplicationProvider.getApplicationContext(); + private final TestVoidCallbackListenerImpl voidCallbackListener = + new TestVoidCallbackListenerImpl(); + private final TestStringCallbackListenerImpl stringCallbackListener = + new TestStringCallbackListenerImpl(); + private final TestBooleanCallbackListenerImpl booleanCallbackListener = + new TestBooleanCallbackListenerImpl(); + private final TestExceptionCallbackListener exceptionCallbackListener = + new TestExceptionCallbackListener(); + private final TestScheduledExecutorService scheduledExecutorService = + new TestScheduledExecutorService(); + private final TestProfileConnector testProfileConnector = + TestProfileConnector.create(context, scheduledExecutorService); + private final RobolectricTestUtilities testUtilities = + new RobolectricTestUtilities(testProfileConnector, scheduledExecutorService); + private final ProfileTestCrossProfileType profileTestCrossProfileType = + ProfileTestCrossProfileType.create(testProfileConnector); + private final ProfileTestCrossProfileTypeWhichNeedsContext + profileTestCrossProfileTypeWhichNeedsContext = + ProfileTestCrossProfileTypeWhichNeedsContext.create(testProfileConnector); + private final NonSimpleCallbackListenerImpl nonSimpleCallbackListener = + new NonSimpleCallbackListenerImpl(); + + @Before + public void setUp() { + Service profileAwareService = Robolectric.setupService(TestApplication.getService()); + testUtilities.initTests(); + IBinder binder = profileAwareService.onBind(/* intent= */ null); + testUtilities.setBinding(binder, RobolectricTestUtilities.TEST_CONNECTOR_CLASS_NAME); + testUtilities.createWorkUser(); + testUtilities.turnOnWorkProfile(); + testUtilities.setRunningOnPersonalProfile(); + testUtilities.setRequestsPermissions(INTERACT_ACROSS_USERS); + testUtilities.grantPermissions(INTERACT_ACROSS_USERS); + } + + @Test + public void other_async_callbackTriggeredMultipleTimes_isOnlyReceivedOnce() { + profileTestCrossProfileType + .other() + .asyncVoidMethodWhichCallsBackTwice(voidCallbackListener, exceptionCallbackListener); + + assertThat(voidCallbackListener.callbackMethodCalls).isEqualTo(1); + } + + @Test + public void + other_async_automaticConnection_workProfileIsTurnedOff_doesReceiveUnavailableProfileExceptionImmediately() { + testProfileConnector.stopManualConnectionManagement(); + testUtilities.turnOffWorkProfile(); + + profileTestCrossProfileType + .other() + .asyncVoidMethod(voidCallbackListener, exceptionCallbackListener); + + assertThat(exceptionCallbackListener.lastException) + .isInstanceOf(UnavailableProfileException.class); + } + + @Test + public void + other_async_automaticConnection_workProfileIsTurnedOn_doesNotSetUnavailableProfileException() { + testProfileConnector.stopManualConnectionManagement(); + testUtilities.turnOnWorkProfile(); + + profileTestCrossProfileType + .other() + .asyncVoidMethodWithNonBlockingDelay( + voidCallbackListener, /* secondsDelay= */ 5, exceptionCallbackListener); + testUtilities.advanceTimeBySeconds(5); + + assertThat(exceptionCallbackListener.lastException).isNull(); + } + + @Test + public void other_async_automaticConnection_callsMethod() { + testProfileConnector.stopManualConnectionManagement(); + testUtilities.turnOnWorkProfile(); + + profileTestCrossProfileType + .other() + .asyncVoidMethod(voidCallbackListener, exceptionCallbackListener); + + assertThat(TestCrossProfileType.voidMethodCalls).isEqualTo(1); + } + + @Test + public void other_async_automaticConnection_resultIsSet() { + testProfileConnector.stopManualConnectionManagement(); + testUtilities.turnOnWorkProfile(); + + profileTestCrossProfileType + .other() + .asyncIdentityStringMethod(STRING, stringCallbackListener, exceptionCallbackListener); + + assertThat(stringCallbackListener.stringCallbackValue).isEqualTo(STRING); + } + + @Test + public void + other_async_automaticConnection_connectionIsDroppedDuringCall_setUnavailableProfileException() { + testProfileConnector.stopManualConnectionManagement(); + testUtilities.turnOnWorkProfile(); + profileTestCrossProfileType + .other() + .asyncMethodWhichNeverCallsBack(stringCallbackListener, exceptionCallbackListener); + testUtilities.advanceTimeBySeconds(5); + + testUtilities.turnOffWorkProfile(); + + assertThat(exceptionCallbackListener.lastException) + .isInstanceOf(UnavailableProfileException.class); + } + + @Test + public void other_async_timeoutSetOnMethod_doesNotTimeoutEarly() { + profileTestCrossProfileType + .other() + .asyncMethodWhichNeverCallsBackWith5SecondTimeout( + stringCallbackListener, exceptionCallbackListener); + + testUtilities.advanceTimeBySeconds(4); + + assertThat(exceptionCallbackListener.lastException).isNull(); + } + + @Test + public void other_async_timeoutSetOnMethod_timesOut() { + profileTestCrossProfileType + .other() + .asyncMethodWhichNeverCallsBackWith5SecondTimeout( + stringCallbackListener, exceptionCallbackListener); + + testUtilities.advanceTimeBySeconds(6); + + assertThat(exceptionCallbackListener.lastException) + .isInstanceOf(UnavailableProfileException.class); + } + + @Test + public void other_async_timeoutSetOnType_doesNotTimeoutEarly() { + profileTestCrossProfileType + .other() + .asyncMethodWhichNeverCallsBackWith7SecondTimeout( + stringCallbackListener, exceptionCallbackListener); + + testUtilities.advanceTimeBySeconds(6); + + assertThat(exceptionCallbackListener.lastException).isNull(); + } + + @Test + public void other_async_timeoutSetOnType_timesOut() { + profileTestCrossProfileType + .other() + .asyncMethodWhichNeverCallsBackWith7SecondTimeout( + stringCallbackListener, exceptionCallbackListener); + + testUtilities.advanceTimeBySeconds(8); + + assertThat(exceptionCallbackListener.lastException) + .isInstanceOf(UnavailableProfileException.class); + } + + @Test + public void other_async_timeoutSetByDefault_doesNotTimeoutEarly() throws Exception { + profileTestCrossProfileTypeWhichNeedsContext + .other() + .asyncMethodWhichNeverCallsBackWithDefaultTimeout( + stringCallbackListener, exceptionCallbackListener); + + scheduledExecutorService.advanceTimeBy( + CrossProfileAnnotation.DEFAULT_TIMEOUT_MILLIS - 1, TimeUnit.MILLISECONDS); + + assertThat(exceptionCallbackListener.lastException).isNull(); + } + + @Test + public void other_async_timeoutSetByDefault_timesOut() throws Exception { + profileTestCrossProfileTypeWhichNeedsContext + .other() + .asyncMethodWhichNeverCallsBackWithDefaultTimeout( + stringCallbackListener, exceptionCallbackListener); + + scheduledExecutorService.advanceTimeBy( + CrossProfileAnnotation.DEFAULT_TIMEOUT_MILLIS + 1, TimeUnit.MILLISECONDS); + + assertThat(exceptionCallbackListener.lastException) + .isInstanceOf(UnavailableProfileException.class); + } + + @Test + public void other_async_timeoutSetByCaller_doesNotTimeoutEarly() throws Exception { + long timeoutMillis = 5000; + profileTestCrossProfileTypeWhichNeedsContext + .other() + .timeout(timeoutMillis) + .asyncMethodWhichNeverCallsBackWithDefaultTimeout( + stringCallbackListener, exceptionCallbackListener); + + scheduledExecutorService.advanceTimeBy(timeoutMillis - 1, TimeUnit.MILLISECONDS); + + assertThat(exceptionCallbackListener.lastException).isNull(); + } + + @Test + public void other_async_timeoutSetByCaller_timesOut() throws Exception { + long timeoutMillis = 5000; + profileTestCrossProfileTypeWhichNeedsContext + .other() + .timeout(timeoutMillis) + .asyncMethodWhichNeverCallsBackWithDefaultTimeout( + stringCallbackListener, exceptionCallbackListener); + + scheduledExecutorService.advanceTimeBy(timeoutMillis + 1, TimeUnit.MILLISECONDS); + + assertThat(exceptionCallbackListener.lastException) + .isInstanceOf(UnavailableProfileException.class); + } + + @Test + public void other_async_doesNotTimeoutAfterCompletion() throws Exception { + // We would expect an exception if the timeout continued after completion + profileTestCrossProfileType + .other() + .asyncVoidMethod(voidCallbackListener, exceptionCallbackListener); + + scheduledExecutorService.advanceTimeBy( + CrossProfileAnnotation.DEFAULT_TIMEOUT_MILLIS + 1, TimeUnit.MILLISECONDS); + + assertThat(exceptionCallbackListener.lastException).isNull(); + } + + @Test + public void other_async_throwsException_exceptionIsWrapped() { + // The exception is only catchable when the connection is already established. + testUtilities.startConnectingAndWait(); + + try { + profileTestCrossProfileType + .other() + .asyncStringMethodWhichThrowsRuntimeException( + stringCallbackListener, exceptionCallbackListener); + fail(); + } catch (ProfileRuntimeException expected) { + assertThat(expected).hasCauseThat().isInstanceOf(CustomRuntimeException.class); + } + } + + @Test + public void other_async_contextArgument_works() { + profileTestCrossProfileType + .other() + .asyncIsContextArgumentPassed(booleanCallbackListener, exceptionCallbackListener); + + assertThat(booleanCallbackListener.booleanCallbackValue).isTrue(); + } + + @Test + public void other_async_nonSimpleCallback_works() { + nonSimpleCallbackListener.callbackMethodCalls = 0; + profileTestCrossProfileType + .other() + .asyncMethodWithNonSimpleCallback( + nonSimpleCallbackListener, STRING, STRING2, exceptionCallbackListener); + + assertThat(nonSimpleCallbackListener.callbackMethodCalls).isEqualTo(1); + assertThat(nonSimpleCallbackListener.string1CallbackValue).isEqualTo(STRING); + assertThat(nonSimpleCallbackListener.string2CallbackValue).isEqualTo(STRING2); + } + + @Test + public void other_async_nonSimpleCallback_secondMethod_works() { + profileTestCrossProfileType + .other() + .asyncMethodWithNonSimpleCallbackCallsSecondMethod( + nonSimpleCallbackListener, STRING, STRING2, exceptionCallbackListener); + + assertThat(nonSimpleCallbackListener.string3CallbackValue).isEqualTo(STRING); + assertThat(nonSimpleCallbackListener.string4CallbackValue).isEqualTo(STRING2); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/OtherProfileListenableFutureTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/OtherProfileListenableFutureTest.java new file mode 100644 index 0000000..7d71ade --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/OtherProfileListenableFutureTest.java @@ -0,0 +1,297 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.robotests; + +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.INTERACT_ACROSS_USERS; +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.assertFutureDoesNotHaveException; +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.assertFutureHasException; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; +import static org.robolectric.annotation.LooperMode.Mode.LEGACY; + +import android.app.Application; +import android.app.Service; +import android.os.Build.VERSION_CODES; +import android.os.IBinder; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.RobolectricTestUtilities; +import com.google.android.enterprise.connectedapps.TestScheduledExecutorService; +import com.google.android.enterprise.connectedapps.exceptions.ProfileRuntimeException; +import com.google.android.enterprise.connectedapps.exceptions.UnavailableProfileException; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.interfaces.CrossProfileAnnotation; +import com.google.android.enterprise.connectedapps.testapp.CustomRuntimeException; +import com.google.android.enterprise.connectedapps.testapp.configuration.TestApplication; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileType; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileTypeWhichNeedsContext; +import com.google.android.enterprise.connectedapps.testapp.types.TestCrossProfileType; +import com.google.common.util.concurrent.ListenableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.annotation.LooperMode; + +@LooperMode(LEGACY) +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.O) +public class OtherProfileListenableFutureTest { + + private final Application context = ApplicationProvider.getApplicationContext(); + private final TestScheduledExecutorService scheduledExecutorService = + new TestScheduledExecutorService(); + private final TestProfileConnector testProfileConnector = + TestProfileConnector.create(context, scheduledExecutorService); + private final RobolectricTestUtilities testUtilities = + new RobolectricTestUtilities(testProfileConnector, scheduledExecutorService); + private final ProfileTestCrossProfileType profileTestCrossProfileType = + ProfileTestCrossProfileType.create(testProfileConnector); + private final ProfileTestCrossProfileTypeWhichNeedsContext + profileTestCrossProfileTypeWhichNeedsContext = + ProfileTestCrossProfileTypeWhichNeedsContext.create(testProfileConnector); + + @Before + public void setUp() { + Service profileAwareService = Robolectric.setupService(TestApplication.getService()); + testUtilities.initTests(); + IBinder binder = profileAwareService.onBind(/* intent= */ null); + testUtilities.setBinding(binder, RobolectricTestUtilities.TEST_CONNECTOR_CLASS_NAME); + testUtilities.createWorkUser(); + testUtilities.turnOnWorkProfile(); + testUtilities.setRunningOnPersonalProfile(); + testUtilities.setRequestsPermissions(INTERACT_ACROSS_USERS); + testUtilities.grantPermissions(INTERACT_ACROSS_USERS); + } + + @Test + public void + other_listenableFuture_automaticConnection_workProfileIsTurnedOff_doesSetUnavailableProfileExceptionImmediately() { + testProfileConnector.stopManualConnectionManagement(); + testUtilities.turnOffWorkProfile(); + + ListenableFuture<Void> future = + profileTestCrossProfileType.other().listenableFutureVoidMethod(); + + assertFutureHasException(future, UnavailableProfileException.class); + } + + @Test + public void + other_listenableFuture_automaticConnection_workProfileIsTurnedOn_doesNotSetUnavailableProfileException() { + testProfileConnector.stopManualConnectionManagement(); + testUtilities.turnOnWorkProfile(); + + ListenableFuture<Void> future = + profileTestCrossProfileType + .other() + .listenableFutureVoidMethodWithNonBlockingDelay(/* secondsDelay= */ 5); + Robolectric.getForegroundThreadScheduler().advanceBy(5, TimeUnit.SECONDS); + + assertFutureDoesNotHaveException(future, UnavailableProfileException.class); + } + + @Test + public void other_listenableFuture_automaticConnection_callsMethod() + throws ExecutionException, InterruptedException { + testProfileConnector.stopManualConnectionManagement(); + testUtilities.turnOnWorkProfile(); + + profileTestCrossProfileType.other().listenableFutureVoidMethod().get(); + + assertThat(TestCrossProfileType.voidMethodCalls).isEqualTo(1); + } + + @Test + public void other_listenableFuture_automaticConnection_setsFuture() + throws ExecutionException, InterruptedException { + testProfileConnector.stopManualConnectionManagement(); + testUtilities.turnOnWorkProfile(); + + // This would throw an exception if it wasn't set + assertThat(profileTestCrossProfileType.other().listenableFutureVoidMethod().get()).isNull(); + } + + @Test + public void + other_listenableFuture_automaticConnection_connectionIsDroppedDuringCall_setUnavailableProfileException() { + testProfileConnector.stopManualConnectionManagement(); + testUtilities.turnOnWorkProfile(); + ListenableFuture<Void> future = + profileTestCrossProfileType.other().listenableFutureMethodWhichNeverSetsTheValue(); + Robolectric.getForegroundThreadScheduler().advanceBy(5, TimeUnit.SECONDS); + + testUtilities.turnOffWorkProfile(); + + assertFutureHasException(future, UnavailableProfileException.class); + } + + @Test + public void other_listenableFuture_timeoutSetOnMethod_doesNotTimeoutEarly() throws Exception { + ListenableFuture<Void> listenableFuture = + profileTestCrossProfileType + .other() + .listenableFutureMethodWhichNeverSetsTheValueWith5SecondTimeout(); + + scheduledExecutorService.advanceTimeBy(4, TimeUnit.SECONDS); + + assertFutureDoesNotHaveException(listenableFuture, UnavailableProfileException.class); + } + + @Test + public void other_listenableFuture_timeoutSetOnMethod_timesOut() throws Exception { + ListenableFuture<Void> listenableFuture = + profileTestCrossProfileType + .other() + .listenableFutureMethodWhichNeverSetsTheValueWith5SecondTimeout(); + + scheduledExecutorService.advanceTimeBy(6, TimeUnit.SECONDS); + + assertFutureHasException(listenableFuture, UnavailableProfileException.class); + } + + @Test + public void other_listenableFuture_timeoutSetOnType_doesNotTimeoutEarly() throws Exception { + ListenableFuture<Void> listenableFuture = + profileTestCrossProfileType + .other() + .listenableFutureMethodWhichNeverSetsTheValueWith7SecondTimeout(); + + scheduledExecutorService.advanceTimeBy(6, TimeUnit.SECONDS); + + assertFutureDoesNotHaveException(listenableFuture, UnavailableProfileException.class); + } + + @Test + public void other_listenableFuture_timeoutSetOnType_timesOut() throws Exception { + ListenableFuture<Void> listenableFuture = + profileTestCrossProfileType + .other() + .listenableFutureMethodWhichNeverSetsTheValueWith7SecondTimeout(); + + scheduledExecutorService.advanceTimeBy(8, TimeUnit.SECONDS); + + assertFutureHasException(listenableFuture, UnavailableProfileException.class); + } + + @Test + public void other_listenableFuture_timeoutSetByDefault_doesNotTimeoutEarly() throws Exception { + ListenableFuture<Void> listenableFuture = + profileTestCrossProfileTypeWhichNeedsContext + .other() + .listenableFutureMethodWhichNeverSetsTheValueWithDefaultTimeout(); + + scheduledExecutorService.advanceTimeBy( + CrossProfileAnnotation.DEFAULT_TIMEOUT_MILLIS - 1, TimeUnit.MILLISECONDS); + + assertFutureDoesNotHaveException(listenableFuture, UnavailableProfileException.class); + } + + @Test + public void other_listenableFuture_timeoutSetByDefault_timesOut() throws Exception { + ListenableFuture<Void> listenableFuture = + profileTestCrossProfileTypeWhichNeedsContext + .other() + .listenableFutureMethodWhichNeverSetsTheValueWithDefaultTimeout(); + + scheduledExecutorService.advanceTimeBy( + CrossProfileAnnotation.DEFAULT_TIMEOUT_MILLIS + 1, TimeUnit.MILLISECONDS); + + assertFutureHasException(listenableFuture, UnavailableProfileException.class); + } + + @Test + public void other_listenableFuture_timeoutSetByCaller_doesNotTimeoutEarly() throws Exception { + long timeoutMillis = 5000; + ListenableFuture<Void> listenableFuture = + profileTestCrossProfileTypeWhichNeedsContext + .other() + .timeout(timeoutMillis) + .listenableFutureMethodWhichNeverSetsTheValueWithDefaultTimeout(); + + scheduledExecutorService.advanceTimeBy(timeoutMillis - 1, TimeUnit.MILLISECONDS); + + assertFutureDoesNotHaveException(listenableFuture, UnavailableProfileException.class); + } + + @Test + public void other_listenableFuture_timeoutSetByCaller_timesOut() throws Exception { + long timeoutMillis = 5000; + ListenableFuture<Void> listenableFuture = + profileTestCrossProfileTypeWhichNeedsContext + .other() + .timeout(timeoutMillis) + .listenableFutureMethodWhichNeverSetsTheValueWithDefaultTimeout(); + + scheduledExecutorService.advanceTimeBy(timeoutMillis + 1, TimeUnit.MILLISECONDS); + + assertFutureHasException(listenableFuture, UnavailableProfileException.class); + } + + @Test + public void other_listenableFuture_doesNotTimeoutAfterCompletion() throws Exception { + // We would expect an exception if the timeout continued after completion + ListenableFuture<Void> listenableFuture = + profileTestCrossProfileType.other().listenableFutureVoidMethod(); + + scheduledExecutorService.advanceTimeBy( + CrossProfileAnnotation.DEFAULT_TIMEOUT_MILLIS + 1, TimeUnit.MILLISECONDS); + + assertFutureDoesNotHaveException(listenableFuture, UnavailableProfileException.class); + } + + @Test + public void other_listenableFuture_doesNotTimeoutAfterException() throws Exception { + // We would expect an exception if the timeout continued after completion + ListenableFuture<Void> unusedFuture = + profileTestCrossProfileType + .other() + .listenableFutureVoidMethodWhichSetsIllegalStateException(); + + scheduledExecutorService.advanceTimeBy( + CrossProfileAnnotation.DEFAULT_TIMEOUT_MILLIS + 1, TimeUnit.MILLISECONDS); + + // We expect there would be an exception thrown due to setting the future twice if it timed out + // now + } + + @Test + public void other_listenableFuture_throwsException_exceptionIsWrapped() { + // The exception is only catchable when the connection is already established. + testUtilities.startConnectingAndWait(); + + try { + ListenableFuture<Void> unusedFuture = + profileTestCrossProfileType + .other() + .listenableFutureVoidMethodWhichThrowsRuntimeException(); + fail(); + } catch (ProfileRuntimeException expected) { + assertThat(expected).hasCauseThat().isInstanceOf(CustomRuntimeException.class); + } + } + + @Test + public void other_listenableFuture_contextArgument_works() throws Exception { + ListenableFuture<Boolean> result = + profileTestCrossProfileType.other().futureIsContextArgumentPassed(); + + assertThat(result.get()).isTrue(); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/OtherProfileManualAsyncTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/OtherProfileManualAsyncTest.java new file mode 100644 index 0000000..cac6278 --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/OtherProfileManualAsyncTest.java @@ -0,0 +1,196 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.robotests; + +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.INTERACT_ACROSS_USERS; +import static com.google.common.truth.Truth.assertThat; +import static org.robolectric.annotation.LooperMode.Mode.LEGACY; + +import android.app.Application; +import android.app.Service; +import android.os.Build.VERSION_CODES; +import android.os.IBinder; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.RobolectricTestUtilities; +import com.google.android.enterprise.connectedapps.TestExceptionCallbackListener; +import com.google.android.enterprise.connectedapps.TestScheduledExecutorService; +import com.google.android.enterprise.connectedapps.TestStringCallbackListenerImpl; +import com.google.android.enterprise.connectedapps.TestVoidCallbackListenerImpl; +import com.google.android.enterprise.connectedapps.exceptions.UnavailableProfileException; +import com.google.android.enterprise.connectedapps.testapp.configuration.TestApplication; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileType; +import com.google.android.enterprise.connectedapps.testapp.types.TestCrossProfileType; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.annotation.LooperMode; + +@LooperMode(LEGACY) +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.O) +public class OtherProfileManualAsyncTest { + + private static final String STRING = "String"; + + private final Application context = ApplicationProvider.getApplicationContext(); + private final TestStringCallbackListenerImpl stringCallbackListener = + new TestStringCallbackListenerImpl(); + private final TestVoidCallbackListenerImpl voidCallbackListener = + new TestVoidCallbackListenerImpl(); + private final TestExceptionCallbackListener exceptionCallbackListener = + new TestExceptionCallbackListener(); + private final TestScheduledExecutorService scheduledExecutorService = + new TestScheduledExecutorService(); + private final TestProfileConnector testProfileConnector = + TestProfileConnector.create(context, scheduledExecutorService); + private final RobolectricTestUtilities testUtilities = + new RobolectricTestUtilities(testProfileConnector, scheduledExecutorService); + + private final ProfileTestCrossProfileType profileTestCrossProfileType = + ProfileTestCrossProfileType.create(testProfileConnector); + + @Before + public void setUp() { + Service profileAwareService = Robolectric.setupService(TestApplication.getService()); + testUtilities.initTests(); + IBinder binder = profileAwareService.onBind(/* intent= */ null); + testUtilities.setBinding(binder, RobolectricTestUtilities.TEST_CONNECTOR_CLASS_NAME); + testUtilities.createWorkUser(); + testUtilities.turnOnWorkProfile(); + testUtilities.setRunningOnPersonalProfile(); + testUtilities.setRequestsPermissions(INTERACT_ACROSS_USERS); + testUtilities.grantPermissions(INTERACT_ACROSS_USERS); + } + + @Test + public void other_async_manualConnection_isBound_callsMethod() { + testUtilities.turnOnWorkProfile(); + testUtilities.startConnectingAndWait(); + + profileTestCrossProfileType + .other() + .asyncVoidMethod(voidCallbackListener, exceptionCallbackListener); + + assertThat(TestCrossProfileType.voidMethodCalls).isEqualTo(1); + } + + @Test + public void other_async_manualConnection_isBound_firesCallback() { + testUtilities.turnOnWorkProfile(); + testUtilities.startConnectingAndWait(); + + profileTestCrossProfileType + .other() + .asyncVoidMethod(voidCallbackListener, exceptionCallbackListener); + + assertThat(voidCallbackListener.callbackMethodCalls).isEqualTo(1); + } + + @Test + public void other_async_manualConnection_isBound_unbundlesCorrectly() { + testUtilities.turnOnWorkProfile(); + testUtilities.startConnectingAndWait(); + + profileTestCrossProfileType + .other() + .asyncIdentityStringMethod(STRING, stringCallbackListener, exceptionCallbackListener); + + assertThat(stringCallbackListener.stringCallbackValue).isEqualTo(STRING); + } + + @Test // This behaviour is expected right now but will change + public void other_async_manualConnection_isBound_blockingMethod_blocks() { + testUtilities.turnOnWorkProfile(); + testUtilities.startConnectingAndWait(); + + profileTestCrossProfileType + .other() + .asyncVoidMethodWithDelay( + voidCallbackListener, /* secondsDelay= */ 5, exceptionCallbackListener); + + assertThat(voidCallbackListener.callbackMethodCalls).isEqualTo(1); + } + + @Test + public void other_async_manualConnection_isBound_nonBlockingMethod_doesNotBlock() { + testUtilities.turnOnWorkProfile(); + testUtilities.startConnectingAndWait(); + + profileTestCrossProfileType + .other() + .asyncVoidMethodWithNonBlockingDelay( + voidCallbackListener, /* secondsDelay= */ 5, exceptionCallbackListener); + + assertThat(stringCallbackListener.callbackMethodCalls).isEqualTo(0); + } + + @Test + public void other_async_manualConnection_isBound_nonBlockingMethod_doesCallback() { + testUtilities.turnOnWorkProfile(); + testUtilities.startConnectingAndWait(); + + profileTestCrossProfileType + .other() + .asyncVoidMethodWithNonBlockingDelay( + voidCallbackListener, /* secondsDelay= */ 5, exceptionCallbackListener); + testUtilities.advanceTimeBySeconds(10); + + assertThat(voidCallbackListener.callbackMethodCalls).isEqualTo(1); + } + + @Test + public void other_async_manualConnection_isNotBound_doesReturnUnavailableProfileException() { + testUtilities.turnOffWorkProfile(); + + profileTestCrossProfileType + .other() + .asyncVoidMethod(voidCallbackListener, exceptionCallbackListener); + + assertThat(exceptionCallbackListener.lastException) + .isInstanceOf(UnavailableProfileException.class); + } + + @Test + public void other_asyncMethod_manualConnection_isNotBound_binds() { + testUtilities.turnOnWorkProfile(); + + profileTestCrossProfileType + .other() + .asyncVoidMethod(voidCallbackListener, exceptionCallbackListener); + + assertThat(testProfileConnector.isConnected()).isTrue(); + } + + @Test + public void + other_async_manualConnection_connectionIsDroppedDuringCall_setUnavailableProfileException() { + testUtilities.turnOnWorkProfile(); + testUtilities.startConnectingAndWait(); + profileTestCrossProfileType + .other() + .asyncMethodWhichNeverCallsBack(stringCallbackListener, exceptionCallbackListener); + testUtilities.advanceTimeBySeconds(5); + + testUtilities.turnOffWorkProfile(); + + assertThat(exceptionCallbackListener.lastException) + .isInstanceOf(UnavailableProfileException.class); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/OtherProfileManualListenableFutureTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/OtherProfileManualListenableFutureTest.java new file mode 100644 index 0000000..ae6aafb --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/OtherProfileManualListenableFutureTest.java @@ -0,0 +1,184 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.robotests; + +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.INTERACT_ACROSS_USERS; +import static com.google.common.truth.Truth.assertThat; +import static org.robolectric.annotation.LooperMode.Mode.LEGACY; + +import android.app.Application; +import android.app.Service; +import android.os.Build.VERSION_CODES; +import android.os.IBinder; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.RobolectricTestUtilities; +import com.google.android.enterprise.connectedapps.TestScheduledExecutorService; +import com.google.android.enterprise.connectedapps.exceptions.UnavailableProfileException; +import com.google.android.enterprise.connectedapps.testapp.configuration.TestApplication; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileType; +import com.google.android.enterprise.connectedapps.testapp.types.TestCrossProfileType; +import com.google.common.util.concurrent.ListenableFuture; +import java.util.concurrent.ExecutionException; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.annotation.LooperMode; + +@LooperMode(LEGACY) +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.O) +public class OtherProfileManualListenableFutureTest { + + private static final String STRING = "String"; + + private final Application context = ApplicationProvider.getApplicationContext(); + private final TestScheduledExecutorService scheduledExecutorService = + new TestScheduledExecutorService(); + private final TestProfileConnector testProfileConnector = + TestProfileConnector.create(context, scheduledExecutorService); + private final RobolectricTestUtilities testUtilities = + new RobolectricTestUtilities(testProfileConnector, scheduledExecutorService); + private final ProfileTestCrossProfileType profileTestCrossProfileType = + ProfileTestCrossProfileType.create(testProfileConnector); + + @Before + public void setUp() { + Service profileAwareService = Robolectric.setupService(TestApplication.getService()); + testUtilities.initTests(); + IBinder binder = profileAwareService.onBind(/* intent= */ null); + testUtilities.setBinding(binder, RobolectricTestUtilities.TEST_CONNECTOR_CLASS_NAME); + testUtilities.createWorkUser(); + testUtilities.turnOnWorkProfile(); + testUtilities.setRunningOnPersonalProfile(); + testUtilities.setRequestsPermissions(INTERACT_ACROSS_USERS); + testUtilities.grantPermissions(INTERACT_ACROSS_USERS); + } + + @Test + public void + other_listenableFuture_manualConnection_workProfileIsTurnedOff_doesSetUnavailableProfileExceptionImmediately() { + testUtilities.startConnectingAndWait(); + testUtilities.turnOffWorkProfile(); + + testUtilities.assertFutureHasException( + profileTestCrossProfileType.other().listenableFutureVoidMethod(), + UnavailableProfileException.class); + } + + @Test + public void other_listenableFuture_manualConnection_isBound_callsMethod() + throws ExecutionException, InterruptedException { + testUtilities.turnOnWorkProfile(); + testUtilities.startConnectingAndWait(); + + profileTestCrossProfileType.other().listenableFutureVoidMethod().get(); + + assertThat(TestCrossProfileType.voidMethodCalls).isEqualTo(1); + } + + @Test + public void + other_listenableFuture_manualConnection_isNotBound_doesNotThrowUnavailableProfileException() { + testUtilities.turnOffWorkProfile(); + + ListenableFuture<Void> unusedFuture = + profileTestCrossProfileType.other().listenableFutureVoidMethod(); + } + + @Test + public void + other_listenableFuture_manualConnection_isNotBound_returnsThrowUnavailableProfileException() { + testUtilities.turnOffWorkProfile(); + + ListenableFuture<Void> future = + profileTestCrossProfileType.other().listenableFutureVoidMethod(); + + testUtilities.assertFutureHasException(future, UnavailableProfileException.class); + } + + @Test // This behaviour is expected right now but will change + public void other_listenableFuture_manualConnection_blockingMethod_blocks() { + ListenableFuture<Void> voidFuture = + profileTestCrossProfileType + .other() + .listenableFutureVoidMethodWithDelay(/* secondsDelay= */ 5); + + assertThat(voidFuture.isDone()).isTrue(); + } + + @Test + public void other_listenableFuture_manualConnection_setsFuture() + throws ExecutionException, InterruptedException { + + // This would throw an exception if it wasn't set + assertThat(profileTestCrossProfileType.other().listenableFutureVoidMethod().get()).isNull(); + } + + @Test + public void other_listenableFuture_manualConnection_setsException_isSet() { + testUtilities.assertFutureHasException( + profileTestCrossProfileType + .other() + .listenableFutureVoidMethodWhichSetsIllegalStateException(), + IllegalStateException.class); + } + + @Test + public void other_listenableFuture_manualConnection_passesParametersCorrectly() + throws ExecutionException, InterruptedException { + ListenableFuture<String> future = + profileTestCrossProfileType.other().listenableFutureIdentityStringMethod(STRING); + + assertThat(future.get()).isEqualTo(STRING); + } + + @Test + public void other_listenableFuture_manualConnection_nonblockingMethod_doesNotBlock() { + ListenableFuture<Void> future = + profileTestCrossProfileType + .other() + .listenableFutureVoidMethodWithNonBlockingDelay(/* secondsDelay= */ 5); + + assertThat(future.isDone()).isFalse(); + } + + @Test + public void other_listenableFuture_manualConnection_nonblockingMethod_doesCallback() { + ListenableFuture<Void> future = + profileTestCrossProfileType + .other() + .listenableFutureVoidMethodWithNonBlockingDelay(/* secondsDelay= */ 5); + testUtilities.advanceTimeBySeconds(10); + + assertThat(future.isDone()).isTrue(); + } + + @Test + public void + other_listenableFuture_manualConnection_connectionIsDroppedDuringCall_setUnavailableProfileException() { + ListenableFuture<Void> future = + profileTestCrossProfileType.other().listenableFutureMethodWhichNeverSetsTheValue(); + testUtilities.advanceTimeBySeconds(5); + + testUtilities.turnOffWorkProfile(); + + testUtilities.assertFutureHasException(future, UnavailableProfileException.class); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/OtherProfileSynchronousTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/OtherProfileSynchronousTest.java new file mode 100644 index 0000000..5b40c0f --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/OtherProfileSynchronousTest.java @@ -0,0 +1,164 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.robotests; + +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.INTERACT_ACROSS_USERS; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; +import static org.robolectric.annotation.LooperMode.Mode.LEGACY; + +import android.app.Application; +import android.app.Service; +import android.os.Build.VERSION_CODES; +import android.os.IBinder; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.RobolectricTestUtilities; +import com.google.android.enterprise.connectedapps.TestScheduledExecutorService; +import com.google.android.enterprise.connectedapps.exceptions.UnavailableProfileException; +import com.google.android.enterprise.connectedapps.testapp.NotReallySerializableObject; +import com.google.android.enterprise.connectedapps.testapp.ParcelableObject; +import com.google.android.enterprise.connectedapps.testapp.configuration.TestApplication; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileType; +import com.google.common.util.concurrent.ListenableFuture; +import java.io.IOException; +import java.sql.SQLException; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.annotation.LooperMode; + +@LooperMode(LEGACY) +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.O) +public class OtherProfileSynchronousTest { + + private static final String STRING = "String"; + private static final ParcelableObject PARCELABLE_OBJECT = new ParcelableObject(""); + private static final NotReallySerializableObject NOT_REALLY_SERIALIZABLE_OBJECT = + new NotReallySerializableObject(PARCELABLE_OBJECT); + + private final Application context = ApplicationProvider.getApplicationContext(); + private final TestScheduledExecutorService scheduledExecutorService = + new TestScheduledExecutorService(); + private final TestProfileConnector testProfileConnector = + TestProfileConnector.create(context, scheduledExecutorService); + private final RobolectricTestUtilities testUtilities = + new RobolectricTestUtilities(testProfileConnector, scheduledExecutorService); + private final ProfileTestCrossProfileType profileTestCrossProfileType = + ProfileTestCrossProfileType.create(testProfileConnector); + + @Before + public void setUp() { + Service profileAwareService = Robolectric.setupService(TestApplication.getService()); + testUtilities.initTests(); + IBinder binder = profileAwareService.onBind(/* intent= */ null); + testUtilities.setBinding(binder, RobolectricTestUtilities.TEST_CONNECTOR_CLASS_NAME); + testUtilities.createWorkUser(); + testUtilities.turnOnWorkProfile(); + testUtilities.setRunningOnPersonalProfile(); + testUtilities.setRequestsPermissions(INTERACT_ACROSS_USERS); + testUtilities.grantPermissions(INTERACT_ACROSS_USERS); + testUtilities.startConnectingAndWait(); + } + + @Test + public void other_synchronous_isBound_callsMethod() throws UnavailableProfileException { + testUtilities.turnOnWorkProfile(); + testUtilities.startConnectingAndWait(); + + assertThat(profileTestCrossProfileType.other().identityStringMethod(STRING)).isEqualTo(STRING); + } + + @Test + public void other_synchronous_isNotBound_throwsUnavailableProfileException() { + testUtilities.turnOffWorkProfile(); + + assertThrows( + UnavailableProfileException.class, + () -> profileTestCrossProfileType.other().identityStringMethod(STRING)); + } + + @Test + public void other_synchronous_isNotInitialised_throwsUnavailableProfileException() { + ProfileTestCrossProfileType profileTestCrossProfileType = + ProfileTestCrossProfileType.create(TestProfileConnector.create(context)); + + assertThrows( + UnavailableProfileException.class, + () -> profileTestCrossProfileType.other().identityStringMethod(STRING)); + } + + @Test + public void + other_synchronous_isBound_automaticConnectionManagement_throwsUnavailableProfileException() { + testUtilities.turnOnWorkProfile(); + testProfileConnector.stopManualConnectionManagement(); + ListenableFuture<Void> ignored = + profileTestCrossProfileType.other().listenableFutureVoidMethod(); // Causes it to bind + + assertThrows( + UnavailableProfileException.class, + () -> profileTestCrossProfileType.other().voidMethod()); + } + + @Test + public void other_serializableObjectParameterIsNotReallySerializable_throwsException() { + assertThrows( + RuntimeException.class, + () -> + profileTestCrossProfileType + .other() + .identityNotReallySerializableObjectMethod(NOT_REALLY_SERIALIZABLE_OBJECT)); + } + + @Test + public void other_synchronous_contextArgument_works() throws Exception { + assertThat(profileTestCrossProfileType.other().isContextArgumentPassed()).isTrue(); + } + + @Test + public void other_synchronous_declaresButDoesNotThrowException_works() throws Exception { + assertThat( + profileTestCrossProfileType + .other() + .identityStringMethodDeclaresButDoesNotThrowIOException(STRING)) + .isEqualTo(STRING); + } + + @Test + public void other_synchronous_throwsException_works() { + assertThrows( + IOException.class, + () -> + profileTestCrossProfileType + .other() + .identityStringMethodThrowsIOException(STRING)); + } + + @Test + public void other_synchronous_declaresMultipleExceptions_throwsException_works() { + assertThrows( + SQLException.class, + () -> + profileTestCrossProfileType + .other() + .identityStringMethodDeclaresIOExceptionThrowsSQLException(STRING)); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/ProfilesTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/ProfilesTest.java new file mode 100644 index 0000000..4150353 --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/ProfilesTest.java @@ -0,0 +1,148 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.robotests; + +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.INTERACT_ACROSS_USERS; +import static com.google.common.truth.Truth.assertThat; +import static org.robolectric.annotation.LooperMode.Mode.LEGACY; + +import android.app.Application; +import android.app.Service; +import android.os.Build.VERSION_CODES; +import android.os.IBinder; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.Profile; +import com.google.android.enterprise.connectedapps.RobolectricTestUtilities; +import com.google.android.enterprise.connectedapps.TestScheduledExecutorService; +import com.google.android.enterprise.connectedapps.testapp.configuration.TestApplication; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileType; +import com.google.android.enterprise.connectedapps.testapp.types.TestCrossProfileType; +import java.util.Map; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.annotation.LooperMode; + +@LooperMode(LEGACY) +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.O) +public class ProfilesTest { + + private static final String STRING = "String"; + + private final Application context = ApplicationProvider.getApplicationContext(); + private final TestScheduledExecutorService scheduledExecutorService = + new TestScheduledExecutorService(); + private final TestProfileConnector testProfileConnector = + TestProfileConnector.create(context, scheduledExecutorService); + private final RobolectricTestUtilities testUtilities = + new RobolectricTestUtilities(testProfileConnector, scheduledExecutorService); + private final ProfileTestCrossProfileType profileTestCrossProfileType = + ProfileTestCrossProfileType.create(testProfileConnector); + + private final Profile currentProfileIdentifier = testProfileConnector.utils().getCurrentProfile(); + private final Profile otherProfileIdentifier = testProfileConnector.utils().getOtherProfile(); + + @Before + public void setUp() { + Service profileAwareService = Robolectric.setupService(TestApplication.getService()); + testUtilities.initTests(); + IBinder binder = profileAwareService.onBind(/* intent= */ null); + testUtilities.setBinding(binder, RobolectricTestUtilities.TEST_CONNECTOR_CLASS_NAME); + testUtilities.createWorkUser(); + testUtilities.turnOnWorkProfile(); + testUtilities.setRunningOnPersonalProfile(); + testUtilities.setRequestsPermissions(INTERACT_ACROSS_USERS); + testUtilities.grantPermissions(INTERACT_ACROSS_USERS); + testUtilities.startConnectingAndWait(); + } + + @Test + public void profiles_isBound_resultContainsAllProfileResults() { + testUtilities.turnOnWorkProfile(); + testUtilities.startConnectingAndWait(); + + Map<Profile, String> result = + profileTestCrossProfileType + .profiles(currentProfileIdentifier, otherProfileIdentifier) + .identityStringMethod(STRING); + + assertThat(result.get(currentProfileIdentifier)).isEqualTo(STRING); + assertThat(result.get(otherProfileIdentifier)).isEqualTo(STRING); + } + + @Test + public void profiles_isNotBound_resultDoesNotContainOtherProfileResult() { + testUtilities.turnOffWorkProfile(); + + Map<Profile, String> result = + profileTestCrossProfileType + .profiles(currentProfileIdentifier, otherProfileIdentifier) + .identityStringMethod(STRING); + + assertThat(result.get(currentProfileIdentifier)).isEqualTo(STRING); + assertThat(result).doesNotContainKey(otherProfileIdentifier); + } + + @Test + public void profiles_passedCurrentProfile_runsOnlyOnCurrentProfile() { + Map<Profile, String> result = + profileTestCrossProfileType.profiles(currentProfileIdentifier).identityStringMethod(STRING); + + assertThat(result).hasSize(1); + assertThat(result).containsKey(currentProfileIdentifier); + } + + @Test + public void profiles_passedOtherProfile_runsOnlyOnOtherProfile() { + testUtilities.turnOnWorkProfile(); + + Map<Profile, String> result = + profileTestCrossProfileType.profiles(otherProfileIdentifier).identityStringMethod(STRING); + + assertThat(result).hasSize(1); + assertThat(result).containsKey(otherProfileIdentifier); + } + + @Test + public void profiles_passedMultipleOfSameProfile_runsOnlyOncePerProfile() { + profileTestCrossProfileType + .profiles(currentProfileIdentifier, currentProfileIdentifier) + .voidMethod(); + + assertThat(TestCrossProfileType.voidMethodCalls).isEqualTo(1); + } + + @Test + public void suppliers_runningOnSecondaryProfile_runsOnlyOnce() { + testUtilities.setRunningOnPersonalProfile(); // Work profile is primary + profileTestCrossProfileType.suppliers().voidMethod(); + + assertThat(TestCrossProfileType.voidMethodCalls).isEqualTo(1); + } + + @Test + public void suppliers_runningOnPrimaryProfile_runsOnEachProfile() { + testUtilities.setRunningOnWorkProfile(); // Work profile is primary + profileTestCrossProfileType.suppliers().voidMethod(); + + assertThat(TestCrossProfileType.voidMethodCalls).isEqualTo(2); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/ProviderTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/ProviderTest.java new file mode 100644 index 0000000..e66c09a --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/ProviderTest.java @@ -0,0 +1,60 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.robotests; + +import android.content.Context; +import android.os.Build.VERSION_CODES; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.RobolectricTestUtilities; +import com.google.android.enterprise.connectedapps.TestScheduledExecutorService; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileType; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileTypeWhichNeedsContext; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.O) +public class ProviderTest { + + private final TestScheduledExecutorService scheduledExecutorService = + new TestScheduledExecutorService(); + private final Context context = ApplicationProvider.getApplicationContext(); + private final TestProfileConnector testProfileConnector = + TestProfileConnector.create(context, scheduledExecutorService); + private final RobolectricTestUtilities testUtilities = + new RobolectricTestUtilities(testProfileConnector, scheduledExecutorService); + + @Before + public void setup() { + testUtilities.startConnectingAndWait(); + } + + @Test + public void provideWithoutContext_doesNotThrowException() { + ProfileTestCrossProfileType.create(testProfileConnector).current().voidMethod(); + } + + @Test + public void provideWithContext_doesNotThrowException() { + ProfileTestCrossProfileTypeWhichNeedsContext.create(testProfileConnector) + .current() + .voidMethod(); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/StaticTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/StaticTest.java new file mode 100644 index 0000000..5ad5534 --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/StaticTest.java @@ -0,0 +1,241 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.robotests; + +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.INTERACT_ACROSS_USERS; +import static com.google.common.truth.Truth.assertThat; + +import android.app.Application; +import android.app.Service; +import android.os.Build.VERSION_CODES; +import android.os.IBinder; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.Profile; +import com.google.android.enterprise.connectedapps.RobolectricTestUtilities; +import com.google.android.enterprise.connectedapps.TestExceptionCallbackListener; +import com.google.android.enterprise.connectedapps.TestScheduledExecutorService; +import com.google.android.enterprise.connectedapps.TestStringCallbackListenerImpl; +import com.google.android.enterprise.connectedapps.TestStringCallbackListenerMultiImpl; +import com.google.android.enterprise.connectedapps.testapp.configuration.TestApplication; +import com.google.android.enterprise.connectedapps.testapp.connector.FakeTestProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.types.FakeProfileNonInstantiableTestCrossProfileType; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileNonInstantiableTestCrossProfileType; +import com.google.android.enterprise.connectedapps.testing.annotations.CrossProfileTest; +import com.google.common.util.concurrent.ListenableFuture; +import java.util.Map; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Tests specific to static @CrossProfile methods. + * + * <p>These tests are located here rather than with e.g. {@link BothProfilesAsyncTest} etc. because + * they will be used to also test that static methods can be used without instantiating the type. + */ +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.O) +@CrossProfileTest(configuration = TestApplication.class) +public final class StaticTest { + + private static final String STRING = "string"; + + private final Application context = ApplicationProvider.getApplicationContext(); + private final TestScheduledExecutorService scheduledExecutorService = + new TestScheduledExecutorService(); + private final TestProfileConnector testProfileConnector = + TestProfileConnector.create(context, scheduledExecutorService); + private final RobolectricTestUtilities testUtilities = + new RobolectricTestUtilities(testProfileConnector, scheduledExecutorService); + private final ProfileNonInstantiableTestCrossProfileType type = + ProfileNonInstantiableTestCrossProfileType.create(testProfileConnector); + + private final FakeTestProfileConnector fakeConnector = new FakeTestProfileConnector(context); + private final FakeProfileNonInstantiableTestCrossProfileType fakeType = + FakeProfileNonInstantiableTestCrossProfileType.builder().connector(fakeConnector).build(); + + private final TestStringCallbackListenerImpl stringCallbackListener = + new TestStringCallbackListenerImpl(); + private final TestStringCallbackListenerMultiImpl stringCallbackMultiListener = + new TestStringCallbackListenerMultiImpl(); + private final TestExceptionCallbackListener exceptionCallbackListener = + new TestExceptionCallbackListener(); + + @Before + public void setUp() { + Service profileAwareService = Robolectric.setupService(TestApplication.getService()); + testUtilities.initTests(); + IBinder binder = profileAwareService.onBind(/* intent= */ null); + testUtilities.setBinding(binder, TestProfileConnector.class.getName()); + testUtilities.createWorkUser(); + testUtilities.turnOnWorkProfile(); + testUtilities.setRunningOnPersonalProfile(); + testUtilities.setRequestsPermissions(INTERACT_ACROSS_USERS); + testUtilities.grantPermissions(INTERACT_ACROSS_USERS); + testUtilities.startConnectingAndWait(); + } + + @Test + public void staticCrossProfileMethod_blocking_other_works() throws Exception { + assertThat(type.other().staticIdentityStringMethod(STRING)).isEqualTo(STRING); + } + + @Test + public void staticCrossProfileMethod_blocking_current_works() { + assertThat(type.current().staticIdentityStringMethod(STRING)).isEqualTo(STRING); + } + + @Test + public void staticCrossProfileMethod_blocking_both_works() { + Map<Profile, String> result = type.both().staticIdentityStringMethod(STRING); + + assertThat(result).containsKey(testProfileConnector.utils().getCurrentProfile()); + assertThat(result).containsKey(testProfileConnector.utils().getOtherProfile()); + } + + @Test + public void staticCrossProfileMethod_fake_blocking_other_works() throws Exception { + fakeConnector.turnOnWorkProfile(); + fakeConnector.startConnecting(); + + assertThat(fakeType.other().staticIdentityStringMethod(STRING)).isEqualTo(STRING); + } + + @Test + public void staticCrossProfileMethod_fake_blocking_current_works() { + assertThat(fakeType.current().staticIdentityStringMethod(STRING)).isEqualTo(STRING); + } + + @Test + public void staticCrossProfileMethod_fake_blocking_both_works() { + fakeConnector.turnOnWorkProfile(); + fakeConnector.startConnecting(); + + Map<Profile, String> result = fakeType.both().staticIdentityStringMethod(STRING); + + assertThat(result).containsKey(testProfileConnector.utils().getCurrentProfile()); + assertThat(result).containsKey(testProfileConnector.utils().getOtherProfile()); + } + + @Test + public void staticCrossProfileMethod_async_other_works() { + type.other() + .staticAsyncIdentityStringMethod(STRING, stringCallbackListener, exceptionCallbackListener); + + assertThat(stringCallbackListener.stringCallbackValue).isEqualTo(STRING); + } + + @Test + public void staticCrossProfileMethod_async_current_works() { + type.current().staticAsyncIdentityStringMethod(STRING, stringCallbackListener); + + assertThat(stringCallbackListener.stringCallbackValue).isEqualTo(STRING); + } + + @Test + public void staticCrossProfileMethod_async_both_works() { + type.both().staticAsyncIdentityStringMethod(STRING, stringCallbackMultiListener); + + Map<Profile, String> result = stringCallbackMultiListener.stringCallbackValues; + assertThat(result).containsKey(testProfileConnector.utils().getCurrentProfile()); + assertThat(result).containsKey(testProfileConnector.utils().getOtherProfile()); + } + + @Test + public void staticCrossProfileMethod_fake_async_other_works() { + fakeConnector.turnOnWorkProfile(); + + fakeType + .other() + .staticAsyncIdentityStringMethod(STRING, stringCallbackListener, exceptionCallbackListener); + + assertThat(stringCallbackListener.stringCallbackValue).isEqualTo(STRING); + } + + @Test + public void staticCrossProfileMethod_fake_async_current_works() { + fakeType.current().staticAsyncIdentityStringMethod(STRING, stringCallbackListener); + + assertThat(stringCallbackListener.stringCallbackValue).isEqualTo(STRING); + } + + @Test + public void staticCrossProfileMethod_fake_async_both_works() { + fakeConnector.turnOnWorkProfile(); + + fakeType.both().staticAsyncIdentityStringMethod(STRING, stringCallbackMultiListener); + + Map<Profile, String> result = stringCallbackMultiListener.stringCallbackValues; + assertThat(result).containsKey(testProfileConnector.utils().getCurrentProfile()); + assertThat(result).containsKey(testProfileConnector.utils().getOtherProfile()); + } + + @Test + public void staticCrossProfileMethod_future_other_works() throws Exception { + ListenableFuture<String> result = type.other().staticFutureIdentityStringMethod(STRING); + + assertThat(result.get()).isEqualTo(STRING); + } + + @Test + public void staticCrossProfileMethod_future_current_works() throws Exception { + ListenableFuture<String> result = type.current().staticFutureIdentityStringMethod(STRING); + + assertThat(result.get()).isEqualTo(STRING); + } + + @Test + public void staticCrossProfileMethod_future_both_works() throws Exception { + ListenableFuture<Map<Profile, String>> resultFuture = + type.both().staticFutureIdentityStringMethod(STRING); + + Map<Profile, String> result = resultFuture.get(); + assertThat(result).containsKey(testProfileConnector.utils().getCurrentProfile()); + assertThat(result).containsKey(testProfileConnector.utils().getOtherProfile()); + } + + @Test + public void staticCrossProfileMethod_fake_future_other_works() throws Exception { + fakeConnector.turnOnWorkProfile(); + + ListenableFuture<String> result = fakeType.other().staticFutureIdentityStringMethod(STRING); + + assertThat(result.get()).isEqualTo(STRING); + } + + @Test + public void staticCrossProfileMethod_fake_future_current_works() throws Exception { + ListenableFuture<String> result = fakeType.current().staticFutureIdentityStringMethod(STRING); + + assertThat(result.get()).isEqualTo(STRING); + } + + @Test + public void staticCrossProfileMethod_fake_future_both_works() throws Exception { + fakeConnector.turnOnWorkProfile(); + + ListenableFuture<Map<Profile, String>> resultFuture = + fakeType.both().staticFutureIdentityStringMethod(STRING); + + Map<Profile, String> result = resultFuture.get(); + assertThat(result).containsKey(testProfileConnector.utils().getCurrentProfile()); + assertThat(result).containsKey(testProfileConnector.utils().getOtherProfile()); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/TypesTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/TypesTest.java new file mode 100644 index 0000000..ff5d66c --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/robotests/TypesTest.java @@ -0,0 +1,647 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.robotests; + +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.INTERACT_ACROSS_USERS; +import static com.google.common.truth.Truth.assertThat; + +import android.app.Application; +import android.app.Service; +import android.content.Context; +import android.graphics.Bitmap; +import android.os.Build.VERSION_CODES; +import android.os.IBinder; +import android.util.Pair; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.RobolectricTestUtilities; +import com.google.android.enterprise.connectedapps.TestCustomWrapperCallbackListenerImpl; +import com.google.android.enterprise.connectedapps.TestExceptionCallbackListener; +import com.google.android.enterprise.connectedapps.TestScheduledExecutorService; +import com.google.android.enterprise.connectedapps.exceptions.UnavailableProfileException; +import com.google.android.enterprise.connectedapps.testapp.CustomWrapper; +import com.google.android.enterprise.connectedapps.testapp.CustomWrapper2; +import com.google.android.enterprise.connectedapps.testapp.ParcelableObject; +import com.google.android.enterprise.connectedapps.testapp.SerializableObject; +import com.google.android.enterprise.connectedapps.testapp.SimpleFuture; +import com.google.android.enterprise.connectedapps.testapp.StringWrapper; +import com.google.android.enterprise.connectedapps.testapp.configuration.TestApplication; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileType; +import com.google.android.enterprise.connectedapps.testapp.types.ProfileTestCrossProfileType_SingleSenderCanThrow; +import com.google.android.enterprise.connectedapps.testapp.types.TestCrossProfileType; +import com.google.common.base.Optional; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.ParameterizedRobolectricTestRunner; +import org.robolectric.Robolectric; +import org.robolectric.annotation.Config; +import org.robolectric.annotation.LooperMode; + +@LooperMode(LooperMode.Mode.LEGACY) +@RunWith(ParameterizedRobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.O) +public class TypesTest { + + private static final String STRING = "string"; + private static final byte BYTE = 1; + private static final Byte BYTE_BOXED = 1; + private static final short SHORT = 1; + private static final Short SHORT_BOXED = 1; + private static final int INT = 1; + private static final Integer INTEGER = 1; + private static final long LONG = 1; + private static final Long LONG_BOXED = 1L; + private static final float FLOAT = 1; + private static final Float FLOAT_BOXED = 1f; + private static final double DOUBLE = 1; + private static final Double DOUBLE_BOXED = 1d; + private static final char CHAR = 1; + private static final Character CHARACTER = 1; + private static final boolean BOOLEAN = true; + private static final Boolean BOOLEAN_BOXED = true; + private static final ParcelableObject PARCELABLE = new ParcelableObject("test"); + private static final SerializableObject SERIALIZABLE = new SerializableObject("test"); + private static final List<String> listOfString = Collections.singletonList(STRING); + private static final List<List<String>> listOfListOfString = ImmutableList.of(listOfString); + private static final List<ParcelableObject> listOfParcelable = ImmutableList.of(PARCELABLE); + private static final List<SerializableObject> listOfSerializable = ImmutableList.of(SERIALIZABLE); + private static final ImmutableMap<String, String> IMMUTABLE_MAP_STRING_TO_STRING = + ImmutableMap.of(STRING, STRING); + private static final Set<String> setOfString = ImmutableSet.of(STRING); + private static final Collection<String> collectionOfString = ImmutableList.of(STRING); + // private static final TestProto PROTO = TestProto.newBuilder().setText(STRING).build(); + // private static final List<TestProto> listOfProto = ImmutableList.of(PROTO); + private static final String[] arrayOfString = new String[] {STRING}; + private static final Collection<String[]> collectionOfStringArray = + ImmutableList.of(arrayOfString); + private static final ParcelableObject[] arrayOfParcelable = new ParcelableObject[] {PARCELABLE}; + private static final SerializableObject[] arrayOfSerializable = + new SerializableObject[] {SERIALIZABLE}; + private static final Collection<ParcelableObject[]> collectionOfParcelableArray = + ImmutableList.of(arrayOfParcelable); + private static final Collection<SerializableObject[]> collectionOfSerializableArray = + ImmutableList.of(arrayOfSerializable); + // private static final TestProto[] arrayOfProto = new TestProto[] {PROTO}; + private static final String[] emptyStringArray = new String[] {}; + private static final CustomWrapper<String> CUSTOM_WRAPPER = new CustomWrapper<>(STRING); + private static final CustomWrapper2<String> CUSTOM_WRAPPER2 = new CustomWrapper2<>(STRING); + private static final StringWrapper STRING_WRAPPER = new StringWrapper(STRING); + private static final Optional<ParcelableObject> GUAVA_OPTIONAL = Optional.of(PARCELABLE); + private static final int[] BITMAP_PIXELS = {1, 2, 3, 4, 5, 6, 7, 8, 9}; + + private final Application context = ApplicationProvider.getApplicationContext(); + // Android type can't be static due to Robolectric + private final Pair<String, Integer> pair = new Pair<>(STRING, INTEGER); + private final Bitmap bitmap = Bitmap.createBitmap(BITMAP_PIXELS, 3, 3, Bitmap.Config.ARGB_8888); + + private final TestScheduledExecutorService scheduledExecutorService = + new TestScheduledExecutorService(); + private final TestProfileConnector testProfileConnector = + TestProfileConnector.create(context, scheduledExecutorService); + private final RobolectricTestUtilities testUtilities = + new RobolectricTestUtilities(testProfileConnector, scheduledExecutorService); + + private interface SenderProvider { + ProfileTestCrossProfileType_SingleSenderCanThrow provide( + Context context, TestProfileConnector testProfileConnector); + } + + @ParameterizedRobolectricTestRunner.Parameters(name = "{0}") + public static Collection<Object[]> data() { + + SenderProvider currentProfileSenderProvider = + (Context context, TestProfileConnector testProfileConnector) -> + (ProfileTestCrossProfileType_SingleSenderCanThrow) + ProfileTestCrossProfileType.create(testProfileConnector).current(); + SenderProvider otherProfileSenderProvider = + (Context context, TestProfileConnector testProfileConnector) -> + ProfileTestCrossProfileType.create(testProfileConnector).other(); + + return Arrays.asList( + new Object[][] { + {"CurrentProfile", currentProfileSenderProvider}, + {"OtherProfile", otherProfileSenderProvider}, + }); + } + + @Before + public void setUp() { + Service profileAwareService = Robolectric.setupService(TestApplication.getService()); + testUtilities.initTests(); + IBinder binder = profileAwareService.onBind(/* intent= */ null); + testUtilities.setBinding(binder, TestProfileConnector.class.getName()); + testUtilities.createWorkUser(); + testUtilities.turnOnWorkProfile(); + testUtilities.setRunningOnPersonalProfile(); + testUtilities.setRequestsPermissions(INTERACT_ACROSS_USERS); + testUtilities.grantPermissions(INTERACT_ACROSS_USERS); + testUtilities.startConnectingAndWait(); + } + + private SenderProvider senderProvider; + + public TypesTest(String profile, SenderProvider senderProvider) { + this.senderProvider = senderProvider; + } + + @Test + public void voidMethodWithNoArguments_callsMethod() throws UnavailableProfileException { + TestCrossProfileType.voidMethodCalls = 0; + + senderProvider.provide(context, testProfileConnector).voidMethod(); + + assertThat(TestCrossProfileType.voidMethodCalls).isEqualTo(1); + } + + @Test + public void voidMethodWithArguments_callsMethod() throws UnavailableProfileException { + TestCrossProfileType.voidMethodCalls = 0; + + senderProvider.provide(context, testProfileConnector).voidMethod("argument"); + + assertThat(TestCrossProfileType.voidMethodCalls).isEqualTo(1); + } + + @Test + public void stringReturnTypeAndArgument_bothWork() throws UnavailableProfileException { + assertThat(senderProvider.provide(context, testProfileConnector).identityStringMethod(STRING)) + .isEqualTo(STRING); + } + + @Test + public void byteReturnTypeAndArgument_bothWork() throws UnavailableProfileException { + assertThat(senderProvider.provide(context, testProfileConnector).identityByteMethod(BYTE)) + .isEqualTo(BYTE); + } + + @Test + public void boxedByteReturnTypeAndArgument_bothWork() throws UnavailableProfileException { + assertThat(senderProvider.provide(context, testProfileConnector).identityByteMethod(BYTE_BOXED)) + .isEqualTo(BYTE_BOXED); + } + + @Test + public void shortReturnTypeAndArgument_bothWork() throws UnavailableProfileException { + assertThat(senderProvider.provide(context, testProfileConnector).identityShortMethod(SHORT)) + .isEqualTo(SHORT); + } + + @Test + public void boxedShortReturnTypeAndArgument_bothWork() throws UnavailableProfileException { + assertThat( + senderProvider.provide(context, testProfileConnector).identityShortMethod(SHORT_BOXED)) + .isEqualTo(SHORT_BOXED); + } + + @Test + public void intReturnTypeAndArgument_bothWork() throws UnavailableProfileException { + assertThat(senderProvider.provide(context, testProfileConnector).identityIntMethod(INT)) + .isEqualTo(INT); + } + + @Test + public void integerReturnTypeAndArgument_bothWork() throws UnavailableProfileException { + assertThat(senderProvider.provide(context, testProfileConnector).identityIntegerMethod(INTEGER)) + .isEqualTo(INTEGER); + } + + @Test + public void longReturnTypeAndArgument_bothWork() throws UnavailableProfileException { + assertThat(senderProvider.provide(context, testProfileConnector).identityLongMethod(LONG)) + .isEqualTo(LONG); + } + + @Test + public void boxedLongReturnTypeAndArgument_bothWork() throws UnavailableProfileException { + assertThat(senderProvider.provide(context, testProfileConnector).identityLongMethod(LONG_BOXED)) + .isEqualTo(LONG_BOXED); + } + + @Test + public void floatReturnTypeAndArgument_bothWork() throws UnavailableProfileException { + assertThat(senderProvider.provide(context, testProfileConnector).identityFloatMethod(FLOAT)) + .isEqualTo(FLOAT); + } + + @Test + public void boxedFloatReturnTypeAndArgument_bothWork() throws UnavailableProfileException { + assertThat( + senderProvider.provide(context, testProfileConnector).identityFloatMethod(FLOAT_BOXED)) + .isEqualTo(FLOAT_BOXED); + } + + @Test + public void doubleReturnTypeAndArgument_bothWork() throws UnavailableProfileException { + assertThat(senderProvider.provide(context, testProfileConnector).identityDoubleMethod(DOUBLE)) + .isEqualTo(DOUBLE); + } + + @Test + public void boxedDoubleReturnTypeAndArgument_bothWork() throws UnavailableProfileException { + assertThat( + senderProvider + .provide(context, testProfileConnector) + .identityDoubleMethod(DOUBLE_BOXED)) + .isEqualTo(DOUBLE_BOXED); + } + + @Test + public void charReturnTypeAndArgument_bothWork() throws UnavailableProfileException { + assertThat(senderProvider.provide(context, testProfileConnector).identityCharMethod(CHAR)) + .isEqualTo(CHAR); + } + + @Test + public void characterReturnTypeAndArgument_bothWork() throws UnavailableProfileException { + assertThat( + senderProvider + .provide(context, testProfileConnector) + .identityCharacterMethod(CHARACTER)) + .isEqualTo(CHARACTER); + } + + @Test + public void booleanReturnTypeAndArgument_bothWork() throws UnavailableProfileException { + assertThat(senderProvider.provide(context, testProfileConnector).identityBooleanMethod(BOOLEAN)) + .isEqualTo(BOOLEAN); + } + + @Test + public void boxedBooleanReturnTypeAndArgument_bothWork() throws UnavailableProfileException { + assertThat( + senderProvider + .provide(context, testProfileConnector) + .identityBooleanMethod(BOOLEAN_BOXED)) + .isEqualTo(BOOLEAN_BOXED); + } + + @Test + public void parcelableReturnTypeAndArgument_bothWork() throws UnavailableProfileException { + assertThat( + senderProvider + .provide(context, testProfileConnector) + .identityParcelableMethod(PARCELABLE)) + .isEqualTo(PARCELABLE); + } + + @Test + public void serializableReturnTypeAndArgument_bothWork() throws UnavailableProfileException { + assertThat( + senderProvider + .provide(context, testProfileConnector) + .identitySerializableObjectMethod(SERIALIZABLE)) + .isEqualTo(SERIALIZABLE); + } + + @Test + public void parcelableWrapperOfParcelableTypeAndArgument_bothWork() + throws UnavailableProfileException { + assertThat( + senderProvider + .provide(context, testProfileConnector) + .identityParcelableWrapperOfParcelableMethod(listOfParcelable)) + .isEqualTo(listOfParcelable); + } + + @Test + public void parcelableWrapperOfSerializableTypeAndArgument_bothWork() + throws UnavailableProfileException { + assertThat( + senderProvider + .provide(context, testProfileConnector) + .identityParcelableWrapperOfSerializableMethod(listOfSerializable)) + .isEqualTo(listOfSerializable); + } + + @Test + public void parcelableWrapperOfParcelableWrapperTypeAndArgument_bothWork() + throws UnavailableProfileException { + assertThat( + senderProvider + .provide(context, testProfileConnector) + .identityParcelableWrapperOfParcelableWrapperMethod(listOfListOfString)) + .isEqualTo(listOfListOfString); + } + + @Test + public void listReturnTypeAndArgument_bothWork() throws UnavailableProfileException { + assertThat( + senderProvider.provide(context, testProfileConnector).identityListMethod(listOfString)) + .isEqualTo(listOfString); + } + + @Test + public void mapReturnTypeAndArgument_bothWork() throws UnavailableProfileException { + assertThat( + senderProvider + .provide(context, testProfileConnector) + .identityMapMethod(IMMUTABLE_MAP_STRING_TO_STRING)) + .isEqualTo(IMMUTABLE_MAP_STRING_TO_STRING); + } + + @Test + public void immutableMapReturnTypeAndArgument_bothWork() throws UnavailableProfileException { + assertThat( + senderProvider + .provide(context, testProfileConnector) + .identityImmutableMapMethod(IMMUTABLE_MAP_STRING_TO_STRING)) + .isEqualTo(IMMUTABLE_MAP_STRING_TO_STRING); + } + + @Test + public void setReturnTypeAndArgument_bothWork() throws UnavailableProfileException { + assertThat(senderProvider.provide(context, testProfileConnector).identitySetMethod(setOfString)) + .isEqualTo(setOfString); + } + + @Test + public void collectionReturnTypeAndArgument_bothWork() throws UnavailableProfileException { + assertThat( + senderProvider + .provide(context, testProfileConnector) + .identityCollectionMethod(collectionOfString)) + .containsExactlyElementsIn(collectionOfString); + } + + @Test + public void arrayReturnTypeAndArgument_bothWork() throws UnavailableProfileException { + assertThat( + senderProvider + .provide(context, testProfileConnector) + .identityStringArrayMethod(arrayOfString)) + .asList() + .containsExactlyElementsIn(arrayOfString); + } + + @Test + public void collectionOfArrayReturnTypeAndArgument_bothWork() throws UnavailableProfileException { + ProfileTestCrossProfileType_SingleSenderCanThrow sender = + senderProvider.provide(context, testProfileConnector); + + List<String[]> originalAsList = new ArrayList<>(collectionOfStringArray); + List<String[]> resultAsList = + new ArrayList<>(sender.identityCollectionOfStringArrayMethod(collectionOfStringArray)); + + assertThat(sender.identityCollectionOfStringArrayMethod(collectionOfStringArray)) + .hasSize(collectionOfStringArray.size()); + for (int i = 0; i < collectionOfStringArray.size(); i++) { + assertThat(resultAsList.get(i)).asList().containsExactlyElementsIn(originalAsList.get(i)); + } + } + + @Test + public void parcelableArrayReturnTypeAndArgument_bothWork() throws UnavailableProfileException { + assertThat( + senderProvider + .provide(context, testProfileConnector) + .identityParcelableObjectArrayMethod(arrayOfParcelable)) + .asList() + .containsExactlyElementsIn(arrayOfParcelable); + } + + @Test + public void serializableArrayReturnTypeAndArgument_bothWork() throws UnavailableProfileException { + assertThat( + senderProvider + .provide(context, testProfileConnector) + .identitySerializableObjectArrayMethod(arrayOfSerializable)) + .asList() + .containsExactlyElementsIn(arrayOfSerializable); + } + + @Test + public void collectionOfParcelableArrayReturnTypeAndArgument_bothWork() + throws UnavailableProfileException { + ProfileTestCrossProfileType_SingleSenderCanThrow sender = + senderProvider.provide(context, testProfileConnector); + + List<ParcelableObject[]> originalAsList = new ArrayList<>(collectionOfParcelableArray); + List<ParcelableObject[]> resultAsList = + new ArrayList<>( + sender.identityCollectionOfParcelableObjectArrayMethod(collectionOfParcelableArray)); + + assertThat(resultAsList).hasSize(collectionOfParcelableArray.size()); + for (int i = 0; i < collectionOfParcelableArray.size(); i++) { + assertThat(resultAsList.get(i)).asList().containsExactlyElementsIn(originalAsList.get(i)); + } + } + + @Test + public void collectionOfSerializableArrayReturnTypeAndArgument_bothWork() + throws UnavailableProfileException { + ProfileTestCrossProfileType_SingleSenderCanThrow sender = + senderProvider.provide(context, testProfileConnector); + + List<SerializableObject[]> originalAsList = new ArrayList<>(collectionOfSerializableArray); + List<SerializableObject[]> resultAsList = + new ArrayList<>( + sender.identityCollectionOfSerializableObjectArrayMethod( + collectionOfSerializableArray)); + + assertThat(resultAsList).hasSize(collectionOfSerializableArray.size()); + for (int i = 0; i < collectionOfSerializableArray.size(); i++) { + assertThat(resultAsList.get(i)).asList().containsExactlyElementsIn(originalAsList.get(i)); + } + } + + @Test + public void pairReturnTypeAndArgument_bothWork() throws UnavailableProfileException { + assertThat(senderProvider.provide(context, testProfileConnector).identityPairMethod(pair)) + .isEqualTo(pair); + } + + @Test + public void optionalReturnTypeAndArgument_bothWork() throws UnavailableProfileException { + assertThat( + senderProvider + .provide(context, testProfileConnector) + .identityGuavaOptionalMethod(GUAVA_OPTIONAL)) + .isEqualTo(GUAVA_OPTIONAL); + } + + // TODO: Disabled because use of Optional fails lint check. Re-enable when this is disabled. + // @Test + // public void optionalReturnTypeAndArgument_bothWork() throws UnavailableProfileException { + // assertThat(senderProvider.provide(context, + // testProfileConnector).identityOptionalMethod(OPTIONAL_OF_STRING)).isEqualTo(OPTIONAL_OF_STRING); + // } + + @Test + public void voidObjectReturnType_works() throws UnavailableProfileException { + TestCrossProfileType.voidMethodCalls = 0; + + assertThat(senderProvider.provide(context, testProfileConnector).identityVoidMethod()) + .isEqualTo(null); + assertThat(TestCrossProfileType.voidMethodCalls).isEqualTo(1); + } + + @Test + public void methodWhichReturnsNull_works() throws UnavailableProfileException { + assertThat(senderProvider.provide(context, testProfileConnector).getNull()).isNull(); + } + + @Test + public void methodWhichReturnsNullCollection_works() throws UnavailableProfileException { + assertThat(senderProvider.provide(context, testProfileConnector).getNullCollection()).isNull(); + } + + @Test + public void methodWhichReturnsNullList_works() throws UnavailableProfileException { + assertThat(senderProvider.provide(context, testProfileConnector).getNullList()).isNull(); + } + + @Test + public void methodWhichReturnsNullMap_works() throws UnavailableProfileException { + assertThat(senderProvider.provide(context, testProfileConnector).getNullMap()).isNull(); + } + + // @Test + // public void methodWhichReturnsNullOptional_works() throws UnavailableProfileException { + // assertThat(senderProvider.provide(context, testProfileConnector).getNullOptional()).isNull(); + // } + + @Test + public void methodWhichReturnsNullSet_works() throws UnavailableProfileException { + assertThat(senderProvider.provide(context, testProfileConnector).getNullSet()).isNull(); + } + + // @Test + // public void methodWhichReturnsNullProto_works() throws UnavailableProfileException { + // assertThat(senderProvider.provide(context, testProfileConnector).getNullProto()).isNull(); + // } + + @Test + public void emptyArray_works() throws UnavailableProfileException { + assertThat( + senderProvider + .provide(context, testProfileConnector) + .identityStringArrayMethod(emptyStringArray)) + .asList() + .containsExactlyElementsIn(emptyStringArray); + } + + @Test + public void nullArray_works() throws UnavailableProfileException { + assertThat( + senderProvider.provide(context, testProfileConnector).identityStringArrayMethod(null)) + .isNull(); + } + + @Test + public void customParcelableWrapperDefinedOnTypeReturnTypeAndArgument_bothWork() + throws UnavailableProfileException { + assertThat( + senderProvider + .provide(context, testProfileConnector) + .identityCustomWrapper2Method(CUSTOM_WRAPPER2)) + .isEqualTo(CUSTOM_WRAPPER2); + } + + @Test + public void customParcelableWrapperReturnTypeAndArgument_bothWork() + throws UnavailableProfileException { + assertThat( + senderProvider + .provide(context, testProfileConnector) + .identityCustomWrapperMethod(CUSTOM_WRAPPER)) + .isEqualTo(CUSTOM_WRAPPER); + } + + @Test + public void customParcelableWrapperFutureReturnType_works() + throws ExecutionException, InterruptedException { + assertThat( + senderProvider + .provide(context, testProfileConnector) + .listenableFutureIdentityCustomWrapperMethod(CUSTOM_WRAPPER) + .get()) + .isEqualTo(CUSTOM_WRAPPER); + } + + @Test + public void customParcelableWrapperAsyncMethod_works() { + TestCustomWrapperCallbackListenerImpl callbackListener = + new TestCustomWrapperCallbackListenerImpl(); + TestExceptionCallbackListener exceptionListener = new TestExceptionCallbackListener(); + + senderProvider + .provide(context, testProfileConnector) + .asyncIdentityCustomWrapperMethod(CUSTOM_WRAPPER, callbackListener, exceptionListener); + + assertThat(callbackListener.customWrapperCallbackValue).isEqualTo(CUSTOM_WRAPPER); + } + + @Test + public void customFutureWrapper_works() { + SimpleFuture<String> future = + senderProvider + .provide(context, testProfileConnector) + .simpleFutureIdentityStringMethodWithNonBlockingDelay(STRING, /* secondsDelay= */ 5); + testUtilities.advanceTimeBySeconds(10); + + assertThat(future.get()).isEqualTo(STRING); + } + + @Test + public void parcelableWrapperWithoutGenericReturnTypeAndArgument_bothWork() + throws UnavailableProfileException { + assertThat( + senderProvider + .provide(context, testProfileConnector) + .identityStringWrapperMethod(STRING_WRAPPER)) + .isEqualTo(STRING_WRAPPER); + } + + @Test + public void bitmapReturnTypeAndArgument_bothWork() throws UnavailableProfileException { + Bitmap returnBitmap = + senderProvider.provide(context, testProfileConnector).identityBitmapMethod(bitmap); + + assertThat(returnBitmap.getConfig()).isEqualTo(bitmap.getConfig()); + assertThat(returnBitmap.getWidth()).isEqualTo(bitmap.getWidth()); + assertThat(returnBitmap.getHeight()).isEqualTo(bitmap.getHeight()); + assertThat(getBitmapPixels(returnBitmap)).isEqualTo(BITMAP_PIXELS); + } + + @Test + public void nullBitmap_works() throws UnavailableProfileException { + assertThat(senderProvider.provide(context, testProfileConnector).identityBitmapMethod(null)) + .isNull(); + } + + private static int[] getBitmapPixels(Bitmap bitmap) { + int[] pixels = new int[bitmap.getHeight() * bitmap.getWidth()]; + bitmap.getPixels(pixels, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight()); + return pixels; + } + + @Test + public void contextArgument_works() throws UnavailableProfileException { + assertThat(senderProvider.provide(context, testProfileConnector).isContextArgumentPassed()) + .isTrue(); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/testing/AbstractFakeProfileConnectorTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/testing/AbstractFakeProfileConnectorTest.java new file mode 100644 index 0000000..f42d89b --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/testing/AbstractFakeProfileConnectorTest.java @@ -0,0 +1,407 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testing; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import android.content.Context; +import android.os.Build.VERSION_CODES; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.ConnectedAppsUtils; +import com.google.android.enterprise.connectedapps.TestAvailabilityListener; +import com.google.android.enterprise.connectedapps.TestConnectionListener; +import com.google.android.enterprise.connectedapps.annotations.CustomProfileConnector.ProfileType; +import com.google.android.enterprise.connectedapps.exceptions.UnavailableProfileException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.O) +public class AbstractFakeProfileConnectorTest { + static class FakeProfileConnector extends AbstractFakeProfileConnector { + FakeProfileConnector(Context context, ProfileType primaryProfileType) { + super(context, primaryProfileType); + } + } + + private final Context context = ApplicationProvider.getApplicationContext(); + private final FakeProfileConnector fakeProfileConnector = + new FakeProfileConnector(context, /* primaryProfileType= */ ProfileType.NONE); + private final TestAvailabilityListener availabilityListener = new TestAvailabilityListener(); + private final TestConnectionListener connectionListener = new TestConnectionListener(); + + @Test + public void startConnecting_connectionIsAvailable_isConnected() { + fakeProfileConnector.turnOnWorkProfile(); + + fakeProfileConnector.startConnecting(); + + assertThat(fakeProfileConnector.isConnected()).isTrue(); + } + + @Test + public void startConnecting_connectionIsAvailable_notifiesConnectionChanged() { + fakeProfileConnector.turnOnWorkProfile(); + fakeProfileConnector.registerConnectionListener(connectionListener); + + fakeProfileConnector.startConnecting(); + + assertThat(connectionListener.connectionChangedCount()).isEqualTo(1); + } + + @Test + public void startConnecting_unregisteredConnectionListener_doesNotNotifyConnectionChanged() { + fakeProfileConnector.turnOnWorkProfile(); + fakeProfileConnector.registerConnectionListener(connectionListener); + fakeProfileConnector.unregisterConnectionListener(connectionListener); + + fakeProfileConnector.startConnecting(); + + assertThat(connectionListener.connectionChangedCount()).isEqualTo(0); + } + + @Test + public void startConnecting_connectionIsNotAvailable_doesNotNotifyOfConnectionChanged() { + fakeProfileConnector.removeWorkProfile(); + fakeProfileConnector.registerConnectionListener(connectionListener); + + fakeProfileConnector.startConnecting(); + + assertThat(connectionListener.connectionChangedCount()).isEqualTo(0); + } + + @Test + public void connect_connectionIsAvailable_isConnected() throws Exception { + fakeProfileConnector.turnOnWorkProfile(); + + fakeProfileConnector.connect(); + + assertThat(fakeProfileConnector.isConnected()).isTrue(); + } + + @Test + public void connect_connectionIsAvailable_notifiesConnectionChanged() throws Exception { + fakeProfileConnector.turnOnWorkProfile(); + fakeProfileConnector.registerConnectionListener(connectionListener); + + fakeProfileConnector.connect(); + + assertThat(connectionListener.connectionChangedCount()).isEqualTo(1); + } + + @Test + public void connect_unregisteredConnectionListener_doesNotNotifyConnectionChanged() + throws Exception { + fakeProfileConnector.turnOnWorkProfile(); + fakeProfileConnector.registerConnectionListener(connectionListener); + fakeProfileConnector.unregisterConnectionListener(connectionListener); + + fakeProfileConnector.connect(); + + assertThat(connectionListener.connectionChangedCount()).isEqualTo(0); + } + + @Test + public void connect_connectionIsNotAvailable_throwsUnavailableProfileException() { + fakeProfileConnector.removeWorkProfile(); + fakeProfileConnector.registerConnectionListener(connectionListener); + + assertThrows(UnavailableProfileException.class, fakeProfileConnector::connect); + } + + @Test + public void turnOnWorkProfile_workProfileWasOff_notifiesAvailabilityChange() { + fakeProfileConnector.registerAvailabilityListener(availabilityListener); + + fakeProfileConnector.turnOnWorkProfile(); + + assertThat(availabilityListener.availabilityChangedCount()).isEqualTo(1); + } + + @Test + public void turnOnWorkProfile_workProfileWasOn_doesNotNotifyAvailabilityChange() { + fakeProfileConnector.turnOnWorkProfile(); + fakeProfileConnector.registerAvailabilityListener(availabilityListener); + + fakeProfileConnector.turnOnWorkProfile(); + + assertThat(availabilityListener.availabilityChangedCount()).isEqualTo(0); + } + + @Test + public void turnOffWorkProfile_workProfileWasOn_notifiesAvailabilityChange() { + fakeProfileConnector.turnOnWorkProfile(); + fakeProfileConnector.registerAvailabilityListener(availabilityListener); + + fakeProfileConnector.turnOffWorkProfile(); + + assertThat(availabilityListener.availabilityChangedCount()).isEqualTo(1); + } + + @Test + public void turnOffWorkProfile_workProfileWasOff_doesNotNotifyAvailabilityChange() { + fakeProfileConnector.turnOffWorkProfile(); + fakeProfileConnector.registerAvailabilityListener(availabilityListener); + + fakeProfileConnector.turnOffWorkProfile(); + + assertThat(availabilityListener.availabilityChangedCount()).isEqualTo(0); + } + + @Test + public void turnOffWorkProfile_wasConnected_notifiesConnectionChange() throws Exception { + fakeProfileConnector.turnOnWorkProfile(); + fakeProfileConnector.connect(); + fakeProfileConnector.registerConnectionListener(connectionListener); + + fakeProfileConnector.turnOffWorkProfile(); + + assertThat(connectionListener.connectionChangedCount()).isEqualTo(1); + } + + @Test + public void setRunningOnProfile_setsRunningOnProfile() { + fakeProfileConnector.setRunningOnProfile(ProfileType.WORK); + + assertThat(fakeProfileConnector.runningOnProfile()).isEqualTo(ProfileType.WORK); + } + + @Test + public void setRunningOnWorkProfile_startsWorkProfile() { + fakeProfileConnector.setRunningOnProfile(ProfileType.WORK); + fakeProfileConnector.setRunningOnProfile(ProfileType.PERSONAL); + + assertThat(fakeProfileConnector.isAvailable()).isTrue(); + } + + @Test + public void removeWorkProfile_workProfileBecomesUnavailable() { + fakeProfileConnector.turnOnWorkProfile(); + fakeProfileConnector.removeWorkProfile(); + + assertThat(fakeProfileConnector.isAvailable()).isFalse(); + } + + @Test + public void isConnected_isConnected_returnsTrue() throws Exception { + fakeProfileConnector.turnOnWorkProfile(); + + fakeProfileConnector.connect(); + + assertThat(fakeProfileConnector.isConnected()).isTrue(); + } + + @Test + public void isConnected_isNotConnected_returnsFalse() { + fakeProfileConnector.turnOnWorkProfile(); + + fakeProfileConnector.disconnect(); + + assertThat(fakeProfileConnector.isConnected()).isFalse(); + } + + @Test + public void getCurrentProfile_getOtherProfile_areDifferent() { + ConnectedAppsUtils utils = fakeProfileConnector.utils(); + assertThat(utils.getCurrentProfile()).isNotEqualTo(utils.getOtherProfile()); + } + + @Test + public void getWorkProfile_runningOnWorkProfile_returnsCurrent() { + ConnectedAppsUtils utils = fakeProfileConnector.utils(); + fakeProfileConnector.setRunningOnProfile(ProfileType.WORK); + + assertThat(utils.getWorkProfile()).isEqualTo(utils.getCurrentProfile()); + } + + @Test + public void getWorkProfile_runningOnPersonalProfile_returnsOther() { + ConnectedAppsUtils utils = fakeProfileConnector.utils(); + fakeProfileConnector.setRunningOnProfile(ProfileType.PERSONAL); + + assertThat(utils.getWorkProfile()).isEqualTo(utils.getOtherProfile()); + } + + @Test + public void getPersonalProfile_runningOnPersonalProfile_returnsCurrent() { + ConnectedAppsUtils utils = fakeProfileConnector.utils(); + fakeProfileConnector.setRunningOnProfile(ProfileType.PERSONAL); + + assertThat(utils.getPersonalProfile()).isEqualTo(utils.getCurrentProfile()); + } + + @Test + public void getPersonalProfile_runningOnWorkProfile_returnsOther() { + ConnectedAppsUtils utils = fakeProfileConnector.utils(); + fakeProfileConnector.setRunningOnProfile(ProfileType.WORK); + + assertThat(utils.getPersonalProfile()).isEqualTo(utils.getOtherProfile()); + } + + @Test + public void getPrimaryProfile_noPrimaryProfileSet_throwsIllegalStateException() { + FakeProfileConnector fakeProfileConnector = + new FakeProfileConnector(context, /* primaryProfileType= */ ProfileType.NONE); + ConnectedAppsUtils utils = fakeProfileConnector.utils(); + + assertThrows(IllegalStateException.class, () -> utils.getPrimaryProfile()); + } + + @Test + public void getSecondaryProfile_noPrimaryProfileSet_throwsIllegalStateException() { + FakeProfileConnector fakeProfileConnector = + new FakeProfileConnector(context, /* primaryProfileType= */ ProfileType.NONE); + ConnectedAppsUtils utils = fakeProfileConnector.utils(); + + assertThrows(IllegalStateException.class, () -> utils.getSecondaryProfile()); + } + + @Test + public void getPrimaryProfile_primaryProfileSetToWork_returnsWork() { + FakeProfileConnector fakeProfileConnector = + new FakeProfileConnector(context, /* primaryProfileType= */ ProfileType.WORK); + ConnectedAppsUtils utils = fakeProfileConnector.utils(); + + assertThat(utils.getPrimaryProfile()).isEqualTo(utils.getWorkProfile()); + } + + @Test + public void getPrimaryProfile_primaryProfileSetToPersonal_returnsPersonal() { + FakeProfileConnector fakeProfileConnector = + new FakeProfileConnector(context, /* primaryProfileType= */ ProfileType.PERSONAL); + ConnectedAppsUtils utils = fakeProfileConnector.utils(); + + assertThat(utils.getPrimaryProfile()).isEqualTo(utils.getPersonalProfile()); + } + + @Test + public void getSecondaryProfile_primaryProfileSetToWork_returnsPersonal() { + FakeProfileConnector fakeProfileConnector = + new FakeProfileConnector(context, /* primaryProfileType= */ ProfileType.WORK); + ConnectedAppsUtils utils = fakeProfileConnector.utils(); + + assertThat(utils.getSecondaryProfile()).isEqualTo(utils.getPersonalProfile()); + } + + @Test + public void getSecondaryProfile_primaryProfileSetToPersonal_returnsWork() { + FakeProfileConnector fakeProfileConnector = + new FakeProfileConnector(context, /* primaryProfileType= */ ProfileType.PERSONAL); + ConnectedAppsUtils utils = fakeProfileConnector.utils(); + + assertThat(utils.getSecondaryProfile()).isEqualTo(utils.getWorkProfile()); + } + + @Test + public void runningOnWork_runningOnWork_returnsTrue() { + fakeProfileConnector.setRunningOnProfile(ProfileType.WORK); + + assertThat(fakeProfileConnector.utils().runningOnWork()).isTrue(); + } + + @Test + public void runningOnWork_runningOnPersonal_returnsFalse() { + fakeProfileConnector.setRunningOnProfile(ProfileType.PERSONAL); + + assertThat(fakeProfileConnector.utils().runningOnWork()).isFalse(); + } + + @Test + public void runningOnPersonal_runningOnPersonal_returnsTrue() { + fakeProfileConnector.setRunningOnProfile(ProfileType.PERSONAL); + + assertThat(fakeProfileConnector.utils().runningOnPersonal()).isTrue(); + } + + @Test + public void runningOnPersonal_runningOnWork_returnsFalse() { + fakeProfileConnector.setRunningOnProfile(ProfileType.WORK); + + assertThat(fakeProfileConnector.utils().runningOnPersonal()).isFalse(); + } + + @Test + public void canMakeCrossProfileCalls_defaultsToTrue() { + assertThat(fakeProfileConnector.permissions().canMakeCrossProfileCalls()).isTrue(); + } + + @Test + public void canMakeCrossProfileCalls_setToFalse_returnsFalse() { + fakeProfileConnector.setHasPermissionToMakeCrossProfileCalls(false); + + assertThat(fakeProfileConnector.permissions().canMakeCrossProfileCalls()).isFalse(); + } + + @Test + public void canMakeCrossProfileCalls_setToTrue_returnsTrue() { + fakeProfileConnector.setHasPermissionToMakeCrossProfileCalls(false); + fakeProfileConnector.setHasPermissionToMakeCrossProfileCalls(true); + + assertThat(fakeProfileConnector.permissions().canMakeCrossProfileCalls()).isTrue(); + } + + @Test + public void isManuallyManagingConnection_returnsFalse() { + assertThat(fakeProfileConnector.isManuallyManagingConnection()).isFalse(); + } + + @Test + public void isManuallyManagingConnection_hasStartedManuallyConnecting_returnsTrue() { + fakeProfileConnector.startConnecting(); + + assertThat(fakeProfileConnector.isManuallyManagingConnection()).isTrue(); + } + + @Test + public void isManuallyManagingConnection_hasManuallyConnected_returnsTrue() throws Exception { + fakeProfileConnector.turnOnWorkProfile(); + fakeProfileConnector.connect(); + + assertThat(fakeProfileConnector.isManuallyManagingConnection()).isTrue(); + } + + @Test + public void isManuallyManagingConnection_hasCalledStopManualConnectionManagement_returnsFalse() { + fakeProfileConnector.startConnecting(); + + fakeProfileConnector.stopManualConnectionManagement(); + + assertThat(fakeProfileConnector.isManuallyManagingConnection()).isFalse(); + } + + @Test + public void timeoutConnection_isManuallyManagingConnection_doesNotDisconnect() throws Exception { + fakeProfileConnector.turnOnWorkProfile(); + fakeProfileConnector.connect(); + + fakeProfileConnector.timeoutConnection(); + + assertThat(fakeProfileConnector.isConnected()).isTrue(); + } + + @Test + public void timeoutConnection_isNotManuallyManagingConnection_disconnects() { + fakeProfileConnector.turnOnWorkProfile(); + fakeProfileConnector.stopManualConnectionManagement(); + + fakeProfileConnector.timeoutConnection(); + + assertThat(fakeProfileConnector.isConnected()).isFalse(); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/testing/FakeCrossProfileTypeTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/testing/FakeCrossProfileTypeTest.java new file mode 100644 index 0000000..3599dd6 --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/testing/FakeCrossProfileTypeTest.java @@ -0,0 +1,821 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testing; + +import static com.google.android.enterprise.connectedapps.SharedTestUtilities.assertFutureHasException; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.fail; + +import android.content.Context; +import android.os.Build.VERSION_CODES; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.NonSimpleCallbackListenerImpl; +import com.google.android.enterprise.connectedapps.Profile; +import com.google.android.enterprise.connectedapps.TestBooleanCallbackListenerImpl; +import com.google.android.enterprise.connectedapps.TestBooleanCallbackListenerMultiImpl; +import com.google.android.enterprise.connectedapps.TestConnectionListener; +import com.google.android.enterprise.connectedapps.TestExceptionCallbackListener; +import com.google.android.enterprise.connectedapps.TestStringCallbackListenerImpl; +import com.google.android.enterprise.connectedapps.TestVoidCallbackListenerImpl; +import com.google.android.enterprise.connectedapps.annotations.CustomProfileConnector.ProfileType; +import com.google.android.enterprise.connectedapps.exceptions.ProfileRuntimeException; +import com.google.android.enterprise.connectedapps.exceptions.UnavailableProfileException; +import com.google.android.enterprise.connectedapps.testapp.CustomRuntimeException; +import com.google.android.enterprise.connectedapps.testapp.configuration.TestApplication; +import com.google.android.enterprise.connectedapps.testapp.connector.FakeTestProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.types.FakeProfileTestCrossProfileType; +import com.google.android.enterprise.connectedapps.testapp.types.TestCrossProfileType; +import com.google.android.enterprise.connectedapps.testing.annotations.CrossProfileTest; +import com.google.common.util.concurrent.ListenableFuture; +import java.io.IOException; +import java.sql.SQLException; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@CrossProfileTest(configuration = TestApplication.class) +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.O) +public class FakeCrossProfileTypeTest { + + private static final String STRING = "String"; + private static final String STRING2 = "String2"; + + private final Context context = ApplicationProvider.getApplicationContext(); + private final FakeTestProfileConnector connector = new FakeTestProfileConnector(context); + private final TestCrossProfileType personal = new TestCrossProfileType(); + private final TestCrossProfileType work = new TestCrossProfileType(); + + private final TestVoidCallbackListenerImpl voidCallbackListener = + new TestVoidCallbackListenerImpl(); + private final TestExceptionCallbackListener exceptionCallbackListener = + new TestExceptionCallbackListener(); + private final TestStringCallbackListenerImpl stringCallbackListener = + new TestStringCallbackListenerImpl(); + private final TestBooleanCallbackListenerImpl booleanCallbackListener = + new TestBooleanCallbackListenerImpl(); + private final TestBooleanCallbackListenerMultiImpl booleanMultiCallbackListener = + new TestBooleanCallbackListenerMultiImpl(); + private final NonSimpleCallbackListenerImpl nonSimpleCallbackListener = + new NonSimpleCallbackListenerImpl(); + private final TestConnectionListener connectionListener = new TestConnectionListener(); + + FakeProfileTestCrossProfileType fakeCrossProfileType = + FakeProfileTestCrossProfileType.builder() + .personal(personal) + .work(work) + .connector(connector) + .build(); + + @Before + public void setUp() { + connector.setRunningOnProfile(ProfileType.PERSONAL); + connector.turnOnWorkProfile(); + connector.startConnecting(); + connector.registerConnectionListener(connectionListener); + } + + @Test + public void personal_callsOnPersonal() throws UnavailableProfileException { + fakeCrossProfileType.personal().voidMethod(); + + assertThat(personal.voidMethodInstanceCalls).isEqualTo(1); + } + + @Test + public void personal_doesNotCallOnWork() throws UnavailableProfileException { + fakeCrossProfileType.personal().voidMethod(); + + assertThat(work.voidMethodInstanceCalls).isEqualTo(0); + } + + @Test + public void work_callsOnWork() throws UnavailableProfileException { + fakeCrossProfileType.work().voidMethod(); + + assertThat(work.voidMethodInstanceCalls).isEqualTo(1); + } + + @Test + public void work_doesNotCallOnPersonal() throws UnavailableProfileException { + fakeCrossProfileType.work().voidMethod(); + + assertThat(personal.voidMethodInstanceCalls).isEqualTo(0); + } + + @Test + public void current_runningOnPersonal_callsPersonal() { + connector.setRunningOnProfile(ProfileType.PERSONAL); + fakeCrossProfileType.current().voidMethod(); + + assertThat(personal.voidMethodInstanceCalls).isEqualTo(1); + } + + @Test + public void current_runningOnWork_callsWork() { + connector.setRunningOnProfile(ProfileType.WORK); + fakeCrossProfileType.current().voidMethod(); + + assertThat(work.voidMethodInstanceCalls).isEqualTo(1); + } + + @Test + public void other_runningOnPersonal_callsWork() throws UnavailableProfileException { + connector.setRunningOnProfile(ProfileType.PERSONAL); + fakeCrossProfileType.other().voidMethod(); + + assertThat(work.voidMethodInstanceCalls).isEqualTo(1); + } + + @Test + public void other_runningOnWork_callsPersonal() throws UnavailableProfileException { + connector.setRunningOnProfile(ProfileType.WORK); + fakeCrossProfileType.other().voidMethod(); + + assertThat(personal.voidMethodInstanceCalls).isEqualTo(1); + } + + @Test + public void both_callsBoth() { + fakeCrossProfileType.both().voidMethod(); + + assertThat(personal.voidMethodInstanceCalls).isEqualTo(1); + assertThat(work.voidMethodInstanceCalls).isEqualTo(1); + } + + @Test + public void primary_callsPrimary() throws UnavailableProfileException { + // Work is primary for TestProfileConnector + fakeCrossProfileType.primary().voidMethod(); + + assertThat(work.voidMethodInstanceCalls).isEqualTo(1); + } + + @Test + public void secondary_callsSecondary() throws UnavailableProfileException { + // Work is primary for TestProfileConnector + fakeCrossProfileType.secondary().voidMethod(); + + assertThat(personal.voidMethodInstanceCalls).isEqualTo(1); + } + + @Test + public void suppliers_runningOnPrimary_callsPrimaryAndSecondary() { + // Work is primary for TestProfileConnector + connector.setRunningOnProfile(ProfileType.WORK); + + fakeCrossProfileType.suppliers().voidMethod(); + + assertThat(personal.voidMethodInstanceCalls).isEqualTo(1); + assertThat(work.voidMethodInstanceCalls).isEqualTo(1); + } + + @Test + public void suppliers_runningOnSecondary_onlyCallsSecondary() { + // Work is primary for TestProfileConnector + connector.setRunningOnProfile(ProfileType.PERSONAL); + + fakeCrossProfileType.suppliers().voidMethod(); + + assertThat(personal.voidMethodInstanceCalls).isEqualTo(1); + assertThat(work.voidMethodInstanceCalls).isEqualTo(0); + } + + @Test + public void profile_specifiesCurrent_callsCurrent() throws UnavailableProfileException { + connector.setRunningOnProfile(ProfileType.PERSONAL); + + fakeCrossProfileType.profile(connector.utils().getCurrentProfile()).voidMethod(); + + assertThat(personal.voidMethodInstanceCalls).isEqualTo(1); + } + + @Test + public void profile_specifiesOther_callsOther() throws UnavailableProfileException { + connector.setRunningOnProfile(ProfileType.PERSONAL); + + fakeCrossProfileType.profile(connector.utils().getOtherProfile()).voidMethod(); + + assertThat(work.voidMethodInstanceCalls).isEqualTo(1); + } + + @Test + public void profile_specifiesPersonal_callsPersonal() throws UnavailableProfileException { + fakeCrossProfileType.profile(connector.utils().getPersonalProfile()).voidMethod(); + + assertThat(personal.voidMethodInstanceCalls).isEqualTo(1); + } + + @Test + public void profile_specifiesWork_callsWork() throws UnavailableProfileException { + fakeCrossProfileType.profile(connector.utils().getWorkProfile()).voidMethod(); + + assertThat(work.voidMethodInstanceCalls).isEqualTo(1); + } + + @Test + public void profile_specifiesPrimary_callsPrimary() throws UnavailableProfileException { + // Work is primary for TestProfileConnector + fakeCrossProfileType.profile(connector.utils().getPrimaryProfile()).voidMethod(); + + assertThat(work.voidMethodInstanceCalls).isEqualTo(1); + } + + @Test + public void profile_specifiesSecondary_callsSecondary() throws UnavailableProfileException { + // Work is primary for TestProfileConnector + fakeCrossProfileType.profile(connector.utils().getSecondaryProfile()).voidMethod(); + + assertThat(personal.voidMethodInstanceCalls).isEqualTo(1); + } + + @Test + public void profiles_callsSpecifiedProfiles() { + fakeCrossProfileType + .profiles(connector.utils().getPersonalProfile(), connector.utils().getWorkProfile()) + .voidMethod(); + + assertThat(personal.voidMethodInstanceCalls).isEqualTo(1); + assertThat(work.voidMethodInstanceCalls).isEqualTo(1); + } + + @Test + public void build_noPersonalSpecified_throwsIllegalStateException() { + assertThrows( + IllegalStateException.class, + () -> FakeProfileTestCrossProfileType.builder().connector(connector).work(work).build()); + } + + @Test + public void build_noWorkSpecified_throwsIllegalStateException() { + assertThrows( + IllegalStateException.class, + () -> + FakeProfileTestCrossProfileType.builder() + .connector(connector) + .personal(personal) + .build()); + } + + @Test + public void build_noConnectorSpecified_throwsIllegalStateException() { + assertThrows( + IllegalStateException.class, + () -> FakeProfileTestCrossProfileType.builder().personal(personal).work(work).build()); + } + + @Test + public void blockingCall_workProfileNotConnected_throwsUnavailableProfileException() { + connector.disconnect(); + + assertThrows(UnavailableProfileException.class, () -> fakeCrossProfileType.work().voidMethod()); + } + + @Test + public void blockingCall_notManuallyManagingConnection_throwsUnavailableProfileException() + throws Exception { + connector.stopManualConnectionManagement(); + connector.turnOnWorkProfile(); + fakeCrossProfileType.other().listenableFutureVoidMethod().get(); // Force connection + + assertThrows( + UnavailableProfileException.class, () -> fakeCrossProfileType.other().voidMethod()); + } + + @Test + public void asyncCall_workProfileAvailableButNotConnected_works() { + connector.turnOnWorkProfile(); + connector.disconnect(); + + fakeCrossProfileType.work().asyncVoidMethod(voidCallbackListener, exceptionCallbackListener); + + assertThat(voidCallbackListener.callbackMethodCalls).isEqualTo(1); + assertThat(exceptionCallbackListener.lastException()).isNull(); + } + + @Test + public void asyncCall_notConnected_connects() { + connector.turnOnWorkProfile(); + connector.disconnect(); + connectionListener.resetConnectionChangedCount(); + + fakeCrossProfileType.work().asyncVoidMethod(voidCallbackListener, exceptionCallbackListener); + + assertThat(connectionListener.connectionChangedCount()).isEqualTo(1); + assertThat(connector.isConnected()).isTrue(); + } + + @Test + public void asyncCall_notConnected_doesNotStartManualConnectionManagement() { + connector.turnOnWorkProfile(); + connector.stopManualConnectionManagement(); + connector.disconnect(); + + fakeCrossProfileType.work().asyncVoidMethod(voidCallbackListener, exceptionCallbackListener); + + assertThat(connector.isManuallyManagingConnection()).isFalse(); + } + + @Test + public void asyncCall_workProfileUnavailable_callsWithUnavailableProfileException() { + connector.removeWorkProfile(); + + fakeCrossProfileType.work().asyncVoidMethod(voidCallbackListener, exceptionCallbackListener); + + assertThat(voidCallbackListener.callbackMethodCalls).isEqualTo(0); + assertThat(exceptionCallbackListener.lastException()) + .isInstanceOf(UnavailableProfileException.class); + } + + @Test + public void futureCall_workProfileAvailableButNotConnected_works() + throws ExecutionException, InterruptedException { + connector.turnOnWorkProfile(); + connector.disconnect(); + + ListenableFuture<Void> future = fakeCrossProfileType.work().listenableFutureVoidMethod(); + + assertThat(future.get()).isNull(); + } + + @Test + public void futureCall_notConnected_connects() { + connector.turnOnWorkProfile(); + connector.disconnect(); + connectionListener.resetConnectionChangedCount(); + + ListenableFuture<Void> unusedFuture = fakeCrossProfileType.work().listenableFutureVoidMethod(); + + assertThat(connectionListener.connectionChangedCount()).isEqualTo(1); + assertThat(connector.isConnected()).isTrue(); + } + + @Test + public void futureCall_notConnected_doesNotStartManualConnectionManagement() { + connector.turnOnWorkProfile(); + connector.stopManualConnectionManagement(); + connector.disconnect(); + + ListenableFuture<Void> unusedFuture = fakeCrossProfileType.work().listenableFutureVoidMethod(); + + assertThat(connector.isManuallyManagingConnection()).isFalse(); + } + + @Test + public void futureCall_workProfileUnavailable_setsUnavailableProfileException() { + connector.removeWorkProfile(); + + ListenableFuture<Void> future = fakeCrossProfileType.work().listenableFutureVoidMethod(); + + assertFutureHasException(future, UnavailableProfileException.class); + } + + @Test + public void blockingCallOnBoth_notConnected_onlyCallsOnCurrent() { + connector.setRunningOnProfile(ProfileType.PERSONAL); + connector.turnOnWorkProfile(); + connector.disconnect(); + + fakeCrossProfileType.both().voidMethod(); + + assertThat(personal.voidMethodInstanceCalls).isEqualTo(1); + assertThat(work.voidMethodInstanceCalls).isEqualTo(0); + } + + @Test + public void asyncCallOnBoth_notAvailable_onlyCallsOnCurrent() { + connector.setRunningOnProfile(ProfileType.PERSONAL); + connector.turnOffWorkProfile(); + + fakeCrossProfileType + .both() + .asyncVoidMethod( + () -> { + // Ignored + }); + + assertThat(personal.voidMethodInstanceCalls).isEqualTo(1); + assertThat(work.voidMethodInstanceCalls).isEqualTo(0); + } + + @Test + public void futureCallOnBoth_notAvailable_onlyCallsOnCurrent() { + connector.setRunningOnProfile(ProfileType.PERSONAL); + connector.turnOffWorkProfile(); + + ListenableFuture<Map<Profile, Void>> unusedFuture = + fakeCrossProfileType.both().listenableFutureVoidMethod(); + + assertThat(personal.voidMethodInstanceCalls).isEqualTo(1); + assertThat(work.voidMethodInstanceCalls).isEqualTo(0); + } + + @Test + public void current_synchronous_throwsRuntimeException_runtimeExceptionIsThrown() { + assertThrows( + CustomRuntimeException.class, + () -> { + fakeCrossProfileType.current().methodWhichThrowsRuntimeException(); + }); + } + + @Test + public void other_synchronous_throwsRuntimeException_exceptionIsWrapped() + throws UnavailableProfileException { + try { + fakeCrossProfileType.other().methodWhichThrowsRuntimeException(); + fail(); + } catch (ProfileRuntimeException expected) { + assertThat(expected).hasCauseThat().isInstanceOf(CustomRuntimeException.class); + } + } + + @Test + public void current_async_throwsRuntimeException_runtimeExceptionIsThrown() { + assertThrows( + CustomRuntimeException.class, + () -> { + fakeCrossProfileType + .current() + .asyncStringMethodWhichThrowsRuntimeException(/* callback= */ null); + }); + } + + @Test + public void other_async_throwsRuntimeException_exceptionIsWrapped() { + try { + fakeCrossProfileType + .other() + .asyncStringMethodWhichThrowsRuntimeException( + /* callback= */ null, /* exceptionCallback= */ null); + fail(); + } catch (ProfileRuntimeException expected) { + assertThat(expected).hasCauseThat().isInstanceOf(CustomRuntimeException.class); + } + } + + @Test + public void current_future_throwsRuntimeException_runtimeExceptionIsThrown() { + assertThrows( + CustomRuntimeException.class, + () -> { + fakeCrossProfileType.current().listenableFutureVoidMethodWhichThrowsRuntimeException(); + }); + } + + @Test + public void other_future_throwsRuntimeException_exceptionIsWrapped() { + try { + fakeCrossProfileType.other().listenableFutureVoidMethodWhichThrowsRuntimeException(); + fail(); + } catch (ProfileRuntimeException expected) { + assertThat(expected).hasCauseThat().isInstanceOf(CustomRuntimeException.class); + } + } + + @Test + public void both_synchronous_throwsRuntimeException_exceptionIsThrown() { + // Which one is thrown when both throw exceptions is not specified + try { + fakeCrossProfileType.both().methodWhichThrowsRuntimeException(); + fail(); + } catch (CustomRuntimeException expected) { + + } catch (ProfileRuntimeException expected) { + assertThat(expected).hasCauseThat().isInstanceOf(CustomRuntimeException.class); + } + } + + @Test + public void both_async_throwsRuntimeException_exceptionIsThrown() { + // Which one is thrown when both throw exceptions is not specified + try { + fakeCrossProfileType + .both() + .asyncStringMethodWhichThrowsRuntimeException(/* callback= */ null); + fail(); + } catch (CustomRuntimeException expected) { + + } catch (ProfileRuntimeException expected) { + assertThat(expected).hasCauseThat().isInstanceOf(CustomRuntimeException.class); + } + } + + @Test + public void both_future_throwsRuntimeException_exceptionIsThrown() { + // Which one is thrown when both throw exceptions is not specified + try { + fakeCrossProfileType.both().listenableFutureVoidMethodWhichThrowsRuntimeException(); + fail(); + } catch (CustomRuntimeException expected) { + + } catch (ProfileRuntimeException expected) { + assertThat(expected).hasCauseThat().isInstanceOf(CustomRuntimeException.class); + } + } + + @Test + public void ifAvailable_synchronous_notConnected_returnsDefaultValue() { + connector.disconnect(); + + assertThat( + fakeCrossProfileType + .other() + .ifAvailable() + .identityStringMethod(STRING, /* defaultValue= */ STRING2)) + .isEqualTo(STRING2); + } + + @Test + public void ifAvailable_synchronousVoid_notConnected_doesNotCallMethod() { + connector.setRunningOnProfile(ProfileType.PERSONAL); + connector.disconnect(); + + fakeCrossProfileType.other().ifAvailable().voidMethod(); + + assertThat(work.voidMethodInstanceCalls).isEqualTo(0); + } + + @Test + public void ifAvailable_synchronous_connected_returnsCorrectValue() { + connector.startConnecting(); + + assertThat( + fakeCrossProfileType + .other() + .ifAvailable() + .identityStringMethod(STRING, /* defaultValue= */ STRING2)) + .isEqualTo(STRING); + } + + @Test + public void ifAvailable_synchronousVoid_connected_callsMethod() { + connector.startConnecting(); + connector.setRunningOnProfile(ProfileType.PERSONAL); + fakeCrossProfileType.other().ifAvailable().voidMethod(); + + assertThat(work.voidMethodInstanceCalls).isEqualTo(1); + } + + @Test + public void ifAvailable_callback_notAvailable_returnsDefaultValue() { + connector.turnOffWorkProfile(); + + fakeCrossProfileType + .other() + .ifAvailable() + .asyncIdentityStringMethod(STRING, stringCallbackListener, /* defaultValue= */ STRING2); + + assertThat(stringCallbackListener.stringCallbackValue).isEqualTo(STRING2); + } + + @Test + public void ifAvailable_voidCallback_notAvailable_callsback() { + connector.turnOffWorkProfile(); + + fakeCrossProfileType.other().ifAvailable().asyncVoidMethod(voidCallbackListener); + + assertThat(voidCallbackListener.callbackMethodCalls).isEqualTo(1); + } + + @Test + public void ifAvailable_callback_available_returnsCorrectValue() { + connector.turnOnWorkProfile(); + + fakeCrossProfileType + .other() + .ifAvailable() + .asyncIdentityStringMethod(STRING, stringCallbackListener, /* defaultValue= */ STRING2); + + assertThat(stringCallbackListener.stringCallbackValue).isEqualTo(STRING); + } + + @Test + public void ifAvailable_voidCallback_available_callsMethod() { + connector.turnOnWorkProfile(); + + fakeCrossProfileType.other().ifAvailable().asyncVoidMethod(voidCallbackListener); + + assertThat(voidCallbackListener.callbackMethodCalls).isEqualTo(1); + } + + @Test + public void ifAvailable_future_available_returnsCorrectValue() + throws ExecutionException, InterruptedException { + connector.turnOnWorkProfile(); + + ListenableFuture<String> future = + fakeCrossProfileType + .other() + .ifAvailable() + .listenableFutureIdentityStringMethod(STRING, STRING2); + + assertThat(future.get()).isEqualTo(STRING); + } + + @Test + public void ifAvailable_future_notAvailable_returnsDefaultValue() + throws ExecutionException, InterruptedException { + connector.turnOffWorkProfile(); + + ListenableFuture<String> future = + fakeCrossProfileType + .other() + .ifAvailable() + .listenableFutureIdentityStringMethod(STRING, STRING2); + + assertThat(future.get()).isEqualTo(STRING2); + } + + @Test + public void contextArgument_synchronous_currentProfile_works() { + assertThat(fakeCrossProfileType.current().isContextArgumentPassed()).isTrue(); + } + + @Test + public void contextArgument_synchronous_otherProfile_works() throws UnavailableProfileException { + assertThat(fakeCrossProfileType.other().isContextArgumentPassed()).isTrue(); + } + + @Test + public void contextArgument_synchronous_both_works() { + Map<Profile, Boolean> result = fakeCrossProfileType.both().isContextArgumentPassed(); + + assertThat(result.get(connector.utils().getCurrentProfile())).isTrue(); + assertThat(result.get(connector.utils().getOtherProfile())).isTrue(); + } + + @Test + public void contextArgument_async_currentProfile_works() { + fakeCrossProfileType.current().asyncIsContextArgumentPassed(booleanCallbackListener); + + assertThat(booleanCallbackListener.booleanCallbackValue).isTrue(); + } + + @Test + public void contextArgument_async_otherProfile_works() { + fakeCrossProfileType + .other() + .asyncIsContextArgumentPassed(booleanCallbackListener, exceptionCallbackListener); + + assertThat(booleanCallbackListener.booleanCallbackValue).isTrue(); + } + + @Test + public void contextArgument_async_both_works() { + fakeCrossProfileType.both().asyncIsContextArgumentPassed(booleanMultiCallbackListener); + + Map<Profile, Boolean> result = booleanMultiCallbackListener.booleanCallbackValues; + assertThat(result.get(connector.utils().getCurrentProfile())).isTrue(); + assertThat(result.get(connector.utils().getOtherProfile())).isTrue(); + } + + @Test + public void contextArgument_future_currentProfile_works() throws Exception { + ListenableFuture<Boolean> future = + fakeCrossProfileType.current().futureIsContextArgumentPassed(); + + assertThat(future.get()).isTrue(); + } + + @Test + public void contextArgument_future_otherProfile_works() throws Exception { + ListenableFuture<Boolean> future = fakeCrossProfileType.other().futureIsContextArgumentPassed(); + + assertThat(future.get()).isTrue(); + } + + @Test + public void contextArgument_future_both_works() throws Exception { + ListenableFuture<Map<Profile, Boolean>> resultFuture = + fakeCrossProfileType.both().futureIsContextArgumentPassed(); + + Map<Profile, Boolean> result = resultFuture.get(); + assertThat(result.get(connector.utils().getCurrentProfile())).isTrue(); + assertThat(result.get(connector.utils().getOtherProfile())).isTrue(); + } + + @Test + public void current_synchronous_declaresButDoesNotThrowException_works() throws Exception { + assertThat( + fakeCrossProfileType + .current() + .identityStringMethodDeclaresButDoesNotThrowIOException(STRING)) + .isEqualTo(STRING); + } + + @Test + public void current_synchronous_throwsException_works() { + assertThrows( + IOException.class, + () -> + fakeCrossProfileType + .current() + .identityStringMethodThrowsIOException(STRING)); + } + + @Test + public void current_synchronous_declaresMultipleExceptions_throwsException_works() { + assertThrows( + SQLException.class, + () -> + fakeCrossProfileType + .current() + .identityStringMethodDeclaresIOExceptionThrowsSQLException( + STRING)); + } + + @Test + public void other_synchronous_declaresButDoesNotThrowException_works() throws Exception { + assertThat( + fakeCrossProfileType + .other() + .identityStringMethodDeclaresButDoesNotThrowIOException(STRING)) + .isEqualTo(STRING); + } + + @Test + public void other_synchronous_throwsException_works() { + assertThrows( + IOException.class, + () -> + fakeCrossProfileType + .other() + .identityStringMethodThrowsIOException(STRING)); + } + + @Test + public void other_synchronous_declaresMultipleExceptions_throwsException_works() { + assertThrows( + SQLException.class, + () -> + fakeCrossProfileType + .other() + .identityStringMethodDeclaresIOExceptionThrowsSQLException(STRING)); + } + + @Test + public void current_async_nonSimpleCallback_works() { + nonSimpleCallbackListener.callbackMethodCalls = 0; + fakeCrossProfileType + .current() + .asyncMethodWithNonSimpleCallback(nonSimpleCallbackListener, STRING, STRING2); + + assertThat(nonSimpleCallbackListener.callbackMethodCalls).isEqualTo(1); + assertThat(nonSimpleCallbackListener.string1CallbackValue).isEqualTo(STRING); + assertThat(nonSimpleCallbackListener.string2CallbackValue).isEqualTo(STRING2); + } + + @Test + public void other_async_nonSimpleCallback_works() { + nonSimpleCallbackListener.callbackMethodCalls = 0; + fakeCrossProfileType + .other() + .asyncMethodWithNonSimpleCallback( + nonSimpleCallbackListener, STRING, STRING2, exceptionCallbackListener); + + assertThat(nonSimpleCallbackListener.callbackMethodCalls).isEqualTo(1); + assertThat(nonSimpleCallbackListener.string1CallbackValue).isEqualTo(STRING); + assertThat(nonSimpleCallbackListener.string2CallbackValue).isEqualTo(STRING2); + } + + @Test + public void current_async_nonSimpleCallback_secondMethod_works() { + fakeCrossProfileType + .current() + .asyncMethodWithNonSimpleCallbackCallsSecondMethod( + nonSimpleCallbackListener, STRING, STRING2); + + assertThat(nonSimpleCallbackListener.string3CallbackValue).isEqualTo(STRING); + assertThat(nonSimpleCallbackListener.string4CallbackValue).isEqualTo(STRING2); + } + + @Test + public void other_async_nonSimpleCallback_secondMethod_works() { + fakeCrossProfileType + .other() + .asyncMethodWithNonSimpleCallbackCallsSecondMethod( + nonSimpleCallbackListener, STRING, STRING2, exceptionCallbackListener); + + assertThat(nonSimpleCallbackListener.string3CallbackValue).isEqualTo(STRING); + assertThat(nonSimpleCallbackListener.string4CallbackValue).isEqualTo(STRING2); + } +} diff --git a/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/testing/GeneratedFakeProfileConnectorTest.java b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/testing/GeneratedFakeProfileConnectorTest.java new file mode 100644 index 0000000..187d95d --- /dev/null +++ b/tests/robotests/src/test/java/com/google/android/enterprise/connectedapps/testing/GeneratedFakeProfileConnectorTest.java @@ -0,0 +1,52 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testing; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.Context; +import android.os.Build.VERSION_CODES; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.enterprise.connectedapps.testapp.configuration.TestApplication; +import com.google.android.enterprise.connectedapps.testapp.connector.FakeTestProfileConnector; +import com.google.android.enterprise.connectedapps.testing.annotations.CrossProfileTest; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Test specifics of a single generated fake for {@link + * com.google.android.enterprise.connectedapps.ProfileConnector} + * + * <p>More extensive tests of this functionality are in {@link AbstractFakeProfileConnectorTest}. + */ +@CrossProfileTest(configuration = TestApplication.class) +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = VERSION_CODES.O) +public class GeneratedFakeProfileConnectorTest { + + private final Context context = ApplicationProvider.getApplicationContext(); + private final FakeTestProfileConnector fakeTestProfileConnector = + new FakeTestProfileConnector(context); + + @Test + public void getPrimaryProfile_equalsWorkProfile() { + // The TestProfileConnector's primary profile is set to work + assertThat(fakeTestProfileConnector.utils().getPrimaryProfile()) + .isEqualTo(fakeTestProfileConnector.utils().getWorkProfile()); + } +} diff --git a/tests/shared/additional_types/AndroidManifest.xml b/tests/shared/additional_types/AndroidManifest.xml new file mode 100644 index 0000000..3aea2f3 --- /dev/null +++ b/tests/shared/additional_types/AndroidManifest.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2021 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<manifest + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + package="com.google.android.enterprise.connectedapps.shared.additional_types"> + <application> + </application> +</manifest>
\ No newline at end of file diff --git a/tests/shared/additional_types/build.gradle b/tests/shared/additional_types/build.gradle new file mode 100644 index 0000000..342d739 --- /dev/null +++ b/tests/shared/additional_types/build.gradle @@ -0,0 +1,46 @@ +buildscript { + repositories { + mavenCentral() + } +} + +plugins { + id 'com.android.library' +} + +dependencies { + api project(path: ':connectedapps-testapp_basictypes') + api project(path: ':connectedapps-testapp_connector') + api project(path: ':connectedapps-testapp_wrappers') + + + implementation project(path: ':connectedapps') + implementation project(path: ':connectedapps-annotations') + implementation project(path: ':connectedapps-processor') + annotationProcessor project(path: ':connectedapps-processor') +} + +android { + defaultConfig { + compileSdkVersion 30 + minSdkVersion 26 + } + + testOptions.unitTests.includeAndroidResources = true + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + sourceSets { + main { + java.srcDirs = [file('../src/main/java')] + java.includes = [ + "com/google/android/enterprise/connectedapps/testapp/types/TestCrossProfileInterface.java", + "com/google/android/enterprise/connectedapps/testapp/types/TestInterfaceProvider.java", + ] + manifest.srcFile 'AndroidManifest.xml' + } + } +} diff --git a/tests/shared/app/AndroidManifest.xml b/tests/shared/app/AndroidManifest.xml new file mode 100644 index 0000000..2ea278d --- /dev/null +++ b/tests/shared/app/AndroidManifest.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2021 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<manifest + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + package="com.google.android.enterprise.connectedapps.shared.app"> + <application> + </application> +</manifest>
\ No newline at end of file diff --git a/tests/shared/app/build.gradle b/tests/shared/app/build.gradle new file mode 100644 index 0000000..8d92955 --- /dev/null +++ b/tests/shared/app/build.gradle @@ -0,0 +1,35 @@ +buildscript { + repositories { + mavenCentral() + } +} + +plugins { + id 'com.android.application' +} + +dependencies { + api project(path: ':connectedapps-testapp_additional_types') + api project(path: ':connectedapps-testapp_types') +} + +android { + defaultConfig { + compileSdkVersion 30 + minSdkVersion 26 + } + + testOptions.unitTests.includeAndroidResources = true + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + sourceSets { + main { + java.srcDirs = [] + manifest.srcFile 'AndroidManifest.xml' + } + } +} diff --git a/tests/shared/basictypes/AndroidManifest.xml b/tests/shared/basictypes/AndroidManifest.xml new file mode 100644 index 0000000..e774d40 --- /dev/null +++ b/tests/shared/basictypes/AndroidManifest.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2021 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<manifest + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + package="com.google.android.enterprise.connectedapps.shared.basictypes"> + <application> + </application> +</manifest>
\ No newline at end of file diff --git a/tests/shared/basictypes/build.gradle b/tests/shared/basictypes/build.gradle new file mode 100644 index 0000000..f00a2ac --- /dev/null +++ b/tests/shared/basictypes/build.gradle @@ -0,0 +1,55 @@ +buildscript { + repositories { + mavenCentral() + } +} + +plugins { + id 'com.android.library' +} + +dependencies { + api deps.guava + + implementation project(path: ':connectedapps') + implementation project(path: ':connectedapps-annotations') + implementation project(path: ':connectedapps-processor') + annotationProcessor project(path: ':connectedapps-processor') +} + +android { + defaultConfig { + compileSdkVersion 30 + minSdkVersion 26 + } + + testOptions.unitTests.includeAndroidResources = true + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + sourceSets { + main { + java.srcDirs = [file('../src/main/java')] + java.includes = [ + "com/google/android/enterprise/connectedapps/testapp/CustomRuntimeException.java", + "com/google/android/enterprise/connectedapps/testapp/CustomWrapper.java", + "com/google/android/enterprise/connectedapps/testapp/CustomWrapper2.java", + "com/google/android/enterprise/connectedapps/testapp/NonSimpleCallbackListener.java", + "com/google/android/enterprise/connectedapps/testapp/NotReallySerializableObject.java", + "com/google/android/enterprise/connectedapps/testapp/ParcelableObject.java", + "com/google/android/enterprise/connectedapps/testapp/SerializableObject.java", + "com/google/android/enterprise/connectedapps/testapp/SimpleFuture.java", + "com/google/android/enterprise/connectedapps/testapp/StringWrapper.java", + "com/google/android/enterprise/connectedapps/testapp/TestBooleanCallbackListener.java", + "com/google/android/enterprise/connectedapps/testapp/TestCustomWrapperCallbackListener.java", + "com/google/android/enterprise/connectedapps/testapp/TestNotReallySerializableObjectCallbackListener.java", + "com/google/android/enterprise/connectedapps/testapp/TestStringCallbackListener.java", + "com/google/android/enterprise/connectedapps/testapp/TestVoidCallbackListener.java", + ] + manifest.srcFile 'AndroidManifest.xml' + } + } +} diff --git a/tests/shared/build.gradle b/tests/shared/build.gradle new file mode 100644 index 0000000..4642f3b --- /dev/null +++ b/tests/shared/build.gradle @@ -0,0 +1,46 @@ +buildscript { + repositories { + mavenCentral() + } +} + +plugins { + id 'com.android.library' +} + +dependencies { + api deps.checkerFramework + api project(path: ':connectedapps-testapp') + implementation project(path: ':connectedapps-annotations') + implementation 'org.robolectric:robolectric:4.4' + implementation 'junit:junit:4.13.1' + implementation 'com.google.truth:truth:1.1.2' + implementation 'androidx.test:core:1.3.0' + implementation project(path: ':connectedapps') + implementation project(path: ':connectedapps-annotations') + implementation project(path: ':connectedapps-processor') + annotationProcessor project(path: ':connectedapps-processor') + implementation project(path: ':connectedapps-testing') + implementation project(path: ':connectedapps-testing-annotations') + implementation project(path: ':connectedapps-testapp_types') +} + +android { + defaultConfig { + compileSdkVersion 30 + minSdkVersion 26 + } + + testOptions.unitTests.includeAndroidResources = true + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + sourceSets { + main { + java.includes = ["com/google/android/enterprise/connectedapps/*.java"] + } + } +} diff --git a/tests/shared/configuration/AndroidManifest.xml b/tests/shared/configuration/AndroidManifest.xml new file mode 100644 index 0000000..9a8e3b2 --- /dev/null +++ b/tests/shared/configuration/AndroidManifest.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2021 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<manifest + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + package="com.google.android.enterprise.connectedapps.shared.configuration"> + <application> + </application> +</manifest>
\ No newline at end of file diff --git a/tests/shared/configuration/build.gradle b/tests/shared/configuration/build.gradle new file mode 100644 index 0000000..96b3ee9 --- /dev/null +++ b/tests/shared/configuration/build.gradle @@ -0,0 +1,44 @@ +buildscript { + repositories { + mavenCentral() + } +} + +plugins { + id 'com.android.library' +} + +dependencies { + api project(path: ':connectedapps-testapp_types_providers') + api project(path: ':connectedapps-testapp_types') + api project(path: ':connectedapps-testapp_additional_types') + + implementation project(path: ':connectedapps') + implementation project(path: ':connectedapps-annotations') + implementation project(path: ':connectedapps-processor') + annotationProcessor project(path: ':connectedapps-processor') +} + +android { + defaultConfig { + compileSdkVersion 30 + minSdkVersion 26 + } + + testOptions.unitTests.includeAndroidResources = true + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + sourceSets { + main { + java.srcDirs = [file('../src/main/java')] + java.includes = [ + "com/google/android/enterprise/connectedapps/testapp/configuration/TestApplication.java" + ] + manifest.srcFile 'AndroidManifest.xml' + } + } +} diff --git a/tests/shared/connector/AndroidManifest.xml b/tests/shared/connector/AndroidManifest.xml new file mode 100644 index 0000000..37f0eda --- /dev/null +++ b/tests/shared/connector/AndroidManifest.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2021 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<manifest + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + package="com.google.android.enterprise.connectedapps.shared.connector"> + <application> + </application> +</manifest>
\ No newline at end of file diff --git a/tests/shared/connector/build.gradle b/tests/shared/connector/build.gradle new file mode 100644 index 0000000..c24d502 --- /dev/null +++ b/tests/shared/connector/build.gradle @@ -0,0 +1,46 @@ +buildscript { + repositories { + mavenCentral() + } +} + +plugins { + id 'com.android.library' +} + +dependencies { + api project(path: ':connectedapps-testapp_wrappers') + + + implementation project(path: ':connectedapps') + implementation project(path: ':connectedapps-annotations') + implementation project(path: ':connectedapps-processor') + annotationProcessor project(path: ':connectedapps-processor') +} + +android { + defaultConfig { + compileSdkVersion 30 + minSdkVersion 26 + } + + testOptions.unitTests.includeAndroidResources = true + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + sourceSets { + main { + java.srcDirs = [file('../src/main/java')] + java.includes = [ + "com/google/android/enterprise/connectedapps/testapp/ConnectorSingleton.java", + "com/google/android/enterprise/connectedapps/testapp/connector/DirectBootAwareConnector.java", + "com/google/android/enterprise/connectedapps/testapp/connector/TestProfileConnector.java", + "com/google/android/enterprise/connectedapps/testapp/connector/TestProfileConnectorWithCustomServiceClass.java", + ] + manifest.srcFile 'AndroidManifest.xml' + } + } +} diff --git a/tests/shared/crossuser/AndroidManifest.xml b/tests/shared/crossuser/AndroidManifest.xml new file mode 100644 index 0000000..0e07eca --- /dev/null +++ b/tests/shared/crossuser/AndroidManifest.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2021 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<manifest + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + package="com.google.android.enterprise.connectedapps.shared.crossuser"> + <application> + </application> +</manifest>
\ No newline at end of file diff --git a/tests/shared/crossuser/build.gradle b/tests/shared/crossuser/build.gradle new file mode 100644 index 0000000..4cceaba --- /dev/null +++ b/tests/shared/crossuser/build.gradle @@ -0,0 +1,40 @@ +buildscript { + repositories { + mavenCentral() + } +} + +plugins { + id 'com.android.library' +} + +dependencies { + implementation project(path: ':connectedapps') + implementation project(path: ':connectedapps-annotations') + implementation project(path: ':connectedapps-processor') + annotationProcessor project(path: ':connectedapps-processor') +} + +android { + defaultConfig { + compileSdkVersion 30 + minSdkVersion 26 + } + + testOptions.unitTests.includeAndroidResources = true + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + sourceSets { + main { + java.srcDirs = [file('../src/main/java')] + java.includes = [ + "com/google/android/enterprise/connectedapps/testapp/crossuser/*.java" + ] + manifest.srcFile 'AndroidManifest.xml' + } + } +} diff --git a/tests/shared/src/main/AndroidManifest.xml b/tests/shared/src/main/AndroidManifest.xml new file mode 100644 index 0000000..cc92adf --- /dev/null +++ b/tests/shared/src/main/AndroidManifest.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2021 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<manifest + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + package="com.google.android.enterprise.connectedapps.shared"> + + <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" tools:ignore="ProtectedPermissions" /> + + <application> + </application> +</manifest>
\ No newline at end of file diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/NonSimpleCallbackListenerImpl.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/NonSimpleCallbackListenerImpl.java new file mode 100644 index 0000000..cdb1f6f --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/NonSimpleCallbackListenerImpl.java @@ -0,0 +1,41 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import com.google.android.enterprise.connectedapps.testapp.NonSimpleCallbackListener; + +public class NonSimpleCallbackListenerImpl implements NonSimpleCallbackListener { + + public int callbackMethodCalls = 0; + public String string1CallbackValue; + public String string2CallbackValue; + public String string3CallbackValue; + public String string4CallbackValue; + + @Override + public void callback(String string1, String string2) { + string1CallbackValue = string1; + string2CallbackValue = string2; + callbackMethodCalls++; + } + + @Override + public void callback2(String string3, String string4) { + string3CallbackValue = string3; + string4CallbackValue = string4; + callbackMethodCalls++; + } +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/SharedTestUtilities.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/SharedTestUtilities.java new file mode 100644 index 0000000..5ff3b0b --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/SharedTestUtilities.java @@ -0,0 +1,96 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; + +import android.os.UserHandle; +import com.google.common.util.concurrent.FluentFuture; +import com.google.common.util.concurrent.ListenableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** Test utilities shared between Robolectric and Instrumented tests. */ +public final class SharedTestUtilities { + + public static final String INTERACT_ACROSS_USERS = "android.permission.INTERACT_ACROSS_USERS"; + public static final String INTERACT_ACROSS_USERS_FULL = + "android.permission.INTERACT_ACROSS_USERS_FULL"; + + private static final String OF_METHOD_NAME = "of"; + + /** Get the {@link UserHandle} for the given user ID. */ + public static UserHandle getUserHandleForUserId(int userId) { + try { + return (UserHandle) + UserHandle.class.getMethod(OF_METHOD_NAME, int.class).invoke(null, userId); + } catch (ReflectiveOperationException e) { + throw new IllegalStateException("Error getting current user handle", e); + } + } + + public static @Nullable Throwable assertFutureHasException( + ListenableFuture<?> future, Class<? extends Throwable> throwable) { + AtomicReference<Throwable> thrown = new AtomicReference<>(); + try { + FluentFuture.from(future) + .catching( + throwable, + t -> { + // Expected + thrown.set(t); + return null; + }, + directExecutor()) + .get(); + } catch (InterruptedException | ExecutionException e) { + throw new AssertionError("Unhandled exception", e); + } + + assertThat(thrown.get()).isNotNull(); + return thrown.get(); + } + + public static void assertFutureDoesNotHaveException( + ListenableFuture<?> future, Class<? extends Throwable> throwable) { + AtomicBoolean didThrow = new AtomicBoolean(false); + try { + FluentFuture.from(future) + .catching( + throwable, + expected -> { + didThrow.set(true); + return null; + }, + directExecutor()) + .withTimeout(1, TimeUnit.SECONDS, Executors.newSingleThreadScheduledExecutor()) + .get(); + } catch (InterruptedException e) { + throw new AssertionError("Unhandled exception", e); + } catch (ExecutionException e) { + // This is called when the 1 second times out - which means nothing was thrown + } + + assertThat(didThrow.get()).isFalse(); + } + + private SharedTestUtilities() {} +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/StringUtilities.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/StringUtilities.java new file mode 100644 index 0000000..7b13028 --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/StringUtilities.java @@ -0,0 +1,34 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import java.util.Random; + +public class StringUtilities { + private StringUtilities() {} + + private static final long RANDOM_SEED = 1; + + /** Generate a random String of the given length. */ + public static String randomString(int length) { + Random r = new Random(RANDOM_SEED); + char[] chars = new char[length]; + for (int i = 0; i < length; i++) { + chars[i] = (char) (r.nextInt(26) + 'a'); + } + return new String(chars); + } +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestAvailabilityListener.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestAvailabilityListener.java new file mode 100644 index 0000000..0e78d3a --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestAvailabilityListener.java @@ -0,0 +1,55 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import static java.util.concurrent.TimeUnit.SECONDS; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +public class TestAvailabilityListener implements AvailabilityListener { + + private static final long DEFAULT_TIMEOUT = 30; + private static final TimeUnit DEFAULT_UNIT = SECONDS; + + private int availabilityChangedCount = 0; + private CountDownLatch latch = new CountDownLatch(1); + + public int availabilityChangedCount() { + return availabilityChangedCount; + } + + public void reset() { + availabilityChangedCount = 0; + latch.countDown(); + latch = new CountDownLatch(1); + } + + public int awaitAvailabilityChange() throws InterruptedException { + return awaitAvailabilityChange(DEFAULT_TIMEOUT, DEFAULT_UNIT); + } + + public int awaitAvailabilityChange(long timeout, TimeUnit unit) throws InterruptedException { + latch.await(timeout, unit); + return availabilityChangedCount(); + } + + @Override + public void availabilityChanged() { + availabilityChangedCount++; + latch.countDown(); + } +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestBooleanCallbackListenerImpl.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestBooleanCallbackListenerImpl.java new file mode 100644 index 0000000..2b88f3c --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestBooleanCallbackListenerImpl.java @@ -0,0 +1,30 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import com.google.android.enterprise.connectedapps.testapp.TestBooleanCallbackListener; + +public class TestBooleanCallbackListenerImpl implements TestBooleanCallbackListener { + + public int callbackMethodCalls = 0; + public boolean booleanCallbackValue; + + @Override + public void booleanCallback(boolean b) { + callbackMethodCalls++; + booleanCallbackValue = b; + } +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestBooleanCallbackListenerMultiImpl.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestBooleanCallbackListenerMultiImpl.java new file mode 100644 index 0000000..c6bba6e --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestBooleanCallbackListenerMultiImpl.java @@ -0,0 +1,30 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import com.google.android.enterprise.connectedapps.testapp.TestBooleanCallbackListener_Multi; +import java.util.Map; + +public class TestBooleanCallbackListenerMultiImpl implements TestBooleanCallbackListener_Multi { + public int numberOfResults = 0; + public Map<Profile, Boolean> booleanCallbackValues; + + @Override + public void booleanCallback(Map<Profile, Boolean> b) { + numberOfResults = b.size(); + booleanCallbackValues = b; + } +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestConnectionListener.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestConnectionListener.java new file mode 100644 index 0000000..1cc4309 --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestConnectionListener.java @@ -0,0 +1,34 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +public class TestConnectionListener implements ConnectionListener { + + private int connectionChangedCount = 0; + + public int connectionChangedCount() { + return connectionChangedCount; + } + + public void resetConnectionChangedCount() { + connectionChangedCount = 0; + } + + @Override + public void connectionChanged() { + connectionChangedCount++; + } +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestCustomWrapperCallbackListenerImpl.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestCustomWrapperCallbackListenerImpl.java new file mode 100644 index 0000000..cb70b33 --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestCustomWrapperCallbackListenerImpl.java @@ -0,0 +1,31 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import com.google.android.enterprise.connectedapps.testapp.CustomWrapper; +import com.google.android.enterprise.connectedapps.testapp.TestCustomWrapperCallbackListener; + +public class TestCustomWrapperCallbackListenerImpl implements TestCustomWrapperCallbackListener { + + public int callbackMethodCalls = 0; + public CustomWrapper<String> customWrapperCallbackValue; + + @Override + public void customWrapperCallback(CustomWrapper<String> c) { + callbackMethodCalls++; + customWrapperCallbackValue = c; + } +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestExceptionCallbackListener.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestExceptionCallbackListener.java new file mode 100644 index 0000000..40229cc --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestExceptionCallbackListener.java @@ -0,0 +1,36 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +public class TestExceptionCallbackListener implements ExceptionCallback { + + public int exceptionCalls = 0; + public Throwable lastException = null; + + public int exceptionCalls() { + return exceptionCalls; + } + + public Throwable lastException() { + return lastException; + } + + @Override + public void onException(Throwable throwable) { + lastException = throwable; + exceptionCalls++; + } +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestNotReallySerializableObjectCallbackListenerImpl.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestNotReallySerializableObjectCallbackListenerImpl.java new file mode 100644 index 0000000..6e939d1 --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestNotReallySerializableObjectCallbackListenerImpl.java @@ -0,0 +1,31 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import com.google.android.enterprise.connectedapps.testapp.NotReallySerializableObject; +import com.google.android.enterprise.connectedapps.testapp.TestNotReallySerializableObjectCallbackListener; + +public class TestNotReallySerializableObjectCallbackListenerImpl + implements TestNotReallySerializableObjectCallbackListener { + public int callbackMethodCalls = 0; + public NotReallySerializableObject notReallySerializableObjectCallbackValue; + + @Override + public void callback(NotReallySerializableObject n) { + callbackMethodCalls++; + notReallySerializableObjectCallbackValue = n; + } +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestStringCallbackListenerImpl.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestStringCallbackListenerImpl.java new file mode 100644 index 0000000..4207e7b --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestStringCallbackListenerImpl.java @@ -0,0 +1,30 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import com.google.android.enterprise.connectedapps.testapp.TestStringCallbackListener; + +public class TestStringCallbackListenerImpl implements TestStringCallbackListener { + + public int callbackMethodCalls = 0; + public String stringCallbackValue; + + @Override + public void stringCallback(String s) { + callbackMethodCalls++; + stringCallbackValue = s; + } +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestStringCallbackListenerMultiImpl.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestStringCallbackListenerMultiImpl.java new file mode 100644 index 0000000..5f3043e --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestStringCallbackListenerMultiImpl.java @@ -0,0 +1,30 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import com.google.android.enterprise.connectedapps.testapp.TestStringCallbackListener_Multi; +import java.util.Map; + +public class TestStringCallbackListenerMultiImpl implements TestStringCallbackListener_Multi { + public int numberOfResults = 0; + public Map<Profile, String> stringCallbackValues; + + @Override + public void stringCallback(Map<Profile, String> s) { + numberOfResults = s.size(); + stringCallbackValues = s; + } +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestVoidCallbackListenerImpl.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestVoidCallbackListenerImpl.java new file mode 100644 index 0000000..d7f4b66 --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestVoidCallbackListenerImpl.java @@ -0,0 +1,28 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import com.google.android.enterprise.connectedapps.testapp.TestVoidCallbackListener; + +public class TestVoidCallbackListenerImpl implements TestVoidCallbackListener { + + public int callbackMethodCalls = 0; + + @Override + public void callback() { + callbackMethodCalls++; + } +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestVoidCallbackListenerMultiImpl.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestVoidCallbackListenerMultiImpl.java new file mode 100644 index 0000000..90c4beb --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/TestVoidCallbackListenerMultiImpl.java @@ -0,0 +1,28 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps; + +import com.google.android.enterprise.connectedapps.testapp.TestVoidCallbackListener_Multi; + +public class TestVoidCallbackListenerMultiImpl implements TestVoidCallbackListener_Multi { + + public int callbackMethodCalls = 0; + + @Override + public void callback() { + callbackMethodCalls++; + } +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/ConnectorSingleton.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/ConnectorSingleton.java new file mode 100644 index 0000000..99c21bd --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/ConnectorSingleton.java @@ -0,0 +1,35 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp; + +import android.content.Context; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; + +/** Holder of a singleton {@link TestProfileConnector}. */ +public final class ConnectorSingleton { + private static TestProfileConnector connector; + + public static TestProfileConnector getConnector(Context context) { + if (connector == null) { + synchronized (ConnectorSingleton.class) { + connector = TestProfileConnector.create(context); + } + } + return connector; + } + + private ConnectorSingleton() {} +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/CustomRuntimeException.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/CustomRuntimeException.java new file mode 100644 index 0000000..22f187b --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/CustomRuntimeException.java @@ -0,0 +1,22 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp; + +public class CustomRuntimeException extends RuntimeException { + public CustomRuntimeException(String msg) { + super(msg); + } +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/CustomWrapper.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/CustomWrapper.java new file mode 100644 index 0000000..a2b1b51 --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/CustomWrapper.java @@ -0,0 +1,47 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp; + +import java.util.Objects; + +public class CustomWrapper<F> { + private final F value; + + public CustomWrapper(F value) { + this.value = value; + } + + public F value() { + return value; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof CustomWrapper)) { + return false; + } + CustomWrapper<?> that = (CustomWrapper<?>) o; + return Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + return Objects.hashCode(value); + } +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/CustomWrapper2.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/CustomWrapper2.java new file mode 100644 index 0000000..add6d3b --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/CustomWrapper2.java @@ -0,0 +1,47 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp; + +import java.util.Objects; + +public class CustomWrapper2<F> { + private final F value; + + public CustomWrapper2(F value) { + this.value = value; + } + + public F value() { + return value; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof CustomWrapper2)) { + return false; + } + CustomWrapper2<?> that = (CustomWrapper2<?>) o; + return Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + return Objects.hashCode(value); + } +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/NonSimpleCallbackListener.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/NonSimpleCallbackListener.java new file mode 100644 index 0000000..cc56283 --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/NonSimpleCallbackListener.java @@ -0,0 +1,25 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp; + +import com.google.android.enterprise.connectedapps.annotations.CrossProfileCallback; + +@CrossProfileCallback +public interface NonSimpleCallbackListener { + void callback(String string1, String string2); + + void callback2(String string3, String string4); +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/NotReallySerializableObject.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/NotReallySerializableObject.java new file mode 100644 index 0000000..d1d58c6 --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/NotReallySerializableObject.java @@ -0,0 +1,27 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp; + +import java.io.Serializable; + +public class NotReallySerializableObject implements Serializable { + // ParcelableObject does not implement Serializable + private final ParcelableObject parcelableObject; + + public NotReallySerializableObject(ParcelableObject parcelableObject) { + this.parcelableObject = parcelableObject; + } +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/ParcelableObject.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/ParcelableObject.java new file mode 100644 index 0000000..f499df4 --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/ParcelableObject.java @@ -0,0 +1,80 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp; + +import static com.google.common.base.Preconditions.checkNotNull; + +import android.os.Parcel; +import android.os.Parcelable; +import java.util.Objects; + +public final class ParcelableObject implements Parcelable { + + @SuppressWarnings("rawtypes") + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public ParcelableObject createFromParcel(Parcel in) { + return new ParcelableObject(in); + } + + @Override + public ParcelableObject[] newArray(int size) { + return new ParcelableObject[size]; + } + }; + + private final String value; + + public String value() { + return value; + } + + public ParcelableObject(Parcel in) { + this(in.readString()); + } + + public ParcelableObject(String value) { + this.value = checkNotNull(value); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(value); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ParcelableObject that = (ParcelableObject) o; + return value.equals(that.value); + } + + @Override + public int hashCode() { + return Objects.hashCode(value); + } +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/SerializableObject.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/SerializableObject.java new file mode 100644 index 0000000..d8fc9d8 --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/SerializableObject.java @@ -0,0 +1,49 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp; + +import java.io.Serializable; +import java.util.Objects; + +public final class SerializableObject implements Serializable { + + private final String value; + + public String value() { + return value; + } + + public SerializableObject(String value) { + this.value = value; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SerializableObject that = (SerializableObject) o; + return Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + return Objects.hashCode(value); + } +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/SimpleFuture.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/SimpleFuture.java new file mode 100644 index 0000000..9c388ee --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/SimpleFuture.java @@ -0,0 +1,71 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp; + +import java.util.concurrent.CountDownLatch; + +/** A very simple implementation of the future pattern used to test custom future wrappers. */ +public class SimpleFuture<E> { + + public static interface Consumer<E> { + void accept(E value); + } + + private E value; + private Throwable thrown; + private final CountDownLatch countDownLatch = new CountDownLatch(1); + private Consumer<E> callback; + private Consumer<Throwable> exceptionCallback; + + public void set(E value) { + this.value = value; + countDownLatch.countDown(); + if (callback != null) { + callback.accept(value); + } + } + + public void setException(Throwable t) { + this.thrown = t; + countDownLatch.countDown(); + if (exceptionCallback != null) { + exceptionCallback.accept(thrown); + } + } + + public E get() { + try { + countDownLatch.await(); + } catch (InterruptedException e) { + return null; + } + if (thrown != null) { + throw new RuntimeException(thrown); + } + return value; + } + + public void setCallback(Consumer<E> callback, Consumer<Throwable> exceptionCallback) { + if (value != null) { + callback.accept(value); + } else if (thrown != null) { + exceptionCallback.accept(thrown); + } else { + this.callback = callback; + this.exceptionCallback = exceptionCallback; + } + } +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/StringWrapper.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/StringWrapper.java new file mode 100644 index 0000000..4828f91 --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/StringWrapper.java @@ -0,0 +1,47 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp; + +import java.util.Objects; + +public class StringWrapper { + private final String value; + + public StringWrapper(String value) { + this.value = value; + } + + public String value() { + return value; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof StringWrapper)) { + return false; + } + StringWrapper that = (StringWrapper) o; + return Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + return Objects.hashCode(value); + } +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/TestBooleanCallbackListener.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/TestBooleanCallbackListener.java new file mode 100644 index 0000000..9e82374 --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/TestBooleanCallbackListener.java @@ -0,0 +1,23 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp; + +import com.google.android.enterprise.connectedapps.annotations.CrossProfileCallback; + +@CrossProfileCallback +public interface TestBooleanCallbackListener { + void booleanCallback(boolean b); +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/TestCustomWrapperCallbackListener.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/TestCustomWrapperCallbackListener.java new file mode 100644 index 0000000..a6e3899 --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/TestCustomWrapperCallbackListener.java @@ -0,0 +1,23 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp; + +import com.google.android.enterprise.connectedapps.annotations.CrossProfileCallback; + +@CrossProfileCallback +public interface TestCustomWrapperCallbackListener { + void customWrapperCallback(CustomWrapper<String> c); +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/TestNotReallySerializableObjectCallbackListener.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/TestNotReallySerializableObjectCallbackListener.java new file mode 100644 index 0000000..f6085d7 --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/TestNotReallySerializableObjectCallbackListener.java @@ -0,0 +1,23 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp; + +import com.google.android.enterprise.connectedapps.annotations.CrossProfileCallback; + +@CrossProfileCallback +public interface TestNotReallySerializableObjectCallbackListener { + void callback(NotReallySerializableObject object); +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/TestStringCallbackListener.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/TestStringCallbackListener.java new file mode 100644 index 0000000..dc25651 --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/TestStringCallbackListener.java @@ -0,0 +1,23 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp; + +import com.google.android.enterprise.connectedapps.annotations.CrossProfileCallback; + +@CrossProfileCallback +public interface TestStringCallbackListener { + void stringCallback(String s); +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/TestVoidCallbackListener.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/TestVoidCallbackListener.java new file mode 100644 index 0000000..01d28b9 --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/TestVoidCallbackListener.java @@ -0,0 +1,23 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp; + +import com.google.android.enterprise.connectedapps.annotations.CrossProfileCallback; + +@CrossProfileCallback +public interface TestVoidCallbackListener { + void callback(); +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/configuration/TestApplication.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/configuration/TestApplication.java new file mode 100644 index 0000000..de1d8f5 --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/configuration/TestApplication.java @@ -0,0 +1,35 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp.configuration; + +import android.app.Service; +import com.google.android.enterprise.connectedapps.annotations.CrossProfileConfiguration; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector_Service; +import com.google.android.enterprise.connectedapps.testapp.types.SeparateBuildTargetProvider; +import com.google.android.enterprise.connectedapps.testapp.types.TestInterfaceProvider; +import com.google.android.enterprise.connectedapps.testapp.types.TestProvider; + +@CrossProfileConfiguration(providers = { + TestProvider.class, SeparateBuildTargetProvider.class, TestInterfaceProvider.class}) +public abstract class TestApplication { + + // This is available so the test targets can access the generated Service class. + public static Class<? extends Service> getService() { + return TestProfileConnector_Service.class; + } + + private TestApplication() {} +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/connector/DirectBootAwareConnector.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/connector/DirectBootAwareConnector.java new file mode 100644 index 0000000..56dcf08 --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/connector/DirectBootAwareConnector.java @@ -0,0 +1,38 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp.connector; + +import android.content.Context; +import com.google.android.enterprise.connectedapps.ProfileConnector; +import com.google.android.enterprise.connectedapps.annotations.AvailabilityRestrictions; +import com.google.android.enterprise.connectedapps.annotations.CustomProfileConnector; +import com.google.android.enterprise.connectedapps.annotations.GeneratedProfileConnector; +import java.util.concurrent.ScheduledExecutorService; + +@GeneratedProfileConnector +@CustomProfileConnector(availabilityRestrictions = AvailabilityRestrictions.DIRECT_BOOT_AWARE) +public interface DirectBootAwareConnector extends ProfileConnector { + static DirectBootAwareConnector create(Context context) { + return GeneratedDirectBootAwareConnector.builder(context).build(); + } + + static DirectBootAwareConnector create( + Context context, ScheduledExecutorService scheduledExecutorService) { + return GeneratedDirectBootAwareConnector.builder(context) + .setScheduledExecutorService(scheduledExecutorService) + .build(); + } +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/connector/TestProfileConnector.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/connector/TestProfileConnector.java new file mode 100644 index 0000000..296b820 --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/connector/TestProfileConnector.java @@ -0,0 +1,43 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp.connector; + +import android.content.Context; +import com.google.android.enterprise.connectedapps.ProfileConnector; +import com.google.android.enterprise.connectedapps.annotations.CustomProfileConnector; +import com.google.android.enterprise.connectedapps.annotations.CustomProfileConnector.ProfileType; +import com.google.android.enterprise.connectedapps.annotations.GeneratedProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.wrappers.ParcelableCustomWrapper; +import com.google.android.enterprise.connectedapps.testapp.wrappers.SimpleFutureWrapper; +import java.util.concurrent.ScheduledExecutorService; + +@GeneratedProfileConnector +@CustomProfileConnector( + primaryProfile = ProfileType.WORK, + parcelableWrappers = {ParcelableCustomWrapper.class}, + futureWrappers = {SimpleFutureWrapper.class}) +public interface TestProfileConnector extends ProfileConnector { + static TestProfileConnector create(Context context) { + return GeneratedTestProfileConnector.builder(context).build(); + } + + static TestProfileConnector create( + Context context, ScheduledExecutorService scheduledExecutorService) { + return GeneratedTestProfileConnector.builder(context) + .setScheduledExecutorService(scheduledExecutorService) + .build(); + } +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/connector/TestProfileConnectorWithCustomServiceClass.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/connector/TestProfileConnectorWithCustomServiceClass.java new file mode 100644 index 0000000..a5cfa6a --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/connector/TestProfileConnectorWithCustomServiceClass.java @@ -0,0 +1,33 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp.connector; + +import android.content.Context; +import com.google.android.enterprise.connectedapps.ProfileConnector; +import com.google.android.enterprise.connectedapps.annotations.CustomProfileConnector; +import com.google.android.enterprise.connectedapps.annotations.GeneratedProfileConnector; +import java.util.concurrent.ScheduledExecutorService; + +@GeneratedProfileConnector +@CustomProfileConnector(serviceClassName = "com.google.CustomServiceClass") +public interface TestProfileConnectorWithCustomServiceClass extends ProfileConnector { + static TestProfileConnectorWithCustomServiceClass create( + Context context, ScheduledExecutorService scheduledExecutorService) { + return GeneratedTestProfileConnectorWithCustomServiceClass.builder(context) + .setScheduledExecutorService(scheduledExecutorService) + .build(); + } +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/crossuser/TestCrossUserConfiguration.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/crossuser/TestCrossUserConfiguration.java new file mode 100644 index 0000000..f8ac4e8 --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/crossuser/TestCrossUserConfiguration.java @@ -0,0 +1,31 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp.crossuser; + +import android.app.Service; +import com.google.android.enterprise.connectedapps.annotations.CrossUserConfiguration; +import com.google.android.enterprise.connectedapps.annotations.CrossUserConfigurations; + +@CrossUserConfigurations(@CrossUserConfiguration(providers = TestCrossUserProvider.class)) +public abstract class TestCrossUserConfiguration { + + // This is available so the test targets can access the generated Service class. + public static Class<? extends Service> getService() { + return TestCrossUserConnector_Service.class; + } + + private TestCrossUserConfiguration() {} +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/crossuser/TestCrossUserConnector.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/crossuser/TestCrossUserConnector.java new file mode 100644 index 0000000..72c3a8c --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/crossuser/TestCrossUserConnector.java @@ -0,0 +1,38 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp.crossuser; + +import android.content.Context; +import com.google.android.enterprise.connectedapps.ProfileConnector; +import com.google.android.enterprise.connectedapps.annotations.CustomProfileConnector; +import com.google.android.enterprise.connectedapps.annotations.CustomProfileConnector.ProfileType; +import com.google.android.enterprise.connectedapps.annotations.GeneratedProfileConnector; +import java.util.concurrent.ScheduledExecutorService; + +@GeneratedProfileConnector +@CustomProfileConnector(primaryProfile = ProfileType.WORK) +public interface TestCrossUserConnector extends ProfileConnector { + static TestCrossUserConnector create(Context context) { + return GeneratedTestCrossUserConnector.builder(context).build(); + } + + static TestCrossUserConnector create( + Context context, ScheduledExecutorService scheduledExecutorService) { + return GeneratedTestCrossUserConnector.builder(context) + .setScheduledExecutorService(scheduledExecutorService) + .build(); + } +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/crossuser/TestCrossUserProvider.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/crossuser/TestCrossUserProvider.java new file mode 100644 index 0000000..6c6e4c6 --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/crossuser/TestCrossUserProvider.java @@ -0,0 +1,26 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp.crossuser; + +import com.google.android.enterprise.connectedapps.annotations.CrossUserProvider; + +public class TestCrossUserProvider { + + @CrossUserProvider + public TestCrossUserType provideTestCrossUserType() { + return new TestCrossUserType(); + } +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/crossuser/TestCrossUserStringCallbackListener.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/crossuser/TestCrossUserStringCallbackListener.java new file mode 100644 index 0000000..2ec080e --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/crossuser/TestCrossUserStringCallbackListener.java @@ -0,0 +1,23 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp.crossuser; + +import com.google.android.enterprise.connectedapps.annotations.CrossUserCallback; + +@CrossUserCallback +public interface TestCrossUserStringCallbackListener { + void stringCallback(String s); +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/crossuser/TestCrossUserStringCallbackListenerImpl.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/crossuser/TestCrossUserStringCallbackListenerImpl.java new file mode 100644 index 0000000..482a154 --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/crossuser/TestCrossUserStringCallbackListenerImpl.java @@ -0,0 +1,29 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp.crossuser; + +public class TestCrossUserStringCallbackListenerImpl + implements TestCrossUserStringCallbackListener { + + public int callbackMethodCalls = 0; + public String stringCallbackValue; + + @Override + public void stringCallback(String s) { + callbackMethodCalls++; + stringCallbackValue = s; + } +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/crossuser/TestCrossUserType.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/crossuser/TestCrossUserType.java new file mode 100644 index 0000000..9be071d --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/crossuser/TestCrossUserType.java @@ -0,0 +1,27 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp.crossuser; + +import com.google.android.enterprise.connectedapps.annotations.CrossUser; + +@CrossUser(connector = TestCrossUserConnector.class, timeoutMillis = 7000) +public class TestCrossUserType { + + @CrossUser + public void passString(String string, TestCrossUserStringCallbackListener callbackListener) { + callbackListener.stringCallback(string); + } +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/types/NonInstantiableTestCrossProfileType.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/types/NonInstantiableTestCrossProfileType.java new file mode 100644 index 0000000..0fa102c --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/types/NonInstantiableTestCrossProfileType.java @@ -0,0 +1,42 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp.types; + +import static com.google.common.util.concurrent.Futures.immediateFuture; + +import com.google.android.enterprise.connectedapps.annotations.CrossProfile; +import com.google.android.enterprise.connectedapps.testapp.TestStringCallbackListener; +import com.google.common.util.concurrent.ListenableFuture; + +public final class NonInstantiableTestCrossProfileType { + private NonInstantiableTestCrossProfileType() {} + + @CrossProfile + public static String staticIdentityStringMethod(String s) { + return s; + } + + @CrossProfile + public static void staticAsyncIdentityStringMethod( + String s, TestStringCallbackListener callback) { + callback.stringCallback(s); + } + + @CrossProfile + public static ListenableFuture<String> staticFutureIdentityStringMethod(String s) { + return immediateFuture(s); + } +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/types/SeparateBuildTargetProvider.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/types/SeparateBuildTargetProvider.java new file mode 100644 index 0000000..67d6003 --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/types/SeparateBuildTargetProvider.java @@ -0,0 +1,28 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp.types; + +import android.content.Context; +import com.google.android.enterprise.connectedapps.annotations.CrossProfileProvider; + +/** A provider which is in a separate build target to the type it provides. */ +public class SeparateBuildTargetProvider { + @CrossProfileProvider + public TestCrossProfileTypeWhichNeedsContext provideTestCrossProfileTypeWhichNeedsContext( + Context context) { + return new TestCrossProfileTypeWhichNeedsContext(context); + } +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/types/TestCrossProfileInterface.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/types/TestCrossProfileInterface.java new file mode 100644 index 0000000..7dfa616 --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/types/TestCrossProfileInterface.java @@ -0,0 +1,29 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp.types; + +import com.google.android.enterprise.connectedapps.annotations.CrossProfile; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import java.util.List; + +@CrossProfile(connector = TestProfileConnector.class) +public interface TestCrossProfileInterface { + + // This needs to be a type which has a parcelable wrapper to ensure that we generate duplicate + // parcelable wrappers in multiple targets and they are resolved correctly + @CrossProfile + List<String> identityListOfStringMethod(List<String> s); +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/types/TestCrossProfileType.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/types/TestCrossProfileType.java new file mode 100644 index 0000000..ddadc81 --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/types/TestCrossProfileType.java @@ -0,0 +1,602 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp.types; + +import static com.google.common.util.concurrent.Futures.immediateFuture; +import static com.google.common.util.concurrent.Futures.immediateVoidFuture; + +import android.content.Context; +import android.graphics.Bitmap; +import android.os.Handler; +import android.util.Pair; +import com.google.android.enterprise.connectedapps.annotations.CrossProfile; +import com.google.android.enterprise.connectedapps.testapp.CustomRuntimeException; +import com.google.android.enterprise.connectedapps.testapp.CustomWrapper; +import com.google.android.enterprise.connectedapps.testapp.CustomWrapper2; +import com.google.android.enterprise.connectedapps.testapp.NonSimpleCallbackListener; +import com.google.android.enterprise.connectedapps.testapp.NotReallySerializableObject; +import com.google.android.enterprise.connectedapps.testapp.ParcelableObject; +import com.google.android.enterprise.connectedapps.testapp.SerializableObject; +import com.google.android.enterprise.connectedapps.testapp.SimpleFuture; +import com.google.android.enterprise.connectedapps.testapp.StringWrapper; +import com.google.android.enterprise.connectedapps.testapp.TestBooleanCallbackListener; +import com.google.android.enterprise.connectedapps.testapp.TestCustomWrapperCallbackListener; +import com.google.android.enterprise.connectedapps.testapp.TestNotReallySerializableObjectCallbackListener; +import com.google.android.enterprise.connectedapps.testapp.TestStringCallbackListener; +import com.google.android.enterprise.connectedapps.testapp.TestVoidCallbackListener; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import com.google.android.enterprise.connectedapps.testapp.wrappers.ParcelableCustomWrapper2; +import com.google.android.enterprise.connectedapps.testapp.wrappers.ParcelableStringWrapper; +import com.google.common.base.Optional; +import com.google.common.collect.ImmutableMap; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; +import java.io.IOException; +import java.sql.SQLException; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +@CrossProfile( + connector = TestProfileConnector.class, + timeoutMillis = 7000, + parcelableWrappers = {ParcelableCustomWrapper2.class, ParcelableStringWrapper.class}) +public class TestCrossProfileType { + + public static int voidMethodCalls = 0; + public int voidMethodInstanceCalls; + + @CrossProfile + public void voidMethod() { + voidMethodCalls += 1; + voidMethodInstanceCalls += 1; + } + + @CrossProfile + public void voidMethod(String s) { + voidMethod(); + } + + @CrossProfile + public String methodWhichThrowsRuntimeException() { + throw new CustomRuntimeException("Exception"); + } + + @CrossProfile + public ListenableFuture<Void> listenableFutureVoidMethod() { + voidMethod(); + return immediateFuture(null); + } + + @CrossProfile + public ListenableFuture<Void> listenableFutureMethodWhichNeverSetsTheValue() { + return SettableFuture.create(); + } + + @CrossProfile // Timeout is inherited + public ListenableFuture<Void> listenableFutureMethodWhichNeverSetsTheValueWith7SecondTimeout() { + return SettableFuture.create(); + } + + @CrossProfile(timeoutMillis = 5000) + public ListenableFuture<Void> listenableFutureMethodWhichNeverSetsTheValueWith5SecondTimeout() { + return SettableFuture.create(); + } + + @CrossProfile + public ListenableFuture<Void> listenableFutureVoidMethodWhichThrowsRuntimeException() { + throw new CustomRuntimeException("Exception"); + } + + @CrossProfile + public ListenableFuture<Void> listenableFutureVoidMethodWhichSetsIllegalStateException() { + return Futures.immediateFailedFuture(new IllegalStateException("Illegal State")); + } + + @CrossProfile + public ListenableFuture<Void> listenableFutureVoidMethodWithDelay(int secondsDelay) { + try { + TimeUnit.SECONDS.sleep(secondsDelay); + } catch (InterruptedException e) { + throw new IllegalStateException("Error during delay"); + } + return listenableFutureVoidMethod(); + } + + @CrossProfile + public ListenableFuture<Void> listenableFutureVoidMethodWithNonBlockingDelay(int secondsDelay) { + SettableFuture<Void> v = SettableFuture.create(); + + new Handler() + .postDelayed( + () -> { + voidMethod(); + v.set(null); + }, + TimeUnit.SECONDS.toMillis(secondsDelay)); + return v; + } + + @CrossProfile + public ListenableFuture<String> listenableFutureIdentityStringMethodWithNonBlockingDelay( + String s, int secondsDelay) { + SettableFuture<String> v = SettableFuture.create(); + + new Handler().postDelayed(() -> v.set(s), TimeUnit.SECONDS.toMillis(secondsDelay)); + return v; + } + + @CrossProfile(timeoutMillis = 3000) + public ListenableFuture<String> + listenableFutureIdentityStringMethodWithNonBlockingDelayWith3SecondTimeout( + String s, int secondsDelay) { + SettableFuture<String> v = SettableFuture.create(); + + new Handler().postDelayed(() -> v.set(s), TimeUnit.SECONDS.toMillis(secondsDelay)); + return v; + } + + @CrossProfile + public void asyncStringMethodWhichThrowsRuntimeException(TestStringCallbackListener callback) { + throw new CustomRuntimeException("Exception"); + } + + @CrossProfile + public void asyncVoidMethodWhichCallsBackTwice(TestVoidCallbackListener callback) { + voidMethod(); + callback.callback(); + callback.callback(); + } + + @CrossProfile + public void asyncVoidMethod(TestVoidCallbackListener callback) { + voidMethod(); + callback.callback(); + } + + @CrossProfile + public void asyncMethodWhichNeverCallsBack(TestStringCallbackListener callback) {} + + @CrossProfile // Timeout is inherited + public void asyncMethodWhichNeverCallsBackWith7SecondTimeout( + TestStringCallbackListener callback) {} + + @CrossProfile(timeoutMillis = 5000) + public void asyncMethodWhichNeverCallsBackWith5SecondTimeout( + TestStringCallbackListener callback) {} + + @CrossProfile + public void asyncVoidMethodWithDelay(TestVoidCallbackListener callback, int secondsDelay) { + try { + TimeUnit.SECONDS.sleep(secondsDelay); + } catch (InterruptedException e) { + throw new IllegalStateException("Error during delay"); + } + asyncVoidMethod(callback); + } + + @CrossProfile + public void asyncVoidMethodWithNonBlockingDelay( + TestVoidCallbackListener callback, int secondsDelay) { + new Handler() + .postDelayed(() -> asyncVoidMethod(callback), TimeUnit.SECONDS.toMillis(secondsDelay)); + } + + @CrossProfile(timeoutMillis = 50000) + public void asyncVoidMethodWithNonBlockingDelayWith50SecondTimeout( + TestVoidCallbackListener callback, int secondsDelay) { + new Handler() + .postDelayed(() -> asyncVoidMethod(callback), TimeUnit.SECONDS.toMillis(secondsDelay)); + } + + @CrossProfile(timeoutMillis = 3000) + public void asyncIdentityStringMethodWithNonBlockingDelayWith3SecondTimeout( + String s, TestStringCallbackListener callback, int secondsDelay) { + new Handler() + .postDelayed( + () -> asyncIdentityStringMethod(s, callback), TimeUnit.SECONDS.toMillis(secondsDelay)); + } + + @CrossProfile + public void asyncIdentityStringMethodWithNonBlockingDelay( + String s, TestStringCallbackListener callback, int secondsDelay) { + new Handler() + .postDelayed( + () -> asyncIdentityStringMethod(s, callback), TimeUnit.SECONDS.toMillis(secondsDelay)); + } + + @CrossProfile + public Void identityVoidMethod() { + voidMethod(); + return null; + } + + @CrossProfile + public String getNull() { + return null; + } + + @CrossProfile + public Collection<String> getNullCollection() { + return null; + } + + @CrossProfile + public List<String> getNullList() { + return null; + } + + @CrossProfile + public Map<String, String> getNullMap() { + return null; + } + + // @CrossProfile + // public Optional<String> getNullOptional() { + // return null; + // } + + @CrossProfile + public Set<String> getNullSet() { + return null; + } + + // @CrossProfile + // public TestProto getNullProto() { + // return null; + // } + + @CrossProfile + public String identityStringMethod(String s) { + return s; + } + + @CrossProfile + public void asyncIdentityStringMethod(String s, TestStringCallbackListener callback) { + callback.stringCallback(s); + } + + @CrossProfile + public ListenableFuture<String> listenableFutureIdentityStringMethod(String s) { + return immediateFuture(s); + } + + @CrossProfile + public byte identityByteMethod(byte b) { + return b; + } + + @CrossProfile + public Byte identityByteMethod(Byte b) { + return b; + } + + @CrossProfile + public short identityShortMethod(short s) { + return s; + } + + @CrossProfile + public Short identityShortMethod(Short s) { + return s; + } + + @CrossProfile + public int identityIntMethod(int i) { + return i; + } + + @CrossProfile + public Integer identityIntegerMethod(Integer i) { + return i; + } + + @CrossProfile + public long identityLongMethod(long l) { + return l; + } + + @CrossProfile + public Long identityLongMethod(Long l) { + return l; + } + + @CrossProfile + public float identityFloatMethod(float f) { + return f; + } + + @CrossProfile + public Float identityFloatMethod(Float f) { + return f; + } + + @CrossProfile + public double identityDoubleMethod(double d) { + return d; + } + + @CrossProfile + public Double identityDoubleMethod(Double d) { + return d; + } + + @CrossProfile + public char identityCharMethod(char c) { + return c; + } + + @CrossProfile + public Character identityCharacterMethod(Character c) { + return c; + } + + @CrossProfile + public boolean identityBooleanMethod(boolean b) { + return b; + } + + @CrossProfile + public Boolean identityBooleanMethod(Boolean b) { + return b; + } + + @CrossProfile + public ParcelableObject identityParcelableMethod(ParcelableObject p) { + return p; + } + + @CrossProfile + public SerializableObject identitySerializableObjectMethod(SerializableObject s) { + return s; + } + + @CrossProfile + public List<String> identityListMethod(List<String> l) { + return l; + } + + @CrossProfile + public Map<String, String> identityMapMethod(Map<String, String> m) { + return m; + } + + @CrossProfile + public Set<String> identitySetMethod(Set<String> s) { + return s; + } + + // TODO: Disabled because use of Optional fails lint check. Re-enable when this is disabled. + // @CrossProfile + // public Optional<String> identityOptionalMethod(Optional<String> o) { + // return o; + // } + + @CrossProfile + public ImmutableMap<String, String> identityImmutableMapMethod(ImmutableMap<String, String> m) { + return m; + } + + // @CrossProfile + // public TestProto identityProtoMethod(TestProto p) { + // return p; + // } + + // @CrossProfile + // public List<TestProto> identityListOfProtoMethod(List<TestProto> l) { + // return l; + // } + + @CrossProfile + public Collection<String> identityCollectionMethod(Collection<String> c) { + return c; + } + + @CrossProfile + public List<ParcelableObject> identityParcelableWrapperOfParcelableMethod( + List<ParcelableObject> l) { + return l; + } + + @CrossProfile + public List<SerializableObject> identityParcelableWrapperOfSerializableMethod( + List<SerializableObject> l) { + return l; + } + + @CrossProfile + public List<List<String>> identityParcelableWrapperOfParcelableWrapperMethod( + List<List<String>> l) { + return l; + } + + @CrossProfile + public String[] identityStringArrayMethod(String[] s) { + return s; + } + + @CrossProfile + public ListenableFuture<String[]> asyncIdentityStringArrayMethod(String[] s) { + return immediateFuture(s); + } + + @CrossProfile + public Collection<String[]> identityCollectionOfStringArrayMethod(Collection<String[]> c) { + return c; + } + + @CrossProfile + public ParcelableObject[] identityParcelableObjectArrayMethod(ParcelableObject[] p) { + return p; + } + + @CrossProfile + public SerializableObject[] identitySerializableObjectArrayMethod(SerializableObject[] s) { + return s; + } + + @CrossProfile + public Collection<ParcelableObject[]> identityCollectionOfParcelableObjectArrayMethod( + Collection<ParcelableObject[]> c) { + return c; + } + + @CrossProfile + public Collection<SerializableObject[]> identityCollectionOfSerializableObjectArrayMethod( + Collection<SerializableObject[]> c) { + return c; + } + + // @CrossProfile + // public TestProto[] identityProtoArrayMethod(TestProto[] p) { + // return p; + // } + + @CrossProfile + public Pair<String, Integer> identityPairMethod(Pair<String, Integer> p) { + return p; + } + + @CrossProfile + public Optional<ParcelableObject> identityGuavaOptionalMethod(Optional<ParcelableObject> p) { + return p; + } + + @CrossProfile + public Bitmap identityBitmapMethod(Bitmap p) { + return p; + } + + @CrossProfile + public NotReallySerializableObject identityNotReallySerializableObjectMethod( + NotReallySerializableObject n) { + return n; + } + + @CrossProfile + public NotReallySerializableObject returnNotReallySerializableObjectMethod() { + return new NotReallySerializableObject(new ParcelableObject("")); + } + + @CrossProfile + public void asyncGetNotReallySerializableObjectMethod( + TestNotReallySerializableObjectCallbackListener callbackListener) { + callbackListener.callback(new NotReallySerializableObject(new ParcelableObject("TEST"))); + } + + @CrossProfile + public ListenableFuture<NotReallySerializableObject> + futureGetNotReallySerializableObjectMethod() { + return immediateFuture(new NotReallySerializableObject(new ParcelableObject("TEST"))); + } + + @CrossProfile + public CustomWrapper<String> identityCustomWrapperMethod(CustomWrapper<String> c) { + return c; + } + + @CrossProfile + public ListenableFuture<CustomWrapper<String>> listenableFutureIdentityCustomWrapperMethod( + CustomWrapper<String> c) { + return immediateFuture(c); + } + + @CrossProfile + public void asyncIdentityCustomWrapperMethod( + CustomWrapper<String> c, TestCustomWrapperCallbackListener callbackListener) { + callbackListener.customWrapperCallback(c); + } + + @CrossProfile + public CustomWrapper2<String> identityCustomWrapper2Method(CustomWrapper2<String> c) { + return c; + } + + @CrossProfile + public SimpleFuture<String> simpleFutureIdentityStringMethodWithNonBlockingDelay( + String s, int secondsDelay) { + SimpleFuture<String> future = new SimpleFuture<>(); + + new Handler().postDelayed(() -> future.set(s), TimeUnit.SECONDS.toMillis(secondsDelay)); + + return future; + } + + @CrossProfile + public StringWrapper identityStringWrapperMethod(StringWrapper s) { + return s; + } + + @CrossProfile + public int getUserId() { + return android.os.Process.myUid() / 100000; + } + + @CrossProfile + public ListenableFuture<Void> killApp() { + android.os.Process.killProcess(android.os.Process.myPid()); + return immediateVoidFuture(); + } + + @CrossProfile + public boolean isContextArgumentPassed(Context context) { + return context != null; + } + + @CrossProfile + public void asyncIsContextArgumentPassed( + Context contextArg, TestBooleanCallbackListener callback) { + callback.booleanCallback(isContextArgumentPassed(contextArg)); + } + + @CrossProfile + public ListenableFuture<Boolean> futureIsContextArgumentPassed(Context contextArg) { + return immediateFuture(isContextArgumentPassed(contextArg)); + } + + @CrossProfile + public String identityStringMethodDeclaresButDoesNotThrowIOException(String s) + throws IOException { + return s; + } + + @CrossProfile + public String identityStringMethodThrowsIOException(String s) + throws IOException { + throw new IOException("Requested to throw"); + } + + @CrossProfile + public String identityStringMethodDeclaresIOExceptionThrowsSQLException(String s) + throws IOException, SQLException { + throw new SQLException("Requested to throw"); + } + + @CrossProfile + public void asyncMethodWithNonSimpleCallback( + NonSimpleCallbackListener callback, String s1, String s2) { + callback.callback(s1, s2); + } + + @CrossProfile + public void asyncMethodWithNonSimpleCallbackCallsSecondMethod( + NonSimpleCallbackListener callback, String s1, String s2) { + callback.callback2(s1, s2); + } +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/types/TestCrossProfileTypeWhichDoesNotSpecifyConnector.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/types/TestCrossProfileTypeWhichDoesNotSpecifyConnector.java new file mode 100644 index 0000000..0785ab0 --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/types/TestCrossProfileTypeWhichDoesNotSpecifyConnector.java @@ -0,0 +1,26 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp.types; + +import com.google.android.enterprise.connectedapps.annotations.CrossProfile; + +public class TestCrossProfileTypeWhichDoesNotSpecifyConnector { + + @CrossProfile + public String identityStringMethod(String s) { + return s; + } +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/types/TestCrossProfileTypeWhichNeedsContext.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/types/TestCrossProfileTypeWhichNeedsContext.java new file mode 100644 index 0000000..f98a5b1 --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/types/TestCrossProfileTypeWhichNeedsContext.java @@ -0,0 +1,159 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp.types; + +import static com.google.common.util.concurrent.Futures.immediateFuture; + +import android.content.Context; +import android.os.Handler; +import android.os.Looper; +import com.google.android.enterprise.connectedapps.annotations.CrossProfile; +import com.google.android.enterprise.connectedapps.exceptions.UnavailableProfileException; +import com.google.android.enterprise.connectedapps.testapp.ConnectorSingleton; +import com.google.android.enterprise.connectedapps.testapp.TestStringCallbackListener; +import com.google.android.enterprise.connectedapps.testapp.connector.TestProfileConnector; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; + +@CrossProfile(connector = TestProfileConnector.class) +public class TestCrossProfileTypeWhichNeedsContext { + + private static final int TEN_SECONDS = 10000; + + public static int voidMethodCalls = 0; + + private final Context context; + + private final ProfileTestCrossProfileType profileTestCrossProfileType; + + public TestCrossProfileTypeWhichNeedsContext(Context context) { + this.context = context; + this.profileTestCrossProfileType = + ProfileTestCrossProfileType.create(ConnectorSingleton.getConnector(context)); + } + + @CrossProfile // Timeout is not specified on type or method so will be default + public ListenableFuture<Void> listenableFutureMethodWhichNeverSetsTheValueWithDefaultTimeout() { + return SettableFuture.create(); + } + + @CrossProfile // Timeout is not specified on type or method so will be default + public void asyncMethodWhichNeverCallsBackWithDefaultTimeout( + TestStringCallbackListener callback) {} + + @CrossProfile + public void voidMethod() { + voidMethodCalls += 1; + } + + @CrossProfile + public void connectToOtherProfile() { + // This, when called cross-profile, causes the other profile to create a connection back to the + // original profile + ConnectorSingleton.getConnector(context).startConnecting(); + } + + @CrossProfile + public boolean isConnectedToOtherProfile() { + return ConnectorSingleton.getConnector(context).isConnected(); + } + + @CrossProfile + public String methodWhichCallsIdentityStringMethodOnOtherProfile(String s) { + try { + return profileTestCrossProfileType.other().identityStringMethod(s); + } catch (UnavailableProfileException e) { + throw new RuntimeException("Cannot call back to other profile", e); + } + } + + @CrossProfile + public void asyncMethodWhichCallsIdentityStringMethodOnOtherProfile( + String s, TestStringCallbackListener callback) { + profileTestCrossProfileType + .other() + .asyncIdentityStringMethod( + s, + callback, + throwable -> { + throw new RuntimeException(throwable); + }); + } + + @CrossProfile + public ListenableFuture<String> + listenableFutureMethodWhichCallsIdentityStringMethodOnOtherProfile(String s) { + return profileTestCrossProfileType.other().listenableFutureIdentityStringMethod(s); + } + + @CrossProfile + public String identityStringMethodWhichDelays10SecondsOnWorkProfile(String s) { + if (ConnectorSingleton.getConnector(context).utils().runningOnWork()) { + try { + Thread.sleep(TEN_SECONDS); + } catch (InterruptedException e) { + throw new RuntimeException("Sleep interrupted", e); + } + } + return s; + } + + @CrossProfile + public void asyncIdentityStringMethodWhichDelays10SecondsOnWorkProfile( + String s, TestStringCallbackListener callback) { + if (ConnectorSingleton.getConnector(context).utils().runningOnWork()) { + new Handler(Looper.getMainLooper()) + .postDelayed( + () -> { + callback.stringCallback(s); + }, + TEN_SECONDS); + } else { + callback.stringCallback(s); + } + } + + @CrossProfile + public ListenableFuture<String> futureIdentityStringMethodWhichDelays10SecondsOnWorkProfile( + String s) { + if (ConnectorSingleton.getConnector(context).utils().runningOnWork()) { + SettableFuture<String> future = SettableFuture.create(); + new Handler(Looper.getMainLooper()) + .postDelayed( + () -> { + future.set(s); + }, + TEN_SECONDS); + return future; + } + + return immediateFuture(s); + } + + @CrossProfile + public int getUserId() { + return android.os.Process.myUid() / 100000; + } + + @CrossProfile + public int getOtherUserId() { + try { + return profileTestCrossProfileType.other().getUserId(); + } catch (UnavailableProfileException e) { + throw new RuntimeException("Cannot call back to other profile", e); + } + } +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/types/TestInterfaceProvider.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/types/TestInterfaceProvider.java new file mode 100644 index 0000000..06c01ba --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/types/TestInterfaceProvider.java @@ -0,0 +1,26 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp.types; + +import com.google.android.enterprise.connectedapps.annotations.CrossProfileProvider; + +public class TestInterfaceProvider { + + @CrossProfileProvider + public TestCrossProfileInterface provideCrossProfileInterface() { + return s -> s; + } +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/types/TestProvider.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/types/TestProvider.java new file mode 100644 index 0000000..29473a8 --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/types/TestProvider.java @@ -0,0 +1,33 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp.types; + +import com.google.android.enterprise.connectedapps.annotations.CrossProfileProvider; + +@CrossProfileProvider(staticTypes = {NonInstantiableTestCrossProfileType.class}) +public class TestProvider { + + @CrossProfileProvider + public TestCrossProfileType provideTestCrossProfileType() { + return new TestCrossProfileType(); + } + + @CrossProfileProvider + public TestCrossProfileTypeWhichDoesNotSpecifyConnector + provideTestCrossProfileTypeWhichDoesNotSpecifyConnector() { + return new TestCrossProfileTypeWhichDoesNotSpecifyConnector(); + } +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/wrappers/ParcelableCustomWrapper.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/wrappers/ParcelableCustomWrapper.java new file mode 100644 index 0000000..a9545a2 --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/wrappers/ParcelableCustomWrapper.java @@ -0,0 +1,112 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp.wrappers; + +import android.os.Parcel; +import android.os.Parcelable; +import com.google.android.enterprise.connectedapps.annotations.CustomParcelableWrapper; +import com.google.android.enterprise.connectedapps.internal.Bundler; +import com.google.android.enterprise.connectedapps.internal.BundlerType; +import com.google.android.enterprise.connectedapps.testapp.CustomWrapper; + +@CustomParcelableWrapper(originalType = CustomWrapper.class) +public class ParcelableCustomWrapper<E> implements Parcelable { + + private static final int NULL = -1; + private static final int NOT_NULL = 1; + + private final Bundler bundler; + private final BundlerType type; + private final CustomWrapper<E> customWrapper; + + /** + * Create a wrapper for a given {@link CustomWrapper}. + * + * <p>The passed in {@link Bundler} must be capable of bundling {@code F}. + */ + public static <F> ParcelableCustomWrapper<F> of( + Bundler bundler, BundlerType type, CustomWrapper<F> customWrapper) { + return new ParcelableCustomWrapper<>(bundler, type, customWrapper); + } + + public CustomWrapper<E> get() { + return customWrapper; + } + + private ParcelableCustomWrapper( + Bundler bundler, BundlerType type, CustomWrapper<E> customWrapper) { + if (bundler == null || type == null) { + throw new NullPointerException(); + } + this.bundler = bundler; + this.type = type; + this.customWrapper = customWrapper; + } + + private ParcelableCustomWrapper(Parcel in) { + bundler = in.readParcelable(Bundler.class.getClassLoader()); + + int presentValue = in.readInt(); + + if (presentValue == NULL) { + type = null; + customWrapper = null; + return; + } + + type = (BundlerType) in.readParcelable(Bundler.class.getClassLoader()); + BundlerType valueType = type.typeArguments().get(0); + + @SuppressWarnings("unchecked") + E value = (E) bundler.readFromParcel(in, valueType); + + customWrapper = new CustomWrapper<>(value); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeParcelable(bundler, flags); + + if (customWrapper == null) { + dest.writeInt(NULL); + return; + } + + dest.writeInt(NOT_NULL); + dest.writeParcelable(type, flags); + BundlerType valueType = type.typeArguments().get(0); + bundler.writeToParcel(dest, customWrapper.value(), valueType, flags); + } + + @Override + public int describeContents() { + return 0; + } + + @SuppressWarnings("rawtypes") + public static final Creator<ParcelableCustomWrapper> CREATOR = + new Creator<ParcelableCustomWrapper>() { + @Override + public ParcelableCustomWrapper createFromParcel(Parcel in) { + return new ParcelableCustomWrapper(in); + } + + @Override + public ParcelableCustomWrapper[] newArray(int size) { + return new ParcelableCustomWrapper[size]; + } + }; +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/wrappers/ParcelableCustomWrapper2.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/wrappers/ParcelableCustomWrapper2.java new file mode 100644 index 0000000..4721445 --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/wrappers/ParcelableCustomWrapper2.java @@ -0,0 +1,113 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp.wrappers; + +import android.os.Parcel; +import android.os.Parcelable; +import com.google.android.enterprise.connectedapps.annotations.CustomParcelableWrapper; +import com.google.android.enterprise.connectedapps.internal.Bundler; +import com.google.android.enterprise.connectedapps.internal.BundlerType; +import com.google.android.enterprise.connectedapps.testapp.CustomWrapper; +import com.google.android.enterprise.connectedapps.testapp.CustomWrapper2; + +@CustomParcelableWrapper(originalType = CustomWrapper2.class) +public class ParcelableCustomWrapper2<E> implements Parcelable { + + private static final int NULL = -1; + private static final int NOT_NULL = 1; + + private final Bundler bundler; + private final BundlerType type; + private final CustomWrapper2<E> customWrapper; + + /** + * Create a wrapper for a given {@link CustomWrapper}. + * + * <p>The passed in {@link Bundler} must be capable of bundling {@code F}. + */ + public static <F> ParcelableCustomWrapper2<F> of( + Bundler bundler, BundlerType type, CustomWrapper2<F> customWrapper) { + return new ParcelableCustomWrapper2<>(bundler, type, customWrapper); + } + + public CustomWrapper2<E> get() { + return customWrapper; + } + + private ParcelableCustomWrapper2( + Bundler bundler, BundlerType type, CustomWrapper2<E> customWrapper) { + if (bundler == null || type == null) { + throw new NullPointerException(); + } + this.bundler = bundler; + this.type = type; + this.customWrapper = customWrapper; + } + + private ParcelableCustomWrapper2(Parcel in) { + bundler = in.readParcelable(Bundler.class.getClassLoader()); + + int presentValue = in.readInt(); + + if (presentValue == NULL) { + type = null; + customWrapper = null; + return; + } + + type = (BundlerType) in.readParcelable(Bundler.class.getClassLoader()); + BundlerType valueType = type.typeArguments().get(0); + + @SuppressWarnings("unchecked") + E value = (E) bundler.readFromParcel(in, valueType); + + customWrapper = new CustomWrapper2<>(value); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeParcelable(bundler, flags); + + if (customWrapper == null) { + dest.writeInt(NULL); + return; + } + + dest.writeInt(NOT_NULL); + dest.writeParcelable(type, flags); + BundlerType valueType = type.typeArguments().get(0); + bundler.writeToParcel(dest, customWrapper.value(), valueType, flags); + } + + @Override + public int describeContents() { + return 0; + } + + @SuppressWarnings("rawtypes") + public static final Creator<ParcelableCustomWrapper2> CREATOR = + new Creator<ParcelableCustomWrapper2>() { + @Override + public ParcelableCustomWrapper2 createFromParcel(Parcel in) { + return new ParcelableCustomWrapper2(in); + } + + @Override + public ParcelableCustomWrapper2[] newArray(int size) { + return new ParcelableCustomWrapper2[size]; + } + }; +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/wrappers/ParcelableStringWrapper.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/wrappers/ParcelableStringWrapper.java new file mode 100644 index 0000000..c7728f9 --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/wrappers/ParcelableStringWrapper.java @@ -0,0 +1,93 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp.wrappers; + +import android.os.Parcel; +import android.os.Parcelable; +import com.google.android.enterprise.connectedapps.annotations.CustomParcelableWrapper; +import com.google.android.enterprise.connectedapps.internal.Bundler; +import com.google.android.enterprise.connectedapps.internal.BundlerType; +import com.google.android.enterprise.connectedapps.testapp.StringWrapper; + +@CustomParcelableWrapper(originalType = StringWrapper.class) +public class ParcelableStringWrapper implements Parcelable { + + private static final int NULL = -1; + private static final int NOT_NULL = 1; + + private final StringWrapper stringWrapper; + + /** + * Create a wrapper for a given {@link StringWrapper}. + * + * <p>The passed in {@link Bundler} and {@link BundlerType} are ignored. + */ + public static ParcelableStringWrapper of( + Bundler bundler, BundlerType type, StringWrapper stringWrapper) { + return new ParcelableStringWrapper(bundler, type, stringWrapper); + } + + public StringWrapper get() { + return stringWrapper; + } + + private ParcelableStringWrapper(Bundler bundler, BundlerType type, StringWrapper stringWrapper) { + // Ignore bundler and type as we aren't generic + this.stringWrapper = stringWrapper; + } + + private ParcelableStringWrapper(Parcel in) { + int presentValue = in.readInt(); + + if (presentValue == NULL) { + stringWrapper = null; + return; + } + + String value = in.readString(); + + stringWrapper = new StringWrapper(value); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + if (stringWrapper == null) { + dest.writeInt(NULL); + return; + } + + dest.writeInt(NOT_NULL); + dest.writeString(stringWrapper.value()); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Creator<ParcelableStringWrapper> CREATOR = + new Creator<ParcelableStringWrapper>() { + @Override + public ParcelableStringWrapper createFromParcel(Parcel in) { + return new ParcelableStringWrapper(in); + } + + @Override + public ParcelableStringWrapper[] newArray(int size) { + return new ParcelableStringWrapper[size]; + } + }; +} diff --git a/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/wrappers/SimpleFutureWrapper.java b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/wrappers/SimpleFutureWrapper.java new file mode 100644 index 0000000..7ea8418 --- /dev/null +++ b/tests/shared/src/main/java/com/google/android/enterprise/connectedapps/testapp/wrappers/SimpleFutureWrapper.java @@ -0,0 +1,83 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.enterprise.connectedapps.testapp.wrappers; + +import com.google.android.enterprise.connectedapps.FutureWrapper; +import com.google.android.enterprise.connectedapps.Profile; +import com.google.android.enterprise.connectedapps.internal.Bundler; +import com.google.android.enterprise.connectedapps.internal.BundlerType; +import com.google.android.enterprise.connectedapps.internal.CrossProfileCallbackMultiMerger; +import com.google.android.enterprise.connectedapps.internal.FutureResultWriter; +import com.google.android.enterprise.connectedapps.testapp.SimpleFuture; +import java.util.Map; + +/** Wrapper for adding support for {@link SimpleFuture} to the Connected Apps SDK. */ +@com.google.android.enterprise.connectedapps.annotations.CustomFutureWrapper( + originalType = SimpleFuture.class) +public final class SimpleFutureWrapper<E> extends FutureWrapper<E> { + + private final SimpleFuture<E> future = new SimpleFuture<>(); + + public static <E> SimpleFutureWrapper<E> create(Bundler bundler, BundlerType bundlerType) { + return new SimpleFutureWrapper<>(bundler, bundlerType); + } + + public static <E> SimpleFuture<E> immediateFailedFuture(Throwable t) { + SimpleFuture<E> future = new SimpleFuture<>(); + future.setException(t); + return future; + } + + private SimpleFutureWrapper(Bundler bundler, BundlerType bundlerType) { + super(bundler, bundlerType); + } + + public SimpleFuture<E> getFuture() { + return future; + } + + @Override + public void onResult(E result) { + future.set(result); + } + + @Override + public void onException(Throwable throwable) { + future.setException(throwable); + } + + public static <E> void writeFutureResult( + SimpleFuture<E> future, FutureResultWriter<E> resultWriter) { + + future.setCallback(resultWriter::onSuccess, resultWriter::onFailure); + } + + public static <E> SimpleFuture<Map<Profile, E>> groupResults( + Map<Profile, SimpleFuture<E>> results) { + SimpleFuture<Map<Profile, E>> m = new SimpleFuture<>(); + + CrossProfileCallbackMultiMerger<E> merger = + new CrossProfileCallbackMultiMerger<>(results.size(), m::set); + for (Map.Entry<Profile, SimpleFuture<E>> result : results.entrySet()) { + result + .getValue() + .setCallback( + (value) -> merger.onResult(result.getKey(), value), + (throwable) -> merger.missingResult(result.getKey())); + } + return m; + } +} diff --git a/tests/shared/src/main/proto/connectedappssdk/TestProto2.proto b/tests/shared/src/main/proto/connectedappssdk/TestProto2.proto new file mode 100644 index 0000000..da3cd1c --- /dev/null +++ b/tests/shared/src/main/proto/connectedappssdk/TestProto2.proto @@ -0,0 +1,22 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +syntax = "proto2"; + +package connectedappssdk; + +message TestProto2 { + optional string text = 1; +} diff --git a/tests/shared/testapp/AndroidManifest.xml b/tests/shared/testapp/AndroidManifest.xml new file mode 100644 index 0000000..6a9b2de --- /dev/null +++ b/tests/shared/testapp/AndroidManifest.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2021 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<manifest + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + package="com.google.android.enterprise.connectedapps.shared.testapp"> + <application> + </application> +</manifest>
\ No newline at end of file diff --git a/tests/shared/testapp/build.gradle b/tests/shared/testapp/build.gradle new file mode 100644 index 0000000..16da3a4 --- /dev/null +++ b/tests/shared/testapp/build.gradle @@ -0,0 +1,41 @@ +buildscript { + repositories { + mavenCentral() + } +} + +plugins { + id 'com.android.library' +} + +dependencies { + api project(path: ':connectedapps-testapp_additional_types') + api project(path: ':connectedapps-testapp_basictypes') + api project(path: ':connectedapps-testapp_configuration') + api project(path: ':connectedapps-testapp_connector') + api project(path: ':connectedapps-testapp_types') + api project(path: ':connectedapps-testapp_types_providers') + api project(path: ':connectedapps-testapp_wrappers') + api project(path: ':connectedapps-testapp_crossuser') +} + +android { + defaultConfig { + compileSdkVersion 30 + minSdkVersion 26 + } + + testOptions.unitTests.includeAndroidResources = true + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + sourceSets { + main { + java.srcDirs = [] + manifest.srcFile 'AndroidManifest.xml' + } + } +} diff --git a/tests/shared/types/AndroidManifest.xml b/tests/shared/types/AndroidManifest.xml new file mode 100644 index 0000000..edfbe25 --- /dev/null +++ b/tests/shared/types/AndroidManifest.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2021 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<manifest + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + package="com.google.android.enterprise.connectedapps.shared.types"> + <application> + </application> +</manifest>
\ No newline at end of file diff --git a/tests/shared/types/build.gradle b/tests/shared/types/build.gradle new file mode 100644 index 0000000..b7f77f8 --- /dev/null +++ b/tests/shared/types/build.gradle @@ -0,0 +1,49 @@ +buildscript { + repositories { + mavenCentral() + } +} + +plugins { + id 'com.android.library' +} + +dependencies { + api project(path: ':connectedapps-testapp_basictypes') + api project(path: ':connectedapps-testapp_connector') + api project(path: ':connectedapps-testapp_wrappers') + + + implementation project(path: ':connectedapps') + implementation project(path: ':connectedapps-annotations') + implementation project(path: ':connectedapps-processor') + annotationProcessor project(path: ':connectedapps-processor') +} + +android { + defaultConfig { + compileSdkVersion 30 + minSdkVersion 26 + } + + testOptions.unitTests.includeAndroidResources = true + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + sourceSets { + main { + java.srcDirs = [file('../src/main/java')] + java.includes = [ + "com/google/android/enterprise/connectedapps/testapp/types/NonInstantiableTestCrossProfileType.java", + "com/google/android/enterprise/connectedapps/testapp/types/TestCrossProfileType.java", + "com/google/android/enterprise/connectedapps/testapp/types/TestCrossProfileTypeWhichDoesNotSpecifyConnector.java", + "com/google/android/enterprise/connectedapps/testapp/types/TestCrossProfileTypeWhichNeedsContext.java", + "com/google/android/enterprise/connectedapps/testapp/types/TestProvider.java", + ] + manifest.srcFile 'AndroidManifest.xml' + } + } +} diff --git a/tests/shared/types_providers/AndroidManifest.xml b/tests/shared/types_providers/AndroidManifest.xml new file mode 100644 index 0000000..52d649a --- /dev/null +++ b/tests/shared/types_providers/AndroidManifest.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2021 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<manifest + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + package="com.google.android.enterprise.connectedapps.shared.types_providers"> + <application> + </application> +</manifest>
\ No newline at end of file diff --git a/tests/shared/types_providers/build.gradle b/tests/shared/types_providers/build.gradle new file mode 100644 index 0000000..9694b83 --- /dev/null +++ b/tests/shared/types_providers/build.gradle @@ -0,0 +1,44 @@ +buildscript { + repositories { + mavenCentral() + } +} + +plugins { + id 'com.android.library' +} + +dependencies { + api project(path: ':connectedapps-testapp_basictypes') + api project(path: ':connectedapps-testapp_connector') + api project(path: ':connectedapps-testapp_types') + + implementation project(path: ':connectedapps') + implementation project(path: ':connectedapps-annotations') + implementation project(path: ':connectedapps-processor') + annotationProcessor project(path: ':connectedapps-processor') +} + +android { + defaultConfig { + compileSdkVersion 30 + minSdkVersion 26 + } + + testOptions.unitTests.includeAndroidResources = true + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + sourceSets { + main { + java.srcDirs = [file('../src/main/java')] + java.includes = [ + "com/google/android/enterprise/connectedapps/testapp/types/SeparateBuildTargetProvider.java", + ] + manifest.srcFile 'AndroidManifest.xml' + } + } +} diff --git a/tests/shared/wrappers/AndroidManifest.xml b/tests/shared/wrappers/AndroidManifest.xml new file mode 100644 index 0000000..606e72a --- /dev/null +++ b/tests/shared/wrappers/AndroidManifest.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2021 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<manifest + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + package="com.google.android.enterprise.connectedapps.shared.wrappers"> + <application> + </application> +</manifest>
\ No newline at end of file diff --git a/tests/shared/wrappers/build.gradle b/tests/shared/wrappers/build.gradle new file mode 100644 index 0000000..d08e47f --- /dev/null +++ b/tests/shared/wrappers/build.gradle @@ -0,0 +1,45 @@ +buildscript { + repositories { + mavenCentral() + } +} + +plugins { + id 'com.android.library' +} + +dependencies { + api project(path: ':connectedapps-testapp_basictypes') + + implementation project(path: ':connectedapps') + implementation project(path: ':connectedapps-annotations') + implementation project(path: ':connectedapps-processor') + annotationProcessor project(path: ':connectedapps-processor') +} + +android { + defaultConfig { + compileSdkVersion 30 + minSdkVersion 26 + } + + testOptions.unitTests.includeAndroidResources = true + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + sourceSets { + main { + java.srcDirs = [file('../src/main/java')] + java.includes = [ + "com/google/android/enterprise/connectedapps/testapp/wrappers/ParcelableCustomWrapper.java", + "com/google/android/enterprise/connectedapps/testapp/wrappers/ParcelableCustomWrapper2.java", + "com/google/android/enterprise/connectedapps/testapp/wrappers/ParcelableStringWrapper.java", + "com/google/android/enterprise/connectedapps/testapp/wrappers/SimpleFutureWrapper.java", + ] + manifest.srcFile 'AndroidManifest.xml' + } + } +} |