[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")
+    }
+}