Files
seaweedfs/weed/storage/blockvol/testrunner/infra/artifacts.go
T
Ping QiuandClaude Opus 4.6 da1b81d1c9 feat: CP8-3-1 durability modes + testrunner platform + 21 adversarial tests
Durability mode implementation (sync_all, sync_quorum, best_effort):
- DurabilityMode type with superblock persistence, parse/validate/string
- MakeDistributedSync mode-aware barrier enforcement in dist_group_commit
- blockerr sentinel package (ErrDurabilityBarrierFailed, ErrDurabilityQuorumLost)
- gRPC create path: mode validation, idempotent create consistency, partial cleanup
- F1: strict mode rejects partial replica provisioning with cleanup
- F3: empty heartbeat does not overwrite persisted strict mode
- F4: SCSI error mapping uses errors.Is sentinels (not string matching)
- Proto/wire/blockapi/CLI/UI plumbing for durability_mode field
- Observability dashboard: cluster health cards + per-volume columns

Testrunner platform (YAML-driven integration test framework):
- Engine, parser, registry, reporter (JUnit XML + HTML), metrics scraping
- 52 registered actions: block, iSCSI, I/O, fault injection, assertions
- Baseline regression framework with 7 hard-fail conditions
- 15 YAML scenarios (smoke, crash, HA, fault, consistency, snapshot)
- 49 unit tests for testrunner internals

QA adversarial suite (21 tests, all PASS):
- Idempotent create mode/RF mismatch detection
- Heartbeat mode downgrade prevention (F3)
- sync_all/sync_quorum partial replica enforcement (F1)
- Concurrent create race safety
- Failover/expand mode preservation
- Cleanup resilience when delete fails
- Master restart auto-register mode handling
- Superblock roundtrip all 3 modes
- Validate edge cases (mode×RF matrix)
- RequiredReplicas quorum math verification
- Sentinel error categorization

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 01:06:51 -08:00

95 lines
2.6 KiB
Go

package infra
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"time"
)
// LogCollector is implemented by any target that can provide a log file.
type LogCollector interface {
CollectLog() (string, error)
}
// ArtifactCollector gathers diagnostic info on failure.
type ArtifactCollector struct {
Dir string // base artifacts directory
Node *Node // initiator node for dmesg/lsblk
Log *log.Logger
}
// NewArtifactCollector creates a collector rooted at the given directory.
func NewArtifactCollector(dir string, node *Node, logger *log.Logger) *ArtifactCollector {
if logger == nil {
logger = log.Default()
}
return &ArtifactCollector{
Dir: dir,
Node: node,
Log: logger,
}
}
// Collect gathers diagnostics when failed is true.
func (a *ArtifactCollector) Collect(failed bool, tgt LogCollector, label string) {
if !failed {
return
}
a.CollectLabeled(tgt, label)
}
// CollectLabeled gathers diagnostics unconditionally.
func (a *ArtifactCollector) CollectLabeled(tgt LogCollector, label string) {
ts := time.Now().Format("20060102-150405")
testDir := filepath.Join(a.Dir, ts)
if err := os.MkdirAll(testDir, 0755); err != nil {
a.Log.Printf("artifacts: mkdir failed: %v", err)
return
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if tgt != nil {
if logContent, err := tgt.CollectLog(); err == nil && logContent != "" {
a.writeArtifact(filepath.Join(testDir, label+".log"), logContent)
}
}
if stdout, _, _, err := a.Node.RunRoot(ctx, "iscsiadm -m session 2>&1"); err == nil {
a.writeArtifact(filepath.Join(testDir, "iscsi-session.txt"), stdout)
}
if stdout, _, _, err := a.Node.RunRoot(ctx, "dmesg | tail -200"); err == nil {
a.writeArtifact(filepath.Join(testDir, "dmesg.txt"), stdout)
}
if stdout, _, _, err := a.Node.Run(ctx, "lsblk 2>&1"); err == nil {
a.writeArtifact(filepath.Join(testDir, "lsblk.txt"), stdout)
}
a.Log.Printf("artifacts saved to %s", testDir)
}
func (a *ArtifactCollector) writeArtifact(path, content string) {
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
a.Log.Printf("artifacts: write %s: %v", path, err)
} else {
a.Log.Printf("artifacts: wrote %s (%d bytes)", filepath.Base(path), len(content))
}
}
// CollectPerf saves performance results to a timestamped JSON file.
func (a *ArtifactCollector) CollectPerf(name string, data string) {
ts := time.Now().Format("20060102-150405")
path := filepath.Join(a.Dir, fmt.Sprintf("perf-%s-%s.json", name, ts))
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
a.Log.Printf("artifacts: mkdir failed: %v", err)
return
}
a.writeArtifact(path, data)
}