Skip to content

Commit

Permalink
wip template
Browse files Browse the repository at this point in the history
Signed-off-by: Maxime Soulé <btik-git@scoubidou.com>
  • Loading branch information
maxatome committed Nov 17, 2022
1 parent a3fcff2 commit 8842b11
Show file tree
Hide file tree
Showing 10 changed files with 887 additions and 159 deletions.
12 changes: 12 additions & 0 deletions helpers/tdhttp/internal/any.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright (c) 2022, Maxime Soulé
// All rights reserved.
//
// This source code is licensed under the BSD-style license found in the
// LICENSE file in the root directory of this source tree.

//go:build !go1.18
// +build !go1.18

package internal

type any = interface{}
79 changes: 79 additions & 0 deletions helpers/tdhttp/internal/dump_response.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Copyright (c) 2021, 2022, Maxime Soulé
// All rights reserved.
//
// This source code is licensed under the BSD-style license found in the
// LICENSE file in the root directory of this source tree.

package internal

import (
"bytes"
"net/http"
"net/http/httputil"
"testing"
"unicode/utf8"
)

// canBackquote is the same as strconv.CanBackquote but works on
// []byte and accepts '\n' and '\r'.
func canBackquote(b []byte) bool {
for len(b) > 0 {
r, wid := utf8.DecodeRune(b)
b = b[wid:]
if wid > 1 {
if r == '\ufeff' {
return false // BOMs are invisible and should not be quoted.
}
continue // All other multibyte runes are correctly encoded and assumed printable.
}
if r == utf8.RuneError {
return false
}
if (r < ' ' && r != '\t' && r != '\n' && r != '\r') || r == '`' || r == '\u007F' {
return false
}
}
return true
}

func replaceCrLf(b []byte) []byte {
return bytes.Replace(b, []byte("\r\n"), []byte("\n"), -1) //nolint: gocritic
}

func backquote(b []byte) ([]byte, bool) {
// if there is as many \r\n as \n, replace all occurrences by \n
// so we can conveniently print the buffer inside `…`.
crnl := bytes.Count(b, []byte("\r\n"))
cr := bytes.Count(b, []byte("\r"))
if crnl != 0 {
nl := bytes.Count(b, []byte("\n"))
if crnl != nl || crnl != cr {
return nil, false
}
return replaceCrLf(b), true
}

return b, cr == 0
}

// DumpResponse logs resp using Logf method of t.
//
// It tries to produce a result as readable as possible first using
// backquotes then falling back to double-quotes.
func DumpResponse(t testing.TB, resp *http.Response) {
t.Helper()

const label = "Received response:\n"
b, _ := httputil.DumpResponse(resp, true)
if canBackquote(b) {
bodyPos := bytes.Index(b, []byte("\r\n\r\n"))

if body, ok := backquote(b[bodyPos+4:]); ok {
headers := replaceCrLf(b[:bodyPos])
t.Logf(label+"`%s\n\n%s`", headers, body)
return
}
}

t.Logf(label+"%q", b)
}
108 changes: 108 additions & 0 deletions helpers/tdhttp/internal/dump_response_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// Copyright (c) 2021, 2022, Maxime Soulé
// All rights reserved.
//
// This source code is licensed under the BSD-style license found in the
// LICENSE file in the root directory of this source tree.

package internal_test

import (
"bytes"
"io"
"net/http"
"testing"

"github.com/maxatome/go-testdeep/helpers/tdhttp/internal"
"github.com/maxatome/go-testdeep/internal/test"
"github.com/maxatome/go-testdeep/td"
)

func newResponse(body string) *http.Response {
return &http.Response{
Status: "200 OK",
StatusCode: 200,
Proto: "HTTP/1.0",
ProtoMajor: 1,
ProtoMinor: 0,
Header: http.Header{
"A": []string{"foo"},
"B": []string{"bar"},
},
Body: io.NopCloser(bytes.NewBufferString(body)),
}
}

func inBQ(s string) string {
return "`" + s + "`"
}

func TestDumpResponse(t *testing.T) {
tb := test.NewTestingTB("TestDumpResponse")
internal.DumpResponse(tb, newResponse("one-line"))
td.Cmp(t, tb.LastMessage(),
`Received response:
`+inBQ(`HTTP/1.0 200 OK
A: foo
B: bar
one-line`))

tb.ResetMessages()
internal.DumpResponse(tb, newResponse("multi\r\nlines\r\nand\ttabs héhé"))
td.Cmp(t, tb.LastMessage(),
`Received response:
`+inBQ(`HTTP/1.0 200 OK
A: foo
B: bar
multi
lines
`+"and\ttabs héhé"))

tb.ResetMessages()
internal.DumpResponse(tb, newResponse("multi\nlines\nand\ttabs héhé"))
td.Cmp(t, tb.LastMessage(),
`Received response:
`+inBQ(`HTTP/1.0 200 OK
A: foo
B: bar
multi
lines
`+"and\ttabs héhé"))

// one \r more in body
tb.ResetMessages()
internal.DumpResponse(tb, newResponse("multi\r\nline\r"))
td.Cmp(t, tb.LastMessage(),
`Received response:
"HTTP/1.0 200 OK\r\nA: foo\r\nB: bar\r\n\r\nmulti\r\nline\r"`)

// BOM
tb.ResetMessages()
internal.DumpResponse(tb, newResponse("\ufeff"))
td.Cmp(t, tb.LastMessage(),
`Received response:
"HTTP/1.0 200 OK\r\nA: foo\r\nB: bar\r\n\r\n\ufeff"`)

// Rune error
tb.ResetMessages()
internal.DumpResponse(tb, newResponse("\xf4\x9f\xbf\xbf"))
td.Cmp(t, tb.LastMessage(),
`Received response:
"HTTP/1.0 200 OK\r\nA: foo\r\nB: bar\r\n\r\n\xf4\x9f\xbf\xbf"`)

// `
tb.ResetMessages()
internal.DumpResponse(tb, newResponse("he`o"))
td.Cmp(t, tb.LastMessage(),
`Received response:
"HTTP/1.0 200 OK\r\nA: foo\r\nB: bar\r\n\r\nhe`+"`"+`o"`)

// 0x7f
tb.ResetMessages()
internal.DumpResponse(tb, newResponse("\x7f"))
td.Cmp(t, tb.LastMessage(),
td.Re(`Received response:
"HTTP/1.0 200 OK\\r\\nA: foo\\r\\nB: bar\\r\\n\\r\\n(\\u007f|\\x7f)"`))
}
83 changes: 27 additions & 56 deletions helpers/tdhttp/internal/response.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) 2021, Maxime Soulé
// Copyright (c) 2022, Maxime Soulé
// All rights reserved.
//
// This source code is licensed under the BSD-style license found in the
Expand All @@ -7,73 +7,44 @@
package internal

import (
"bytes"
"encoding/json"
"net/http"
"net/http/httputil"
"testing"
"unicode/utf8"
"net/http/httptest"
"sync"
)

// canBackquote is the same as strconv.CanBackquote but works on
// []byte and accepts '\n' and '\r'.
func canBackquote(b []byte) bool {
for len(b) > 0 {
r, wid := utf8.DecodeRune(b)
b = b[wid:]
if wid > 1 {
if r == '\ufeff' {
return false // BOMs are invisible and should not be quoted.
}
continue // All other multibyte runes are correctly encoded and assumed printable.
}
if r == utf8.RuneError {
return false
}
if (r < ' ' && r != '\t' && r != '\n' && r != '\r') || r == '`' || r == '\u007F' {
return false
}
}
return true
}
type Response struct {
sync.Mutex

func replaceCrLf(b []byte) []byte {
return bytes.ReplaceAll(b, []byte("\r\n"), []byte("\n"))
name string
response *httptest.ResponseRecorder

asJSON any
jsonDecoded bool
}

func backquote(b []byte) ([]byte, bool) {
// if there is as many \r\n as \n, replace all occurrences by \n
// so we can conveniently print the buffer inside `…`.
crnl := bytes.Count(b, []byte("\r\n"))
cr := bytes.Count(b, []byte("\r"))
if crnl != 0 {
nl := bytes.Count(b, []byte("\n"))
if crnl != nl || crnl != cr {
return nil, false
}
return replaceCrLf(b), true
func NewResponse(resp *httptest.ResponseRecorder) *Response {
return &Response{
response: resp,
}

return b, cr == 0
}

// DumpResponse logs "resp" using Logf method of "t".
//
// It tries to produce a result as readable as possible first using
// backquotes then falling back to double-quotes.
func DumpResponse(t testing.TB, resp *http.Response) {
t.Helper()
func (r *Response) Response() *http.Response {
// No lock needed here
return r.response.Result()
}

const label = "Received response:\n"
b, _ := httputil.DumpResponse(resp, true)
if canBackquote(b) {
bodyPos := bytes.Index(b, []byte("\r\n\r\n"))
func (r *Response) UnmarshalJSON() (any, error) {
r.Lock()
defer r.Unlock()

if body, ok := backquote(b[bodyPos+4:]); ok {
headers := replaceCrLf(b[:bodyPos])
t.Logf(label+"`%s\n\n%s`", headers, body)
return
if !r.jsonDecoded {
err := json.Unmarshal(r.response.Body.Bytes(), &r.asJSON)
if err != nil {
return nil, err
}
r.jsonDecoded = true
}

t.Logf(label+"%q", b)
return r.asJSON, nil
}
75 changes: 75 additions & 0 deletions helpers/tdhttp/internal/response_list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Copyright (c) 2022, Maxime Soulé
// All rights reserved.
//
// This source code is licensed under the BSD-style license found in the
// LICENSE file in the root directory of this source tree.

package internal

import (
"errors"
"fmt"
"sync"
)

type ResponseList struct {
sync.Mutex
responses map[string]*Response
last *Response
}

func NewResponseList() *ResponseList {
return &ResponseList{
responses: map[string]*Response{},
}
}

func (rl *ResponseList) SetLast(resp *Response) {
rl.Lock()
defer rl.Unlock()

rl.last = resp
}

func (rl *ResponseList) RecordLast(name string) error {
rl.Lock()
defer rl.Unlock()

if rl.last == nil {
return errors.New("no last response to record")
}

rl.last.Lock()
defer rl.last.Unlock()

if rl.last.name != "" {
return fmt.Errorf("last response is already recorded as %q", rl.last.name)
}

rl.responses[name] = rl.last
rl.last.name = name

return nil
}

func (rl *ResponseList) Reset() {
rl.Lock()
defer rl.Unlock()

for name := range rl.responses {
delete(rl.responses, name)
}
rl.last = nil
}

func (rl *ResponseList) Get(name string) *Response {
rl.Lock()
defer rl.Unlock()
return rl.responses[name]
}

func (rl *ResponseList) Last() *Response {
rl.Lock()
defer rl.Unlock()
return rl.last
}

0 comments on commit 8842b11

Please sign in to comment.