-
Notifications
You must be signed in to change notification settings - Fork 383
HttpCookie and its tests #3927
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
base: main
Are you sure you want to change the base?
HttpCookie and its tests #3927
Conversation
I have signed the CLA |
) | ||
} | ||
|
||
override def equals(obj: Any): Boolean = { |
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 style point. I recommend moving the override
of hashCode
below
to be either just before or just after the override of equals()
.
That will make it easier for people reading the code who go on alert
when they see equals
being overridden to convince themselves
that hashCode
is properly overridden in co-ordination.
As long as one thing is moving, I suggest moving toString()
to
the same block so that overrides are together and easier to find.
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.
Late at night now. By the light of day I will try to post a URL
to the current "Best Practice" for an equals
method.
It helped me last week when I had to write an equals
for
fileKey
s. Ultimately, because of some wierdness with fileKey
,
I could not follow it exactly, but at least I knew what I was deviating from and why.
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.
Moved things around and implemented a new equals
as per the advice you linked
def getValue(): String = this._value | ||
def getVersion(): Int = this._version | ||
def hasExpired(): Boolean = this._maxAge == 0 | ||
override def hashCode(): Int = toString().hashCode() |
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 works and balances equals
.
There is a recommendation that I have never quite understood as to
using .##
instead of hashCode
when defining a hashCode method.
I believe that String
(and most other objects) have such. I believe
that .##
handles some special cases.
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.
There a are a number of override hashCode
implementations
in the javalib code base, so it is hard to point out one which
is 'Best (or at least current) Practice'. Answering the question of
'why is that one better (or gives a better distribution)' could take
a semester course in Maths.
The one in java.util.Arrays
gives a basic structure. It comes from
Scala.js, where folks are knowledgeable, experienced, and super strict.
A non array body would look something like the
example from nio/file/attribute/PosixFileAttributeViewImpl.scala
override def hashCode(): Int = {
var res = 1
res = 31 * res + deviceId.##
res = 31 * res + inodeNumber.##
res
}
The idea is a field-by-field comparison by accumulating and shifting the intermediate results. If the fields can be null, one must handle that (long discussion).
If I live a Good Life, Someday I may be saved from having to write
equals/hashcode pairs.
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.
The JVM seems to include the name, value, domain and path into the hashcode, so I changed mine to do so as well, using the formula you gave there.
val cookie = out.get(0) | ||
assertEquals("potato", cookie.getName()) | ||
assertEquals("tomato", cookie.getValue()) | ||
assertEquals(null, cookie.getComment()) |
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 comment based on my personal style. Authors have wide latitude
to follow almost any correct style.
When writing tests, I like to take a few moments to think about
how a person running the test might figure out what went wrong and
where.
When there is only one assert in a Test
, that is not a problem.
When there are 20 or 40 or so (some of the regex code), it can
mean many hours of tedious bi-section search.
assertEquals
and many of its kin have a three argument form,
where the first argument is a message. It takes a developer
minutes of time to place a message, but it can save a debugger hours.
Usually when debugging, time is of the essence: thing are not good and are probably going to get worse.
Here one can probably figure out the failure point if one of the
first two fails first two. I suspect that line 20 and kin would
cause some puzzlement.
A quick search shows unit-tests/shared/src/test/require-scala3-jdk12/org/scalanative/testsuite/javalib/util/stream/CollectorsTestOnJDK12.scala
following that practice.
Now meaningful messages are an ideal. In some cases where I
have had to figure out which of 5 or more assertions (especially
the dreaded assertTrue
& assertFalse
, which do not give values)
are failing and have less than zero time to do it, I fall back to
numbering the assertions "a1", "a2", etc. This gives me a brainless
way to distinguish them.
Yes, I hang my head that I should be taking the time to figure out a better message. That means figuring out what all sorts of quirky code
is supposed to be doing, where that code has nothing to do with my
presenting problem.
The downside is that when I add or delete assertions, I have to do the
tedious job of renumbering, so that later maintainers can
follow the intent. Paying for my transgressions.
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 takes a developer minutes of time to place a message, but it can save a debugger hours.
Lovely piece of advice :) Using CollectorsTestOnJDK12
as a template I tried to add some similar clues to the assertions, let me know if that's what you're looking for here.
assert(!HttpCookie.domainMatches("website.com", "awebsite.com")) | ||
} | ||
|
||
} |
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.
One can spend eternities one does not have writing tests
for failure cases. Users are so good at generating just
the failure case not detected by tests. Such is life.
The code goes to some length to detectIllegalArgumentException("Illegal cookie name")
.
Did I miss a Test for that? If not, you might want to consider adding it.
(is the "Illegal" in the message from JVM? If not, could it be changed
to something which does not keep me up all day wondering if the Sheriff's Deputy is coming for me because I typo-ed a name)
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.
is the "Illegal" in the message from JVM?
Yes, I tried to mimic the behavior of the JVM; at least to a reasonable degree. Added tests for that now as well
} | ||
|
||
} | ||
|
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.
There may need to be a javadoc/scaladoc comment here
so that the documentation build picks this up as a supported class.
@ekrich What is the current process for a newly supported class
to get picked up by the documentation?
I blinked and the process changed.
Thanks.
domaspoliakas Thank you for this contribution. I hope it is the first of many. Overall, it looks very good. It is nice to see the corresponding & careful Tests. I did my traditional "What documentation impacts does this PR have?" and Enough for tonight, I will check the log files by the light of day to see what is failing & try to |
To allow my mind to rest and focus on my current project, I did a spot test of perhaps 3 failing
Hope that helps. |
As promised, FYI: I found this article helpful. In it Alvin references at least two of his original sources. Alvin is careful An almost identical descriptions occur on If I understand Building up enough recent models of the current recommended practice will make it easier for people |
Any idea what the chance of a hash collision is from "concatenationOfAllFields".##? I sure do not |
Addressed some of your comments wrt equals, hashcode and tests. For hashcode I used Also it turns out the JVM keeps track of the time when the object was created for the purpose of evaluating |
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.
Thank you for the contribution! It seams to not be passing the CI - it's failing due to incompatibilities with the JVM semantics - we ran javalib tests on both JVM (first) and then using Native. Because it fails on the JVM it means that tests are incorrect and don't follow JVM semantics. You can run these locally using testsJVM3/test
def getValue(): String = this._value | ||
def getVersion(): Int = this._version | ||
def hasExpired(): Boolean = { | ||
_maxAge >= 0 && ((System |
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.
JVM would return true if maxAge == 0
, so we should start with 2 quick range checks before starting getting current time
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'm not sure I quite follow what you mean wrt range checks, let me know if the new version is ok
if (header.regionMatches(false, 0, "set-cookie2:", 0, 12)) { | ||
header.substring(12) | ||
} else if (header.regionMatches(false, 0, "set-cookie:", 0, 11)) | ||
header.substring(11) |
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.
You're always matching the start of the string you can simply use header.startsWith("...")
to make it more readable.
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.
startsWith
is case-sensitive, which is why I use regionMatches here
Addressed the PR comments now. There's one mismatch between the JVM behavior and my implementation that I'm still trying to track down though, I'll keep working on it in the meantime. |
@Test | ||
def stringEscapingTest(): Unit = { | ||
// I named this test "string escaping", but really it's more about how | ||
// the jvm does no such thing | ||
val out = | ||
HttpCookie.parse( | ||
"set-cookie2:potato=\"toma\\\"t,o=asdf\",second=second;comment=\"something;discard;portlist=123\"" | ||
) | ||
assertEquals("number of cookies", 2, out.size()) | ||
val cookieOne = out.get(0) | ||
assertCookie( | ||
cookieOne, | ||
"cookie one", | ||
name = "potato", | ||
value = "\"toma\\\"t" | ||
) | ||
// despite the fact that the first comma is in a quoted string - the JVM | ||
// still splits it into a separate cookie at that point. Hence we must do so as well | ||
// | ||
// Similarly for semicolons - the JVM doesn't seem to distinguish between semicolons | ||
// in strings and those outside, and hence we do the same | ||
val cookieTwo = out.get(1) | ||
assertCookie( | ||
cookieTwo, | ||
"cookie two", | ||
name = "o", | ||
value = "asdf\",second=second", | ||
comment = "\"something", | ||
discard = true | ||
) |
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.
The way the JVM splits on the comma inside a quoted string in one place, but then totally ignores another comma entirely is just baffling. I'll sleep on this and hopefully get it sorted over the weekend.
nameless.split(",") | ||
// splitUnquotedAt(nameless, ',') | ||
|
||
Arrays.asList(unprocessed).forEach { kv => |
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.
After changing splitUnquotedAt
to split
you're having a Array[String]
as a result, you can use foreach
directly on it without wrapping it into Java List
Arrays.asList(unprocessed).forEach { kv => | |
unprocessed.foreach { kv => |
Fixes #3921
Not a very strict implementation of the RFCs, but I find that it largely matches how the JVM behaves. I tried copying from netty at first (as netty is also apache 2.0), but ultimately found that implementing this was more about matching the quirks of the JVM rather the RFCs, so I ended up implementing something from scratch.
I'll add some more tests, but given that this is my first PR to the project I wanted to make sure that the PR is in order. As such let me know if there's anything else needed here!