| // Copyright 2017 Google Inc. All rights reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package build |
| |
| import ( |
| "fmt" |
| "io/ioutil" |
| "os" |
| "os/exec" |
| "path/filepath" |
| "syscall" |
| "testing" |
| |
| "android/soong/ui/logger" |
| ) |
| |
| // some util methods and data structures that aren't directly part of a test |
| func makeLockDir() (path string, err error) { |
| return ioutil.TempDir("", "soong_lock_test") |
| } |
| func lockOrFail(t *testing.T) (lock fileLock) { |
| lockDir, err := makeLockDir() |
| var lockPointer *fileLock |
| if err == nil { |
| lockPointer, err = newLock(lockDir) |
| } |
| if err != nil { |
| os.RemoveAll(lockDir) |
| t.Fatalf("Failed to create lock: %v", err) |
| } |
| |
| return *lockPointer |
| } |
| func removeTestLock(fileLock fileLock) { |
| lockdir := filepath.Dir(fileLock.File.Name()) |
| os.RemoveAll(lockdir) |
| } |
| |
| // countWaiter only exists for the purposes of testing lockSynchronous |
| type countWaiter struct { |
| numWaitsElapsed int |
| maxNumWaits int |
| } |
| |
| func newCountWaiter(count int) (waiter *countWaiter) { |
| return &countWaiter{0, count} |
| } |
| |
| func (c *countWaiter) wait() { |
| c.numWaitsElapsed++ |
| } |
| func (c *countWaiter) checkDeadline() (done bool, remainder string) { |
| numWaitsRemaining := c.maxNumWaits - c.numWaitsElapsed |
| if numWaitsRemaining < 1 { |
| return true, "" |
| } |
| return false, fmt.Sprintf("%v waits remain", numWaitsRemaining) |
| } |
| func (c countWaiter) summarize() (summary string) { |
| return fmt.Sprintf("waiting %v times", c.maxNumWaits) |
| } |
| |
| // countLock only exists for the purposes of testing lockSynchronous |
| type countLock struct { |
| nextIndex int |
| successIndex int |
| } |
| |
| var _ lockable = (*countLock)(nil) |
| |
| // returns a countLock that succeeds on iteration <index> |
| func testLockCountingTo(index int) (lock *countLock) { |
| return &countLock{nextIndex: 0, successIndex: index} |
| } |
| func (c *countLock) description() (message string) { |
| return fmt.Sprintf("counter that counts from %v to %v", c.nextIndex, c.successIndex) |
| } |
| func (c *countLock) tryLock() (err error) { |
| currentIndex := c.nextIndex |
| c.nextIndex++ |
| if currentIndex == c.successIndex { |
| return nil |
| } |
| return fmt.Errorf("Lock busy: %s", c.description()) |
| } |
| func (c *countLock) Unlock() (err error) { |
| if c.nextIndex == c.successIndex { |
| return nil |
| } |
| return fmt.Errorf("Not locked: %s", c.description()) |
| } |
| |
| // end of util methods |
| |
| // start of tests |
| |
| // simple test |
| func TestGetLock(t *testing.T) { |
| lockfile := lockOrFail(t) |
| defer removeTestLock(lockfile) |
| } |
| |
| // a more complicated test that spans multiple processes |
| var lockPathVariable = "LOCK_PATH" |
| var successStatus = 0 |
| var unexpectedError = 1 |
| var busyStatus = 2 |
| |
| func TestTrylock(t *testing.T) { |
| lockpath := os.Getenv(lockPathVariable) |
| if len(lockpath) < 1 { |
| checkTrylockMainProcess(t) |
| } else { |
| getLockAndExit(lockpath) |
| } |
| } |
| |
| // the portion of TestTrylock that runs in the main process |
| func checkTrylockMainProcess(t *testing.T) { |
| var err error |
| lockfile := lockOrFail(t) |
| defer removeTestLock(lockfile) |
| lockdir := filepath.Dir(lockfile.File.Name()) |
| otherAcquired, message, err := forkAndGetLock(lockdir) |
| if err != nil { |
| t.Fatalf("Unexpected error in subprocess trying to lock uncontested fileLock: %v. Subprocess output: %q", err, message) |
| } |
| if !otherAcquired { |
| t.Fatalf("Subprocess failed to lock uncontested fileLock. Subprocess output: %q", message) |
| } |
| |
| err = lockfile.tryLock() |
| if err != nil { |
| t.Fatalf("Failed to lock fileLock: %v", err) |
| } |
| |
| reacquired, message, err := forkAndGetLock(filepath.Dir(lockfile.File.Name())) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if reacquired { |
| t.Fatalf("Permitted locking fileLock twice. Subprocess output: %q", message) |
| } |
| |
| err = lockfile.Unlock() |
| if err != nil { |
| t.Fatalf("Error unlocking fileLock: %v", err) |
| } |
| |
| reacquired, message, err = forkAndGetLock(filepath.Dir(lockfile.File.Name())) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if !reacquired { |
| t.Fatalf("Subprocess failed to acquire lock after it was released by the main process. Subprocess output: %q", message) |
| } |
| } |
| func forkAndGetLock(lockDir string) (acquired bool, subprocessOutput []byte, err error) { |
| cmd := exec.Command(os.Args[0], "-test.run=TestTrylock") |
| cmd.Env = append(os.Environ(), fmt.Sprintf("%s=%s", lockPathVariable, lockDir)) |
| subprocessOutput, err = cmd.CombinedOutput() |
| exitStatus := successStatus |
| if exitError, ok := err.(*exec.ExitError); ok { |
| if waitStatus, ok := exitError.Sys().(syscall.WaitStatus); ok { |
| exitStatus = waitStatus.ExitStatus() |
| } |
| } |
| if exitStatus == successStatus { |
| return true, subprocessOutput, nil |
| } else if exitStatus == busyStatus { |
| return false, subprocessOutput, nil |
| } else { |
| return false, subprocessOutput, fmt.Errorf("Unexpected status %v", exitStatus) |
| } |
| } |
| |
| // This function runs in a different process. See TestTrylock |
| func getLockAndExit(lockpath string) { |
| fmt.Printf("Will lock path %q\n", lockpath) |
| lockfile, err := newLock(lockpath) |
| exitStatus := unexpectedError |
| if err == nil { |
| err = lockfile.tryLock() |
| if err == nil { |
| exitStatus = successStatus |
| } else { |
| exitStatus = busyStatus |
| } |
| } |
| fmt.Printf("Tried to lock path %s. Received error %v. Exiting with status %v\n", lockpath, err, exitStatus) |
| os.Exit(exitStatus) |
| } |
| |
| func TestLockFirstTrySucceeds(t *testing.T) { |
| noopLogger := logger.New(ioutil.Discard) |
| lock := testLockCountingTo(0) |
| waiter := newCountWaiter(0) |
| err := lockSynchronous(lock, waiter, noopLogger) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if waiter.numWaitsElapsed != 0 { |
| t.Fatalf("Incorrect number of waits elapsed; expected 0, got %v", waiter.numWaitsElapsed) |
| } |
| } |
| func TestLockThirdTrySucceeds(t *testing.T) { |
| noopLogger := logger.New(ioutil.Discard) |
| lock := testLockCountingTo(2) |
| waiter := newCountWaiter(2) |
| err := lockSynchronous(lock, waiter, noopLogger) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if waiter.numWaitsElapsed != 2 { |
| t.Fatalf("Incorrect number of waits elapsed; expected 2, got %v", waiter.numWaitsElapsed) |
| } |
| } |
| func TestLockTimedOut(t *testing.T) { |
| noopLogger := logger.New(ioutil.Discard) |
| lock := testLockCountingTo(3) |
| waiter := newCountWaiter(2) |
| err := lockSynchronous(lock, waiter, noopLogger) |
| if err == nil { |
| t.Fatalf("Appeared to have acquired lock on iteration %v which should not be available until iteration %v", waiter.numWaitsElapsed, lock.successIndex) |
| } |
| if waiter.numWaitsElapsed != waiter.maxNumWaits { |
| t.Fatalf("Waited an incorrect number of times; expected %v, got %v", waiter.maxNumWaits, waiter.numWaitsElapsed) |
| } |
| } |