Measure delivery output and version costs
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# Computational Efficiency Implementation Plan
|
# Computational Efficiency Implementation Plan
|
||||||
|
|
||||||
**Status:** Stage 1 complete; Stages 2–11 pending.
|
**Status:** Stages 1–2 complete; Stages 3–11 pending.
|
||||||
|
|
||||||
## Purpose And Baseline
|
## Purpose And Baseline
|
||||||
|
|
||||||
@@ -126,6 +126,53 @@ measurement support only.
|
|||||||
**Owners:** Benchmark files in `internal/delivery`, `internal/gotool`,
|
**Owners:** Benchmark files in `internal/delivery`, `internal/gotool`,
|
||||||
`internal/runtimeinfo`, and root delivery fixtures.
|
`internal/runtimeinfo`, and root delivery fixtures.
|
||||||
|
|
||||||
|
**Completion:** Complete. Measurements cover declarations and local
|
||||||
|
verification of two deterministic 16 MiB files, prebuilt facade delivery for
|
||||||
|
fresh uploads and matching retries, combined output capture and forwarding at
|
||||||
|
4 KiB and 8 MiB, and injected and real metadata version access. The local HTTP
|
||||||
|
fixture hashes and counts incoming multipart bytes incrementally and streams
|
||||||
|
matching files from immutable fixture data. It verifies uploaded identities and
|
||||||
|
that retries match all three declared assets without uploading again.
|
||||||
|
|
||||||
|
### Delivery, Output, And Runtime Baseline
|
||||||
|
|
||||||
|
The baseline is commit `29e1bab`, before production refactoring, using the same
|
||||||
|
Go, Git, operating system, processor, benchmark command, and three repetitions
|
||||||
|
recorded above. Artifact declaration and preflight fixtures contain two
|
||||||
|
deterministic 16 MiB files (33,554,432 binary bytes per operation); setup and
|
||||||
|
initial writes are outside the timers. Declaration currently hashes each file,
|
||||||
|
and declaration plus local verification therefore measures both hashing passes.
|
||||||
|
The facade benchmark uses a committed tagged source fixture and prebuilt
|
||||||
|
artifacts, so its time includes local source checks, hashing, HTTP requests, and
|
||||||
|
streamed body comparison or upload, but no Go compilation. Fresh hosted state is
|
||||||
|
reset outside the timed region for every operation. Retry state is prepared
|
||||||
|
before timing. The fixture server retains only digest and size metadata for
|
||||||
|
uploads and streams known matching bytes directly from shared fixture content.
|
||||||
|
|
||||||
|
Output measurements call the combined writer directly, with forwarding to
|
||||||
|
`io.Discard`, then capture its completed buffer. They include the current
|
||||||
|
completed-output copy. Runtime fallback measurements call `Version()` repeatedly
|
||||||
|
against real build metadata; injected access measures the selection helper's
|
||||||
|
injected-version branch. The benchmark process reports its own allocations,
|
||||||
|
including local HTTP fixture work; streamed artifact bodies are not buffered by
|
||||||
|
the fixture server.
|
||||||
|
|
||||||
|
| Measurement | Baseline observations | Final comparison |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Artifact declarations, two 16 MiB files | 94.3–95.4 ms; 33,554,432 bytes processed; about 68.9 KB allocated. | Pending Stage 11. |
|
||||||
|
| Declarations plus local verification | 186.7–188.4 ms; 33,554,432 binary bytes per each of two passes; about 136.2 KB allocated. | Pending Stage 11. |
|
||||||
|
| Facade delivery, fresh uploads | 429–447 ms; 32 MiB binary payload, two files plus manifest; 1.43–1.46 MB allocated. | Pending Stage 11. |
|
||||||
|
| Facade delivery, matching retry | 309.1–310.0 ms; 32 MiB of local verification plus streamed remote comparisons; 1.21–1.22 MB allocated. | Pending Stage 11. |
|
||||||
|
| Combined output, 4 KiB and `io.Discard` | 1.82–1.87 μs; 8,256 B and 3 allocations per operation. | Pending Stage 11. |
|
||||||
|
| Combined output, 8 MiB and `io.Discard` | 2.02–2.36 ms; 16.78 MB and 3 allocations per operation, including the completed capture copy. | Pending Stage 11. |
|
||||||
|
| Injected version selection | 0.745–0.754 ns; 0 B and 0 allocations. | Pending Stage 11. |
|
||||||
|
| Repeated real metadata fallback | 1.438–1.474 μs; 1,184 B and 7 allocations per operation. | Pending Stage 11. |
|
||||||
|
|
||||||
|
The delivery observations include local HTTP and filesystem costs specific to
|
||||||
|
these fixtures. The output component benchmark does not include child startup;
|
||||||
|
the real delivery benchmark does include HTTP request handling. These results
|
||||||
|
describe measured workloads and do not establish runtime guarantees.
|
||||||
|
|
||||||
1. Extend the same benchmark prefix to declaration/preflight processing of two
|
1. Extend the same benchmark prefix to declaration/preflight processing of two
|
||||||
deterministic 16 MiB artifact files. Measure prebuilt facade `Deliver`
|
deterministic 16 MiB artifact files. Measure prebuilt facade `Deliver`
|
||||||
against a local HTTP fixture for fresh uploads and matching retries.
|
against a local HTTP fixture for fresh uploads and matching retries.
|
||||||
|
|||||||
85
internal/delivery/performance_test.go
Normal file
85
internal/delivery/performance_test.go
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
package delivery
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
internalbuild "gitea.maximumdirect.net/eric/ReleaseKit/internal/build"
|
||||||
|
"gitea.maximumdirect.net/eric/ReleaseKit/internal/gitea"
|
||||||
|
)
|
||||||
|
|
||||||
|
const performanceArtifactSize = 16 << 20
|
||||||
|
|
||||||
|
func BenchmarkPerformanceAssetPreflight(b *testing.B) {
|
||||||
|
result, totalBytes := performanceBuildResult(b)
|
||||||
|
declarations, err := declaredBuildAssets(result)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.Run("Declarations", func(b *testing.B) {
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.SetBytes(totalBytes)
|
||||||
|
b.ResetTimer()
|
||||||
|
b.ReportMetric(float64(totalBytes), "bytes/op")
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
assets, err := declaredBuildAssets(result)
|
||||||
|
if err != nil || len(assets) != len(declarations) {
|
||||||
|
b.Fatalf("declaredBuildAssets() returned %d assets, err %v", len(assets), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
b.Run("DeclarationsAndLocalVerification", func(b *testing.B) {
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.SetBytes(totalBytes)
|
||||||
|
b.ResetTimer()
|
||||||
|
b.ReportMetric(float64(totalBytes), "bytes/op")
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
assets, err := declaredBuildAssets(result)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := gitea.VerifyLocalAssets(assets); err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func performanceBuildResult(b *testing.B) (internalbuild.Result, int64) {
|
||||||
|
b.Helper()
|
||||||
|
outputDir := b.TempDir()
|
||||||
|
data := make([]byte, performanceArtifactSize)
|
||||||
|
for i := range data {
|
||||||
|
data[i] = byte((i*31 + i/251 + 17) % 251)
|
||||||
|
}
|
||||||
|
digest := sha256.Sum256(data)
|
||||||
|
result := internalbuild.Result{
|
||||||
|
Version: "v1.2.3",
|
||||||
|
BinaryName: "releasekit",
|
||||||
|
Targets: []internalbuild.Target{
|
||||||
|
{OS: "linux", Arch: "amd64"},
|
||||||
|
{OS: "windows", Arch: "amd64"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, target := range result.Targets {
|
||||||
|
name := fmt.Sprintf("releasekit-v1.2.3-%s-%s", target.OS, target.Arch)
|
||||||
|
if target.OS == "windows" {
|
||||||
|
name += ".exe"
|
||||||
|
}
|
||||||
|
path := filepath.Join(outputDir, name)
|
||||||
|
if err := os.WriteFile(path, data, 0o600); err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
result.Artifacts = append(result.Artifacts, internalbuild.Artifact{
|
||||||
|
Path: path, Filename: name, Target: target,
|
||||||
|
SHA256: hex.EncodeToString(digest[:]),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return result, 2 * performanceArtifactSize
|
||||||
|
}
|
||||||
38
internal/gotool/performance_test.go
Normal file
38
internal/gotool/performance_test.go
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
package gotool
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
const performanceDiagnosticSize = 8 << 20
|
||||||
|
|
||||||
|
func BenchmarkPerformanceCombinedOutput(b *testing.B) {
|
||||||
|
for _, testCase := range []struct {
|
||||||
|
name string
|
||||||
|
size int
|
||||||
|
}{
|
||||||
|
{name: "modest", size: 4 << 10},
|
||||||
|
{name: "diagnostic-8MiB", size: performanceDiagnosticSize},
|
||||||
|
} {
|
||||||
|
input := bytes.Repeat([]byte("go tool diagnostic output\n"), (testCase.size+25)/26)
|
||||||
|
input = input[:testCase.size]
|
||||||
|
b.Run(testCase.name+"/discard-forwarding", func(b *testing.B) {
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.SetBytes(int64(len(input)))
|
||||||
|
b.ResetTimer()
|
||||||
|
b.ReportMetric(float64(len(input)), "bytes/op")
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
writer := &combinedWriter{output: io.Discard}
|
||||||
|
if written, err := writer.Write(input); err != nil || written != len(input) {
|
||||||
|
b.Fatalf("write captured output = %d, err %v; want %d bytes", written, err, len(input))
|
||||||
|
}
|
||||||
|
captured := writer.Bytes()
|
||||||
|
if len(captured) != len(input) {
|
||||||
|
b.Fatalf("captured output = %d bytes, want %d", len(captured), len(input))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
31
internal/runtimeinfo/performance_test.go
Normal file
31
internal/runtimeinfo/performance_test.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package runtimeinfo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"runtime/debug"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
var performanceVersionSink string
|
||||||
|
|
||||||
|
func BenchmarkPerformanceVersion(b *testing.B) {
|
||||||
|
b.Run("Injected", func(b *testing.B) {
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
performanceVersionSink = version("v1.2.3", debug.ReadBuildInfo)
|
||||||
|
if performanceVersionSink != "v1.2.3" {
|
||||||
|
b.Fatalf("version() = %q, want injected version", performanceVersionSink)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
b.Run("RealMetadataFallback", func(b *testing.B) {
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
performanceVersionSink = Version()
|
||||||
|
if performanceVersionSink == "" {
|
||||||
|
b.Fatal("Version() returned an empty version")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
304
performance_delivery_test.go
Normal file
304
performance_delivery_test.go
Normal file
@@ -0,0 +1,304 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user