github.com/git-lfs/git-lfs@v2.5.2+incompatible/t/cmd/lfstest-count-tests.go (about) 1 package main 2 3 import ( 4 "context" 5 "fmt" 6 "io" 7 "io/ioutil" 8 "net/http" 9 "os" 10 "os/exec" 11 "path/filepath" 12 "runtime" 13 "strconv" 14 "strings" 15 "time" 16 ) 17 18 var ( 19 // countFile is the path to a file (relative to the $LFSTEST_DIR) who's 20 // contents is the number of actively-running integration tests. 21 countFile = "test_count" 22 // lockFile is the path to a file (relative to the $LFSTEST_DIR) who's 23 // presence indicates that another invocation of the lfstest-count-tests 24 // program is modifying the test_count. 25 lockFile = "test_count.lock" 26 27 // lockAcquireTimeout is the maximum amount of time that we will wait 28 // for lockFile to become available (and thus the amount of time that we 29 // will wait in order to acquire the lock). 30 lockAcquireTimeout = 5 * time.Second 31 32 // errCouldNotAcquire indicates that the program could not acquire the 33 // lock needed to modify the test_count. It is a fatal error. 34 errCouldNotAcquire = fmt.Errorf("could not acquire lock, dying") 35 // errNegativeCount indicates that the count in test_count was negative, 36 // which is unexpected and makes this script behave in an undefined 37 // fashion 38 errNegativeCount = fmt.Errorf("unexpected negative count") 39 ) 40 41 // countFn is a type signature that all functions who wish to modify the 42 // test_count must inhabit. 43 // 44 // The first and only formal parameter is the current number of running tests 45 // found in test_count after acquiring the lock. 46 // 47 // The returned tuple indicates (1) the new number that should be written to 48 // test_count, and (2) if there was an error in computing that value. If err is 49 // non-nil, the program will exit and test_count will not be updated. 50 type countFn func(int) (int, error) 51 52 func main() { 53 if len(os.Args) > 2 { 54 fmt.Fprintf(os.Stderr, 55 "usage: %s [increment|decrement]\n", os.Args[0]) 56 os.Exit(1) 57 } 58 59 ctx, cancel := context.WithTimeout( 60 context.Background(), lockAcquireTimeout) 61 defer cancel() 62 63 if err := acquire(ctx); err != nil { 64 fatal(err) 65 } 66 defer release() 67 68 if len(os.Args) == 1 { 69 // Calling with no arguments indicates that we simply want to 70 // read the contents of test_count. 71 callWithCount(func(n int) (int, error) { 72 fmt.Fprintf(os.Stdout, "%d\n", n) 73 return n, nil 74 }) 75 return 76 } 77 78 var err error 79 80 switch strings.ToLower(os.Args[1]) { 81 case "increment": 82 err = callWithCount(func(n int) (int, error) { 83 if n > 0 { 84 // If n>1, it is therefore true that a 85 // lfstest-gitserver invocation is already 86 // running. 87 // 88 // Hence, let's do nothing here other than 89 // increase the count. 90 return n + 1, nil 91 } 92 93 // The lfstest-gitserver invocation (see: below) does 94 // not itself create a gitserver.log in the appropriate 95 // directory. Thus, let's create it ourselves instead. 96 log, err := os.Create(fmt.Sprintf( 97 "%s/gitserver.log", os.Getenv("LFSTEST_DIR"))) 98 if err != nil { 99 return n, err 100 } 101 102 // The executable name depends on the X environment 103 // variable, which is set in script/cibuild. 104 var cmd *exec.Cmd 105 if runtime.GOOS == "windows" { 106 cmd = exec.Command("lfstest-gitserver.exe") 107 } else { 108 cmd = exec.Command("lfstest-gitserver") 109 } 110 111 // The following are ported from the old 112 // test/testhelpers.sh, and comprise the requisite 113 // environment variables needed to run 114 // lfstest-gitserver. 115 cmd.Env = append(os.Environ(), 116 fmt.Sprintf("LFSTEST_URL=%s", os.Getenv("LFSTEST_URL")), 117 fmt.Sprintf("LFSTEST_SSL_URL=%s", os.Getenv("LFSTEST_SSL_URL")), 118 fmt.Sprintf("LFSTEST_CLIENT_CERT_URL=%s", os.Getenv("LFSTEST_CLIENT_CERT_URL")), 119 fmt.Sprintf("LFSTEST_DIR=%s", os.Getenv("LFSTEST_DIR")), 120 fmt.Sprintf("LFSTEST_CERT=%s", os.Getenv("LFSTEST_CERT")), 121 fmt.Sprintf("LFSTEST_CLIENT_CERT=%s", os.Getenv("LFSTEST_CLIENT_CERT")), 122 fmt.Sprintf("LFSTEST_CLIENT_KEY=%s", os.Getenv("LFSTEST_CLIENT_KEY")), 123 ) 124 cmd.Stdout = log 125 126 // Start performs a fork/execve, hence we can abandon 127 // this process once it has started. 128 if err := cmd.Start(); err != nil { 129 return n, err 130 } 131 return 1, nil 132 }) 133 case "decrement": 134 err = callWithCount(func(n int) (int, error) { 135 if n > 1 { 136 // If there is at least two tests running, we 137 // need not shutdown a lfstest-gitserver 138 // instance. 139 return n - 1, nil 140 } 141 142 // Otherwise, we need to POST to /shutdown, which will 143 // cause the lfstest-gitserver to abort itself. 144 url, err := ioutil.ReadFile(os.Getenv("LFS_URL_FILE")) 145 if err == nil { 146 _, err = http.Post(string(url)+"/shutdown", 147 "application/text", 148 strings.NewReader(time.Now().String())) 149 } 150 151 return 0, nil 152 }) 153 } 154 155 if err != nil { 156 fatal(err) 157 } 158 } 159 160 var ( 161 // acquireTick is the constant time that one tick (i.e., one attempt at 162 // acquiring the lock) should last. 163 acquireTick = 10 * time.Millisecond 164 ) 165 166 // acquire acquires the lock file necessary to perform updates to test_count, 167 // and returns an error if that lock cannot be acquired. 168 func acquire(ctx context.Context) error { 169 if disabled() { 170 return nil 171 } 172 173 path, err := path(lockFile) 174 if err != nil { 175 return err 176 } 177 178 tick := time.NewTicker(acquireTick) 179 defer tick.Stop() 180 181 for { 182 select { 183 case <-tick.C: 184 // Try every tick of the above ticker before giving up 185 // and trying again. 186 _, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL, 0666) 187 if err == nil || !os.IsExist(err) { 188 return err 189 } 190 case <-ctx.Done(): 191 // If the context.Context above has reached its 192 // deadline, we must give up. 193 return errCouldNotAcquire 194 } 195 } 196 } 197 198 // release releases the lock file so that another process can take over, or 199 // returns an error. 200 func release() error { 201 if disabled() { 202 return nil 203 } 204 205 path, err := path(lockFile) 206 if err != nil { 207 return err 208 } 209 return os.Remove(path) 210 } 211 212 // callWithCount calls the given countFn with the current count in test_count, 213 // and updates it with what the function returns. 214 // 215 // If the function produced an error, that will be returned instead. 216 func callWithCount(fn countFn) error { 217 path, err := path(countFile) 218 if err != nil { 219 return err 220 } 221 222 f, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0666) 223 if err != nil { 224 return err 225 } 226 227 contents, err := ioutil.ReadAll(f) 228 if err != nil { 229 return err 230 } 231 232 var n int = 0 233 if len(contents) != 0 { 234 n, err = strconv.Atoi(string(contents)) 235 if err != nil { 236 return err 237 } 238 239 if n < 0 { 240 return errNegativeCount 241 } 242 } 243 244 after, err := fn(n) 245 if err != nil { 246 return err 247 } 248 249 // We want to write over the contents in the file, so "truncate" the 250 // file to a length of 0, and then seek to the beginning of the file to 251 // update the write head. 252 if err := f.Truncate(0); err != nil { 253 return err 254 } 255 if _, err := f.Seek(0, io.SeekStart); err != nil { 256 return err 257 } 258 259 if _, err := fmt.Fprintf(f, "%d", after); err != nil { 260 return err 261 } 262 return nil 263 } 264 265 // path returns an absolute path corresponding to any given path relative to the 266 // 't' directory of the current checkout of Git LFS. 267 func path(s string) (string, error) { 268 p := filepath.Join(filepath.Dir(os.Getenv("LFSTEST_DIR")), s) 269 if err := os.MkdirAll(filepath.Dir(p), 0666); err != nil { 270 return "", err 271 } 272 return p, nil 273 } 274 275 // disabled returns true if and only if the lock acquisition phase is disabled. 276 func disabled() bool { 277 s := os.Getenv("GIT_LFS_LOCK_ACQUIRE_DISABLED") 278 b, err := strconv.ParseBool(s) 279 if err != nil { 280 return false 281 } 282 return b 283 } 284 285 // fatal reports the given error (if non-nil), and then dies. If the error was 286 // nil, nothing happens. 287 func fatal(err error) { 288 if err == nil { 289 return 290 } 291 if err := release(); err != nil { 292 fmt.Fprintf(os.Stderr, "fatal: while dying, got: %s\n", err) 293 } 294 fmt.Fprintf(os.Stderr, "fatal: %s\n", err) 295 os.Exit(1) 296 }