github.com/qri-io/qri@v0.10.1-0.20220104210721-c771715036cb/cmd/test_runner_test.go (about)

     1  package cmd
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"errors"
     7  	"fmt"
     8  	"io/ioutil"
     9  	"os"
    10  	"path/filepath"
    11  	"runtime"
    12  	"strconv"
    13  	"strings"
    14  	"sync"
    15  	"testing"
    16  	"text/scanner"
    17  	"time"
    18  
    19  	"github.com/qri-io/dataset"
    20  	"github.com/qri-io/ioes"
    21  	"github.com/qri-io/qri/auth/key"
    22  	run "github.com/qri-io/qri/automation/run"
    23  	"github.com/qri-io/qri/base"
    24  	"github.com/qri-io/qri/base/dsfs"
    25  	"github.com/qri-io/qri/dsref"
    26  	"github.com/qri-io/qri/lib"
    27  	"github.com/qri-io/qri/logbook"
    28  	"github.com/qri-io/qri/registry"
    29  	"github.com/qri-io/qri/registry/regserver"
    30  	remotemock "github.com/qri-io/qri/remote/mock"
    31  	"github.com/qri-io/qri/repo"
    32  	repotest "github.com/qri-io/qri/repo/test"
    33  	"github.com/qri-io/qri/transform/startf"
    34  	"github.com/spf13/cobra"
    35  )
    36  
    37  // TestRunner holds data used to run tests
    38  type TestRunner struct {
    39  	RepoRoot *repotest.TempRepo
    40  	RepoPath string
    41  
    42  	Context       context.Context
    43  	ContextDone   func()
    44  	TmpDir        string
    45  	Streams       ioes.IOStreams
    46  	InStream      *bytes.Buffer
    47  	OutStream     *bytes.Buffer
    48  	ErrStream     *bytes.Buffer
    49  	DsfsTsFunc    func() time.Time
    50  	LogbookTsFunc func() int64
    51  	LocOrig       *time.Location
    52  	XformVersion  string
    53  	CmdR          *cobra.Command
    54  	Teardown      func()
    55  	CmdDoneCh     chan struct{}
    56  	TestCrypto    key.CryptoGenerator
    57  
    58  	Registry *registry.Registry
    59  }
    60  
    61  // NewTestRunner constructs a new TestRunner
    62  func NewTestRunner(t *testing.T, peerName, testName string) *TestRunner {
    63  	root, err := repotest.NewTempRepoFixedProfileID(peerName, testName)
    64  	if err != nil {
    65  		t.Fatalf("creating temp repo: %s", err)
    66  	}
    67  	return newTestRunnerFromRoot(&root)
    68  }
    69  
    70  // NewTestRunnerWithMockRemoteClient constructs a test runner with a mock remote client
    71  func NewTestRunnerWithMockRemoteClient(t *testing.T, peerName, testName string) *TestRunner {
    72  	root, err := repotest.NewTempRepoFixedProfileID(peerName, testName)
    73  	if err != nil {
    74  		t.Fatalf("creating temp repo: %s", err)
    75  	}
    76  	root.UseMockRemoteClient = true
    77  	return newTestRunnerFromRoot(&root)
    78  }
    79  
    80  // NewTestRunnerUsingPeerInfoWithMockRemoteClient constructs a test runner using an
    81  // explicit testPeer, as well as a mock remote client
    82  func NewTestRunnerUsingPeerInfoWithMockRemoteClient(t *testing.T, peerInfoNum int, peerName, testName string) *TestRunner {
    83  	root, err := repotest.NewTempRepoUsingPeerInfo(peerInfoNum, peerName, testName)
    84  	if err != nil {
    85  		t.Fatalf("creating temp repo: %s", err)
    86  	}
    87  	root.UseMockRemoteClient = true
    88  	return newTestRunnerFromRoot(&root)
    89  }
    90  
    91  // NewTestRunnerWithTempRegistry constructs a test runner with a mock registry connection
    92  func NewTestRunnerWithTempRegistry(t *testing.T, peerName, testName string) *TestRunner {
    93  	t.Helper()
    94  	root, err := repotest.NewTempRepoFixedProfileID(peerName, testName)
    95  	if err != nil {
    96  		t.Fatalf("creating temp repo: %s", err)
    97  	}
    98  
    99  	ctx, cancel := context.WithCancel(context.Background())
   100  	// TODO(dustmop): Switch to root.TestCrypto. Until then, we're reusing the
   101  	// same testPeers, leading to different nodes with the same profileID
   102  	g := repotest.NewTestCrypto()
   103  	reg, teardownRegistry, err := regserver.NewTempRegistry(ctx, "registry", testName+"_registry", g)
   104  	if err != nil {
   105  		t.Fatalf("creating registry: %s", err)
   106  	}
   107  
   108  	// TODO (b5) - wouldn't it be nice if we could pass the client as an instance configuration
   109  	// option? that'd require re-thinking the way we do NewQriCommand
   110  	_, server := regserver.NewMockServerRegistry(*reg)
   111  
   112  	tr := newTestRunnerFromRoot(&root)
   113  	tr.Registry = reg
   114  	prevTeardown := tr.Teardown
   115  	tr.Teardown = func() {
   116  		cancel()
   117  		teardownRegistry()
   118  		server.Close()
   119  		if prevTeardown != nil {
   120  			prevTeardown()
   121  		}
   122  	}
   123  
   124  	root.GetConfig().Registry.Location = server.URL
   125  	if err := root.WriteConfigFile(); err != nil {
   126  		t.Fatalf("writing config file: %s", err)
   127  	}
   128  
   129  	return tr
   130  }
   131  
   132  func useConsistentRunIDs() {
   133  	source := strings.NewReader(strings.Repeat("OmgZombies!?!?!", 200))
   134  	run.SetIDRand(source)
   135  }
   136  
   137  func newTestRunnerFromRoot(root *repotest.TempRepo) *TestRunner {
   138  	ctx, cancel := context.WithCancel(context.Background())
   139  	useConsistentRunIDs()
   140  
   141  	tr := TestRunner{
   142  		RepoRoot:    root,
   143  		RepoPath:    filepath.Join(root.RootPath, "qri"),
   144  		Context:     ctx,
   145  		ContextDone: cancel,
   146  		TestCrypto:  root.TestCrypto,
   147  	}
   148  
   149  	// TmpDir will be removed recursively, only if it is non-empty
   150  	tr.TmpDir = ""
   151  
   152  	// To keep hashes consistent, artificially specify the timestamp by overriding
   153  	// the dsfs.Timestamp func
   154  	dsfsCounter := 0
   155  	tr.DsfsTsFunc = dsfs.Timestamp
   156  	dsfs.Timestamp = func() time.Time {
   157  		dsfsCounter++
   158  		return time.Date(2001, 01, 01, 01, dsfsCounter, 01, 01, time.UTC)
   159  	}
   160  
   161  	// Do the same for logbook.NewTimestamp
   162  	bookCounter := 0
   163  	tr.LogbookTsFunc = logbook.NewTimestamp
   164  	logbook.NewTimestamp = func() int64 {
   165  		bookCounter++
   166  		return time.Date(2001, 01, 01, 01, bookCounter, 01, 01, time.UTC).Unix()
   167  	}
   168  
   169  	// Set IOStreams
   170  	tr.Streams, tr.InStream, tr.OutStream, tr.ErrStream = ioes.NewTestIOStreams()
   171  	setNoColor(true)
   172  	//setNoPrompt(true)
   173  
   174  	// Set the location to New York so that timezone printing is consistent
   175  	location, _ := time.LoadLocation("America/New_York")
   176  	tr.LocOrig = StringerLocation
   177  	StringerLocation = location
   178  
   179  	// Stub the version of starlark, because it is output when transforms run
   180  	tr.XformVersion = startf.Version
   181  	startf.Version = "test_version"
   182  
   183  	return &tr
   184  }
   185  
   186  // Delete cleans up after a TestRunner is done being used.
   187  func (runner *TestRunner) Delete() {
   188  	if runner.Teardown != nil {
   189  		runner.Teardown()
   190  	}
   191  	if runner.TmpDir != "" {
   192  		os.RemoveAll(runner.TmpDir)
   193  	}
   194  	// restore random RunID generator
   195  	run.SetIDRand(nil)
   196  	dsfs.Timestamp = runner.DsfsTsFunc
   197  	logbook.NewTimestamp = runner.LogbookTsFunc
   198  	StringerLocation = runner.LocOrig
   199  	startf.Version = runner.XformVersion
   200  	runner.ContextDone()
   201  	runner.RepoRoot.Delete()
   202  }
   203  
   204  // MakeTmpDir returns a temporary directory that runner will delete when done
   205  func (runner *TestRunner) MakeTmpDir(t *testing.T, pattern string) string {
   206  	if runner.TmpDir != "" {
   207  		t.Fatal("can only make one tmpDir at a time")
   208  	}
   209  	tmpDir, err := ioutil.TempDir("", pattern)
   210  	if err != nil {
   211  		t.Fatal(err)
   212  	}
   213  	runner.TmpDir = tmpDir
   214  	return tmpDir
   215  }
   216  
   217  // TODO(dustmop): Look into using options instead of multiple exec functions.
   218  
   219  // ExecCommand executes the given command string
   220  func (runner *TestRunner) ExecCommand(cmdText string) error {
   221  	var shutdown func() <-chan error
   222  	runner.CmdR, shutdown = runner.CreateCommandRunner(runner.Context)
   223  	if err := executeCommand(runner.CmdR, cmdText); err != nil {
   224  		timedShutdown(fmt.Sprintf("ExecCommand: %q\n", cmdText), shutdown)
   225  		return err
   226  	}
   227  
   228  	return timedShutdown(fmt.Sprintf("ExecCommand: %q\n", cmdText), shutdown)
   229  }
   230  
   231  // ExecCommandWithStdin executes the given command string with the string as stdin content
   232  func (runner *TestRunner) ExecCommandWithStdin(ctx context.Context, cmdText, stdinText string) error {
   233  	setNoColor(true)
   234  	runner.Streams.In = strings.NewReader(stdinText)
   235  	ctors := Constructors{
   236  		CryptoGenerator: runner.RepoRoot.TestCrypto,
   237  		InitIPFS:        repotest.InitIPFSRepo,
   238  	}
   239  	cmd, shutdown := NewQriCommand(ctx, runner.RepoPath, ctors, runner.Streams)
   240  	cmd.SetOutput(runner.OutStream)
   241  	runner.CmdR = cmd
   242  	if err := executeCommand(runner.CmdR, cmdText); err != nil {
   243  		return err
   244  	}
   245  
   246  	return timedShutdown(fmt.Sprintf("ExecCommandWithStdin: %q\n", cmdText), shutdown)
   247  }
   248  
   249  // ExecCommandCombinedOutErr executes the command with a combined stdout and stderr stream
   250  func (runner *TestRunner) ExecCommandCombinedOutErr(cmdText string) error {
   251  	ctx, cancel := context.WithCancel(runner.Context)
   252  	var shutdown func() <-chan error
   253  	runner.CmdR, shutdown = runner.CreateCommandRunnerCombinedOutErr(ctx)
   254  	if err := executeCommand(runner.CmdR, cmdText); err != nil {
   255  		shutDownErr := <-shutdown()
   256  		if shutDownErr != nil {
   257  			log.Errorf("error shutting down %q: %q", cmdText, shutDownErr)
   258  		}
   259  		cancel()
   260  		return err
   261  	}
   262  
   263  	err := timedShutdown(fmt.Sprintf("ExecCommandCombinedOutErr: %q\n", cmdText), shutdown)
   264  	cancel()
   265  	return err
   266  }
   267  
   268  func timedShutdown(cmd string, shutdown func() <-chan error) error {
   269  	waitForDone := make(chan error)
   270  	go func() {
   271  		select {
   272  		case <-time.NewTimer(time.Second * 3).C:
   273  			waitForDone <- fmt.Errorf("%s shutdown didn't send on 'done' channel within 3 seconds of context cancellation", cmd)
   274  		case err := <-shutdown():
   275  			if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
   276  				err = nil
   277  			}
   278  			waitForDone <- err
   279  		}
   280  	}()
   281  	return <-waitForDone
   282  }
   283  
   284  func shutdownRepoGraceful(cancel context.CancelFunc, r repo.Repo) error {
   285  	var (
   286  		wg  sync.WaitGroup
   287  		err error
   288  	)
   289  	wg.Add(1)
   290  
   291  	go func() {
   292  		<-r.Done()
   293  		err = r.DoneErr()
   294  		wg.Done()
   295  	}()
   296  	cancel()
   297  	wg.Wait()
   298  	return err
   299  }
   300  
   301  // ExecCommandWithContext executes the given command with a context
   302  func (runner *TestRunner) ExecCommandWithContext(ctx context.Context, cmdText string) error {
   303  	var shutdown func() <-chan error
   304  	runner.CmdR, shutdown = runner.CreateCommandRunner(ctx)
   305  	if err := executeCommand(runner.CmdR, cmdText); err != nil {
   306  		return err
   307  	}
   308  
   309  	return timedShutdown(fmt.Sprintf("ExecCommandWithContext: %q\n", cmdText), shutdown)
   310  }
   311  
   312  func (runner *TestRunner) MustExecuteQuotedCommand(t *testing.T, quotedCmdText string) string {
   313  	var shutdown func() <-chan error
   314  	runner.CmdR, shutdown = runner.CreateCommandRunner(runner.Context)
   315  
   316  	if err := executeQuotedCommand(runner.CmdR, quotedCmdText); err != nil {
   317  		_, callerFile, callerLine, ok := runtime.Caller(1)
   318  		if !ok {
   319  			t.Fatal(err)
   320  		} else {
   321  			t.Fatalf("%s:%d: %s", callerFile, callerLine, err)
   322  		}
   323  	}
   324  	if err := timedShutdown(fmt.Sprintf("MustExecuteQuotedCommand: %q\n", quotedCmdText), shutdown); err != nil {
   325  		t.Error(err)
   326  	}
   327  	return runner.GetCommandOutput()
   328  }
   329  
   330  // CreateCommandRunner returns a cobra runable command.
   331  func (runner *TestRunner) CreateCommandRunner(ctx context.Context) (*cobra.Command, func() <-chan error) {
   332  	return runner.newCommandRunner(ctx, false)
   333  }
   334  
   335  // CreateCommandRunnerCombinedOutErr returns a cobra command that combines stdout and stderr
   336  func (runner *TestRunner) CreateCommandRunnerCombinedOutErr(ctx context.Context) (*cobra.Command, func() <-chan error) {
   337  	cmd, shutdown := runner.newCommandRunner(ctx, true)
   338  	return cmd, shutdown
   339  }
   340  
   341  func (runner *TestRunner) newCommandRunner(ctx context.Context, combineOutErr bool) (*cobra.Command, func() <-chan error) {
   342  	runner.IOReset()
   343  	streams := runner.Streams
   344  	if combineOutErr {
   345  		streams = ioes.NewIOStreams(runner.InStream, runner.OutStream, runner.OutStream)
   346  	}
   347  	var opts []lib.Option
   348  	if runner.RepoRoot.UseMockRemoteClient {
   349  		opts = append(opts, lib.OptRemoteClientConstructor(remotemock.NewClient))
   350  	}
   351  	ctors := Constructors{
   352  		CryptoGenerator: runner.RepoRoot.TestCrypto,
   353  		InitIPFS:        repotest.InitIPFSRepo,
   354  	}
   355  	cmd, shutdown := NewQriCommand(ctx, runner.RepoPath, ctors, streams, opts...)
   356  	cmd.SetOutput(runner.OutStream)
   357  	return cmd, shutdown
   358  }
   359  
   360  // Username returns the test username from the config's profile
   361  func (runner *TestRunner) Username() string {
   362  	return runner.RepoRoot.GetConfig().Profile.Peername
   363  }
   364  
   365  // IOReset resets the io streams
   366  func (runner *TestRunner) IOReset() {
   367  	runner.InStream.Reset()
   368  	runner.OutStream.Reset()
   369  	runner.ErrStream.Reset()
   370  }
   371  
   372  // FileExists returns whether the file exists
   373  func (runner *TestRunner) FileExists(file string) bool {
   374  	if _, err := os.Stat(file); os.IsNotExist(err) {
   375  		return false
   376  	}
   377  	return true
   378  }
   379  
   380  // LookupVersionInfo returns a versionInfo for the ref, or nil if not found
   381  func (runner *TestRunner) LookupVersionInfo(t *testing.T, refStr string) *dsref.VersionInfo {
   382  	ctx, cancel := context.WithCancel(context.Background())
   383  	defer cancel()
   384  	r, err := runner.RepoRoot.Repo(ctx)
   385  	if err != nil {
   386  		t.Fatal(err)
   387  	}
   388  
   389  	dr, err := dsref.Parse(refStr)
   390  	if err != nil {
   391  		t.Fatal(err)
   392  	}
   393  
   394  	// TODO(b5): me shortcut is handled by an instance, it'd be nice we had a
   395  	// function in the repo package that deduplicated this in both places
   396  	if dr.Username == "me" {
   397  		dr.Username = r.Profiles().Owner(ctx).Peername
   398  	}
   399  
   400  	if _, err := r.ResolveRef(ctx, &dr); err != nil {
   401  		return nil
   402  	}
   403  
   404  	// TODO(b5): TestUnlinkNoHistory relies on a nil-return versionInfo, so
   405  	// we need to ignore this error for now
   406  	vi, _ := repo.GetVersionInfoShim(r, dr)
   407  	// if err != nil {
   408  	// 	t.Fatal(err)
   409  	// }
   410  
   411  	// TODO(b5) - hand-creating a shutdown function to satisfy "timedshutdown",
   412  	// which works with an instance in most other cases
   413  	shutdown := func() <-chan error {
   414  		finished := make(chan error)
   415  		go func() {
   416  			<-r.Done()
   417  			finished <- r.DoneErr()
   418  		}()
   419  
   420  		cancel()
   421  		return finished
   422  	}
   423  
   424  	err = timedShutdown("LookupVersionInfo", shutdown)
   425  	if err != nil {
   426  		t.Fatal(err)
   427  	}
   428  
   429  	return vi
   430  }
   431  
   432  // GetPathForDataset fetches a path for dataset index
   433  func (runner *TestRunner) GetPathForDataset(t *testing.T, index int) string {
   434  	path, err := runner.RepoRoot.GetPathForDataset(index)
   435  	if err != nil {
   436  		t.Fatal(err)
   437  	}
   438  	return path
   439  }
   440  
   441  // ReadBodyFromIPFS reads body data from an IPFS repo by path string,
   442  func (runner *TestRunner) ReadBodyFromIPFS(t *testing.T, path string) string {
   443  	body, err := runner.RepoRoot.ReadBodyFromIPFS(path)
   444  	if err != nil {
   445  		t.Fatal(err)
   446  	}
   447  	return body
   448  }
   449  
   450  // DatasetMarshalJSON reads the dataset head and marshals it as json
   451  func (runner *TestRunner) DatasetMarshalJSON(t *testing.T, ref string) string {
   452  	data, err := runner.RepoRoot.DatasetMarshalJSON(ref)
   453  	if err != nil {
   454  		t.Fatal(err)
   455  	}
   456  	return data
   457  }
   458  
   459  // MustLoadDataset loads the dataset or fails
   460  func (runner *TestRunner) MustLoadDataset(t *testing.T, ref string) *dataset.Dataset {
   461  	ds, err := runner.RepoRoot.LoadDataset(ref)
   462  	if err != nil {
   463  		t.Fatal(err)
   464  	}
   465  	return ds
   466  }
   467  
   468  // MustExec runs a command, returning standard output, failing the test if there's an error
   469  func (runner *TestRunner) MustExec(t *testing.T, cmdText string) string {
   470  	if err := runner.ExecCommand(cmdText); err != nil {
   471  		_, callerFile, callerLine, ok := runtime.Caller(1)
   472  		if !ok {
   473  			t.Fatal(err)
   474  		} else {
   475  			t.Fatalf("executing command: %q\n%s:%d: %s", cmdText, callerFile, callerLine, err)
   476  		}
   477  	}
   478  	return runner.GetCommandOutput()
   479  }
   480  
   481  // MustExec runs a command, returning combined standard output and standard err
   482  func (runner *TestRunner) MustExecCombinedOutErr(t *testing.T, cmdText string) string {
   483  	t.Helper()
   484  	ctx, cancel := context.WithCancel(runner.Context)
   485  	var shutdown func() <-chan error
   486  	runner.CmdR, shutdown = runner.CreateCommandRunnerCombinedOutErr(ctx)
   487  	err := executeCommand(runner.CmdR, cmdText)
   488  	if err != nil {
   489  		cancel()
   490  		_, callerFile, callerLine, ok := runtime.Caller(1)
   491  		if !ok {
   492  			t.Fatal(err)
   493  		} else {
   494  			t.Fatalf("%s:%d: %s", callerFile, callerLine, err)
   495  		}
   496  	}
   497  
   498  	err = timedShutdown("MustExecCombinedOutErr", shutdown)
   499  	cancel()
   500  	if err != nil {
   501  		t.Fatal(err)
   502  	}
   503  	return runner.GetCommandOutput()
   504  }
   505  
   506  // MustWriteFile writes to a file, failing the test if there's an error
   507  func (runner *TestRunner) MustWriteFile(t *testing.T, filename, contents string) {
   508  	if err := ioutil.WriteFile(filename, []byte(contents), os.FileMode(0644)); err != nil {
   509  		t.Fatal(err)
   510  	}
   511  }
   512  
   513  // MustReadFile reads a file, failing the test if there's an error
   514  func (runner *TestRunner) MustReadFile(t *testing.T, filename string) string {
   515  	bytes, err := ioutil.ReadFile(filename)
   516  	if err != nil {
   517  		t.Fatal(err)
   518  	}
   519  	return string(bytes)
   520  }
   521  
   522  // Must asserts that the function result passed to it is not an error
   523  func (runner *TestRunner) Must(t *testing.T, err error) {
   524  	if err != nil {
   525  		_, callerFile, callerLine, ok := runtime.Caller(1)
   526  		if !ok {
   527  			t.Fatal(err)
   528  		} else {
   529  			t.Fatalf("%s:%d: %s", callerFile, callerLine, err)
   530  		}
   531  	}
   532  }
   533  
   534  // GetCommandOutput returns the standard output from the previously executed
   535  // command
   536  func (runner *TestRunner) GetCommandOutput() string {
   537  	outputText := ""
   538  	if buffer, ok := runner.Streams.Out.(*bytes.Buffer); ok {
   539  		outputText = runner.niceifyTempDirs(buffer.String())
   540  	}
   541  	return outputText
   542  }
   543  
   544  // GetCommandErrOutput fetches the stderr value from the previously executed
   545  // command
   546  func (runner *TestRunner) GetCommandErrOutput() string {
   547  	errOutText := ""
   548  	if buffer, ok := runner.Streams.ErrOut.(*bytes.Buffer); ok {
   549  		errOutText = runner.niceifyTempDirs(buffer.String())
   550  	}
   551  	return errOutText
   552  }
   553  
   554  func (runner *TestRunner) niceifyTempDirs(text string) string {
   555  	text = strings.Replace(text, runner.RepoRoot.RootPath, "/root", -1)
   556  	realRoot, err := filepath.EvalSymlinks(runner.RepoRoot.RootPath)
   557  	if err == nil {
   558  		text = strings.Replace(text, realRoot, "/root", -1)
   559  	}
   560  	return text
   561  }
   562  
   563  func executeCommand(root *cobra.Command, cmd string) error {
   564  	// fmt.Printf("exec command: %s\n", cmd)
   565  	cmd = strings.TrimPrefix(cmd, "qri ")
   566  	args := strings.Split(cmd, " ")
   567  	return executeCommandC(root, args...)
   568  }
   569  
   570  func executeQuotedCommand(root *cobra.Command, cmd string) error {
   571  	cmd = strings.TrimPrefix(cmd, "qri ")
   572  
   573  	var s scanner.Scanner
   574  	s.Init(strings.NewReader(cmd))
   575  	var args []string
   576  	for tok := s.Scan(); tok != scanner.EOF; tok = s.Scan() {
   577  		arg := s.TokenText()
   578  		if unquoted, err := strconv.Unquote(arg); err == nil {
   579  			arg = unquoted
   580  		}
   581  
   582  		args = append(args, arg)
   583  	}
   584  
   585  	return executeCommandC(root, args...)
   586  }
   587  
   588  func executeCommandC(root *cobra.Command, args ...string) (err error) {
   589  	root.SetArgs(args)
   590  	_, err = root.ExecuteC()
   591  	return err
   592  }
   593  
   594  // AddDatasetToRefstore adds a dataset to the test runner's refstore. It ignores the upper-levels
   595  // of our stack, namely cmd/ and lib/, which means it can be used to add a dataset with a name
   596  // that is using upper-case characters.
   597  func (runner *TestRunner) AddDatasetToRefstore(t *testing.T, refStr string, ds *dataset.Dataset) {
   598  	ctx, cancel := context.WithCancel(context.Background())
   599  	defer cancel()
   600  
   601  	ref, err := dsref.ParseHumanFriendly(refStr)
   602  	if err != nil && err != dsref.ErrBadCaseName {
   603  		t.Fatal(err)
   604  	}
   605  
   606  	ds.Peername = ref.Username
   607  	ds.Name = ref.Name
   608  
   609  	r, err := runner.RepoRoot.Repo(ctx)
   610  	if err != nil {
   611  		t.Fatal(err)
   612  	}
   613  
   614  	// WARNING: here we're assuming the provided ref matches the owner peername
   615  	author := r.Logbook().Owner()
   616  
   617  	// Reserve the name in the logbook, which provides an initID
   618  	initID, err := r.Logbook().WriteDatasetInit(ctx, author, ds.Name)
   619  	if err != nil {
   620  		t.Fatal(err)
   621  	}
   622  
   623  	// No existing commit
   624  	emptyHeadRef := ""
   625  
   626  	if _, err = base.SaveDataset(ctx, r, r.Filesystem().DefaultWriteFS(), author, initID, emptyHeadRef, ds, nil, base.SaveSwitches{}); err != nil {
   627  		t.Fatal(err)
   628  	}
   629  
   630  	cancel()
   631  	<-r.Done()
   632  }