github.com/jhump/protoreflect@v1.16.0/desc/protoparse/reporting_test.go (about) 1 package protoparse 2 3 import ( 4 "errors" 5 "fmt" 6 "reflect" 7 "strings" 8 "testing" 9 10 "github.com/jhump/protoreflect/internal/testutil" 11 ) 12 13 func TestErrorReporting(t *testing.T) { 14 tooManyErrors := errors.New("too many errors") 15 limitedErrReporter := func(limit int, count *int) ErrorReporter { 16 return func(err ErrorWithPos) error { 17 *count++ 18 if *count > limit { 19 return tooManyErrors 20 } 21 return nil 22 } 23 } 24 trackingReporter := func(errs *[]ErrorWithPos, count *int) ErrorReporter { 25 return func(err ErrorWithPos) error { 26 *count++ 27 *errs = append(*errs, err) 28 return nil 29 } 30 } 31 fail := errors.New("failure!") 32 failFastReporter := func(count *int) ErrorReporter { 33 return func(err ErrorWithPos) error { 34 *count++ 35 return fail 36 } 37 } 38 39 testCases := []struct { 40 fileNames []string 41 files map[string]string 42 expectedErrs []string 43 expectedErrsAlt []string 44 }{ 45 { 46 // multiple syntax errors 47 fileNames: []string{"test.proto"}, 48 files: map[string]string{ 49 "test.proto": ` 50 syntax = "proto"; 51 package foo 52 53 enum State { A = 0; B = 1; C; D } 54 message Foo { 55 foo = 1; 56 } 57 `, 58 }, 59 expectedErrs: []string{ 60 "test.proto:5:41: syntax error: expecting ';'", 61 "test.proto:5:69: syntax error: unexpected ';', expecting '='", 62 "test.proto:7:53: syntax error: unexpected '='", 63 }, 64 }, 65 { 66 // multiple validation errors 67 fileNames: []string{"test.proto"}, 68 files: map[string]string{ 69 "test.proto": ` 70 syntax = "proto3"; 71 message Foo { 72 string foo = 0; 73 } 74 enum State { C } 75 enum Bar { 76 BAZ = 1; 77 BUZZ = 1; 78 } 79 `, 80 }, 81 expectedErrs: []string{ 82 "test.proto:6:56: syntax error: unexpected '}', expecting '='", 83 }, 84 }, 85 { 86 // multiple link errors 87 fileNames: []string{"test.proto"}, 88 files: map[string]string{ 89 "test.proto": ` 90 syntax = "proto3"; 91 message Foo { 92 string foo = 1; 93 } 94 enum Bar { 95 BAZ = 0; 96 BAZ = 2; 97 } 98 service Bar { 99 rpc Foobar (Foo) returns (Foo); 100 rpc Foobar (Frob) returns (Nitz); 101 } 102 `, 103 }, 104 expectedErrs: []string{ 105 `test.proto:8:49: symbol "BAZ" already defined at test.proto:7:49; protobuf uses C++ scoping rules for enum values, so they exist in the scope enclosing the enum`, 106 `test.proto:10:49: symbol "Bar" already defined at test.proto:6:46`, 107 `test.proto:12:53: symbol "Bar.Foobar" already defined at test.proto:11:53`, 108 }, 109 }, 110 { 111 // syntax errors across multiple files 112 fileNames: []string{"test1.proto", "test2.proto"}, 113 files: map[string]string{ 114 "test1.proto": ` 115 syntax = "proto3"; 116 import "test2.proto"; 117 message Foo { 118 string foo = -1; 119 } 120 service Bar { 121 rpc Foo (Foo); 122 } 123 `, 124 "test2.proto": ` 125 syntax = "proto3"; 126 message Baz { 127 required string foo = 1; 128 } 129 service Service { 130 Foo; Bar; Baz; 131 } 132 `, 133 }, 134 expectedErrs: []string{ 135 "test2.proto:7:49: syntax error: unexpected identifier, expecting \"option\" or \"rpc\" or '}'", 136 "test1.proto:5:62: syntax error: unexpected '-', expecting int literal", 137 "test1.proto:8:62: syntax error: unexpected ';', expecting \"returns\"", 138 }, 139 expectedErrsAlt: []string{ 140 "test1.proto:5:62: syntax error: unexpected '-', expecting int literal", 141 "test1.proto:8:62: syntax error: unexpected ';', expecting \"returns\"", 142 "test2.proto:7:49: syntax error: unexpected identifier, expecting \"option\" or \"rpc\" or '}'", 143 }, 144 }, 145 { 146 // link errors across multiple files 147 fileNames: []string{"test1.proto", "test2.proto"}, 148 files: map[string]string{ 149 "test1.proto": ` 150 syntax = "proto3"; 151 import "test2.proto"; 152 message Foo { 153 string foo = 1; 154 } 155 service Bar { 156 rpc Frob (Empty) returns (Nitz); 157 } 158 `, 159 "test2.proto": ` 160 syntax = "proto3"; 161 message Empty {} 162 enum Bar { 163 BAZ = 0; 164 } 165 service Foo { 166 rpc DoSomething (Empty) returns (Foo); 167 } 168 `, 169 }, 170 expectedErrs: []string{ 171 "test2.proto:8:82: method Foo.DoSomething: invalid response type: Foo is a service, not a message", 172 }, 173 }, 174 } 175 176 for i, tc := range testCases { 177 var p Parser 178 p.Accessor = FileContentsFromMap(tc.files) 179 180 var reported []ErrorWithPos 181 count := 0 182 p.ErrorReporter = trackingReporter(&reported, &count) 183 _, err := p.ParseFiles(tc.fileNames...) 184 reportedMsgs := make([]string, len(reported)) 185 for j := range reported { 186 reportedMsgs[j] = reported[j].Error() 187 } 188 t.Logf("case #%d: got %d errors:\n\t%s", i+1, len(reported), strings.Join(reportedMsgs, "\n\t")) 189 190 // returns sentinel, but all actual errors in reported 191 testutil.Eq(t, ErrInvalidSource, err, "case #%d: parse should have failed with invalid source error", i+1) 192 actual := make([]string, len(reported)) 193 for j := range reported { 194 actual[j] = reported[j].Error() 195 } 196 expected := tc.expectedErrs 197 if len(tc.expectedErrsAlt) > 0 && !reflect.DeepEqual(tc.expectedErrs, actual) { 198 expected = tc.expectedErrsAlt 199 } 200 testutil.Eq(t, len(expected), count, "case #%d: parse should have called reporter %d times", i+1, len(tc.expectedErrs)) 201 testutil.Eq(t, expected, actual, "case #%d: wrong errors reported", i+1) 202 for j := range expected { 203 split := strings.SplitN(expected[j], ":", 4) 204 testutil.Eq(t, 4, len(split), "case #%d: expected %q [%d] to contain at least 4 elements split by :", i+1, tc.expectedErrs[j], j) 205 testutil.Eq(t, split[3], " "+reported[j].Unwrap().Error(), "case #%d: parse error underlying[%d] have %q; instead got %q", i+1, j, split[3], reported[j].Unwrap().Error()) 206 } 207 208 count = 0 209 p.ErrorReporter = failFastReporter(&count) 210 _, err = p.ParseFiles(tc.fileNames...) 211 testutil.Eq(t, fail, err, "case #%d: parse should have failed fast", i+1) 212 testutil.Eq(t, 1, count, "case #%d: parse should have called reporter only once", i+1) 213 214 count = 0 215 p.ErrorReporter = limitedErrReporter(3, &count) 216 _, err = p.ParseFiles(tc.fileNames...) 217 if len(tc.expectedErrs) > 3 { 218 testutil.Eq(t, tooManyErrors, err, "case #%d: parse should have failed with too many errors", i+1) 219 testutil.Eq(t, 4, count, "case #%d: parse should have called reporter 4 times", i+1) 220 } else { 221 // less than threshold means reporter always returned nil, 222 // so parse returns ErrInvalidSource sentinel 223 testutil.Eq(t, ErrInvalidSource, err, "case #%d: parse should have failed with invalid source error", i+1) 224 testutil.Eq(t, len(tc.expectedErrs), count, "case #%d: parse should have called reporter %d times", i+1, len(tc.expectedErrs)) 225 } 226 } 227 } 228 229 func TestWarningReporting(t *testing.T) { 230 type msg struct { 231 pos SourcePos 232 text string 233 } 234 var msgs []msg 235 rep := func(warn ErrorWithPos) { 236 msgs = append(msgs, msg{ 237 pos: warn.GetPosition(), text: warn.Unwrap().Error(), 238 }) 239 } 240 241 testCases := []struct { 242 name string 243 sources map[string]string 244 expectedNotices []string 245 }{ 246 { 247 name: "syntax proto2", 248 sources: map[string]string{ 249 "test.proto": `syntax = "proto2"; message Foo {}`, 250 }, 251 }, 252 { 253 name: "syntax proto3", 254 sources: map[string]string{ 255 "test.proto": `syntax = "proto3"; message Foo {}`, 256 }, 257 }, 258 { 259 name: "no syntax", 260 sources: map[string]string{ 261 "test.proto": `message Foo {}`, 262 }, 263 expectedNotices: []string{ 264 "test.proto:1:1: no syntax specified; defaulting to proto2 syntax", 265 }, 266 }, 267 { 268 name: "used import", 269 sources: map[string]string{ 270 "test.proto": `syntax = "proto3"; import "foo.proto"; message Foo { Bar bar = 1; }`, 271 "foo.proto": `syntax = "proto3"; message Bar { string name = 1; }`, 272 }, 273 }, 274 { 275 name: "used public import", 276 sources: map[string]string{ 277 "test.proto": `syntax = "proto3"; import "foo.proto"; message Foo { Bar bar = 1; }`, 278 // we're only asking to compile test.proto, so we won't report unused import for baz.proto 279 "foo.proto": `syntax = "proto3"; import public "bar.proto"; import "baz.proto";`, 280 "bar.proto": `syntax = "proto3"; message Bar { string name = 1; }`, 281 "baz.proto": `syntax = "proto3"; message Baz { }`, 282 }, 283 }, 284 { 285 name: "used nested public import", 286 sources: map[string]string{ 287 "test.proto": `syntax = "proto3"; import "foo.proto"; message Foo { Bar bar = 1; }`, 288 "foo.proto": `syntax = "proto3"; import public "baz.proto";`, 289 "baz.proto": `syntax = "proto3"; import public "bar.proto";`, 290 "bar.proto": `syntax = "proto3"; message Bar { string name = 1; }`, 291 }, 292 }, 293 { 294 name: "unused import", 295 sources: map[string]string{ 296 "test.proto": `syntax = "proto3"; import "foo.proto"; message Foo { string name = 1; }`, 297 "foo.proto": `syntax = "proto3"; message Bar { string name = 1; }`, 298 }, 299 expectedNotices: []string{ 300 `test.proto:1:20: import "foo.proto" not used`, 301 }, 302 }, 303 { 304 name: "multiple unused imports", 305 sources: map[string]string{ 306 "test.proto": `syntax = "proto3"; import "foo.proto"; import "bar.proto"; import "baz.proto"; message Test { Bar bar = 1; }`, 307 "foo.proto": `syntax = "proto3"; message Foo {};`, 308 "bar.proto": `syntax = "proto3"; message Bar {};`, 309 "baz.proto": `syntax = "proto3"; message Baz {};`, 310 }, 311 expectedNotices: []string{ 312 `test.proto:1:20: import "foo.proto" not used`, 313 `test.proto:1:60: import "baz.proto" not used`, 314 }, 315 }, 316 { 317 name: "unused public import is not reported", 318 sources: map[string]string{ 319 "test.proto": `syntax = "proto3"; import public "foo.proto"; message Foo { }`, 320 "foo.proto": `syntax = "proto3"; message Bar { string name = 1; }`, 321 }, 322 }, 323 { 324 name: "unused descriptor.proto import", 325 sources: map[string]string{ 326 "test.proto": `syntax = "proto3"; import "google/protobuf/descriptor.proto"; message Foo { }`, 327 }, 328 expectedNotices: []string{ 329 `test.proto:1:20: import "google/protobuf/descriptor.proto" not used`, 330 }, 331 }, 332 { 333 name: "explicitly used descriptor.proto import", 334 sources: map[string]string{ 335 "test.proto": `syntax = "proto3"; import "google/protobuf/descriptor.proto"; extend google.protobuf.MessageOptions { string foobar = 33333; }`, 336 }, 337 }, 338 { 339 // having options implicitly uses decriptor.proto 340 name: "implicitly used descriptor.proto import", 341 sources: map[string]string{ 342 "test.proto": `syntax = "proto3"; import "google/protobuf/descriptor.proto"; message Foo { option deprecated = true; }`, 343 }, 344 }, 345 { 346 // makes sure we can use a given descriptor.proto to override non-custom options 347 name: "implicitly used descriptor.proto import with new option", 348 sources: map[string]string{ 349 "test.proto": `syntax = "proto3"; import "google/protobuf/descriptor.proto"; message Foo { option foobar = 123; }`, 350 "google/protobuf/descriptor.proto": `syntax = "proto2"; package google.protobuf; message MessageOptions { optional fixed32 foobar = 99; }`, 351 }, 352 }, 353 } 354 for _, testCase := range testCases { 355 t.Run(testCase.name, func(t *testing.T) { 356 accessor := FileContentsFromMap(testCase.sources) 357 p := Parser{ 358 Accessor: accessor, 359 WarningReporter: rep, 360 } 361 msgs = nil 362 _, err := p.ParseFiles("test.proto") 363 testutil.Ok(t, err) 364 365 actualNotices := make([]string, len(msgs)) 366 for j, msg := range msgs { 367 actualNotices[j] = fmt.Sprintf("%s: %s", msg.pos, msg.text) 368 } 369 testutil.Eq(t, testCase.expectedNotices, actualNotices) 370 }) 371 } 372 }