Files
substrate/pkg/objectstorage/sparsezstd_test.go
T
ILLUM1N0X 6a0519d26b ategcs: rename and move to pkg/ for reuse outside atelet (#2013)
Part of #1827

Moves `cmd/atelet/internal/ategcs` to `pkg/objectstorage` and renames
the package to `objectstorage`. No code changes besides the package
name, its import path and uses, the tracer name, and comments that named
the old package.

**Why:** the [Platform-Provided Snapshot API
Proposal](https://docs.google.com/document/d/1ac-hY6zsmX4FmtGOt9CXbNFHeluGouyYluer7kX6bto)
moves snapshot transfer out of atelet and ate-api-server into a snapshot
plugin binary that runs as a sidecar. The reference plugin (GCS and S3)
reuses this package's transfer code as is, and so can snapshot plugins
built outside this repository, which is why it goes in `pkg/` rather
than `internal/`.

**Naming:** `objectstorage`, not `objectstore`, because
`internal/objectstore` already exists. That package only lists, copies
and deletes objects for the control plane and is deliberately separate
from this one (see its package comment, updated here). The snapshot
plugin binary imports both.

This PR stands alone and changes no behavior.

**Reviewing:** `git show -M --stat` shows 20 renames as
`{cmd/atelet/internal/ategcs => pkg/objectstorage}/...`. Each changes
only its `package` clause, except:
- `objects.go`: tracer name `ategcs` → `objectstorage`
- `gcscompose_test.go`, `s3bench_test.go`: the `go test
./pkg/objectstorage` path in a comment; `gcscompose_test.go` also
renames its emulator test bucket

The other 4 files:
- `cmd/atelet/{main,main_test,sandbox_assets}.go`: import path and
`ategcs.` → `objectstorage.`, plus one comment in `main_test.go`
- `internal/objectstore/objectstore.go`: package comment now points at
`pkg/objectstorage`

- [x] Tests pass (`make test`, `make verify`)
- [x] Appropriate changes to documentation are included in the PR (none
needed)
2026-10-01 01:29:23 +00:00

233 lines
6.4 KiB
Go

// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package objectstorage
import (
"bytes"
"encoding/binary"
"os"
"path/filepath"
"strings"
"testing"
"github.com/klauspost/compress/zstd"
)
// region is a run of identical non-zero bytes written at off in a test source.
type region struct {
off int64
len int64
fill byte
}
// writeSparseSource creates a file of logical size `size` with `regions` written
// in (the gaps stay zero, becoming holes on a sparse fs — but the round-trip is
// byte-exact regardless of whether the fs actually punches holes).
func writeSparseSource(t *testing.T, path string, size int64, regions []region) {
t.Helper()
f, err := os.Create(path)
if err != nil {
t.Fatal(err)
}
defer f.Close()
if err := f.Truncate(size); err != nil {
t.Fatal(err)
}
for _, r := range regions {
buf := make([]byte, r.len)
for i := range buf {
buf[i] = r.fill
}
if _, err := f.WriteAt(buf, r.off); err != nil {
t.Fatal(err)
}
}
if err := f.Sync(); err != nil {
t.Fatal(err)
}
}
// TestSparseZstdRoundTrip checks writeSparseZstd -> readSparseZstd is byte-exact
// across hole/data layouts (the correctness property is fs-independent: whether or
// not the fs makes real holes, the decoded file must equal the source).
func TestSparseZstdRoundTrip(t *testing.T) {
const M = 1 << 20
cases := []struct {
name string
size int64
regions []region
}{
{"empty", 0, nil},
{"all-hole", 4 * M, nil},
{"all-data", 256 << 10, []region{{0, 256 << 10, 0xAB}}},
{"leading-hole", 2 * M, []region{{1 * M, 64 << 10, 0x11}}},
{"trailing-hole", 2 * M, []region{{0, 64 << 10, 0x22}}},
{"single-extent-midfile", 4 * M, []region{{2 * M, 70000, 0x33}}},
{"multi-extent", 8 * M, []region{
{0, 4096, 0x44}, {2 * M, 100000, 0x55}, {5 * M, 8192, 0x66},
}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
dir := t.TempDir()
srcPath := filepath.Join(dir, "src")
writeSparseSource(t, srcPath, tc.size, tc.regions)
src, err := os.Open(srcPath)
if err != nil {
t.Fatal(err)
}
defer src.Close()
var buf bytes.Buffer
logical, _, err := writeSparseZstd(&buf, src)
if err != nil {
t.Fatalf("writeSparseZstd: %v", err)
}
if logical != tc.size {
t.Errorf("writeSparseZstd logical=%d, want %d", logical, tc.size)
}
if got := buf.Bytes(); len(got) < len(sparseMagic) || string(got[:len(sparseMagic)]) != sparseMagic {
t.Fatal("output missing sparse magic")
}
dstPath := filepath.Join(dir, "dst")
dst, err := os.Create(dstPath)
if err != nil {
t.Fatal(err)
}
defer dst.Close()
size, err := readSparseZstd(dst, bytes.NewReader(buf.Bytes()[len(sparseMagic):]))
if err != nil {
t.Fatalf("readSparseZstd: %v", err)
}
if size != tc.size {
t.Errorf("readSparseZstd size=%d, want %d", size, tc.size)
}
want, err := os.ReadFile(srcPath)
if err != nil {
t.Fatal(err)
}
got, err := os.ReadFile(dstPath)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(got, want) {
t.Fatalf("round-trip mismatch (len got=%d want=%d)", len(got), len(want))
}
})
}
}
// craftSparseStream builds a sparse-extent stream (after the magic) with arbitrary
// header values, so tests can feed malformed input to readSparseZstd. Each frame is
// (off, len) followed by len zero bytes; an end sentinel is appended when asked.
func craftSparseStream(t *testing.T, version uint32, size int64, frames [][2]int64, sentinel bool) []byte {
t.Helper()
var buf bytes.Buffer
if err := binary.Write(&buf, binary.LittleEndian, version); err != nil {
t.Fatal(err)
}
zw, err := zstd.NewWriter(&buf)
if err != nil {
t.Fatal(err)
}
mustWrite := func(v int64) {
if err := binary.Write(zw, binary.LittleEndian, v); err != nil {
zw.Close()
t.Fatal(err)
}
}
mustWrite(size)
for _, f := range frames {
mustWrite(f[0])
mustWrite(f[1])
if f[1] > 0 {
if _, err := zw.Write(make([]byte, f[1])); err != nil {
zw.Close()
t.Fatal(err)
}
}
}
if sentinel {
mustWrite(sparseEndOffset)
}
if err := zw.Close(); err != nil {
t.Fatal(err)
}
return buf.Bytes()
}
func TestReadSparseZstdRejectsMalformed(t *testing.T) {
cases := []struct {
name string
stream []byte
want string
}{
{
name: "wrong version",
stream: craftSparseStream(t, sparseVersion+1, 0, nil, true),
want: "unsupported sparse snapshot format version",
},
{
name: "negative size",
stream: craftSparseStream(t, sparseVersion, -1, nil, true),
want: "negative totalSize",
},
{
name: "extent past end",
stream: craftSparseStream(t, sparseVersion, 100, [][2]int64{{50, 100}}, true),
want: "out of range",
},
{
name: "negative extent offset (not sentinel)",
stream: craftSparseStream(t, sparseVersion, 100, [][2]int64{{-5, 1}}, true),
want: "out of range",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
dst, err := os.Create(filepath.Join(t.TempDir(), "dst"))
if err != nil {
t.Fatal(err)
}
defer dst.Close()
_, err = readSparseZstd(dst, bytes.NewReader(tc.stream))
if err == nil {
t.Fatalf("expected error containing %q, got nil", tc.want)
}
if !strings.Contains(err.Error(), tc.want) {
t.Fatalf("error = %q, want substring %q", err.Error(), tc.want)
}
})
}
}
// TestReadSparseZstdTruncated confirms a stream cut short (no end sentinel / partial
// extent) is reported as an error rather than silently producing a short file.
func TestReadSparseZstdTruncated(t *testing.T) {
// A valid stream with one extent but NO end sentinel: the reader should hit EOF
// looking for the next frame offset.
stream := craftSparseStream(t, sparseVersion, 1<<20, [][2]int64{{0, 4096}}, false)
dst, err := os.Create(filepath.Join(t.TempDir(), "dst"))
if err != nil {
t.Fatal(err)
}
defer dst.Close()
if _, err := readSparseZstd(dst, bytes.NewReader(stream)); err == nil {
t.Fatal("expected an error for a stream missing its end sentinel, got nil")
}
}