git-lfs/test/testutils.go

339 lines
9.0 KiB
Go
Raw Normal View History

package test
// Utility functions for more complex go tests
// Need to be in a separate test package so they can be imported anywhere
// Also can't add _test.go suffix to exclude from main build (import doesn't work)
// To avoid import cycles, append "_test" to the package statement of any test using
// this package and use "import . original/package/name" to get the same visibility
// as if the test was in the same package (as usual)
import (
"fmt"
"io"
"io/ioutil"
"math/rand"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"time"
"github.com/github/git-lfs/git"
"github.com/github/git-lfs/lfs"
)
type RepoType int
const (
// Normal repo with working copy
RepoTypeNormal = RepoType(iota)
// Bare repo (no working copy)
RepoTypeBare = RepoType(iota)
// Repo with working copy but git dir is separate
RepoTypeSeparateDir = RepoType(iota)
)
type RepoCreateSettings struct {
RepoType RepoType
}
type Repo struct {
// Path to the repo, working copy if non-bare
Path string
// Path to the git dir
GitDir string
// Settings used to create this repo
Settings *RepoCreateSettings
// Previous dir for pushd
popDir string
}
// Change to repo dir but save current dir
func (r *Repo) Pushd(t *testing.T) {
if r.popDir != "" {
t.Fatalf("Cannot Pushd twice")
}
oldwd, err := os.Getwd()
if err != nil {
t.Fatalf("Can't get cwd %v", err)
}
err = os.Chdir(r.Path)
if err != nil {
t.Fatalf("Can't chdir %v", err)
}
r.popDir = oldwd
}
func (r *Repo) Popd(t *testing.T) {
if r.popDir != "" {
err := os.Chdir(r.Path)
if err != nil {
t.Fatalf("Can't chdir %v", err)
}
r.popDir = ""
}
}
func (r *Repo) Cleanup(t *testing.T) {
// pop out if necessary
r.Popd(t)
// Make sure cwd isn't inside a path we're going to delete
oldwd, err := os.Getwd()
if err == nil {
if strings.HasPrefix(oldwd, r.Path) ||
strings.HasPrefix(oldwd, r.GitDir) {
os.Chdir(os.TempDir())
}
}
if r.GitDir != "" {
os.RemoveAll(r.GitDir)
}
if r.Path != "" {
os.RemoveAll(r.Path)
}
}
func NewRepo(t *testing.T) *Repo {
return NewCustomRepo(t, &RepoCreateSettings{RepoType: RepoTypeNormal})
}
func NewCustomRepo(t *testing.T, settings *RepoCreateSettings) *Repo {
ret := &Repo{Settings: settings}
path, err := ioutil.TempDir("", "lfsRepo")
if err != nil {
t.Fatalf("Can't create temp dir for git repo: %v", err)
}
ret.Path = path
args := []string{"init"}
switch settings.RepoType {
case RepoTypeBare:
args = append(args, "--bare")
ret.GitDir = ret.Path
case RepoTypeSeparateDir:
gitdir, err := ioutil.TempDir("", "lfstestgitdir")
if err != nil {
ret.Cleanup(t)
t.Fatalf("Can't create temp dir for git repo: %v", err)
}
args = append(args, "--separate-dir", gitdir)
ret.GitDir = gitdir
default:
ret.GitDir = filepath.Join(ret.Path, ".git")
}
args = append(args, path)
cmd := exec.Command("git", args...)
err = cmd.Run()
if err != nil {
ret.Cleanup(t)
t.Fatalf("Unable to create git repo at %v: %v", path, err)
}
return ret
}
// Simplistic fire & forget running of git command - returns combined output
func RunGitCommand(t *testing.T, failureCheck bool, args ...string) string {
outp, err := exec.Command("git", args...).CombinedOutput()
if failureCheck && err != nil {
t.Fatalf("Error running git command 'git %v': %v", strings.Join(args, " "), err)
}
return string(outp)
}
// Input data for a single file in a commit
type FileInput struct {
// Name of file (required)
Filename string
// Size of file (required)
Size int64
// Input data (optional - if nil, placeholder data of Size will be created)
Data io.Reader
}
// Input for defining commits for test repo
type CommitInput struct {
// Date that we should commit on (optional, leave blank for 'now')
CommitDate time.Time
// List of files to include in this commit
Files []*FileInput
2015-08-19 12:02:58 +00:00
// List of parent branches (all branches must have been created in a previous NewBranch or be master)
// Can be omitted to just use the parent of the previous commit
ParentBranches []string
// Name of a new branch we should create at this commit (optional - master not required)
NewBranch string
2015-08-19 12:02:58 +00:00
// Names of any tags we should create at this commit (optional)
Tags []string
// Name of committer
CommitterName string
// Email of committer
CommitterEmail string
}
// Output struct with details of commits created for test
type CommitOutput struct {
Sha string
Parents []string
Files []*lfs.Pointer
}
func formatGitDate(tm time.Time) string {
// Git format is "Fri Jun 21 20:26:41 2013 +0900" but no zero-leading for day
return tm.Format("Mon Jan 2 15:04:05 2006 -0700")
}
func commitAtDate(atDate time.Time, committerName, committerEmail, msg string) error {
var args []string
if committerName != "" && committerEmail != "" {
args = append(args, "-c", fmt.Sprintf("user.name=%v", committerName))
args = append(args, "-c", fmt.Sprintf("user.email=%v", committerEmail))
}
args = append(args, "commit", "--allow-empty", "-m", msg)
cmd := exec.Command("git", args...)
env := os.Environ()
// set GIT_COMMITTER_DATE environment var e.g. "Fri Jun 21 20:26:41 2013 +0900"
if atDate.IsZero() {
env = append(env, "GIT_COMMITTER_DATE=")
} else {
env = append(env, fmt.Sprintf("GIT_COMMITTER_DATE=%v", formatGitDate(atDate)))
}
cmd.Env = env
return cmd.Run()
}
func (repo *Repo) AddCommits(t *testing.T, inputs []*CommitInput) []*CommitOutput {
if repo.Settings.RepoType == RepoTypeBare {
t.Fatalf("Cannot use SetupRepo on a bare repo; clone it & push changes instead")
}
// Change to repo working dir
oldwd, err := os.Getwd()
if err != nil {
t.Fatalf("Can't get cwd %v", err)
}
err = os.Chdir(repo.Path)
if err != nil {
t.Fatalf("Can't chdir to repo %v", err)
}
// Used to check whether we need to checkout another commit before
lastBranch := "master"
outputs := make([]*CommitOutput, 0, len(inputs))
for i, input := range inputs {
output := &CommitOutput{}
// first, are we on the correct branch
if len(input.ParentBranches) > 0 {
if input.ParentBranches[0] != lastBranch {
RunGitCommand(t, true, "checkout", input.ParentBranches[0])
lastBranch = input.ParentBranches[0]
}
}
// Is this a merge?
if len(input.ParentBranches) > 1 {
// Always take the *other* side in a merge so we adopt changes
// also don't automatically commit, we'll do that below
args := []string{"merge", "--no-ff", "--no-commit", "--strategy-option=theirs"}
args = append(args, input.ParentBranches[1:]...)
RunGitCommand(t, false, args...)
} else if input.NewBranch != "" {
RunGitCommand(t, true, "checkout", "-b", input.NewBranch)
lastBranch = input.NewBranch
}
// Any files to write?
for fi, infile := range input.Files {
inputData := infile.Data
if inputData == nil {
// Different data for each file but deterministic
inputData = NewPlaceholderDataReader(int64(i*fi), infile.Size)
}
cleaned, err := lfs.PointerClean(inputData, infile.Filename, infile.Size, nil)
if err != nil {
t.Errorf("Error creating pointer file: %v", err)
continue
}
output.Files = append(output.Files, cleaned.Pointer)
// Write pointer to local filename for adding (not using clean filter)
os.MkdirAll(filepath.Dir(infile.Filename), 0755)
f, err := os.Create(infile.Filename)
if err != nil {
t.Errorf("Error creating pointer file: %v", err)
continue
}
_, err = cleaned.Pointer.Encode(f)
if err != nil {
f.Close()
t.Errorf("Error encoding pointer file: %v", err)
continue
}
f.Close() // early close in a loop, don't defer
RunGitCommand(t, true, "add", infile.Filename)
}
// Now commit
commitAtDate(input.CommitDate, input.CommitterName, input.CommitterEmail,
fmt.Sprintf("Test commit %d", i))
commit, err := git.GetCommitSummary("HEAD")
if err != nil {
t.Fatalf("Error determining commit SHA: %v", err)
}
2015-08-19 12:02:58 +00:00
// tags
for _, tag := range input.Tags {
// Use annotated tags, assume full release tags (also tag objects have edge cases)
RunGitCommand(t, true, "tag", "-a", "-m", "Added tag", tag)
}
output.Sha = commit.Sha
output.Parents = commit.Parents
outputs = append(outputs, output)
}
// Restore cwd
err = os.Chdir(oldwd)
if err != nil {
t.Fatalf("Can't restore old cwd %v", err)
}
return outputs
}
// Just a psuedo-random stream of bytes (not cryptographic)
// Calls RNG a bit less often than using rand.Source directly
type PlaceholderDataReader struct {
source rand.Source
bytesLeft int64
}
func NewPlaceholderDataReader(seed, size int64) *PlaceholderDataReader {
return &PlaceholderDataReader{rand.NewSource(seed), size}
}
func (r *PlaceholderDataReader) Read(p []byte) (int, error) {
c := len(p)
i := 0
for i < c && r.bytesLeft > 0 {
// Use all 8 bytes of the 64-bit random number
val64 := r.source.Int63()
for j := 0; j < 8 && i < c && r.bytesLeft > 0; j++ {
// Duplicate this byte 16 times (faster)
for k := 0; k < 16 && r.bytesLeft > 0; k++ {
p[i] = byte(val64)
i++
r.bytesLeft--
}
// Next byte from the 8-byte number
val64 = val64 >> 8
}
}
var err error
if r.bytesLeft == 0 {
err = io.EOF
}
return i, err
}