blob: 8554dcd85fbaadf37fd080a8d521dfb00789eaf7 [file] [log] [blame]
// Copyright 2017 The LUCI Authors.
//
// 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 prober
import (
"context"
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"go.chromium.org/luci/common/errors"
"go.chromium.org/luci/common/logging"
"go.chromium.org/luci/common/logging/gologger"
"go.chromium.org/luci/common/system/environ"
. "github.com/smartystreets/goconvey/convey"
. "go.chromium.org/luci/common/testing/assertions"
)
func baseTestContext() context.Context {
c := context.Background()
c = gologger.StdConfig.Use(c)
c = logging.SetLevel(c, logging.Debug)
return c
}
// TestFindSystemGit tests the ability to resolve the current executable.
func TestResolveSelf(t *testing.T) {
t.Parallel()
// On OSX temp dir is a symlink and it interferes with the test.
evalSymlinks := func(p string) string {
p, err := filepath.EvalSymlinks(p)
if err != nil {
t.Fatalf("eval symlinks: %s", err)
}
return p
}
realExec, err := os.Executable()
if err != nil {
t.Fatalf("failed to resolve the real executable: %s", err)
}
realExec = evalSymlinks(realExec)
realExecStat, err := os.Stat(realExec)
if err != nil {
t.Fatalf("failed to stat the real executable %q: %s", realExec, err)
}
Convey(`With a temporary directory`, t, func() {
tdir := evalSymlinks(t.TempDir())
// Set up a base probe.
probe := Probe{
Target: "git",
}
Convey(`Will resolve our executable.`, func() {
So(probe.ResolveSelf(), ShouldBeNil)
So(probe.Self, ShouldEqual, realExec)
So(os.SameFile(probe.SelfStat, realExecStat), ShouldBeTrue)
})
Convey(`When "self" is a symlink to the executable`, func() {
symSelf := filepath.Join(tdir, "me")
if err := os.Symlink(realExec, symSelf); err != nil {
t.Skipf("Could not create symlink %q => %q: %s", realExec, symSelf, err)
return
}
So(probe.ResolveSelf(), ShouldBeNil)
So(probe.Self, ShouldEqual, realExec)
So(os.SameFile(probe.SelfStat, realExecStat), ShouldBeTrue)
})
})
}
// TestFindSystemGit tests the ability to locate the "git" command in PATH.
func TestSystemProbe(t *testing.T) {
t.Parallel()
envBase := environ.New(nil)
var selfEXESuffix, otherEXESuffix string
if runtime.GOOS == "windows" {
selfEXESuffix = ".exe"
otherEXESuffix = ".bat"
envBase.Set("PATHEXT", strings.Join([]string{".com", ".exe", ".bat", ".ohai"}, string(os.PathListSeparator)))
}
Convey(`With a fake PATH setup`, t, func() {
tdir := t.TempDir()
c := baseTestContext()
createExecutable := func(relPath string) (dir string, path string) {
path = filepath.Join(tdir, filepath.FromSlash(relPath))
dir = filepath.Dir(path)
if err := os.MkdirAll(dir, 0755); err != nil {
t.Fatalf("Failed to create base directory [%s]: %s", dir, err)
}
if err := os.WriteFile(path, []byte("fake"), 0755); err != nil {
t.Fatalf("Failed to create executable: %s", err)
}
return
}
// Construct a fake filesystem rooted in "tdir".
var (
selfDir, selfGit = createExecutable("self/git" + selfEXESuffix)
_, overrideGit = createExecutable("self/override/reldir/git" + otherEXESuffix)
fooDir, fooGit = createExecutable("foo/git" + otherEXESuffix)
wrapperDir, _ = createExecutable("wrapper/git" + otherEXESuffix)
brokenDir, _ = createExecutable("broken/git" + otherEXESuffix)
otherDir, _ = createExecutable("other/not_git")
nonexistDir = filepath.Join(tdir, "nonexist")
nonexistGit = filepath.Join(nonexistDir, "git"+otherEXESuffix)
)
// Set up a base probe.
wrapperChecks := 0
probe := Probe{
Target: "git",
CheckWrapper: func(ctx context.Context, path string, env environ.Env) (bool, error) {
wrapperChecks++
switch filepath.Dir(path) {
case wrapperDir, selfDir:
return true, nil
case brokenDir:
return false, errors.New("broken")
default:
return false, nil
}
},
}
selfGitStat, err := os.Stat(selfGit)
if err != nil {
t.Fatalf("failed to stat self Git %q: %s", selfGit, err)
}
probe.Self, probe.SelfStat = selfGit, selfGitStat
env := envBase.Clone()
setPATH := func(v ...string) {
env.Set("PATH", strings.Join(v, string(os.PathListSeparator)))
}
Convey(`Can identify the next Git when it follows self in PATH.`, func() {
setPATH(selfDir, selfDir, fooDir, wrapperDir, otherDir, nonexistDir)
git, err := probe.Locate(c, "", env)
So(err, ShouldBeNil)
So(git, shouldBeSameFileAs, fooGit)
So(wrapperChecks, ShouldEqual, 1)
})
Convey(`When Git precedes self in PATH`, func() {
setPATH(fooDir, selfDir, wrapperDir, otherDir, nonexistDir)
Convey(`Will identify Git`, func() {
git, err := probe.Locate(c, "", env)
So(err, ShouldBeNil)
So(git, shouldBeSameFileAs, fooGit)
So(wrapperChecks, ShouldEqual, 1)
})
Convey(`Will prefer an override Git`, func() {
probe.RelativePathOverride = []string{
"override/reldir", // (see "overrideGit")
}
git, err := probe.Locate(c, "", env)
So(err, ShouldBeNil)
So(git, shouldBeSameFileAs, overrideGit)
So(wrapperChecks, ShouldEqual, 1)
})
})
Convey(`Can identify the next Git when self does not exist.`, func() {
setPATH(wrapperDir, selfDir, wrapperDir, otherDir, nonexistDir, fooDir)
probe.Self = nonexistGit
git, err := probe.Locate(c, "", env)
So(err, ShouldBeNil)
So(git, shouldBeSameFileAs, fooGit)
So(wrapperChecks, ShouldEqual, 2)
})
Convey(`With PATH setup pointing to a wrapper, self, and then the system Git`, func() {
// NOTE: wrapperDir is repeated, but it will only count towards one check,
// since we cache checks on a per-directory basis.
setPATH(wrapperDir, wrapperDir, selfDir, otherDir, fooDir, nonexistDir)
Convey(`Will prefer the cached value.`, func() {
git, err := probe.Locate(c, fooGit, env)
So(err, ShouldBeNil)
So(git, shouldBeSameFileAs, fooGit)
So(wrapperChecks, ShouldEqual, 0)
})
Convey(`Will ignore the cached value if it is self.`, func() {
git, err := probe.Locate(c, selfGit, env)
So(err, ShouldBeNil)
So(git, shouldBeSameFileAs, fooGit)
So(wrapperChecks, ShouldEqual, 2)
})
Convey(`Will ignore the cached value if it does not exist.`, func() {
git, err := probe.Locate(c, nonexistGit, env)
So(err, ShouldBeNil)
So(git, shouldBeSameFileAs, fooGit)
So(wrapperChecks, ShouldEqual, 2)
})
Convey(`Will skip the wrapper and identify the system Git.`, func() {
git, err := probe.Locate(c, "", env)
So(err, ShouldBeNil)
So(git, shouldBeSameFileAs, fooGit)
So(wrapperChecks, ShouldEqual, 2)
})
})
Convey(`Will skip everything if the wrapper check fails.`, func() {
setPATH(wrapperDir, brokenDir, selfDir, otherDir, fooDir, nonexistDir)
git, err := probe.Locate(c, "", env)
So(err, ShouldBeNil)
So(git, shouldBeSameFileAs, fooGit)
So(wrapperChecks, ShouldEqual, 3)
})
Convey(`Will fail if cannot find another Git in PATH.`, func() {
setPATH(selfDir, otherDir, nonexistDir)
_, err := probe.Locate(c, "", env)
So(err, ShouldErrLike, "could not find target in system")
So(wrapperChecks, ShouldEqual, 0)
})
Convey(`When a symlink is created`, func() {
conveyFn := Convey
if err := os.Symlink(selfGit, filepath.Join(otherDir, filepath.Base(selfGit))); err != nil {
t.Logf("Failed to create symlink; skipping symlink test: %s", err)
conveyFn = SkipConvey
}
conveyFn(`Will ignore symlink because it's the same file.`, func() {
setPATH(selfDir, otherDir, fooDir, wrapperDir)
git, err := probe.Locate(c, "", env)
So(err, ShouldBeNil)
So(git, shouldBeSameFileAs, fooGit)
So(wrapperChecks, ShouldEqual, 1)
})
})
})
}
func shouldBeSameFileAs(actual any, expected ...any) string {
aPath, ok := actual.(string)
if !ok {
return "actual must be a path string"
}
if len(expected) != 1 {
return "exactly one expected path string must be provided"
}
expPath, ok := expected[0].(string)
if !ok {
return "expected must be a path string"
}
aSt, err := os.Stat(aPath)
if err != nil {
return fmt.Sprintf("failed to stat actual [%s]: %s", aPath, err)
}
expSt, err := os.Stat(expPath)
if err != nil {
return fmt.Sprintf("failed to stat expected [%s]: %s", expPath, err)
}
if !os.SameFile(aSt, expSt) {
return fmt.Sprintf("[%s] is not the same file as [%s]", expPath, aPath)
}
return ""
}