caddy/modules/caddyhttp/encode/encode_harness_test.go

225 lines
6.9 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

// Package encode_test provides the standard encode benchmark and conformance suite
// for Caddy's gzip and zstd encoder modules.
//
// Run encoder-level benchmarks (direct NewEncoder calls):
//
// go test -bench=BenchmarkStandardEncodingPayloads -benchmem ./modules/caddyhttp/encode/
//
// Run middleware-level benchmarks (Encode.ServeHTTP, writer pools, responseWriter):
//
// go test -bench=BenchmarkEncodeHandlerCorpus -benchmem ./modules/caddyhttp/encode/
//
// Benchmark subtest names:
//
// payload-{html|json|js|css}/encoder-{gzip|zstd}/compress-level-{N|fastest|...}
//
// Each subtest uses 4 parallel workers (benchmarkParallelism in encode_bench_test.go).
// Go may append -{GOMAXPROCS} to the printed benchmark name; ignore it when comparing runs.
//
// Grid: 4 payloads × 6 compress levels (gzip 1/5/9, zstd fastest/default/best) = 24 subtests
// per benchmark function (48 total with encoder + handler).
//
// Run conformance tests (Reset/Flush/Close, Vary, ETag, 304/HEAD/range/WebSocket, minimum_length):
//
// go test -run='TestStandardEncoderContract|TestEncodeCorpusResponse|TestEncodeResponseSemantics' ./modules/caddyhttp/encode/
//
// Conformance also covers Cache-Control no-transform, content-type matcher rejection,
// and encoder Reset-after-Close reuse (pool pattern).
package encode_test
import (
"bytes"
stdgzip "compress/gzip"
"context"
"fmt"
"io"
"os"
"testing"
"github.com/klauspost/compress/zstd"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/modules/caddyhttp/encode"
caddygzip "github.com/caddyserver/caddy/v2/modules/caddyhttp/encode/gzip"
caddyzstd "github.com/caddyserver/caddy/v2/modules/caddyhttp/encode/zstd"
)
// benchmarkCorpus is a fixed payload used by both benchmarks and conformance tests.
type benchmarkCorpus struct {
name string
data []byte
contentType string
}
var (
benchmarkGzipLevels = []int{1, 5, 9}
benchmarkZstdLevels = []string{"fastest", "default", "best"}
)
type encoderCase struct {
name string // conformance subtest label, e.g. gzip-level-5
encoder string // gzip or zstd
level string // gzip numeric level or zstd level name
encoding encode.Encoding
decompress func([]byte) ([]byte, error)
contentType string
}
func benchmarkCorpora(tb testing.TB) []benchmarkCorpus {
tb.Helper()
return []benchmarkCorpus{
{name: "html", data: readBenchmarkPayload(tb, "testdata/caddy_home.html"), contentType: "text/html; charset=utf-8"},
{name: "json", data: readBenchmarkPayload(tb, "testdata/caddy_config_http_servers.json"), contentType: "application/json"},
{name: "js", data: readBenchmarkPayload(tb, "testdata/caddy_asciinema_player.js"), contentType: "application/javascript"},
{name: "css", data: readBenchmarkPayload(tb, "testdata/caddy_asciinema_player.css"), contentType: "text/css"},
}
}
func readBenchmarkPayload(tb testing.TB, filename string) []byte {
tb.Helper()
data, err := os.ReadFile(filename)
if err != nil {
tb.Fatalf("reading benchmark payload %s: %v", filename, err)
}
return data
}
// conformanceLargeBody returns a payload large enough to exceed default minimum_length.
func conformanceLargeBody() []byte {
data, err := os.ReadFile("testdata/caddy_home.html")
if err != nil {
panic("conformanceLargeBody: " + err.Error())
}
return data
}
func standardEncoderCases(t testing.TB) []encoderCase {
t.Helper()
return provisionEncoderCases(t, []int{5}, []string{"default"})
}
func benchmarkEncoderCases(t testing.TB) []encoderCase {
t.Helper()
return provisionEncoderCases(t, benchmarkGzipLevels, benchmarkZstdLevels)
}
func provisionEncoderCases(t testing.TB, gzipLevels []int, zstdLevels []string) []encoderCase {
t.Helper()
ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
t.Cleanup(cancel)
var cases []encoderCase
for _, level := range gzipLevels {
gzipEncoding := &caddygzip.Gzip{Level: level}
if err := gzipEncoding.Provision(ctx); err != nil {
t.Fatalf("gzip level %d Provision() error = %v", level, err)
}
cases = append(cases, encoderCase{
name: fmt.Sprintf("gzip-level-%d", level),
encoder: "gzip",
level: fmt.Sprintf("%d", level),
encoding: gzipEncoding,
decompress: decompressGzip,
contentType: "text/plain",
})
}
for _, level := range zstdLevels {
zstdEncoding := &caddyzstd.Zstd{Level: level}
if err := zstdEncoding.Provision(ctx); err != nil {
t.Fatalf("zstd level %q Provision() error = %v", level, err)
}
cases = append(cases, encoderCase{
name: "zstd-level-" + level,
encoder: "zstd",
level: level,
encoding: zstdEncoding,
decompress: decompressZstd,
contentType: "text/plain",
})
}
return cases
}
func newEncodeHandler(tb testing.TB, encCase encoderCase, minLength int) *encode.Encode {
tb.Helper()
encodingName := encCase.encoding.AcceptEncoding()
enc := &encode.Encode{
EncodingsRaw: caddy.ModuleMap{
encodingName: caddyconfig.JSON(encCase.encoding, nil),
},
Prefer: []string{encodingName},
MinLength: minLength,
}
ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
tb.Cleanup(cancel)
if err := enc.Provision(ctx); err != nil {
tb.Fatalf("Provision() error = %v", err)
}
if err := enc.Validate(); err != nil {
tb.Fatalf("Validate() error = %v", err)
}
return enc
}
func assertDecompresses(t *testing.T, encCase encoderCase, compressed, original []byte) {
t.Helper()
decompressed, err := encCase.decompress(compressed)
if err != nil {
t.Fatalf("decompress %s: %v", encCase.name, err)
}
if !bytes.Equal(decompressed, original) {
t.Fatalf("decompressed len = %d, want len = %d", len(decompressed), len(original))
}
}
// encodeAndVerifyRoundTrip exercises Write → Flush → Write → Close and verifies
// the compressed stream round-trips to original.
func encodeAndVerifyRoundTrip(t *testing.T, encCase encoderCase, encoder encode.Encoder, original []byte) {
t.Helper()
var compressed bytes.Buffer
encoder.Reset(&compressed)
if _, err := encoder.Write(original[:len(original)/2]); err != nil {
t.Fatalf("Write() error = %v", err)
}
if err := encoder.Flush(); err != nil {
t.Fatalf("Flush() error = %v", err)
}
if compressed.Len() == 0 {
t.Fatal("Flush() wrote no compressed bytes")
}
if _, err := encoder.Write(original[len(original)/2:]); err != nil {
t.Fatalf("Write() error = %v", err)
}
if err := encoder.Close(); err != nil {
t.Fatalf("Close() error = %v", err)
}
assertDecompresses(t, encCase, compressed.Bytes(), original)
}
func decompressGzip(compressed []byte) ([]byte, error) {
reader, err := stdgzip.NewReader(bytes.NewReader(compressed))
if err != nil {
return nil, err
}
defer reader.Close()
return io.ReadAll(reader)
}
func decompressZstd(compressed []byte) ([]byte, error) {
decoder, err := zstd.NewReader(nil)
if err != nil {
return nil, err
}
defer decoder.Close()
return decoder.DecodeAll(compressed, nil)
}