package config_test
import (
"bytes"
"strings"
"testing"
"lindenii.org/go/furgit/config"
"lindenii.org/go/furgit/internal/testgit"
"lindenii.org/go/furgit/object/id"
)
func TestConfig(t *testing.T) {
t.Parallel()
testRepo, err := testgit.NewRepo(t, testgit.RepoOptions{}) //nolint:exhaustruct
if err != nil {
t.Fatalf("NewRepo: %v", err)
}
err = testRepo.ConfigSet(t, "test.enabled", "true")
if err != nil {
t.Fatalf("set test.enabled: %v", err)
}
err = testRepo.ConfigSet(t, "test.mode", "false")
if err != nil {
t.Fatalf("set test.mode: %v", err)
}
err = testRepo.ConfigSet(t, "test.name", "Jane Doe")
if err != nil {
t.Fatalf("set test.name: %v", err)
}
err = testRepo.ConfigSet(t, "test.email", "jane@example.org")
if err != nil {
t.Fatalf("set test.email: %v", err)
}
cfgFile, err := testRepo.Root(t).Open(".git/config")
if err != nil {
t.Fatalf("open config: %v", err)
}
defer func() { _ = cfgFile.Close() }()
cfg, err := config.Parse(cfgFile)
if err != nil {
t.Fatalf("Parse failed: %v", err)
}
got, err := cfg.Lookup("test", "", "enabled").String()
if err != nil {
t.Fatalf("test.enabled: %v", err)
}
if got != "true" {
t.Errorf("test.enabled: got %q, want %q", got, "true")
}
got, err = cfg.Lookup("test", "", "mode").String()
if err != nil {
t.Fatalf("test.mode: %v", err)
}
if got != "false" {
t.Errorf("test.mode: got %q, want %q", got, "false")
}
got, err = cfg.Lookup("test", "", "name").String()
if err != nil {
t.Fatalf("test.name: %v", err)
}
if got != "Jane Doe" {
t.Errorf("test.name: got %q, want %q", got, "Jane Doe")
}
got, err = cfg.Lookup("test", "", "email").String()
if err != nil {
t.Fatalf("test.email: %v", err)
}
if got != "jane@example.org" {
t.Errorf("test.email: got %q, want %q", got, "jane@example.org")
}
}
func TestConfigSubsection(t *testing.T) {
t.Parallel()
testRepo, err := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: id.ObjectFormatSHA256})
if err != nil {
t.Fatalf("NewRepo: %v", err)
}
err = testRepo.ConfigSet(t, "test.origin.url", "https://example.org/repo.git")
if err != nil {
t.Fatalf("set test.origin.url: %v", err)
}
err = testRepo.ConfigSet(t, "test.origin.fetch", "+refs/heads/*:refs/remotes/origin/*")
if err != nil {
t.Fatalf("set test.origin.fetch: %v", err)
}
cfgFile, err := testRepo.Root(t).Open(".git/config")
if err != nil {
t.Fatalf("open config: %v", err)
}
defer func() { _ = cfgFile.Close() }()
cfg, err := config.Parse(cfgFile)
if err != nil {
t.Fatalf("Parse failed: %v", err)
}
got, err := cfg.Lookup("test", "origin", "url").String()
if err != nil {
t.Fatalf("test.origin.url: %v", err)
}
if got != "https://example.org/repo.git" {
t.Errorf("test.origin.url: got %q, want %q", got, "https://example.org/repo.git")
}
got, err = cfg.Lookup("test", "origin", "fetch").String()
if err != nil {
t.Fatalf("test.origin.fetch: %v", err)
}
if got != "+refs/heads/*:refs/remotes/origin/*" {
t.Errorf("test.origin.fetch: got %q, want %q", got, "+refs/heads/*:refs/remotes/origin/*")
}
}
func TestConfigMultiValue(t *testing.T) {
t.Parallel()
testRepo, err := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: id.ObjectFormatSHA256})
if err != nil {
t.Fatalf("NewRepo: %v", err)
}
err = testRepo.ConfigAdd(t, "test.origin.fetch", "+refs/heads/main:refs/remotes/origin/main")
if err != nil {
t.Fatalf("add test.origin.fetch: %v", err)
}
err = testRepo.ConfigAdd(t, "test.origin.fetch", "+refs/heads/dev:refs/remotes/origin/dev")
if err != nil {
t.Fatalf("add test.origin.fetch: %v", err)
}
err = testRepo.ConfigAdd(t, "test.origin.fetch", "+refs/tags/*:refs/tags/*")
if err != nil {
t.Fatalf("add test.origin.fetch: %v", err)
}
cfgFile, err := testRepo.Root(t).Open(".git/config")
if err != nil {
t.Fatalf("open config: %v", err)
}
defer func() { _ = cfgFile.Close() }()
cfg, err := config.Parse(cfgFile)
if err != nil {
t.Fatalf("Parse failed: %v", err)
}
fetches := cfg.LookupAll("test", "origin", "fetch")
if len(fetches) != 3 {
t.Fatalf("expected 3 fetch values, got %d", len(fetches))
}
expected := []string{
"+refs/heads/main:refs/remotes/origin/main",
"+refs/heads/dev:refs/remotes/origin/dev",
"+refs/tags/*:refs/tags/*",
}
for i, want := range expected {
got, err := fetches[i].String()
if err != nil {
t.Fatalf("fetch[%d]: %v", i, err)
}
if got != want {
t.Errorf("fetch[%d]: got %q, want %q", i, got, want)
}
}
}
func TestConfigCaseInsensitive(t *testing.T) {
t.Parallel()
testRepo, err := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: id.ObjectFormatSHA256})
if err != nil {
t.Fatalf("NewRepo: %v", err)
}
err = testRepo.ConfigSet(t, "Test.Flag", "true")
if err != nil {
t.Fatalf("set Test.Flag: %v", err)
}
err = testRepo.ConfigSet(t, "TEST.Mode", "false")
if err != nil {
t.Fatalf("set TEST.Mode: %v", err)
}
gitVerifyFlag, err := testRepo.ConfigGet(t, "test.flag")
if err != nil {
t.Fatalf("get test.flag: %v", err)
}
gitVerifyMode, err := testRepo.ConfigGet(t, "test.mode")
if err != nil {
t.Fatalf("get test.mode: %v", err)
}
gitVerifyFlag = strings.TrimSuffix(gitVerifyFlag, "\n")
gitVerifyMode = strings.TrimSuffix(gitVerifyMode, "\n")
cfgFile, err := testRepo.Root(t).Open(".git/config")
if err != nil {
t.Fatalf("open config: %v", err)
}
defer func() { _ = cfgFile.Close() }()
cfg, err := config.Parse(cfgFile)
if err != nil {
t.Fatalf("Parse failed: %v", err)
}
got, err := cfg.Lookup("test", "", "flag").String()
if err != nil {
t.Fatalf("test.flag: %v", err)
}
if got != gitVerifyFlag {
t.Errorf("test.flag: got %q, want %q (from git)", got, gitVerifyFlag)
}
got, err = cfg.Lookup("TEST", "", "FLAG").String()
if err != nil {
t.Fatalf("TEST.FLAG: %v", err)
}
if got != gitVerifyFlag {
t.Errorf("TEST.FLAG: got %q, want %q (from git)", got, gitVerifyFlag)
}
got, err = cfg.Lookup("test", "", "mode").String()
if err != nil {
t.Fatalf("test.mode: %v", err)
}
if got != gitVerifyMode {
t.Errorf("test.mode: got %q, want %q (from git)", got, gitVerifyMode)
}
}
func TestConfigBoolean(t *testing.T) {
t.Parallel()
testRepo, err := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: id.ObjectFormatSHA256})
if err != nil {
t.Fatalf("NewRepo: %v", err)
}
err = testRepo.ConfigSet(t, "test.flag1", "true")
if err != nil {
t.Fatalf("set test.flag1: %v", err)
}
err = testRepo.ConfigSet(t, "test.flag2", "false")
if err != nil {
t.Fatalf("set test.flag2: %v", err)
}
err = testRepo.ConfigSet(t, "test.flag3", "yes")
if err != nil {
t.Fatalf("set test.flag3: %v", err)
}
err = testRepo.ConfigSet(t, "test.flag4", "no")
if err != nil {
t.Fatalf("set test.flag4: %v", err)
}
cfgFile, err := testRepo.Root(t).Open(".git/config")
if err != nil {
t.Fatalf("open config: %v", err)
}
defer func() { _ = cfgFile.Close() }()
cfg, err := config.Parse(cfgFile)
if err != nil {
t.Fatalf("Parse failed: %v", err)
}
tests := make([]struct {
key string
want string
}, 0, 4)
for _, key := range []string{"flag1", "flag2", "flag3", "flag4"} {
want, err := testRepo.ConfigGet(t, "test."+key)
if err != nil {
t.Fatalf("get test.%s: %v", key, err)
}
tests = append(tests, struct {
key string
want string
}{
key: key,
want: strings.TrimSuffix(want, "\n"),
})
}
for _, tt := range tests {
got, err := cfg.Lookup("test", "", tt.key).String()
if err != nil {
t.Fatalf("test.%s: %v", tt.key, err)
}
if got != tt.want {
t.Errorf("test.%s: got %q, want %q (from git)", tt.key, got, tt.want)
}
}
}
func TestConfigLookupKindsAndBool(t *testing.T) {
t.Parallel()
cfgText := `[test]
novalue
empty =
truthy = yes
numeric = -2
leadspace = " 1"
leadtab = " -2"
ksuffix = 1k
hex = 0x10
maxi32 = 2147483647
toobig = 2147483648
toosmall = -2147483649
badnum = " 2x"
`
cfg, err := config.Parse(strings.NewReader(cfgText))
if err != nil {
t.Fatalf("Parse failed: %v", err)
}
novalue := cfg.Lookup("test", "", "novalue")
if novalue.Kind != config.KindValueless {
t.Fatalf("novalue kind: got %v, want %v", novalue.Kind, config.KindValueless)
}
novalueBool, err := novalue.Bool()
if err != nil || !novalueBool {
t.Fatalf("novalue bool: got (%v, %v), want (true, nil)", novalueBool, err)
}
empty := cfg.Lookup("test", "", "empty")
if empty.Kind != config.KindString || empty.Value != "" {
t.Fatalf("empty: got (%v, %q), want (%v, %q)", empty.Kind, empty.Value, config.KindString, "")
}
emptyBool, err := empty.Bool()
if err != nil || emptyBool {
t.Fatalf("empty bool: got (%v, %v), want (false, nil)", emptyBool, err)
}
truthyBool, err := cfg.Lookup("test", "", "truthy").Bool()
if err != nil || !truthyBool {
t.Fatalf("truthy bool: got (%v, %v), want (true, nil)", truthyBool, err)
}
numericBool, err := cfg.Lookup("test", "", "numeric").Bool()
if err != nil || !numericBool {
t.Fatalf("numeric bool: got (%v, %v), want (true, nil)", numericBool, err)
}
leadspaceBool, err := cfg.Lookup("test", "", "leadspace").Bool()
if err != nil || !leadspaceBool {
t.Fatalf("leadspace bool: got (%v, %v), want (true, nil)", leadspaceBool, err)
}
leadtabBool, err := cfg.Lookup("test", "", "leadtab").Bool()
if err != nil || !leadtabBool {
t.Fatalf("leadtab bool: got (%v, %v), want (true, nil)", leadtabBool, err)
}
ksuffixBool, err := cfg.Lookup("test", "", "ksuffix").Bool()
if err != nil || !ksuffixBool {
t.Fatalf("ksuffix bool: got (%v, %v), want (true, nil)", ksuffixBool, err)
}
maxi32Bool, err := cfg.Lookup("test", "", "maxi32").Bool()
if err != nil || !maxi32Bool {
t.Fatalf("maxi32 bool: got (%v, %v), want (true, nil)", maxi32Bool, err)
}
_, err = cfg.Lookup("test", "", "toobig").Bool()
if err == nil {
t.Fatal("toobig bool: expected error")
}
_, err = cfg.Lookup("test", "", "toosmall").Bool()
if err == nil {
t.Fatal("toosmall bool: expected error")
}
_, err = cfg.Lookup("test", "", "badnum").Bool()
if err == nil {
t.Fatal("badnum bool: expected error")
}
_, err = novalue.String()
if err == nil {
t.Fatal("novalue string: expected error")
}
emptyString, err := empty.String()
if err != nil || emptyString != "" {
t.Fatalf("empty string: got (%q, %v), want (%q, nil)", emptyString, err, "")
}
numericInt, err := cfg.Lookup("test", "", "numeric").Int()
if err != nil || numericInt != -2 {
t.Fatalf("numeric int: got (%v, %v), want (-2, nil)", numericInt, err)
}
ksuffixInt, err := cfg.Lookup("test", "", "ksuffix").Int()
if err != nil || ksuffixInt != 1024 {
t.Fatalf("ksuffix int: got (%v, %v), want (1024, nil)", ksuffixInt, err)
}
hexInt64, err := cfg.Lookup("test", "", "hex").Int64()
if err != nil || hexInt64 != 16 {
t.Fatalf("hex int64: got (%v, %v), want (16, nil)", hexInt64, err)
}
_, err = cfg.Lookup("test", "", "badnum").Int()
if err == nil {
t.Fatal("badnum int: expected error")
}
missing := cfg.Lookup("test", "", "missing")
if missing.Kind != config.KindMissing {
t.Fatalf("missing kind: got %v, want %v", missing.Kind, config.KindMissing)
}
_, err = missing.Bool()
if err == nil {
t.Fatal("missing bool: expected error")
}
_, err = missing.Int()
if err == nil {
t.Fatal("missing int: expected error")
}
_, err = missing.String()
if err == nil {
t.Fatal("missing string: expected error")
}
}
func TestConfigComplexValues(t *testing.T) {
t.Parallel()
testRepo, err := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: id.ObjectFormatSHA256})
if err != nil {
t.Fatalf("NewRepo: %v", err)
}
err = testRepo.ConfigSet(t, "test.spaced", "value with spaces")
if err != nil {
t.Fatalf("set test.spaced: %v", err)
}
err = testRepo.ConfigSet(t, "test.special", "value=with=equals")
if err != nil {
t.Fatalf("set test.special: %v", err)
}
err = testRepo.ConfigSet(t, "test.path", "/path/to/something")
if err != nil {
t.Fatalf("set test.path: %v", err)
}
err = testRepo.ConfigSet(t, "test.number", "12345")
if err != nil {
t.Fatalf("set test.number: %v", err)
}
cfgFile, err := testRepo.Root(t).Open(".git/config")
if err != nil {
t.Fatalf("open config: %v", err)
}
defer func() { _ = cfgFile.Close() }()
cfg, err := config.Parse(cfgFile)
if err != nil {
t.Fatalf("Parse failed: %v", err)
}
tests := []string{"spaced", "special", "path", "number"}
for _, key := range tests {
want, err := testRepo.ConfigGet(t, "test."+key)
if err != nil {
t.Fatalf("get test.%s: %v", key, err)
}
want = strings.TrimSuffix(want, "\n")
got, err := cfg.Lookup("test", "", key).String()
if err != nil {
t.Fatalf("test.%s: %v", key, err)
}
if got != want {
t.Errorf("test.%s: got %q, want %q (from git)", key, got, want)
}
}
}
func TestConfigEntries(t *testing.T) {
t.Parallel()
testRepo, err := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: id.ObjectFormatSHA256})
if err != nil {
t.Fatalf("NewRepo: %v", err)
}
err = testRepo.ConfigSet(t, "test.enabled", "true")
if err != nil {
t.Fatalf("set test.enabled: %v", err)
}
err = testRepo.ConfigSet(t, "test.mode", "false")
if err != nil {
t.Fatalf("set test.mode: %v", err)
}
err = testRepo.ConfigSet(t, "test.name", "Test User")
if err != nil {
t.Fatalf("set test.name: %v", err)
}
cfgFile, err := testRepo.Root(t).Open(".git/config")
if err != nil {
t.Fatalf("open config: %v", err)
}
defer func() { _ = cfgFile.Close() }()
cfg, err := config.Parse(cfgFile)
if err != nil {
t.Fatalf("Parse failed: %v", err)
}
entries := cfg.Entries()
if len(entries) < 3 {
t.Errorf("expected at least 3 entries, got %d", len(entries))
}
for _, entry := range entries {
key := entry.Section + "." + entry.Key
if entry.Subsection != "" {
key = entry.Section + "." + entry.Subsection + "." + entry.Key
}
gitValue, err := testRepo.ConfigGet(t, key)
if err != nil {
t.Fatalf("get %s: %v", key, err)
}
gitValue = strings.TrimSuffix(gitValue, "\n")
if entry.Value != gitValue {
t.Errorf("entry %s: got value %q, git has %q", key, entry.Value, gitValue)
}
}
}
func TestConfigErrorCases(t *testing.T) {
t.Parallel()
tests := []struct {
name string
config string
}{
{
name: "key before section",
config: "bare = true",
},
{
name: "invalid section character",
config: "[core/invalid]",
},
{
name: "unterminated section",
config: "[core",
},
{
name: "unterminated quote",
config: "[core]\n\tbare = \"true",
},
{
name: "invalid escape",
config: "[core]\n\tvalue = \"test\\x\"",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
r := strings.NewReader(tt.config)
_, err := config.Parse(r)
if err == nil {
t.Errorf("expected error for %s", tt.name)
}
})
}
}
func TestConfigRawBytes(t *testing.T) {
t.Parallel()
tests := []struct {
name string
cfgData []byte
gitKey string
section string
subsection string
key string
}{
{
name: "EOF after empty value",
cfgData: []byte("[Test]Flag="),
gitKey: "Test.Flag",
section: "Test",
subsection: "",
key: "Flag",
},
{
name: "NUL value",
cfgData: []byte("[Test]Flag=\x00"),
gitKey: "Test.Flag",
section: "Test",
subsection: "",
key: "Flag",
},
{
name: "carriage return separator",
cfgData: []byte("[Test \"sub\"]\rFlag="),
gitKey: "Test.sub.Flag",
section: "Test",
subsection: "sub",
key: "Flag",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
testRepo, err := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: id.ObjectFormatSHA256})
if err != nil {
t.Fatalf("NewRepo: %v", err)
}
err = testRepo.Root(t).WriteFile(".git/config", tt.cfgData, 0o600)
if err != nil {
t.Fatalf("write config: %v", err)
}
gitValue, gitErr := testRepo.ConfigGet(t, tt.gitKey)
furConfig, furErr := config.Parse(bytes.NewReader(tt.cfgData))
if (gitErr == nil) != (furErr == nil) {
t.Fatalf("git: %v\nfur: %v", gitErr, furErr)
}
if furErr != nil {
return
}
gitValue = strings.TrimSuffix(gitValue, "\n")
got, err := furConfig.Lookup(tt.section, tt.subsection, tt.key).String()
if err != nil {
t.Fatalf("%s: %v", tt.gitKey, err)
}
if got != gitValue {
t.Fatalf("git: %q\nfur: %q", gitValue, got)
}
})
}
}
func FuzzConfig(f *testing.F) {
f.Add([]byte("[test]\nflag = true"), "test.flag")
f.Add([]byte("[test]\nflag = true\n[core/invalid]"), "test.flag")
f.Add([]byte("[test \"sub\"]\nflag = true"), "test.sub.flag")
testRepo, err := testgit.NewRepo(f, testgit.RepoOptions{ObjectFormat: id.ObjectFormatSHA256})
if err != nil {
f.Fatalf("NewRepo: %v", err)
}
f.Fuzz(func(t *testing.T, cfgData []byte, gitKey string) {
err := testRepo.Root(t).WriteFile(".git/config", cfgData, 0o600)
if err != nil {
t.Fatalf("write config: %v", err)
}
gitValue, gitErr := testRepo.ConfigGet(t, gitKey)
furConfig, furErr := config.Parse(bytes.NewReader(cfgData))
if furErr == nil && furConfig == nil {
t.Fatalf("Parse returned nil config with nil error")
}
sameErr := (gitErr == nil) == (furErr == nil)
if !sameErr {
if furErr == nil {
return
}
t.Fatalf("git: %v\nfur: %v", gitErr, furErr)
}
if furErr == nil {
parts := strings.SplitN(gitKey, ".", 3)
furSection := parts[0]
var furSubsection, furKey string
switch len(parts) {
case 1:
case 2:
furKey = parts[1]
case 3:
furSubsection = parts[1]
furKey = parts[2]
default:
t.Fatalf("unexpected split(%q): %v", gitKey, parts)
}
gitValue = strings.TrimSuffix(gitValue, "\n")
furValue, err := furConfig.Lookup(furSection, furSubsection, furKey).String()
if err != nil {
t.Fatalf("%s: %v", gitKey, err)
}
if gitValue != furValue {
t.Fatalf(
"key: %v (%v.%v.%v)\ngit: %q\nfur: %q",
gitKey, furSection, furSubsection, furKey, gitValue, furValue,
)
}
}
})
}