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  }