diff --git a/core/build.gradle b/core/build.gradle index d6551b4e4d..b66fb294c9 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -31,6 +31,9 @@ dependencies { testCompile 'org.assertj:assertj-core:3.4.1' testCompile "commons-fileupload:commons-fileupload:1.3.1" + // Guava: Google test library (collections test suite) + testCompile "com.google.guava:guava-testlib:19.0" + compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" compile "org.jetbrains.kotlin:kotlin-test:$kotlin_version" diff --git a/core/src/main/kotlin/com/r3corda/core/utilities/NonEmptySet.kt b/core/src/main/kotlin/com/r3corda/core/utilities/NonEmptySet.kt new file mode 100644 index 0000000000..89739639e5 --- /dev/null +++ b/core/src/main/kotlin/com/r3corda/core/utilities/NonEmptySet.kt @@ -0,0 +1,83 @@ +package com.r3corda.core.utilities + +/** + * A set which is constrained to ensure it can never be empty. An initial value must be provided at + * construction, and attempting to remove the last element will cause an IllegalStateException. + */ +class NonEmptySet(initial: T, private val set: MutableSet = mutableSetOf()) : MutableSet { + init { + require (set.isEmpty()) { "Provided set must be empty." } + set.add(initial) + } + + override val size: Int + get() = set.size + + override fun add(element: T): Boolean = set.add(element) + override fun addAll(elements: Collection): Boolean = set.addAll(elements) + override fun clear() = throw UnsupportedOperationException() + override fun contains(element: T): Boolean = set.contains(element) + override fun containsAll(elements: Collection): Boolean = set.containsAll(elements) + override fun isEmpty(): Boolean = false + + override fun iterator(): MutableIterator = Iterator(set.iterator()) + + override fun remove(element: T): Boolean = + // Test either there's more than one element, or the removal is a no-op + if (size > 1) + set.remove(element) + else if (!contains(element)) + false + else + throw IllegalStateException() + + override fun removeAll(elements: Collection): Boolean = + if (size > elements.size) + set.removeAll(elements) + else if (!containsAll(elements)) + // Remove the common elements + set.removeAll(elements) + else + throw IllegalStateException() + + override fun retainAll(elements: Collection): Boolean { + val iterator = iterator() + val ret = false + + // The iterator will throw an IllegalStateException if we try removing the last element + while (iterator.hasNext()) { + if (!elements.contains(iterator.next())) { + iterator.remove() + } + } + + return ret + } + + override fun equals(other: Any?): Boolean = + if (other is Set<*>) + // Delegate down to the wrapped set's equals() function + set.equals(other) + else + false + + override fun hashCode(): Int = set.hashCode() + override fun toString(): String = set.toString() + + inner class Iterator(val iterator: MutableIterator) : MutableIterator { + override fun hasNext(): Boolean = iterator.hasNext() + override fun next(): T = iterator.next() + override fun remove() = + if (set.size > 1) + iterator.remove() + else + throw IllegalStateException() + } +} + +fun nonEmptySetOf(initial: T, vararg elements: T): NonEmptySet { + val set = NonEmptySet(initial) + // We add the first element twice, but it's a set, so who cares + set.addAll(elements) + return set +} \ No newline at end of file diff --git a/core/src/test/kotlin/com/r3corda/core/utilities/NonEmptySetTest.kt b/core/src/test/kotlin/com/r3corda/core/utilities/NonEmptySetTest.kt new file mode 100644 index 0000000000..bb29d92a6c --- /dev/null +++ b/core/src/test/kotlin/com/r3corda/core/utilities/NonEmptySetTest.kt @@ -0,0 +1,100 @@ +package com.r3corda.core.utilities + +import com.google.common.collect.testing.SetTestSuiteBuilder +import com.google.common.collect.testing.TestIntegerSetGenerator +import com.google.common.collect.testing.features.CollectionFeature +import com.google.common.collect.testing.features.CollectionSize +import com.google.common.collect.testing.testers.CollectionAddAllTester +import com.google.common.collect.testing.testers.CollectionClearTester +import com.google.common.collect.testing.testers.CollectionRemoveAllTester +import com.google.common.collect.testing.testers.CollectionRetainAllTester +import junit.framework.TestSuite +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Suite +import kotlin.test.assertEquals + +@RunWith(Suite::class) +@Suite.SuiteClasses( + NonEmptySetTest.Guava::class, + NonEmptySetTest.Remove::class +) +class NonEmptySetTest { + /** + * Guava test suite generator for NonEmptySet. + */ + class Guava { + companion object { + @JvmStatic + fun suite(): TestSuite + = SetTestSuiteBuilder + .using(NonEmptySetGenerator()) + .named("test NonEmptySet with several values") + .withFeatures( + CollectionSize.SEVERAL, + CollectionFeature.ALLOWS_NULL_VALUES, + CollectionFeature.FAILS_FAST_ON_CONCURRENT_MODIFICATION, + CollectionFeature.GENERAL_PURPOSE + ) + // Kotlin throws the wrong exception in this cases + .suppressing(CollectionAddAllTester::class.java.getMethod("testAddAll_nullCollectionReference")) + // Disable tests that try to remove everything: + .suppressing(CollectionRemoveAllTester::class.java.getMethod("testRemoveAll_nullCollectionReferenceNonEmptySubject")) + .suppressing(CollectionClearTester::class.java.getMethods().toList()) + .suppressing(CollectionRetainAllTester::class.java.getMethods().toList()) + .createTestSuite() + } + } + + /** + * Test removal, which Guava's standard tests can't cover for us. + */ + class Remove { + @Test + fun `construction`() { + val expected = 17 + val basicSet = nonEmptySetOf(expected) + val actual = basicSet.first() + assertEquals(expected, actual) + } + + @Test(expected = IllegalStateException::class) + fun `remove sole element`() { + val basicSet = nonEmptySetOf(-17) + basicSet.remove(-17) + } + + @Test + fun `remove one of two elements`() { + val basicSet = nonEmptySetOf(-17, 17) + basicSet.remove(-17) + } + + @Test + fun `remove element which does not exist`() { + val basicSet = nonEmptySetOf(-17) + basicSet.remove(-5) + assertEquals(1, basicSet.size) + } + + @Test(expected = IllegalStateException::class) + fun `remove via iterator`() { + val basicSet = nonEmptySetOf(-17, 17) + val iterator = basicSet.iterator() + while (iterator.hasNext()) { + iterator.remove() + } + } + } +} + +/** + * Generator of non empty set instances needed for testing. + */ +class NonEmptySetGenerator : TestIntegerSetGenerator() { + override fun create(elements: Array?): NonEmptySet? { + val set = nonEmptySetOf(elements!!.first()) + set.addAll(elements.toList()) + return set + } +} \ No newline at end of file