-
Notifications
You must be signed in to change notification settings - Fork 31
fix: correctly check equality for CaseInsensitiveMap #1235
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
62159ad
8a87bd0
60c8a1b
366cb04
0ba6bb7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| { | ||
| "id": "4820850c-8916-47f5-a7e1-8880e6a00d22", | ||
| "type": "bugfix", | ||
| "description": "Fix errors in equality checks for `CaseInsensitiveMap` which affect `Headers` and `ValuesMap` implementations" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| package aws.smithy.kotlin.runtime.collections | ||
|
|
||
| internal class CaseInsensitiveMutableStringSet( | ||
| initialValues: Iterable<CaseInsensitiveString> = setOf(), | ||
| ) : MutableSet<String> { | ||
| private val delegate = initialValues.toMutableSet() | ||
|
|
||
| override fun add(element: String) = delegate.add(element.toInsensitive()) | ||
| override fun clear() = delegate.clear() | ||
| override fun contains(element: String) = delegate.contains(element.toInsensitive()) | ||
| override fun containsAll(elements: Collection<String>) = elements.all { it in this } | ||
| override fun equals(other: Any?) = other is CaseInsensitiveMutableStringSet && delegate == other.delegate | ||
| override fun hashCode() = delegate.hashCode() | ||
| override fun isEmpty() = delegate.isEmpty() | ||
| override fun remove(element: String) = delegate.remove(element.toInsensitive()) | ||
| override val size: Int get() = delegate.size | ||
| override fun toString() = delegate.toString() | ||
|
|
||
| override fun addAll(elements: Collection<String>) = | ||
| elements.fold(false) { modified, item -> add(item) || modified } | ||
|
|
||
| override fun iterator() = object : MutableIterator<String> { | ||
| val delegate = [email protected]() | ||
| override fun hasNext() = delegate.hasNext() | ||
| override fun next() = delegate.next().normalized | ||
| override fun remove() = delegate.remove() | ||
| } | ||
|
|
||
| override fun removeAll(elements: Collection<String>) = | ||
| elements.fold(false) { modified, item -> remove(item) || modified } | ||
|
|
||
| override fun retainAll(elements: Collection<String>): Boolean { | ||
| val insensitiveElements = elements.map { it.toInsensitive() }.toSet() | ||
| val toRemove = delegate.filterNot { it in insensitiveElements } | ||
| return toRemove.fold(false) { modified, item -> delegate.remove(item) || modified } | ||
| } | ||
| } | ||
|
|
||
| internal fun CaseInsensitiveMutableStringSet(initialValues: Iterable<String>) = | ||
| CaseInsensitiveMutableStringSet(initialValues.map { it.toInsensitive() }) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| package aws.smithy.kotlin.runtime.collections | ||
|
|
||
| internal class CaseInsensitiveString(val original: String) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing tests for CaseInsensitiveString. It is tested indirectly through the other classes though
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Tested indirectly but you're right, it'd be better to have dedicated tests. Will add! |
||
| val normalized = original.lowercase() | ||
| override fun hashCode() = normalized.hashCode() | ||
| override fun equals(other: Any?) = other is CaseInsensitiveString && normalized == other.normalized | ||
| override fun toString() = original | ||
| } | ||
|
|
||
| internal fun String.toInsensitive(): CaseInsensitiveString = | ||
| CaseInsensitiveString(this) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,125 @@ | ||
| package aws.smithy.kotlin.runtime.collections | ||
|
|
||
| import kotlin.test.* | ||
|
|
||
| private val input = setOf("APPLE", "banana", "cHeRrY") | ||
| private val variations = (input + input.map { it.lowercase() } + input.map { it.uppercase() }) | ||
| private val disjoint = setOf("durIAN", "ELdeRBerRY", "FiG") | ||
| private val subset = input - "APPLE" | ||
| private val intersecting = subset + disjoint | ||
|
|
||
| class CaseInsensitiveMutableStringSetTest { | ||
| private fun assertSize(size: Int, set: CaseInsensitiveMutableStringSet) { | ||
| assertEquals(size, set.size) | ||
| val emptyAsserter: (Boolean) -> Unit = if (size == 0) ::assertTrue else ::assertFalse | ||
| emptyAsserter(set.isEmpty()) | ||
| } | ||
|
|
||
| @Test | ||
| fun testInitialization() { | ||
| val set = CaseInsensitiveMutableStringSet(input) | ||
| assertSize(3, set) | ||
| } | ||
|
|
||
| @Test | ||
| fun testAdd() { | ||
| val set = CaseInsensitiveMutableStringSet(input) | ||
| set += "durIAN" | ||
| assertSize(4, set) | ||
| } | ||
|
|
||
| @Test | ||
| fun testAddAll() { | ||
| val set = CaseInsensitiveMutableStringSet(input) | ||
| assertFalse(set.addAll(set)) | ||
|
|
||
| val intersecting = input + "durian" | ||
| assertTrue(set.addAll(intersecting)) | ||
| assertSize(4, set) | ||
| } | ||
|
|
||
| @Test | ||
| fun testClear() { | ||
| val set = CaseInsensitiveMutableStringSet(input) | ||
| set.clear() | ||
| assertSize(0, set) | ||
| } | ||
|
|
||
| @Test | ||
| fun testContains() { | ||
| val set = CaseInsensitiveMutableStringSet(input) | ||
| variations.forEach { assertTrue("Set should contain element $it") { it in set } } | ||
|
|
||
| assertFalse("durian" in set) | ||
| } | ||
|
|
||
| @Test | ||
| fun testContainsAll() { | ||
| val set = CaseInsensitiveMutableStringSet(input) | ||
| assertTrue(set.containsAll(variations)) | ||
|
|
||
| val intersecting = input + "durian" | ||
| assertFalse(set.containsAll(intersecting)) | ||
| } | ||
|
|
||
| @Test | ||
| fun testEquality() { | ||
| val left = CaseInsensitiveMutableStringSet(input) | ||
| val right = CaseInsensitiveMutableStringSet(input) | ||
| assertEquals(left, right) | ||
|
|
||
| left -= "apple" | ||
| assertNotEquals(left, right) | ||
|
|
||
| right -= "ApPlE" | ||
| assertEquals(left, right) | ||
| } | ||
|
|
||
| @Test | ||
| fun testIterator() { | ||
| val set = CaseInsensitiveMutableStringSet(input) | ||
| val iterator = set.iterator() | ||
|
|
||
| assertTrue(iterator.hasNext()) | ||
| assertEquals("apple", iterator.next()) | ||
|
|
||
| assertTrue(iterator.hasNext()) | ||
| assertEquals("banana", iterator.next()) | ||
| iterator.remove() | ||
| assertSize(2, set) | ||
|
|
||
| assertTrue(iterator.hasNext()) | ||
| assertEquals("cherry", iterator.next()) | ||
|
|
||
| assertFalse(iterator.hasNext()) | ||
| assertTrue(set.containsAll(input - "banana")) | ||
| } | ||
|
|
||
| @Test | ||
| fun testRemove() { | ||
| val set = CaseInsensitiveMutableStringSet(input) | ||
| set -= "BANANA" | ||
| assertSize(2, set) | ||
| } | ||
|
|
||
| @Test | ||
| fun testRemoveAll() { | ||
| val set = CaseInsensitiveMutableStringSet(input) | ||
| assertFalse(set.removeAll(disjoint)) | ||
|
|
||
| assertTrue(set.removeAll(intersecting)) | ||
| assertSize(1, set) | ||
| assertTrue("apple" in set) | ||
| } | ||
|
|
||
| @Test | ||
| fun testRetainAll() { | ||
| val set = CaseInsensitiveMutableStringSet(input) | ||
| assertFalse(set.retainAll(set)) | ||
| assertSize(3, set) | ||
|
|
||
| assertTrue(set.retainAll(intersecting)) | ||
| assertSize(2, set) | ||
| assertTrue(set.containsAll(subset)) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| package aws.smithy.kotlin.runtime.collections | ||
|
|
||
| import kotlin.test.Test | ||
| import kotlin.test.assertEquals | ||
| import kotlin.test.assertNotEquals | ||
|
|
||
| class CaseInsensitiveStringTest { | ||
| @Test | ||
| fun testEquality() { | ||
| val left = "Banana".toInsensitive() | ||
| val right = "baNAna".toInsensitive() | ||
| assertEquals(left, right) | ||
| assertNotEquals<Any>("Banana", left) | ||
| assertNotEquals<Any>("baNAna", right) | ||
|
|
||
| val nonMatching = "apple".toInsensitive() | ||
| assertNotEquals(nonMatching, left) | ||
| assertNotEquals(nonMatching, right) | ||
| } | ||
|
|
||
| @Test | ||
| fun testProperties() { | ||
| val s = "BANANA".toInsensitive() | ||
| assertEquals("BANANA", s.original) | ||
| assertEquals("banana", s.normalized) | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could be simplified to
elements.any { add(it) }, same applies forremoveAllThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No,
anyshort-circuits once it finds a match. If we used that, we'd only ever add at most a single item.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh ok. I updated the implementation before posting this comment and all the tests still passed, but it's probably just the way they're written