github.com/xiaoshude/protoreflect@v1.16.1-0.20220310024924-8c94d7247598/desc/protoparse/reporting_test.go (about)

     1  package protoparse
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"strings"
     7  	"testing"
     8  
     9  	"github.com/xiaoshude/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  }