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  }