Transition tests to skip variables, add Goccy specific tests in their place, add datetime preprocessor for date time difference

This commit is contained in:
Mihail Vratchanski 2025-06-08 18:00:44 +03:00
parent 67572e3027
commit c956c31b90
19 changed files with 750 additions and 192 deletions

View File

@ -0,0 +1,191 @@
package yqlib
import (
"regexp"
"strings"
"time"
)
// DateTimePreprocessor handles automatic timestamp tagging for Goccy parser compatibility
type DateTimePreprocessor struct {
enabled bool
}
// NewDateTimePreprocessor creates a new datetime preprocessor
func NewDateTimePreprocessor(enabled bool) *DateTimePreprocessor {
return &DateTimePreprocessor{enabled: enabled}
}
// ISO8601 date/time patterns that should be automatically tagged as timestamps
var dateTimePatterns = []*regexp.Regexp{
// RFC3339 / ISO8601 with timezone: 2006-01-02T15:04:05Z or 2006-01-02T15:04:05+07:00
regexp.MustCompile(`^\s*([0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\.[0-9]+)?(?:Z|[+-][0-9]{2}:[0-9]{2}))\s*$`),
// Date only: 2006-01-02
regexp.MustCompile(`^\s*([0-9]{4}-[0-9]{2}-[0-9]{2})\s*$`),
// RFC3339 without timezone: 2006-01-02T15:04:05
regexp.MustCompile(`^\s*([0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\.[0-9]+)?)\s*$`),
}
// isValidDateTime checks if a string represents a valid ISO8601/RFC3339 datetime
func isValidDateTime(value string) bool {
// Try to parse with RFC3339 first
if _, err := time.Parse(time.RFC3339, value); err == nil {
return true
}
// Try to parse date-only format
if _, err := time.Parse("2006-01-02", value); err == nil {
return true
}
// Try RFC3339 without timezone
if _, err := time.Parse("2006-01-02T15:04:05", value); err == nil {
return true
}
return false
}
// PreprocessDocument automatically adds !!timestamp tags to ISO8601 datetime strings
// in YAML documents when using Goccy parser for consistent datetime arithmetic behaviour
func (dtp *DateTimePreprocessor) PreprocessDocument(yamlContent string) string {
if !dtp.enabled {
return yamlContent
}
lines := strings.Split(yamlContent, "\n")
var result strings.Builder
for i, line := range lines {
processed := dtp.processLine(line)
result.WriteString(processed)
// Add newline except for the last line (preserve original ending)
if i < len(lines)-1 {
result.WriteString("\n")
}
}
return result.String()
}
// processLine processes a single line of YAML, adding timestamp tags where appropriate
func (dtp *DateTimePreprocessor) processLine(line string) string {
// Skip lines that are comments, already have tags, or are part of multi-line constructs
if isSkippableLine(line) {
return line
}
// Look for key-value pairs: "key: value" or "- value"
trimLeft := strings.TrimLeft(line, " \t")
if strings.HasPrefix(trimLeft, "- ") {
// Handle array items first (before key-value pairs)
return dtp.processArrayItemLine(line)
} else if colonIndex := strings.Index(line, ":"); colonIndex != -1 {
// Handle map key-value pairs
return dtp.processKeyValueLine(line, colonIndex)
}
return line
}
// isSkippableLine checks if a line should be skipped from datetime preprocessing
func isSkippableLine(line string) bool {
trimmed := strings.TrimSpace(line)
// Skip empty lines, comments, directives, document separators
if trimmed == "" || strings.HasPrefix(trimmed, "#") ||
strings.HasPrefix(trimmed, "%") || strings.HasPrefix(trimmed, "---") ||
strings.HasPrefix(trimmed, "...") {
return true
}
// Skip lines that already have explicit tags
if strings.Contains(line, "!!") {
return true
}
// Skip multi-line scalar indicators
if strings.HasSuffix(trimmed, "|") || strings.HasSuffix(trimmed, ">") ||
strings.HasSuffix(trimmed, "|-") || strings.HasSuffix(trimmed, ">-") ||
strings.HasSuffix(trimmed, "|+") || strings.HasSuffix(trimmed, ">+") {
return true
}
return false
}
// processKeyValueLine processes a line containing a key-value pair
func (dtp *DateTimePreprocessor) processKeyValueLine(line string, colonIndex int) string {
beforeColon := line[:colonIndex]
afterColon := line[colonIndex+1:]
// Check if the value part looks like a datetime
trimmedValue := strings.TrimSpace(afterColon)
// Skip if value is empty, quoted, or already complex
if trimmedValue == "" ||
strings.HasPrefix(trimmedValue, "\"") ||
strings.HasPrefix(trimmedValue, "'") ||
strings.HasPrefix(trimmedValue, "{") ||
strings.HasPrefix(trimmedValue, "[") ||
strings.HasPrefix(trimmedValue, "&") ||
strings.HasPrefix(trimmedValue, "*") {
return line
}
// Check if it matches datetime patterns and is valid
if dtp.matchesDateTimePattern(trimmedValue) && isValidDateTime(trimmedValue) {
// Insert !!timestamp tag before the value
return beforeColon + ": !!timestamp " + trimmedValue
}
return line
}
// processArrayItemLine processes a line containing an array item
func (dtp *DateTimePreprocessor) processArrayItemLine(line string) string {
// Find the position of "- " after any leading whitespace
trimLeft := strings.TrimLeft(line, " \t")
if !strings.HasPrefix(trimLeft, "- ") {
return line
}
// Find the actual position of "- " in the original line
leadingWhitespace := line[:len(line)-len(trimLeft)]
dashIndex := len(leadingWhitespace)
beforeDash := line[:dashIndex+2] // Include leading whitespace and "- "
afterDash := line[dashIndex+2:]
trimmedValue := strings.TrimSpace(afterDash)
// Skip if value is empty, quoted, or already complex
if trimmedValue == "" ||
strings.HasPrefix(trimmedValue, "\"") ||
strings.HasPrefix(trimmedValue, "'") ||
strings.HasPrefix(trimmedValue, "{") ||
strings.HasPrefix(trimmedValue, "[") ||
strings.HasPrefix(trimmedValue, "&") ||
strings.HasPrefix(trimmedValue, "*") {
return line
}
// Check if it matches datetime patterns and is valid
if dtp.matchesDateTimePattern(trimmedValue) && isValidDateTime(trimmedValue) {
// Insert !!timestamp tag before the value
return beforeDash + "!!timestamp " + trimmedValue
}
return line
}
// matchesDateTimePattern checks if a value matches any of the datetime regex patterns
func (dtp *DateTimePreprocessor) matchesDateTimePattern(value string) bool {
for _, pattern := range dateTimePatterns {
if pattern.MatchString(value) {
return true
}
}
return false
}

View File

@ -0,0 +1,366 @@
package yqlib
import (
"testing"
)
func TestDateTimePreprocessorBasicFunctionality(t *testing.T) {
preprocessor := NewDateTimePreprocessor(true)
testCases := []struct {
name string
input string
expected string
}{
{
name: "RFC3339 with timezone",
input: "timestamp: 2021-01-01T00:00:00Z",
expected: "timestamp: !!timestamp 2021-01-01T00:00:00Z",
},
{
name: "RFC3339 with offset timezone",
input: "timestamp: 2021-01-01T03:10:00+03:00",
expected: "timestamp: !!timestamp 2021-01-01T03:10:00+03:00",
},
{
name: "Date only",
input: "date: 2021-01-01",
expected: "date: !!timestamp 2021-01-01",
},
{
name: "RFC3339 without timezone",
input: "timestamp: 2021-01-01T15:04:05",
expected: "timestamp: !!timestamp 2021-01-01T15:04:05",
},
{
name: "RFC3339 with milliseconds",
input: "timestamp: 2021-01-01T00:00:00.123Z",
expected: "timestamp: !!timestamp 2021-01-01T00:00:00.123Z",
},
{
name: "Array item with timestamp",
input: "- 2021-01-01T00:00:00Z",
expected: "- !!timestamp 2021-01-01T00:00:00Z",
},
{
name: "Multiple timestamps in document",
input: "start: 2021-01-01T00:00:00Z\nend: 2021-12-31T23:59:59Z",
expected: "start: !!timestamp 2021-01-01T00:00:00Z\nend: !!timestamp 2021-12-31T23:59:59Z",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := preprocessor.PreprocessDocument(tc.input)
if result != tc.expected {
t.Errorf("Expected:\n%s\nGot:\n%s", tc.expected, result)
}
})
}
}
func TestDateTimePreprocessorDisabled(t *testing.T) {
preprocessor := NewDateTimePreprocessor(false)
input := "timestamp: 2021-01-01T00:00:00Z"
expected := "timestamp: 2021-01-01T00:00:00Z"
result := preprocessor.PreprocessDocument(input)
if result != expected {
t.Errorf("Expected preprocessing to be disabled. Got: %s", result)
}
}
func TestDateTimePreprocessorSkipsCases(t *testing.T) {
preprocessor := NewDateTimePreprocessor(true)
testCases := []struct {
name string
input string
expected string // Should remain unchanged
}{
{
name: "Already tagged",
input: "timestamp: !!timestamp 2021-01-01T00:00:00Z",
expected: "timestamp: !!timestamp 2021-01-01T00:00:00Z",
},
{
name: "Quoted string",
input: "timestamp: \"2021-01-01T00:00:00Z\"",
expected: "timestamp: \"2021-01-01T00:00:00Z\"",
},
{
name: "Single quoted string",
input: "timestamp: '2021-01-01T00:00:00Z'",
expected: "timestamp: '2021-01-01T00:00:00Z'",
},
{
name: "Comment line",
input: "# This is 2021-01-01T00:00:00Z",
expected: "# This is 2021-01-01T00:00:00Z",
},
{
name: "Empty line",
input: "",
expected: "",
},
{
name: "YAML directive",
input: "%YAML 1.1",
expected: "%YAML 1.1",
},
{
name: "Document separator",
input: "---",
expected: "---",
},
{
name: "Document end",
input: "...",
expected: "...",
},
{
name: "Anchor reference",
input: "timestamp: *my_timestamp",
expected: "timestamp: *my_timestamp",
},
{
name: "Anchor definition",
input: "timestamp: &my_timestamp 2021-01-01T00:00:00Z",
expected: "timestamp: &my_timestamp 2021-01-01T00:00:00Z",
},
{
name: "Map value",
input: "timestamp: {year: 2021, month: 01}",
expected: "timestamp: {year: 2021, month: 01}",
},
{
name: "Array value",
input: "timestamp: [2021, 01, 01]",
expected: "timestamp: [2021, 01, 01]",
},
{
name: "Multi-line scalar literal",
input: "description: |",
expected: "description: |",
},
{
name: "Multi-line scalar folded",
input: "description: >",
expected: "description: >",
},
{
name: "Invalid date format",
input: "invalid: 2021/01/01",
expected: "invalid: 2021/01/01",
},
{
name: "Non-date numeric string",
input: "number: 20210101",
expected: "number: 20210101",
},
{
name: "Partial date",
input: "partial: 2021-01",
expected: "partial: 2021-01",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := preprocessor.PreprocessDocument(tc.input)
if result != tc.expected {
t.Errorf("Expected no change. Input: %s, Got: %s", tc.input, result)
}
})
}
}
func TestDateTimePreprocessorComplexDocuments(t *testing.T) {
preprocessor := NewDateTimePreprocessor(true)
testCases := []struct {
name string
input string
expected string
}{
{
name: "Mixed document with comments and timestamps",
input: `# Configuration file
version: 1.0
created: 2021-01-01T00:00:00Z
# Last modified date
modified: 2021-12-31T23:59:59Z
author: "John Doe"
tags:
- production
- 2021-01-01`,
expected: `# Configuration file
version: 1.0
created: !!timestamp 2021-01-01T00:00:00Z
# Last modified date
modified: !!timestamp 2021-12-31T23:59:59Z
author: "John Doe"
tags:
- production
- !!timestamp 2021-01-01`,
},
{
name: "Nested structures with timestamps",
input: `events:
start:
date: 2021-01-01
time: 2021-01-01T09:00:00Z
end:
date: 2021-01-02
time: 2021-01-02T17:00:00Z`,
expected: `events:
start:
date: !!timestamp 2021-01-01
time: !!timestamp 2021-01-01T09:00:00Z
end:
date: !!timestamp 2021-01-02
time: !!timestamp 2021-01-02T17:00:00Z`,
},
{
name: "Array of timestamps",
input: `dates:
- 2021-01-01T00:00:00Z
- 2021-06-15T12:30:45Z
- 2021-12-31T23:59:59Z`,
expected: `dates:
- !!timestamp 2021-01-01T00:00:00Z
- !!timestamp 2021-06-15T12:30:45Z
- !!timestamp 2021-12-31T23:59:59Z`,
},
{
name: "Mixed array with timestamps and other values",
input: `mixed:
- "string value"
- 2021-01-01T00:00:00Z
- 42
- 2021-12-31`,
expected: `mixed:
- "string value"
- !!timestamp 2021-01-01T00:00:00Z
- 42
- !!timestamp 2021-12-31`,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := preprocessor.PreprocessDocument(tc.input)
if result != tc.expected {
t.Errorf("Expected:\n%s\nGot:\n%s", tc.expected, result)
}
})
}
}
func TestDateTimePreprocessorEdgeCases(t *testing.T) {
preprocessor := NewDateTimePreprocessor(true)
testCases := []struct {
name string
input string
expected string
}{
{
name: "Whitespace around timestamp",
input: "timestamp: 2021-01-01T00:00:00Z ",
expected: "timestamp: !!timestamp 2021-01-01T00:00:00Z",
},
{
name: "Indented timestamp",
input: " timestamp: 2021-01-01T00:00:00Z",
expected: " timestamp: !!timestamp 2021-01-01T00:00:00Z",
},
{
name: "Deeply indented array item",
input: " - 2021-01-01T00:00:00Z",
expected: " - !!timestamp 2021-01-01T00:00:00Z",
},
{
name: "Empty value",
input: "timestamp:",
expected: "timestamp:",
},
{
name: "Only key",
input: "timestamp",
expected: "timestamp",
},
{
name: "Colon in value (not key-value pair)",
input: "description: Time is 15:30:45",
expected: "description: Time is 15:30:45",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := preprocessor.PreprocessDocument(tc.input)
if result != tc.expected {
t.Errorf("Expected:\n%s\nGot:\n%s", tc.expected, result)
}
})
}
}
func TestIsValidDateTime(t *testing.T) {
testCases := []struct {
input string
expected bool
}{
{"2021-01-01T00:00:00Z", true},
{"2021-01-01T00:00:00+03:00", true},
{"2021-01-01T00:00:00", true},
{"2021-01-01", true},
{"2021-01-01T00:00:00.123Z", true},
{"invalid-date", false},
{"2021/01/01", false},
{"2021-13-01", false},
{"2021-01-32", false},
{"2021-01-01T25:00:00Z", false},
{"", false},
}
for _, tc := range testCases {
t.Run(tc.input, func(t *testing.T) {
result := isValidDateTime(tc.input)
if result != tc.expected {
t.Errorf("isValidDateTime(%s) = %v, expected %v", tc.input, result, tc.expected)
}
})
}
}
func TestMatchesDateTimePattern(t *testing.T) {
preprocessor := NewDateTimePreprocessor(true)
testCases := []struct {
input string
expected bool
}{
{"2021-01-01T00:00:00Z", true},
{"2021-01-01T00:00:00+03:00", true},
{"2021-01-01T00:00:00", true},
{"2021-01-01", true},
{"2021-01-01T00:00:00.123Z", true},
{" 2021-01-01T00:00:00Z ", true}, // with whitespace
{"not-a-date", false},
{"2021/01/01", false},
{"15:30:45", false}, // time only
{"", false},
}
for _, tc := range testCases {
t.Run(tc.input, func(t *testing.T) {
result := preprocessor.matchesDateTimePattern(tc.input)
if result != tc.expected {
t.Errorf("matchesDateTimePattern(%s) = %v, expected %v", tc.input, result, tc.expected)
}
})
}
}

View File

@ -40,6 +40,9 @@ type goccyYamlDecoder struct {
// It is reset by Init() for each new stream.
anchorMap map[string]*CandidateNode
// dateTimePreprocessor handles automatic timestamp tagging for datetime arithmetic compatibility
dateTimePreprocessor *DateTimePreprocessor
readAnything bool // Flag to track if any actual YAML node (or synthesized comment node) has been decoded.
firstFile bool // Flag for 'evaluateTogether' mode to handle leading content only once.
documentIndex uint // Index of the current document within the stream.
@ -48,7 +51,11 @@ type goccyYamlDecoder struct {
// NewGoccyYAMLDecoder creates a new YAML decoder using the goccy/go-yaml library,
// configured with the given preferences.
func NewGoccyYAMLDecoder(prefs YamlPreferences) Decoder {
return &goccyYamlDecoder{prefs: prefs, firstFile: true}
return &goccyYamlDecoder{
prefs: prefs,
firstFile: true,
dateTimePreprocessor: NewDateTimePreprocessor(true), // Enable datetime preprocessing for Goccy
}
}
// processReadStream pre-processes the input stream to extract leading comments,
@ -129,9 +136,29 @@ func (dec *goccyYamlDecoder) Init(reader io.Reader) error {
if err != nil {
return err
}
// Apply datetime preprocessing to the remaining content
if dec.dateTimePreprocessor != nil {
remainingBytes, err := io.ReadAll(readerToUse)
if err != nil && errors.Is(err, io.EOF) {
return err
}
preprocessedContent := dec.dateTimePreprocessor.PreprocessDocument(string(remainingBytes))
readerToUse = strings.NewReader(preprocessedContent)
}
} else if !dec.prefs.LeadingContentPreProcessing {
// If not preprocessing, TeeReader captures initial bytes in case it's a comment-only document.
readerToUse = io.TeeReader(reader, &dec.bufferRead)
if dec.dateTimePreprocessor != nil {
// Read all content, apply datetime preprocessing, then create new reader
allBytes, err := io.ReadAll(reader)
if err != nil && errors.Is(err, io.EOF) {
return err
}
preprocessedContent := dec.dateTimePreprocessor.PreprocessDocument(string(allBytes))
readerToUse = io.TeeReader(strings.NewReader(preprocessedContent), &dec.bufferRead)
} else {
readerToUse = io.TeeReader(reader, &dec.bufferRead)
}
}
dec.leadingContent = processedLeadingContent

View File

@ -428,21 +428,9 @@ var addOperatorScenarios = []expressionScenario{
},
}
func testAddScenarioWithParserCheck(t *testing.T, s *expressionScenario) {
// Skip datetime arithmetic tests for goccy as it requires explicit timestamp tagging
// while yaml.v3 auto-detects ISO8601 strings as timestamps
if ConfiguredYamlPreferences.UseGoccyParser {
if s.description == "Date addition" || s.description == "Date addition -date only" {
t.Skip("goccy parser requires explicit timestamp tagging for datetime arithmetic - more YAML spec compliant")
return
}
}
testScenario(t, s)
}
func TestAddOperatorScenarios(t *testing.T) {
for _, tt := range addOperatorScenarios {
testAddScenarioWithParserCheck(t, &tt)
testScenario(t, &tt)
}
documentOperatorScenarios(t, "add", addOperatorScenarios)
}

View File

@ -60,6 +60,7 @@ var collectOperatorScenarios = []expressionScenario{
expected: []string{
"D0, P[], (!!seq)::- {a: apple}\n",
},
skipForGoccy: true,
},
{
skipDoc: true,
@ -136,18 +137,9 @@ var collectOperatorScenarios = []expressionScenario{
},
}
func testCollectScenarioWithParserCheck(t *testing.T, s *expressionScenario) {
// Skip comment-related tests for goccy as it handles comment placement more strictly
if s.description == "with comments" && ConfiguredYamlPreferences.UseGoccyParser {
t.Skip("goccy parser handles trailing comments more strictly - structurally equivalent but different comment handling")
return
}
testScenario(t, s)
}
func TestCollectOperatorScenarios(t *testing.T) {
for _, tt := range collectOperatorScenarios {
testCollectScenarioWithParserCheck(t, &tt)
testScenario(t, &tt)
}
documentOperatorScenarios(t, "collect-into-array", collectOperatorScenarios)
}

View File

@ -62,6 +62,7 @@ var commentOperatorScenarios = []expressionScenario{
expected: []string{
"D0, P[], (!!map)::a: cat # single\n",
},
skipForGoccy: true,
},
{
description: "Set line comment of a maps/arrays",
@ -71,6 +72,7 @@ var commentOperatorScenarios = []expressionScenario{
expected: []string{
"D0, P[], (!!map)::a: # single\n b: things\n",
},
skipForGoccy: true,
},
{
skipDoc: true,
@ -79,6 +81,7 @@ var commentOperatorScenarios = []expressionScenario{
expected: []string{
"D0, P[], (!!map)::a: cat # dog\nb: dog\n",
},
skipForGoccy: true,
},
{
skipDoc: true,
@ -88,6 +91,7 @@ var commentOperatorScenarios = []expressionScenario{
"D0, P[], (!!map)::a: cat # 0\n",
"D1, P[], (!!map)::a: dog # 1\n",
},
skipForGoccy: true,
},
{
description: "Use update assign to perform relative updates",
@ -96,6 +100,7 @@ var commentOperatorScenarios = []expressionScenario{
expected: []string{
"D0, P[], (!!map)::a: cat # cat\nb: dog # dog\n",
},
skipForGoccy: true,
},
{
skipDoc: true,
@ -104,6 +109,7 @@ var commentOperatorScenarios = []expressionScenario{
expected: []string{
"D0, P[], (!!map)::a: cat # cat\n# cat\n\n# cat\nb: dog # dog\n# dog\n\n# dog\n",
},
skipForGoccy: true,
},
{
description: "Where is the comment - map key example",
@ -113,6 +119,7 @@ var commentOperatorScenarios = []expressionScenario{
expected: []string{
expectedWhereIsMyCommentMapKey,
},
skipForGoccy: true,
},
{
description: "Retrieve comment - map key example",
@ -122,6 +129,7 @@ var commentOperatorScenarios = []expressionScenario{
expected: []string{
"D0, P[hello], (!!str)::hello-world-comment\n",
},
skipForGoccy: true,
},
{
description: "Where is the comment - array example",
@ -131,6 +139,7 @@ var commentOperatorScenarios = []expressionScenario{
expected: []string{
expectedWhereIsMyCommentArray,
},
skipForGoccy: true,
},
{
description: "Retrieve comment - array example",
@ -140,6 +149,7 @@ var commentOperatorScenarios = []expressionScenario{
expected: []string{
"D0, P[name 0], (!!str)::under-name-comment\n",
},
skipForGoccy: true,
},
{
description: "Set head comment",
@ -148,6 +158,7 @@ var commentOperatorScenarios = []expressionScenario{
expected: []string{
"D0, P[], (!!map)::# single\na: cat\n",
},
skipForGoccy: true,
},
{
description: "Set head comment of a map entry",
@ -156,6 +167,7 @@ var commentOperatorScenarios = []expressionScenario{
expected: []string{
"D0, P[], (!!map)::f: foo\n# single\na:\n b: cat\n",
},
skipForGoccy: true,
},
{
description: "Set foot comment, using an expression",
@ -164,6 +176,7 @@ var commentOperatorScenarios = []expressionScenario{
expected: []string{
"D0, P[], (!!map)::a: cat\n# cat\n",
},
skipForGoccy: true,
},
{
skipDoc: true,
@ -173,6 +186,7 @@ var commentOperatorScenarios = []expressionScenario{
expected: []string{
"D0, P[], (!!map)::a: cat\n",
},
skipForGoccy: true,
},
{
skipDoc: true,
@ -181,6 +195,7 @@ var commentOperatorScenarios = []expressionScenario{
expected: []string{
"D0, P[], (!!map)::a: cat\n",
},
skipForGoccy: true,
},
{
skipDoc: true,
@ -189,6 +204,7 @@ var commentOperatorScenarios = []expressionScenario{
expected: []string{
"D0, P[], (!!map)::a: cat\n",
},
skipForGoccy: true,
},
{
description: "Remove comment",
@ -197,6 +213,7 @@ var commentOperatorScenarios = []expressionScenario{
expected: []string{
"D0, P[], (!!map)::a: cat\nb: dog # leave this\n",
},
skipForGoccy: true,
},
{
description: "Remove (strip) all comments",
@ -206,6 +223,7 @@ var commentOperatorScenarios = []expressionScenario{
expected: []string{
"D0, P[], (!!map)::a: cat\nb:\n",
},
skipForGoccy: true,
},
{
description: "Get line comment",
@ -214,6 +232,7 @@ var commentOperatorScenarios = []expressionScenario{
expected: []string{
"D0, P[a], (!!str)::meow\n",
},
skipForGoccy: true,
},
{
description: "Get head comment",
@ -223,6 +242,7 @@ var commentOperatorScenarios = []expressionScenario{
expected: []string{
"D0, P[], (!!str)::welcome!\n\n",
},
skipForGoccy: true,
},
{
skipDoc: true,
@ -232,6 +252,7 @@ var commentOperatorScenarios = []expressionScenario{
expected: []string{
"D0, P[], (!!map)::a: cat\n",
},
skipForGoccy: true,
},
{
skipDoc: true,
@ -241,6 +262,7 @@ var commentOperatorScenarios = []expressionScenario{
expected: []string{
"D0, P[], (!!map)::a: cat\n",
},
skipForGoccy: true,
},
{
description: "Head comment with document split",
@ -250,6 +272,7 @@ var commentOperatorScenarios = []expressionScenario{
expected: []string{
"D0, P[], (!!str)::welcome!\nbob\n",
},
skipForGoccy: true,
},
{
description: "Get foot comment",
@ -259,6 +282,7 @@ var commentOperatorScenarios = []expressionScenario{
expected: []string{
"D0, P[], (!!str)::have a great day\nno really\n",
},
skipForGoccy: true,
},
{
description: "leading spaces",
@ -268,6 +292,7 @@ var commentOperatorScenarios = []expressionScenario{
expected: []string{
"D0, P[], (!!null):: # hi\n",
},
skipForGoccy: true,
},
{
description: "string spaces",
@ -277,6 +302,7 @@ var commentOperatorScenarios = []expressionScenario{
expected: []string{
"D0, P[], (!!str)::# hi\ncat\n",
},
skipForGoccy: true,
},
{
description: "leading spaces with new line",
@ -286,6 +312,7 @@ var commentOperatorScenarios = []expressionScenario{
expected: []string{
"D0, P[], (!!null):: # hi\n",
},
skipForGoccy: true,
},
{
description: "directive",
@ -295,22 +322,90 @@ var commentOperatorScenarios = []expressionScenario{
expected: []string{
"D0, P[], (!!null)::%YAML 1.1\n# hi\n",
},
skipForGoccy: true,
},
}
func testCommentScenarioWithParserCheck(t *testing.T, s *expressionScenario) {
// Skip comment tests for goccy as it handles comment placement and formatting differently
// The structural data is preserved but comment positioning varies between parsers
if ConfiguredYamlPreferences.UseGoccyParser {
t.Skip("goccy parser handles comment placement and formatting differently - data integrity preserved")
return
}
testScenario(t, s)
// Goccy-specific comment operator scenarios - these validate the actual behaviour of Goccy parser
var goccyCommentOperatorScenarios = []expressionScenario{
{
description: "Goccy: Set line comment - basic functionality",
subdescription: "Goccy parser maintains comment functionality with potentially different placement",
document: `a: cat`,
expression: `.a line_comment="single"`,
expected: []string{
"D0, P[], (!!map)::a: cat # single\n",
},
skipForYamlV3: true,
},
{
description: "Goccy: Get line comment - basic functionality",
subdescription: "Goccy parser can read existing line comments",
document: "a: cat # meow",
expression: `.a | line_comment`,
expected: []string{
"D0, P[a], (!!str)::meow\n",
},
skipForYamlV3: true,
},
{
description: "Goccy: Set head comment - basic functionality",
subdescription: "Goccy parser can set head comments",
document: `a: cat`,
expression: `. head_comment="header"`,
expected: []string{
"D0, P[], (!!map)::# header\na: cat\n",
},
skipForYamlV3: true,
},
{
description: "Goccy: Get head comment - basic functionality",
subdescription: "Goccy parser can read existing head comments",
document: "# welcome!\na: cat",
expression: `. | head_comment`,
expected: []string{
"D0, P[], (!!str)::welcome!\n",
},
skipForYamlV3: true,
},
{
description: "Goccy: Set foot comment - basic functionality",
subdescription: "Goccy parser can set foot comments",
document: `a: cat`,
expression: `. foot_comment="footer"`,
expected: []string{
"D0, P[], (!!map)::a: cat\n# footer\n",
},
skipForYamlV3: true,
},
{
description: "Goccy: Remove line comment",
subdescription: "Goccy parser can remove comments",
document: "a: cat # remove_me\nb: dog # keep_me",
expression: `.a line_comment=""`,
expected: []string{
"D0, P[], (!!map)::a: cat\nb: dog # keep_me\n",
},
skipForYamlV3: true,
},
{
description: "Goccy: Comment preservation during data operations",
subdescription: "Goccy parser preserves structural integrity while handling comments",
document: "# header\na: cat # inline\nb: dog\n# footer",
expression: `.c = "new"`,
expected: []string{
"D0, P[], (!!map)::# header\na: cat # inline\nb: dog\nc: new\n# footer\n",
},
skipForYamlV3: true,
},
}
func TestCommentOperatorScenarios(t *testing.T) {
for _, tt := range commentOperatorScenarios {
testCommentScenarioWithParserCheck(t, &tt)
testScenario(t, &tt)
}
for _, tt := range goccyCommentOperatorScenarios {
testScenario(t, &tt)
}
documentOperatorScenarios(t, "comment-operators", commentOperatorScenarios)
}

View File

@ -128,19 +128,9 @@ var dateTimeOperatorScenarios = []expressionScenario{
},
}
func testDatetimeScenarioWithParserCheck(t *testing.T, s *expressionScenario) {
// Skip datetime arithmetic tests for goccy as it requires explicit timestamp tagging
// while yaml.v3 auto-detects ISO8601 strings as timestamps
if (s.description == "Date addition" || s.description == "Date subtraction") && ConfiguredYamlPreferences.UseGoccyParser {
t.Skip("goccy parser requires explicit timestamp tagging for datetime arithmetic - more YAML spec compliant")
return
}
testScenario(t, s)
}
func TestDatetimeOperatorScenarios(t *testing.T) {
for _, tt := range dateTimeOperatorScenarios {
testDatetimeScenarioWithParserCheck(t, &tt)
testScenario(t, &tt)
}
documentOperatorScenarios(t, "datetime", dateTimeOperatorScenarios)
}

View File

@ -134,21 +134,13 @@ var entriesOperatorScenarios = []expressionScenario{
expected: []string{
"D0, P[], (!!map)::# abc\na: {b: bird}\n# xyz\n",
},
skipForGoccy: true,
},
}
func testEntriesScenarioWithParserCheck(t *testing.T, s *expressionScenario) {
// Skip comment-related tests for goccy as it handles comment placement more strictly
if s.description == "Use with_entries to filter the map; head comment" && ConfiguredYamlPreferences.UseGoccyParser {
t.Skip("goccy parser handles trailing comments more strictly - structurally equivalent but different comment handling")
return
}
testScenario(t, s)
}
func TestEntriesOperatorScenarios(t *testing.T) {
for _, tt := range entriesOperatorScenarios {
testEntriesScenarioWithParserCheck(t, &tt)
testScenario(t, &tt)
}
documentOperatorScenarios(t, "entries", entriesOperatorScenarios)
}

View File

@ -169,21 +169,13 @@ var envOperatorScenarios = []expressionScenario{
expected: []string{
"D0, P[], (!!map)::# abc\n{v: \"cat meow\"}\n# xyz\n",
},
skipForGoccy: true,
},
}
func testEnvScenarioWithParserCheck(t *testing.T, s *expressionScenario) {
// Skip comment-related tests for goccy as it handles comment placement more strictly
if s.description == "with header/footer" && ConfiguredYamlPreferences.UseGoccyParser {
t.Skip("goccy parser handles trailing comments more strictly - structurally equivalent but different comment handling")
return
}
testScenario(t, s)
}
func TestEnvOperatorScenarios(t *testing.T) {
for _, tt := range envOperatorScenarios {
testEnvScenarioWithParserCheck(t, &tt)
testScenario(t, &tt)
}
documentOperatorScenarios(t, "env-variable-operators", envOperatorScenarios)
}

View File

@ -110,6 +110,7 @@ var keysOperatorScenarios = []expressionScenario{
expected: []string{
"D0, P[a x], (!!str)::comment on key\n",
},
skipForGoccy: true,
},
{
description: "Check node is a key",
@ -121,18 +122,9 @@ var keysOperatorScenarios = []expressionScenario{
},
}
func testKeysScenarioWithParserCheck(t *testing.T, s *expressionScenario) {
// Skip comment-related tests for goccy as it handles comment placement differently
if s.description == "Get comment from map key" && ConfiguredYamlPreferences.UseGoccyParser {
t.Skip("goccy parser handles comment placement differently - data integrity preserved")
return
}
testScenario(t, s)
}
func TestKeysOperatorScenarios(t *testing.T) {
for _, tt := range keysOperatorScenarios {
testKeysScenarioWithParserCheck(t, &tt)
testScenario(t, &tt)
}
documentOperatorScenarios(t, "keys", keysOperatorScenarios)
}

View File

@ -94,6 +94,7 @@ var multiplyOperatorScenarios = []expressionScenario{
expected: []string{
"D0, P[], (!!map)::sample:\n - &a\n - !!merge <<: *a\n",
},
skipForGoccy: true,
},
{
skipDoc: true,
@ -112,6 +113,7 @@ var multiplyOperatorScenarios = []expressionScenario{
expected: []string{
"D0, P[], (!!map)::# here\n\na: apple\nb: banana\n",
},
skipForGoccy: true,
},
{
skipDoc: true,
@ -121,6 +123,7 @@ var multiplyOperatorScenarios = []expressionScenario{
expected: []string{
"D0, P[node], (!!map)::# here\na: apple\nb: banana\n",
},
skipForGoccy: true,
},
{
skipDoc: true,
@ -130,6 +133,7 @@ var multiplyOperatorScenarios = []expressionScenario{
expected: []string{
"D0, P[], (!!map)::# here\n\nb: banana\na: apple\n",
},
skipForGoccy: true,
},
{
skipDoc: true,
@ -139,6 +143,7 @@ var multiplyOperatorScenarios = []expressionScenario{
expected: []string{
"D0, P[], (!!map)::b: banana\n# here\na: apple\n",
},
skipForGoccy: true,
},
{
skipDoc: true,
@ -148,6 +153,7 @@ var multiplyOperatorScenarios = []expressionScenario{
expected: []string{
"D0, P[], (!!map)::a: apple\nb: banana\n# footer\n",
},
skipForGoccy: true,
},
{
skipDoc: true,
@ -157,6 +163,7 @@ var multiplyOperatorScenarios = []expressionScenario{
expected: []string{ // not sure why there's an extra newline *shrug*
"D0, P[], (!!map)::a: apple\n# footer\n\nb: banana\n",
},
skipForGoccy: true,
},
{
description: "doc 2 has footer",
@ -167,6 +174,7 @@ var multiplyOperatorScenarios = []expressionScenario{
expected: []string{
"D0, P[], (!!map)::b: banana\na: apple\n# footer\n",
},
skipForGoccy: true,
},
{
description: "Multiply integers",
@ -667,30 +675,36 @@ var multiplyOperatorScenarios = []expressionScenario{
},
}
func testMultiplyScenarioWithParserCheck(t *testing.T, s *expressionScenario) {
// Skip tests where goccy correctly rejects invalid YAML at parse time
if ConfiguredYamlPreferences.UseGoccyParser {
// Skip merge anchor tests - goccy correctly rejects merge anchors with null values
if s.document == mergeArrayWithAnchors {
t.Skip("goccy parser correctly rejects merge anchors with null values at parse time")
return
}
// Skip comment-related tests - goccy handles comment placement differently
if s.document == docWithHeader || s.document == nodeWithHeader ||
s.document2 == docWithHeader || s.document2 == nodeWithHeader ||
s.document == docWithFooter || s.document == nodeWithFooter ||
s.document2 == docWithFooter || s.document2 == nodeWithFooter {
t.Skip("goccy parser handles comment placement differently - data integrity preserved")
return
}
}
testScenario(t, s)
// Goccy-specific multiply operator scenarios
var goccyMultiplyOperatorScenarios = []expressionScenario{
{
description: "Goccy: Basic merge operation preserves structure",
subdescription: "Goccy parser maintains structural integrity during merge operations",
document: `{a: apple, b: banana}`,
expression: `. * {c: cherry}`,
expected: []string{
"D0, P[], (!!map)::{a: apple, b: banana, c: cherry}\n",
},
skipForYamlV3: true, // different comment handling behaviour
},
{
description: "Goccy: Merge with valid anchor structure",
subdescription: "Goccy handles valid YAML anchor structures correctly",
document: `{a: &anchor value, b: *anchor}`,
expression: `. * {c: new}`,
expected: []string{
"D0, P[], (!!map)::{a: &anchor value, b: *anchor, c: new}\n",
},
skipForYamlV3: true, // different anchor handling nuances
},
}
func TestMultiplyOperatorScenarios(t *testing.T) {
for _, tt := range multiplyOperatorScenarios {
testMultiplyScenarioWithParserCheck(t, &tt)
testScenario(t, &tt)
}
for _, tt := range goccyMultiplyOperatorScenarios {
testScenario(t, &tt)
}
documentOperatorScenarios(t, "multiply-merge", multiplyOperatorScenarios)
}

View File

@ -41,6 +41,7 @@ var omitOperatorScenarios = []expressionScenario{
expected: []string{
"D0, P[], (!!map)::# abc\nmyMap: {dog: bark, thing: hamster}\n# xyz\n",
},
skipForGoccy: true,
},
{
description: "Omit indices from array",
@ -59,23 +60,13 @@ var omitOperatorScenarios = []expressionScenario{
expected: []string{
"D0, P[], (!!seq)::# abc\n[leopard]\n# xyz\n",
},
skipForGoccy: true,
},
}
func testOmitScenarioWithParserCheck(t *testing.T, s *expressionScenario) {
// Skip comment-related tests for goccy as it handles comment placement more strictly
if ConfiguredYamlPreferences.UseGoccyParser {
if s.description == "Omit keys from map with comments" || s.description == "Omit indices from array with comments" {
t.Skip("goccy parser handles trailing comments more strictly - structurally equivalent but different comment handling")
return
}
}
testScenario(t, s)
}
func TestOmitOperatorScenarios(t *testing.T) {
for _, tt := range omitOperatorScenarios {
testOmitScenarioWithParserCheck(t, &tt)
testScenario(t, &tt)
}
documentOperatorScenarios(t, "omit", omitOperatorScenarios)
}

View File

@ -50,6 +50,7 @@ var pickOperatorScenarios = []expressionScenario{
expected: []string{
"D0, P[], (!!map)::# abc\nmyMap: {hamster: squeak, cat: meow}\n# xyz\n",
},
skipForGoccy: true,
},
{
description: "Pick indices from array",
@ -68,23 +69,13 @@ var pickOperatorScenarios = []expressionScenario{
expected: []string{
"D0, P[], (!!seq)::# abc\n[lion, cat]\n# xyz\n",
},
skipForGoccy: true,
},
}
func testPickScenarioWithParserCheck(t *testing.T, s *expressionScenario) {
// Skip comment-related tests for goccy as it handles comment placement more strictly
if ConfiguredYamlPreferences.UseGoccyParser {
if s.description == "Pick keys from map with comments" || s.description == "Pick indices from array with comments" {
t.Skip("goccy parser handles trailing comments more strictly - structurally equivalent but different comment handling")
return
}
}
testScenario(t, s)
}
func TestPickOperatorScenarios(t *testing.T) {
for _, tt := range pickOperatorScenarios {
testPickScenarioWithParserCheck(t, &tt)
testScenario(t, &tt)
}
documentOperatorScenarios(t, "pick", pickOperatorScenarios)
}

View File

@ -62,21 +62,13 @@ var reverseOperatorScenarios = []expressionScenario{
expected: []string{
"D0, P[], (!!seq)::# abc\n[{a: cat}, {a: banana}, {a: apple}]\n# xyz\n",
},
skipForGoccy: true,
},
}
func testReverseScenarioWithParserCheck(t *testing.T, s *expressionScenario) {
// Skip comment-related tests for goccy as it handles comment placement more strictly
if s.description == "Sort descending by string field, with comments" && ConfiguredYamlPreferences.UseGoccyParser {
t.Skip("goccy parser handles trailing comments more strictly - structurally equivalent but different comment handling")
return
}
testScenario(t, s)
}
func TestReverseOperatorScenarios(t *testing.T) {
for _, tt := range reverseOperatorScenarios {
testReverseScenarioWithParserCheck(t, &tt)
testScenario(t, &tt)
}
documentOperatorScenarios(t, "reverse", reverseOperatorScenarios)
}

View File

@ -170,21 +170,13 @@ var sortByOperatorScenarios = []expressionScenario{
expected: []string{
"D0, P[], (!!seq)::# abc\n- def\n# ghi\n",
},
skipForGoccy: true,
},
}
func testSortScenarioWithParserCheck(t *testing.T, s *expressionScenario) {
// Skip trailing comment test for goccy as it handles comments more strictly
if s.description == "head comment" && ConfiguredYamlPreferences.UseGoccyParser {
t.Skip("goccy parser handles trailing comments differently - structurally equivalent but different comment placement")
return
}
testScenario(t, s)
}
func TestSortByOperatorScenarios(t *testing.T) {
func TestSortOperatorScenarios(t *testing.T) {
for _, tt := range sortByOperatorScenarios {
testSortScenarioWithParserCheck(t, &tt)
testScenario(t, &tt)
}
documentOperatorScenarios(t, "sort", sortByOperatorScenarios)
}

View File

@ -52,6 +52,7 @@ var styleOperatorScenarios = []expressionScenario{
expected: []string{
"D0, P[], (!!map)::'bing': &foo {'x': 'z'}\n'a':\n 'c': 'cat'\n !!merge <<: [*foo]\n",
},
skipForGoccy: true,
},
{
description: "Set single quote style",
@ -169,21 +170,9 @@ g:
},
}
func testStyleScenarioWithParserCheck(t *testing.T, s *expressionScenario) {
// Skip tests where goccy correctly rejects invalid YAML at parse time
if ConfiguredYamlPreferences.UseGoccyParser {
// Check if the document contains merge anchor with sequence (invalid YAML)
if s.document == "bing: &foo frog\na:\n c: cat\n <<: [*foo]" {
t.Skip("goccy parser correctly rejects merge anchors with sequences at parse time")
return
}
}
testScenario(t, s)
}
func TestStyleOperatorScenarios(t *testing.T) {
for _, tt := range styleOperatorScenarios {
testStyleScenarioWithParserCheck(t, &tt)
testScenario(t, &tt)
}
documentOperatorScenarios(t, "style", styleOperatorScenarios)
}

View File

@ -159,21 +159,9 @@ var subtractOperatorScenarios = []expressionScenario{
},
}
func testSubtractScenarioWithParserCheck(t *testing.T, s *expressionScenario) {
// Skip datetime arithmetic tests for goccy as it requires explicit timestamp tagging
// while yaml.v3 auto-detects ISO8601 strings as timestamps
if ConfiguredYamlPreferences.UseGoccyParser {
if s.description == "Date subtraction" || s.description == "Date subtraction - only date" {
t.Skip("goccy parser requires explicit timestamp tagging for datetime arithmetic - more YAML spec compliant")
return
}
}
testScenario(t, s)
}
func TestSubtractOperatorScenarios(t *testing.T) {
for _, tt := range subtractOperatorScenarios {
testSubtractScenarioWithParserCheck(t, &tt)
testScenario(t, &tt)
}
documentOperatorScenarios(t, "subtract", subtractOperatorScenarios)
}

View File

@ -558,24 +558,9 @@ var traversePathOperatorScenarios = []expressionScenario{
},
}
func testTraverseScenarioWithParserCheck(t *testing.T, s *expressionScenario) {
// Skip tests where goccy correctly rejects invalid YAML at parse time
if ConfiguredYamlPreferences.UseGoccyParser {
if s.description == "strange map with key but no value" {
t.Skip("goccy parser correctly rejects malformed YAML with explicit null tag followed by sequence")
return
}
if s.expectedError == "can only use merge anchors with maps (!!map), but got !!seq" {
t.Skip("goccy parser correctly rejects merge anchors with sequences at parse time")
return
}
}
testScenario(t, s)
}
func TestTraversePathOperatorScenarios(t *testing.T) {
for _, tt := range traversePathOperatorScenarios {
testTraverseScenarioWithParserCheck(t, &tt)
testScenario(t, &tt)
}
documentOperatorScenarios(t, "traverse-read", traversePathOperatorScenarios)
}

View File

@ -103,18 +103,9 @@ var uniqueOperatorScenarios = []expressionScenario{
},
}
func testUniqueScenarioWithParserCheck(t *testing.T, s *expressionScenario) {
// Skip comment-related tests for goccy as it handles comment placement more strictly
if s.document == "# abc\n[{name: harry, pet: cat}, {pet: fish}, {name: harry, pet: dog}]\n# xyz" && ConfiguredYamlPreferences.UseGoccyParser {
t.Skip("goccy parser handles trailing comments more strictly - structurally equivalent but different comment handling")
return
}
testScenario(t, s)
}
func TestUniqueOperatorScenarios(t *testing.T) {
for _, tt := range uniqueOperatorScenarios {
testUniqueScenarioWithParserCheck(t, &tt)
testScenario(t, &tt)
}
documentOperatorScenarios(t, "unique", uniqueOperatorScenarios)
}