carlos / todomvc-bakeoff Public

Clone
git clone https://amadan.net/carlos/todomvc-bakeoff

Plain git — no account needed to clone.

Download

Download this file

1// Package bakeoff is the bakeoff's measurement harness: it drives any app
2// that keeps CONTRACT.md and records what it measures in results/<slug>.json.
3package bakeoff
4
5import (
6 "fmt"
7 "io"
8 "net/http"
9 "net/http/cookiejar"
10 "net/url"
11 "regexp"
12 "strings"
13 "time"
14)
15
16// Session is one visitor: a cookie jar (so one list), an Origin header
17// matching the app, and a client that never follows redirects.
18type Session struct {
19 Base *url.URL
20 Client *http.Client
21 // ToggleID is the item load tests toggle, chosen once by Seed so a
22 // load step never has to fetch the page from an app under strain.
23 ToggleID string
24}
25
26// SessionNoFetch starts a visitor for the app at base without making any
27// request (so a sleeping instance stays asleep until the caller's first one).
28func SessionNoFetch(base string) (*Session, error) {
29 u, err := url.Parse(strings.TrimRight(base, "/"))
30 if err != nil {
31 return nil, err
32 }
33 jar, _ := cookiejar.New(nil)
34 return &Session{Base: u, Client: &http.Client{
35 Jar: jar,
36 Timeout: 60 * time.Second,
37 CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse },
38 }}, nil
39}
40
41// NewSession starts a visitor for the app at base and fetches / once so the
42// app sets its list cookie.
43func NewSession(base string) (*Session, error) {
44 s, err := SessionNoFetch(base)
45 if err != nil {
46 return nil, err
47 }
48 if _, err := s.get("/"); err != nil {
49 return nil, err
50 }
51 return s, nil
52}
53
54func (s *Session) origin() string { return s.Base.Scheme + "://" + s.Base.Host }
55
56func (s *Session) get(path string) (string, error) {
57 res, err := s.Client.Get(s.Base.String() + path)
58 if err != nil {
59 return "", err
60 }
61 defer res.Body.Close()
62 b, err := io.ReadAll(res.Body)
63 if res.StatusCode != 200 {
64 return "", fmt.Errorf("GET %s: %s", path, res.Status)
65 }
66 return string(b), err
67}
68
69func (s *Session) post(path string, form url.Values) error {
70 req, _ := http.NewRequest("POST", s.Base.String()+path, strings.NewReader(form.Encode()))
71 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
72 req.Header.Set("Origin", s.origin())
73 res, err := s.Client.Do(req)
74 if err != nil {
75 return err
76 }
77 io.Copy(io.Discard, res.Body)
78 res.Body.Close()
79 if res.StatusCode != http.StatusSeeOther {
80 return fmt.Errorf("POST %s: %s, want 303", path, res.Status)
81 }
82 return nil
83}
84
85// Add creates one item.
86func (s *Session) Add(title string) error {
87 return s.post("/todos", url.Values{"title": {title}, "filter": {"all"}})
88}
89
90// Toggle flips one item.
91func (s *Session) Toggle(id string) error {
92 return s.post("/todos/"+id+"/toggle", url.Values{"filter": {"all"}})
93}
94
95var liID = regexp.MustCompile(`<li\b[^>]*\bdata-id="([^"]+)"`)
96
97// IDs lists the item ids shown on the page at path, in order.
98func (s *Session) IDs(path string) ([]string, error) {
99 body, err := s.get(path)
100 if err != nil {
101 return nil, err
102 }
103 var ids []string
104 for _, m := range liID.FindAllStringSubmatch(body, -1) {
105 ids = append(ids, m[1])
106 }
107 return ids, nil
108}
109
110// CookieHeader is the Cookie header this visitor sends, for tools like oha.
111func (s *Session) CookieHeader() string {
112 var parts []string
113 for _, c := range s.Client.Jar.Cookies(s.Base) {
114 parts = append(parts, c.Name+"="+c.Value)
115 }
116 return strings.Join(parts, "; ")
117}
118
119// Seed adds "item 1".."item n" and completes every second one, returning
120// the ids in list order.
121func Seed(s *Session, n int) ([]string, error) {
122 for i := 1; i <= n; i++ {
123 if err := s.Add(fmt.Sprintf("item %d", i)); err != nil {
124 return nil, err
125 }
126 }
127 ids, err := s.IDs("/")
128 if err != nil {
129 return nil, err
130 }
131 if len(ids) != n {
132 return nil, fmt.Errorf("seeded %d items, page shows %d", n, len(ids))
133 }
134 for i := 1; i < n; i += 2 {
135 if err := s.Toggle(ids[i]); err != nil {
136 return nil, err
137 }
138 }
139 s.ToggleID = ids[0]
140 return ids, nil
141}
142