github.com/hoveychen/protoreflect@v1.4.7-0.20221103114119-0b4b3385ec76/desc/protoparse/error_reporting_test.go (about) 1 package protoparse 2 3 import ( 4 "errors" 5 "strings" 6 "testing" 7 8 "github.com/hoveychen/protoreflect/internal/testutil" 9 ) 10 11 func TestErrorReporting(t *testing.T) { 12 tooManyErrors := errors.New("too many errors") 13 limitedErrReporter := func(limit int, count *int) ErrorReporter { 14 return func(err ErrorWithPos) error { 15 *count++ 16 if *count > limit { 17 return tooManyErrors 18 } 19 return nil 20 } 21 } 22 trackingReporter := func(errs *[]ErrorWithPos, count *int) ErrorReporter { 23 return func(err ErrorWithPos) error { 24 *count++ 25 *errs = append(*errs, err) 26 return nil 27 } 28 } 29 fail := errors.New("failure!") 30 failFastReporter := func(count *int) ErrorReporter { 31 return func(err ErrorWithPos) error { 32 *count++ 33 return fail 34 } 35 } 36 37 testCases := []struct { 38 fileNames []string 39 files map[string]string 40 expectedErrs []string 41 }{ 42 { 43 // multiple syntax errors 44 fileNames: []string{"test.proto"}, 45 files: map[string]string{ 46 "test.proto": ` 47 syntax = "proto"; 48 package foo 49 50 message Foo { 51 foo = 1; 52 } 53 `, 54 }, 55 expectedErrs: []string{ 56 "test.proto:2:50: syntax value must be 'proto2' or 'proto3'", 57 "test.proto:5:41: syntax error: unexpected \"message\", expecting ';'", 58 "test.proto:6:53: syntax error: unexpected '='", 59 }, 60 }, 61 { 62 // multiple validation errors 63 fileNames: []string{"test.proto"}, 64 files: map[string]string{ 65 "test.proto": ` 66 syntax = "proto3"; 67 message Foo { 68 string foo = 0; 69 } 70 enum Bar { 71 BAZ = 1; 72 BUZZ = 1; 73 } 74 `, 75 }, 76 expectedErrs: []string{ 77 "test.proto:4:62: tag number 0 must be greater than zero", 78 "test.proto:7:55: enum Bar: proto3 requires that first value in enum have numeric value of 0", 79 "test.proto:8:56: enum Bar: values BAZ and BUZZ both have the same numeric value 1; use allow_alias option if intentional", 80 }, 81 }, 82 { 83 // multiple link errors 84 fileNames: []string{"test.proto"}, 85 files: map[string]string{ 86 "test.proto": ` 87 syntax = "proto3"; 88 message Foo { 89 string foo = 1; 90 } 91 enum Bar { 92 BAZ = 0; 93 BAZ = 2; 94 } 95 service Bar { 96 rpc Foo (Foo) returns (Foo); 97 rpc Foo (Frob) returns (Nitz); 98 } 99 `, 100 }, 101 expectedErrs: []string{ 102 "test.proto:8:49: duplicate symbol Bar.BAZ: already defined as enum value", 103 "test.proto:10:41: duplicate symbol Bar: already defined as enum", 104 "test.proto:12:49: duplicate symbol Bar.Foo: already defined as method", 105 "test.proto:12:58: method Bar.Foo: unknown request type Frob", 106 "test.proto:12:73: method Bar.Foo: unknown response type Nitz", 107 }, 108 }, 109 { 110 // syntax errors across multiple files 111 fileNames: []string{"test1.proto", "test2.proto"}, 112 files: map[string]string{ 113 "test1.proto": ` 114 syntax = "proto3"; 115 import "test2.proto"; 116 message Foo { 117 string foo = -1; 118 } 119 service Bar { 120 rpc Foo (Foo); 121 } 122 `, 123 "test2.proto": ` 124 syntax = "proto3"; 125 message Baz { 126 required string foo = 1; 127 } 128 service Service { 129 Foo; Bar; Baz; 130 } 131 `, 132 }, 133 expectedErrs: []string{ 134 "test1.proto:5:62: syntax error: unexpected '-', expecting int literal", 135 "test1.proto:8:62: syntax error: unexpected ';', expecting \"returns\"", 136 "test2.proto:7:49: syntax error: unexpected identifier, expecting \"option\" or \"rpc\" or ';' or '}'", 137 "test2.proto:4:49: field Baz.foo: field has label LABEL_REQUIRED, but proto3 must omit labels other than 'repeated'", 138 }, 139 }, 140 { 141 // link errors across multiple files 142 fileNames: []string{"test1.proto", "test2.proto"}, 143 files: map[string]string{ 144 "test1.proto": ` 145 syntax = "proto3"; 146 import "test2.proto"; 147 message Foo { 148 string foo = 1; 149 } 150 service Bar { 151 rpc Frob (Empty) returns (Nitz); 152 } 153 `, 154 "test2.proto": ` 155 syntax = "proto3"; 156 message Empty {} 157 enum Bar { 158 BAZ = 0; 159 } 160 service Foo { 161 rpc DoSomething (Empty) returns (Foo); 162 } 163 `, 164 }, 165 expectedErrs: []string{ 166 "test2.proto:4:41: duplicate symbol Bar: already defined as service in \"test1.proto\"", 167 "test2.proto:7:41: duplicate symbol Foo: already defined as message in \"test1.proto\"", 168 "test1.proto:8:75: method Bar.Frob: unknown response type Nitz", 169 "test2.proto:8:82: method Foo.DoSomething: invalid response type: Foo is a service, not a message", 170 }, 171 }, 172 } 173 174 for i, tc := range testCases { 175 var p Parser 176 p.Accessor = FileContentsFromMap(tc.files) 177 178 var reported []ErrorWithPos 179 count := 0 180 p.ErrorReporter = trackingReporter(&reported, &count) 181 _, err := p.ParseFiles(tc.fileNames...) 182 reportedMsgs := make([]string, len(reported)) 183 for j := range reported { 184 reportedMsgs[j] = reported[j].Error() 185 } 186 t.Logf("case #%d: got %d errors:\n\t%s", i+1, len(reported), strings.Join(reportedMsgs, "\n\t")) 187 188 // returns sentinel, but all actual errors in reported 189 testutil.Eq(t, ErrInvalidSource, err, "case #%d: parse should have failed with invalid source error", i+1) 190 testutil.Eq(t, len(tc.expectedErrs), count, "case #%d: parse should have called reporter %d times", i+1, len(tc.expectedErrs)) 191 testutil.Eq(t, len(tc.expectedErrs), len(reported), "case #%d: wrong number of errors reported", i+1) 192 for j := range tc.expectedErrs { 193 testutil.Require(t, strings.Contains(reported[j].Error(), tc.expectedErrs[j]), "case #%d: parse error[%d] have %q; instead got %q", i+1, j, tc.expectedErrs[j], reported[j].Error()) 194 } 195 196 count = 0 197 p.ErrorReporter = failFastReporter(&count) 198 _, err = p.ParseFiles(tc.fileNames...) 199 testutil.Eq(t, fail, err, "case #%d: parse should have failed fast", i+1) 200 testutil.Eq(t, 1, count, "case #%d: parse should have called reporter only once", i+1) 201 202 count = 0 203 p.ErrorReporter = limitedErrReporter(3, &count) 204 _, err = p.ParseFiles(tc.fileNames...) 205 if len(tc.expectedErrs) > 3 { 206 testutil.Eq(t, tooManyErrors, err, "case #%d: parse should have failed with too many errors", i+1) 207 testutil.Eq(t, 4, count, "case #%d: parse should have called reporter 4 times", i+1) 208 } else { 209 // less than threshold means reporter always returned nil, 210 // so parse returns ErrInvalidSource sentinel 211 testutil.Eq(t, ErrInvalidSource, err, "case #%d: parse should have failed with invalid source error", i+1) 212 testutil.Eq(t, len(tc.expectedErrs), count, "case #%d: parse should have called reporter %d times", i+1, len(tc.expectedErrs)) 213 } 214 } 215 }