Files
mikefarah-yq/pkg/yqlib/operator_equals_test.go
dualfroz 699d0e35c0 fix(equals): compare integers by value, not by raw text
The == and != operators matched the raw text of two scalars, so the same
integer written in a different base never matched: `0x10 == 16`,
`0o644 == 420` and `1_000 == 1000` were all false.

This contradicted yq's own comparison operators, which parse those
formats and agree the values are equal - for `0o644`, both `> 419` and
`< 421` are true, yet `== 420` was false. It also contradicted the
documented "Match number" behaviour of `==`.

Give ==/!= the same !!int/!!int branch the comparison operators already
have: parse both sides with the existing parseInt64 helper and compare
the resulting int64s. If either side does not fit in an int64, fall back
to the previous raw match rather than raising a new error. String
globbing, floats and null handling are unchanged.

Fixes #1977
2026-09-03 13:29:22 +02:00

247 lines
5.1 KiB
Go

package yqlib
import (
"testing"
)
var equalsOperatorScenarios = []expressionScenario{
{
skipDoc: true,
expression: ".a == .b",
expected: []string{
"D0, P[a], (!!bool)::true\n",
},
},
{
expression: `(.k | length) == 0`,
skipDoc: true,
expected: []string{
"D0, P[k], (!!bool)::true\n",
},
},
{
skipDoc: true,
document: `a: cat`,
expression: ".a == .b",
expected: []string{
"D0, P[a], (!!bool)::false\n",
},
},
{
skipDoc: true,
document: `a: cat`,
expression: ".b == .a",
expected: []string{
"D0, P[b], (!!bool)::false\n",
},
},
{
skipDoc: true,
document: "cat",
document2: "dog",
expression: "select(fi==0) == select(fi==1)",
expected: []string{
"D0, P[], (!!bool)::false\n",
},
},
{
skipDoc: true,
document: "{}",
expression: "(.a == .b) as $x | .",
expected: []string{
"D0, P[], (!!map)::{}\n",
},
},
{
skipDoc: true,
document: "{}",
expression: ".a == .b",
expected: []string{
"D0, P[a], (!!bool)::true\n",
},
},
{
skipDoc: true,
document: "{}",
expression: "(.a != .b) as $x | .",
expected: []string{
"D0, P[], (!!map)::{}\n",
},
},
{
skipDoc: true,
document: "{}",
expression: ".a != .b",
expected: []string{
"D0, P[], (!!bool)::false\n",
},
},
{
skipDoc: true,
document: "{a: {b: 10}}",
expression: "select(.c != null)",
expected: []string{},
},
{
skipDoc: true,
document: "{a: {b: 10}}",
expression: "select(.d == .c)",
expected: []string{
"D0, P[], (!!map)::{a: {b: 10}}\n",
},
},
{
skipDoc: true,
document: "{a: {b: 10}}",
expression: "select(null == .c)",
expected: []string{
"D0, P[], (!!map)::{a: {b: 10}}\n",
},
},
{
skipDoc: true,
document: "{a: { b: {things: \"\"}, f: [1], g: [] }}",
expression: ".. | select(. == \"\")",
expected: []string{
"D0, P[a b things], (!!str)::\n",
},
},
{
description: "Match string",
document: `[cat,goat,dog]`,
expression: `.[] | (. == "*at")`,
expected: []string{
"D0, P[0], (!!bool)::true\n",
"D0, P[1], (!!bool)::true\n",
"D0, P[2], (!!bool)::false\n",
},
},
{
description: "Don't match string",
document: `[cat,goat,dog]`,
expression: `.[] | (. != "*at")`,
expected: []string{
"D0, P[0], (!!bool)::false\n",
"D0, P[1], (!!bool)::false\n",
"D0, P[2], (!!bool)::true\n",
},
},
{
description: "Match number",
document: `[3, 4, 5]`,
expression: `.[] | (. == 4)`,
expected: []string{
"D0, P[0], (!!bool)::false\n",
"D0, P[1], (!!bool)::true\n",
"D0, P[2], (!!bool)::false\n",
},
},
{
description: "Don't match number",
document: `[3, 4, 5]`,
expression: `.[] | (. != 4)`,
expected: []string{
"D0, P[0], (!!bool)::true\n",
"D0, P[1], (!!bool)::false\n",
"D0, P[2], (!!bool)::true\n",
},
},
{
description: "Match number written in a different base",
subdescription: "Integers are compared by value, so the hexadecimal and octal forms of 16 match the decimal form.",
document: `[0x10, 0o20, 16]`,
expression: `.[] | (. == 16)`,
expected: []string{
"D0, P[0], (!!bool)::true\n",
"D0, P[1], (!!bool)::true\n",
"D0, P[2], (!!bool)::true\n",
},
},
{
skipDoc: true,
document: `[0x10, 0o20, 16]`,
expression: `.[] | (. != 16)`,
expected: []string{
"D0, P[0], (!!bool)::false\n",
"D0, P[1], (!!bool)::false\n",
"D0, P[2], (!!bool)::false\n",
},
},
{
skipDoc: true,
document: `[1_000, 1000]`,
expression: `.[] | (. == 1000)`,
expected: []string{
"D0, P[0], (!!bool)::true\n",
"D0, P[1], (!!bool)::true\n",
},
},
{
skipDoc: true,
document: `[0x10, 17]`,
expression: `.[] | (. == 16)`,
expected: []string{
"D0, P[0], (!!bool)::true\n",
"D0, P[1], (!!bool)::false\n",
},
},
{
// integers that don't fit in an int64 can't be compared numerically,
// fall back to matching the raw values.
skipDoc: true,
document: `[9223372036854775808, 9223372036854775808]`,
expression: `.[0] == .[1]`,
expected: []string{
"D0, P[0], (!!bool)::true\n",
},
},
{
skipDoc: true,
document: `a: { cat: {b: apple, c: whatever}, pat: {b: banana} }`,
expression: `.a | (.[].b == "apple")`,
expected: []string{
"D0, P[a cat b], (!!bool)::true\n",
"D0, P[a pat b], (!!bool)::false\n",
},
},
{
skipDoc: true,
document: ``,
expression: `null == null`,
expected: []string{
"D0, P[], (!!bool)::true\n",
},
},
{
description: "Match nulls",
document: ``,
expression: `null == ~`,
expected: []string{
"D0, P[], (!!bool)::true\n",
},
},
{
description: "Non existent key doesn't equal a value",
document: "a: frog",
expression: `select(.b != "thing")`,
expected: []string{
"D0, P[], (!!map)::a: frog\n",
},
},
{
description: "Two non existent keys are equal",
document: "a: frog",
expression: `select(.b == .c)`,
expected: []string{
"D0, P[], (!!map)::a: frog\n",
},
},
}
func TestEqualOperatorScenarios(t *testing.T) {
for _, tt := range equalsOperatorScenarios {
testScenario(t, &tt)
}
documentOperatorScenarios(t, "equals", equalsOperatorScenarios)
}