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  }