blob: 224ccf8566fbbd7b056ee33466b20e26e36d033e [file] [log] [blame]
// Copyright 2023 The Shac 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 engine
import (
"context"
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"golang.org/x/mod/sumdb/dirhash"
)
func TestFSToDigest_Reproducible(t *testing.T) {
t.Parallel()
// Reuse a simple Go project that is in Go Proxy
// (https://proxy.golang.org/). This ensures the algorithm matches the
// expected value.
//
// Created with:
// git clone https://github.com/maruel/ut -b v1.0.0
// cd ut
// cp *.go LICENSE README.md .travis.yml .git/config ../internal/engine/testdata/ut
//
// Ideally we could use a module already included the vendor directory, but
// Go's vendoring process strips out test files and .git files even though
// they are taken into account by the mod hash computation. We need to take
// an alternate approach that doesn't strip out those files, so bypass the
// normal vendoring process.
srcDir := filepath.Join("testdata", "ut")
root := t.TempDir()
copyTree(t, root, srcDir, map[string]string{
// Git doesn't allow committing the .git directory, but .git/config
// needs to be considered in the hash computation, so we commit it to
// `config` instead of `.git/config` and then copy it into the right
// place.
"config": ".git/config",
})
const prefix = "github.com/maruel/ut@v1.0.0"
// Retrieved from an empty directory:
// go mod init main
// go get github.com/maruel/ut@v1.0.0
// grep maruel/ut go.sum
const knownHash = "h1:Tg5f5waOijrohsOwnMlr1bZmv+wHEbuMEacNBE8kQ7k="
// Test our code to ensure it matches the go proxy.
if d, err := FSToDigest(os.DirFS(root), prefix); err != nil {
t.Fatal(err)
} else if d != knownHash {
t.Errorf("expected %s, got %s", knownHash, d)
}
// Now test with the standard enumerating code.
// Remove .git/config here, since this function doesn't filter it out.
if err := os.Remove(filepath.Join(root, ".git", "config")); err != nil {
t.Fatal(err)
}
if d, err := dirhash.HashDir(root, prefix, dirhash.Hash1); err != nil {
t.Fatal(err)
} else if d != knownHash {
t.Errorf("expected %s, got %s", knownHash, d)
}
}
func copyTree(t *testing.T, dstDir, srcDir string, renamings map[string]string) {
err := filepath.WalkDir(srcDir, func(path string, d fs.DirEntry, err error) error {
if err != nil || d.IsDir() {
return err
}
rel, err := filepath.Rel(srcDir, path)
if err != nil {
return err
}
if newName, ok := renamings[rel]; ok {
rel = newName
}
dest := filepath.Join(dstDir, rel)
b, err := os.ReadFile(path)
if err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(dest), 0o700); err != nil {
return err
}
return os.WriteFile(dest, b, 0o600)
})
if err != nil {
t.Fatal(err)
}
}
func TestFSToDigest_Fail(t *testing.T) {
t.Parallel()
root := t.TempDir()
if d, err := FSToDigest(os.DirFS(root), ""); d != "" {
t.Fatal(d)
} else if err == nil || err.Error() != "prefix is required" {
t.Fatal(err)
}
want := "stat .: no such file or directory"
if runtime.GOOS == "windows" {
want = "CreateFile .: The system cannot find the file specified."
}
if d, err := FSToDigest(os.DirFS(filepath.Join(root, "inexistant")), "prefix"); d != "" {
t.Fatal(d)
} else if err == nil || err.Error() != want {
t.Fatal(err)
}
}
func TestPackageManager(t *testing.T) {
// Do not run in parallel since it's modifying globals.
root := t.TempDir()
var cmds []string
if pkgConcurrency != 8 {
t.Fatal("expected 8")
}
oldPkgConcurrency := pkgConcurrency
oldGitCommand := gitCommand
t.Cleanup(func() {
pkgConcurrency = oldPkgConcurrency
gitCommand = oldGitCommand
})
pkgConcurrency = 1
gitCommand = func(ctx context.Context, d string, args ...string) error {
if !strings.HasPrefix(d, root) {
t.Errorf("%s doesn't have prefix %s", d, root)
}
if len(args) >= 2 && args[0] == "clone" {
// Kind of a hack to create the directory on git clone when mocking.
if err := os.Mkdir(filepath.Join(d, args[2]), 0o700); err != nil && errors.Is(err, fs.ErrNotExist) {
t.Error(err)
}
}
s := fmt.Sprintf("%s %s", strings.ReplaceAll(d[len(root):], "\\", "/"), strings.Join(args, " "))
cmds = append(cmds, s)
return nil
}
p := PackageManager{Root: root}
doc := Document{
Requirements: &Requirements{
Direct: []*Dependency{
{
Url: "example.com/bar",
Version: "version",
},
{
Url: "github.com/shac-project/shac",
Version: "pull/1/head",
},
},
Indirect: []*Dependency{
{
Url: "example.com/gerrit",
Version: "refs/changes/45/12345/12",
},
},
},
Sum: &Sum{
Known: []*Known{
{
Url: "example.com/bar",
Seen: []*VersionDigest{
{
Version: "version",
Digest: "h1:47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=",
},
},
},
{
Url: "example.com/gerrit",
Seen: []*VersionDigest{
{
Version: "refs/changes/45/12345/12",
Digest: "h1:47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=",
},
},
},
{
Url: "github.com/shac-project/shac",
Seen: []*VersionDigest{
{
Version: "pull/1/head",
Digest: "h1:47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=",
},
},
},
},
},
}
_, err := p.RetrievePackages(context.Background(), root, &doc)
if err != nil {
t.Error(err)
}
want := []string{
"/example.com clone https://example.com/bar bar@version",
"/example.com/bar@version checkout version",
"/github.com/shac-project/shac fetch https://github.com/shac-project/shac pull/1/head",
"/github.com/shac-project clone https://github.com/shac-project/shac shac",
"/github.com/shac-project/shac checkout FETCH_HEAD",
"/example.com/gerrit fetch https://example.com/gerrit refs/changes/45/12345/12",
"/example.com clone https://example.com/gerrit gerrit",
"/example.com/gerrit checkout FETCH_HEAD",
}
if diff := cmp.Diff(want, cmds); diff != "" {
t.Fatalf("mismatch (-want +got):\n%s", diff)
}
}
func TestPackageManager_Err(t *testing.T) {
doc := Document{}
d := t.TempDir()
p := PackageManager{Root: "foo"}
if _, err := p.RetrievePackages(context.Background(), d, &doc); err == nil {
t.Fatal("expected error; path is not absolute")
}
p = PackageManager{Root: filepath.Join(d, "non_existent")}
if _, err := p.RetrievePackages(context.Background(), d, &doc); err == nil {
t.Fatal("expected error")
}
p = PackageManager{Root: d}
if _, err := p.RetrievePackages(context.Background(), "foo", &doc); err == nil {
t.Fatal("expected error; path is not absolute")
}
if _, err := p.RetrievePackages(context.Background(), filepath.Join(d, "non_existent"), &doc); err == nil {
t.Fatal("expected error")
}
}