github.com/phpstudyer/protoreflect@v1.7.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/phpstudyer/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 Bar.BAZ: already defined as enum value", 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 source string 239 expectedNotices []string 240 }{ 241 { 242 source: `syntax = "proto2"; message Foo {}`, 243 }, 244 { 245 source: `syntax = "proto3"; message Foo {}`, 246 }, 247 { 248 source: `message Foo {}`, 249 expectedNotices: []string{ 250 "test.proto:1:1: no syntax specified; defaulting to proto2 syntax", 251 }, 252 }, 253 } 254 for _, testCase := range testCases { 255 accessor := FileContentsFromMap(map[string]string{ 256 "test.proto": testCase.source, 257 }) 258 p := Parser{ 259 Accessor: accessor, 260 WarningReporter: rep, 261 } 262 msgs = nil 263 _, err := p.ParseFiles("test.proto") 264 testutil.Ok(t, err) 265 266 actualNotices := make([]string, len(msgs)) 267 for j, msg := range msgs { 268 actualNotices[j] = fmt.Sprintf("%s: %s", msg.pos, msg.text) 269 } 270 testutil.Eq(t, testCase.expectedNotices, actualNotices) 271 } 272 }