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
Reuse the container when requesting for a new nested container with the old key in JSONEncoder and PlistEncoder #20951
Reuse the container when requesting for a new nested container with the old key in JSONEncoder and PlistEncoder #20951
Conversation
0953217
to
781b797
Compare
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.
Thanks for putting this together! I think this is pretty close — we could probably rephrase the precondition message to be a little bit more direct (suggestion inline), and a few tests to ensure the precondition is being tested would be good to have.
After this, we'll need to port the changes over to swift-corelibs-foundation as well.
let dictionary = NSMutableDictionary() | ||
self.container[_converted(key).stringValue] = dictionary | ||
let containerKey = _converted(key).stringValue | ||
let existingContainer = self.container[containerKey] |
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 feel that on the whole, this might be more idiomatically expressed as something like
let containerKey = _converted(key).stringValue
let dictionary: NSMutableDictionary
if let existingContainer = self.container[containerKey] {
precondition(existingContainer is NSMutableDictionary, "Attempt to re-encode into nested KeyedEncodingContainer<\(Key.self)> for key \"\(containerKey)\" is invalid: non-keyed container already encoded for this key")
dictionary = existingContainer as! NSMutableDictionary
} else {
dictionary = NSMutableDictionary()
self.container[containerKey] = dictionary
}
Same for plist
@@ -510,8 +514,12 @@ fileprivate struct _JSONKeyedEncodingContainer<K : CodingKey> : KeyedEncodingCon | |||
} | |||
|
|||
public mutating func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer { | |||
let array = NSMutableArray() | |||
self.container[_converted(key).stringValue] = array | |||
let containerKey = _converted(key).stringValue |
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.
Similarly, I think we can match here with
let containerKey = _converted(key).stringValue
let array: NSMutableArray
if let existingContainer = self.container[containerKey] {
precondition(existingContainer is NSMutableArray, "Attempt to re-encode into nested UnkeyedEncodingContainer for key \"\(containerKey)\" is invalid: keyed container/single value already encoded for this key")
array = existingContainer as! NSMutableArray
} else {
array = NSMutableArray()
self.container[containerKey] = array
}
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 also thought about this approach too but just end up with the one in PR. Will update to this style
test/stdlib/TestJSONEncoder.swift
Outdated
} | ||
} | ||
|
||
enum TopLevelCodingKeys : String, CodingKey { |
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.
Can these key types be declared within Model
itself? Just as a scoping thing
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.
Sure
@@ -1615,6 +1671,7 @@ JSONEncoderTests.test("testEncodingTopLevelStructuredSingleClass") { TestJSONEnc | |||
JSONEncoderTests.test("testEncodingTopLevelDeepStructuredType") { TestJSONEncoder().testEncodingTopLevelDeepStructuredType()} | |||
JSONEncoderTests.test("testEncodingClassWhichSharesEncoderWithSuper") { TestJSONEncoder().testEncodingClassWhichSharesEncoderWithSuper() } | |||
JSONEncoderTests.test("testEncodingTopLevelNullableType") { TestJSONEncoder().testEncodingTopLevelNullableType() } | |||
JSONEncoderTests.test("testEncodingMultipleNestedContainersWithTheSameTopLevelKey") { TestJSONEncoder().testEncodingMultipleNestedContainersWithTheSameTopLevelKey() } |
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.
We should add tests to ensure that re-encoding into different containers fails. Each test runs in its own process, so the expected fatal error will not take down the rest of the tests. I believe that the right way to assert that we expect a test to crash is JSONEncoderTests.test("<test name here>").xfail(.always).code { TestJSONEncoder().<whatever> }
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 didn’t know this trick. I manually tested it and wait for it to fail then delete the code. This is great!!! I’ll update my test for this
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.
If you want it to crash, you shouldn't XFAIL it at all. You should use expectCrash(withMessage: "message goes here") { … }
in the body of the 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.
@jrose-apple Wasn't aware of that — is that new?
@pitiphong-p Sorry to have given bad info; we should probably do what Jordan suggests instead of XFAIL'ing the 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.
Nope, it's been around! There just aren't that many crash tests, I guess.
@itaiferber Already change as you requested. Please have a look again :) |
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.
A few small leftover nits but the rest looks great! If you're willing to make these final tiny changes, I think we'll be good to go.
test/stdlib/TestJSONEncoder.swift
Outdated
} | ||
} | ||
|
||
func testEncodingConfilictedTypeNestedContainersWithTheSameTopLevelKey() { |
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.
Nit: small typo — s/Confilicted/Conflicted
test/stdlib/TestJSONEncoder.swift
Outdated
} | ||
|
||
func testEncodingConfilictedTypeNestedContainersWithTheSameTopLevelKey() { | ||
struct Model : Codable, Equatable { |
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.
Since only the encode
side is being tested here, we can simplify this by dropping the Decodable
side of things and SecondNestedCodingKeys
var firstNestedContainer = container.nestedContainer(keyedBy: FirstNestedCodingKeys.self, forKey: .top) | ||
try firstNestedContainer.encode(self.first, forKey: .first) | ||
|
||
var secondNestedContainer = container.nestedUnkeyedContainer(forKey: .top) |
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 would be good to explicitly comment here (and below before the _testRoundTrip
to indicate that this access is invalid, and that we expect this to assert (and that the test is explicitly xfail
'd). (The xfail
comment below covers this well, but for someone just looking at the test, the behavior alone is confusing unless they know to scroll all the way down to look for the xfail
)
test/stdlib/TestPlistEncoder.swift
Outdated
} | ||
|
||
func testEncodingConfilictedTypeNestedContainersWithTheSameTopLevelKey() { | ||
struct Model : Codable, Equatable { |
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.
Same typo and Decodable
comments here
@itaiferber @xwu Thank you. I added and changed as you ask already. Please have a look :) |
@pitiphong-p Everything looks good to me, short of switching over to Jordan's suggestion to |
@itaiferber OK I'll rebase the branch against the current develop branch and fix the test as Jordan's suggestion |
…he Same Top Level Key case Fixed test
…with the same key in JSONEncoder
…with the same key in PlistEncoder
…ested container with the same key
… message on the condition
…ed mismatched container type
a519e49
to
61238ed
Compare
@itaiferber @jrose-apple I rebased my branch against the current master to fix the conflicted already. Also change the test to use the Please have a look and feel free to give me an advice on the issue related to |
What was the message you were trying to match? |
This is a message I got on the failed test (which should succeed). I was trying to find the stderr but couldn't find one. So I'm not sure what's the full message that was thrown |
That definitely sounds like it should work. You should be able to see the full crash and stderr by running the command printed at the bottom of the StdlibUnittest output, the one that has |
@jrose-apple Thank you for the trick. Above is the output from my console with the given option |
That…definitely seems like the message is there. Hm. |
@jrose-apple Any clue that I can use for further investigation? |
@pitiphong-p Is it sufficient to just leave the crash message empty for now? I don't think we particularly care about the exact wording of the message, and if it allows the test to pass, that might be enough. |
I'm unhappy that we don't know why it's not working, but yeah, that is an option. |
@itaiferber it’s sufficient to leave the crash message empty at the moment. The test will pass. Also I already leave the crash message empty at the moment. You can trigger Swift CI to run test if you want to |
Let's just get this testing and I'll see if I can get some time to dig into why the crash message doesn't work. |
@swift-ci Please test macOS |
@swift-ci Please smoke test Linux |
Hi @itaiferber, sorry to ping you here but the tests are all passed so how should we proceed on this PR |
@jrose-apple Do you have any further objections to merging this now and investigating the crash message separately? |
No. I mostly just don't want to lose it! |
Hi, @itaiferber @jrose-apple, any update on this PR? Sorry for the ping though |
*sigh* I guess we both thought the other was going to merge it. Let me run the tests again, since it's been a month and a half! @swift-ci Please test |
This comment has been minimized.
This comment has been minimized.
@pitiphong-p Sorry about that! I had planned on merging but this was right before vacation, and it slipped through the cracks. My fault. @jrose-apple Thanks for merging :) |
@itaiferber @jrose-apple Thank both of you for helping me on this PR 😄 One more question, should I open another PR onto the swift-5? |
I think we've missed the boat on that at this point. The release managers are pretty much only taking very bad regressions and ABI issues. |
OK I got it. Thank you again :) |
At the moment, JSONEncoder and PlistEncoder always create a new container when asking for nested keyed container and nested unkeyed container. It works fine for most of the case. But this causes a bug in the case when a type may need to ask for 2 separate nested containers for the same key.
This PR change the logic of both encoders to check for the existing container and reuse it if one found, otherwise will create a new container like the old behavior.
Resolves SR-9385.