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