- оба манифеста стали данными, которые инструмент переписывает целиком, так что и наборный получает точку в начале: он служебный файл, а не документ репозитория - переименование механическое, одна константа manifest.Name; литералы в фикстурах и ссылки в README.md и CLAUDE.md обновлены следом
164 lines
5.0 KiB
Go
164 lines
5.0 KiB
Go
package source_test
|
|
|
|
import (
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.vakhrushev.me/av/convy/internal/source"
|
|
)
|
|
|
|
func TestParseTellsTheTransportsApart(t *testing.T) {
|
|
cases := []struct {
|
|
raw string
|
|
kind source.Kind
|
|
location string
|
|
rev string
|
|
}{
|
|
{"../dev-conventions", source.Local, "../dev-conventions", ""},
|
|
{"/srv/conventions", source.Local, "/srv/conventions", ""},
|
|
{"https://git.example.org/av/conventions.git", source.Git, "https://git.example.org/av/conventions.git", ""},
|
|
{"http://git.example.org/av/conventions.git#v2", source.Git, "http://git.example.org/av/conventions.git", "v2"},
|
|
{"ssh://git@git.example.org:2222/av/conventions.git", source.Git, "ssh://git@git.example.org:2222/av/conventions.git", ""},
|
|
{"git@git.example.org:av/conventions.git", source.Git, "git@git.example.org:av/conventions.git", ""},
|
|
// A plain path is the directory as it lies; file:// is the same
|
|
// repository as it is committed.
|
|
{"file:///srv/conventions#main", source.Git, "file:///srv/conventions", "main"},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.raw, func(t *testing.T) {
|
|
ref, err := source.Parse(tc.raw)
|
|
if err != nil {
|
|
t.Fatalf("parsing %q: %v", tc.raw, err)
|
|
}
|
|
if ref.Kind != tc.kind {
|
|
t.Errorf("read as %s, expected %s", ref.Kind, tc.kind)
|
|
}
|
|
if ref.Location != tc.location {
|
|
t.Errorf("location %q, expected %q", ref.Location, tc.location)
|
|
}
|
|
if ref.Rev != tc.rev {
|
|
t.Errorf("revision %q, expected %q", ref.Rev, tc.rev)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseRefusesWhatItCannotMean(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
raw string
|
|
want string
|
|
}{
|
|
{"nothing at all", " ", "empty"},
|
|
{"a revision of a directory", "../conventions#main", "only a git repository has"},
|
|
{"a home directory", "~/projects/conventions", "every other machine"},
|
|
{"a scheme nobody serves", "rclone://remote/conventions", "over git"},
|
|
{"a hash with no revision", "https://git.example.org/c.git#", "names no revision"},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
_, err := source.Parse(tc.raw)
|
|
if err == nil {
|
|
t.Fatalf("the reference %q went through", tc.raw)
|
|
}
|
|
if !strings.Contains(err.Error(), tc.want) {
|
|
t.Errorf("the refusal does not say %q: %s", tc.want, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestOpenResolvesAPathAgainstTheManifest(t *testing.T) {
|
|
base := t.TempDir()
|
|
if err := os.MkdirAll(filepath.Join(base, "vendor", "conventions"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
ref, err := source.Parse("vendor/conventions")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
tree, err := source.Open(ref, base)
|
|
if err != nil {
|
|
t.Fatalf("opening a relative path: %v", err)
|
|
}
|
|
defer tree.Close()
|
|
|
|
if tree.Dir() != filepath.Join(base, "vendor", "conventions") {
|
|
t.Errorf("resolved to %s", tree.Dir())
|
|
}
|
|
// Nothing was fetched, so nothing is released: the directory was there
|
|
// before the command and stays after it.
|
|
tree.Close()
|
|
if _, err := os.Stat(tree.Dir()); err != nil {
|
|
t.Errorf("closing removed a directory that was not fetched: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestOpenRefusesAFileWhereALevelIsExpected(t *testing.T) {
|
|
base := t.TempDir()
|
|
if err := os.WriteFile(filepath.Join(base, "conventions"), []byte("x"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ref, _ := source.Parse("conventions")
|
|
if _, err := source.Open(ref, base); err == nil || !strings.Contains(err.Error(), "a level is a directory") {
|
|
t.Errorf("a file passed as a level: %v", err)
|
|
}
|
|
}
|
|
|
|
// A git reference takes what is committed rather than what lies in the working
|
|
// tree, and that is the difference between the two spellings of a local suite.
|
|
func TestOpenGitTakesTheCommittedState(t *testing.T) {
|
|
if _, err := exec.LookPath("git"); err != nil {
|
|
t.Skip("no git in PATH")
|
|
}
|
|
repo := t.TempDir()
|
|
name := filepath.Join(repo, ".conventions-suite.toml")
|
|
if err := os.WriteFile(name, []byte("committed\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, args := range [][]string{
|
|
{"init", "--quiet", "-b", "main"},
|
|
{"-c", "user.email=t@example.org", "-c", "user.name=t", "add", ".conventions-suite.toml"},
|
|
{"-c", "user.email=t@example.org", "-c", "user.name=t", "commit", "--quiet", "-m", "first"},
|
|
} {
|
|
cmd := exec.Command("git", args...)
|
|
cmd.Dir = repo
|
|
if out, err := cmd.CombinedOutput(); err != nil {
|
|
t.Fatalf("git %v: %v\n%s", args, err, out)
|
|
}
|
|
}
|
|
if err := os.WriteFile(name, []byte("uncommitted\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
ref, err := source.Parse("file://" + repo + "#main")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
tree, err := source.Open(ref, ".")
|
|
if err != nil {
|
|
t.Fatalf("cloning a local repository: %v", err)
|
|
}
|
|
body, err := os.ReadFile(filepath.Join(tree.Dir(), ".conventions-suite.toml"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.TrimSpace(string(body)) != "committed" {
|
|
t.Errorf("the working tree leaked into the clone: %q", body)
|
|
}
|
|
|
|
dir := tree.Dir()
|
|
if err := tree.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := os.Stat(dir); err == nil {
|
|
t.Errorf("the clone survived the close: %s", dir)
|
|
}
|
|
}
|