| // 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 "" |
| } |