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 "github.com/git-lfs/git-lfs/tasklog"
17 "github.com/git-lfs/git-lfs/test"
18 "github.com/git-lfs/git-lfs/tq"
19 "github.com/spf13/cobra"
22 type TestObject struct {
27 type ServerTest struct {
29 F func(m *tq.Manifest, oidsExist, oidsMissing []TestObject) error
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",
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)")
55 if len(args) != 0 && len(args) != 2 {
56 exit("Must supply either no file arguments or both the exists AND missing file")
59 if len(args) != 0 && len(savePrefix) > 0 {
60 exit("Cannot combine input files and --save option")
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 := test.NewRepo(&callback)
69 // Force loading of config before we alter it
74 manifest, err := buildManifest(repo)
76 exit("error building tq.Manifest: " + err.Error())
79 var oidsExist, oidsMissing []TestObject
81 fmt.Printf("Reading test data from files (no server content changes)\n")
82 oidsExist = readTestOids(args[0])
83 oidsMissing = readTestOids(args[1])
85 fmt.Printf("Creating test data (will upload to server)\n")
87 oidsExist, oidsMissing, err = buildTestData(repo, manifest)
89 exit("Failed to set up test data, aborting")
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)
101 ok := runTests(manifest, oidsExist, oidsMissing)
103 exit("One or more tests failed, see above")
105 fmt.Println("All tests passed")
108 func readTestOids(filename string) []TestObject {
109 f, err := os.OpenFile(filename, os.O_RDONLY, 0644)
111 exit("Error opening file %s", filename)
116 rdr := bufio.NewReader(f)
117 line, err := rdr.ReadString('\n')
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})
125 line, err = rdr.ReadString('\n')
131 type testDataCallback struct{}
133 func (*testDataCallback) Fatalf(format string, args ...interface{}) {
134 exit(format, args...)
136 func (*testDataCallback) Errorf(format string, args ...interface{}) {
137 fmt.Printf(format, args...)
140 func buildManifest(r *test.Repo) (*tq.Manifest, error) {
141 // Configure the endpoint manually
142 finder := lfsapi.NewEndpointFinder(r)
144 var endp lfsapi.Endpoint
145 if len(cloneUrl) > 0 {
146 endp = finder.NewEndpointFromCloneURL(cloneUrl)
148 endp = finder.NewEndpoint(apiUrl)
151 apiClient, err := lfsapi.NewClient(r)
152 apiClient.Endpoints = &constantEndpoint{
154 EndpointFinder: apiClient.Endpoints,
159 return tq.NewManifest(r.Filesystem(), apiClient, "", ""), nil
162 type constantEndpoint struct {
165 lfsapi.EndpointFinder
168 func (c *constantEndpoint) NewEndpointFromCloneURL(rawurl string) lfsapi.Endpoint { return c.e }
170 func (c *constantEndpoint) NewEndpoint(rawurl string) lfsapi.Endpoint { return c.e }
172 func (c *constantEndpoint) Endpoint(operation, remote string) lfsapi.Endpoint { return c.e }
174 func (c *constantEndpoint) RemoteEndpoint(operation, remote string) lfsapi.Endpoint { return c.e }
176 func buildTestData(repo *test.Repo, manifest *tq.Manifest) (oidsExist, oidsMissing []TestObject, err error) {
178 oidsExist = make([]TestObject, 0, oidCount)
179 oidsMissing = make([]TestObject, 0, oidCount)
182 logger := tasklog.NewLogger(os.Stdout)
183 meter := tq.NewMeter()
184 meter.Logger = meter.LoggerFromEnv(repo.OSEnv())
185 logger.Enqueue(meter)
186 commit := test.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, &test.FileInput{Filename: filename, Size: sz})
193 outputs := repo.AddCommits([]*test.CommitInput{&commit})
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})
200 t, err := uploadTransfer(repo.Filesystem(), f.Oid, "Test file")
204 uploadQueue.Add(t.Name, t.Path, t.Oid, t.Size)
208 for _, err := range uploadQueue.Errors() {
209 if errors.IsFatalError(err) {
210 exit("Fatal error setting up test data: %s", err)
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})
224 return oidsExist, oidsMissing, nil
227 func saveTestOids(filename string, objs []TestObject) {
228 f, err := os.OpenFile(filename, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
230 exit("Error opening file %s", filename)
234 for _, o := range objs {
235 f.WriteString(fmt.Sprintf("%s %d\n", o.Oid, o.Size))
240 func runTests(manifest *tq.Manifest, oidsExist, oidsMissing []TestObject) bool {
242 fmt.Printf("Running %d tests...\n", len(tests))
243 for _, t := range tests {
244 err := runTest(t, manifest, oidsExist, oidsMissing)
252 func runTest(t ServerTest, manifest *tq.Manifest, oidsExist, oidsMissing []TestObject) error {
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)))
260 fmt.Printf("%s...\r", line)
262 err := t.F(manifest, oidsExist, oidsMissing)
264 fmt.Printf("%s FAILED\n", line)
265 fmt.Println(err.Error())
267 fmt.Printf("%s OK\n", line)
272 // Exit prints a formatted message and exits.
273 func exit(format string, args ...interface{}) {
274 fmt.Fprintf(os.Stderr, format, args...)
278 func addTest(name string, f func(manifest *tq.Manifest, oidsExist, oidsMissing []TestObject) error) {
279 tests = append(tests, ServerTest{Name: name, F: f})
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})
288 bres, err := tq.Batch(manifest, dir, "origin", nil, apiobjs)
292 return bres.Objects, nil
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
300 count := len(slice1) + len(slice2)
301 ret := make([]TestObject, 0, count)
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)
311 for i := rand.Intn(3) + 1; slice2Idx < len(slice2) && i > 0; i-- {
312 obj := slice2[slice2Idx]
313 ret = append(ret, obj)
321 func uploadTransfer(fs *fs.Filesystem, oid, filename string) (*tq.Transfer, error) {
322 localMediaPath, err := fs.ObjectPath(oid)
324 return nil, errors.Wrapf(err, "Error uploading file %s (%s)", filename, oid)
327 fi, err := os.Stat(localMediaPath)
329 return nil, errors.Wrapf(err, "Error uploading file %s (%s)", filename, oid)
334 Path: localMediaPath,
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")