Skip to content

Do not create new map when testing for containsOnly and containsOnlyKeys for maps - #2160

Closed
filiphr wants to merge 1 commit into
assertj:mainfrom
filiphr:2159
Closed

filiphr wants to merge 1 commit into
assertj:mainfrom
filiphr:2159

Conversation

@filiphr

@filiphr filiphr commented Apr 6, 2021

Copy link
Copy Markdown
Contributor

Check List:

@joel-costigliola

Copy link
Copy Markdown
Member

We have a similar issue for assertContainsExactly when it checks that the entries are in the same order and compares keys with equals.
@filiphr would you like to fix that too? no problem if you can't, I'm happy to do it.

@filiphr

filiphr commented Apr 7, 2021

Copy link
Copy Markdown
Contributor Author

I didn't have much ideas about the containsExactly one, but I just got an idea. I'll try something out shortly

@filiphr

filiphr commented Apr 7, 2021

Copy link
Copy Markdown
Contributor Author

@joel-costigliola I've adapted the PR to include the fix for the assertContainsExactly problem as well.

Map.Entry<K, V> actualEntry = entry(keyFromActual, actual.get(keyFromActual));
for (Map.Entry<K, V> entryFromActual : actual.entrySet()) {
V value = actual.get(entries[index].getKey());
if (!areEqual(value, entryFromActual.getValue())) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comparing only the values is not enough, the following test should fail but succeeds with this implementation because all the keys have the same value:

Map<String, String> actual = new LinkedHashMap<>(2);
actual.put("a", "1");
actual.put("b", "1");
maps.assertContainsExactly(INFO, actual, entry("b", "1"), entry("a", "1"));

We could solve the problem if we were able to use the actual map way to compare keys to compare actualKeys[i] to entries[i].key but I'm not sure this is possible.

To be pragmatic I have created #2165 and I will remove the containsExactly changes from this PR when integrating (don't bother pushing a commit for that)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right, I didn't think about that use case. Thanks for removing the changes in the contains exactly method.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

btw, it would be good to add the test case you shared in this commit to the codebase :)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good idea, I'll add it to the other tests

while (foundKeysIterator.hasNext()) {
K foundKey = foundKeysIterator.next();
V foundValue = actual.get(foundKey);
if (areEqual(foundValue, entry.getValue())) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure to see why we need to compare values when the assertion is only checking keys, am I missing something here?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we have the same issue as with containsExactly since we have to compare keys and we can't access actual's map key comparison strategy.

This test fails as it should but it does not report the correct error:

    actual = new CaseInsensitiveMap<>();
    actual.put("NAME", "green");
    actual.put("cool", "green");
    // THEN
    maps.assertContainsOnlyKeys(someInfo(), actual, "Name", "Color");

error is:

Expecting actual:
  {"cool"="green", "name"="green"}
to contain only following keys:
  ["Name", "Color"]
keys not found:
  ["Color"]
and keys not expected:
  ["name"]

"keys not found" is correctly reported but "keys not expected" should have shown "cool", it reports "name" instead which actually is a map key.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess the only way forward is to find a way to clone actual's map which might be possible using introspection to call a a no arg constructor and then populate the cloned map. Question is now: is there always a no arg constructor ...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right. What we can also do is to support sime well known case insensitive maps behind the scenes, and for everything else create a normal copy. Reusing constructors might lead to using unmodifiable / immutable maps. I can give it another go on Sunday afternoon, no access to a laptop before that

@scordio scordio Apr 11, 2021 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess the only way forward is to find a way to clone actual's map which might be possible using introspection to call a a no arg constructor and then populate the cloned map. Question is now: is there always a no arg constructor ...

Objenesis could deal with it and I think it would be fine to have it as a shaded dependency. However, unmodifiable / immutable maps might still be an issue as @filiphr mentioned.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking we could try to instantiate the map (I'm fine using Objenesis) and if it fails for whatever reason use a simple LinkedHashMap as in the existing implementation, that won't completely solve the issue but will work for a vast majority of use cases, finally we would mention this limitation in the javadoc.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have tried Objenesis and that did not go well with CaseInsensitiveMap, creating the instance worked but would not initialize transient HashEntry<K, V>[] data; which is accessed later on when putting actual entries in the new map.

Interestingly it works with ConstructorInvoker (some old internal class in our codebase) and it worked.

Here's the stack trace for the record:

java.lang.NullPointerException
	at org.apache.commons.collections4.map.AbstractHashedMap.ensureCapacity(AbstractHashedMap.java:627)
	at org.apache.commons.collections4.map.AbstractHashedMap._putAll(AbstractHashedMap.java:325)
	at org.apache.commons.collections4.map.AbstractHashedMap.putAll(AbstractHashedMap.java:304)
	at org.assertj.core.internal.Maps.compareActualMapAndExpectedKeys(Maps.java:823)

@joel-costigliola

Copy link
Copy Markdown
Member

I think we should close this PR in favor of #2167 which tries the approach with cloning the actual map

@joel-costigliola

Copy link
Copy Markdown
Member

closing this PR in favor of #2167

@filiphr
filiphr deleted the 2159 branch April 26, 2021 13:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Map containsOnly assertions don't work correctly with case insensitive keys

3 participants