308 lines
9.7 KiB
Go
308 lines
9.7 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.NewUnstartedServer(http.HandlerFunc(state.serveHTTP))
|
|
state.baseURL = "http://" + server.Listener.Addr().String()
|
|
server.Start()
|
|
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.TB) (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, "go.mod", "module example.com/deliveryfixture\n\ngo 1.26.0\n")
|
|
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"),
|
|
ModuleDir: repository, ModulePath: "example.com/deliveryfixture", Tag: "v1.2.3",
|
|
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.TB) {
|
|
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)
|
|
}
|