blob: e8eff4ecf9842226101b8bc065b0ba5ba840e554 [file] [log] [blame] [edit]
// Open source parser tests.
// N.b.: take care when editing this file, as it contains significant trailing whitespace.
package parser
import (
"fmt"
"strings"
"testing"
"github.com/kylelemons/godebug/diff"
"github.com/kylelemons/godebug/pretty"
"github.com/protocolbuffers/txtpbfmt/config"
)
func TestError(t *testing.T) {
inputs := []struct {
in string
err string
}{{
in: "list_wrong_end: [one, two}",
}, {
in: "multy_string_list: ['a' 'b', 'c']",
err: "multiple-string value",
}, {
in: `mapping {{
my_proto_field: "foo"
}}`, err: "Failed to find a FieldName",
}, {
in: `name: "string with literal new line
"`, err: "new line",
}, {
in: `# txtpbfmt: off`,
err: "unterminated txtpbfmt off",
}}
for _, input := range inputs {
out, err := Format([]byte(input.in))
if err == nil {
nodes, err := Parse([]byte(input.in))
if err != nil {
t.Errorf("Parse %v returned err %v", input.in, err)
continue
}
t.Errorf("Expected a formatting error but none was raised while formatting:\n%v\nout\n%s\ntree\n%s\n", input.in, out, DebugFormat(nodes, 0))
continue
}
if !strings.Contains(err.Error(), input.err) {
t.Errorf(
"Expected a formatting error containing \"%v\",\n error was: \"%v\"\nwhile formatting:\n%v", input.err, err, input.in)
}
}
}
func TestDisable(t *testing.T) {
inputs := []string{
`#txtpbfmt:disable
some{random:field}`,
`#a
#b
# txtpbfmt: disable
some{random:field}`,
}
for _, input := range inputs {
output, err := Format([]byte(input))
if err != nil {
t.Errorf("%v: %v", err, input)
continue
}
if diff := pretty.Compare(input, string(output)); diff != "" {
t.Errorf("disable ineffective (-want, +have):\n%s", diff)
}
}
}
func TestFormat(t *testing.T) {
inputs := []struct {
name string
in string
out string
}{{
name: "file comment + block comment",
in: `# file comment 1
# file comment 2
# presubmit comment 1
# presubmit comment 2
presubmit: {
# review comment 1
# review comment 2
review_notify: "address" # review same line comment 1
# extra comment block 1 line 1
# extra comment block 1 line 2
# extra comment block 2 line 1
# extra comment block 2 line 2
}
`,
out: `# file comment 1
# file comment 2
# presubmit comment 1
# presubmit comment 2
presubmit: {
# review comment 1
# review comment 2
review_notify: "address" # review same line comment 1
# extra comment block 1 line 1
# extra comment block 1 line 2
# extra comment block 2 line 1
# extra comment block 2 line 2
}
`}, {
name: "2x file comment",
in: `# file comment block 1 line 1
# file comment block 1 line 2
# file comment block 2 line 1
# file comment block 2 line 2
presubmit: {}
`,
out: `# file comment block 1 line 1
# file comment block 1 line 2
# file comment block 2 line 1
# file comment block 2 line 2
presubmit: {}
`}, {
name: "much blank space",
in: `
# presubmit comment 1
# presubmit comment 2
presubmit : {
# review comment 1
# review comment 2
review_notify : "address" # review same line comment 2
}
`,
out: `# presubmit comment 1
# presubmit comment 2
presubmit: {
# review comment 1
# review comment 2
review_notify: "address" # review same line comment 2
}
`}, {
name: "empty children",
in: `
# file comment 1
# file comment 2
# presubmit comment 1
# presubmit comment 2
presubmit: {
}
`,
out: `# file comment 1
# file comment 2
# presubmit comment 1
# presubmit comment 2
presubmit: {
}
`}, {
name: "list notation with []",
in: `
presubmit: {
check_tests: {
action: [ MAIL, REVIEW, SUBMIT ]
}
}
`,
out: `presubmit: {
check_tests: {
action: [MAIL, REVIEW, SUBMIT]
}
}
`}, {
name: "list notation with [{}] with lots of comments",
in: `# list comment
list: [
# first comment
{}, # first inline comment
# second comment
{} # second inline comment
# last comment
] # list inline comment
# other comment`,
out: `# list comment
list: [
# first comment
{}, # first inline comment
# second comment
{} # second inline comment
# last comment
] # list inline comment
# other comment
`}, {
name: "list notation with [{}] with inline children",
in: `children: [ { name: "node_2.1" } , { name: "node_2.2" },{name:"node_2.3"}]
`,
out: `children: [ { name: "node_2.1" }, { name: "node_2.2" }, { name: "node_2.3" } ]
`}, {
name: "list notation with [{}] without comma separators between multiline children",
in: `children: [
{
name: "node_2.1"
}
{
name: "node_2.2"
}
{
name: "node_2.3"
}
]
`,
out: `children: [
{
name: "node_2.1"
},
{
name: "node_2.2"
},
{
name: "node_2.3"
}
]
`}, {
name: "list notation with [{}]",
in: `children: [
# Line 1
# Line 2
# Line 3
{
name: "node_1"
},
{
name: "node_2"
children: [ { name: "node_2.1" }, {name:"node_2.2"},{name:"node_2.3" }]
},
{
name: "node_3"
children : [
{
name: "node_3.1"
}, # after-node comment.
# Line 1
# Line 2
{
name: "node_3.2",
},
{
name: "node_3.3"
}
]
}
]
`,
out: `children: [
# Line 1
# Line 2
# Line 3
{
name: "node_1"
},
{
name: "node_2"
children: [ { name: "node_2.1" }, { name: "node_2.2" }, { name: "node_2.3" } ]
},
{
name: "node_3"
children: [
{
name: "node_3.1"
}, # after-node comment.
# Line 1
# Line 2
{
name: "node_3.2"
},
{
name: "node_3.3"
}
]
}
]
`}, {
name: "multiline string",
in: `
name: "Foo"
description:
"Foo is an open-source, scalable, and efficient storage solution "
"for the web. It is based on MySQLβ€”so it supports major MySQL features "
"like transactions, indexes, and joinsβ€”but it also provides the scalability "
"of NoSQL. As such, Foo offers the best of both the RDBMS and NoSQL "
"worlds."
`,
out: `name: "Foo"
description:
"Foo is an open-source, scalable, and efficient storage solution "
"for the web. It is based on MySQLβ€”so it supports major MySQL features "
"like transactions, indexes, and joinsβ€”but it also provides the scalability "
"of NoSQL. As such, Foo offers the best of both the RDBMS and NoSQL "
"worlds."
`}, {
name: "escaped \"",
in: `
check_contents: {
required_regexp: "\\s*syntax\\s*=\\s*\".*\""
}
`,
out: `check_contents: {
required_regexp: "\\s*syntax\\s*=\\s*\".*\""
}
`}, {
name: "single-quote inside a double-quote-delimited string (and vice-versa)",
in: `
description:
"foo's fork of mod_python's Cookie submodule. "
"as well as \"marshalled\" cookies -- cookies that contain marshalled python "
'double quote " inside single-quote-delimited string'
`,
out: `description:
"foo's fork of mod_python's Cookie submodule. "
"as well as \"marshalled\" cookies -- cookies that contain marshalled python "
"double quote \" inside single-quote-delimited string"
`}, {
name: "list with inline comments; comments after list and after last child",
in: `
tricorder: {
options: {
build_args: [
# Other build_args comment.
# LT.IfChange
"first line",
# LT.ThenChange(//foo)
"--config=android_x86", # Inline comment for android_x86.
"--config=android_release" # Inline comment for last child.
# Comment after list.
]
}
}
`,
out: `tricorder: {
options: {
build_args: [
# Other build_args comment.
# LT.IfChange
"first line",
# LT.ThenChange(//foo)
"--config=android_x86", # Inline comment for android_x86.
"--config=android_release" # Inline comment for last child.
# Comment after list.
]
}
}
`}, {
name: "';' at end of value",
in: `name: "value";`,
out: `name: "value"
`}, {
name: "multi-line string with inline comments",
in: `# cm
options:
"first line" # first comment
"second line" # second comment
`,
out: `# cm
options:
"first line" # first comment
"second line" # second comment
`}, {
name: "all kinds of inline comments",
in: `# presubmit pre comment 1
# presubmit pre comment 2
presubmit: {
# review pre comment 1
# review pre comment 2
review_notify: "review_notify_value" # review inline comment
# comment for project
project: [
# project1 pre comment 1
# project1 pre comment 2
"project1", # project1 inline comment
# project2 pre comment 1
# project2 pre comment 2
"project2" # project2 inline comment
# after comment 1
# after comment 2
]
# description pre comment 1
# description pre comment 2
description: "line1" # line1 inline comment
# line2 pre comment 1
# line2 pre comment 2
"line2" # line2 inline comment
# after comment 1
# after comment 2
name { name: value } # inline comment
} # inline comment
`,
out: `# presubmit pre comment 1
# presubmit pre comment 2
presubmit: {
# review pre comment 1
# review pre comment 2
review_notify: "review_notify_value" # review inline comment
# comment for project
project: [
# project1 pre comment 1
# project1 pre comment 2
"project1", # project1 inline comment
# project2 pre comment 1
# project2 pre comment 2
"project2" # project2 inline comment
# after comment 1
# after comment 2
]
# description pre comment 1
# description pre comment 2
description:
"line1" # line1 inline comment
# line2 pre comment 1
# line2 pre comment 2
"line2" # line2 inline comment
# after comment 1
# after comment 2
name { name: value } # inline comment
} # inline comment
`}, {
name: "more ';'",
in: `list_with_semicolon: [one, two];
string_with_semicolon: "str one";
multi_line_with_semicolon: "line 1"
"line 2";
other_name: other_value`,
out: `list_with_semicolon: [one, two]
string_with_semicolon: "str one"
multi_line_with_semicolon:
"line 1"
"line 2"
other_name: other_value
`}, {
name: "keep lists",
in: `list_two_items_inline: [one, two];
list_two_items: [one,
two];
list_one_item: [one]
list_one_item_multiline: [
one]
list_one_item_inline_comment: [one # with inline comment
]
list_one_item_pre_comment: [
# one item comment
one
]
list_one_item_post_comment: [
one
# post comment
]
list_no_item: []
list_no_item: [
]
# comment
list_no_item_comment: [
# as you can see there are no items
]
list_no_item_inline_comment: [] # Nothing here`,
out: `list_two_items_inline: [one, two]
list_two_items: [
one,
two
]
list_one_item: [one]
list_one_item_multiline: [
one
]
list_one_item_inline_comment: [
one # with inline comment
]
list_one_item_pre_comment: [
# one item comment
one
]
list_one_item_post_comment: [
one
# post comment
]
list_no_item: []
list_no_item: [
]
# comment
list_no_item_comment: [
# as you can see there are no items
]
list_no_item_inline_comment: [] # Nothing here
`}, {
name: "',' as field separator",
in: `# cm
presubmit: {
path_expression: "...",
other: "other"
}`,
out: `# cm
presubmit: {
path_expression: "..."
other: "other"
}
`}, {
name: "comment between name and value",
in: `
address: "address"
options:
# LT.IfChange
"first line"
# LT.ThenChange(//foo)
other: OTHER
`,
out: `address: "address"
options:
# LT.IfChange
"first line"
# LT.ThenChange(//foo)
other: OTHER
`}, {
name: "another example of comment between name and value",
in: `
address: # comment
"value"
options: # comment
"line 1"
"line 2"
`,
out: `address:
# comment
"value"
options:
# comment
"line 1"
"line 2"
`}, {
name: "new line with spaces between comment and value",
in: `
# comment
check_tests: {
}
`,
out: `# comment
check_tests: {
}
`}, {
name: "proto extension",
in: `[foo.bar.Baz] {
}`,
out: `[foo.bar.Baz] {
}
`}, {
name: "multiple nested in the same line",
in: `
expr {
union { userset { ref { relation: "_this" } } }
}
`,
out: `expr {
union { userset { ref { relation: "_this" } } }
}
`}, {
name: "comment on the last line without new line at the end of the file",
in: `name: "value" # comment`,
out: `name: "value" # comment
`}, {
name: "white space inside extension name",
in: `[foo.Bar.
Baz] {
name: "value"
}
`,
out: `[foo.Bar.Baz] {
name: "value"
}
`}, {
name: "one blank line at the end",
in: `presubmit {
}
`,
out: `presubmit {
}
`}, {
name: "template directive",
in: `[ext]: {
offset: %offset%
%if (offset < 0)% %for i : offset_count%
# directive comment
%if enabled%
# innermost comment
# innermost comment
offset_type: PACKETS
%end%
%end% %end%
# my comment
# my comment
%for (leading_timestamps : leading_timestamps_array)%
leading_timestamps: %leading_timestamps.timestamp%
%end%
}
`,
out: `[ext]: {
offset: %offset%
%if (offset < 0)%
%for i : offset_count%
# directive comment
%if enabled%
# innermost comment
# innermost comment
offset_type: PACKETS
%end%
%end%
%end%
# my comment
# my comment
%for (leading_timestamps : leading_timestamps_array)%
leading_timestamps: %leading_timestamps.timestamp%
%end%
}
`}, {
name: "template directive with >",
in: `[ext]: {
offset: %offset%
%if (offset > 0)% %for i : offset_count%
# directive comment
%if enabled%
# innermost comment
# innermost comment
offset_type: PACKETS
%end%
%end% %end%
# my comment
# my comment
%for (leading_timestamps : leading_timestamps_array)%
leading_timestamps: %leading_timestamps.timestamp%
%end%
}
`,
out: `[ext]: {
offset: %offset%
%if (offset > 0)%
%for i : offset_count%
# directive comment
%if enabled%
# innermost comment
# innermost comment
offset_type: PACKETS
%end%
%end%
%end%
# my comment
# my comment
%for (leading_timestamps : leading_timestamps_array)%
leading_timestamps: %leading_timestamps.timestamp%
%end%
}
`}, {
name: "template directive with >=",
in: `[ext]: {
offset: %offset%
%if (offset >= 0)% %for i : offset_count%
# directive comment
%if enabled%
# innermost comment
# innermost comment
offset_type: PACKETS
%end%
%end% %end%
# my comment
# my comment
%for (leading_timestamps : leading_timestamps_array)%
leading_timestamps: %leading_timestamps.timestamp%
%end%
}
`,
out: `[ext]: {
offset: %offset%
%if (offset >= 0)%
%for i : offset_count%
# directive comment
%if enabled%
# innermost comment
# innermost comment
offset_type: PACKETS
%end%
%end%
%end%
# my comment
# my comment
%for (leading_timestamps : leading_timestamps_array)%
leading_timestamps: %leading_timestamps.timestamp%
%end%
}
`}, {
name: "template directive escaped",
in: `node {
name: %"value \"% value"%
}
`,
out: `node {
name: %"value \"% value"%
}
`}, {
name: "no_directives (as opposed to next tests)",
in: `presubmit: { check_presubmit_service: { address: "address" failure_status: WARNING options: "options" } }
`,
out: `presubmit: { check_presubmit_service: { address: "address" failure_status: WARNING options: "options" } }
`}, {
name: "expand_all_children",
in: `# txtpbfmt: expand_all_children
presubmit: { check_presubmit_service: { address: "address" failure_status: WARNING options: "options" } }
`,
out: `# txtpbfmt: expand_all_children
presubmit: {
check_presubmit_service: {
address: "address"
failure_status: WARNING
options: "options"
}
}
`}, {
name: "skip_all_colons",
in: `# txtpbfmt: skip_all_colons
presubmit: { check_presubmit_service: { address: "address" failure_status: WARNING options: "options" } }
`,
out: `# txtpbfmt: skip_all_colons
presubmit { check_presubmit_service { address: "address" failure_status: WARNING options: "options" } }
`}, {
name: "separate_directives",
in: `# txtpbfmt: expand_all_children
# txtpbfmt: skip_all_colons
presubmit: { check_presubmit_service: { address: "address" failure_status: WARNING options: "options" } }
`,
out: `# txtpbfmt: expand_all_children
# txtpbfmt: skip_all_colons
presubmit {
check_presubmit_service {
address: "address"
failure_status: WARNING
options: "options"
}
}
`}, {
name: "combined_directives",
in: `# txtpbfmt: expand_all_children, skip_all_colons
presubmit: { check_presubmit_service: { address: "address" failure_status: WARNING options: "options" } }
`,
out: `# txtpbfmt: expand_all_children, skip_all_colons
presubmit {
check_presubmit_service {
address: "address"
failure_status: WARNING
options: "options"
}
}
`}, {
name: "preserve angle brackets",
in: `# txtpbfmt: preserve_angle_brackets
foo <
a: 1
>
foo {
b: 2
}
`,
out: `# txtpbfmt: preserve_angle_brackets
foo <
a: 1
>
foo {
b: 2
}
`,
}, {
name: "repeated proto format",
in: `{
a: 1;
b: 2}
{ a: 1;
b: 4
nested: {
# nested_string is optional
nested_string: "foo"
}
}
{ a: 1;
b:4,
c: 5}`,
out: `{
a: 1
b: 2
}
{
a: 1
b: 4
nested: {
# nested_string is optional
nested_string: "foo"
}
}
{
a: 1
b: 4
c: 5
}
`}, {
name: "repeated proto format with short messages",
in: `{ a: 1}
{ a: 2 }
{ a: 3}`,
out: `{ a: 1 }
{ a: 2 }
{ a: 3 }
`}, {
name: "allow unnamed nodes everywhere",
in: `
# txtpbfmt: allow_unnamed_nodes_everywhere
mapping {{
my_proto_field: "foo"
}}`,
out: `# txtpbfmt: allow_unnamed_nodes_everywhere
mapping {
{
my_proto_field: "foo"
}
}
`}, {
name: "sort fields and values",
in: `# txtpbfmt: sort_fields_by_field_name
# txtpbfmt: sort_repeated_fields_by_content
presubmit: {
auto_reviewers: "reviewerB"
check_contents: {
action: [
# Should go after REVIEW
SUBMIT,
REVIEW
]
# Should go after ADD
operation: EDIT
operation: ADD
prohibited_regexp: "UnsafeFunction"
check_delta_only: true
}
# Should go before reviewerB
auto_reviewers: "reviewerA"
}
`,
out: `# txtpbfmt: sort_fields_by_field_name
# txtpbfmt: sort_repeated_fields_by_content
presubmit: {
# Should go before reviewerB
auto_reviewers: "reviewerA"
auto_reviewers: "reviewerB"
check_contents: {
action: [
REVIEW,
# Should go after REVIEW
SUBMIT
]
check_delta_only: true
operation: ADD
# Should go after ADD
operation: EDIT
prohibited_regexp: "UnsafeFunction"
}
}
`}, {
name: "sort fields and values in reverse order",
in: `# txtpbfmt: sort_fields_by_field_name
# txtpbfmt: sort_repeated_fields_by_content
# txtpbfmt: reverse_sort
presubmit: {
auto_reviewers: "reviewerB"
check_contents: {
action: [
# Should go after SUBMIT
REVIEW,
SUBMIT
]
# Should go after EDIT
operation: ADD
operation: EDIT
prohibited_regexp: "UnsafeFunction"
check_delta_only: true
}
# Should go after reviewerB
auto_reviewers: "reviewerA"
}
`,
out: `# txtpbfmt: sort_fields_by_field_name
# txtpbfmt: sort_repeated_fields_by_content
# txtpbfmt: reverse_sort
presubmit: {
check_contents: {
prohibited_regexp: "UnsafeFunction"
operation: EDIT
# Should go after EDIT
operation: ADD
check_delta_only: true
action: [
SUBMIT,
# Should go after SUBMIT
REVIEW
]
}
auto_reviewers: "reviewerB"
# Should go after reviewerB
auto_reviewers: "reviewerA"
}
`}, {
name: "reverse sort does nothing without another sort_* config option",
in: `# txtpbfmt: reverse_sort
presubmit: {
auto_reviewers: "reviewerB"
check_contents: {
action: [
# Should go after SUBMIT
REVIEW,
SUBMIT
]
# Should go after EDIT
operation: ADD
operation: EDIT
prohibited_regexp: "UnsafeFunction"
check_delta_only: true
}
# Should go after reviewerB
auto_reviewers: "reviewerA"
}
`,
out: `# txtpbfmt: reverse_sort
presubmit: {
auto_reviewers: "reviewerB"
check_contents: {
action: [
# Should go after SUBMIT
REVIEW,
SUBMIT
]
# Should go after EDIT
operation: ADD
operation: EDIT
prohibited_regexp: "UnsafeFunction"
check_delta_only: true
}
# Should go after reviewerB
auto_reviewers: "reviewerA"
}
`}, {
name: "blank lines are collapsed to one",
in: `# txtpbfmt: sort_repeated_fields_by_content
presubmit: {
check_contents: {
# Should go after ADD.
# And the empty lines above this are collapsed into one blank line.
operation: EDIT
operation: ADD
operation: REMOVE
}
}
`,
out: `# txtpbfmt: sort_repeated_fields_by_content
presubmit: {
check_contents: {
operation: ADD
# Should go after ADD.
# And the empty lines above this are collapsed into one blank line.
operation: EDIT
operation: REMOVE
}
}
`}, {
name: "blank line after comment separates it from the node",
in: `# txtpbfmt: sort_repeated_fields_by_content
presubmit: {
check_contents: {
# This comment is a separate node and does not move when the fields are sorted.
operation: EDIT
operation: ADD
}
}
`,
out: `# txtpbfmt: sort_repeated_fields_by_content
presubmit: {
check_contents: {
# This comment is a separate node and does not move when the fields are sorted.
operation: ADD
operation: EDIT
}
}
`}, {
name: "sort by subfield values",
in: `# txtpbfmt: sort_repeated_fields_by_subfield=operation.name
# txtpbfmt: sort_repeated_fields_by_subfield=test.id
presubmit: {
operation {
name: EDIT
}
operation {
name: ADD
}
test {
id: 4
}
test {
id: 2
}
}
`,
out: `# txtpbfmt: sort_repeated_fields_by_subfield=operation.name
# txtpbfmt: sort_repeated_fields_by_subfield=test.id
presubmit: {
operation {
name: ADD
}
operation {
name: EDIT
}
test {
id: 2
}
test {
id: 4
}
}
`}, {
name: "reverse sort by subfield values",
in: `# txtpbfmt: sort_repeated_fields_by_subfield=operation.name
# txtpbfmt: sort_repeated_fields_by_subfield=test.id
# txtpbfmt: reverse_sort
presubmit: {
operation {
name: ADD
}
operation {
name: EDIT
}
test {
id: 2
}
test {
id: 4
}
}
`,
out: `# txtpbfmt: sort_repeated_fields_by_subfield=operation.name
# txtpbfmt: sort_repeated_fields_by_subfield=test.id
# txtpbfmt: reverse_sort
presubmit: {
operation {
name: EDIT
}
operation {
name: ADD
}
test {
id: 4
}
test {
id: 2
}
}
`}, {
name: "sort by deeper subfield path",
in: `# txtpbfmt: sort_repeated_fields_by_subfield=test.metadata.identifiers.id
presubmit: {
test {
metadata {
identifiers {
id: 4
}
}
}
test {
metadata {
identifiers {
id: 2
}
}
}
}
`,
out: `# txtpbfmt: sort_repeated_fields_by_subfield=test.metadata.identifiers.id
presubmit: {
test {
metadata {
identifiers {
id: 2
}
}
}
test {
metadata {
identifiers {
id: 4
}
}
}
}
`}, {
name: "reverse sort by deeper subfield path",
in: `# txtpbfmt: sort_repeated_fields_by_subfield=test.metadata.identifiers.id
# txtpbfmt: reverse_sort
presubmit: {
test {
metadata {
identifiers {
id: 2
}
}
}
test {
metadata {
identifiers {
id: 4
}
}
}
}
`,
out: `# txtpbfmt: sort_repeated_fields_by_subfield=test.metadata.identifiers.id
# txtpbfmt: reverse_sort
presubmit: {
test {
metadata {
identifiers {
id: 4
}
}
}
test {
metadata {
identifiers {
id: 2
}
}
}
}
`}, {
name: "No sort for repeated final subfield",
in: `# txtpbfmt: sort_repeated_fields_by_subfield=test.metadata.identifiers.id
presubmit: {
test {
metadata {
identifiers {
id: 3
id: 4
}
}
}
test {
metadata {
identifiers {
id: 1
id: 2
}
}
}
}
`,
out: `# txtpbfmt: sort_repeated_fields_by_subfield=test.metadata.identifiers.id
presubmit: {
test {
metadata {
identifiers {
id: 3
id: 4
}
}
}
test {
metadata {
identifiers {
id: 1
id: 2
}
}
}
}
`}, {
name: "No sort for message final subfield",
in: `# txtpbfmt: sort_repeated_fields_by_subfield=test.metadata.identifiers
presubmit: {
test {
metadata {
identifiers {
id: 4
}
}
}
test {
metadata {
identifiers {
id: 2
}
}
}
}
`,
out: `# txtpbfmt: sort_repeated_fields_by_subfield=test.metadata.identifiers
presubmit: {
test {
metadata {
identifiers {
id: 4
}
}
}
test {
metadata {
identifiers {
id: 2
}
}
}
}
`}, {
// In this test multiple subfields of `test` are given. The expected behavior is: first sort by
// test.id; in case of a tie, sort by test.type; in case of a tie again, sort by test.name.
name: "sort by multiple subfield values",
in: `# txtpbfmt: sort_repeated_fields_by_subfield=operation.name
# txtpbfmt: sort_repeated_fields_by_subfield=test.id
# txtpbfmt: sort_repeated_fields_by_subfield=test.type
# txtpbfmt: sort_repeated_fields_by_subfield=test.name
presubmit: {
operation {
name: EDIT
}
operation {
name: ADD
}
test {
id: 4
name: bar
unrelated_field: 1
type: type_1
}
test {
id: 2
name: foo
unrelated_field: 3
type: type_2
}
test {
id: 2
name: baz
unrelated_field: 2
type: type_1
}
test {
id: 2
name: bar
unrelated_field: 1
type: type_2
}
}
`,
out: `# txtpbfmt: sort_repeated_fields_by_subfield=operation.name
# txtpbfmt: sort_repeated_fields_by_subfield=test.id
# txtpbfmt: sort_repeated_fields_by_subfield=test.type
# txtpbfmt: sort_repeated_fields_by_subfield=test.name
presubmit: {
operation {
name: ADD
}
operation {
name: EDIT
}
test {
id: 2
name: baz
unrelated_field: 2
type: type_1
}
test {
id: 2
name: bar
unrelated_field: 1
type: type_2
}
test {
id: 2
name: foo
unrelated_field: 3
type: type_2
}
test {
id: 4
name: bar
unrelated_field: 1
type: type_1
}
}
`}, {
// In this test multiple subfields of `test` are given. The expected behavior is: first reverse
// sort by test.id; in case of a tie, reverse sort by test.type; in case of a tie again, reverse
// sort by test.name.
name: "reverse sort by multiple subfield values",
in: `# txtpbfmt: sort_repeated_fields_by_subfield=operation.name
# txtpbfmt: sort_repeated_fields_by_subfield=test.id
# txtpbfmt: sort_repeated_fields_by_subfield=test.type
# txtpbfmt: reverse_sort
# txtpbfmt: sort_repeated_fields_by_subfield=test.name
presubmit: {
operation {
name: ADD
}
operation {
name: EDIT
}
test {
id: 2
name: foo
unrelated_field: 3
type: type_2
}
test {
id: 4
name: bar
unrelated_field: 1
type: type_1
}
test {
id: 2
name: baz
unrelated_field: 2
type: type_1
}
test {
id: 2
name: bar
unrelated_field: 1
type: type_2
}
}
`,
out: `# txtpbfmt: sort_repeated_fields_by_subfield=operation.name
# txtpbfmt: sort_repeated_fields_by_subfield=test.id
# txtpbfmt: sort_repeated_fields_by_subfield=test.type
# txtpbfmt: reverse_sort
# txtpbfmt: sort_repeated_fields_by_subfield=test.name
presubmit: {
operation {
name: EDIT
}
operation {
name: ADD
}
test {
id: 4
name: bar
unrelated_field: 1
type: type_1
}
test {
id: 2
name: foo
unrelated_field: 3
type: type_2
}
test {
id: 2
name: bar
unrelated_field: 1
type: type_2
}
test {
id: 2
name: baz
unrelated_field: 2
type: type_1
}
}
`}, {
name: "sort and remove duplicates",
in: `# txtpbfmt: sort_fields_by_field_name
# txtpbfmt: sort_repeated_fields_by_content
# txtpbfmt: remove_duplicate_values_for_repeated_fields
presubmit: {
auto_reviewers: "reviewerB"
# Should go before reviewerB
auto_reviewers: "reviewerA"
check_contents: {
operation: EDIT
operation: ADD
# Should be removed
operation: EDIT
prohibited_regexp: "UnsafeFunction"
# Should go before operation: ADD
check_delta_only: true
}
# Should be removed
auto_reviewers: "reviewerA"
}
`,
out: `# txtpbfmt: sort_fields_by_field_name
# txtpbfmt: sort_repeated_fields_by_content
# txtpbfmt: remove_duplicate_values_for_repeated_fields
presubmit: {
# Should go before reviewerB
auto_reviewers: "reviewerA"
auto_reviewers: "reviewerB"
check_contents: {
# Should go before operation: ADD
check_delta_only: true
operation: ADD
operation: EDIT
prohibited_regexp: "UnsafeFunction"
}
}
`}, {
name: "reverse sort and remove duplicates",
in: `# txtpbfmt: sort_fields_by_field_name
# txtpbfmt: sort_repeated_fields_by_content
# txtpbfmt: reverse_sort
# txtpbfmt: remove_duplicate_values_for_repeated_fields
presubmit: {
auto_reviewers: "reviewerA"
# Should go before reviewerA
auto_reviewers: "reviewerB"
check_contents: {
operation: ADD
operation: EDIT
# Should be removed
operation: EDIT
prohibited_regexp: "UnsafeFunction"
# Should go after operation: ADD
check_delta_only: true
}
# Should be removed
auto_reviewers: "reviewerA"
}
`,
out: `# txtpbfmt: sort_fields_by_field_name
# txtpbfmt: sort_repeated_fields_by_content
# txtpbfmt: reverse_sort
# txtpbfmt: remove_duplicate_values_for_repeated_fields
presubmit: {
check_contents: {
prohibited_regexp: "UnsafeFunction"
operation: EDIT
operation: ADD
# Should go after operation: ADD
check_delta_only: true
}
# Should go before reviewerA
auto_reviewers: "reviewerB"
auto_reviewers: "reviewerA"
}
`}, {
name: "multiple groups of repeated fields",
in: `# txtpbfmt: sort_repeated_fields_by_content
# txtpbfmt: sort_repeated_fields_by_subfield=id
# field b
field: "b"
# field a
field: "a"
message: { id: "b" }
message: { id: "a" }
# new group
# field b
field: "b"
# field a
field: "a"
message: { id: "b" }
message: { id: "a" }
`,
out: `# txtpbfmt: sort_repeated_fields_by_content
# txtpbfmt: sort_repeated_fields_by_subfield=id
# field a
field: "a"
# field b
field: "b"
message: { id: "a" }
message: { id: "b" }
# new group
# field a
field: "a"
# field b
field: "b"
message: { id: "a" }
message: { id: "b" }
`}, {
name: "reverse sort multiple groups of repeated fields",
in: `# txtpbfmt: sort_repeated_fields_by_content
# txtpbfmt: sort_repeated_fields_by_subfield=id
# txtpbfmt: reverse_sort
# field a
field: "a"
# field b
field: "b"
message: { id: "a" }
message: { id: "b" }
# new group
# field a
field: "a"
# field b
field: "b"
message: { id: "a" }
message: { id: "b" }
`,
out: `# txtpbfmt: sort_repeated_fields_by_content
# txtpbfmt: sort_repeated_fields_by_subfield=id
# txtpbfmt: reverse_sort
# field b
field: "b"
# field a
field: "a"
message: { id: "b" }
message: { id: "a" }
# new group
# field b
field: "b"
# field a
field: "a"
message: { id: "b" }
message: { id: "a" }
`}, {
name: "detached comment creates a new group for sorting",
in: `# txtpbfmt: sort_repeated_fields_by_content
# field c
field: "c"
# field a
field: "a"
# new group - the fields below don't get sorted with the fields above
# field b
field: "b"
`,
out: `# txtpbfmt: sort_repeated_fields_by_content
# field a
field: "a"
# field c
field: "c"
# new group - the fields below don't get sorted with the fields above
# field b
field: "b"
`}, {
name: "detached comment creates a new group for reverse sorting",
in: `# txtpbfmt: sort_repeated_fields_by_content
# txtpbfmt: reverse_sort
# field a
field: "a"
# field c
field: "c"
# new group - the fields below don't get sorted with the fields above
# field b
field: "b"
`,
out: `# txtpbfmt: sort_repeated_fields_by_content
# txtpbfmt: reverse_sort
# field c
field: "c"
# field a
field: "a"
# new group - the fields below don't get sorted with the fields above
# field b
field: "b"
`}, {
name: "trailing comma / semicolon",
in: `dict: {
arg: {
key: "first_value"
value: { num: 0 }
},
arg: {
key: "second_value"
value: { num: 1 }
};
}
`,
out: `dict: {
arg: {
key: "first_value"
value: { num: 0 }
}
arg: {
key: "second_value"
value: { num: 1 }
}
}
`}, {
name: "plx dashboard mixed quotes",
in: `# txtpbfmt: wrap_strings_after_newlines
# txtpbfmt: smartquotes
types_text_content: {
text: "Some text\nwith a <a href=\"https://www.google.com\">hyperlink</a>\nincluded"
}
chart_spec: "{\"columnDefinitions\":[]}"
inline_script: "SELECT \'Hello\' AS hello"
`,
out: `# txtpbfmt: wrap_strings_after_newlines
# txtpbfmt: smartquotes
types_text_content: {
text:
'Some text\n'
'with a <a href="https://www.google.com">hyperlink</a>\n'
'included'
}
chart_spec: '{"columnDefinitions":[]}'
inline_script: "SELECT 'Hello' AS hello"
`}, {
name: "txtpbfmt off/on",
in: `# txtpbfmt: off
fmt: "off" # txtpbfmt: on
foo: "bar"
bar {
baz: "qux"
# txtpbfmt: off
# comment
# comment
no_format {
foo: "bar"
}
enabled: {TEMPLATE_plx}
# txtpbfmt: on
}
should_format {
foo: "bar"
}
# txtpbfmt: off
no_format { foo: "bar" } # txtpbfmt: on
should_format {
foo: "bar"
}
`,
out: `# txtpbfmt: off
fmt: "off" # txtpbfmt: on
foo: "bar"
bar {
baz: "qux"
# txtpbfmt: off
# comment
# comment
no_format {
foo: "bar"
}
enabled: {TEMPLATE_plx}
# txtpbfmt: on
}
should_format {
foo: "bar"
}
# txtpbfmt: off
no_format { foo: "bar" } # txtpbfmt: on
should_format {
foo: "bar"
}
`}, {
name: "txtpbfmt off/on doesn't work within list",
in: `foo: [
# txtpbfmt: off
a, b,
c
# txtpbfmt: on
]
`,
out: `foo: [
# txtpbfmt: off
a,
b,
c
# txtpbfmt: on
]
`}, {
name: "carriage return \\r is formatted away",
in: `foo: "bar"` + "\r" + `baz: "bat"` + "\r",
out: `foo: "bar"` + "\n" + `baz: "bat"` + "\n",
}, {
name: "Windows-style newline \\r\\n is formatted away",
in: `foo: "bar"` + "\r\n" + `baz: "bat"` + "\r\n",
out: `foo: "bar"` + "\n" + `baz: "bat"` + "\n",
}, {
name: "use short repeated primitive fields",
in: `# txtpbfmt: use_short_repeated_primitive_fields
foo: 1
foo: 2
foo: 3
`,
out: `# txtpbfmt: use_short_repeated_primitive_fields
foo: [1, 2, 3]
`}, {
name: "use short repeated primitive fields with single element list",
in: `# txtpbfmt: use_short_repeated_primitive_fields
foo: 1
`,
out: `# txtpbfmt: use_short_repeated_primitive_fields
foo: 1
`}, {
name: "use short repeated primitive fields with interleaved list",
in: `# txtpbfmt: use_short_repeated_primitive_fields
foo: 1
foo: 2
bar: 3
foo: 4
foo: 5
bar: 6
bar: 7
foo: 8
`,
out: `# txtpbfmt: use_short_repeated_primitive_fields
foo: [1, 2]
bar: 3
foo: [4, 5]
bar: [6, 7]
foo: 8
`}, {
name: "use short repeated primitive fields with string field",
in: `# txtpbfmt: use_short_repeated_primitive_fields
foo: "a"
foo: "b"
`,
out: `# txtpbfmt: use_short_repeated_primitive_fields
foo: ["a", "b"]
`}, {
name: "use short repeated primitive fields with message field",
in: `# txtpbfmt: use_short_repeated_primitive_fields
foo { x: 1 }
foo { x: 2 }
`,
out: `# txtpbfmt: use_short_repeated_primitive_fields
foo { x: 1 }
foo { x: 2 }
`}, {
name: "use short repeated primitive fields inside message",
in: `# txtpbfmt: use_short_repeated_primitive_fields
foo { x: 1 x: 2 x: 3 }
foo { x: 4 }
foo { x: 5 x: 6 }
`,
out: `# txtpbfmt: use_short_repeated_primitive_fields
foo { x: [1, 2, 3] }
foo { x: 4 }
foo { x: [5, 6] }
`}, {
name: "use short repeated primitive fields with inline comments",
in: `# txtpbfmt: use_short_repeated_primitive_fields
foo: 1 # inline comment a
foo: 2
foo: 3 # inline comment b
foo: 4
`,
out: `# txtpbfmt: use_short_repeated_primitive_fields
foo: [
1, # inline comment a
2,
3, # inline comment b
4
]
`}, {
name: "use short repeated primitive fields with pre comments",
in: `# txtpbfmt: use_short_repeated_primitive_fields
# pre comment a
foo: 1
foo: 2
foo: 3
# pre comment b
foo: 4
`,
out: `# txtpbfmt: use_short_repeated_primitive_fields
# pre comment a
foo: [1, 2, 3]
# pre comment b
foo: 4
`}, {
name: "use short repeated primitive fields with mixed comments",
in: `# txtpbfmt: use_short_repeated_primitive_fields
foo: 1
foo: 2
foo: 3 # inline comment
# pre comment
foo: 4
`,
out: `# txtpbfmt: use_short_repeated_primitive_fields
foo: [
1,
2,
3 # inline comment
]
# pre comment
foo: 4
`},
}
for _, input := range inputs {
out, err := Format([]byte(input.in))
if err != nil {
t.Errorf("Format[%s] %v returned err %v", input.name, input.in, err)
continue
}
if diff := diff.Diff(input.out, string(out)); diff != "" {
nodes, err := Parse([]byte(input.in))
if err != nil {
t.Errorf("Parse[%s] %v returned err %v", input.name, input.in, err)
continue
}
t.Errorf("Format[%s](\n%s\n)\nparsed tree\n%s\n\nreturned diff (-want, +got):\n%s", input.name, input.in, DebugFormat(nodes, 0), diff)
}
}
}
func TestParserConfigs(t *testing.T) {
inputs := []struct {
name string
in string
config config.Config
out string
wantErr string
}{{
name: "AlreadyExpandedConfigOff",
in: `presubmit: {
check_presubmit_service: {
address: "address"
failure_status: WARNING
options: "options"
}
}
`,
config: config.Config{ExpandAllChildren: false},
out: `presubmit: {
check_presubmit_service: {
address: "address"
failure_status: WARNING
options: "options"
}
}
`,
}, {
name: "AlreadyExpandedConfigOn",
in: `presubmit: {
check_presubmit_service: {
address: "address"
failure_status: WARNING
options: "options"
}
}
`,
config: config.Config{ExpandAllChildren: true},
out: `presubmit: {
check_presubmit_service: {
address: "address"
failure_status: WARNING
options: "options"
}
}
`,
}, {
name: "NotExpandedOnFix",
in: `presubmit: { check_presubmit_service: { address: "address" failure_status: WARNING options: "options" } }
`,
config: config.Config{ExpandAllChildren: false},
out: `presubmit: { check_presubmit_service: { address: "address" failure_status: WARNING options: "options" } }
`,
}, {
name: "ExpandedOnFix",
in: `presubmit: { check_presubmit_service: { address: "address" failure_status: WARNING options: "options" } }
`,
config: config.Config{ExpandAllChildren: true},
out: `presubmit: {
check_presubmit_service: {
address: "address"
failure_status: WARNING
options: "options"
}
}
`,
}, {
name: "SortFieldNames",
in: `presubmit: {
auto_reviewers: "reviewerB"
check_contents: {
operation: EDIT
operation: ADD
prohibited_regexp: "UnsafeFunction"
check_delta_only: true
}
# Should remain below reviewerB
auto_reviewers: "reviewerA"
}
`,
config: config.Config{SortFieldsByFieldName: true},
out: `presubmit: {
auto_reviewers: "reviewerB"
# Should remain below reviewerB
auto_reviewers: "reviewerA"
check_contents: {
check_delta_only: true
operation: EDIT
operation: ADD
prohibited_regexp: "UnsafeFunction"
}
}
`,
}, {
name: "SortFieldContents",
in: `presubmit: {
auto_reviewers: "reviewerB"
check_contents: {
# Should go after ADD
operation: EDIT
operation: ADD
prohibited_regexp: "UnsafeFunction"
check_delta_only: true
}
# Should remain below
auto_reviewers: "reviewerA"
}
`,
config: config.Config{SortRepeatedFieldsByContent: true},
out: `presubmit: {
auto_reviewers: "reviewerB"
check_contents: {
operation: ADD
# Should go after ADD
operation: EDIT
prohibited_regexp: "UnsafeFunction"
check_delta_only: true
}
# Should remain below
auto_reviewers: "reviewerA"
}
`,
}, {
name: "SortNamedFieldBySubfieldContents",
in: `presubmit: {
auto_reviewers: "reviewerB"
check_contents: {
# Should go after ADD
operation: {
name: EDIT
}
operation: {
name: ADD
}
prohibited_regexp: "UnsafeFunction"
check_delta_only: true
}
# Should remain below
auto_reviewers: "reviewerA"
}
`,
config: config.Config{SortRepeatedFieldsBySubfield: []string{"operation.name"}},
out: `presubmit: {
auto_reviewers: "reviewerB"
check_contents: {
operation: {
name: ADD
}
# Should go after ADD
operation: {
name: EDIT
}
prohibited_regexp: "UnsafeFunction"
check_delta_only: true
}
# Should remain below
auto_reviewers: "reviewerA"
}
`,
}, {
name: "SortNamedFieldByMultipleSubfieldContents",
in: `presubmit: {
operation {
name: EDIT
}
operation {
name: ADD
}
test {
id: 4
}
test {
id: 2
}
}
`,
config: config.Config{SortRepeatedFieldsBySubfield: []string{"operation.name", "test.id"}},
out: `presubmit: {
operation {
name: ADD
}
operation {
name: EDIT
}
test {
id: 2
}
test {
id: 4
}
}
`,
}, {
name: "SortAnyFieldBySubfieldContents",
in: `presubmit: {
auto_reviewers: "reviewerB"
check_contents: {
# Should go after ADD
operation: {
name: EDIT
}
operation: {
name: ADD
}
prohibited_regexp: "UnsafeFunction"
check_delta_only: true
}
# Should remain below
auto_reviewers: "reviewerA"
}
`,
config: config.Config{SortRepeatedFieldsBySubfield: []string{"name"}},
out: `presubmit: {
auto_reviewers: "reviewerB"
check_contents: {
operation: {
name: ADD
}
# Should go after ADD
operation: {
name: EDIT
}
prohibited_regexp: "UnsafeFunction"
check_delta_only: true
}
# Should remain below
auto_reviewers: "reviewerA"
}
`,
}, {
name: "SortBySubfieldsDontSortFieldsWithDifferentNames",
in: `presubmit: {
check_contents: {
operation1: {
name: EDIT
}
operation2: {
name: ADD
}
}
}
`,
config: config.Config{SortRepeatedFieldsBySubfield: []string{"name"}},
out: `presubmit: {
check_contents: {
operation1: {
name: EDIT
}
operation2: {
name: ADD
}
}
}
`,
}, {
name: "SortSeparatedFieldBySubfieldContents",
in: `presubmit: {
auto_reviewers: "reviewerB"
check_contents: {
# Should go after ADD
operation: {
name: EDIT
}
split: 1
operation: {
name: ADD
}
}
# Should move above
auto_reviewers: "reviewerA"
}
`,
config: config.Config{SortFieldsByFieldName: true, SortRepeatedFieldsBySubfield: []string{"name"}},
out: `presubmit: {
auto_reviewers: "reviewerB"
# Should move above
auto_reviewers: "reviewerA"
check_contents: {
operation: {
name: ADD
}
# Should go after ADD
operation: {
name: EDIT
}
split: 1
}
}
`,
}, {
name: "SortSubfieldsIgnoreEmptySubfieldName",
in: `presubmit: {
auto_reviewers: "reviewerB"
check_contents: {
operation: {
name: EDIT
}
operation: {
name: ADD
}
}
auto_reviewers: "reviewerA"
}
`,
config: config.Config{SortRepeatedFieldsBySubfield: []string{"operation."}},
out: `presubmit: {
auto_reviewers: "reviewerB"
check_contents: {
operation: {
name: EDIT
}
operation: {
name: ADD
}
}
auto_reviewers: "reviewerA"
}
`,
}, {
name: "SortRepeatedFieldsByContentInDNSOrder",
in: `head {
host: "a.com"
host: "b.au"
host: "c.com"
}
`,
config: config.Config{SortRepeatedFieldsByContent: true, DNSSortOrder: true},
out: `head {
host: "b.au"
host: "a.com"
host: "c.com"
}
`,
}, {
name: "SortRepeatedFieldsBySubfieldInDNSOrder",
in: `head {
hostinfo {
name: "a.com"
}
hostinfo {
name: "b.au"
}
hostinfo {
name: "c.com"
}
}
`,
config: config.Config{SortRepeatedFieldsBySubfield: []string{"hostinfo.name"}, DNSSortOrder: true},
out: `head {
hostinfo {
name: "b.au"
}
hostinfo {
name: "a.com"
}
hostinfo {
name: "c.com"
}
}
`,
}, {
name: "SortFieldNamesAndContents",
in: `presubmit: {
auto_reviewers: "reviewerB"
check_contents: {
# Should go after ADD
operation: EDIT
operation: ADD
prohibited_regexp: "UnsafeFunction"
check_delta_only: true
}
# Should go before reviewerB
auto_reviewers: "reviewerA"
}
`,
config: config.Config{SortFieldsByFieldName: true, SortRepeatedFieldsByContent: true},
out: `presubmit: {
# Should go before reviewerB
auto_reviewers: "reviewerA"
auto_reviewers: "reviewerB"
check_contents: {
check_delta_only: true
operation: ADD
# Should go after ADD
operation: EDIT
prohibited_regexp: "UnsafeFunction"
}
}
`,
}, {
name: "SortBySpecifiedFieldOrder",
in: `
below_wrapper: true
wrapper: {
unmoved_not_in_config: "foo"
check_contents: {
# Not really at the top; attached to x_third
x_third: "3"
# attached to EDIT
z_first: EDIT
unknown_bubbles_to_top: true
x_second: true
z_first: ADD
also_unknown_bubbles_to_top: true
# Trailing comment is on different node; should not confuse ordering logic.
# These always sort below fields in the sorting config, and thus stay at bottom.
}
# Should also not move
unmoved_not_in_config: "bar"
}
`,
config: config.Config{
FieldSortOrder: map[string][]string{
config.RootName: {"wrapper", "below_wrapper"},
"check_contents": {"z_first", "x_second", "x_third"},
},
},
// Nodes are sorted by the specified order, else left untouched.
out: `wrapper: {
unmoved_not_in_config: "foo"
check_contents: {
unknown_bubbles_to_top: true
also_unknown_bubbles_to_top: true
# attached to EDIT
z_first: EDIT
z_first: ADD
x_second: true
# Not really at the top; attached to x_third
x_third: "3"
# Trailing comment is on different node; should not confuse ordering logic.
# These always sort below fields in the sorting config, and thus stay at bottom.
}
# Should also not move
unmoved_not_in_config: "bar"
}
below_wrapper: true
`,
}, {
name: "SortBySpecifiedFieldOrderAndNameAndValue",
in: `presubmit: {
# attached to bar
sort_by_name_and_value: "bar"
check_contents: {
x_third: "3"
# attached to EDIT
z_first: EDIT
unknown_bubbles_to_top: true
x_second: true
z_first: ADD
also_unknown_bubbles_to_top: true
# The nested check_contents bubbles to the top, since it's not in the fieldSortOrder.
check_contents: {
x_second: true
z_first: ADD
}
}
sort_by_name_and_value: "foo"
}
`,
config: config.Config{
FieldSortOrder: map[string][]string{
"check_contents": {"z_first", "x_second", "x_third", "not_required"},
},
SortFieldsByFieldName: true,
SortRepeatedFieldsByContent: true,
},
// Nodes are sorted by name/value first, then by the specified order. Hence the specified
// repeated fields (z_first) is also sorted by value rather than in original order.
out: `presubmit: {
check_contents: {
also_unknown_bubbles_to_top: true
# The nested check_contents bubbles to the top, since it's not in the fieldSortOrder.
check_contents: {
z_first: ADD
x_second: true
}
unknown_bubbles_to_top: true
z_first: ADD
# attached to EDIT
z_first: EDIT
x_second: true
x_third: "3"
}
# attached to bar
sort_by_name_and_value: "bar"
sort_by_name_and_value: "foo"
}
`,
}, {
name: "SortBySpecifiedFieldOrderErrorHandling",
in: `presubmit: {
node_not_in_config_will_not_trigger_error: true
check_contents: {
x_third: "3"
z_first: EDIT
unknown_field_triggers_error: true
x_second: true
z_first: ADD
}
}
`,
config: config.Config{
FieldSortOrder: map[string][]string{
"check_contents": {"z_first", "x_second", "x_third"},
},
RequireFieldSortOrderToMatchAllFieldsInNode: true,
},
wantErr: `parent field: "check_contents", unsorted field: "unknown_field_triggers_error"`,
}, {
name: "RemoveRepeats",
in: `presubmit: {
auto_reviewers: "reviewerB"
auto_reviewers: "reviewerA"
check_contents: {
operation: EDIT
operation: ADD
# Should be removed
operation: EDIT
prohibited_regexp: "UnsafeFunction"
check_delta_only: true
}
# Should be removed
auto_reviewers: "reviewerA"
}
`,
config: config.Config{RemoveDuplicateValuesForRepeatedFields: true},
out: `presubmit: {
auto_reviewers: "reviewerB"
auto_reviewers: "reviewerA"
check_contents: {
operation: EDIT
operation: ADD
prohibited_regexp: "UnsafeFunction"
check_delta_only: true
}
}
`,
}, {
name: "SortEverythingAndRemoveRepeats",
in: `presubmit: {
auto_reviewers: "reviewerB"
# Should go before reviewerB
auto_reviewers: "reviewerA"
check_contents: {
operation: EDIT
operation: ADD
# Should be removed
operation: EDIT
prohibited_regexp: "UnsafeFunction"
# Should go before operation: ADD
check_delta_only: true
}
# Should be removed
auto_reviewers: "reviewerA"
}
`,
config: config.Config{
SortFieldsByFieldName: true,
SortRepeatedFieldsByContent: true,
RemoveDuplicateValuesForRepeatedFields: true},
out: `presubmit: {
# Should go before reviewerB
auto_reviewers: "reviewerA"
auto_reviewers: "reviewerB"
check_contents: {
# Should go before operation: ADD
check_delta_only: true
operation: ADD
operation: EDIT
prohibited_regexp: "UnsafeFunction"
}
}
`,
}, {
name: "TripleQuotedStrings",
config: config.Config{
AllowTripleQuotedStrings: true,
},
in: `foo: """bar"""
`,
out: `foo: """bar"""
`,
}, {
name: "TripleQuotedStrings_multiLine",
config: config.Config{
AllowTripleQuotedStrings: true,
},
in: `foo: """
bar
"""
`,
out: `foo: """
bar
"""
`,
}, {
name: "TripleQuotedStrings_singleQuotes",
config: config.Config{
AllowTripleQuotedStrings: true,
},
in: `foo: '''
bar
'''
`,
out: `foo: '''
bar
'''
`,
}, {
name: "TripleQuotedStrings_brackets",
config: config.Config{
AllowTripleQuotedStrings: true,
},
in: `s: """ "}" """
`,
out: `s: """ "}" """
`,
}, {
name: "TripleQuotedStrings_metaComment",
in: `# txtpbfmt: allow_triple_quoted_strings
foo: """
bar
"""
`,
out: `# txtpbfmt: allow_triple_quoted_strings
foo: """
bar
"""
`,
}, {
name: "WrapStringsAtColumn",
config: config.Config{
WrapStringsAtColumn: 15,
},
in: `# Comments are not wrapped
s: "one two three four five"
`,
out: `# Comments are not wrapped
s:
"one two "
"three four "
"five"
`,
}, {
name: "WrapStringsAtColumn_inlineChildren",
config: config.Config{
WrapStringsAtColumn: 14,
},
in: `root {
# inline children don't wrap
inner { inline: "89 1234" }
# Verify that skipping an inline string doesn't skip the rest of the file.
wrappable {
s: "will wrap"
}
}
`,
out: `root {
# inline children don't wrap
inner { inline: "89 1234" }
# Verify that skipping an inline string doesn't skip the rest of the file.
wrappable {
s:
"will "
"wrap"
}
}
`,
}, {
name: "WrapStringsAtColumn_exactlyNumColumnsDoesNotWrap",
config: config.Config{
WrapStringsAtColumn: 14,
},
in: `root {
inner {
s: "89 123"
}
}
`,
out: `root {
inner {
s: "89 123"
}
}
`,
}, {
name: "WrapStringsAtColumn_numColumnsPlus1Wraps",
config: config.Config{
WrapStringsAtColumn: 14,
},
in: `root {
inner {
s:
"89 123 "
"123 56"
}
}
`,
out: `root {
inner {
s:
"89 "
"123 "
"123 "
"56"
}
}
`,
}, {
name: "WrapStringsAtColumn_commentKeptWhenLinesReduced",
config: config.Config{
WrapStringsAtColumn: 15,
},
in: `root {
label:
# before
"56789 next-line" # trailing
label:
# inside top
"56789 next-line " # trailing line1
"v "
"v "
"v "
"v "
# inside in-between
"straggler" # trailing line2
# next-node comment
}
`,
out: `root {
label:
# before
"56789 " # trailing
"next-line"
label:
# inside top
"56789 " # trailing line1
"next-line "
"v v v v "
"straggler"
# inside in-between
# trailing line2
# next-node comment
}
`,
}, {
name: "WrapStringsAtColumn_doNotBreakLongWords",
config: config.Config{
WrapStringsAtColumn: 15,
},
in: `s: "one@two_three-four&five"
`,
out: `s: "one@two_three-four&five"
`,
}, {
name: "WrapStringsAtColumn_wrapHtml",
config: config.Config{
WrapStringsAtColumn: 15,
WrapHTMLStrings: true,
},
in: `s: "one two three <four>"
`,
out: `s:
"one two "
"three "
"<four>"
`,
}, {
name: "WrapStringsAtColumn_empty",
config: config.Config{
WrapStringsAtColumn: 15,
},
in: `s:
`,
out: `s:
`,
}, {
name: "WrapStringsAtColumn_doNoWrapHtmlByDefault",
config: config.Config{
WrapStringsAtColumn: 15,
},
in: `s: "one two three <four>"
`,
out: `s: "one two three <four>"
`,
}, {
name: "WrapStringsAtColumn_doNoWrapHtmlRealistic",
config: config.Config{
WrapStringsAtColumn: 40,
},
in: `text:
"The two lines below should not wrap since they contains HTML tags "
"<a href=\"https://support.example.com/project/a/pageone/pagetwo/pagethree?hl=1234\">"
"some' text</a>."`,
out: `text:
"The two lines below should not wrap since they contains HTML tags "
"<a href=\"https://support.example.com/project/a/pageone/pagetwo/pagethree?hl=1234\">"
"some' text</a>."
`,
}, {
name: "WrapStringsAtColumn_metaComment",
in: `# txtpbfmt: wrap_strings_at_column=15
# txtpbfmt: wrap_html_strings
s: "one two three <four>"
`,
out: `# txtpbfmt: wrap_strings_at_column=15
# txtpbfmt: wrap_html_strings
s:
"one two "
"three "
"<four>"
`,
}, {
name: "WrapStringsAtColumn_doNotWrapNonStrings",
config: config.Config{
WrapStringsAtColumn: 15,
},
in: `e: VERY_LONG_ENUM_VALUE
i: 12345678901234567890
r: [1, 2, 3, 4, 5, 6, 7, 8, 9, 0]
`,
out: `e: VERY_LONG_ENUM_VALUE
i: 12345678901234567890
r: [1, 2, 3, 4, 5, 6, 7, 8, 9, 0]
`,
}, {
name: "WrapStringsAtColumn_alreadyWrappedStringsAreNotRewrapped",
config: config.Config{
WrapStringsAtColumn: 15,
},
// Total length >15, but each existing line <15, so don't re-wrap 1st line to "I am ".
in: `s:
"I "
"am already "
"wrapped"
`,
out: `s:
"I "
"am already "
"wrapped"
`,
}, {
name: "WrapStringsAtColumn_alreadyWrappedStringsAreNotRewrappedUnlessSomeAreLonger",
config: config.Config{
WrapStringsAtColumn: 15,
},
in: `s:
"I "
"am already "
"wrapped"
" but I am not!"
`,
out: `s:
"I am "
"already "
"wrapped "
"but I am "
"not!"
`,
}, {
name: "WrapStringsAtColumn_tripleQuotedStringsAreNotWrapped",
config: config.Config{
WrapStringsAtColumn: 15,
AllowTripleQuotedStrings: true,
},
in: `s1: """one two three four five"""
s2: '''six seven eight nine'''
`,
out: `s1: """one two three four five"""
s2: '''six seven eight nine'''
`,
}, {
name: "WrapStringsAfterNewlines",
config: config.Config{
WrapStringsAfterNewlines: true,
},
in: `# Comments are not \n wrapped
s: "one two \nthree four\nfive"
`,
out: `# Comments are not \n wrapped
s:
"one two \n"
"three four\n"
"five"
`,
}, {
name: "WrapStringsAfterNewlines_motivatingExampleWithMarkup",
config: config.Config{
WrapStringsAfterNewlines: true,
},
in: `root {
doc: "<body>\n <p>\n Hello\n </p>\n</body>\n"
}
`,
out: `root {
doc:
"<body>\n"
" <p>\n"
" Hello\n"
" </p>\n"
"</body>\n"
}
`,
}, {
name: "WrapStringsAfterNewlines_inlineChildren",
config: config.Config{
WrapStringsAfterNewlines: true,
},
in: `root {
# inline children don't wrap
inner { inline: "89 1234" }
# Verify that skipping an inline string doesn't skip the rest of the file.
wrappable {
s: "will \nwrap"
}
}
`,
out: `root {
# inline children don't wrap
inner { inline: "89 1234" }
# Verify that skipping an inline string doesn't skip the rest of the file.
wrappable {
s:
"will \n"
"wrap"
}
}
`,
}, {
name: "WrapStringsAfterNewlines_noNewlineDoesNotWrap",
config: config.Config{
WrapStringsAfterNewlines: true,
},
in: `root {
inner {
s: "89 123"
}
}
`,
out: `root {
inner {
s: "89 123"
}
}
`,
}, {
name: "WrapStringsAfterNewlines_trailingNewlineDoesNotWrap",
config: config.Config{
WrapStringsAfterNewlines: true,
},
in: `root {
s: "89 123\n"
}
`,
out: `root {
s: "89 123\n"
}
`,
}, {
name: "WrapStringsAfterNewlines_trailingNewlineDoesNotLeaveSuperfluousEmptyString",
config: config.Config{
WrapStringsAfterNewlines: true,
},
in: `root {
s: "89\n123\n"
}
`,
out: `root {
s:
"89\n"
"123\n"
}
`,
}, {
name: "WrapStringsAfterNewlines_empty",
config: config.Config{
WrapStringsAfterNewlines: true,
},
in: `s:
`,
out: `s:
`,
}, {
name: "WrapStringsAfterNewlines_metaComment",
in: `# txtpbfmt: wrap_strings_after_newlines
s: "one two \nthree\n four"
`,
out: `# txtpbfmt: wrap_strings_after_newlines
s:
"one two \n"
"three\n"
" four"
`,
}, {
name: "WrapStringsAfterNewlines_alreadyWrappedStringsAreRewrapped",
config: config.Config{
WrapStringsAfterNewlines: true,
},
in: `s:
"I "
"am already\n"
"wrapped. \nBut this was not."
`,
out: `s:
"I am already\n"
"wrapped. \n"
"But this was not."
`,
}, {
name: "WrapStringsAfterNewlines_tripleQuotedStringsAreNotWrapped",
config: config.Config{
WrapStringsAfterNewlines: true,
AllowTripleQuotedStrings: true,
},
in: `s1: """one two three four five"""
s2: '''six seven \neight nine'''
`,
out: `s1: """one two three four five"""
s2: '''six seven \neight nine'''
`,
}, {
name: "WrapStringsAfterNewlines_tooManyEscapesDoesNotWrap",
config: config.Config{
WrapStringsAfterNewlines: true,
},
in: `s: "7\nsev\xADen\x00"
`,
out: `s: "7\nsev\xADen\x00"
`,
}, {
name: "WrapStringsAfterNewlines_wayTooManyEscapesDoesNotWrap",
config: config.Config{
WrapStringsAfterNewlines: true,
},
in: `s: "οΎ­\xde\x00\x00\x00\x08\n(\x02\n\x0b\x00\x07\x01h\x0c\x14\x01"
`,
out: `s: "οΎ­\xde\x00\x00\x00\x08\n(\x02\n\x0b\x00\x07\x01h\x0c\x14\x01"
`,
}, {
name: "WrapStringsAfterNewlines_aFewEscapesStillWrap",
config: config.Config{
WrapStringsAfterNewlines: true,
},
in: `s: "aaaaaaaaaa \n bbbbbbbbbb \n cccccccccc \n dddddddddd \n eeeeeeeeee\x00 \n"
`,
out: `s:
"aaaaaaaaaa \n"
" bbbbbbbbbb \n"
" cccccccccc \n"
" dddddddddd \n"
" eeeeeeeeee\x00 \n"
`,
}, {
name: "WrapStringsAtColumn_noWordwrap",
config: config.Config{
WrapStringsAtColumn: 12,
WrapStringsWithoutWordwrap: true,
},
in: `# 3456789012
s: "Curabitur\040elit\x20nec mi egestas,\u000Dtincidunt \U00010309nterdum elit porta.\n"
`,
out: `# 3456789012
s:
"Curabitu"
"r\040eli"
"t\x20nec"
" mi eges"
"tas,"
"\u000Dti"
"ncidunt "
"\U00010309"
"nterdum "
"elit por"
"ta.\n"
`,
}, {
name: "WrapStringsAtColumn_noWordwrapDeep",
config: config.Config{
WrapStringsAtColumn: 12,
WrapStringsWithoutWordwrap: true,
},
in: `
this_field_name_displays_wider_than_the_twelve_requested: "this_goes_to_a_new_line"
`,
out: `this_field_name_displays_wider_than_the_twelve_requested:
"this_goe"
"s_to_a_n"
"ew_line"
`,
}, {
name: "WrapStringsAtColumn_noWordwrapDeepInlinePromotion",
config: config.Config{
WrapStringsAtColumn: 12,
WrapStringsWithoutWordwrap: true,
},
in: `
this_field_name_displays_wider_than_the_twelve_requested: "0C" # XII
`,
out: `this_field_name_displays_wider_than_the_twelve_requested:
# XII
"0C"
`,
}, {
name: "WrapStringsAtColumn_noWordwrapMetacomment",
in: `# txtpbfmt: wrap_strings_at_column=12
# txtpbfmt: wrap_strings_without_wordwrap
# 3456789012
s: "1\tone\r\n2\ttwo\r\n3\tthree\r\n4\tfour\r\n"
`,
out: `# txtpbfmt: wrap_strings_at_column=12
# txtpbfmt: wrap_strings_without_wordwrap
# 3456789012
s:
"1\tone\r"
"\n2\ttwo"
"\r\n3\tt"
"hree\r\n"
"4\tfour"
"\r\n"
`,
}, {
name: "PreserveAngleBrackets",
config: config.Config{
PreserveAngleBrackets: true,
},
in: `foo <
a: 1
>
foo {
b: 2
}
`,
out: `foo <
a: 1
>
foo {
b: 2
}
`,
}, {
name: "legacy quote behavior",
config: config.Config{
SmartQuotes: false,
},
in: `foo: "\"bar\""`,
out: `foo: "\"bar\""
`,
}, {
name: "smart quotes",
config: config.Config{
SmartQuotes: true,
},
in: `foo: "\"bar\""`,
out: `foo: '"bar"'
`,
}, {
name: "smart quotes via meta comment",
config: config.Config{
SmartQuotes: false,
},
in: `# txtpbfmt: smartquotes
foo: "\"bar\""`,
out: `# txtpbfmt: smartquotes
foo: '"bar"'
`,
}, {
name: "carriage returns",
in: "a{\r\n}\r\n",
out: "a {\n}\n",
},
}
// Test FormatWithConfig with inputs.
for _, input := range inputs {
got, err := FormatWithConfig([]byte(input.in), input.config)
if input.wantErr != "" {
if err == nil {
t.Errorf("FormatWithConfig[%s] got err=nil, want err=%v", input.name, input.wantErr)
continue
}
if !strings.Contains(err.Error(), input.wantErr) {
t.Errorf("FormatWithConfig[%s] got err=%v, want err=%v", input.name, err, input.wantErr)
}
continue
}
if err != nil {
t.Errorf("FormatWithConfig[%s] %v with config %v returned err %v", input.name, input.in, input.config, err)
continue
}
if diff := diff.Diff(input.out, string(got)); diff != "" {
t.Errorf("FormatWithConfig[%s](\n%s\n)\nreturned different output from expected (-want, +got):\n%s", input.name, input.in, diff)
}
}
// Test ParseWithConfig with inputs.
for _, input := range inputs {
nodes, err := ParseWithConfig([]byte(input.in), input.config)
if input.wantErr != "" {
if err == nil {
t.Errorf("ParseWithConfig[%s] got err=nil, want err=%v", input.name, input.wantErr)
continue
}
if !strings.Contains(err.Error(), input.wantErr) {
t.Errorf("ParseWithConfig[%s] got err=%v, want err=%v", input.name, err, input.wantErr)
}
continue
}
if err != nil {
t.Errorf("ParseWithConfig[%s] %v with config %v returned err %v", input.name, input.in, input.config, err)
continue
}
got := Pretty(nodes, 0)
if diff := diff.Diff(input.out, got); diff != "" {
t.Errorf("ParseWithConfig[%s](\n%s\n)\nreturned different Pretty output from expected (-want, +got):\n%s", input.name, input.in, diff)
}
}
}
func TestDebugFormat(t *testing.T) {
inputs := []struct {
in string
want string
}{{
in: `name: { name: "value" }`,
want: `
name: "name"
PreComments: "" (len 0)
children:
. name: "name"
. PreComments: "" (len 0)
. values: [{Value: "\"value\"", PreComments: "", InlineComment: ""}]
`,
}}
for _, input := range inputs {
nodes, err := Parse([]byte(input.in))
if err != nil {
t.Errorf("Parse %v returned err %v", input.in, err)
continue
}
if len(nodes) == 0 {
t.Errorf("Parse %v returned no nodes", input.in)
continue
}
got := DebugFormat(nodes, 0 /* depth */)
if diff := diff.Diff(input.want, got); diff != "" {
t.Errorf("DebugFormat %v returned diff (-want, +got):\n%s", input.in, diff)
}
}
}
func TestSmartQuotes(t *testing.T) {
inputs := []struct {
in string
wantLegacy string
wantSmart string
}{
{`1`, `1`, `1`},
{`""`, `""`, `""`},
{`''`, `""`, `""`},
{`"a"`, `"a"`, `"a"`},
{`'a'`, `"a"`, `"a"`},
{`'a"b'`, `"a\"b"`, `'a"b'`},
{`"a\"b"`, `"a\"b"`, `'a"b'`},
{`'a\'b'`, `"a\'b"`, `"a'b"`},
{`'a"b\'c'`, `"a\"b\'c"`, `"a\"b'c"`},
{`"a\"b'c"`, `"a\"b'c"`, `"a\"b'c"`},
{`'a\"b\'c'`, `"a\"b\'c"`, `"a\"b'c"`},
{`"a\"b\'c"`, `"a\"b\'c"`, `"a\"b'c"`},
{`"'\\\'"`, `"'\\\'"`, `"'\\'"`},
}
for _, tc := range inputs {
in := `foo: ` + tc.in
name := fmt.Sprintf("Format [ %s ] with legacy quote behavior", tc.in)
want := `foo: ` + tc.wantLegacy
gotRaw, err := FormatWithConfig([]byte(in), config.Config{SmartQuotes: false})
got := strings.TrimSpace(string(gotRaw))
if err != nil {
t.Errorf("%s: got error: %s, want no error and [ %s ]", name, err, want)
} else if got != want {
t.Errorf("%s: got [ %s ], want [ %s ]", name, got, want)
}
name = fmt.Sprintf("Format [ %s ] with smart quote behavior", tc.in)
want = `foo: ` + tc.wantSmart
gotRaw, err = FormatWithConfig([]byte(`foo: `+tc.in), config.Config{SmartQuotes: true})
got = strings.TrimSpace(string(gotRaw))
if err != nil {
t.Errorf("%s: got error: %s, want no error and [ %s ]", name, err, want)
} else if got != want {
t.Errorf("%s: got [ %s ], want [ %s ]", name, got, want)
}
}
}
func FuzzParse(f *testing.F) {
testcases := []string{"", "a: 123", "input { dimension: [2, 4, 6, 8] }", "]", "\":%\"",
"%''#''0'0''0''0''0''0\""}
for _, tc := range testcases {
f.Add([]byte(tc))
}
f.Fuzz(func(t *testing.T, in []byte) {
Parse(in)
})
}