diff --git a/tests/simple/testdata/string_ext.textproto b/tests/simple/testdata/string_ext.textproto index 7202a88..a87c950 100644 --- a/tests/simple/testdata/string_ext.textproto +++ b/tests/simple/testdata/string_ext.textproto @@ -368,6 +368,669 @@ section: { } } } +section: { + name: "format" + test: { + name: "no-op" + expr: '"no substitution".format([])' + value: { + string_value: 'no substitution', + } + } + test: { + name: "mid-string substitution" + expr: '"str is %s and some more".format(["filler"])' + value: { + string_value: 'str is filler and some more', + } + } + test: { + name: "percent escaping" + expr: '"%% and also %%".format([])' + value: { + string_value: '% and also %', + } + } + test: { + name: "substution inside escaped percent signs" + expr: '"%%%s%%".format(["text"])' + value: { + string_value: '%text%', + } + } + test: { + name: "substitution with one escaped percent sign on the right" + expr: '"%s%%".format(["percent on the right"])' + value: { + string_value: 'percent on the right%', + } + } + test: { + name: "substitution with one escaped percent sign on the left" + expr: '"%%%s".format(["percent on the left"])' + value: { + string_value: '%percent on the left', + } + } + test: { + name: "multiple substitutions" + expr: '"%d %d %d, %s %s %s, %d %d %d, %s %s %s".format([1, 2, 3, "A", "B", "C", 4, 5, 6, "D", "E", "F"])' + value: { + string_value: '1 2 3, A B C, 4 5 6, D E F', + } + } + test: { + name: "percent sign escape sequence support" + expr: '"%%escaped %s%%".format(["percent"])' + value: { + string_value: '%escaped percent%', + } + } + test: { + name: "fixed point formatting clause" + expr: '"%.3f".format([1.2345])' + value: { + string_value: '1.234', + } + } + test: { + name: "binary formatting clause" + expr: '"this is 5 in binary: %b".format([5])' + value: { + string_value: 'this is 5 in binary: 101', + } + } + test: { + name: "uint support for binary formatting" + expr: '"unsigned 64 in binary: %b".format([uint(64)])' + value: { + string_value: 'unsigned 64 in binary: 1000000', + } + } + test: { + name: "bool support for binary formatting" + expr: '"bit set from bool: %b".format([true])' + value: { + string_value: 'bit set from bool: 1', + } + } + test: { + name: "octal formatting clause" + expr: '"%o".format([11])' + value: { + string_value: '13', + } + } + test: { + name: "uint support for octal formatting clause" + expr: '"this is an unsigned octal: %o".format([uint(65535)])' + value: { + string_value: 'this is an unsigned octal: 177777', + } + } + test: { + name: "lowercase hexadecimal formatting clause" + expr: '"%x is 20 in hexadecimal".format([30])' + value: { + string_value: '1e is 20 in hexadecimal', + } + } + test: { + name: "uppercase hexadecimal formatting clause" + expr: '"%X is 20 in hexadecimal".format([30])' + value: { + string_value: '1E is 20 in hexadecimal', + } + } + test: { + name: "unsigned support for hexadecimal formatting clause" + expr: '"%X is 6000 in hexadecimal".format([uint(6000)])' + value: { + string_value: '1770 is 6000 in hexadecimal', + } + } + test: { + name: "string support with hexadecimal formatting clause" + expr: '"%x".format(["Hello world!"])' + value: { + string_value: '48656c6c6f20776f726c6421', + } + } + test: { + name: "string support with uppercase hexadecimal formatting clause" + expr: '"%X".format(["Hello world!"])' + value: { + string_value: '48656C6C6F20776F726C6421', + } + } + test: { + name: "byte support with hexadecimal formatting clause" + expr: '"%x".format([b"byte string"])' + value: { + string_value: '6279746520737472696e67', + } + } + test: { + name: "byte support with uppercase hexadecimal formatting clause" + expr: '"%X".format([b"byte string"])' + value: { + string_value: '6279746520737472696E67', + } + } + test: { + name: "scientific notation formatting clause" + expr: '"%.6e".format([1052.032911275])' + value: { + string_value: '1.052033 × 10⁰³', + } + } + test: { + name: "default precision for fixed-point clause" + expr: '"%f".format([2.71828])' + value: { + string_value: '2.718280', + } + } + test: { + name: "default precision for scientific notation" + expr: '"%e".format([2.71828])' + value: { + string_value: '2.718280 × 10⁰⁰', + } + } + test: { + name: "unicode output for scientific notation" + expr: '"unescaped unicode: %e, escaped unicode: %e".format([2.71828, 2.71828])' + value: { + string_value: 'unescaped unicode: 2.718280 × 10⁰⁰, escaped unicode: 2.718280\u202f\u00d7\u202f10\u2070\u2070', + } + } + test: { + name: "NaN support for fixed-point" + expr: '"%f".format(["NaN"])' + value: { + string_value: 'NaN', + } + } + test: { + name: "positive infinity support for fixed-point" + expr: '"%f".format(["Infinity"])' + value: { + string_value: '∞', + } + } + test: { + name: "negative infinity support for fixed-point" + expr: '"%f".format(["-Infinity"])' + value: { + string_value: '-∞', + } + } + test: { + name: "uint support for decimal clause" + expr: '"%d".format([uint(64)])' + value: { + string_value: '64', + } + } + test: { + name: "null support for string" + expr: '"null: %s".format([null])' + value: { + string_value: 'null: null', + } + } + test: { + name: "int support for string" + expr: '"%s".format([999999999999])' + value: { + string_value: '999999999999', + } + } + test: { + name: "bytes support for string" + expr: '"some bytes: %s".format([b"xyz"])' + value: { + string_value: 'some bytes: xyz', + } + } + test: { + name: "type() support for string" + expr: '"type is %s".format([type("test string")])' + value: { + string_value: 'type is string', + } + } + test: { + name: "timestamp support for string" + expr: '"%s".format([timestamp("2023-02-03T23:31:20+00:00")])' + value: { + string_value: '2023-02-03T23:31:20Z', + } + } + test: { + name: "duration support for string" + expr: '"%s".format([duration("1h45m47s")])' + value: { + string_value: '6347s', + } + } + test: { + name: "list support for string" + expr: '"%s".format([["abc", 3.14, null, [9, 8, 7, 6], timestamp("2023-02-03T23:31:20Z")]])' + value: { + string_value: '["abc", 3.140000, null, [9, 8, 7, 6], timestamp("2023-02-03T23:31:20Z")]', + } + } + test: { + name: "map support for string" + expr: '"%s".format([{"key1": b"xyz", "key5": null, "key2": duration("2h"), "key4": true, "key3": 2.71828}])' + value: { + string_value: '{"key1":b"xyz", "key2":duration("7200s"), "key3":2.718280, "key4":true, "key5":null}', + } + } + test: { + name: "map support (all key types)" + expr: '"map with multiple key types: %s".format([{1: "value1", uint(2): "value2", true: double("NaN")}])' + value: { + string_value: 'map with multiple key types: {1:"value1", 2:"value2", true:"NaN"}', + } + } + test: { + name: "boolean support for %s" + expr: '"true bool: %s, false bool: %s".format([true, false])' + value: { + string_value: 'true bool: true, false bool: false', + } + } + test: { + name: "dyntype support for string formatting clause" + expr: '"dynamic string: %s".format([dyn("a string")])' + value: { + string_value: 'dynamic string: a string', + } + } + test: { + name: "dyntype support for numbers with string formatting clause" + expr: '"dynIntStr: %s dynDoubleStr: %s".format([dyn(32), dyn(56.8)])' + value: { + string_value: 'dynIntStr: 32 dynDoubleStr: 56.8', + } + } + test: { + name: "dyntype support for integer formatting clause" + expr: '"dynamic int: %d".format([dyn(128)])' + value: { + string_value: 'dynamic int: 128', + } + } + test: { + name: "dyntype support for integer formatting clause (unsigned)" + expr: '"dynamic unsigned int: %d".format([dyn(256u)])' + value: { + string_value: 'dynamic unsigned int: 256', + } + } + test: { + name: "dyntype support for hex formatting clause" + expr: '"dynamic hex int: %x".format([dyn(22)])' + value: { + string_value: 'dynamic hex int: 16', + } + } + test: { + name: "dyntype support for hex formatting clause (uppercase)" + expr: '"dynamic hex int: %X (uppercase)".format([dyn(26)])' + value: { + string_value: 'dynamic hex int: 1A (uppercase)', + } + } + test: { + name: "dyntype support for unsigned hex formatting clause" + expr: '"dynamic hex int: %x (unsigned)".format([dyn(500u)])' + value: { + string_value: 'dynamic hex int: 1f4 (unsigned)', + } + } + test: { + name: "dyntype support for fixed-point formatting clause" + expr: '"dynamic double: %.3f".format([dyn(4.5)])' + value: { + string_value: 'dynamic double: 4.500', + } + } + test: { + name: "dyntype support for scientific notation" + expr: '"(dyntype) e: %e".format([dyn(2.71828)])' + value: { + string_value: '(dyntype) e: 2.718280 × 10⁰⁰', + } + } + test: { + name: "dyntype NaN/infinity support for fixed-point" + expr: '"NaN: %f, infinity: %f".format([dyn("NaN"), dyn("Infinity")])' + value: { + string_value: 'NaN: NaN, infinity: ∞', + } + } + test: { + name: "dyntype support for timestamp" + expr: '"dyntype timestamp: %s".format([dyn(timestamp("2009-11-10T23:00:00Z"))])' + value: { + string_value: 'dyntype timestamp: 2009-11-10T23:00:00Z', + } + } + test: { + name: "dyntype support for duration" + expr: '"dyntype duration: %s".format([dyn(duration("8747s"))])' + value: { + string_value: 'dyntype duration: 8747s', + } + } + test: { + name: "dyntype support for lists" + expr: '"dyntype list: %s".format([dyn([6, 4.2, "a string"])])' + value: { + string_value: 'dyntype list: [6, 4.200000, "a string"]', + } + } + test: { + name: "dyntype support for maps" + expr: '"dyntype map: %s".format([{"strKey":"x", 6:duration("422s"), true:42}])' + value: { + string_value: 'dyntype map: {"strKey":"x", 6:duration("422s"), true:42}', + } + } + test: { + name: "message field support" + expr: '"message field msg.single_int32: %d, msg.single_double: %.1f".format([2, 1.0])' + value: { + string_value: 'message field msg.single_int32: 2, msg.single_double: 1.0', + } + } + test: { + name: "string substitution in a string variable" + expr: 'str_var.format(["filler"])' + type_env: { + name: "str_var", + ident: { type: { primitive: STRING } } + } + bindings: { + key: "str_var" + value: { value: { string_value: "str is %s and some more" } } + } + value: { + string_value: 'str is filler and some more', + } + } + test: { + name: "multiple substitutions in a string variable" + expr: 'str_var.format([1, 2, 3, "A", "B", "C", 4, 5, 6, "D", "E", "F"])' + type_env: { + name: "str_var", + ident: { type: { primitive: STRING } } + } + bindings: { + key: "str_var" + value: { value: { string_value: "%d %d %d, %s %s %s, %d %d %d, %s %s %s" } } + } + value: { + string_value: '1 2 3, A B C, 4 5 6, D E F', + } + } + test: { + name: "substution inside escaped percent signs in a string variable" + expr: 'str_var.format(["text"])' + type_env: { + name: "str_var", + ident: { type: { primitive: STRING } } + } + bindings: { + key: "str_var" + value: { value: { string_value: "%%%s%%" } } + } + value: { + string_value: '%text%', + } + } + test: { + name: "fixed point formatting clause in a string variable" + expr: 'str_var.format([1.2345])' + type_env: { + name: "str_var", + ident: { type: { primitive: STRING } } + } + bindings: { + key: "str_var" + value: { value: { string_value: "%.3f" } } + } + value: { + string_value: '1.234', + } + } + test: { + name: "binary formatting clause in a string variable" + expr: 'str_var.format([5])' + type_env: { + name: "str_var", + ident: { type: { primitive: STRING } } + } + bindings: { + key: "str_var" + value: { value: { string_value: "this is 5 in binary: %b" } } + } + value: { + string_value: 'this is 5 in binary: 101', + } + } + test: { + name: "scientific notation formatting clause in a string variable" + expr: 'str_var.format([1052.032911275])' + type_env: { + name: "str_var", + ident: { type: { primitive: STRING } } + } + bindings: { + key: "str_var" + value: { value: { string_value: "%.6e" } } + } + value: { + string_value: '1.052033 × 10⁰³', + } + } + test: { + name: "default precision for fixed-point clause in a string variable" + expr: 'str_var.format([2.71828])' + type_env: { + name: "str_var", + ident: { type: { primitive: STRING } } + } + bindings: { + key: "str_var" + value: { value: { string_value: "%f" } } + } + value: { + string_value: '2.718280', + } + } +} +section: { + name: "format_errors" + test: { + name: "multiline" + expr: "strings.quote(\"first\\nsecond\") == \"\\\"first\\\\nsecond\\\"\"" + } + test: { + name: "unrecognized formatting clause" + expr: '"%a".format([1])' + disable_check: true + eval_error: { + errors: { + message: 'could not parse formatting clause: unrecognized formatting clause "a"' + } + } + } + test: { + name: "out of bounds arg index" + expr: '"%d %d %d".format([0, 1])' + disable_check: true + eval_error: { + errors: { + message: 'index 2 out of range' + } + } + } + test: { + name: "string substitution is not allowed with binary clause" + expr: '"string is %b".format(["abc"])' + disable_check: true + eval_error: { + errors: { + message: 'error during formatting: only integers and bools can be formatted as binary, was given string' + } + } + } + test: { + name: "duration substitution not allowed with decimal clause" + expr: '"%d".format([duration("30m2s")])' + disable_check: true + eval_error: { + errors: { + message: 'error during formatting: decimal clause can only be used on integers, was given google.protobuf.Duration' + } + } + } + test: { + name: "string substitution not allowed with octal clause" + expr: '"octal: %o".format(["a string"])' + disable_check: true + eval_error: { + errors: { + message: 'error during formatting: octal clause can only be used on integers, was given string' + } + } + } + test: { + name: "double substitution not allowed with hex clause" + expr: '"double is %x".format([0.5])' + disable_check: true + eval_error: { + errors: { + message: 'error during formatting: only integers, byte buffers, and strings can be formatted as hex, was given double' + } + } + } + test: { + name: "uppercase not allowed for scientific clause" + expr: '"double is %E".format([0.5])' + disable_check: true + eval_error: { + errors: { + message: 'could not parse formatting clause: unrecognized formatting clause "E"' + } + } + } + test: { + name: "object not allowed" + expr: '"object is %s".format([google.api.expr.test.v1.proto3.TestAllTypes{}])' + disable_check: true + eval_error: { + errors: { + message: 'error during formatting: string clause can only be used on strings, bools, bytes, ints, doubles, maps, lists, types, durations, and timestamps, was given google.api.expr.test.v1.proto3.TestAllTypes' + } + } + } + test: { + name: "object inside list" + expr: '"%s".format([[1, 2, google.api.expr.test.v1.proto3.TestAllTypes{}]])' + disable_check: true + eval_error: { + errors: { + message: 'error during formatting: string clause can only be used on strings, bools, bytes, ints, doubles, maps, lists, types, durations, and timestamps, was given google.api.expr.test.v1.proto3.TestAllTypes' + } + } + } + test: { + name: "object inside map" + expr: '"%s".format([{1: "a", 2: google.api.expr.test.v1.proto3.TestAllTypes{}}])' + disable_check: true + eval_error: { + errors: { + message: 'error during formatting: string clause can only be used on strings, bools, bytes, ints, doubles, maps, lists, types, durations, and timestamps, was given google.api.expr.test.v1.proto3.TestAllTypes' + } + } + } + test: { + name: "null not allowed for %d" + expr: '"null: %d".format([null])' + disable_check: true + eval_error: { + errors: { + message: 'error during formatting: decimal clause can only be used on integers, was given null_type' + } + } + } + test: { + name: "null not allowed for %e" + expr: '"null: %e".format([null])' + disable_check: true + eval_error: { + errors: { + message: 'error during formatting: scientific clause can only be used on doubles, was given null_type' + } + } + } + test: { + name: "null not allowed for %f" + expr: '"null: %f".format([null])' + disable_check: true + eval_error: { + errors: { + message: 'error during formatting: fixed-point clause can only be used on doubles, was given null_type' + } + } + } + test: { + name: "null not allowed for %x" + expr: '"null: %x".format([null])' + disable_check: true + eval_error: { + errors: { + message: 'error during formatting: only integers, byte buffers, and strings can be formatted as hex, was given null_type' + } + } + } + test: { + name: "null not allowed for %X" + expr: '"null: %X".format([null])' + disable_check: true + eval_error: { + errors: { + message: 'error during formatting: only integers, byte buffers, and strings can be formatted as hex, was given null_type' + } + } + } + test: { + name: "null not allowed for %b" + expr: '"null: %b".format([null])' + disable_check: true + eval_error: { + errors: { + message: 'error during formatting: only integers and bools can be formatted as binary, was given null_type' + } + } + } + test: { + name: "null not allowed for %o" + expr: '"null: %o".format([null])' + disable_check: true + eval_error: { + errors: { + message: 'error during formatting: octal clause can only be used on integers, was given null_type' + } + } + } +} section: { name: "value_errors" test: {