|
1 | 1 | package driver |
2 | 2 |
|
3 | 3 | import ( |
| 4 | + "context" |
| 5 | + "errors" |
| 6 | + "fmt" |
| 7 | + "os" |
| 8 | + "os/exec" |
| 9 | + "path/filepath" |
4 | 10 | "strings" |
5 | 11 | "testing" |
6 | 12 | ) |
7 | 13 |
|
| 14 | +// fakeBinDir holds the directory containing the built fakegt binary. |
| 15 | +// Set by TestMain before any tests run. |
| 16 | +var fakeBinDir string |
| 17 | + |
| 18 | +func TestMain(m *testing.M) { |
| 19 | + // Build fakegt and prepend its directory to PATH so that |
| 20 | + // exec.LookPath("gt") and runGT find our test double. |
| 21 | + dir, err := os.MkdirTemp("", "fakegt-*") |
| 22 | + if err != nil { |
| 23 | + fmt.Fprintf(os.Stderr, "creating temp dir: %v\n", err) |
| 24 | + os.Exit(1) |
| 25 | + } |
| 26 | + defer os.RemoveAll(dir) |
| 27 | + |
| 28 | + gtBin := filepath.Join(dir, "gt") |
| 29 | + cmd := exec.Command("go", "build", "-o", gtBin, "./testdata/fakegt") |
| 30 | + cmd.Stderr = os.Stderr |
| 31 | + if err := cmd.Run(); err != nil { |
| 32 | + fmt.Fprintf(os.Stderr, "building fakegt: %v\n", err) |
| 33 | + os.Exit(1) |
| 34 | + } |
| 35 | + |
| 36 | + fakeBinDir = dir |
| 37 | + os.Setenv("PATH", dir+":"+os.Getenv("PATH")) |
| 38 | + |
| 39 | + os.Exit(m.Run()) |
| 40 | +} |
| 41 | + |
| 42 | +// initGitRepo creates a temp git repo with an initial commit, chdir's into it, |
| 43 | +// and restores the original directory on cleanup. This is needed because the |
| 44 | +// git package operates on the current working directory. |
| 45 | +func initGitRepo(t *testing.T) (dir string, ctx context.Context) { |
| 46 | + t.Helper() |
| 47 | + dir = t.TempDir() |
| 48 | + ctx = context.Background() |
| 49 | + |
| 50 | + gitEnv := []string{ |
| 51 | + "GIT_AUTHOR_NAME=Test User", |
| 52 | + "GIT_AUTHOR_EMAIL=test@example.com", |
| 53 | + "GIT_COMMITTER_NAME=Test User", |
| 54 | + "GIT_COMMITTER_EMAIL=test@example.com", |
| 55 | + "GIT_CONFIG_NOSYSTEM=1", |
| 56 | + "HOME=" + dir, |
| 57 | + } |
| 58 | + |
| 59 | + gitCmd := func(args ...string) { |
| 60 | + t.Helper() |
| 61 | + cmd := exec.Command("git", args...) |
| 62 | + cmd.Dir = dir |
| 63 | + cmd.Env = append(os.Environ(), gitEnv...) |
| 64 | + out, err := cmd.CombinedOutput() |
| 65 | + if err != nil { |
| 66 | + t.Fatalf("setup git %s: %s\n%s", strings.Join(args, " "), err, out) |
| 67 | + } |
| 68 | + } |
| 69 | + |
| 70 | + gitCmd("init", "-b", "main") |
| 71 | + gitCmd("commit", "--allow-empty", "-m", "init") |
| 72 | + |
| 73 | + origDir, err := os.Getwd() |
| 74 | + if err != nil { |
| 75 | + t.Fatal(err) |
| 76 | + } |
| 77 | + if err := os.Chdir(dir); err != nil { |
| 78 | + t.Fatal(err) |
| 79 | + } |
| 80 | + t.Cleanup(func() { _ = os.Chdir(origDir) }) |
| 81 | + |
| 82 | + for _, e := range gitEnv { |
| 83 | + parts := strings.SplitN(e, "=", 2) |
| 84 | + t.Setenv(parts[0], parts[1]) |
| 85 | + } |
| 86 | + |
| 87 | + return dir, ctx |
| 88 | +} |
| 89 | + |
| 90 | +// --- Unit tests for parseSubmitResult --- |
| 91 | + |
8 | 92 | func TestParseSubmitResult(t *testing.T) { |
9 | 93 | tests := []struct { |
10 | 94 | name string |
@@ -81,3 +165,208 @@ pp--06-14-part_3: https://app.graphite.com/github/pr/withgraphite/repo/102 (crea |
81 | 165 | }) |
82 | 166 | } |
83 | 167 | } |
| 168 | + |
| 169 | +// --- Integration tests using fakegt + temp git repos --- |
| 170 | + |
| 171 | +func TestNewGraphiteWithFakeGT(t *testing.T) { |
| 172 | + // fakegt is on PATH via TestMain, so NewGraphite should succeed. |
| 173 | + g, err := NewGraphite() |
| 174 | + if err != nil { |
| 175 | + t.Fatalf("NewGraphite() with fakegt on PATH: %v", err) |
| 176 | + } |
| 177 | + if g.Name() != "graphite" { |
| 178 | + t.Errorf("Name() = %q, want %q", g.Name(), "graphite") |
| 179 | + } |
| 180 | +} |
| 181 | + |
| 182 | +func TestGraphiteCreateBranch(t *testing.T) { |
| 183 | + _, ctx := initGitRepo(t) |
| 184 | + |
| 185 | + g := &Graphite{} |
| 186 | + |
| 187 | + // CreateBranch checks out the parent (real git) then calls gt create (fakegt). |
| 188 | + if err := g.CreateBranch(ctx, "my-feature", "main"); err != nil { |
| 189 | + t.Fatalf("CreateBranch: %v", err) |
| 190 | + } |
| 191 | +} |
| 192 | + |
| 193 | +func TestGraphitePush(t *testing.T) { |
| 194 | + tests := []struct { |
| 195 | + name string |
| 196 | + submitOut string |
| 197 | + branch string |
| 198 | + opts PushOpts |
| 199 | + wantPR int |
| 200 | + wantCreated bool |
| 201 | + wantErr string |
| 202 | + }{ |
| 203 | + { |
| 204 | + name: "new PR created", |
| 205 | + submitOut: "feat-a: https://app.graphite.com/github/pr/owner/repo/77 (created)", |
| 206 | + branch: "feat-a", |
| 207 | + opts: PushOpts{ |
| 208 | + Branch: "feat-a", |
| 209 | + Base: "main", |
| 210 | + Title: "Add feature A", |
| 211 | + }, |
| 212 | + wantPR: 77, |
| 213 | + wantCreated: true, |
| 214 | + }, |
| 215 | + { |
| 216 | + name: "existing PR updated by gt", |
| 217 | + submitOut: "feat-b: https://app.graphite.com/github/pr/owner/repo/88 (updated)", |
| 218 | + branch: "feat-b", |
| 219 | + opts: PushOpts{ |
| 220 | + Branch: "feat-b", |
| 221 | + Base: "main", |
| 222 | + }, |
| 223 | + wantPR: 88, |
| 224 | + wantCreated: false, |
| 225 | + }, |
| 226 | + { |
| 227 | + name: "existing PR via ExistingPR field", |
| 228 | + submitOut: "feat-c: https://app.graphite.com/github/pr/owner/repo/99 (updated)", |
| 229 | + branch: "feat-c", |
| 230 | + opts: PushOpts{ |
| 231 | + Branch: "feat-c", |
| 232 | + Base: "main", |
| 233 | + ExistingPR: intPtr(55), |
| 234 | + }, |
| 235 | + wantPR: 55, |
| 236 | + wantCreated: false, |
| 237 | + }, |
| 238 | + } |
| 239 | + |
| 240 | + for _, tt := range tests { |
| 241 | + t.Run(tt.name, func(t *testing.T) { |
| 242 | + t.Setenv("FAKEGT_SUBMIT_OUTPUT", tt.submitOut) |
| 243 | + ctx := context.Background() |
| 244 | + g := &Graphite{} |
| 245 | + |
| 246 | + result, err := g.Push(ctx, tt.opts) |
| 247 | + if tt.wantErr != "" { |
| 248 | + if err == nil { |
| 249 | + t.Fatalf("expected error containing %q, got nil", tt.wantErr) |
| 250 | + } |
| 251 | + if !strings.Contains(err.Error(), tt.wantErr) { |
| 252 | + t.Errorf("error = %q, want containing %q", err.Error(), tt.wantErr) |
| 253 | + } |
| 254 | + return |
| 255 | + } |
| 256 | + if err != nil { |
| 257 | + t.Fatalf("Push: %v", err) |
| 258 | + } |
| 259 | + if result.PRNumber != tt.wantPR { |
| 260 | + t.Errorf("PRNumber = %d, want %d", result.PRNumber, tt.wantPR) |
| 261 | + } |
| 262 | + if result.Created != tt.wantCreated { |
| 263 | + t.Errorf("Created = %v, want %v", result.Created, tt.wantCreated) |
| 264 | + } |
| 265 | + }) |
| 266 | + } |
| 267 | +} |
| 268 | + |
| 269 | +func TestGraphitePushPassesBodyAndTitle(t *testing.T) { |
| 270 | + // Verify that --title and --description flags are passed to gt submit. |
| 271 | + recordFile := filepath.Join(t.TempDir(), "record.txt") |
| 272 | + t.Setenv("FAKEGT_RECORD", recordFile) |
| 273 | + t.Setenv("FAKEGT_SUBMIT_OUTPUT", "my-feat: https://app.graphite.com/github/pr/o/r/1 (created)") |
| 274 | + |
| 275 | + ctx := context.Background() |
| 276 | + g := &Graphite{} |
| 277 | + |
| 278 | + _, err := g.Push(ctx, PushOpts{ |
| 279 | + Branch: "my-feat", |
| 280 | + Base: "main", |
| 281 | + Title: "My title", |
| 282 | + Body: "My description", |
| 283 | + }) |
| 284 | + if err != nil { |
| 285 | + t.Fatalf("Push: %v", err) |
| 286 | + } |
| 287 | + |
| 288 | + recorded, err := os.ReadFile(recordFile) |
| 289 | + if err != nil { |
| 290 | + t.Fatalf("reading record file: %v", err) |
| 291 | + } |
| 292 | + args := string(recorded) |
| 293 | + |
| 294 | + if !strings.Contains(args, "--title My title") { |
| 295 | + t.Errorf("expected --title in args, got: %s", args) |
| 296 | + } |
| 297 | + if !strings.Contains(args, "--description My description") { |
| 298 | + t.Errorf("expected --description in args, got: %s", args) |
| 299 | + } |
| 300 | +} |
| 301 | + |
| 302 | +func TestGraphitePushDraft(t *testing.T) { |
| 303 | + recordFile := filepath.Join(t.TempDir(), "record.txt") |
| 304 | + t.Setenv("FAKEGT_RECORD", recordFile) |
| 305 | + t.Setenv("FAKEGT_SUBMIT_OUTPUT", "my-feat: https://app.graphite.com/github/pr/o/r/1 (created)") |
| 306 | + |
| 307 | + ctx := context.Background() |
| 308 | + g := &Graphite{} |
| 309 | + |
| 310 | + _, err := g.Push(ctx, PushOpts{ |
| 311 | + Branch: "my-feat", |
| 312 | + Base: "main", |
| 313 | + Title: "Draft PR", |
| 314 | + Draft: true, |
| 315 | + }) |
| 316 | + if err != nil { |
| 317 | + t.Fatalf("Push: %v", err) |
| 318 | + } |
| 319 | + |
| 320 | + recorded, err := os.ReadFile(recordFile) |
| 321 | + if err != nil { |
| 322 | + t.Fatalf("reading record file: %v", err) |
| 323 | + } |
| 324 | + if !strings.Contains(string(recorded), "--draft") { |
| 325 | + t.Errorf("expected --draft in args, got: %s", recorded) |
| 326 | + } |
| 327 | +} |
| 328 | + |
| 329 | +func TestGraphitePushFailure(t *testing.T) { |
| 330 | + t.Setenv("FAKEGT_FAIL", "1") |
| 331 | + ctx := context.Background() |
| 332 | + g := &Graphite{} |
| 333 | + |
| 334 | + _, err := g.Push(ctx, PushOpts{Branch: "feat", Base: "main"}) |
| 335 | + if err == nil { |
| 336 | + t.Fatal("expected error when gt submit fails") |
| 337 | + } |
| 338 | + if !strings.Contains(err.Error(), "gt submit") { |
| 339 | + t.Errorf("error = %q, want containing 'gt submit'", err.Error()) |
| 340 | + } |
| 341 | +} |
| 342 | + |
| 343 | +func TestGraphiteRebase(t *testing.T) { |
| 344 | + ctx := context.Background() |
| 345 | + g := &Graphite{} |
| 346 | + |
| 347 | + // Normal restack succeeds. |
| 348 | + if err := g.Rebase(ctx, "main", "feature"); err != nil { |
| 349 | + t.Fatalf("Rebase: %v", err) |
| 350 | + } |
| 351 | +} |
| 352 | + |
| 353 | +func TestGraphiteRebaseConflict(t *testing.T) { |
| 354 | + t.Setenv("FAKEGT_CONFLICT", "1") |
| 355 | + ctx := context.Background() |
| 356 | + g := &Graphite{} |
| 357 | + |
| 358 | + err := g.Rebase(ctx, "main", "feature") |
| 359 | + if err == nil { |
| 360 | + t.Fatal("expected error on conflict") |
| 361 | + } |
| 362 | + |
| 363 | + var conflictErr *RebaseConflictError |
| 364 | + if !errors.As(err, &conflictErr) { |
| 365 | + t.Fatalf("expected RebaseConflictError, got %T: %v", err, err) |
| 366 | + } |
| 367 | + if !strings.Contains(conflictErr.Detail, "CONFLICT") { |
| 368 | + t.Errorf("Detail = %q, want containing 'CONFLICT'", conflictErr.Detail) |
| 369 | + } |
| 370 | +} |
| 371 | + |
| 372 | +func intPtr(n int) *int { return &n } |
0 commit comments