diff --git a/cmd/version_test.go b/cmd/version_test.go index 1ffc64ae..6640d914 100644 --- a/cmd/version_test.go +++ b/cmd/version_test.go @@ -1,6 +1,9 @@ package cmd -import "testing" +import ( + "strings" + "testing" +) func TestGetVersionDisplay(t *testing.T) { var expectedVersion = ProductName + " (https://github.com/mikefarah/yq/) version " + Version @@ -25,6 +28,18 @@ func TestGetVersionDisplay(t *testing.T) { } func Test_getHumanVersion(t *testing.T) { + // Save original values + origGitDescribe := GitDescribe + origGitCommit := GitCommit + origVersionPrerelease := VersionPrerelease + + // Restore after test + defer func() { + GitDescribe = origGitDescribe + GitCommit = origGitCommit + VersionPrerelease = origVersionPrerelease + }() + GitDescribe = "e42813d" GitCommit = "e42813d+CHANGES" var wanted string @@ -49,3 +64,118 @@ func Test_getHumanVersion(t *testing.T) { } } } + +func Test_getHumanVersion_NoGitDescribe(t *testing.T) { + // Save original values + origGitDescribe := GitDescribe + origGitCommit := GitCommit + origVersionPrerelease := VersionPrerelease + + // Restore after test + defer func() { + GitDescribe = origGitDescribe + GitCommit = origGitCommit + VersionPrerelease = origVersionPrerelease + }() + + GitDescribe = "" + GitCommit = "" + VersionPrerelease = "" + + got := getHumanVersion() + if got != Version { + t.Errorf("getHumanVersion() = %v, want %v", got, Version) + } +} + +func Test_getHumanVersion_WithPrerelease(t *testing.T) { + // Save original values + origGitDescribe := GitDescribe + origGitCommit := GitCommit + origVersionPrerelease := VersionPrerelease + + // Restore after test + defer func() { + GitDescribe = origGitDescribe + GitCommit = origGitCommit + VersionPrerelease = origVersionPrerelease + }() + + GitDescribe = "" + GitCommit = "abc123" + VersionPrerelease = "beta" + + got := getHumanVersion() + expected := Version + "-beta (abc123)" + if got != expected { + t.Errorf("getHumanVersion() = %v, want %v", got, expected) + } +} + +func Test_getHumanVersion_PrereleaseInVersion(t *testing.T) { + // Save original values + origGitDescribe := GitDescribe + origGitCommit := GitCommit + origVersionPrerelease := VersionPrerelease + + // Restore after test + defer func() { + GitDescribe = origGitDescribe + GitCommit = origGitCommit + VersionPrerelease = origVersionPrerelease + }() + + GitDescribe = "v1.2.3-rc1" + GitCommit = "xyz789" + VersionPrerelease = "rc1" + + got := getHumanVersion() + // Should not duplicate "rc1" since it's already in GitDescribe + expected := "v1.2.3-rc1 (xyz789)" + if got != expected { + t.Errorf("getHumanVersion() = %v, want %v", got, expected) + } +} + +func Test_getHumanVersion_StripSingleQuotes(t *testing.T) { + // Save original values + origGitDescribe := GitDescribe + origGitCommit := GitCommit + origVersionPrerelease := VersionPrerelease + + // Restore after test + defer func() { + GitDescribe = origGitDescribe + GitCommit = origGitCommit + VersionPrerelease = origVersionPrerelease + }() + + GitDescribe = "'v1.2.3'" + GitCommit = "'commit123'" + VersionPrerelease = "" + + got := getHumanVersion() + // Should strip single quotes + if strings.Contains(got, "'") { + t.Errorf("getHumanVersion() = %v, should not contain single quotes", got) + } + expected := "v1.2.3" + if got != expected { + t.Errorf("getHumanVersion() = %v, want %v", got, expected) + } +} + +func TestProductName(t *testing.T) { + if ProductName != "yq" { + t.Errorf("ProductName = %v, want yq", ProductName) + } +} + +func TestVersionIsSet(t *testing.T) { + if Version == "" { + t.Error("Version should not be empty") + } + if !strings.HasPrefix(Version, "v") { + t.Errorf("Version %v should start with 'v'", Version) + } +} diff --git a/pkg/yqlib/decoder_uri_test.go b/pkg/yqlib/decoder_uri_test.go new file mode 100644 index 00000000..bbec5d0a --- /dev/null +++ b/pkg/yqlib/decoder_uri_test.go @@ -0,0 +1,160 @@ +//go:build !yq_nouri + +package yqlib + +import ( + "io" + "strings" + "testing" + + "github.com/mikefarah/yq/v4/test" +) + +func TestUriDecoder_Init(t *testing.T) { + decoder := NewUriDecoder() + reader := strings.NewReader("test") + err := decoder.Init(reader) + test.AssertResult(t, nil, err) +} + +func TestUriDecoder_DecodeSimpleString(t *testing.T) { + decoder := NewUriDecoder() + reader := strings.NewReader("hello%20world") + err := decoder.Init(reader) + test.AssertResult(t, nil, err) + + node, err := decoder.Decode() + test.AssertResult(t, nil, err) + test.AssertResult(t, "!!str", node.Tag) + test.AssertResult(t, "hello world", node.Value) +} + +func TestUriDecoder_DecodeSpecialCharacters(t *testing.T) { + decoder := NewUriDecoder() + reader := strings.NewReader("hello%21%40%23%24%25") + err := decoder.Init(reader) + test.AssertResult(t, nil, err) + + node, err := decoder.Decode() + test.AssertResult(t, nil, err) + test.AssertResult(t, "hello!@#$%", node.Value) +} + +func TestUriDecoder_DecodeUTF8(t *testing.T) { + decoder := NewUriDecoder() + reader := strings.NewReader("%E2%9C%93%20check") + err := decoder.Init(reader) + test.AssertResult(t, nil, err) + + node, err := decoder.Decode() + test.AssertResult(t, nil, err) + test.AssertResult(t, "✓ check", node.Value) +} + +func TestUriDecoder_DecodePlusSign(t *testing.T) { + decoder := NewUriDecoder() + reader := strings.NewReader("a+b") + err := decoder.Init(reader) + test.AssertResult(t, nil, err) + + node, err := decoder.Decode() + test.AssertResult(t, nil, err) + // Note: url.QueryUnescape does NOT convert + to space + // That's only for form encoding (url.ParseQuery) + test.AssertResult(t, "a b", node.Value) +} + +func TestUriDecoder_DecodeEmptyString(t *testing.T) { + decoder := NewUriDecoder() + reader := strings.NewReader("") + err := decoder.Init(reader) + test.AssertResult(t, nil, err) + + node, err := decoder.Decode() + test.AssertResult(t, nil, err) + test.AssertResult(t, "", node.Value) + + // Second decode should return EOF + node, err = decoder.Decode() + test.AssertResult(t, io.EOF, err) + test.AssertResult(t, (*CandidateNode)(nil), node) +} + +func TestUriDecoder_DecodeMultipleCalls(t *testing.T) { + decoder := NewUriDecoder() + reader := strings.NewReader("test") + err := decoder.Init(reader) + test.AssertResult(t, nil, err) + + // First decode + node, err := decoder.Decode() + test.AssertResult(t, nil, err) + test.AssertResult(t, "test", node.Value) + + // Second decode should return EOF since we've consumed all input + node, err = decoder.Decode() + test.AssertResult(t, io.EOF, err) + test.AssertResult(t, (*CandidateNode)(nil), node) +} + +func TestUriDecoder_DecodeInvalidEscape(t *testing.T) { + decoder := NewUriDecoder() + reader := strings.NewReader("test%ZZ") + err := decoder.Init(reader) + test.AssertResult(t, nil, err) + + _, err = decoder.Decode() + // Should return an error for invalid escape sequence + if err == nil { + t.Error("Expected error for invalid escape sequence, got nil") + } +} + +func TestUriDecoder_DecodeSlashAndQuery(t *testing.T) { + decoder := NewUriDecoder() + reader := strings.NewReader("path%2Fto%2Ffile%3Fquery%3Dvalue") + err := decoder.Init(reader) + test.AssertResult(t, nil, err) + + node, err := decoder.Decode() + test.AssertResult(t, nil, err) + test.AssertResult(t, "path/to/file?query=value", node.Value) +} + +func TestUriDecoder_DecodePercent(t *testing.T) { + decoder := NewUriDecoder() + reader := strings.NewReader("100%25") + err := decoder.Init(reader) + test.AssertResult(t, nil, err) + + node, err := decoder.Decode() + test.AssertResult(t, nil, err) + test.AssertResult(t, "100%", node.Value) +} + +func TestUriDecoder_DecodeNoEscaping(t *testing.T) { + decoder := NewUriDecoder() + reader := strings.NewReader("simple_text-123") + err := decoder.Init(reader) + test.AssertResult(t, nil, err) + + node, err := decoder.Decode() + test.AssertResult(t, nil, err) + test.AssertResult(t, "simple_text-123", node.Value) +} + +// Mock reader that returns an error +type errorReader struct{} + +func (e *errorReader) Read(_ []byte) (n int, err error) { + return 0, io.ErrUnexpectedEOF +} + +func TestUriDecoder_DecodeReadError(t *testing.T) { + decoder := NewUriDecoder() + err := decoder.Init(&errorReader{}) + test.AssertResult(t, nil, err) + + _, err = decoder.Decode() + test.AssertResult(t, io.ErrUnexpectedEOF, err) +} diff --git a/pkg/yqlib/hcl_test.go b/pkg/yqlib/hcl_test.go index 05d314de..a8f22065 100644 --- a/pkg/yqlib/hcl_test.go +++ b/pkg/yqlib/hcl_test.go @@ -4,6 +4,7 @@ package yqlib import ( "bufio" + "bytes" "fmt" "testing" @@ -543,6 +544,35 @@ func documentHclRoundTripScenario(w *bufio.Writer, s formatScenario) { writeOrPanic(w, fmt.Sprintf("```hcl\n%v```\n\n", mustProcessFormatScenario(s, NewHclDecoder(), NewHclEncoder(ConfiguredHclPreferences)))) } +func TestHclEncoderPrintDocumentSeparator(t *testing.T) { + encoder := NewHclEncoder(ConfiguredHclPreferences) + var buf bytes.Buffer + writer := bufio.NewWriter(&buf) + + err := encoder.PrintDocumentSeparator(writer) + writer.Flush() + + test.AssertResult(t, nil, err) + test.AssertResult(t, "", buf.String()) +} + +func TestHclEncoderPrintLeadingContent(t *testing.T) { + encoder := NewHclEncoder(ConfiguredHclPreferences) + var buf bytes.Buffer + writer := bufio.NewWriter(&buf) + + err := encoder.PrintLeadingContent(writer, "some content") + writer.Flush() + + test.AssertResult(t, nil, err) + test.AssertResult(t, "", buf.String()) +} + +func TestHclEncoderCanHandleAliases(t *testing.T) { + encoder := NewHclEncoder(ConfiguredHclPreferences) + test.AssertResult(t, false, encoder.CanHandleAliases()) +} + func TestHclFormatScenarios(t *testing.T) { for _, tt := range hclFormatScenarios { testHclScenario(t, tt) diff --git a/pkg/yqlib/printer_node_info_test.go b/pkg/yqlib/printer_node_info_test.go index 91b29ccc..f779a0c2 100644 --- a/pkg/yqlib/printer_node_info_test.go +++ b/pkg/yqlib/printer_node_info_test.go @@ -49,3 +49,179 @@ func TestNodeInfoPrinter_PrintResults(t *testing.T) { test.AssertResult(t, true, strings.Contains(outStr, "footComment: foot")) test.AssertResult(t, true, strings.Contains(outStr, "anchor: anchor")) } + +func TestNodeInfoPrinter_PrintedAnything_True(t *testing.T) { + node := &CandidateNode{ + Kind: ScalarNode, + Tag: "!!str", + Value: "test", + } + listNodes := list.New() + listNodes.PushBack(node) + + var output bytes.Buffer + writer := bufio.NewWriter(&output) + printer := NewNodeInfoPrinter(NewSinglePrinterWriter(writer)) + + // Before printing, should be false + test.AssertResult(t, false, printer.PrintedAnything()) + + err := printer.PrintResults(listNodes) + writer.Flush() + if err != nil { + t.Fatalf("PrintResults error: %v", err) + } + + // After printing, should be true + test.AssertResult(t, true, printer.PrintedAnything()) +} + +func TestNodeInfoPrinter_PrintedAnything_False(t *testing.T) { + listNodes := list.New() + + var output bytes.Buffer + writer := bufio.NewWriter(&output) + printer := NewNodeInfoPrinter(NewSinglePrinterWriter(writer)) + + err := printer.PrintResults(listNodes) + writer.Flush() + if err != nil { + t.Fatalf("PrintResults error: %v", err) + } + + // No nodes printed, should still be false + test.AssertResult(t, false, printer.PrintedAnything()) +} + +func TestNodeInfoPrinter_SetNulSepOutput(_ *testing.T) { + var output bytes.Buffer + writer := bufio.NewWriter(&output) + printer := NewNodeInfoPrinter(NewSinglePrinterWriter(writer)) + + // Should not panic or error + printer.SetNulSepOutput(true) + printer.SetNulSepOutput(false) +} + +func TestNodeInfoPrinter_SetAppendix(t *testing.T) { + node := &CandidateNode{ + Kind: ScalarNode, + Tag: "!!str", + Value: "test", + } + listNodes := list.New() + listNodes.PushBack(node) + + var output bytes.Buffer + writer := bufio.NewWriter(&output) + printer := NewNodeInfoPrinter(NewSinglePrinterWriter(writer)) + + appendixText := "This is appendix text\n" + appendixReader := strings.NewReader(appendixText) + printer.SetAppendix(appendixReader) + + err := printer.PrintResults(listNodes) + writer.Flush() + if err != nil { + t.Fatalf("PrintResults error: %v", err) + } + + outStr := output.String() + test.AssertResult(t, true, strings.Contains(outStr, "test")) + test.AssertResult(t, true, strings.Contains(outStr, appendixText)) +} + +func TestNodeInfoPrinter_MultipleNodes(t *testing.T) { + node1 := &CandidateNode{ + Kind: ScalarNode, + Tag: "!!str", + Value: "first", + } + node2 := &CandidateNode{ + Kind: ScalarNode, + Tag: "!!str", + Value: "second", + } + listNodes := list.New() + listNodes.PushBack(node1) + listNodes.PushBack(node2) + + var output bytes.Buffer + writer := bufio.NewWriter(&output) + printer := NewNodeInfoPrinter(NewSinglePrinterWriter(writer)) + + err := printer.PrintResults(listNodes) + writer.Flush() + if err != nil { + t.Fatalf("PrintResults error: %v", err) + } + + outStr := output.String() + test.AssertResult(t, true, strings.Contains(outStr, "value: first")) + test.AssertResult(t, true, strings.Contains(outStr, "value: second")) +} + +func TestNodeInfoPrinter_SequenceNode(t *testing.T) { + node := &CandidateNode{ + Kind: SequenceNode, + Tag: "!!seq", + Style: FlowStyle, + } + listNodes := list.New() + listNodes.PushBack(node) + + var output bytes.Buffer + writer := bufio.NewWriter(&output) + printer := NewNodeInfoPrinter(NewSinglePrinterWriter(writer)) + + err := printer.PrintResults(listNodes) + writer.Flush() + if err != nil { + t.Fatalf("PrintResults error: %v", err) + } + + outStr := output.String() + test.AssertResult(t, true, strings.Contains(outStr, "kind: SequenceNode")) + test.AssertResult(t, true, strings.Contains(outStr, "tag: '!!seq'")) + test.AssertResult(t, true, strings.Contains(outStr, "style: FlowStyle")) +} + +func TestNodeInfoPrinter_MappingNode(t *testing.T) { + node := &CandidateNode{ + Kind: MappingNode, + Tag: "!!map", + } + listNodes := list.New() + listNodes.PushBack(node) + + var output bytes.Buffer + writer := bufio.NewWriter(&output) + printer := NewNodeInfoPrinter(NewSinglePrinterWriter(writer)) + + err := printer.PrintResults(listNodes) + writer.Flush() + if err != nil { + t.Fatalf("PrintResults error: %v", err) + } + + outStr := output.String() + test.AssertResult(t, true, strings.Contains(outStr, "kind: MappingNode")) + test.AssertResult(t, true, strings.Contains(outStr, "tag: '!!map'")) +} + +func TestNodeInfoPrinter_EmptyList(t *testing.T) { + listNodes := list.New() + + var output bytes.Buffer + writer := bufio.NewWriter(&output) + printer := NewNodeInfoPrinter(NewSinglePrinterWriter(writer)) + + err := printer.PrintResults(listNodes) + writer.Flush() + if err != nil { + t.Fatalf("PrintResults error: %v", err) + } + + test.AssertResult(t, "", output.String()) + test.AssertResult(t, false, printer.PrintedAnything()) +} diff --git a/pkg/yqlib/toml_test.go b/pkg/yqlib/toml_test.go index 0ccbb54f..395a69d8 100644 --- a/pkg/yqlib/toml_test.go +++ b/pkg/yqlib/toml_test.go @@ -230,12 +230,16 @@ name = "Tom" # name comment // Reproduce bug for https://github.com/mikefarah/yq/issues/2588 // Bug: standalone comments inside a table cause subsequent key-values to be assigned at root. -var issue2588RustToolchainWithComments = ` -[owner] +var issue2588RustToolchainWithComments = `[owner] # comment name = "Tomer" ` +var tableWithComment = `[owner] +# comment +[things] +` + var sampleFromWeb = `# This is a TOML document title = "TOML Example" @@ -574,6 +578,19 @@ var tomlScenarios = []formatScenario{ expected: "null\n", scenarioType: "decode", }, + { + skipDoc: true, + input: issue2588RustToolchainWithComments, + expected: issue2588RustToolchainWithComments, + scenarioType: "roundtrip", + }, + { + skipDoc: true, + input: tableWithComment, + expression: ".owner | headComment", + expected: "comment\n", + scenarioType: "roundtrip", + }, { description: "Roundtrip: sample from web", input: sampleFromWeb, @@ -933,3 +950,32 @@ func TestTomlStringEscapeColourization(t *testing.T) { }) } } + +func TestTomlEncoderPrintDocumentSeparator(t *testing.T) { + encoder := NewTomlEncoder() + var buf bytes.Buffer + writer := bufio.NewWriter(&buf) + + err := encoder.PrintDocumentSeparator(writer) + writer.Flush() + + test.AssertResult(t, nil, err) + test.AssertResult(t, "", buf.String()) +} + +func TestTomlEncoderPrintLeadingContent(t *testing.T) { + encoder := NewTomlEncoder() + var buf bytes.Buffer + writer := bufio.NewWriter(&buf) + + err := encoder.PrintLeadingContent(writer, "some content") + writer.Flush() + + test.AssertResult(t, nil, err) + test.AssertResult(t, "", buf.String()) +} + +func TestTomlEncoderCanHandleAliases(t *testing.T) { + encoder := NewTomlEncoder() + test.AssertResult(t, false, encoder.CanHandleAliases()) +} diff --git a/project-words.txt b/project-words.txt index 8f5dcece..9449a4e7 100644 --- a/project-words.txt +++ b/project-words.txt @@ -293,4 +293,8 @@ buildvcs behaviour GOFLAGS gocache -subsubarray \ No newline at end of file +subsubarray +Ffile +Fquery +coverpkg +gsub \ No newline at end of file diff --git a/scripts/coverage.sh b/scripts/coverage.sh index a64e16b5..53310a47 100755 --- a/scripts/coverage.sh +++ b/scripts/coverage.sh @@ -3,7 +3,9 @@ set -e echo "Running tests and generating coverage..." -go test -coverprofile=coverage.out -v $(go list ./... | grep -v -E 'examples' | grep -v -E 'test') +packages=$(go list ./... | grep -v -E 'examples' | grep -v -E 'test' | tr '\n' ',' | sed 's/,$//') +test_packages=$(go list ./... | grep -v -E 'examples' | grep -v -E 'test' | grep -v '^github.com/mikefarah/yq/v4$') +go test -coverprofile=coverage.out -coverpkg="$packages" -v $test_packages echo "Generating HTML coverage report..." go tool cover -html=coverage.out -o coverage.html @@ -58,11 +60,31 @@ tail -n +1 coverage_sorted.txt | while read percent file; do done echo "" -echo "Top 10 files needing attention (lowest coverage):" +echo "Top 10 files by uncovered statements:" echo "=================================================" -grep -v "TOTAL:" coverage_sorted.txt | tail -10 | while read percent file; do +# Calculate uncovered statements for each file and sort by that +go tool cover -func=coverage.out | grep -E "\.go:[0-9]+:" | \ +awk '{ + # Extract filename and percentage + split($1, parts, ":") + file = parts[1] + pct = $NF + gsub(/%/, "", pct) + + # Track stats per file + total[file]++ + covered[file] += pct +} +END { + for (file in total) { + avg_pct = covered[file] / total[file] + uncovered = total[file] * (100 - avg_pct) / 100 + covered_count = total[file] - uncovered + printf "%.0f %d %.0f %.1f %s\n", uncovered, total[file], covered_count, avg_pct, file + } +}' | sort -rn | head -10 | while read uncovered total covered pct file; do filename=$(basename "$file") - printf "%-60s %8.1f%%\n" "$filename" "$percent" + printf "%-60s %4d uncovered (%4d/%4d, %5.1f%%)\n" "$filename" "$uncovered" "$covered" "$total" "$pct" done echo ""