Skip to content

Commit d8ffcd3

Browse files
authored
Merge pull request #60 from go-git/conformance-tests
conformance: expand the upstream-test gate from t2008+t5308 to five tests / 42 cases
2 parents 286484d + d7f4071 commit d8ffcd3

70 files changed

Lines changed: 4912 additions & 162 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.entire/settings.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"enabled": true,
3+
"telemetry": true,
4+
"strategy_options": {
5+
"checkpoint_remote": {
6+
"provider": "github",
7+
"repo": "go-git/entire"
8+
}
9+
}
10+
}

.github/workflows/build.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ jobs:
5858
shell: bash
5959
env:
6060
GO_GIT_REF: ${{ inputs.go_git_ref }}
61+
CONFORMANCE_VERBOSE: "1"
6162
run: make conformance
6263
- name: Upload TAP results
6364
if: failure()

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ $(GOLANGCI):
1010

1111
.PHONY: build
1212
build:
13-
go build -o build/ ./...
13+
go build -o build/bin/ ./...
1414

1515
validate: validate-lint validate-dirty
1616

README.md

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,24 @@
22

33
This provides a Go Git CLI binary using `go-git`.
44

5+
## Purpose
6+
7+
The CLI exposes key features from `go-git` so it can be assessed and tested
8+
against upstream Git on a like-for-like basis. Each subcommand is a thin
9+
wrapper over a `go-git` API, so the behaviour exercised by `gogit <command>`
10+
is the behaviour that `go-git` consumers will see.
11+
12+
## Bridging gaps in `go-git`
13+
14+
When upstream Git tests require a feature that `go-git` does not yet
15+
expose, the gap is filled temporarily in `internal/<package>/`. This
16+
directory is a staging area whose layout mirrors the eventual go-git
17+
home (e.g. `internal/plumbing/format/idxfile` is shaped for upstream's
18+
`plumbing/format/idxfile`). Not everything that lives here belongs in
19+
`go-git` — some bits are test-only shims — but the layering keeps
20+
upstream tests that exercise the bulk of `go-git` unblocked while the
21+
upstreaming conversation is in progress.
22+
523
## Installation
624

725
```bash
@@ -14,7 +32,41 @@ go install github.com/go-git/cli/cmd/gogit
1432
gogit <command> [flags]
1533
```
1634

35+
## Conformance testing
36+
37+
`make conformance` runs a curated set of upstream Git tests against
38+
`gogit`, using upstream's own `t/` harness. The runner
39+
(`conformance/run.sh`) builds `gogit`, symlinks it as `git` in a
40+
staging directory, and points `GIT_TEST_INSTALLED` there so the
41+
upstream framework picks our binary up unchanged.
42+
43+
The upstream test source comes from either:
44+
45+
- A local checkout pointed at by `$GIT_SRC` (e.g.
46+
`GIT_SRC=$HOME/git/go-git/git make conformance`), or
47+
- A shallow clone of `github.com/git/git`'s default branch, re-fetched
48+
on every run so upstream behaviour drift surfaces immediately.
49+
50+
The set of curated tests is `conformance/tests.txt` — one filename per
51+
line, `#` comments are ignored:
52+
53+
```
54+
t2008-checkout-subdir.sh
55+
t5308-pack-detect-duplicates.sh
56+
t5325-reverse-index.sh
57+
```
58+
59+
Individual tests can be run directly:
60+
61+
```bash
62+
./conformance/run.sh t2008-checkout-subdir.sh # one test, all cases
63+
./conformance/run.sh t2008-checkout-subdir.sh 1,3 # one test, selected cases
64+
```
65+
66+
`$GO_GIT_REF` (commit SHA, tag, or branch) overrides the `go.mod` pin
67+
for a single run so the gate can be evaluated against an in-flight
68+
`go-git` change.
69+
1770
## License
1871

1972
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
20-

cmd/gogit-test-tool/delta.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package main
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"os"
7+
8+
"github.com/go-git/go-git/v6/plumbing/format/packfile"
9+
)
10+
11+
// runDelta implements `test-tool delta {-d|-p} <base> <delta> <out>`. We only
12+
// need -p (apply); upstream's -d (compute) is not used by the curated tests.
13+
func runDelta(args []string) error {
14+
if len(args) != 4 || args[0] != "-p" {
15+
return errors.New("usage: delta -p <base> <delta> <output>")
16+
}
17+
18+
base, err := os.ReadFile(args[1])
19+
if err != nil {
20+
return fmt.Errorf("read base %s: %w", args[1], err)
21+
}
22+
23+
delta, err := os.ReadFile(args[2])
24+
if err != nil {
25+
return fmt.Errorf("read delta %s: %w", args[2], err)
26+
}
27+
28+
result, err := packfile.PatchDelta(base, delta)
29+
if err != nil {
30+
return fmt.Errorf("apply delta: %w", err)
31+
}
32+
33+
if err := os.WriteFile(args[3], result, 0o644); err != nil {
34+
return fmt.Errorf("write %s: %w", args[3], err)
35+
}
36+
37+
return nil
38+
}

cmd/gogit-test-tool/delta_test.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package main
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"testing"
7+
8+
"github.com/go-git/go-git/v6/plumbing/format/packfile"
9+
)
10+
11+
func TestDeltaApplyRoundTrip(t *testing.T) {
12+
t.Parallel()
13+
dir := t.TempDir()
14+
15+
src := []byte("hello, world\nthis is the base text\n")
16+
tgt := []byte("hello, world\nthis is the target text with edits\n")
17+
18+
deltaBytes := packfile.DiffDelta(src, tgt)
19+
20+
srcPath := filepath.Join(dir, "src")
21+
deltaPath := filepath.Join(dir, "delta")
22+
outPath := filepath.Join(dir, "out")
23+
24+
if err := os.WriteFile(srcPath, src, 0o644); err != nil {
25+
t.Fatal(err)
26+
}
27+
28+
if err := os.WriteFile(deltaPath, deltaBytes, 0o644); err != nil {
29+
t.Fatal(err)
30+
}
31+
32+
if err := runDelta([]string{"-p", srcPath, deltaPath, outPath}); err != nil {
33+
t.Fatalf("runDelta: %v", err)
34+
}
35+
36+
got, err := os.ReadFile(outPath)
37+
if err != nil {
38+
t.Fatal(err)
39+
}
40+
41+
if string(got) != string(tgt) {
42+
t.Fatalf("round-trip mismatch:\n got: %q\nwant: %q", got, tgt)
43+
}
44+
}
45+
46+
func TestDeltaApplyRejectsCorrupt(t *testing.T) {
47+
t.Parallel()
48+
dir := t.TempDir()
49+
50+
src := []byte("base")
51+
corruptDelta := []byte{0xff, 0xff, 0xff} // not a valid delta header
52+
53+
srcPath := filepath.Join(dir, "src")
54+
deltaPath := filepath.Join(dir, "delta")
55+
outPath := filepath.Join(dir, "out")
56+
57+
if err := os.WriteFile(srcPath, src, 0o644); err != nil {
58+
t.Fatal(err)
59+
}
60+
61+
if err := os.WriteFile(deltaPath, corruptDelta, 0o644); err != nil {
62+
t.Fatal(err)
63+
}
64+
65+
if err := runDelta([]string{"-p", srcPath, deltaPath, outPath}); err == nil {
66+
t.Fatal("expected non-nil error on corrupt delta")
67+
}
68+
}

cmd/gogit-test-tool/genrandom.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package main
2+
3+
import (
4+
"bufio"
5+
"errors"
6+
"fmt"
7+
"io"
8+
"strconv"
9+
)
10+
11+
// genRandom emits length deterministic bytes derived from seed, byte-for-byte
12+
// equivalent to upstream's t/helper/test-genrandom.c. The seed loop in C is
13+
// `do { next = next*11 + *c; } while (*c++);` — the body runs once per byte
14+
// INCLUDING the terminating NUL. The PRNG is the POSIX.1-2001 rand() LCG:
15+
// next = next*1103515245 + 12345; output byte = (next>>16) & 0xff. All
16+
// arithmetic is uint64 to match `unsigned long` on 64-bit hosts.
17+
func genRandom(w io.Writer, seed string, length uint64) error {
18+
var next uint64
19+
20+
for _, c := range []byte(seed) {
21+
next = next*11 + uint64(c)
22+
}
23+
// Fold the implicit NUL terminator (matching C do/while semantics).
24+
next *= 11
25+
26+
bw := bufio.NewWriter(w)
27+
defer bw.Flush()
28+
29+
for range length {
30+
next = next*1103515245 + 12345
31+
32+
if err := bw.WriteByte(byte((next >> 16) & 0xff)); err != nil {
33+
return err
34+
}
35+
}
36+
37+
return nil
38+
}
39+
40+
func runGenRandom(args []string) error {
41+
if len(args) < 1 || len(args) > 2 {
42+
return errors.New("usage: genrandom <seed_string> [<size>]")
43+
}
44+
45+
seed := args[0]
46+
length := ^uint64(0) // ULONG_MAX equivalent
47+
48+
if len(args) == 2 {
49+
v, err := strconv.ParseUint(args[1], 10, 64)
50+
if err != nil {
51+
return fmt.Errorf("cannot parse size %q: %w", args[1], err)
52+
}
53+
54+
length = v
55+
}
56+
57+
return genRandom(stdoutWriter(), seed, length)
58+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"testing"
6+
)
7+
8+
func TestGenRandomMatchesUpstream(t *testing.T) {
9+
t.Parallel()
10+
11+
var buf bytes.Buffer
12+
13+
if err := genRandom(&buf, "foo", 8); err != nil {
14+
t.Fatalf("genRandom: %v", err)
15+
}
16+
17+
// Golden bytes locked in from genRandom("foo", 8) output and verified
18+
// against a hand-trace of the algorithm for the first two bytes.
19+
want := []byte{0xd3, 0x1c, 0x75, 0x5b, 0xc4, 0x0f, 0x9d, 0xd0}
20+
21+
if !bytes.Equal(buf.Bytes(), want) {
22+
t.Fatalf("genRandom(\"foo\", 8) = %x; want %x", buf.Bytes(), want)
23+
}
24+
}
25+
26+
func TestGenRandomDeterministic(t *testing.T) {
27+
t.Parallel()
28+
29+
var a, b bytes.Buffer
30+
31+
if err := genRandom(&a, "seed", 64); err != nil {
32+
t.Fatalf("first: %v", err)
33+
}
34+
35+
if err := genRandom(&b, "seed", 64); err != nil {
36+
t.Fatalf("second: %v", err)
37+
}
38+
39+
if !bytes.Equal(a.Bytes(), b.Bytes()) {
40+
t.Fatal("two invocations with same seed/length differ")
41+
}
42+
43+
if a.Len() != 64 {
44+
t.Fatalf("length = %d want 64", a.Len())
45+
}
46+
}

cmd/gogit-test-tool/main.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Package main is the gogit-test-tool helper: a drop-in for upstream's
2+
// t/helper/test-tool used during conformance runs. Subcommands implemented
3+
// here mirror the upstream subcommands the curated tests actually exercise.
4+
package main
5+
6+
import (
7+
"errors"
8+
"fmt"
9+
"io"
10+
"os"
11+
)
12+
13+
func main() {
14+
if err := run(os.Args[1:]); err != nil {
15+
fmt.Fprintln(os.Stderr, err)
16+
os.Exit(1)
17+
}
18+
}
19+
20+
func run(args []string) error {
21+
if len(args) == 0 {
22+
return errors.New("usage: gogit-test-tool <subcommand> [args...]")
23+
}
24+
25+
sub, rest := args[0], args[1:]
26+
27+
switch sub {
28+
case "genrandom":
29+
return runGenRandom(rest)
30+
case "delta":
31+
return runDelta(rest)
32+
case "sha1":
33+
return runSHA1(rest)
34+
case "sha256":
35+
return runSHA256(rest)
36+
case "date":
37+
return runDate(rest)
38+
case "path-utils":
39+
return runPathUtils(rest)
40+
case "env-helper":
41+
return runEnvHelper(rest)
42+
default:
43+
return fmt.Errorf("unimplemented subcommand: %s", sub)
44+
}
45+
}
46+
47+
// stdoutWriter returns the writer that subcommands emit raw output to.
48+
// Indirected so tests can drop a buffer in if needed (not done in v1).
49+
func stdoutWriter() io.Writer {
50+
return os.Stdout
51+
}

0 commit comments

Comments
 (0)