Skip to content
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

Merged

Conversation

pitiphong-p
Copy link
Contributor

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.

@pitiphong-p pitiphong-p force-pushed the encoders-nested-key-edge-case-bug branch from 0953217 to 781b797 Compare December 3, 2018 05:06
@itaiferber itaiferber self-requested a review December 3, 2018 16:23
Copy link
Contributor

@itaiferber itaiferber left a 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]
Copy link
Contributor

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
Copy link
Contributor

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
}

Copy link
Contributor Author

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

}
}

enum TopLevelCodingKeys : String, CodingKey {
Copy link
Contributor

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

Copy link
Contributor Author

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() }
Copy link
Contributor

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> }

Copy link
Contributor Author

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

Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Contributor

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.

@pitiphong-p
Copy link
Contributor Author

@itaiferber Already change as you requested. Please have a look again :)

Copy link
Contributor

@itaiferber itaiferber left a 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.

}
}

func testEncodingConfilictedTypeNestedContainersWithTheSameTopLevelKey() {
Copy link
Contributor

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

}

func testEncodingConfilictedTypeNestedContainersWithTheSameTopLevelKey() {
struct Model : Codable, Equatable {
Copy link
Contributor

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)
Copy link
Contributor

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)

}

func testEncodingConfilictedTypeNestedContainersWithTheSameTopLevelKey() {
struct Model : Codable, Equatable {
Copy link
Contributor

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

test/stdlib/TestPlistEncoder.swift Outdated Show resolved Hide resolved
test/stdlib/TestJSONEncoder.swift Outdated Show resolved Hide resolved
@pitiphong-p
Copy link
Contributor Author

@itaiferber @xwu Thank you. I added and changed as you ask already. Please have a look :)

@itaiferber
Copy link
Contributor

itaiferber commented Dec 7, 2018

@pitiphong-p Everything looks good to me, short of switching over to Jordan's suggestion to expectCrash. Looks like we have some merge conflicts now though 😕 If we can resolve those I'll kick off the tests.

@pitiphong-p
Copy link
Contributor Author

@itaiferber OK I'll rebase the branch against the current develop branch and fix the test as Jordan's suggestion

@pitiphong-p pitiphong-p force-pushed the encoders-nested-key-edge-case-bug branch from a519e49 to 61238ed Compare December 8, 2018 09:48
@pitiphong-p
Copy link
Contributor Author

@itaiferber @jrose-apple I rebased my branch against the current master to fix the conflicted already. Also change the test to use the expectCrash instead of the xfail too. However I can't manage to get the expectCrash(withMessage: ) variant to success. It always said that it can't find the expected message

Please have a look and feel free to give me an advice on the issue related to expectCrash(withMessage: )

@jrose-apple
Copy link
Contributor

What was the message you were trying to match?

@pitiphong-p
Copy link
Contributor Author

stderr>>> FAIL: saw expected "crashed: sigill", but without message "Attempt to re-encode into nested UnkeyedEncodingContainer for key "top" is invalid: keyed container/single value already encoded for this key" before it

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

@jrose-apple
Copy link
Contributor

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 --stdlib-unittest-in-process in it.

@pitiphong-p
Copy link
Contributor Author

StdlibUnittest: using filter: testEncodingConflictedTypeNestedContainersWithTheSameTopLevelKey
[ RUN ] TestJSONEncoder.testEncodingConflictedTypeNestedContainersWithTheSameTopLevelKey
STDLIB_UNITTEST;expectCrash;false
STDLIB_UNITTEST;expectCrash;Attempt to re-encode into nested UnkeyedEncodingContainer for key "top" is invalid: keyed container/single value already encoded for this key
Illegal instruction: 4

@jrose-apple Thank you for the trick. Above is the output from my console with the given option

@jrose-apple
Copy link
Contributor

That…definitely seems like the message is there. Hm.

@pitiphong-p
Copy link
Contributor Author

@jrose-apple Any clue that I can use for further investigation?

@itaiferber
Copy link
Contributor

@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.

@jrose-apple
Copy link
Contributor

I'm unhappy that we don't know why it's not working, but yeah, that is an option.

@pitiphong-p
Copy link
Contributor Author

@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

@itaiferber
Copy link
Contributor

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.

@itaiferber
Copy link
Contributor

@swift-ci Please test macOS

@itaiferber
Copy link
Contributor

@swift-ci Please smoke test Linux

@pitiphong-p
Copy link
Contributor Author

Hi @itaiferber, sorry to ping you here but the tests are all passed so how should we proceed on this PR

@itaiferber
Copy link
Contributor

@jrose-apple Do you have any further objections to merging this now and investigating the crash message separately?

@jrose-apple
Copy link
Contributor

No. I mostly just don't want to lose it!

@pitiphong-p
Copy link
Contributor Author

pitiphong-p commented Feb 4, 2019

Hi, @itaiferber @jrose-apple, any update on this PR? Sorry for the ping though

@jrose-apple
Copy link
Contributor

*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

@jrose-apple jrose-apple self-assigned this Feb 4, 2019
@swift-ci

This comment has been minimized.

@jrose-apple jrose-apple merged commit b0f5815 into apple:master Feb 4, 2019
@itaiferber
Copy link
Contributor

@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 :)

@pitiphong-p pitiphong-p deleted the encoders-nested-key-edge-case-bug branch February 5, 2019 04:16
@pitiphong-p
Copy link
Contributor Author

pitiphong-p commented Feb 5, 2019

@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?

@jrose-apple
Copy link
Contributor

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.

@pitiphong-p
Copy link
Contributor Author

OK I got it. Thank you again :)

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.

None yet

5 participants