295536f63a8e9312dfeecb9db1b8828695a71139
[scm/test.git] / test / git-lfs-test-server-api / main.go
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         "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"
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 := test.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 *test.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 *test.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 := 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})
191                 meter.Add(sz)
192         }
193         outputs := repo.AddCommits([]*test.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 }