github.com/stffabi/git-lfs@v2.3.5-0.20180214015214-8eeaa8d88902+incompatible/test/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 "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 }