Files
ReleaseKit/performance_delivery_test.go

305 lines
9.5 KiB
Go

package releasekit_test
import (
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"hash"
"io"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"sort"
"strings"
"sync"
"testing"
releasekit "gitea.maximumdirect.net/eric/ReleaseKit"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/gitea"
)
const performanceDeliveryArtifactSize = 16 << 20
type deliveryBenchmarkAsset struct {
name string
data []byte
digest string
}
type deliveryBenchmarkServer struct {
mu sync.Mutex
baseURL string
commit string
assets []deliveryBenchmarkAsset
page bool
attached map[string]deliveryBenchmarkIdentity
}
type deliveryBenchmarkIdentity struct {
digest string
size int64
}
func BenchmarkPerformanceFacadeDeliver(b *testing.B) {
repository, build, expected := newDeliveryBenchmarkFixture(b)
state := &deliveryBenchmarkServer{commit: gitText(b, repository, "rev-parse", "HEAD"), assets: expected}
server := httptest.NewServer(http.HandlerFunc(state.serveHTTP))
state.baseURL = server.URL
b.Cleanup(server.Close)
request := releasekit.DeliveryRequest{
RepositoryDir: repository,
Product: "Sample App",
Version: "v1.2.3",
Destination: facadeDeliveryDestination(server.URL),
Prebuilt: &build,
}
totalBytes := int64(2 * performanceDeliveryArtifactSize)
b.Run("FreshUploads", func(b *testing.B) {
b.ReportAllocs()
b.SetBytes(totalBytes)
b.ReportMetric(float64(totalBytes), "artifact-bytes/op")
b.ReportMetric(2, "binary-files/op")
for i := 0; i < b.N; i++ {
b.StopTimer()
state.reset(false)
b.StartTimer()
result, err := releasekit.Deliver(context.Background(), request)
b.StopTimer()
if err != nil {
b.Fatalf("Deliver() error = %v", err)
}
if len(result.Uploaded) != 3 {
b.Fatalf("uploaded %d assets, want two binaries and a manifest", len(result.Uploaded))
}
state.verifyUploads(b)
b.StartTimer()
}
})
b.Run("MatchingRetry", func(b *testing.B) {
b.StopTimer()
state.reset(true)
b.ReportAllocs()
b.SetBytes(totalBytes)
b.ReportMetric(float64(totalBytes), "artifact-bytes/op")
b.ReportMetric(2, "binary-files/op")
b.ResetTimer()
b.StartTimer()
for i := 0; i < b.N; i++ {
result, err := releasekit.Deliver(context.Background(), request)
if err != nil {
b.Fatalf("Deliver() error = %v", err)
}
if len(result.Uploaded) != 0 || len(result.Matching) != 3 {
b.Fatalf("matching retry uploaded %d and matched %d assets; want 0 and 3", len(result.Uploaded), len(result.Matching))
}
}
})
}
func newDeliveryBenchmarkFixture(b *testing.B) (string, releasekit.BuildResult, []deliveryBenchmarkAsset) {
b.Helper()
repository := filepath.Join(b.TempDir(), "repository")
if err := os.MkdirAll(repository, 0o755); err != nil {
b.Fatal(err)
}
gitRun(b, repository, "init", "-b", "main")
gitRun(b, repository, "config", "user.name", "ReleaseKit Benchmark")
gitRun(b, repository, "config", "user.email", "benchmark@example.invalid")
writeFile(b, repository, "docs/releases/v1.2.3.md", "# Sample App v1.2.3\n\nDelivery fixture.\n")
gitRun(b, repository, "add", ".")
gitRun(b, repository, "commit", "-m", "prepare delivery benchmark")
gitRun(b, repository, "tag", "v1.2.3")
data := make([]byte, performanceDeliveryArtifactSize)
for i := range data {
data[i] = byte((i*17 + i/127 + 29) % 251)
}
digest := sha256.Sum256(data)
digestText := hex.EncodeToString(digest[:])
outputDir := filepath.Join(b.TempDir(), "artifacts")
if err := os.MkdirAll(outputDir, 0o700); err != nil {
b.Fatal(err)
}
targets := []releasekit.Target{{OS: "linux", Arch: "amd64"}, {OS: "windows", Arch: "amd64"}}
build := releasekit.BuildResult{
RepositoryDir: repository, CandidateCommit: gitText(b, repository, "rev-parse", "HEAD"),
SourceVerified: true, Version: "v1.2.3", MainPackage: "./cmd/app",
BinaryName: "releasekit", Targets: targets, OutputDir: outputDir,
}
assets := make([]deliveryBenchmarkAsset, 0, len(targets)+1)
for _, target := range targets {
filename := fmt.Sprintf("releasekit-v1.2.3-%s-%s", target.OS, target.Arch)
if target.OS == "windows" {
filename += ".exe"
}
path := filepath.Join(outputDir, filename)
if err := os.WriteFile(path, data, 0o600); err != nil {
b.Fatal(err)
}
build.Artifacts = append(build.Artifacts, releasekit.BuildArtifact{Path: path, Filename: filename, Target: target, SHA256: digestText})
assets = append(assets, deliveryBenchmarkAsset{name: filename, data: data, digest: digestText})
}
manifest := performanceDeliveryManifest(build.Artifacts)
manifestHash := sha256.Sum256(manifest)
assets = append(assets, deliveryBenchmarkAsset{name: gitea.ManifestFilename, data: manifest, digest: hex.EncodeToString(manifestHash[:])})
return repository, build, assets
}
func performanceDeliveryManifest(artifacts []releasekit.BuildArtifact) []byte {
ordered := append([]releasekit.BuildArtifact(nil), artifacts...)
sort.Slice(ordered, func(i, j int) bool { return ordered[i].Filename < ordered[j].Filename })
var manifest strings.Builder
for _, artifact := range ordered {
fmt.Fprintf(&manifest, "%s %s\n", artifact.SHA256, artifact.Filename)
}
return []byte(manifest.String())
}
func (state *deliveryBenchmarkServer) reset(populated bool) {
state.mu.Lock()
defer state.mu.Unlock()
state.page = populated
state.attached = make(map[string]deliveryBenchmarkIdentity)
if populated {
for _, asset := range state.assets {
state.attached[asset.name] = deliveryBenchmarkIdentity{digest: asset.digest, size: int64(len(asset.data))}
}
}
}
func (state *deliveryBenchmarkServer) verifyUploads(b *testing.B) {
b.Helper()
state.mu.Lock()
defer state.mu.Unlock()
if !state.page || len(state.attached) != len(state.assets) {
b.Fatalf("fresh upload left release page %t and %d attached assets, want page and %d assets", state.page, len(state.attached), len(state.assets))
}
for _, asset := range state.assets {
identity, ok := state.attached[asset.name]
if !ok || identity.digest != asset.digest || identity.size != int64(len(asset.data)) {
b.Fatalf("uploaded %q identity = %#v, present %t; want digest %s and size %d", asset.name, identity, ok, asset.digest, len(asset.data))
}
}
}
func (state *deliveryBenchmarkServer) serveHTTP(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/v1/repos/owner/example/tags/v1.2.3":
_, _ = fmt.Fprintf(w, `{"name":"v1.2.3","commit":{"sha":%q}}`, state.commit)
case "/api/v1/repos/owner/example/releases/tags/v1.2.3":
state.mu.Lock()
page := state.page
state.mu.Unlock()
if !page {
http.NotFound(w, r)
return
}
state.writeRelease(w)
case "/api/v1/repos/owner/example/releases":
state.mu.Lock()
state.page = true
state.mu.Unlock()
state.writeRelease(w)
case "/api/v1/repos/owner/example/releases/10/assets":
if r.Method == http.MethodGet {
state.writeAssetList(w)
return
}
state.receiveUpload(w, r)
default:
if strings.HasPrefix(r.URL.Path, "/files/") {
state.serveFile(w, r)
return
}
http.NotFound(w, r)
}
}
func (state *deliveryBenchmarkServer) writeRelease(w http.ResponseWriter) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{
"id": 10, "html_url": state.baseURL + "/releases/10", "tag_name": "v1.2.3",
"target_commitish": state.commit, "name": "Fixture v1.2.3",
"body": "# Sample App v1.2.3\n\nDelivery fixture.\n", "draft": false, "prerelease": false,
})
}
func (state *deliveryBenchmarkServer) writeAssetList(w http.ResponseWriter) {
state.mu.Lock()
assets := make([]map[string]any, 0, len(state.attached))
for name, identity := range state.attached {
assets = append(assets, map[string]any{
"id": len(assets) + 1, "name": name, "size": identity.size,
"browser_download_url": state.baseURL + "/files/" + name,
})
}
state.mu.Unlock()
_ = json.NewEncoder(w).Encode(assets)
}
func (state *deliveryBenchmarkServer) receiveUpload(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
reader, err := r.MultipartReader()
if err != nil {
http.Error(w, "invalid multipart request", http.StatusBadRequest)
return
}
var digest hash.Hash = sha256.New()
var size int64
for {
part, err := reader.NextPart()
if err == io.EOF {
break
}
if err != nil {
http.Error(w, "invalid multipart body", http.StatusBadRequest)
return
}
if part.FormName() == "attachment" {
size, err = io.Copy(digest, part)
if err != nil {
_ = part.Close()
http.Error(w, "cannot read attachment", http.StatusBadRequest)
return
}
}
_ = part.Close()
}
identity := deliveryBenchmarkIdentity{digest: hex.EncodeToString(digest.Sum(nil)), size: size}
var expected *deliveryBenchmarkAsset
for i := range state.assets {
if state.assets[i].name == name {
expected = &state.assets[i]
break
}
}
if expected == nil || identity.digest != expected.digest || identity.size != int64(len(expected.data)) {
http.Error(w, "attachment digest or size mismatch", http.StatusBadRequest)
return
}
state.mu.Lock()
state.attached[name] = identity
assetID := len(state.attached)
state.mu.Unlock()
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(map[string]any{"id": assetID, "name": name, "size": size})
}
func (state *deliveryBenchmarkServer) serveFile(w http.ResponseWriter, r *http.Request) {
name := strings.TrimPrefix(r.URL.Path, "/files/")
for _, asset := range state.assets {
if asset.name == name {
w.Header().Set("Content-Length", fmt.Sprint(len(asset.data)))
_, _ = io.Copy(w, bytes.NewReader(asset.data))
return
}
}
http.NotFound(w, r)
}