carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1// Package dbreadstest runs the box's half of a forwarded read inside a test
2// process: the real dbreads.Agent and the real dbreads.ServeWorker, joined by
3// net.Pipe instead of the installed socket. Test support only. It is not
4// acceptance for the units (internal/host's root-runner tests are), and
5// nothing outside a _test.go file may import it.
6package dbreadstest
7
8import (
9 "bytes"
10 "context"
11 "crypto/ecdh"
12 "crypto/ed25519"
13 "crypto/sha256"
14 "encoding/hex"
15 "encoding/json"
16 "net"
17 "sync/atomic"
18 "testing"
19 "time"
20
21 "github.com/carlosframework/platform/internal/dbdoor"
22 "github.com/carlosframework/platform/internal/dbreads"
23 "github.com/carlosframework/platform/internal/release"
24)
25
26type Options struct {
27 Store release.Store
28 Region string
29 Box string
30 Pub ed25519.PublicKey
31 HibernatePrefix string
32 OpenReplica func(ctx context.Context, prefix string) (dbdoor.ReplicaReader, error)
33 MaxRestored int64
34 // Worker's Scratch, Exit and Self are filled in when empty.
35 Worker dbreads.WorkerOptions
36}
37
38type Box struct {
39 Agent *dbreads.Agent
40 Dials atomic.Int64
41 live atomic.Int64
42 worker dbreads.WorkerOptions
43}
44
45// Start builds a capable in-process box: its probe passes against an
46// in-process worker reporting a sandboxed view.
47func Start(t testing.TB, o Options) *Box {
48 t.Helper()
49 b := &Box{worker: o.Worker}
50 if b.worker.Scratch == "" {
51 b.worker.Scratch = t.TempDir()
52 }
53 if b.worker.Exit == nil {
54 b.worker.Exit = func(int) {}
55 }
56 if b.worker.Self == nil {
57 b.worker.Self = func() dbreads.HelloReply {
58 return dbreads.HelloReply{V: dbreads.ProtocolVersion, Interfaces: []string{"lo"}}
59 }
60 }
61 b.Agent = dbreads.NewAgent(dbreads.AgentConfig{
62 Store: o.Store, Region: o.Region, Box: o.Box, Boot: "boot-" + o.Box, Pub: o.Pub,
63 HibernatePrefix: o.HibernatePrefix, OpenReplica: o.OpenReplica,
64 DialWorker: b.dial, Units: b.units,
65 Free: func() (uint64, uint64, error) { return 100 << 30, 200 << 30, nil },
66 MaxRestored: o.MaxRestored, Jitter: func() time.Duration { return 0 }, Logf: t.Logf,
67 })
68 b.Agent.Probe(context.Background())
69 if !b.Agent.Capable() {
70 t.Fatal("dbreadstest: the in-process box failed its probe")
71 }
72 return b
73}
74
75func (b *Box) dial(context.Context) (net.Conn, error) {
76 agentEnd, workerEnd := net.Pipe()
77 b.Dials.Add(1)
78 b.live.Add(1)
79 go func() {
80 defer b.live.Add(-1)
81 dbreads.ServeWorker(workerEnd, b.worker)
82 workerEnd.Close()
83 }()
84 return agentEnd, nil
85}
86
87func (b *Box) units(context.Context) (int, error) { return int(b.live.Load()), nil }
88
89// Serve passes until the read's result exists.
90func (b *Box) Serve(t testing.TB, store release.Store, id string, timeout time.Duration) {
91 t.Helper()
92 deadline := time.Now().Add(timeout)
93 for time.Now().Before(deadline) {
94 b.Agent.Pass(context.Background())
95 b.Agent.Wait()
96 if _, _, err := store.Get(context.Background(), dbreads.ResultKey(id)); err == nil {
97 return
98 }
99 time.Sleep(20 * time.Millisecond)
100 }
101 t.Fatalf("dbreadstest: no result for %s within %s", id, timeout)
102}
103
104// Submit is the console's two creates for req, signed with root: for tests
105// that have no console.
106func Submit(t testing.TB, store release.Store, root ed25519.PrivateKey, req dbreads.Request) {
107 t.Helper()
108 wire, err := dbreads.Sign(release.RootSigner(root), req)
109 if err != nil {
110 t.Fatal(err)
111 }
112 if _, err := store.Put(context.Background(), dbreads.RequestKey(req.ID), wire, ""); err != nil {
113 t.Fatal(err)
114 }
115 issued, err := req.Issued()
116 if err != nil {
117 t.Fatal(err)
118 }
119 if _, err := store.Put(context.Background(), dbreads.QueueKey(req.Region, issued, req.ID), nil, ""); err != nil {
120 t.Fatal(err)
121 }
122}
123
124// Answer reads a read's result and opens its chunks with the CLI's key,
125// checking the count and hash a done read's summary carries.
126func Answer(t testing.TB, store release.Store, id string, priv *ecdh.PrivateKey, stream string) ([]byte, dbreads.Summary, dbreads.Result) {
127 t.Helper()
128 raw, _, err := store.Get(context.Background(), dbreads.ResultKey(id))
129 if err != nil {
130 t.Fatalf("dbreadstest: no result for %s: %v", id, err)
131 }
132 var r dbreads.Result
133 if err := json.Unmarshal(raw, &r); err != nil || r.Valid() != nil {
134 t.Fatalf("dbreadstest: result %s: %v", raw, err)
135 }
136 s, err := dbreads.OpenSummary(priv, id, r.Sealed)
137 if err != nil {
138 t.Fatal(err)
139 }
140 var out bytes.Buffer
141 for seq := int64(1); seq <= r.LastSeq; seq++ {
142 sealed, _, err := store.Get(context.Background(), dbreads.ChunkKey(id, seq))
143 if err != nil {
144 t.Fatalf("dbreadstest: chunk %d: %v", seq, err)
145 }
146 plain, err := dbreads.OpenChunk(priv, id, seq, stream, seq == r.LastSeq, sealed)
147 if err != nil {
148 t.Fatalf("dbreadstest: chunk %d: %v", seq, err)
149 }
150 out.Write(plain)
151 }
152 if r.State == dbreads.StateDone {
153 sum := sha256.Sum256(out.Bytes())
154 if int64(out.Len()) != s.Bytes || hex.EncodeToString(sum[:]) != s.SHA256 {
155 t.Fatalf("dbreadstest: the answer does not add up: %d bytes, summary %+v", out.Len(), s)
156 }
157 }
158 return out.Bytes(), s, r
159}
160