| 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "os" |
| 6 | "path/filepath" |
| 7 | "slices" |
| 8 | "strings" |
| 9 | "testing" |
| 10 | ) |
| 11 | |
| 12 | func gitScopeRepo(t *testing.T) string { |
| 13 | t.Helper() |
| 14 | root := t.TempDir() |
| 15 | for _, args := range [][]string{ |
| 16 | {"init", "-b", "main"}, |
| 17 | {"-c", "user.name=Test", "-c", "user.email=test@example.invalid", "commit", "--allow-empty", "-m", "init"}, |
| 18 | {"branch", "shared"}, |
| 19 | } { |
| 20 | if out, err := workspaceGit(append([]string{"-C", root}, args...)...).CombinedOutput(); err != nil { |
| 21 | t.Fatalf("git fixture: %v: %s", err, out) |
| 22 | } |
| 23 | } |
| 24 | return root |
| 25 | } |
| 26 | |
| 27 | func TestWorkspaceGitBranchScope(t *testing.T) { |
| 28 | rootA, rootB := gitScopeRepo(t), gitScopeRepo(t) |
| 29 | a := &App{tabs: map[string]*WorkspaceTab{ |
| 30 | "a": {WorkspaceRoot: rootA}, "b": {WorkspaceRoot: rootB}, |
| 31 | }} |
| 32 | // The requested tab owns the target even when it is not active. |
| 33 | list, err := a.GitBranchesForTab("a", rootA) |
| 34 | if err != nil || !slices.Contains(list, "shared") { |
| 35 | t.Fatalf("branches: %v, %v", list, err) |
| 36 | } |
| 37 | base, err := a.gitWorkspaceBaseForTab("a", rootA) |
| 38 | if err != nil { |
| 39 | t.Fatal(err) |
| 40 | } |
| 41 | a.tabs["a"].WorkspaceRoot = rootB |
| 42 | if err := a.GitCheckoutForTab("a", rootA, "shared"); err == nil { |
| 43 | t.Fatal("accepted stale workspace identity") |
| 44 | } |
| 45 | if err := workspaceCheckoutBranch(base, "shared", false); err != nil { |
| 46 | t.Fatal(err) |
| 47 | } |
| 48 | if workspaceGitBranch(rootA) != "shared" || workspaceGitBranch(rootB) != "main" { |
| 49 | t.Fatal("checkout changed the replacement workspace") |
| 50 | } |
| 51 | if err := a.GitCreateBranchForTab("b", rootB, "new-branch"); err != nil { |
| 52 | t.Fatal(err) |
| 53 | } |
| 54 | if workspaceGitBranch(rootA) != "shared" || workspaceGitBranch(rootB) != "new-branch" { |
| 55 | t.Fatal("create crossed project boundaries") |
| 56 | } |
| 57 | if err := a.GitCheckoutForTab("a", rootB, "--detach"); err == nil { |
| 58 | t.Fatal("accepted option as branch") |
| 59 | } |
| 60 | for _, id := range []string{"", "missing", "relative", "deleted"} { |
| 61 | a.tabs["relative"] = &WorkspaceTab{WorkspaceRoot: "."} |
| 62 | a.tabs["deleted"] = &WorkspaceTab{WorkspaceRoot: filepath.Join(t.TempDir(), "gone")} |
| 63 | if _, err := a.GitBranchesForTab(id, rootA); err == nil { |
| 64 | t.Fatalf("accepted %q", id) |
| 65 | } |
| 66 | if err := a.GitCheckoutForTab(id, rootA, "shared"); err == nil { |
| 67 | t.Fatalf("checkout accepted %q", id) |
| 68 | } |
| 69 | if err := a.GitCreateBranchForTab(id, rootA, "new"); err == nil { |
| 70 | t.Fatalf("create accepted %q", id) |
| 71 | } |
| 72 | } |
| 73 | } |
| 74 | |
| 75 | func TestWorkspaceTallyBudgetAndFileKinds(t *testing.T) { |
| 76 | base := gitScopeRepo(t) |
| 77 | root, err := os.OpenRoot(base) |
| 78 | if err != nil { |
| 79 | t.Fatal(err) |
| 80 | } |
| 81 | defer root.Close() |
| 82 | for _, fixture := range []struct { |
| 83 | name, body string |
| 84 | want int |
| 85 | partial bool |
| 86 | }{ |
| 87 | {"empty", "", 0, false}, {"text", "one\ntwo", 2, false}, |
| 88 | {"binary", "one\n\x00two", 0, false}, |
| 89 | {"large", strings.Repeat("x\n", workspaceDiffTallyFileLimit/2) + "tail", workspaceDiffTallyFileLimit / 2, true}, |
| 90 | } { |
| 91 | if err := root.WriteFile(fixture.name, []byte(fixture.body), 0600); err != nil { |
| 92 | t.Fatal(err) |
| 93 | } |
| 94 | remaining := int64(workspaceDiffTallyReadLimit) |
| 95 | count, partial := workspaceCountFileLines(context.Background(), root, fixture.name, &remaining) |
| 96 | if count != fixture.want || partial != fixture.partial || remaining < workspaceDiffTallyReadLimit-workspaceDiffTallyFileLimit { |
| 97 | t.Fatalf("%s: count=%d partial=%v remaining=%d", fixture.name, count, partial, remaining) |
| 98 | } |
| 99 | } |
| 100 | for _, path := range []string{".git", "../outside", "missing"} { |
| 101 | remaining := int64(100) |
| 102 | if n, partial := workspaceCountFileLines(context.Background(), root, path, &remaining); n != 0 || !partial || remaining != 100 { |
| 103 | t.Fatalf("read nonregular path %s", path) |
| 104 | } |
| 105 | } |
| 106 | if err := root.Symlink("text", "link"); err == nil { |
| 107 | remaining := int64(100) |
| 108 | if n, partial := workspaceCountFileLines(context.Background(), root, "link", &remaining); n != 0 || !partial || remaining != 100 { |
| 109 | t.Fatal("followed symlink") |
| 110 | } |
| 111 | if f, err := openWorkspaceTallyFile(root, "link"); err == nil { |
| 112 | f.Close() |
| 113 | t.Fatal("platform open followed symlink") |
| 114 | } |
| 115 | } |
| 116 | paths := make([]string, 17) |
| 117 | for i := range paths { |
| 118 | paths[i] = "large" |
| 119 | } |
| 120 | added, _, partial := workspaceGitDiffTally(context.Background(), base, paths) |
| 121 | if added != workspaceDiffTallyReadLimit/2 || !partial { |
| 122 | t.Fatalf("aggregate budget: %d, %v", added, partial) |
| 123 | } |
| 124 | } |
| 125 | |
| 126 | func TestWorkspaceChangesExpiredDeadlineIsIncomplete(t *testing.T) { |
| 127 | base := gitScopeRepo(t) |
| 128 | a := &App{tabs: map[string]*WorkspaceTab{"a": {WorkspaceRoot: base}}} |
| 129 | ctx, cancel := context.WithCancel(context.Background()) |
| 130 | cancel() |
| 131 | view := a.workspaceChanges(ctx, "a") |
| 132 | if view.GitAvailable || !view.Incomplete || view.GitErr == "" { |
| 133 | t.Fatalf("expired scan: %+v", view) |
| 134 | } |
| 135 | root, err := os.OpenRoot(base) |
| 136 | if err != nil { |
| 137 | t.Fatal(err) |
| 138 | } |
| 139 | defer root.Close() |
| 140 | remaining := int64(100) |
| 141 | if _, partial := workspaceCountFileLines(ctx, root, "anything", &remaining); !partial || remaining != 100 { |
| 142 | t.Fatal("read after deadline") |
| 143 | } |
| 144 | } |
| 145 |