Read the JSON of a Newtonsoft contract test the way a response is read, and say what a date-like string is - #2478
Merged
Merged
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## 14.x #2478 +/- ##
==========================================
+ Coverage 93.64% 93.67% +0.02%
==========================================
Files 279 279
Lines 7903 7903
Branches 885 885
==========================================
+ Hits 7401 7403 +2
+ Misses 311 310 -1
+ Partials 191 190 -1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
GraphsonSupportTest turned the JSON of a contract test into a token with JToken.Parse. A response is not read that way: DeferToNewtonsoftConverterFactory sets DateParseHandling.None on its reader, so that a string stays a string and Gremlinq's own converters decide whether it is a date. JToken.Parse leaves the default, DateParseHandling.DateTime, under which a string that looks like a date is a Date token before any converter has seen it. For such strings the contract tests took a path no response ever takes, and answered differently, depending on the machine's time zone. Measured in Europe/Berlin, with JToken.Parse and as a response is read: - "2020-01-02T03:04:05+02:00" as a DateTimeOffset: 02:04:05 +01:00, against 01:04:05 +00:00. As a DateTime: 02:04:05 Local, against 01:04:05 Utc. - The same string as an object: a DateTime, against the string. As a string: "01/02/2020 02:04:05", against the string as it came. - "2020-01-02T03:04:05Z" as an object: a DateTime, against the string. CreateNativeToken now reads as DeferToNewtonsoftConverterFactory does: UTF-8 bytes through a StreamReader and a JsonTextReader with DateParseHandling.None, everything else on the reader as it is created, and the token built by a default JsonSerializer. That last part differs from JToken.Load in ways no test depends on today, and is mirrored so that none comes to depend on the difference: a name that occurs twice is kept at its last place rather than its first, a comment is kept as a token, and text after the token is not an error. No snapshot changes: no existing contract test has a string that looks like a date and would be read as anything but the UTC date it names. All 299 tests pass as before. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…asked for The contract tests had three strings that look like a date, all naming UTC and all asked for as a date. What a string naming an offset is read as, what one naming none is read as, and what either is when no date is asked for, was said nowhere - and until the Newtonsoft tests read their JSON the way a response is read, it could not have been said: they answered with what Newtonsoft's own date parsing made of the string, in the machine's time zone. Fifteen tests, after DateTimeOffset_from_invalid_string, each answered alike by both implementations: - A string naming an offset is read as the instant it names and handed back in UTC: "2020-01-02T03:04:05+02:00" is 01:04:05 +0 as a DateTimeOffset and 01:04:05 Utc as a DateTime. A fraction of a second is kept down to the tick. DateTimeOffset_from_double is the counterpart DateTime_from_double did not have. - A string naming no offset, "2020-01-02", is the local midnight of the machine reading it. The instant differs from machine to machine, so these two tests hold that it is that local midnight instead of the instant. The DateTimeOffset is compared by its instant alone: its offset is UTC on Newtonsoft and the machine's own on System.Text.Json, wherever the machine is not in UTC. - Asked for as a string or as an object, the string is the string it came as: on its own with UTC, with an offset and as a date alone, in an array read as object[], and as a value of a g:Map read as an object. - As a property of an entity it is what the property says. Person.RegistrationDate reads it as the date, Person.Name keeps it as the string, and a RegistrationDate that is no date costs the property, not the person. The Newtonsoft tests, 314 now, pass in Europe/Berlin, America/New_York and Asia/Kolkata alike, and so do these on System.Text.Json. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ds one in Read as a response is read, no token of a contract test holds a date any more, so the arms of DateTimeConverterFactory and DateTimeOffsetConverterFactory that take a JValue holding a DateTime were reached by nothing: Codecov counted three lines less on the pull request, in these two files alone. The arms are not dead. A token that a caller read or built on its own may hold a date, and the deserializer is handed it all the same. Four tests in TransformerTest hand in such a token: - A DateTime from a token holding a DateTime is that DateTime as it is held, its kind included: one of unspecified kind stays unspecified and is not moved. - A DateTime from a token holding a DateTimeOffset is the instant in UTC. - A DateTimeOffset from a token holding a DateTime is that instant. - A DateTimeOffset from a token holding a DateTimeOffset keeps its offset. This arm was covered by nothing before either. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
danielcweber
force-pushed
the
fix/contract-test-date-parsing
branch
from
September 30, 2026 22:46
489b6be to
7e82345
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The Newtonsoft contract tests now read their JSON the way a server response is read, with
DateParseHandling.None, so that a string that looks like a date reaches Gremlinq's converters as the string it is. Until now they usedJToken.Parse, which turns such a string into a date before any converter has seen it, in the machine's time zone - a path no response takes, and one on which both deserializers could not be held to the same answer. With that gone, the contract says what a date-like string is read as: a date where a date is asked for, and the string it came as everywhere else.Changes
GraphsonSupportTest.CreateNativeTokenmirrorsDeferToNewtonsoftConverterFactory: UTF-8 bytes through aStreamReaderand aJsonTextReaderwithDateParseHandling.None, everything else on the reader as it is created (FloatParseHandling.Double, aMaxDepthof 64, the invariant culture), and the token built by a defaultJsonSerializerrather than byJToken.Load. The last part is mirrored too, because it is not the same: a name that occurs twice is kept at its last place rather than its first, a comment is kept as a token, and text after the token is not an error.TransformerTesthand the deserializer aJValuethat holds aDateTimeor aDateTimeOffset. No token of a response holds a date, so with the contract tests reading as a response is read, the arms ofDateTimeConverterFactoryandDateTimeOffsetConverterFactorythat take such a token were reached by nothing - yet a token a caller read or built on its own may hold one. The date it holds is the date that is read, as it is held: aDateTimekeeps its kind, aDateTimeOffsetits offset.DateTimeOffset_from_invalid_string, each answered alike by Newtonsoft and System.Text.Json:DateTimeOffsetand as aDateTime: the instant it names, in UTC. A fraction of a second is kept down to the tick.DateTimeOffset_from_doubleis the counterpartDateTime_from_doubledid not have."2020-01-02", as aDateTimeand as aDateTimeOffset: the local midnight of the machine reading it. The instant differs from machine to machine, so these two hold that it is that local midnight rather than the instant.stringand as anobject- naming UTC, naming an offset, a date alone, in an array read asobject[]and as a value of ag:Map: the string it came as.Person.RegistrationDatereads it as the date,Person.Namekeeps it as the string, and aRegistrationDatethat is no date costs the property, not the person.Notes
src/changes, henceskip-changelog."2020-01-02T03:04:05+02:00"as aDateTimeOffsetwas02:04:05 +01:00withJToken.Parseand is01:04:05 +00:00as a response is read; as anobjectit was aDateTimeand is the string; as astringit was01/02/2020 02:04:05and is the string as it came.codecov/projectfail at 93.55% (-0.04%), three lines in the two date converters named above and nowhere else. The fourTransformerTesttests are the answer to that; the arm ofDateTimeOffsetConverterFactoryfor a token holding aDateTimeOffsethad been covered by nothing before either.Support.NewtonsoftJson.Teststhat parse withJObject.Parse(TransformerTest,JTokenHeuristicTests,DynamicDictionaryTests) are left as they are: none of their literals has a date-like string, a repeated name or a comment, so both ways of reading give them the same token.Issue219already goes through the reader a response goes through.DateTimeOffsetread from a string naming none (UTC on Newtonsoft, the machine's own on System.Text.Json, the instant being the same),"/Date(1577934245000)/"(a date on Newtonsoft through its own fallback, declined by System.Text.Json), and dates at the very ends of the range.🤖 Generated with Claude Code