-
Notifications
You must be signed in to change notification settings - Fork 1.3k
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
fix: do not crash on unexpected map format in GenericMapTypeHandler #5062
Conversation
- detect incorrect map format (object instead of array) early, log expected format ``` "mapName": [ { "key": "...", "value": "..." } ] ``` - add null check to avoid crashing if key "key" not found in case of incorrect map format (no map entry for "key") - add null check to avoid crashing if key "value" not found in case of incorrect map format (no map entry for "value")
final Optional<K> key = keyHandler.deserialize(rawKey); | ||
if (key.isEmpty()) { | ||
logger.warn("Could not deserialize key '{}' as '{}'", rawKey, keyHandler.getClass().getSimpleName()); | ||
return Optional.empty(); |
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.
@keturn I saw the test cases in GenericMapTypeHandlerTest.java
and wanted to ask why it's desirable to have an empty map returned in case of a mismatched key / value handler...? My current assumption is that we might want that because failing to deserialize the ChangingBlocks
component should not result in the whole prefab deserialization failing (although I'm not even sure that would be the case when returning Optional.empty()
instead 🤔
Can you confirm this assumption or otherwise explain the reason for expecting an empty map in this case?
My intention with using Optional.empty()
here is to indicate that the serialization failed but not return a half-way deserialized map as might happen for instance in case the prefab holds:
myMap: [
{ "key": "myKey", "value": "myValue" },
{ "key2": "myKey2", "value2": "myValue2" }
]
With returning Optional.of(result)
, I would expect the first map entry to be put into the result map and the deserialization failing for the second entry, resulting in a half-way deserialized map which might lead to weird and potentially hard to debug behavior in game.
Taking the growing plant feature, for example, the plant would never grow to the second stage as the second stage never ended up in game because its deserialization failed.
Please correct me if I got any of the type handling logic etc. wrong leading to incorrect assumptions/expectations 😅
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.
It looks like I wrote those test cases after @skaldarnar wrote the implementation, so we should ask that guy too.
I'm struggling with how to answer these questions because I think you raise some good points, and—
- I'm not sure how to give advice on the best use of
Optional.empty
here, because I feel likeOptional
is bad at communicating error states and I've felt frustrated with trying to use it here myself, and - You don't want the result to be a partly-filled data structure. I empathize, but I think a partial response like that would be more consistent with the way the other deserialization methods are written. If I understand correctly, it seems to be working with a best-effort approach, where it'll do its best to return something even if it might have holes in it.
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.
Please add a test for the case that was crashing.
...y/src/main/java/org/terasology/persistence/typeHandling/coreTypes/GenericMapTypeHandler.java
Show resolved
Hide resolved
The type handler interface documents that Lines 39 to 45 in b367a18
When we talked about this offline, I raised similar concerns and comments to what @keturn mentioned above. Half-parsed components or prefabs are most likely no good to anybody, so being more strict is likely a good choice. To avoid a situation where we change it for one thing but not the others I'd like to take a bit of time to figure out how we actually want this code to behave and look like, and then do a sweep over it to improve all (or most) places where we use type handlers. In any case, I'd like to be able to assemble errors (and error message) bottom up to be able to present the user with a more meaningful error message on where they have to look to fix broken prefab. Coming from functional programming in Scala and ReasonML I'm fond of using types to express logic. In this particular case, I'm looking at the Result type, c.f. its variant in Kotlin. I think the "more Java" way is to use plain exceptions... I believe for starters we can try to work with what we already have in the type handler interface, and gradually improve it (with a bit of focus on it for the next week(s)). Let's make this type handler more strict, and also apply it to others. Try to make better use of the information that something went wrong higher hup in the stack. @keturn @jdrueckert what would be your take on this? |
done ✔️ |
Sounds fair to me. I adjusted the existing test case and added some more incl. a test case to test that a half-way valid map is still not half-way returned according to your "let's make this type handler more strict". |
"Expected\n" + | ||
" \"mapName\": [\n" + | ||
" { \"key\": \"...\", \"value\": \"...\" }\n" + | ||
" ]\n" + | ||
"but found \n'{}'", data); |
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.
Used in multiple places, maybe create a helper method to be used here and below.
"Expected\n" + | |
" \"mapName\": [\n" + | |
" { \"key\": \"...\", \"value\": \"...\" }\n" + | |
" ]\n" + | |
"but found \n'{}'", data); | |
getUsageInfo(data)); |
private String getUsageInfo(PersistedData data) {
return "Expected\n" +
" \"mapName\": [\n" +
" { \"key\": \"...\", \"value\": \"...\" }\n" +
" ]\n" +
"but found \n'" + data + "'";
}
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.
done ✔️
"Expected\n" + | ||
" \"mapName\": [\n" + | ||
" { \"key\": \"...\", \"value\": \"...\" }\n" + | ||
" ]\n" + | ||
"but found \n'{}'", data); |
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.
"Expected\n" + | |
" \"mapName\": [\n" + | |
" { \"key\": \"...\", \"value\": \"...\" }\n" + | |
" ]\n" + | |
"but found \n'{}'", data); | |
getUsageInfo(data)); |
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.
done ✔️
@@ -32,6 +31,39 @@ GenericMapTypeHandler.VALUE, new PersistedLong(TEST_VALUE) | |||
)) | |||
)); | |||
|
|||
private final PersistedData testDataMalformatted = new PersistedValueArray(List.of( |
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.
Maybe we can add the JSON equivalent we want to express here in a comment? Probably helps in both understanding the test case and figuring out if the data strucutre represents the thing we want to test.
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.
done ✔️
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.
😟 It's also the textbook example of "comment that's out of date as soon as you change the code."
If you think it helps despite that, go ahead and keep it, but I'm a little concerned.
Also note Javadoc does not have triple-quote code blocks. Either change these to javadoc syntax, or do not start the comment with the double-asterisk /**
so IntelliJ and javdoc don't treat it as javadoc.
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.
I've replaced the triple-quotes.
javadoc does not have triple-quote code blocks.
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.
Alright, lets go ahead with this.
I appreciate that you've covered a range of cases with the tests, but I'm curious: which case was it that you initially bumped in to and prompted this PR?
@DisplayName("A map containing both, correctly and incorrectly formatted data, fails deserialization (returns empty `Optional`)") | ||
void testDeserializeWithValidAndInvalidEntries() { | ||
var th = new GenericMapTypeHandler<>( | ||
new StringTypeHandler(), | ||
new LongTypeHandler() | ||
); | ||
|
||
assertThat(th.deserialize(testDataValidAndInvalidMix)).isEmpty(); |
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.
This is the case I'm a little worried about, since it's a change from the previous behavior. Not that the previous behavior was better, but it's one of those things where something might have been implicitly depending on it.
But this has a test case now, and the hypothetical I'm worrying about doesn't, so this wins.
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.
I don't think (or hope) something was depending on it. Rather, we'll see some things either work out better or even worse than before, hopefully just epxosing the underlying problem better.
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.
My expectation is that if this makes something work worse than before, then it's mainly making the underlying issue more visible than before.
I'm curious: which case was it that you initially bumped in to and prompted this PR?
I ran into the crash after making ChangingBlocks
use uris instead of strings for the map key.
The prefabs in PlantPack that I tested this were working before with the basic string key type map deserialization but didn't have the format required by the generic map type handler (outer array, entries with "key" and "value" keys). So the three malformatting test cases are covering this now.
Edit: Just saw that Skal already answered to this and with way more detail 😅 Thanks for that! ❤️
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.
@keturn The issue we found was due to the prefab using the JSON object format to specify the mapping after switching the type to be SimpleUri
.
{
"plantpack:radish1": 3000,
"plantpack:radish2": 2000
}
We currently have two variants for defining maps:
-
String
->V
if the keys are Strings, value can be anything, handled by a different type handler{ "plantpack:radish1": 3000, "plantpack:radish2": 2000 }
-
K
->V
where both keys and values can be arbitrary objects that can be de-/serialized[ { "key": "plantpack:radish1": , "value": 3000 }, { "key": "plantpack:radish2": , "value": 2000 }, ]
In the future, we probabyl want to add a third option (or replace/enhance the first one) if the key type
K
can be parsed from a plain String: -
K
->V
if the keys are Strings which can be parsed to some typeK
(e.g., all the URIs and Names), value can be anything, handled by a different type handler{ "plantpack:radish1": 3000, "plantpack:radish2": 2000 }
...c/test/java/org/terasology/persistence/typeHandling/coreTypes/GenericMapTypeHandlerTest.java
Outdated
Show resolved
Hide resolved
Co-authored-by: Tobias Nett <skaldarnar@googlemail.com>
Contains
How to test
Can be tested in combination with Terasology/PlantPack#20
Outstanding before merging