Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 17 additions & 8 deletions fixtures.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,16 +204,25 @@ func GetRemotes(t *testing.T, repoPath string) map[string]string {
if line == "" {
continue
}
name, rest, ok := strings.Cut(line, "\t")
name, remainder, ok := strings.Cut(line, "\t")
if !ok {
continue
// Fallback for unusual formatting that does not use tabs.
idx := strings.IndexAny(line, " \t")
if idx == -1 {
continue
}
name = strings.TrimSpace(line[:idx])
remainder = strings.TrimSpace(line[idx+1:])
} else {
name = strings.TrimSpace(name)
remainder = strings.TrimSpace(remainder)
}
name = strings.TrimSpace(name)
rest = strings.TrimSpace(rest)
rest = strings.TrimSuffix(rest, " (fetch)")
rest = strings.TrimSuffix(rest, " (push)")
if name != "" && rest != "" {
remotes[name] = rest

remainder = strings.TrimSuffix(remainder, " (fetch)")
remainder = strings.TrimSuffix(remainder, " (push)")

if name != "" && remainder != "" {
remotes[name] = remainder
}
}

Expand Down
20 changes: 20 additions & 0 deletions fixtures_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,26 @@ func TestCreateTestRepo_WithRemotes(t *testing.T) {
}
}

func TestCreateTestRepo_WithRemotePathContainingSpaces(t *testing.T) {
remotePath := testutil.CreateBareRemote(t, "origin with space")

repoPath := testutil.CreateTestRepo(t, testutil.RepoOptions{
Name: "remote-space-repo",
Remotes: map[string]string{
"origin": remotePath,
},
})

remotes := testutil.GetRemotes(t, repoPath)
originURL, exists := remotes["origin"]
if !exists {
t.Fatal("Expected 'origin' remote to be configured")
}
if originURL != remotePath {
t.Fatalf("Expected origin URL %q, got %q", remotePath, originURL)
}
}

func TestCreateBareRemote(t *testing.T) {
remotePath := testutil.CreateBareRemote(t, "test-remote")

Expand Down
183 changes: 183 additions & 0 deletions usb_fixtures.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
package testutil

import (
"fmt"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"testing"
"time"
)

// validateFixtureLayoutDir reports whether layoutDir may be joined under a fixture root.
// Empty layoutDir is allowed (caller may default it).
func validateFixtureLayoutDir(layoutDir string) error {
if layoutDir == "" {
return nil
}
clean := filepath.Clean(layoutDir)
if filepath.IsAbs(clean) {
return fmt.Errorf("must be relative to fixture root: %q", layoutDir)
}
if clean == ".." || strings.HasPrefix(clean, ".."+string(filepath.Separator)) {
return fmt.Errorf("must be relative to fixture root: %q", layoutDir)
}
return nil
}

type USBVolumeOptions struct {
LayoutDir string
Strategy string
CreateReposDir bool
}

type USBVolumeConfig struct {
SchemaVersion int
LayoutDir string
Strategy string
CreatedAt time.Time
}

func mustRelativeLayoutDir(t *testing.T, layoutDir string) string {
t.Helper()
if layoutDir == "" {
return "repos"
}
if err := validateFixtureLayoutDir(layoutDir); err != nil {
t.Fatalf("layout_dir %v", err)
}
return filepath.Clean(layoutDir)
}

func MustUSBVolumeRoot(t *testing.T, opts USBVolumeOptions) string {
t.Helper()
root := t.TempDir()
cfg := USBVolumeConfig{
SchemaVersion: 1,
LayoutDir: mustRelativeLayoutDir(t, opts.LayoutDir),
Strategy: opts.Strategy,
CreatedAt: time.Now().UTC(),
}
if cfg.Strategy == "" {
cfg.Strategy = "git-mirror"
}
WriteUSBVolumeConfig(t, root, cfg)
if opts.CreateReposDir {
if err := os.MkdirAll(filepath.Join(root, cfg.LayoutDir), 0o755); err != nil {
t.Fatalf("failed creating repos dir: %v", err)
}
}
return root
}

func WriteUSBVolumeConfig(t *testing.T, root string, cfg USBVolumeConfig) {
t.Helper()
if cfg.SchemaVersion <= 0 {
cfg.SchemaVersion = 1
}
cfg.LayoutDir = mustRelativeLayoutDir(t, cfg.LayoutDir)
if cfg.Strategy == "" {
cfg.Strategy = "git-mirror"
}
if cfg.CreatedAt.IsZero() {
cfg.CreatedAt = time.Now().UTC()
}
content := fmt.Sprintf(
"schema_version = %d\nlayout_dir = %q\nstrategy = %q\ncreated_at = %q\n",
cfg.SchemaVersion,
cfg.LayoutDir,
cfg.Strategy,
cfg.CreatedAt.Format(time.RFC3339),
)
if err := os.WriteFile(filepath.Join(root, ".git-fire"), []byte(content), 0o644); err != nil {
t.Fatalf("failed writing .git-fire: %v", err)
}
}

func readUSBVolumeConfigBytes(data []byte) (USBVolumeConfig, error) {
cfg := USBVolumeConfig{}
lines := strings.Split(string(data), "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") {
continue
}
key, val, ok := strings.Cut(line, "=")
if !ok {
continue
}
key = strings.TrimSpace(key)
val = strings.Trim(strings.TrimSpace(val), "\"")
switch key {
case "schema_version":
n, err := strconv.Atoi(val)
if err != nil {
return cfg, fmt.Errorf("invalid schema_version %q: %w", val, err)
}
cfg.SchemaVersion = n
Comment thread
coderabbitai[bot] marked this conversation as resolved.
case "layout_dir":
if err := validateFixtureLayoutDir(val); err != nil {
return cfg, fmt.Errorf("layout_dir: %w", err)
}
if val == "" {
cfg.LayoutDir = ""
} else {
cfg.LayoutDir = filepath.Clean(val)
}
case "strategy":
cfg.Strategy = val
case "created_at":
if val == "" {
return cfg, fmt.Errorf("created_at: empty value")
}
ts, err := time.Parse(time.RFC3339, val)
if err != nil {
return cfg, fmt.Errorf("invalid created_at %q: %w", val, err)
}
cfg.CreatedAt = ts
}
}
return cfg, nil
}

func ReadUSBVolumeConfig(t *testing.T, root string) USBVolumeConfig {
t.Helper()
data, err := os.ReadFile(filepath.Join(root, ".git-fire"))
if err != nil {
t.Fatalf("failed reading .git-fire: %v", err)
}
cfg, err := readUSBVolumeConfigBytes(data)
if err != nil {
t.Fatalf("parse .git-fire: %v", err)
}
return cfg
}

func AssertGitDirAt(t *testing.T, path string, wantBare bool) {
t.Helper()
if wantBare {
if _, err := os.Stat(filepath.Join(path, "HEAD")); err != nil {
t.Fatalf("expected bare repo at %s: %v", path, err)
}
return
}
if _, err := os.Stat(filepath.Join(path, ".git")); err != nil {
t.Fatalf("expected non-bare repo at %s: %v", path, err)
}
}

func FileURLForPath(t *testing.T, path string) string {
t.Helper()
abs, err := filepath.Abs(path)
if err != nil {
t.Fatalf("failed to make abs path: %v", err)
}
uPath := filepath.ToSlash(abs)
if filepath.VolumeName(abs) != "" && !strings.HasPrefix(uPath, "/") {
uPath = "/" + uPath
}
u := &url.URL{Scheme: "file", Path: uPath}
return u.String()
}
147 changes: 147 additions & 0 deletions usb_fixtures_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package testutil

import (
"net/url"
"os"
"path/filepath"
"strings"
"testing"
)

func TestMustUSBVolumeRoot(t *testing.T) {
root := MustUSBVolumeRoot(t, USBVolumeOptions{
LayoutDir: "repos",
Strategy: "git-mirror",
CreateReposDir: true,
})
if _, err := os.Stat(filepath.Join(root, ".git-fire")); err != nil {
t.Fatalf("expected .git-fire marker: %v", err)
}
if _, err := os.Stat(filepath.Join(root, "repos")); err != nil {
t.Fatalf("expected repos dir: %v", err)
}
}

func TestReadWriteUSBVolumeConfig(t *testing.T) {
root := t.TempDir()
WriteUSBVolumeConfig(t, root, USBVolumeConfig{
SchemaVersion: 2,
LayoutDir: "custom",
Strategy: "git-clone",
})
cfg := ReadUSBVolumeConfig(t, root)
if cfg.SchemaVersion != 2 {
t.Fatalf("schema mismatch: %d", cfg.SchemaVersion)
}
if cfg.LayoutDir != "custom" {
t.Fatalf("layout mismatch: %s", cfg.LayoutDir)
}
if cfg.Strategy != "git-clone" {
t.Fatalf("strategy mismatch: %s", cfg.Strategy)
}
}

func TestValidateFixtureLayoutDir(t *testing.T) {
t.Parallel()
root := t.TempDir()
absUnderRoot := filepath.Join(root, "abs-layout")
if err := os.MkdirAll(absUnderRoot, 0o755); err != nil {
t.Fatal(err)
}
bad := []string{
"..",
"../escape",
"nested/../../../escape",
absUnderRoot,
}
for _, dir := range bad {
if err := validateFixtureLayoutDir(dir); err == nil {
t.Errorf("validateFixtureLayoutDir(%q): want error, got nil", dir)
}
}
good := []string{"", "repos", "nested", "nested/../repos"}
for _, dir := range good {
if err := validateFixtureLayoutDir(dir); err != nil {
t.Errorf("validateFixtureLayoutDir(%q): %v", dir, err)
}
}
}

func TestReadUSBVolumeConfigBytes_roundTrip(t *testing.T) {
t.Parallel()
input := "schema_version = 2\nlayout_dir = \"custom\"\nstrategy = \"git-clone\"\ncreated_at = \"2020-01-02T15:04:05Z\"\n"
cfg, err := readUSBVolumeConfigBytes([]byte(input))
if err != nil {
t.Fatal(err)
}
if cfg.SchemaVersion != 2 {
t.Fatalf("schema: got %d", cfg.SchemaVersion)
}
if cfg.LayoutDir != "custom" {
t.Fatalf("layout: got %q", cfg.LayoutDir)
}
if cfg.Strategy != "git-clone" {
t.Fatalf("strategy: got %q", cfg.Strategy)
}
if cfg.CreatedAt.IsZero() {
t.Fatal("created_at: zero")
}
}

func TestReadUSBVolumeConfigBytes_errors(t *testing.T) {
t.Parallel()
cases := []struct {
name, content, wantSubstring string
}{
{
name: "invalid_schema_version",
content: "schema_version = notint\n",
wantSubstring: "schema_version",
},
{
name: "layout_dir_escape",
content: "layout_dir = ../x\n",
wantSubstring: "layout_dir",
},
{
name: "invalid_created_at",
content: "created_at = not-a-date\n",
wantSubstring: "created_at",
},
{
name: "empty_created_at",
content: "created_at = \n",
wantSubstring: "created_at",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
_, err := readUSBVolumeConfigBytes([]byte(tc.content))
if err == nil {
t.Fatal("expected error")
}
if !strings.Contains(err.Error(), tc.wantSubstring) {
t.Fatalf("error %q does not contain %q", err.Error(), tc.wantSubstring)
}
})
}
}

func TestFileURLForPath(t *testing.T) {
root := t.TempDir()
got := FileURLForPath(t, root)
parsed, err := url.Parse(got)
if err != nil {
t.Fatalf("parse URL: %v", err)
}
if parsed.Scheme != "file" {
t.Fatalf("scheme %q, want file", parsed.Scheme)
}
if parsed.Path == "" || parsed.Path[0] != '/' {
t.Fatalf("expected absolute path in URL, got path=%q for %q", parsed.Path, got)
}
if !strings.HasPrefix(got, "file:///") {
t.Fatalf("expected canonical file URL with empty authority (file:///...), got %q", got)
}
}
Loading