master
go 246 lines 6.18 KB
Raw
1 package harness
2
3 import (
4 "errors"
5 "fmt"
6 "os"
7 "path/filepath"
8 "strings"
9 "testing"
10 "time"
11
12 logging "github.com/ipfs/go-log/v2"
13 . "github.com/ipfs/kubo/test/cli/testutils"
14 "github.com/libp2p/go-libp2p/core/peer"
15 "github.com/multiformats/go-multiaddr"
16 )
17
18 // Harness tracks state for a test, such as temp dirs and IFPS nodes, and cleans them up after the test.
19 type Harness struct {
20 Dir string
21 IPFSBin string
22 Runner *Runner
23 NodesRoot string
24 Nodes Nodes
25 stubPeers *stubPeerPool // ephemeral DHT peers for TEST_DHT_STUB mode
26 }
27
28 // TODO: use zaptest.NewLogger(t) instead
29 func EnableDebugLogging() {
30 err := logging.SetLogLevel("testharness", "DEBUG")
31 if err != nil {
32 panic(err)
33 }
34 }
35
36 // NewT constructs a harness that cleans up after the given test is done.
37 func NewT(t *testing.T, options ...func(h *Harness)) *Harness {
38 h := New(options...)
39 t.Cleanup(h.Cleanup)
40 return h
41 }
42
43 func New(options ...func(h *Harness)) *Harness {
44 h := &Harness{Runner: &Runner{Env: osEnviron()}}
45
46 // walk up to find the root dir, from which we can locate the binary
47 wd, err := os.Getwd()
48 if err != nil {
49 panic(err)
50 }
51 goMod := FindUp("go.mod", wd)
52 if goMod == "" {
53 panic("unable to find root dir")
54 }
55 rootDir := filepath.Dir(goMod)
56 h.IPFSBin = filepath.Join(rootDir, "cmd", "ipfs", "ipfs")
57
58 // setup working dir
59 tmpDir, err := os.MkdirTemp("", "")
60 if err != nil {
61 log.Panicf("error creating temp dir: %s", err)
62 }
63 h.Dir = tmpDir
64 h.Runner.Dir = h.Dir
65
66 h.NodesRoot = filepath.Join(h.Dir, ".nodes")
67
68 // apply any customizations
69 // this should happen after all initialization
70 for _, o := range options {
71 o(h)
72 }
73
74 return h
75 }
76
77 // BootstrapWithStubDHT configures each node to bootstrap from
78 // ephemeral in-process DHT peers on loopback instead of the public
79 // swarm. Call after Init() and before StartDaemon().
80 //
81 // Creates 20 ephemeral DHT peers lazily on the first call, shared
82 // across all nodes in this harness. Sets TEST_DHT_STUB on each
83 // node's environment so the daemon lifts WAN DHT filters to accept
84 // loopback peers. Peers are shut down in Cleanup().
85 //
86 // The sweep provider needs >=20 DHT peers to estimate the network
87 // size (prefix length). Without enough peers it stays offline and
88 // never provides.
89 func (h *Harness) BootstrapWithStubDHT(nodes Nodes) {
90 if h.stubPeers == nil {
91 pool, err := newStubPeerPool(stubDHTPeerCount)
92 if err != nil {
93 log.Panicf("creating stub peer pool: %s", err)
94 }
95 h.stubPeers = pool
96 }
97 var addrs []string
98 for _, host := range h.stubPeers.hosts {
99 for _, addr := range host.Addrs() {
100 addrs = append(addrs, addr.String()+"/p2p/"+host.ID().String())
101 }
102 }
103 for _, node := range nodes {
104 node.SetIPFSConfig("Bootstrap", addrs)
105 // Tell the daemon to lift WAN DHT filters so loopback
106 // ephemeral peers enter the WAN routing table.
107 node.Runner.Env["TEST_DHT_STUB"] = "1"
108 }
109 }
110
111 func osEnviron() map[string]string {
112 m := map[string]string{}
113 for _, entry := range os.Environ() {
114 split := strings.Split(entry, "=")
115 m[split[0]] = split[1]
116 }
117 return m
118 }
119
120 func (h *Harness) NewNode() *Node {
121 nodeID := len(h.Nodes)
122 node := BuildNode(h.IPFSBin, h.NodesRoot, nodeID)
123 h.Nodes = append(h.Nodes, node)
124 return node
125 }
126
127 func (h *Harness) NewNodes(count int) Nodes {
128 var newNodes []*Node
129 for range count {
130 newNodes = append(newNodes, h.NewNode())
131 }
132 return newNodes
133 }
134
135 // WriteToTemp writes the given contents to a guaranteed-unique temp file, returning its path.
136 func (h *Harness) WriteToTemp(contents string) string {
137 f := h.TempFile()
138 _, err := f.WriteString(contents)
139 if err != nil {
140 log.Panicf("writing to temp file: %s", err.Error())
141 }
142 err = f.Close()
143 if err != nil {
144 log.Panicf("closing temp file: %s", err.Error())
145 }
146 return f.Name()
147 }
148
149 // TempFile creates a new unique temp file.
150 func (h *Harness) TempFile() *os.File {
151 f, err := os.CreateTemp(h.Dir, "")
152 if err != nil {
153 log.Panicf("creating temp file: %s", err.Error())
154 }
155 return f
156 }
157
158 // WriteFile writes a file given a filename and its contents.
159 // The filename must be a relative path, or this panics.
160 func (h *Harness) WriteFile(filename, contents string) {
161 if filepath.IsAbs(filename) {
162 log.Panicf("%s must be a relative path", filename)
163 }
164 absPath := filepath.Join(h.Runner.Dir, filename)
165 err := os.MkdirAll(filepath.Dir(absPath), 0o777)
166 if err != nil {
167 log.Panicf("creating intermediate dirs for %q: %s", filename, err.Error())
168 }
169 err = os.WriteFile(absPath, []byte(contents), 0o644)
170 if err != nil {
171 log.Panicf("writing %q (%q): %s", filename, absPath, err.Error())
172 }
173 }
174
175 func WaitForFile(path string, timeout time.Duration) error {
176 start := time.Now()
177 timer := time.NewTimer(timeout)
178 ticker := time.NewTicker(1 * time.Millisecond)
179 defer timer.Stop()
180 defer ticker.Stop()
181 for {
182 select {
183 case <-timer.C:
184 return fmt.Errorf("timeout waiting for %s after %v", path, time.Since(start))
185 case <-ticker.C:
186 _, err := os.Stat(path)
187 if err == nil {
188 return nil
189 }
190 if errors.Is(err, os.ErrNotExist) {
191 continue
192 }
193 return fmt.Errorf("error waiting for %s: %w", path, err)
194 }
195 }
196 }
197
198 func (h *Harness) Mkdirs(paths ...string) {
199 for _, path := range paths {
200 if filepath.IsAbs(path) {
201 log.Panicf("%s must be a relative path when making dirs", path)
202 }
203 absPath := filepath.Join(h.Runner.Dir, path)
204 err := os.MkdirAll(absPath, 0o777)
205 if err != nil {
206 log.Panicf("recursively making dirs under %s: %s", absPath, err)
207 }
208 }
209 }
210
211 func (h *Harness) Sh(expr string) *RunResult {
212 return h.Runner.Run(RunRequest{
213 Path: "bash",
214 Args: []string{"-c", expr},
215 })
216 }
217
218 func (h *Harness) Cleanup() {
219 log.Debugf("cleaning up cluster")
220 h.Nodes.StopDaemons()
221 h.stubPeers.Close()
222 log.Debugf("removing harness dir")
223 err := os.RemoveAll(h.Dir)
224 if err != nil {
225 log.Panicf("removing temp dir %s: %s", h.Dir, err)
226 }
227 }
228
229 // ExtractPeerID extracts a peer ID from the given multiaddr, and fatals if it does not contain a peer ID.
230 func (h *Harness) ExtractPeerID(m multiaddr.Multiaddr) peer.ID {
231 var peerIDStr string
232 multiaddr.ForEach(m, func(c multiaddr.Component) bool {
233 if c.Protocol().Code == multiaddr.P_P2P {
234 peerIDStr = c.Value()
235 }
236 return true
237 })
238 if peerIDStr == "" {
239 panic(multiaddr.ErrProtocolNotFound)
240 }
241 peerID, err := peer.Decode(peerIDStr)
242 if err != nil {
243 panic(err)
244 }
245 return peerID
246 }