[AGP DSL] Add lockable set implementation
Not registered for use in AGP yet.
Bug: 140406102
Test: Added new unit test, will be used in a subsequent CL
Change-Id: Idffc39617596e0fd7efab126b2f751eb820522b8
diff --git a/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/dsl/decorator/LockableSet.kt b/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/dsl/decorator/LockableSet.kt
new file mode 100644
index 0000000..2443142
--- /dev/null
+++ b/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/dsl/decorator/LockableSet.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * 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
+ *
+ * http://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.android.build.gradle.internal.dsl.decorator
+
+import com.android.build.gradle.internal.dsl.AgpDslLockedException
+import com.android.build.gradle.internal.dsl.Lockable
+import javax.inject.Inject
+
+/**
+ * An implementation of a set for use in AGP DSL that can be locked.
+ *
+ * This set implementation preserves insertion order.
+ *
+ * This is intentionally not serializable, as model classes should take copies
+ * e.g. [com.google.common.collect.ImmutableList.copyOf]
+ */
+class LockableSet<T> @Inject constructor(
+ private val name: String
+) : java.util.AbstractSet<T>(), MutableSet<T>, Lockable {
+
+ private val delegate: MutableSet<T> = mutableSetOf()
+
+ private var locked = false
+
+ override fun lock() {
+ locked = true;
+ }
+
+ private inline fun <R>check(action: () -> R): R {
+ if (locked) {
+ throw AgpDslLockedException(
+ "It is too late to modify $name\n" +
+ "It has already been read to configure this project.\n" +
+ "Consider either moving this call to be during evaluation,\n" +
+ "or using the variant API."
+ )
+ }
+ return action.invoke()
+ }
+
+ override val size: Int get() = delegate.size
+
+ override fun add(element: T): Boolean = check {
+ delegate.add(element)
+ }
+
+ override fun iterator(): MutableIterator<T> {
+ return LockableIterator(delegate.iterator())
+ }
+
+ private inner class LockableIterator<T>(private val delegate: MutableIterator<T>): MutableIterator<T> {
+ override fun hasNext(): Boolean = delegate.hasNext()
+ override fun next(): T = delegate.next()
+ override fun remove() = check {
+ delegate.remove()
+ }
+ }
+
+}
diff --git a/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/dsl/decorator/SupportedPropertyType.kt b/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/dsl/decorator/SupportedPropertyType.kt
index 17110dc..956eb9a 100644
--- a/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/dsl/decorator/SupportedPropertyType.kt
+++ b/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/dsl/decorator/SupportedPropertyType.kt
@@ -60,6 +60,14 @@
Type.getType(Iterable::class.java),
),
)
+ object Set : Val(
+ Type.getType(kotlin.collections.Set::class.java),
+ implementationType = Type.getType(LockableSet::class.java),
+ bridgeTypes = listOf(
+ Type.getType(Collection::class.java),
+ Type.getType(Iterable::class.java),
+ ),
+ )
}
override fun toString(): String = "SupportedPropertyType(type=${type.className})"
diff --git a/build-system/gradle-core/src/test/java/com/android/build/gradle/internal/dsl/decorator/DslDecoratorUnitTest.kt b/build-system/gradle-core/src/test/java/com/android/build/gradle/internal/dsl/decorator/DslDecoratorUnitTest.kt
index 97ca0a2..28d6574 100644
--- a/build-system/gradle-core/src/test/java/com/android/build/gradle/internal/dsl/decorator/DslDecoratorUnitTest.kt
+++ b/build-system/gradle-core/src/test/java/com/android/build/gradle/internal/dsl/decorator/DslDecoratorUnitTest.kt
@@ -336,4 +336,23 @@
Eval.me("withList", withList, "withList.list = withList.list")
assertThat(withList.list).containsExactly("one", "two", "three").inOrder()
}
+
+ interface WithSet {
+ val set: MutableSet<String>
+ }
+
+ @Test
+ fun `check groovy setter generation for set`() {
+ val decorated = DslDecorator(listOf(SupportedPropertyType.Val.Set))
+ .decorate(WithSet::class)
+ val withSet = decorated.getDeclaredConstructor().newInstance()
+ assertThat(withSet.set::class.java).isEqualTo(LockableSet::class.java)
+ Eval.me("withSet", withSet, "withSet.set += ['one', 'two']")
+ assertThat(withSet.set).containsExactly("one", "two").inOrder()
+ Eval.me("withSet", withSet, "withSet.set += 'three'")
+ assertThat(withSet.set).containsExactly("one", "two", "three").inOrder()
+ // Check self-assignment preserves values
+ Eval.me("withSet", withSet, "withSet.set = withSet.set")
+ assertThat(withSet.set).containsExactly("one", "two", "three").inOrder()
+ }
}
diff --git a/build-system/gradle-core/src/test/java/com/android/build/gradle/internal/dsl/decorator/LockableListTest.kt b/build-system/gradle-core/src/test/java/com/android/build/gradle/internal/dsl/decorator/LockableListTest.kt
index 67cc851..399bbc6 100644
--- a/build-system/gradle-core/src/test/java/com/android/build/gradle/internal/dsl/decorator/LockableListTest.kt
+++ b/build-system/gradle-core/src/test/java/com/android/build/gradle/internal/dsl/decorator/LockableListTest.kt
@@ -24,6 +24,16 @@
internal class LockableListTest {
@Test
+ fun `check behaves as a list`() {
+ val lockableList = LockableList<String>("someStrings")
+ lockableList += "one"
+ lockableList += "one"
+ assertThat(lockableList).containsExactly("one", "one")
+ lockableList -= "one"
+ assertThat(lockableList).containsExactly("one")
+ }
+
+ @Test
fun `check list locking addition`() {
val lockableList = LockableList<String>("someStrings")
lockableList += "zero"
diff --git a/build-system/gradle-core/src/test/java/com/android/build/gradle/internal/dsl/decorator/LockableSetTest.kt b/build-system/gradle-core/src/test/java/com/android/build/gradle/internal/dsl/decorator/LockableSetTest.kt
new file mode 100644
index 0000000..57c55c1
--- /dev/null
+++ b/build-system/gradle-core/src/test/java/com/android/build/gradle/internal/dsl/decorator/LockableSetTest.kt
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * 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
+ *
+ * http://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.android.build.gradle.internal.dsl.decorator
+
+import com.android.build.gradle.internal.dsl.AgpDslLockedException
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import kotlin.test.assertFailsWith
+
+internal class LockableSetTest {
+
+ @Test
+ fun `check behaves as a set`() {
+ val lockableSet = LockableSet<String>("someStrings")
+ lockableSet += "one"
+ lockableSet += "one"
+ assertThat(lockableSet).containsExactly("one")
+ lockableSet -= "one"
+ assertThat(lockableSet).isEmpty()
+ }
+
+ @Test
+ fun `check order is preserved`() {
+ val lockableSet = LockableSet<String>("someStrings")
+ lockableSet += "one"
+ lockableSet += "two"
+ assertThat(lockableSet).containsExactly("one", "two").inOrder()
+
+ val lockableSet2 = LockableSet<String>("someStrings2")
+ lockableSet2 += "two"
+ lockableSet2 += "one"
+ assertThat(lockableSet2).containsExactly("two", "one").inOrder()
+ }
+
+ @Test
+ fun `check set locking addition`() {
+ val lockableSet = LockableSet<String>("someStrings")
+ lockableSet += "one"
+ lockableSet += "two"
+ assertThat(lockableSet).containsExactly("one", "two")
+
+ lockableSet.lock()
+
+ val failure = assertFailsWith<AgpDslLockedException> {
+ lockableSet += "three"
+ }
+ assertThat(failure).hasMessageThat().contains("It is too late to modify someStrings")
+ assertThat(lockableSet).containsExactly("one", "two")
+ }
+
+
+ @Test
+ fun `check set locking removal`() {
+ val lockableSet = LockableSet<String>("someStrings")
+ lockableSet += setOf("one", "two")
+ assertThat(lockableSet).containsExactly("one", "two")
+ val result = lockableSet.remove("two")
+ assertThat(result).isTrue()
+ assertThat(lockableSet).containsExactly("one")
+
+ lockableSet.lock()
+
+ val failure = assertFailsWith<AgpDslLockedException> {
+ lockableSet.remove("one")
+ }
+ assertThat(failure).hasMessageThat().contains("It is too late to modify someStrings")
+ assertThat(lockableSet).containsExactly("one")
+ }
+
+
+ @Test
+ fun `check iterator locking removal`() {
+ val lockableSet = LockableSet<String>("someStrings")
+ lockableSet += setOf("one", "two")
+ val iterator = lockableSet.iterator()
+ assertThat(iterator.next()).isEqualTo("one")
+ assertThat(iterator.hasNext()).named("iterator.hasNext()").isTrue()
+ iterator.remove()
+ assertThat(iterator.next()).isEqualTo("two")
+ assertThat(lockableSet).containsExactly( "two")
+
+ lockableSet.lock()
+
+ val failure = assertFailsWith<AgpDslLockedException> {
+ iterator.remove()
+ }
+ assertThat(failure).hasMessageThat().contains("It is too late to modify someStrings")
+ assertThat(lockableSet).containsExactly( "two")
+ }
+}