- package pex
-
- import (
- "encoding/hex"
- "fmt"
- "io/ioutil"
- "os"
- "path/filepath"
- "testing"
- "time"
-
- "github.com/gogo/protobuf/proto"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-
- "github.com/tendermint/tendermint/config"
- "github.com/tendermint/tendermint/libs/log"
- "github.com/tendermint/tendermint/p2p"
- "github.com/tendermint/tendermint/p2p/mock"
- tmp2p "github.com/tendermint/tendermint/proto/tendermint/p2p"
- )
-
- var (
- cfg *config.P2PConfig
- )
-
- func init() {
- cfg = config.DefaultP2PConfig()
- cfg.PexReactor = true
- cfg.AllowDuplicateIP = true
- }
-
- func TestPEXReactorBasic(t *testing.T) {
- r, _ := createReactor(t, &ReactorConfig{})
-
- assert.NotNil(t, r)
- assert.NotEmpty(t, r.GetChannels())
- }
-
- func TestPEXReactorAddRemovePeer(t *testing.T) {
- r, book := createReactor(t, &ReactorConfig{})
-
- size := book.Size()
- peer := p2p.CreateRandomPeer(false)
-
- r.AddPeer(peer)
- assert.Equal(t, size+1, book.Size())
-
- r.RemovePeer(peer, "peer not available")
-
- outboundPeer := p2p.CreateRandomPeer(true)
-
- r.AddPeer(outboundPeer)
- assert.Equal(t, size+1, book.Size(), "outbound peers should not be added to the address book")
-
- r.RemovePeer(outboundPeer, "peer not available")
- }
-
- // --- FAIL: TestPEXReactorRunning (11.10s)
- // pex_reactor_test.go:411: expected all switches to be connected to at
- // least one peer (switches: 0 => {outbound: 1, inbound: 0}, 1 =>
- // {outbound: 0, inbound: 1}, 2 => {outbound: 0, inbound: 0}, )
- //
- // EXPLANATION: peers are getting rejected because in switch#addPeer we check
- // if any peer (who we already connected to) has the same IP. Even though local
- // peers have different IP addresses, they all have the same underlying remote
- // IP: 127.0.0.1.
- //
- func TestPEXReactorRunning(t *testing.T) {
- N := 3
- switches := make([]*p2p.Switch, N)
-
- // directory to store address books
- dir := tempDir(t)
-
- books := make([]AddrBook, N)
- logger := log.TestingLogger()
-
- // create switches
- for i := 0; i < N; i++ {
- switches[i] = p2p.MakeSwitch(cfg, i, "testing", "123.123.123", func(i int, sw *p2p.Switch) *p2p.Switch {
- books[i] = NewAddrBook(filepath.Join(dir, fmt.Sprintf("addrbook%d.json", i)), false)
- books[i].SetLogger(logger.With("pex", i))
- sw.SetAddrBook(books[i])
-
- sw.SetLogger(logger.With("pex", i))
-
- r := NewReactor(books[i], &ReactorConfig{})
- r.SetLogger(logger.With("pex", i))
- r.SetEnsurePeersPeriod(250 * time.Millisecond)
- sw.AddReactor("pex", r)
-
- return sw
- })
- }
-
- addOtherNodeAddrToAddrBook := func(switchIndex, otherSwitchIndex int) {
- addr := switches[otherSwitchIndex].NetAddress()
- err := books[switchIndex].AddAddress(addr, addr)
- require.NoError(t, err)
- }
-
- addOtherNodeAddrToAddrBook(0, 1)
- addOtherNodeAddrToAddrBook(1, 0)
- addOtherNodeAddrToAddrBook(2, 1)
-
- for _, sw := range switches {
- err := sw.Start() // start switch and reactors
- require.Nil(t, err)
- }
-
- assertPeersWithTimeout(t, switches, 10*time.Millisecond, 10*time.Second, N-1)
-
- // stop them
- for _, s := range switches {
- err := s.Stop()
- require.NoError(t, err)
- }
- }
-
- func TestPEXReactorReceive(t *testing.T) {
- r, book := createReactor(t, &ReactorConfig{})
- peer := p2p.CreateRandomPeer(false)
-
- // we have to send a request to receive responses
- r.RequestAddrs(peer)
-
- size := book.Size()
- na, err := peer.NodeInfo().NetAddress()
- require.NoError(t, err)
- msg := mustEncode(&tmp2p.PexAddrs{Addrs: []tmp2p.NetAddress{na.ToProto()}})
- r.Receive(PexChannel, peer, msg)
- assert.Equal(t, size+1, book.Size())
-
- msg = mustEncode(&tmp2p.PexRequest{})
- r.Receive(PexChannel, peer, msg) // should not panic.
- }
-
- func TestPEXReactorRequestMessageAbuse(t *testing.T) {
- r, book := createReactor(t, &ReactorConfig{})
- sw := createSwitchAndAddReactors(r)
- sw.SetAddrBook(book)
-
- peer := mock.NewPeer(nil)
- peerAddr := peer.SocketAddr()
- p2p.AddPeerToSwitchPeerSet(sw, peer)
- assert.True(t, sw.Peers().Has(peer.ID()))
- err := book.AddAddress(peerAddr, peerAddr)
- require.NoError(t, err)
- require.True(t, book.HasAddress(peerAddr))
-
- id := string(peer.ID())
- msg := mustEncode(&tmp2p.PexRequest{})
-
- // first time creates the entry
- r.Receive(PexChannel, peer, msg)
- assert.True(t, r.lastReceivedRequests.Has(id))
- assert.True(t, sw.Peers().Has(peer.ID()))
-
- // next time sets the last time value
- r.Receive(PexChannel, peer, msg)
- assert.True(t, r.lastReceivedRequests.Has(id))
- assert.True(t, sw.Peers().Has(peer.ID()))
-
- // third time is too many too soon - peer is removed
- r.Receive(PexChannel, peer, msg)
- assert.False(t, r.lastReceivedRequests.Has(id))
- assert.False(t, sw.Peers().Has(peer.ID()))
- assert.True(t, book.IsBanned(peerAddr))
- }
-
- func TestPEXReactorAddrsMessageAbuse(t *testing.T) {
- r, book := createReactor(t, &ReactorConfig{})
- sw := createSwitchAndAddReactors(r)
- sw.SetAddrBook(book)
-
- peer := mock.NewPeer(nil)
- p2p.AddPeerToSwitchPeerSet(sw, peer)
- assert.True(t, sw.Peers().Has(peer.ID()))
-
- id := string(peer.ID())
-
- // request addrs from the peer
- r.RequestAddrs(peer)
- assert.True(t, r.requestsSent.Has(id))
- assert.True(t, sw.Peers().Has(peer.ID()))
-
- msg := mustEncode(&tmp2p.PexAddrs{Addrs: []tmp2p.NetAddress{peer.SocketAddr().ToProto()}})
-
- // receive some addrs. should clear the request
- r.Receive(PexChannel, peer, msg)
- assert.False(t, r.requestsSent.Has(id))
- assert.True(t, sw.Peers().Has(peer.ID()))
-
- // receiving more unsolicited addrs causes a disconnect and ban
- r.Receive(PexChannel, peer, msg)
- assert.False(t, sw.Peers().Has(peer.ID()))
- assert.True(t, book.IsBanned(peer.SocketAddr()))
- }
-
- func TestCheckSeeds(t *testing.T) {
- // directory to store address books
- dir := tempDir(t)
-
- // 1. test creating peer with no seeds works
- peerSwitch := testCreateDefaultPeer(dir, 0)
- require.Nil(t, peerSwitch.Start())
- peerSwitch.Stop() // nolint:errcheck // ignore for tests
-
- // 2. create seed
- seed := testCreateSeed(dir, 1, []*p2p.NetAddress{}, []*p2p.NetAddress{})
-
- // 3. test create peer with online seed works
- peerSwitch = testCreatePeerWithSeed(dir, 2, seed)
- require.Nil(t, peerSwitch.Start())
- peerSwitch.Stop() // nolint:errcheck // ignore for tests
-
- // 4. test create peer with all seeds having unresolvable DNS fails
- badPeerConfig := &ReactorConfig{
- Seeds: []string{"ed3dfd27bfc4af18f67a49862f04cc100696e84d@bad.network.addr:26657",
- "d824b13cb5d40fa1d8a614e089357c7eff31b670@anotherbad.network.addr:26657"},
- }
- peerSwitch = testCreatePeerWithConfig(dir, 2, badPeerConfig)
- require.Error(t, peerSwitch.Start())
- peerSwitch.Stop() // nolint:errcheck // ignore for tests
-
- // 5. test create peer with one good seed address succeeds
- badPeerConfig = &ReactorConfig{
- Seeds: []string{"ed3dfd27bfc4af18f67a49862f04cc100696e84d@bad.network.addr:26657",
- "d824b13cb5d40fa1d8a614e089357c7eff31b670@anotherbad.network.addr:26657",
- seed.NetAddress().String()},
- }
- peerSwitch = testCreatePeerWithConfig(dir, 2, badPeerConfig)
- require.Nil(t, peerSwitch.Start())
- peerSwitch.Stop() // nolint:errcheck // ignore for tests
- }
-
- func TestPEXReactorUsesSeedsIfNeeded(t *testing.T) {
- // directory to store address books
- dir := tempDir(t)
-
- // 1. create seed
- seed := testCreateSeed(dir, 0, []*p2p.NetAddress{}, []*p2p.NetAddress{})
- require.Nil(t, seed.Start())
- t.Cleanup(func() { _ = seed.Stop() })
-
- // 2. create usual peer with only seed configured.
- peer := testCreatePeerWithSeed(dir, 1, seed)
- require.Nil(t, peer.Start())
- t.Cleanup(func() { _ = peer.Stop() })
-
- // 3. check that the peer connects to seed immediately
- assertPeersWithTimeout(t, []*p2p.Switch{peer}, 10*time.Millisecond, 3*time.Second, 1)
- }
-
- func TestConnectionSpeedForPeerReceivedFromSeed(t *testing.T) {
- // directory to store address books
- dir := tempDir(t)
-
- // 1. create peer
- peerSwitch := testCreateDefaultPeer(dir, 1)
- require.Nil(t, peerSwitch.Start())
- t.Cleanup(func() { _ = peerSwitch.Stop() })
-
- // 2. Create seed which knows about the peer
- peerAddr := peerSwitch.NetAddress()
- seed := testCreateSeed(dir, 2, []*p2p.NetAddress{peerAddr}, []*p2p.NetAddress{peerAddr})
- require.Nil(t, seed.Start())
- t.Cleanup(func() { _ = seed.Stop() })
-
- // 3. create another peer with only seed configured.
- secondPeer := testCreatePeerWithSeed(dir, 3, seed)
- require.Nil(t, secondPeer.Start())
- t.Cleanup(func() { _ = secondPeer.Stop() })
-
- // 4. check that the second peer connects to seed immediately
- assertPeersWithTimeout(t, []*p2p.Switch{secondPeer}, 10*time.Millisecond, 3*time.Second, 1)
-
- // 5. check that the second peer connects to the first peer immediately
- assertPeersWithTimeout(t, []*p2p.Switch{secondPeer}, 10*time.Millisecond, 1*time.Second, 2)
- }
-
- func TestPEXReactorSeedMode(t *testing.T) {
- // directory to store address books
- dir := tempDir(t)
-
- pexRConfig := &ReactorConfig{SeedMode: true, SeedDisconnectWaitPeriod: 10 * time.Millisecond}
- pexR, book := createReactor(t, pexRConfig)
- sw := createSwitchAndAddReactors(pexR)
-
- sw.SetAddrBook(book)
- require.NoError(t, sw.Start())
- t.Cleanup(func() { _ = sw.Stop() })
-
- assert.Zero(t, sw.Peers().Size())
-
- peerSwitch := testCreateDefaultPeer(dir, 1)
- require.NoError(t, peerSwitch.Start())
- t.Cleanup(func() { _ = peerSwitch.Stop() })
-
- // 1. Test crawlPeers dials the peer
- pexR.crawlPeers([]*p2p.NetAddress{peerSwitch.NetAddress()})
- assert.Equal(t, 1, sw.Peers().Size())
- assert.True(t, sw.Peers().Has(peerSwitch.NodeInfo().ID()))
-
- // 2. attemptDisconnects should not disconnect because of wait period
- pexR.attemptDisconnects()
- assert.Equal(t, 1, sw.Peers().Size())
-
- // sleep for SeedDisconnectWaitPeriod
- time.Sleep(pexRConfig.SeedDisconnectWaitPeriod + 1*time.Millisecond)
-
- // 3. attemptDisconnects should disconnect after wait period
- pexR.attemptDisconnects()
- assert.Equal(t, 0, sw.Peers().Size())
- }
-
- func TestPEXReactorDoesNotDisconnectFromPersistentPeerInSeedMode(t *testing.T) {
- // directory to store address books
- dir := tempDir(t)
-
- pexRConfig := &ReactorConfig{SeedMode: true, SeedDisconnectWaitPeriod: 1 * time.Millisecond}
- pexR, book := createReactor(t, pexRConfig)
- sw := createSwitchAndAddReactors(pexR)
-
- sw.SetAddrBook(book)
- require.NoError(t, sw.Start())
- t.Cleanup(func() { _ = sw.Stop() })
-
- assert.Zero(t, sw.Peers().Size())
-
- peerSwitch := testCreatePeerWithConfig(dir, 1, pexRConfig)
- require.NoError(t, peerSwitch.Start())
- t.Cleanup(func() { _ = peerSwitch.Stop() })
-
- require.NoError(t, sw.AddPersistentPeers([]string{peerSwitch.NetAddress().String()}))
-
- // 1. Test crawlPeers dials the peer
- pexR.crawlPeers([]*p2p.NetAddress{peerSwitch.NetAddress()})
- assert.Equal(t, 1, sw.Peers().Size())
- assert.True(t, sw.Peers().Has(peerSwitch.NodeInfo().ID()))
-
- // sleep for SeedDisconnectWaitPeriod
- time.Sleep(pexRConfig.SeedDisconnectWaitPeriod + 1*time.Millisecond)
-
- // 2. attemptDisconnects should not disconnect because the peer is persistent
- pexR.attemptDisconnects()
- assert.Equal(t, 1, sw.Peers().Size())
- }
-
- func TestPEXReactorDialsPeerUpToMaxAttemptsInSeedMode(t *testing.T) {
- // directory to store address books
- pexR, book := createReactor(t, &ReactorConfig{SeedMode: true})
- sw := createSwitchAndAddReactors(pexR)
-
- sw.SetAddrBook(book)
- // No need to start sw since crawlPeers is called manually here.
-
- peer := mock.NewPeer(nil)
- addr := peer.SocketAddr()
-
- require.NoError(t, book.AddAddress(addr, addr))
-
- assert.True(t, book.HasAddress(addr))
-
- // imitate maxAttemptsToDial reached
- pexR.attemptsToDial.Store(addr.DialString(), _attemptsToDial{maxAttemptsToDial + 1, time.Now()})
- pexR.crawlPeers([]*p2p.NetAddress{addr})
-
- assert.False(t, book.HasAddress(addr))
- }
-
- // connect a peer to a seed, wait a bit, then stop it.
- // this should give it time to request addrs and for the seed
- // to call FlushStop, and allows us to test calling Stop concurrently
- // with FlushStop. Before a fix, this non-deterministically reproduced
- // https://github.com/tendermint/tendermint/issues/3231.
- func TestPEXReactorSeedModeFlushStop(t *testing.T) {
- N := 2
- switches := make([]*p2p.Switch, N)
-
- // directory to store address books
- dir := tempDir(t)
-
- books := make([]AddrBook, N)
- logger := log.TestingLogger()
-
- // create switches
- for i := 0; i < N; i++ {
- switches[i] = p2p.MakeSwitch(cfg, i, "testing", "123.123.123", func(i int, sw *p2p.Switch) *p2p.Switch {
- books[i] = NewAddrBook(filepath.Join(dir, fmt.Sprintf("addrbook%d.json", i)), false)
- books[i].SetLogger(logger.With("pex", i))
- sw.SetAddrBook(books[i])
-
- sw.SetLogger(logger.With("pex", i))
-
- config := &ReactorConfig{}
- if i == 0 {
- // first one is a seed node
- config = &ReactorConfig{SeedMode: true}
- }
- r := NewReactor(books[i], config)
- r.SetLogger(logger.With("pex", i))
- r.SetEnsurePeersPeriod(250 * time.Millisecond)
- sw.AddReactor("pex", r)
-
- return sw
- })
- }
-
- for _, sw := range switches {
- err := sw.Start() // start switch and reactors
- require.Nil(t, err)
- }
-
- reactor := switches[0].Reactors()["pex"].(*Reactor)
- peerID := switches[1].NodeInfo().ID()
-
- assert.NoError(t, switches[1].DialPeerWithAddress(switches[0].NetAddress()))
-
- // sleep up to a second while waiting for the peer to send us a message.
- // this isn't perfect since it's possible the peer sends us a msg and we FlushStop
- // before this loop catches it. but non-deterministically it works pretty well.
- for i := 0; i < 1000; i++ {
- v := reactor.lastReceivedRequests.Get(string(peerID))
- if v != nil {
- break
- }
- time.Sleep(time.Millisecond)
- }
-
- // by now the FlushStop should have happened. Try stopping the peer.
- // it should be safe to do this.
- peers := switches[0].Peers().List()
- for _, peer := range peers {
- err := peer.Stop()
- require.NoError(t, err)
- }
-
- // stop the switches
- for _, s := range switches {
- err := s.Stop()
- require.NoError(t, err)
- }
- }
-
- func TestPEXReactorDoesNotAddPrivatePeersToAddrBook(t *testing.T) {
- peer := p2p.CreateRandomPeer(false)
-
- pexR, book := createReactor(t, &ReactorConfig{})
- book.AddPrivateIDs([]string{string(peer.NodeInfo().ID())})
-
- // we have to send a request to receive responses
- pexR.RequestAddrs(peer)
-
- size := book.Size()
- na, err := peer.NodeInfo().NetAddress()
- require.NoError(t, err)
- msg := mustEncode(&tmp2p.PexAddrs{Addrs: []tmp2p.NetAddress{na.ToProto()}})
- pexR.Receive(PexChannel, peer, msg)
- assert.Equal(t, size, book.Size())
-
- pexR.AddPeer(peer)
- assert.Equal(t, size, book.Size())
- }
-
- func TestPEXReactorDialPeer(t *testing.T) {
- pexR, book := createReactor(t, &ReactorConfig{})
- sw := createSwitchAndAddReactors(pexR)
-
- sw.SetAddrBook(book)
-
- peer := mock.NewPeer(nil)
- addr := peer.SocketAddr()
-
- assert.Equal(t, 0, pexR.AttemptsToDial(addr))
-
- // 1st unsuccessful attempt
- err := pexR.dialPeer(addr)
- require.Error(t, err)
-
- assert.Equal(t, 1, pexR.AttemptsToDial(addr))
-
- // 2nd unsuccessful attempt
- err = pexR.dialPeer(addr)
- require.Error(t, err)
-
- // must be skipped because it is too early
- assert.Equal(t, 1, pexR.AttemptsToDial(addr))
-
- if !testing.Short() {
- time.Sleep(3 * time.Second)
-
- // 3rd attempt
- err = pexR.dialPeer(addr)
- require.Error(t, err)
-
- assert.Equal(t, 2, pexR.AttemptsToDial(addr))
- }
- }
-
- func assertPeersWithTimeout(
- t *testing.T,
- switches []*p2p.Switch,
- checkPeriod, timeout time.Duration,
- nPeers int,
- ) {
- var (
- ticker = time.NewTicker(checkPeriod)
- remaining = timeout
- )
-
- for {
- select {
- case <-ticker.C:
- // check peers are connected
- allGood := true
- for _, s := range switches {
- outbound, inbound, _ := s.NumPeers()
- if outbound+inbound < nPeers {
- allGood = false
- break
- }
- }
- remaining -= checkPeriod
- if remaining < 0 {
- remaining = 0
- }
- if allGood {
- return
- }
- case <-time.After(remaining):
- numPeersStr := ""
- for i, s := range switches {
- outbound, inbound, _ := s.NumPeers()
- numPeersStr += fmt.Sprintf("%d => {outbound: %d, inbound: %d}, ", i, outbound, inbound)
- }
- t.Errorf(
- "expected all switches to be connected to at least %d peer(s) (switches: %s)",
- nPeers, numPeersStr,
- )
- return
- }
- }
- }
-
- // Creates a peer with the provided config
- func testCreatePeerWithConfig(dir string, id int, config *ReactorConfig) *p2p.Switch {
- peer := p2p.MakeSwitch(
- cfg,
- id,
- "127.0.0.1",
- "123.123.123",
- func(i int, sw *p2p.Switch) *p2p.Switch {
- book := NewAddrBook(filepath.Join(dir, fmt.Sprintf("addrbook%d.json", id)), false)
- book.SetLogger(log.TestingLogger())
- sw.SetAddrBook(book)
-
- sw.SetLogger(log.TestingLogger())
-
- r := NewReactor(
- book,
- config,
- )
- r.SetLogger(log.TestingLogger())
- sw.AddReactor("pex", r)
- return sw
- },
- )
- return peer
- }
-
- // Creates a peer with the default config
- func testCreateDefaultPeer(dir string, id int) *p2p.Switch {
- return testCreatePeerWithConfig(dir, id, &ReactorConfig{})
- }
-
- // Creates a seed which knows about the provided addresses / source address pairs.
- // Starting and stopping the seed is left to the caller
- func testCreateSeed(dir string, id int, knownAddrs, srcAddrs []*p2p.NetAddress) *p2p.Switch {
- seed := p2p.MakeSwitch(
- cfg,
- id,
- "127.0.0.1",
- "123.123.123",
- func(i int, sw *p2p.Switch) *p2p.Switch {
- book := NewAddrBook(filepath.Join(dir, "addrbookSeed.json"), false)
- book.SetLogger(log.TestingLogger())
- for j := 0; j < len(knownAddrs); j++ {
- book.AddAddress(knownAddrs[j], srcAddrs[j]) // nolint:errcheck // ignore for tests
- book.MarkGood(knownAddrs[j].ID)
- }
- sw.SetAddrBook(book)
-
- sw.SetLogger(log.TestingLogger())
-
- r := NewReactor(book, &ReactorConfig{})
- r.SetLogger(log.TestingLogger())
- sw.AddReactor("pex", r)
- return sw
- },
- )
- return seed
- }
-
- // Creates a peer which knows about the provided seed.
- // Starting and stopping the peer is left to the caller
- func testCreatePeerWithSeed(dir string, id int, seed *p2p.Switch) *p2p.Switch {
- conf := &ReactorConfig{
- Seeds: []string{seed.NetAddress().String()},
- }
- return testCreatePeerWithConfig(dir, id, conf)
- }
-
- func createReactor(t *testing.T, conf *ReactorConfig) (r *Reactor, book AddrBook) {
- // directory to store address book
- book = NewAddrBook(filepath.Join(tempDir(t), "addrbook.json"), true)
- book.SetLogger(log.TestingLogger())
-
- r = NewReactor(book, conf)
- r.SetLogger(log.TestingLogger())
- return
- }
-
- func createSwitchAndAddReactors(reactors ...p2p.Reactor) *p2p.Switch {
- sw := p2p.MakeSwitch(cfg, 0, "127.0.0.1", "123.123.123", func(i int, sw *p2p.Switch) *p2p.Switch {
- for _, r := range reactors {
- sw.AddReactor(r.String(), r)
- }
- return sw
- })
- sw.SetLogger(log.TestingLogger())
- return sw
- }
-
- func TestPexVectors(t *testing.T) {
- addr := tmp2p.NetAddress{
- ID: "1",
- IP: "127.0.0.1",
- Port: 9090,
- }
-
- testCases := []struct {
- testName string
- msg proto.Message
- expBytes string
- }{
- {"PexRequest", &tmp2p.PexRequest{}, "0a00"},
- {"PexAddrs", &tmp2p.PexAddrs{Addrs: []tmp2p.NetAddress{addr}}, "12130a110a013112093132372e302e302e31188247"},
- }
-
- for _, tc := range testCases {
- tc := tc
-
- bz := mustEncode(tc.msg)
-
- require.Equal(t, tc.expBytes, hex.EncodeToString(bz), tc.testName)
- }
- }
-
- // FIXME: This function is used in place of testing.TB.TempDir()
- // as the latter seems to cause test cases to fail when it is
- // unable to remove the temporary directory once the test case
- // execution terminates. This seems to happen often with pex
- // reactor test cases.
- //
- // References:
- // https://github.com/tendermint/tendermint/pull/5733
- // https://github.com/tendermint/tendermint/issues/5732
- func tempDir(t *testing.T) string {
- t.Helper()
- dir, err := ioutil.TempDir("", "")
- require.NoError(t, err)
- t.Cleanup(func() { _ = os.RemoveAll(dir) })
- return dir
- }
|