Commit 6bbd12f1 authored by Volker Dobler's avatar Volker Dobler Committed by Nigel Tao

exp/cookiejar: make cookie sorting deterministic.

Re-enable TestUpdateAndDelete, TestExpiration, TestChromiumDomain and
TestChromiumDeletion on Windows.

Sorting of cookies with same path length and same creation
time is done by an additional seqNum field.
This makes the order in which cookies are returned in Cookies
deterministic, even if the system clock is manipulated or on
systems with a low-resolution clock.

The tests now use a synthetic time: This makes cookie testing
reliable in case of bogus system clocks and speeds up the
expiration tests.

R=nigeltao, alex.brainman, dave
CC=golang-dev
https://golang.org/cl/7323063
parent 556dd0bf
...@@ -63,6 +63,10 @@ type Jar struct { ...@@ -63,6 +63,10 @@ type Jar struct {
// entries is a set of entries, keyed by their eTLD+1 and subkeyed by // entries is a set of entries, keyed by their eTLD+1 and subkeyed by
// their name/domain/path. // their name/domain/path.
entries map[string]map[string]entry entries map[string]map[string]entry
// nextSeqNum is the next sequence number assigned to a new cookie
// created SetCookies.
nextSeqNum uint64
} }
// New returns a new cookie jar. A nil *Options is equivalent to a zero // New returns a new cookie jar. A nil *Options is equivalent to a zero
...@@ -78,7 +82,9 @@ func New(o *Options) (*Jar, error) { ...@@ -78,7 +82,9 @@ func New(o *Options) (*Jar, error) {
} }
// entry is the internal representation of a cookie. // entry is the internal representation of a cookie.
// The fields are those of RFC 6265. //
// This struct type is not used outside of this package per se, but the exported
// fields are those of RFC 6265.
type entry struct { type entry struct {
Name string Name string
Value string Value string
...@@ -91,6 +97,11 @@ type entry struct { ...@@ -91,6 +97,11 @@ type entry struct {
Expires time.Time Expires time.Time
Creation time.Time Creation time.Time
LastAccess time.Time LastAccess time.Time
// seqNum is a sequence number so that Cookies returns cookies in a
// deterministic order, even for cookies that have equal Path length and
// equal Creation time. This simplifies testing.
seqNum uint64
} }
// Id returns the domain;path;name triple of e as an id. // Id returns the domain;path;name triple of e as an id.
...@@ -135,11 +146,13 @@ type byPathLength []entry ...@@ -135,11 +146,13 @@ type byPathLength []entry
func (s byPathLength) Len() int { return len(s) } func (s byPathLength) Len() int { return len(s) }
func (s byPathLength) Less(i, j int) bool { func (s byPathLength) Less(i, j int) bool {
in, jn := len(s[i].Path), len(s[j].Path) if len(s[i].Path) != len(s[j].Path) {
if in == jn { return len(s[i].Path) > len(s[j].Path)
}
if !s[i].Creation.Equal(s[j].Creation) {
return s[i].Creation.Before(s[j].Creation) return s[i].Creation.Before(s[j].Creation)
} }
return in > jn return s[i].seqNum < s[j].seqNum
} }
func (s byPathLength) Swap(i, j int) { s[i], s[j] = s[j], s[i] } func (s byPathLength) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
...@@ -148,6 +161,11 @@ func (s byPathLength) Swap(i, j int) { s[i], s[j] = s[j], s[i] } ...@@ -148,6 +161,11 @@ func (s byPathLength) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
// //
// It returns an empty slice if the URL's scheme is not HTTP or HTTPS. // It returns an empty slice if the URL's scheme is not HTTP or HTTPS.
func (j *Jar) Cookies(u *url.URL) (cookies []*http.Cookie) { func (j *Jar) Cookies(u *url.URL) (cookies []*http.Cookie) {
return j.cookies(u, time.Now())
}
// cookies is like Cookies but takes the current time as a parameter.
func (j *Jar) cookies(u *url.URL, now time.Time) (cookies []*http.Cookie) {
if u.Scheme != "http" && u.Scheme != "https" { if u.Scheme != "http" && u.Scheme != "https" {
return cookies return cookies
} }
...@@ -165,7 +183,6 @@ func (j *Jar) Cookies(u *url.URL) (cookies []*http.Cookie) { ...@@ -165,7 +183,6 @@ func (j *Jar) Cookies(u *url.URL) (cookies []*http.Cookie) {
return cookies return cookies
} }
now := time.Now()
https := u.Scheme == "https" https := u.Scheme == "https"
path := u.Path path := u.Path
if path == "" { if path == "" {
...@@ -208,6 +225,11 @@ func (j *Jar) Cookies(u *url.URL) (cookies []*http.Cookie) { ...@@ -208,6 +225,11 @@ func (j *Jar) Cookies(u *url.URL) (cookies []*http.Cookie) {
// //
// It does nothing if the URL's scheme is not HTTP or HTTPS. // It does nothing if the URL's scheme is not HTTP or HTTPS.
func (j *Jar) SetCookies(u *url.URL, cookies []*http.Cookie) { func (j *Jar) SetCookies(u *url.URL, cookies []*http.Cookie) {
j.setCookies(u, cookies, time.Now())
}
// setCookies is like SetCookies but takes the current time as parameter.
func (j *Jar) setCookies(u *url.URL, cookies []*http.Cookie, now time.Time) {
if len(cookies) == 0 { if len(cookies) == 0 {
return return
} }
...@@ -225,7 +247,6 @@ func (j *Jar) SetCookies(u *url.URL, cookies []*http.Cookie) { ...@@ -225,7 +247,6 @@ func (j *Jar) SetCookies(u *url.URL, cookies []*http.Cookie) {
defer j.mu.Unlock() defer j.mu.Unlock()
submap := j.entries[key] submap := j.entries[key]
now := time.Now()
modified := false modified := false
for _, cookie := range cookies { for _, cookie := range cookies {
...@@ -249,16 +270,15 @@ func (j *Jar) SetCookies(u *url.URL, cookies []*http.Cookie) { ...@@ -249,16 +270,15 @@ func (j *Jar) SetCookies(u *url.URL, cookies []*http.Cookie) {
if old, ok := submap[id]; ok { if old, ok := submap[id]; ok {
e.Creation = old.Creation e.Creation = old.Creation
e.seqNum = old.seqNum
} else { } else {
e.Creation = now e.Creation = now
e.seqNum = j.nextSeqNum
j.nextSeqNum++
} }
e.LastAccess = now e.LastAccess = now
submap[id] = e submap[id] = e
modified = true modified = true
// Make Creation and LastAccess strictly monotonic forcing
// deterministic behaviour during sorting.
// TODO: check if this is conforming to RFC 6265.
now = now.Add(1 * time.Nanosecond)
} }
if modified { if modified {
...@@ -384,7 +404,7 @@ func (j *Jar) newEntry(c *http.Cookie, now time.Time, defPath, host string) (e e ...@@ -384,7 +404,7 @@ func (j *Jar) newEntry(c *http.Cookie, now time.Time, defPath, host string) (e e
e.Expires = endOfTime e.Expires = endOfTime
e.Persistent = false e.Persistent = false
} else { } else {
if c.Expires.Before(now) { if !c.Expires.After(now) {
return e, true, nil return e, true, nil
} }
e.Expires = c.Expires e.Expires = c.Expires
......
...@@ -14,6 +14,9 @@ import ( ...@@ -14,6 +14,9 @@ import (
"time" "time"
) )
// tNow is the synthetic current time used as now during testing.
var tNow = time.Date(2013, 1, 1, 12, 0, 0, 0, time.UTC)
// testPSL implements PublicSuffixList with just two rules: "co.uk" // testPSL implements PublicSuffixList with just two rules: "co.uk"
// and the default rule "*". // and the default rule "*".
type testPSL struct{} type testPSL struct{}
...@@ -199,9 +202,9 @@ func TestDomainAndType(t *testing.T) { ...@@ -199,9 +202,9 @@ func TestDomainAndType(t *testing.T) {
} }
} }
// expiresIn creates an expires attribute delta seconds from now. // expiresIn creates an expires attribute delta seconds from tNow.
func expiresIn(delta int) string { func expiresIn(delta int) string {
t := time.Now().Round(time.Second).Add(time.Duration(delta) * time.Second) t := tNow.Add(time.Duration(delta) * time.Second)
return "expires=" + t.Format(time.RFC1123) return "expires=" + t.Format(time.RFC1123)
} }
...@@ -216,9 +219,12 @@ func mustParseURL(s string) *url.URL { ...@@ -216,9 +219,12 @@ func mustParseURL(s string) *url.URL {
// jarTest encapsulates the following actions on a jar: // jarTest encapsulates the following actions on a jar:
// 1. Perform SetCookies with fromURL and the cookies from setCookies. // 1. Perform SetCookies with fromURL and the cookies from setCookies.
// (Done at time tNow + 0 ms.)
// 2. Check that the entries in the jar matches content. // 2. Check that the entries in the jar matches content.
// (Done at time tNow + 1001 ms.)
// 3. For each query in tests: Check that Cookies with toURL yields the // 3. For each query in tests: Check that Cookies with toURL yields the
// cookies in want. // cookies in want.
// (Query n done at tNow + (n+2)*1001 ms.)
type jarTest struct { type jarTest struct {
description string // The description of what this test is supposed to test description string // The description of what this test is supposed to test
fromURL string // The full URL of the request from which Set-Cookie headers where received fromURL string // The full URL of the request from which Set-Cookie headers where received
...@@ -235,6 +241,8 @@ type query struct { ...@@ -235,6 +241,8 @@ type query struct {
// run runs the jarTest. // run runs the jarTest.
func (test jarTest) run(t *testing.T, jar *Jar) { func (test jarTest) run(t *testing.T, jar *Jar) {
now := tNow
// Populate jar with cookies. // Populate jar with cookies.
setCookies := make([]*http.Cookie, len(test.setCookies)) setCookies := make([]*http.Cookie, len(test.setCookies))
for i, cs := range test.setCookies { for i, cs := range test.setCookies {
...@@ -244,11 +252,11 @@ func (test jarTest) run(t *testing.T, jar *Jar) { ...@@ -244,11 +252,11 @@ func (test jarTest) run(t *testing.T, jar *Jar) {
} }
setCookies[i] = cookies[0] setCookies[i] = cookies[0]
} }
jar.SetCookies(mustParseURL(test.fromURL), setCookies) jar.setCookies(mustParseURL(test.fromURL), setCookies, now)
now = now.Add(1001 * time.Millisecond)
// Serialize non-expired entries in the form "name1=val1 name2=val2". // Serialize non-expired entries in the form "name1=val1 name2=val2".
var cs []string var cs []string
now := time.Now().UTC()
for _, submap := range jar.entries { for _, submap := range jar.entries {
for _, cookie := range submap { for _, cookie := range submap {
if !cookie.Expires.After(now) { if !cookie.Expires.After(now) {
...@@ -268,8 +276,9 @@ func (test jarTest) run(t *testing.T, jar *Jar) { ...@@ -268,8 +276,9 @@ func (test jarTest) run(t *testing.T, jar *Jar) {
// Test different calls to Cookies. // Test different calls to Cookies.
for i, query := range test.queries { for i, query := range test.queries {
now = now.Add(1001 * time.Millisecond)
var s []string var s []string
for _, c := range jar.Cookies(mustParseURL(query.toURL)) { for _, c := range jar.cookies(mustParseURL(query.toURL), now) {
s = append(s, c.Name+"="+c.Value) s = append(s, c.Name+"="+c.Value)
} }
if got := strings.Join(s, " "); got != query.want { if got := strings.Join(s, " "); got != query.want {
...@@ -588,7 +597,6 @@ var updateAndDeleteTests = [...]jarTest{ ...@@ -588,7 +597,6 @@ var updateAndDeleteTests = [...]jarTest{
} }
func TestUpdateAndDelete(t *testing.T) { func TestUpdateAndDelete(t *testing.T) {
t.Skip("test is broken on windows/386") // issue 4823
jar := newTestJar() jar := newTestJar()
for _, test := range updateAndDeleteTests { for _, test := range updateAndDeleteTests {
test.run(t, jar) test.run(t, jar)
...@@ -596,29 +604,26 @@ func TestUpdateAndDelete(t *testing.T) { ...@@ -596,29 +604,26 @@ func TestUpdateAndDelete(t *testing.T) {
} }
func TestExpiration(t *testing.T) { func TestExpiration(t *testing.T) {
t.Skip("test is broken on windows/386") // issue 4823
jar := newTestJar() jar := newTestJar()
jarTest{ jarTest{
"Fill jar.", "Expiration.",
"http://www.host.test", "http://www.host.test",
[]string{ []string{
"a=1", "a=1",
"b=2; max-age=1", // should expire in 1 second "b=2; max-age=3",
"c=3; " + expiresIn(1), // should expire in 1 second "c=3; " + expiresIn(3),
"d=4; max-age=100", "d=4; max-age=5",
"e=5; " + expiresIn(5),
"f=6; max-age=100",
},
"a=1 b=2 c=3 d=4 e=5 f=6", // executed at t0 + 1001 ms
[]query{
{"http://www.host.test", "a=1 b=2 c=3 d=4 e=5 f=6"}, // t0 + 2002 ms
{"http://www.host.test", "a=1 d=4 e=5 f=6"}, // t0 + 3003 ms
{"http://www.host.test", "a=1 d=4 e=5 f=6"}, // t0 + 4004 ms
{"http://www.host.test", "a=1 f=6"}, // t0 + 5005 ms
{"http://www.host.test", "a=1 f=6"}, // t0 + 6006 ms
}, },
"a=1 b=2 c=3 d=4",
[]query{{"http://www.host.test", "a=1 b=2 c=3 d=4"}},
}.run(t, jar)
time.Sleep(1500 * time.Millisecond)
jarTest{
"Check jar.",
"http://www.host.test",
[]string{},
"a=1 d=4",
[]query{{"http://www.host.test", "a=1 d=4"}},
}.run(t, jar) }.run(t, jar)
} }
...@@ -885,7 +890,6 @@ var chromiumDomainTests = [...]jarTest{ ...@@ -885,7 +890,6 @@ var chromiumDomainTests = [...]jarTest{
} }
func TestChromiumDomain(t *testing.T) { func TestChromiumDomain(t *testing.T) {
t.Skip("test is broken on windows/amd64") // issue 4823
jar := newTestJar() jar := newTestJar()
for _, test := range chromiumDomainTests { for _, test := range chromiumDomainTests {
test.run(t, jar) test.run(t, jar)
...@@ -954,7 +958,6 @@ var chromiumDeletionTests = [...]jarTest{ ...@@ -954,7 +958,6 @@ var chromiumDeletionTests = [...]jarTest{
} }
func TestChromiumDeletion(t *testing.T) { func TestChromiumDeletion(t *testing.T) {
t.Skip("test is broken on windows/386") // issue 4823
jar := newTestJar() jar := newTestJar()
for _, test := range chromiumDeletionTests { for _, test := range chromiumDeletionTests {
test.run(t, jar) test.run(t, jar)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment