Some checks failed
go: build dev binaries / cleanup (push) Successful in 4s
Codespell / Check for spelling errors (push) Failing after 3s
docker: build dev containers / build-rust-binaries (amd64, x86_64-unknown-linux-musl) (push) Failing after 3s
docker: build dev containers / build-dev-containers (push) Has been skipped
docker: build dev containers / build-rust-binaries (arm64, true, aarch64-unknown-linux-musl) (push) Failing after 3s
End to End / FUSE Mount (push) Failing after 2s
EC Integration Tests / EC Integration Tests (push) Failing after 30s
FUSE DLM Integration Tests / FUSE DLM Integration Tests (push) Failing after 3s
FUSE Volume Server Failover Tests / FUSE Volume Server Failover (push) Failing after 3s
FUSE Integration Tests / FUSE Integration Testing (push) Failing after 2s
FUSE P2P Peer Chunk Sharing Integration Tests / FUSE P2P Peer Chunk Sharing (push) Failing after 3s
go: build binary / Go Vet (push) Failing after 2s
go: build binary / Go Vet 32-bit (push) Failing after 3s
go: build binary / Build (push) Failing after 4s
go: build binary / Build cross-platform (freebsd/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (darwin/arm64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/arm64) (push) Failing after 3s
go: build binary / Test (push) Failing after 2s
go: build binary / Test 32-bit (push) Failing after 3s
helm: lint and test charts / lint-test (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (11) (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (17) (push) Failing after 3s
go: build dev binaries / build_dev_linux_windows (amd64, linux) (push) Failing after 2s
go: build dev binaries / build_dev_linux_windows (amd64, windows) (push) Failing after 3s
go: build dev binaries / build_dev_darwin (amd64, darwin) (push) Has been skipped
go: build dev binaries / build_dev_darwin (arm64, darwin) (push) Has been skipped
Java Client Unit Tests / Java Unit Tests (11) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (17) (push) Failing after 2s
Java Client Unit Tests / Java Unit Tests (21) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (8) (push) Failing after 3s
Kafka Quick Test (Load Test with Schema Registry) / Kafka Client Load Test (Quick) (push) Failing after 3s
Kafka Gateway Tests / Kafka Unit Tests (unit-tests-1) (push) Failing after 11s
Kafka Gateway Tests / Kafka Integration Tests (Critical) (integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka End-to-End Tests (with SMQ) (e2e-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Consumer Group Tests (Highly Isolated) (consumer-group-1) (push) Failing after 2s
Kafka Gateway Tests / Kafka Client Compatibility (with SMQ) (client-compat-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka SMQ Integration Tests (Full Stack) (smq-integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Protocol Tests (Isolated) (protocol-1) (push) Failing after 3s
KMS Tests / KMS Provider Integration Tests (push) Failing after 2s
KMS Tests / S3 KMS End-to-End Tests (push) Failing after 3s
Master Cold Start Tests / Master Cold Start Tests (push) Failing after 4s
Metadata Subscribe Integration Tests / Metadata Subscribe Integration Tests (push) Failing after 3s
Multi-Master Tests / Multi-Master Failover Tests (push) Failing after 3s
Performance / CPU and Heap Profile (push) Failing after 2s
Performance / S3 Read/Write Benchmark (rust volume) (push) Failing after 3s
pjdfstest POSIX Compliance / pjdfstest (push) Failing after 2s
Plugin Worker Integration Tests / Plugin Worker: erasure_coding (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: vacuum (push) Failing after 3s
Rust Volume Server Tests / Rust Integration Tests (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 1) (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 1) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 3) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Distributed Tests (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Performance Tests (push) Failing after 3s
Performance / Throughput Benchmark (go volume) (push) Failing after 3s
Performance / Throughput Benchmark (rust volume) (push) Failing after 2s
Performance / S3 Read/Write Benchmark (go volume) (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: volume_balance (push) Failing after 3s
PostgreSQL Gateway Tests / PostgreSQL Basic Tests (push) Failing after 3s
Rust Volume Server Tests / Rust Unit Tests (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 3) (push) Failing after 3s
Rust Plugin Worker Tests / Rust Plugin Worker Build and Unit Tests (push) Failing after 3s
rust: build dev volume server binaries / cleanup (push) Successful in 3s
S3 ETag and ACL Tests / S3 ETag + ACL Integration Tests (push) Failing after 3s
S3 IAM Integration Tests / IAM Unit Tests (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (advanced) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (basic) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (group) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (policy-enforcement) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (sts) (push) Failing after 4s
S3 Keycloak Integration Tests / S3 Keycloak Integration Tests (push) Failing after 3s
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.11) (push) Failing after 3s
S3 PyArrow Parquet Tests / Go Unit Tests (Implicit Directory) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.12) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-amd64, x86_64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-arm64, aarch64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-linux (linux-amd64, x86_64-unknown-linux-gnu) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.9) (push) Has been cancelled
S3 Proxy Signature Tests / S3 Proxy Signature Verification Tests (push) Has been cancelled
S3 SDK V2 Route Disambiguation Tests / S3 SDK V2 Routing Tests (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (quick) (push) Has been cancelled
S3 SSE Tests / S3 SSE Compatibility Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Metadata Persistence Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Copy Operations Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Multipart Upload Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Performance Test (push) Has been cancelled
S3 SSE Tests / S3 Volume Encryption Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (comprehensive) (push) Has been cancelled
Spark Integration Tests / Spark Integration Tests (push) Failing after 2s
Telemetry Integration Tests / telemetry-integration-test (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 1) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 1) (push) Failing after 3s
Ceph S3 tests / Basic S3 tests (KV store) (push) Has been cancelled
Ceph S3 tests / S3 Versioning & Object Lock tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Unit Tests (push) Failing after 3s
Ceph S3 tests / S3 CORS tests (push) Has been cancelled
Ceph S3 tests / SeaweedFS Custom S3 Copy tests (push) Has been cancelled
Ceph S3 tests / Basic S3 tests (SQL store) (push) Has been cancelled
test s3 over https using aws-cli / awscli-tests (push) Has been cancelled
TLS Rotation Integration Tests / TLS Rotation Integration Tests (push) Has been cancelled
Vacuum Integration Tests / Vacuum Integration Tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Variables Integration Tests (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (advanced-policy) (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (basic-policy) (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (contextual-policy) (push) Failing after 4s
S3 Policy Integration Tests / Trusted Proxy Detection Tests (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Shell Integration Tests (push) Failing after 3s
Samba on FUSE Integration / samba-integration (push) Failing after 2s
SFTP Integration Tests / SFTP Integration Testing (push) Failing after 3s
terraform: validate and test modules / fmt, validate, plan-level tests (push) Failing after 3s
terraform: validate and test modules / local cluster smoke test (real weed) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 3) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 2) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 2) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 3) (push) Failing after 5s
mount: windows conformance / WinFsp conformance (push) Has been cancelled
mount: windows / Mount on Windows (push) Has been cancelled
390 lines
13 KiB
Go
390 lines
13 KiB
Go
package lifecycle
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/seaweedfs/test/testutil"
|
|
"github.com/seaweedfs/seaweedfs/weed/worker/tasks/iceberg"
|
|
)
|
|
|
|
const (
|
|
pyicebergImage = "seaweedfs-lifecycle-pyiceberg"
|
|
duckdbImage = "duckdb/duckdb:latest"
|
|
|
|
// The phase that installs the extension, and so the only one whose failure
|
|
// can mean the image rather than the code.
|
|
firstPhase = "write"
|
|
|
|
// Three appends of this many rows, all inside one month so they land in
|
|
// one partition and compaction has something to merge. The two string
|
|
// columns hold few enough distinct values to be dictionary-encoded, which
|
|
// is the encoding the merge destroyed.
|
|
rowsPerBatch = 4000
|
|
batches = 3
|
|
categories = 7
|
|
values = 13
|
|
)
|
|
|
|
// tally is what a client reports about a table. Both halves of this suite
|
|
// speak it, and the point of the whole suite is that the one taken before
|
|
// maintenance equals the one taken after.
|
|
type tally struct {
|
|
Rows int `json:"rows"`
|
|
Categories int `json:"categories"`
|
|
Values int `json:"values"`
|
|
Digest string `json:"digest"`
|
|
// Lance only: a compaction that merged nothing would otherwise let this
|
|
// test pass without having tested anything.
|
|
Fragments int `json:"fragments"`
|
|
Version int `json:"version"`
|
|
}
|
|
|
|
func TestIcebergTableLifecycle(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skipping integration test in short mode")
|
|
}
|
|
if shared == nil {
|
|
t.Skip("no cluster")
|
|
}
|
|
if !testutil.HasDocker() {
|
|
t.Skip("Docker not available")
|
|
}
|
|
|
|
// DuckDB is the client the bug was reported against, and the only one
|
|
// here that writes the deprecated PLAIN_DICTIONARY encoding a Go writer
|
|
// will not produce. PyIceberg writes the modern one, so between them the
|
|
// merge is checked against both dictionary encodings in the spec.
|
|
t.Run("DuckDB", testIcebergLifecycleWithDuckDB)
|
|
t.Run("PyIceberg", testIcebergLifecycleWithPyIceberg)
|
|
}
|
|
|
|
func testIcebergLifecycleWithDuckDB(t *testing.T) {
|
|
env := shared
|
|
bucket := "lifecycle-duckdb-" + randomSuffix()
|
|
namespace, table := "sales", "events"
|
|
env.createTableBucket(t, bucket, "ICEBERG")
|
|
|
|
inserts := make([]string, 0, batches)
|
|
for i := 0; i < batches; i++ {
|
|
inserts = append(inserts, fmt.Sprintf(
|
|
"INSERT INTO cat.%s.%s SELECT g, TIMESTAMPTZ '2026-03-01 00:00:00' + INTERVAL (g %% 600) MINUTE, "+
|
|
"'cat-' || (g %% %d), 'v-' || (g %% %d) FROM generate_series(%d, %d) t(g);",
|
|
namespace, table, categories, values, i*rowsPerBatch+1, (i+1)*rowsPerBatch))
|
|
}
|
|
|
|
before := env.duckdb(t, bucket, "write", strings.Join([]string{
|
|
fmt.Sprintf("CREATE SCHEMA cat.%s;", namespace),
|
|
// Partitioned, the way the table in the report was. Compaction bins
|
|
// per partition, so this is also what puts more than one bin in play.
|
|
fmt.Sprintf("CREATE TABLE cat.%s.%s(id int, ts timestamptz, category text, value text) PARTITIONED BY (month(ts));", namespace, table),
|
|
strings.Join(inserts, "\n"),
|
|
duckdbTallySQL(namespace, table),
|
|
}, "\n"))
|
|
assertSeeded(t, before)
|
|
|
|
env.maintainIcebergTable(t, bucket, namespace+"/"+table)
|
|
|
|
after := env.duckdb(t, bucket, "verify", duckdbTallySQL(namespace, table))
|
|
assertSameData(t, before, after)
|
|
|
|
env.duckdb(t, bucket, "drop", fmt.Sprintf("DROP TABLE cat.%s.%s;\nDROP SCHEMA cat.%s;", namespace, table, namespace))
|
|
if env.entryExists(t, fmt.Sprintf("/buckets/%s/%s/%s/", bucket, namespace, table)) {
|
|
t.Fatal("the dropped table's data is still on disk")
|
|
}
|
|
}
|
|
|
|
func testIcebergLifecycleWithPyIceberg(t *testing.T) {
|
|
env := shared
|
|
bucket := "lifecycle-pyiceberg-" + randomSuffix()
|
|
namespace, table := "sales", "events"
|
|
env.createTableBucket(t, bucket, "ICEBERG")
|
|
buildClientImage(t, pyicebergImage, "Dockerfile.pyiceberg")
|
|
|
|
before := env.pyiceberg(t, "write", bucket, namespace, table)
|
|
assertSeeded(t, before)
|
|
|
|
env.maintainIcebergTable(t, bucket, namespace+"/"+table)
|
|
|
|
after := env.pyiceberg(t, "verify", bucket, namespace, table)
|
|
assertSameData(t, before, after)
|
|
|
|
env.pyiceberg(t, "drop", bucket, namespace, table)
|
|
if env.entryExists(t, fmt.Sprintf("/buckets/%s/%s/%s/", bucket, namespace, table)) {
|
|
t.Fatal("the dropped table's data is still on disk")
|
|
}
|
|
}
|
|
|
|
// maintainIcebergTable runs the worker's whole maintenance cycle against the
|
|
// live filer, in the order a scheduled worker would: merge the small files,
|
|
// drop the snapshots that referenced them, sweep what nothing references any
|
|
// more, then fold the manifests together.
|
|
func (env *environment) maintainIcebergTable(t *testing.T, bucket, tablePath string) {
|
|
t.Helper()
|
|
|
|
client := env.filerClient(t)
|
|
handler := iceberg.NewHandler(nil)
|
|
config := iceberg.Config{
|
|
TargetFileSizeBytes: 256 << 20,
|
|
MinInputFiles: 2,
|
|
MaxCommitRetries: 3,
|
|
SnapshotRetentionMs: 1,
|
|
MaxSnapshotsToKeep: 1,
|
|
OrphanOlderThanHours: 1,
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
|
defer cancel()
|
|
|
|
operations := []struct {
|
|
name string
|
|
run func() (string, map[string]int64, error)
|
|
}{
|
|
{"compact", func() (string, map[string]int64, error) {
|
|
return handler.CompactDataFiles(ctx, client, bucket, tablePath, config)
|
|
}},
|
|
{"expire", func() (string, map[string]int64, error) {
|
|
return handler.ExpireSnapshots(ctx, client, bucket, tablePath, config)
|
|
}},
|
|
{"orphans", func() (string, map[string]int64, error) {
|
|
return handler.RemoveOrphans(ctx, client, bucket, tablePath, config)
|
|
}},
|
|
{"manifests", func() (string, map[string]int64, error) {
|
|
return handler.RewriteManifests(ctx, client, bucket, tablePath, config)
|
|
}},
|
|
}
|
|
for _, operation := range operations {
|
|
result, metrics, err := operation.run()
|
|
if err != nil {
|
|
t.Fatalf("%s: %v", operation.name, err)
|
|
}
|
|
t.Logf("%s: %s %v", operation.name, result, metrics)
|
|
|
|
// A compaction that merged nothing leaves the read below checking a
|
|
// file the worker never wrote, which proves nothing at all.
|
|
if operation.name == "compact" && metrics[iceberg.MetricFilesMerged] < batches {
|
|
t.Fatalf("compaction merged %d files, want all %d written by the client: %s",
|
|
metrics[iceberg.MetricFilesMerged], batches, result)
|
|
}
|
|
}
|
|
}
|
|
|
|
// duckdb runs a script against the catalog and returns whatever tally it
|
|
// printed. The prelude is the reporter's own ATTACH, SigV4 and all.
|
|
func (env *environment) duckdb(t *testing.T, bucket, phase, body string) tally {
|
|
t.Helper()
|
|
|
|
script := fmt.Sprintf(`INSTALL iceberg;
|
|
LOAD iceberg;
|
|
CREATE SECRET s3_secret (TYPE S3, KEY_ID '%s', SECRET '%s', ENDPOINT 'host.docker.internal:%d', URL_STYLE 'path', USE_SSL false);
|
|
ATTACH 's3://%s' AS cat (TYPE ICEBERG, ENDPOINT '%s', AUTHORIZATION_TYPE SigV4, SECRET 's3_secret', SIGV4_SERVICE 's3', SIGV4_REGION 'us-east-1', ACCESS_DELEGATION_MODE 'none', READ_ONLY false);
|
|
%s
|
|
`, accessKey, secretKey, env.s3Port, bucket, env.containerURL(env.icebergPort), body)
|
|
|
|
name := fmt.Sprintf("duckdb-%s-%s.sql", bucket, phase)
|
|
if err := os.WriteFile(filepath.Join(env.dataDir, name), []byte(script), 0644); err != nil {
|
|
t.Fatalf("write the DuckDB script: %v", err)
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), clientTimeout)
|
|
defer cancel()
|
|
|
|
cmd := exec.CommandContext(ctx, "docker", "run", "--rm",
|
|
"-v", fmt.Sprintf("%s:/test", env.dataDir),
|
|
"--add-host", "host.docker.internal:host-gateway",
|
|
"--entrypoint", "duckdb",
|
|
duckdbImage,
|
|
"-init", "/test/"+name,
|
|
"-c", "SELECT 1",
|
|
)
|
|
out, err := cmd.CombinedOutput()
|
|
t.Logf("DuckDB %s:\n%s", phase, out)
|
|
if err != nil {
|
|
if phase == firstPhase && lacksIcebergExtension(string(out)) {
|
|
t.Skipf("this DuckDB image has no iceberg extension: %v", err)
|
|
}
|
|
t.Fatalf("DuckDB %s: %v", phase, err)
|
|
}
|
|
if !strings.Contains(body, "TALLY") {
|
|
return tally{}
|
|
}
|
|
return parseDuckDBTally(t, string(out))
|
|
}
|
|
|
|
func duckdbTallySQL(namespace, table string) string {
|
|
// The digest covers whole rows - every column, not just the two the
|
|
// cardinalities watch - because counting each column on its own passes a
|
|
// merge that keeps every column's cardinality and hands the values to the
|
|
// wrong rows. ts goes in as microseconds so the session's timezone cannot
|
|
// change how it renders between the two runs.
|
|
return fmt.Sprintf("SELECT 'TALLY ' || count(*) || ' ' || count(DISTINCT category) || ' ' || count(DISTINCT value)"+
|
|
" || ' ' || md5(string_agg(id || '|' || epoch_us(ts) || '|' || category || '|' || value, chr(10) ORDER BY id)) AS marker"+
|
|
" FROM cat.%s.%s;",
|
|
namespace, table)
|
|
}
|
|
|
|
var duckdbTallyPattern = regexp.MustCompile(`TALLY (\d+) (\d+) (\d+) ([0-9a-f]{32})`)
|
|
|
|
func parseDuckDBTally(t *testing.T, out string) tally {
|
|
t.Helper()
|
|
|
|
fields := duckdbTallyPattern.FindStringSubmatch(out)
|
|
if fields == nil {
|
|
t.Fatalf("DuckDB printed no tally:\n%s", out)
|
|
}
|
|
number := func(s string) int {
|
|
value, err := strconv.Atoi(s)
|
|
if err != nil {
|
|
t.Fatalf("parse %q: %v", s, err)
|
|
}
|
|
return value
|
|
}
|
|
return tally{
|
|
Rows: number(fields[1]),
|
|
Categories: number(fields[2]),
|
|
Values: number(fields[3]),
|
|
Digest: fields[4],
|
|
}
|
|
}
|
|
|
|
// lacksIcebergExtension reports whether DuckDB never got as far as this
|
|
// repository's code, which is the only failure worth skipping over.
|
|
//
|
|
// The extension ships separately from the image, so an environment that cannot
|
|
// fetch it has nothing to say about compaction. Everything past LOAD - a parse
|
|
// error on the ATTACH, a refusal from the catalog, a bad read - is ours, and
|
|
// has to fail: this is the one test covering the PLAIN_DICTIONARY encoding, and
|
|
// a skip nobody reads is how the corruption it was written for shipped.
|
|
func lacksIcebergExtension(out string) bool {
|
|
for _, marker := range []string{
|
|
"iceberg extension is not available",
|
|
"Failed to download extension",
|
|
`Extension "iceberg" not found`,
|
|
"Unknown extension",
|
|
} {
|
|
if strings.Contains(out, marker) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (env *environment) pyiceberg(t *testing.T, phase, bucket, namespace, table string) tally {
|
|
t.Helper()
|
|
|
|
out := env.runClient(t, pyicebergImage, phase, "/app/iceberg_lifecycle.py",
|
|
"--catalog-url", env.containerURL(env.icebergPort),
|
|
"--s3-endpoint", env.containerURL(env.s3Port),
|
|
"--bucket", bucket,
|
|
"--namespace", namespace,
|
|
"--table", table,
|
|
)
|
|
if phase == "drop" {
|
|
return tally{}
|
|
}
|
|
return decodeTally(t, out)
|
|
}
|
|
|
|
// runClient runs one phase of a python client and returns its stdout.
|
|
func (env *environment) runClient(t *testing.T, image, phase, script string, args ...string) string {
|
|
t.Helper()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), clientTimeout)
|
|
defer cancel()
|
|
|
|
run := []string{"run", "--rm",
|
|
"--add-host", "host.docker.internal:host-gateway",
|
|
"-e", "AWS_ACCESS_KEY_ID=" + accessKey,
|
|
"-e", "AWS_SECRET_ACCESS_KEY=" + secretKey,
|
|
"-e", "AWS_REGION=us-east-1",
|
|
"-e", "AWS_ALLOW_HTTP=true",
|
|
image, "python3", script, "--phase", phase,
|
|
"--access-key", accessKey, "--secret-key", secretKey,
|
|
}
|
|
cmd := exec.CommandContext(ctx, "docker", append(run, args...)...)
|
|
var stderr strings.Builder
|
|
cmd.Stderr = &stderr
|
|
out, err := cmd.Output()
|
|
if stderr.Len() > 0 {
|
|
t.Logf("%s %s (stderr):\n%s", image, phase, stderr.String())
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("%s %s: %v\n%s", image, phase, err, out)
|
|
}
|
|
t.Logf("%s %s: %s", image, phase, strings.TrimSpace(string(out)))
|
|
return string(out)
|
|
}
|
|
|
|
func decodeTally(t *testing.T, out string) tally {
|
|
t.Helper()
|
|
|
|
// The clients print the tally last; anything a library logged before it
|
|
// is not JSON and not ours.
|
|
lines := strings.Split(strings.TrimSpace(out), "\n")
|
|
var decoded tally
|
|
if err := json.Unmarshal([]byte(lines[len(lines)-1]), &decoded); err != nil {
|
|
t.Fatalf("decode the tally from %q: %v", out, err)
|
|
}
|
|
return decoded
|
|
}
|
|
|
|
func buildClientImage(t *testing.T, image, dockerfile string) {
|
|
t.Helper()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute)
|
|
defer cancel()
|
|
|
|
cmd := exec.CommandContext(ctx, "docker", "build", "-t", image, "-f", dockerfile, ".")
|
|
cmd.Dir = shared.testDir
|
|
if out, err := cmd.CombinedOutput(); err != nil {
|
|
t.Fatalf("build %s: %v\n%s", image, err, out)
|
|
}
|
|
}
|
|
|
|
// assertSeeded checks the client wrote what this suite assumes it wrote. Every
|
|
// comparison below is against these numbers, so a client that quietly seeded
|
|
// one row would otherwise make the whole test vacuous.
|
|
func assertSeeded(t *testing.T, seeded tally) {
|
|
t.Helper()
|
|
|
|
if want := rowsPerBatch * batches; seeded.Rows != want {
|
|
t.Fatalf("seeded %d rows, want %d", seeded.Rows, want)
|
|
}
|
|
if seeded.Categories != categories || seeded.Values != values {
|
|
t.Fatalf("seeded %d categories and %d values, want %d and %d",
|
|
seeded.Categories, seeded.Values, categories, values)
|
|
}
|
|
}
|
|
|
|
// assertSameData is the test. Maintenance rewrites files; it must not change a
|
|
// single row, and the cardinalities are called out separately because that is
|
|
// the shape the failure took: a dictionary column collapsed onto one entry,
|
|
// read back without an error, and answered wrongly.
|
|
func assertSameData(t *testing.T, before, after tally) {
|
|
t.Helper()
|
|
|
|
if after.Rows != before.Rows {
|
|
t.Errorf("maintenance changed the row count: %d -> %d", before.Rows, after.Rows)
|
|
}
|
|
if after.Categories != before.Categories {
|
|
t.Errorf("maintenance collapsed the category column: %d distinct values -> %d",
|
|
before.Categories, after.Categories)
|
|
}
|
|
if after.Values != before.Values {
|
|
t.Errorf("maintenance collapsed the value column: %d distinct values -> %d",
|
|
before.Values, after.Values)
|
|
}
|
|
if after.Digest != before.Digest {
|
|
t.Errorf("maintenance changed the rows: digest %s -> %s", before.Digest, after.Digest)
|
|
}
|
|
}
|