@marcel91200 / caddy Mirror · read-only
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 11, 01:00 UTC

master
go 224 lines 6.88 KB
Raw
1 // Package encode_test provides the standard encode benchmark and conformance suite
2 // for Caddy's gzip and zstd encoder modules.
3 //
4 // Run encoder-level benchmarks (direct NewEncoder calls):
5 //
6 // go test -bench=BenchmarkStandardEncodingPayloads -benchmem ./modules/caddyhttp/encode/
7 //
8 // Run middleware-level benchmarks (Encode.ServeHTTP, writer pools, responseWriter):
9 //
10 // go test -bench=BenchmarkEncodeHandlerCorpus -benchmem ./modules/caddyhttp/encode/
11 //
12 // Benchmark subtest names:
13 //
14 // payload-{html|json|js|css}/encoder-{gzip|zstd}/compress-level-{N|fastest|...}
15 //
16 // Each subtest uses 4 parallel workers (benchmarkParallelism in encode_bench_test.go).
17 // Go may append -{GOMAXPROCS} to the printed benchmark name; ignore it when comparing runs.
18 //
19 // Grid: 4 payloads × 6 compress levels (gzip 1/5/9, zstd fastest/default/best) = 24 subtests
20 // per benchmark function (48 total with encoder + handler).
21 //
22 // Run conformance tests (Reset/Flush/Close, Vary, ETag, 304/HEAD/range/WebSocket, minimum_length):
23 //
24 // go test -run='TestStandardEncoderContract|TestEncodeCorpusResponse|TestEncodeResponseSemantics' ./modules/caddyhttp/encode/
25 //
26 // Conformance also covers Cache-Control no-transform, content-type matcher rejection,
27 // and encoder Reset-after-Close reuse (pool pattern).
28 package encode_test
29
30 import (
31 "bytes"
32 stdgzip "compress/gzip"
33 "context"
34 "fmt"
35 "io"
36 "os"
37 "testing"
38
39 "github.com/klauspost/compress/zstd"
40
41 "github.com/caddyserver/caddy/v2"
42 "github.com/caddyserver/caddy/v2/caddyconfig"
43 "github.com/caddyserver/caddy/v2/modules/caddyhttp/encode"
44 caddygzip "github.com/caddyserver/caddy/v2/modules/caddyhttp/encode/gzip"
45 caddyzstd "github.com/caddyserver/caddy/v2/modules/caddyhttp/encode/zstd"
46 )
47
48 // benchmarkCorpus is a fixed payload used by both benchmarks and conformance tests.
49 type benchmarkCorpus struct {
50 name string
51 data []byte
52 contentType string
53 }
54
55 var (
56 benchmarkGzipLevels = []int{1, 5, 9}
57 benchmarkZstdLevels = []string{"fastest", "default", "best"}
58 )
59
60 type encoderCase struct {
61 name string // conformance subtest label, e.g. gzip-level-5
62 encoder string // gzip or zstd
63 level string // gzip numeric level or zstd level name
64 encoding encode.Encoding
65 decompress func([]byte) ([]byte, error)
66 contentType string
67 }
68
69 func benchmarkCorpora(tb testing.TB) []benchmarkCorpus {
70 tb.Helper()
71
72 return []benchmarkCorpus{
73 {name: "html", data: readBenchmarkPayload(tb, "testdata/caddy_home.html"), contentType: "text/html; charset=utf-8"},
74 {name: "json", data: readBenchmarkPayload(tb, "testdata/caddy_config_http_servers.json"), contentType: "application/json"},
75 {name: "js", data: readBenchmarkPayload(tb, "testdata/caddy_asciinema_player.js"), contentType: "application/javascript"},
76 {name: "css", data: readBenchmarkPayload(tb, "testdata/caddy_asciinema_player.css"), contentType: "text/css"},
77 }
78 }
79
80 func readBenchmarkPayload(tb testing.TB, filename string) []byte {
81 tb.Helper()
82
83 data, err := os.ReadFile(filename)
84 if err != nil {
85 tb.Fatalf("reading benchmark payload %s: %v", filename, err)
86 }
87 return data
88 }
89
90 // conformanceLargeBody returns a payload large enough to exceed default minimum_length.
91 func conformanceLargeBody() []byte {
92 data, err := os.ReadFile("testdata/caddy_home.html")
93 if err != nil {
94 panic("conformanceLargeBody: " + err.Error())
95 }
96 return data
97 }
98
99 func standardEncoderCases(t testing.TB) []encoderCase {
100 t.Helper()
101 return provisionEncoderCases(t, []int{5}, []string{"default"})
102 }
103
104 func benchmarkEncoderCases(t testing.TB) []encoderCase {
105 t.Helper()
106 return provisionEncoderCases(t, benchmarkGzipLevels, benchmarkZstdLevels)
107 }
108
109 func provisionEncoderCases(t testing.TB, gzipLevels []int, zstdLevels []string) []encoderCase {
110 t.Helper()
111
112 ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
113 t.Cleanup(cancel)
114
115 var cases []encoderCase
116 for _, level := range gzipLevels {
117 gzipEncoding := &caddygzip.Gzip{Level: level}
118 if err := gzipEncoding.Provision(ctx); err != nil {
119 t.Fatalf("gzip level %d Provision() error = %v", level, err)
120 }
121 cases = append(cases, encoderCase{
122 name: fmt.Sprintf("gzip-level-%d", level),
123 encoder: "gzip",
124 level: fmt.Sprintf("%d", level),
125 encoding: gzipEncoding,
126 decompress: decompressGzip,
127 contentType: "text/plain",
128 })
129 }
130 for _, level := range zstdLevels {
131 zstdEncoding := &caddyzstd.Zstd{Level: level}
132 if err := zstdEncoding.Provision(ctx); err != nil {
133 t.Fatalf("zstd level %q Provision() error = %v", level, err)
134 }
135 cases = append(cases, encoderCase{
136 name: "zstd-level-" + level,
137 encoder: "zstd",
138 level: level,
139 encoding: zstdEncoding,
140 decompress: decompressZstd,
141 contentType: "text/plain",
142 })
143 }
144 return cases
145 }
146
147 func newEncodeHandler(tb testing.TB, encCase encoderCase, minLength int) *encode.Encode {
148 tb.Helper()
149
150 encodingName := encCase.encoding.AcceptEncoding()
151 enc := &encode.Encode{
152 EncodingsRaw: caddy.ModuleMap{
153 encodingName: caddyconfig.JSON(encCase.encoding, nil),
154 },
155 Prefer: []string{encodingName},
156 MinLength: minLength,
157 }
158 ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
159 tb.Cleanup(cancel)
160 if err := enc.Provision(ctx); err != nil {
161 tb.Fatalf("Provision() error = %v", err)
162 }
163 if err := enc.Validate(); err != nil {
164 tb.Fatalf("Validate() error = %v", err)
165 }
166 return enc
167 }
168
169 func assertDecompresses(t *testing.T, encCase encoderCase, compressed, original []byte) {
170 t.Helper()
171
172 decompressed, err := encCase.decompress(compressed)
173 if err != nil {
174 t.Fatalf("decompress %s: %v", encCase.name, err)
175 }
176 if !bytes.Equal(decompressed, original) {
177 t.Fatalf("decompressed len = %d, want len = %d", len(decompressed), len(original))
178 }
179 }
180
181 // encodeAndVerifyRoundTrip exercises Write → Flush → Write → Close and verifies
182 // the compressed stream round-trips to original.
183 func encodeAndVerifyRoundTrip(t *testing.T, encCase encoderCase, encoder encode.Encoder, original []byte) {
184 t.Helper()
185
186 var compressed bytes.Buffer
187 encoder.Reset(&compressed)
188
189 if _, err := encoder.Write(original[:len(original)/2]); err != nil {
190 t.Fatalf("Write() error = %v", err)
191 }
192 if err := encoder.Flush(); err != nil {
193 t.Fatalf("Flush() error = %v", err)
194 }
195 if compressed.Len() == 0 {
196 t.Fatal("Flush() wrote no compressed bytes")
197 }
198 if _, err := encoder.Write(original[len(original)/2:]); err != nil {
199 t.Fatalf("Write() error = %v", err)
200 }
201 if err := encoder.Close(); err != nil {
202 t.Fatalf("Close() error = %v", err)
203 }
204
205 assertDecompresses(t, encCase, compressed.Bytes(), original)
206 }
207
208 func decompressGzip(compressed []byte) ([]byte, error) {
209 reader, err := stdgzip.NewReader(bytes.NewReader(compressed))
210 if err != nil {
211 return nil, err
212 }
213 defer reader.Close()
214 return io.ReadAll(reader)
215 }
216
217 func decompressZstd(compressed []byte) ([]byte, error) {
218 decoder, err := zstd.NewReader(nil)
219 if err != nil {
220 return nil, err
221 }
222 defer decoder.Close()
223 return decoder.DecodeAll(compressed, nil)
224 }