github.com/git-lfs/git-lfs@v2.5.2+incompatible/t/git-lfs-test-server-api/main.go (about)

     1  package main
     2  
     3  import (
     4  	"bufio"
     5  	"crypto/sha256"
     6  	"encoding/hex"
     7  	"fmt"
     8  	"math/rand"
     9  	"os"
    10  	"strconv"
    11  	"strings"
    12  
    13  	"github.com/git-lfs/git-lfs/errors"
    14  	"github.com/git-lfs/git-lfs/fs"
    15  	"github.com/git-lfs/git-lfs/lfsapi"
    16  	t "github.com/git-lfs/git-lfs/t/cmd/util"
    17  	"github.com/git-lfs/git-lfs/tasklog"
    18  	"github.com/git-lfs/git-lfs/tq"
    19  	"github.com/spf13/cobra"
    20  )
    21  
    22  type TestObject struct {
    23  	Oid  string
    24  	Size int64
    25  }
    26  
    27  type ServerTest struct {
    28  	Name string
    29  	F    func(m *tq.Manifest, oidsExist, oidsMissing []TestObject) error
    30  }
    31  
    32  var (
    33  	RootCmd = &cobra.Command{
    34  		Use:   "git-lfs-test-server-api [--url=<apiurl> | --clone=<cloneurl>] [<oid-exists-file> <oid-missing-file>]",
    35  		Short: "Test a Git LFS API server for compliance",
    36  		Run:   testServerApi,
    37  	}
    38  	apiUrl     string
    39  	cloneUrl   string
    40  	savePrefix string
    41  
    42  	tests []ServerTest
    43  )
    44  
    45  func main() {
    46  	RootCmd.Execute()
    47  }
    48  
    49  func testServerApi(cmd *cobra.Command, args []string) {
    50  	if (len(apiUrl) == 0 && len(cloneUrl) == 0) ||
    51  		(len(apiUrl) != 0 && len(cloneUrl) != 0) {
    52  		exit("Must supply either --url or --clone (and not both)")
    53  	}
    54  
    55  	if len(args) != 0 && len(args) != 2 {
    56  		exit("Must supply either no file arguments or both the exists AND missing file")
    57  	}
    58  
    59  	if len(args) != 0 && len(savePrefix) > 0 {
    60  		exit("Cannot combine input files and --save option")
    61  	}
    62  
    63  	// Build test data for existing files & upload
    64  	// Use test repo for this to simplify the process of making sure data matches oid
    65  	// We're not performing a real test at this point (although an upload fail will break it)
    66  	var callback testDataCallback
    67  	repo := t.NewRepo(&callback)
    68  
    69  	// Force loading of config before we alter it
    70  	repo.GitEnv().All()
    71  	repo.Pushd()
    72  	defer repo.Popd()
    73  
    74  	manifest, err := buildManifest(repo)
    75  	if err != nil {
    76  		exit("error building tq.Manifest: " + err.Error())
    77  	}
    78  
    79  	var oidsExist, oidsMissing []TestObject
    80  	if len(args) >= 2 {
    81  		fmt.Printf("Reading test data from files (no server content changes)\n")
    82  		oidsExist = readTestOids(args[0])
    83  		oidsMissing = readTestOids(args[1])
    84  	} else {
    85  		fmt.Printf("Creating test data (will upload to server)\n")
    86  		var err error
    87  		oidsExist, oidsMissing, err = buildTestData(repo, manifest)
    88  		if err != nil {
    89  			exit("Failed to set up test data, aborting")
    90  		}
    91  		if len(savePrefix) > 0 {
    92  			existFile := savePrefix + "_exists"
    93  			missingFile := savePrefix + "_missing"
    94  			saveTestOids(existFile, oidsExist)
    95  			saveTestOids(missingFile, oidsMissing)
    96  			fmt.Printf("Wrote test to %s, %s for future use\n", existFile, missingFile)
    97  		}
    98  
    99  	}
   100  
   101  	ok := runTests(manifest, oidsExist, oidsMissing)
   102  	if !ok {
   103  		exit("One or more tests failed, see above")
   104  	}
   105  	fmt.Println("All tests passed")
   106  }
   107  
   108  func readTestOids(filename string) []TestObject {
   109  	f, err := os.OpenFile(filename, os.O_RDONLY, 0644)
   110  	if err != nil {
   111  		exit("Error opening file %s", filename)
   112  	}
   113  	defer f.Close()
   114  
   115  	var ret []TestObject
   116  	rdr := bufio.NewReader(f)
   117  	line, err := rdr.ReadString('\n')
   118  	for err == nil {
   119  		fields := strings.Fields(strings.TrimSpace(line))
   120  		if len(fields) == 2 {
   121  			sz, _ := strconv.ParseInt(fields[1], 10, 64)
   122  			ret = append(ret, TestObject{Oid: fields[0], Size: sz})
   123  		}
   124  
   125  		line, err = rdr.ReadString('\n')
   126  	}
   127  
   128  	return ret
   129  }
   130  
   131  type testDataCallback struct{}
   132  
   133  func (*testDataCallback) Fatalf(format string, args ...interface{}) {
   134  	exit(format, args...)
   135  }
   136  func (*testDataCallback) Errorf(format string, args ...interface{}) {
   137  	fmt.Printf(format, args...)
   138  }
   139  
   140  func buildManifest(r *t.Repo) (*tq.Manifest, error) {
   141  	// Configure the endpoint manually
   142  	finder := lfsapi.NewEndpointFinder(r)
   143  
   144  	var endp lfsapi.Endpoint
   145  	if len(cloneUrl) > 0 {
   146  		endp = finder.NewEndpointFromCloneURL(cloneUrl)
   147  	} else {
   148  		endp = finder.NewEndpoint(apiUrl)
   149  	}
   150  
   151  	apiClient, err := lfsapi.NewClient(r)
   152  	apiClient.Endpoints = &constantEndpoint{
   153  		e:              endp,
   154  		EndpointFinder: apiClient.Endpoints,
   155  	}
   156  	if err != nil {
   157  		return nil, err
   158  	}
   159  	return tq.NewManifest(r.Filesystem(), apiClient, "", ""), nil
   160  }
   161  
   162  type constantEndpoint struct {
   163  	e lfsapi.Endpoint
   164  
   165  	lfsapi.EndpointFinder
   166  }
   167  
   168  func (c *constantEndpoint) NewEndpointFromCloneURL(rawurl string) lfsapi.Endpoint { return c.e }
   169  
   170  func (c *constantEndpoint) NewEndpoint(rawurl string) lfsapi.Endpoint { return c.e }
   171  
   172  func (c *constantEndpoint) Endpoint(operation, remote string) lfsapi.Endpoint { return c.e }
   173  
   174  func (c *constantEndpoint) RemoteEndpoint(operation, remote string) lfsapi.Endpoint { return c.e }
   175  
   176  func buildTestData(repo *t.Repo, manifest *tq.Manifest) (oidsExist, oidsMissing []TestObject, err error) {
   177  	const oidCount = 50
   178  	oidsExist = make([]TestObject, 0, oidCount)
   179  	oidsMissing = make([]TestObject, 0, oidCount)
   180  
   181  	// just one commit
   182  	logger := tasklog.NewLogger(os.Stdout)
   183  	meter := tq.NewMeter()
   184  	meter.Logger = meter.LoggerFromEnv(repo.OSEnv())
   185  	logger.Enqueue(meter)
   186  	commit := t.CommitInput{CommitterName: "A N Other", CommitterEmail: "noone@somewhere.com"}
   187  	for i := 0; i < oidCount; i++ {
   188  		filename := fmt.Sprintf("file%d.dat", i)
   189  		sz := int64(rand.Intn(200)) + 50
   190  		commit.Files = append(commit.Files, &t.FileInput{Filename: filename, Size: sz})
   191  		meter.Add(sz)
   192  	}
   193  	outputs := repo.AddCommits([]*t.CommitInput{&commit})
   194  
   195  	// now upload
   196  	uploadQueue := tq.NewTransferQueue(tq.Upload, manifest, "origin", tq.WithProgress(meter))
   197  	for _, f := range outputs[0].Files {
   198  		oidsExist = append(oidsExist, TestObject{Oid: f.Oid, Size: f.Size})
   199  
   200  		t, err := uploadTransfer(repo.Filesystem(), f.Oid, "Test file")
   201  		if err != nil {
   202  			return nil, nil, err
   203  		}
   204  		uploadQueue.Add(t.Name, t.Path, t.Oid, t.Size)
   205  	}
   206  	uploadQueue.Wait()
   207  
   208  	for _, err := range uploadQueue.Errors() {
   209  		if errors.IsFatalError(err) {
   210  			exit("Fatal error setting up test data: %s", err)
   211  		}
   212  	}
   213  
   214  	// Generate SHAs for missing files, random but repeatable
   215  	// No actual file content needed for these
   216  	rand.Seed(int64(oidCount))
   217  	runningSha := sha256.New()
   218  	for i := 0; i < oidCount; i++ {
   219  		runningSha.Write([]byte{byte(rand.Intn(256))})
   220  		oid := hex.EncodeToString(runningSha.Sum(nil))
   221  		sz := int64(rand.Intn(200)) + 50
   222  		oidsMissing = append(oidsMissing, TestObject{Oid: oid, Size: sz})
   223  	}
   224  	return oidsExist, oidsMissing, nil
   225  }
   226  
   227  func saveTestOids(filename string, objs []TestObject) {
   228  	f, err := os.OpenFile(filename, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
   229  	if err != nil {
   230  		exit("Error opening file %s", filename)
   231  	}
   232  	defer f.Close()
   233  
   234  	for _, o := range objs {
   235  		f.WriteString(fmt.Sprintf("%s %d\n", o.Oid, o.Size))
   236  	}
   237  
   238  }
   239  
   240  func runTests(manifest *tq.Manifest, oidsExist, oidsMissing []TestObject) bool {
   241  	ok := true
   242  	fmt.Printf("Running %d tests...\n", len(tests))
   243  	for _, t := range tests {
   244  		err := runTest(t, manifest, oidsExist, oidsMissing)
   245  		if err != nil {
   246  			ok = false
   247  		}
   248  	}
   249  	return ok
   250  }
   251  
   252  func runTest(t ServerTest, manifest *tq.Manifest, oidsExist, oidsMissing []TestObject) error {
   253  	const linelen = 70
   254  	line := t.Name
   255  	if len(line) > linelen {
   256  		line = line[:linelen]
   257  	} else if len(line) < linelen {
   258  		line = fmt.Sprintf("%s%s", line, strings.Repeat(" ", linelen-len(line)))
   259  	}
   260  	fmt.Printf("%s...\r", line)
   261  
   262  	err := t.F(manifest, oidsExist, oidsMissing)
   263  	if err != nil {
   264  		fmt.Printf("%s FAILED\n", line)
   265  		fmt.Println(err.Error())
   266  	} else {
   267  		fmt.Printf("%s OK\n", line)
   268  	}
   269  	return err
   270  }
   271  
   272  // Exit prints a formatted message and exits.
   273  func exit(format string, args ...interface{}) {
   274  	fmt.Fprintf(os.Stderr, format, args...)
   275  	os.Exit(2)
   276  }
   277  
   278  func addTest(name string, f func(manifest *tq.Manifest, oidsExist, oidsMissing []TestObject) error) {
   279  	tests = append(tests, ServerTest{Name: name, F: f})
   280  }
   281  
   282  func callBatchApi(manifest *tq.Manifest, dir tq.Direction, objs []TestObject) ([]*tq.Transfer, error) {
   283  	apiobjs := make([]*tq.Transfer, 0, len(objs))
   284  	for _, o := range objs {
   285  		apiobjs = append(apiobjs, &tq.Transfer{Oid: o.Oid, Size: o.Size})
   286  	}
   287  
   288  	bres, err := tq.Batch(manifest, dir, "origin", nil, apiobjs)
   289  	if err != nil {
   290  		return nil, err
   291  	}
   292  	return bres.Objects, nil
   293  }
   294  
   295  // Combine 2 slices into one by "randomly" interleaving
   296  // Not actually random, same sequence each time so repeatable
   297  func interleaveTestData(slice1, slice2 []TestObject) []TestObject {
   298  	// Predictable sequence, mixin existing & missing semi-randomly
   299  	rand.Seed(21)
   300  	count := len(slice1) + len(slice2)
   301  	ret := make([]TestObject, 0, count)
   302  	slice1Idx := 0
   303  	slice2Idx := 0
   304  	for left := count; left > 0; {
   305  		for i := rand.Intn(3) + 1; slice1Idx < len(slice1) && i > 0; i-- {
   306  			obj := slice1[slice1Idx]
   307  			ret = append(ret, obj)
   308  			slice1Idx++
   309  			left--
   310  		}
   311  		for i := rand.Intn(3) + 1; slice2Idx < len(slice2) && i > 0; i-- {
   312  			obj := slice2[slice2Idx]
   313  			ret = append(ret, obj)
   314  			slice2Idx++
   315  			left--
   316  		}
   317  	}
   318  	return ret
   319  }
   320  
   321  func uploadTransfer(fs *fs.Filesystem, oid, filename string) (*tq.Transfer, error) {
   322  	localMediaPath, err := fs.ObjectPath(oid)
   323  	if err != nil {
   324  		return nil, errors.Wrapf(err, "Error uploading file %s (%s)", filename, oid)
   325  	}
   326  
   327  	fi, err := os.Stat(localMediaPath)
   328  	if err != nil {
   329  		return nil, errors.Wrapf(err, "Error uploading file %s (%s)", filename, oid)
   330  	}
   331  
   332  	return &tq.Transfer{
   333  		Name: filename,
   334  		Path: localMediaPath,
   335  		Oid:  oid,
   336  		Size: fi.Size(),
   337  	}, nil
   338  }
   339  
   340  func init() {
   341  	RootCmd.Flags().StringVarP(&apiUrl, "url", "u", "", "URL of the API (must supply this or --clone)")
   342  	RootCmd.Flags().StringVarP(&cloneUrl, "clone", "c", "", "Clone URL from which to find API (must supply this or --url)")
   343  	RootCmd.Flags().StringVarP(&savePrefix, "save", "s", "", "Saves generated data to <prefix>_exists|missing for subsequent use")
   344  }