github.com/qri-io/qri@v0.10.1-0.20220104210721-c771715036cb/repo/test/temp_repo.go (about) 1 package test 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "io/ioutil" 8 "os" 9 "path/filepath" 10 "time" 11 12 "github.com/qri-io/dataset" 13 "github.com/qri-io/qfs" 14 "github.com/qri-io/qfs/qipfs" 15 "github.com/qri-io/qri/auth/key" 16 "github.com/qri-io/qri/base/dsfs" 17 "github.com/qri-io/qri/config" 18 testcfg "github.com/qri-io/qri/config/test" 19 "github.com/qri-io/qri/event" 20 "github.com/qri-io/qri/repo" 21 "github.com/qri-io/qri/repo/buildrepo" 22 ) 23 24 // TempRepo manages a temporary repository for testing purposes, adding extra 25 // methods for testing convenience 26 type TempRepo struct { 27 RootPath string 28 IPFSPath string 29 QriPath string 30 TestCrypto key.CryptoGenerator 31 32 cfg *config.Config 33 UseMockRemoteClient bool 34 } 35 36 // NewTempRepoFixedProfileID creates a temp repo that always uses the same 37 // PKI credentials 38 func NewTempRepoFixedProfileID(peername, prefix string) (r TempRepo, err error) { 39 return newTempRepo(peername, prefix, NewTestCrypto()) 40 } 41 42 // NewTempRepoUsingPeerInfo creates a temp repo using the given peerInfo 43 func NewTempRepoUsingPeerInfo(peerInfoNum int, peername, prefix string) (r TempRepo, err error) { 44 crypto := NewTestCrypto() 45 for i := 0; i < peerInfoNum; i++ { 46 // TestCrypto uses a list of pre-generated private / public keys pairs, for performance 47 // reasons and to make tests deterministic. Each time TestCrypt.GeneratePrivate... is 48 // called, it will return the next peer info in this list. Most tests should always be 49 // using different peer info, but may occassionally want them to match (to test conflicts). 50 // This function can be used to skip a certain number of peer infos in order to get 51 // a certain private key / profile ID that a test needs. 52 _, _ = crypto.GeneratePrivateKeyAndPeerID() 53 } 54 return newTempRepo(peername, prefix, crypto) 55 } 56 57 // NewTempRepo constructs the test repo and initializes everything as cheaply 58 // as possible. This function is non-deterministic. Each successive call to 59 // TempRepo will use different PKI credentials 60 func NewTempRepo(peername, prefix string, g key.CryptoGenerator) (r TempRepo, err error) { 61 return newTempRepo(peername, prefix, g) 62 } 63 64 func newTempRepo(peername, prefix string, g key.CryptoGenerator) (r TempRepo, err error) { 65 RootPath, err := ioutil.TempDir("", prefix) 66 if err != nil { 67 return r, err 68 } 69 70 // Create directory for new Qri repo. 71 QriPath := filepath.Join(RootPath, "qri") 72 err = os.MkdirAll(QriPath, os.ModePerm) 73 if err != nil { 74 return r, err 75 } 76 // Create directory for new IPFS repo. 77 IPFSPath := filepath.Join(QriPath, "ipfs") 78 err = os.MkdirAll(IPFSPath, os.ModePerm) 79 if err != nil { 80 return r, err 81 } 82 // Build IPFS repo directory by unzipping an empty repo. 83 err = InitIPFSRepo(IPFSPath, "") 84 if err != nil { 85 return r, err 86 } 87 88 // Create empty config.yaml into the test repo. 89 cfg := testcfg.DefaultConfigForTesting().Copy() 90 cfg.Profile.Peername = peername 91 cfg.Profile.PrivKey, cfg.Profile.ID = g.GeneratePrivateKeyAndPeerID() 92 cfg.SetPath(filepath.Join(QriPath, "config.yaml")) 93 cfg.Filesystems = []qfs.Config{ 94 {Type: "ipfs", Config: map[string]interface{}{"path": IPFSPath}}, 95 {Type: "local"}, 96 {Type: "http"}, 97 } 98 99 r = TempRepo{ 100 RootPath: RootPath, 101 IPFSPath: IPFSPath, 102 QriPath: QriPath, 103 TestCrypto: g, 104 cfg: cfg, 105 } 106 107 if err := r.WriteConfigFile(); err != nil { 108 return r, err 109 } 110 111 return r, nil 112 } 113 114 // Repo constructs the repo for use in tests, the passed in context MUST be 115 // cancelled when finished. This repo creates it's own event bus 116 func (r *TempRepo) Repo(ctx context.Context) (repo.Repo, error) { 117 return buildrepo.New(ctx, r.QriPath, r.cfg, func(o *buildrepo.Options) { 118 o.Bus = event.NewBus(ctx) 119 }) 120 } 121 122 // Delete removes the test repo on disk. 123 func (r *TempRepo) Delete() { 124 os.RemoveAll(r.RootPath) 125 } 126 127 // WriteConfigFile serializes the config file and writes it to the qri repository 128 func (r *TempRepo) WriteConfigFile() error { 129 return r.cfg.WriteToFile(filepath.Join(r.QriPath, "config.yaml")) 130 } 131 132 // GetConfig returns the configuration for the test repo. 133 func (r *TempRepo) GetConfig() *config.Config { 134 return r.cfg 135 } 136 137 // GetPathForDataset returns the path to where the index'th dataset is stored on CAFS. 138 func (r *TempRepo) GetPathForDataset(index int) (string, error) { 139 dsRefs := filepath.Join(r.QriPath, "refs.fbs") 140 141 data, err := ioutil.ReadFile(dsRefs) 142 if err != nil { 143 return "", err 144 } 145 146 refs, err := repo.UnmarshalRefsFlatbuffer(data) 147 if err != nil { 148 return "", err 149 } 150 151 // If dataset doesn't exist, return an empty string for the path. 152 if len(refs) == 0 { 153 return "", err 154 } 155 156 return refs[index].Path, nil 157 } 158 159 // ReadBodyFromIPFS reads the body of the dataset at the given keyPath stored 160 // in CAFS 161 func (r *TempRepo) ReadBodyFromIPFS(keyPath string) (string, error) { 162 ctx, cancel := context.WithCancel(context.Background()) 163 defer cancel() 164 165 fs, err := qipfs.NewFilesystem(ctx, map[string]interface{}{ 166 "online": false, 167 "path": r.IPFSPath, 168 }) 169 170 if err != nil { 171 return "", err 172 } 173 174 bodyFile, err := fs.Get(ctx, keyPath) 175 if err != nil { 176 return "", err 177 } 178 179 bodyBytes, err := ioutil.ReadAll(bodyFile) 180 if err != nil { 181 return "", err 182 } 183 184 done := gracefulShutdown(fs.(qfs.ReleasingFilesystem).Done()) 185 cancel() 186 err = <-done 187 return string(bodyBytes), err 188 } 189 190 // DatasetMarshalJSON reads the dataset head and marshals it as json. 191 func (r *TempRepo) DatasetMarshalJSON(ref string) (string, error) { 192 ds, err := r.LoadDataset(ref) 193 if err != nil { 194 return "", err 195 } 196 data, err := json.Marshal(ds) 197 if err != nil { 198 return "", err 199 } 200 return string(data), err 201 } 202 203 // LoadDataset from the temp repository 204 func (r *TempRepo) LoadDataset(ref string) (*dataset.Dataset, error) { 205 ctx, cancel := context.WithCancel(context.Background()) 206 defer cancel() 207 fs, err := qipfs.NewFilesystem(ctx, map[string]interface{}{ 208 "online": false, 209 "path": r.IPFSPath, 210 }) 211 ds, err := dsfs.LoadDataset(ctx, fs, ref) 212 if err != nil { 213 return nil, err 214 } 215 done := gracefulShutdown(fs.(qfs.ReleasingFilesystem).Done()) 216 cancel() 217 err = <-done 218 return ds, err 219 } 220 221 // WriteRootFile writes a file string to the root directory of the temp repo 222 func (r *TempRepo) WriteRootFile(filename, data string) (path string, err error) { 223 path = filepath.Join(r.RootPath, filename) 224 err = ioutil.WriteFile(path, []byte(data), 0667) 225 return path, err 226 } 227 228 func gracefulShutdown(doneCh <-chan struct{}) chan error { 229 waitForDone := make(chan error) 230 go func() { 231 select { 232 case <-time.NewTimer(time.Second).C: 233 waitForDone <- fmt.Errorf("shutdown didn't send on 'done' channel within 1 second of context cancellation") 234 case <-doneCh: 235 waitForDone <- nil 236 } 237 }() 238 return waitForDone 239 }