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  }