| 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 | } |