blob: c77a84c645fa6c16e5b32bc888010af5ea71b211 [file] [log] [blame]
// Copyright 2024 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package protoparse_test
import (
"io"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"google.golang.org/open2opaque/internal/protodetect"
"google.golang.org/open2opaque/internal/protoparse"
gofeaturespb "google.golang.org/protobuf/types/gofeaturespb"
)
const filenameWithOpaqueDefault = "foo.proto"
func TestTextRangeToByteRange(t *testing.T) {
testCases := []struct {
in string
textRange protoparse.TextRange
want string
}{
{
in: "0123456789",
textRange: protoparse.TextRange{
BeginLine: 0,
BeginCol: 2,
EndLine: 0,
EndCol: 6,
},
want: "2345",
},
{
in: `0Hello, 世界0
1Hello, 世界1
2Hello, 世界2
3Hello, 世界3`,
textRange: protoparse.TextRange{
BeginLine: 1,
BeginCol: 8,
EndLine: 3,
EndCol: 6,
},
want: `世界1
2Hello, 世界2
3Hello`,
},
}
for _, tc := range testCases {
from, to, err := tc.textRange.ToByteRange([]byte(tc.in))
if err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(tc.want, string(tc.in[from:to])); diff != "" {
t.Errorf("TextRange.ToByteRange: diff (-want +got):\n%v\n", diff)
}
}
}
func TestFileOpt(t *testing.T) {
testCases := []struct {
name string
protoIn string
wantOpt *protoparse.FileOpt
wantAPIStr string
wantHasLeadingComment bool
}{
{
name: "proto3_default_opaque",
protoIn: `
syntax = "proto3";
package pkg;
message M{}
`,
wantOpt: &protoparse.FileOpt{
File: filenameWithOpaqueDefault,
Package: "pkg",
GoAPI: protodetect.DefaultFileLevel("dummy.proto"),
IsExplicit: false,
Syntax: "proto3",
},
},
{
name: "edition_default_opaque",
protoIn: `
edition = "2023";
package pkg;
message M{}
`,
wantOpt: &protoparse.FileOpt{
File: filenameWithOpaqueDefault,
Package: "pkg",
GoAPI: protodetect.DefaultFileLevel("dummy.proto"),
IsExplicit: false,
Syntax: "editions",
},
},
{
name: "edition_explicit_hybrid",
protoIn: `
edition = "2023";
package pkg;
option features.(pb.go).api_level = API_HYBRID;
message M{}
`,
wantOpt: &protoparse.FileOpt{
File: filenameWithOpaqueDefault,
Package: "pkg",
GoAPI: gofeaturespb.GoFeatures_API_HYBRID,
IsExplicit: true,
Syntax: "editions",
},
wantAPIStr: "option features.(pb.go).api_level = API_HYBRID;",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
parser := protoparse.NewParserWithAccessor(func(string) (io.ReadCloser, error) {
return io.NopCloser(strings.NewReader(tc.protoIn)), nil
})
got, err := parser.ParseFile(filenameWithOpaqueDefault, true)
if err != nil {
t.Fatal(err)
}
ingores := cmp.Options{
cmpopts.IgnoreFields(protoparse.FileOpt{}, "APIInfo"),
cmpopts.IgnoreFields(protoparse.FileOpt{}, "SourceCodeInfo"),
cmpopts.IgnoreFields(protoparse.FileOpt{}, "MessageOpts"),
}
if diff := cmp.Diff(tc.wantOpt, got, ingores...); diff != "" {
t.Errorf("parser.ParseFile(): diff (-want +got):\n%v\n", diff)
}
if !tc.wantOpt.IsExplicit {
// File doesn't explicitly define API flag, don't check content of
// APIInfo
return
}
if got.APIInfo == nil {
t.Fatal("API flag is set explicitly, but APIInfo is nil")
}
if got, want := got.APIInfo.HasLeadingComment, tc.wantHasLeadingComment; got != want {
t.Fatalf("got APIInfo.HasLeadingComment %v, want %v", got, want)
}
from, to, err := got.APIInfo.TextRange.ToByteRange([]byte(tc.protoIn))
if err != nil {
t.Fatalf("TextRange.ToByteRange: %v", err)
}
gotAPIStr := string([]byte(tc.protoIn)[from:to])
if diff := cmp.Diff(tc.wantAPIStr, gotAPIStr); diff != "" {
t.Errorf("API string: diff (-want +got):\n%v\n", diff)
}
})
}
}
func traverseMsg(opt *protoparse.MessageOpt, f func(*protoparse.MessageOpt)) {
f(opt)
for _, c := range opt.Children {
traverseMsg(c, f)
}
}
func TestMessageOpt(t *testing.T) {
type msgInfo struct {
Opt *protoparse.MessageOpt
APIString string
HasLeadingComment bool
}
testCases := []struct {
name string
protoIn string
want []msgInfo
}{
{
name: "edition_file_hybrid_message_one_msg_open",
protoIn: `
edition = "2023";
package pkg;
option features.(pb.go).api_level = API_HYBRID;
message A{
option features.(pb.go).api_level = API_OPEN;
message A1{}
}
`,
want: []msgInfo{
{
Opt: &protoparse.MessageOpt{
Message: "A",
GoAPI: gofeaturespb.GoFeatures_API_OPEN,
IsExplicit: true,
},
APIString: `option features.(pb.go).api_level = API_OPEN;`,
},
{
Opt: &protoparse.MessageOpt{
Message: "A.A1",
// API level of parent message is inherited.
GoAPI: gofeaturespb.GoFeatures_API_OPEN,
IsExplicit: false,
},
},
},
},
{
name: "edition_file_hybrid_message_one_msg_open_with_leading_comment",
protoIn: `
edition = "2023";
package pkg;
option features.(pb.go).api_level = API_HYBRID;
message A{
// leading comment
option features.(pb.go).api_level = API_OPEN;
message A1{
option features.(pb.go).api_level = API_OPAQUE;
}
}
`,
// https://github.com/bufbuild/protocompile/blob/main/ast/file_info.go#L362-L364
want: []msgInfo{
{
Opt: &protoparse.MessageOpt{
Message: "A",
GoAPI: gofeaturespb.GoFeatures_API_OPEN,
IsExplicit: true,
},
APIString: `option features.(pb.go).api_level = API_OPEN;`,
HasLeadingComment: true,
},
{
Opt: &protoparse.MessageOpt{
Message: "A.A1",
GoAPI: gofeaturespb.GoFeatures_API_OPAQUE,
IsExplicit: true,
},
APIString: `option features.(pb.go).api_level = API_OPAQUE;`,
HasLeadingComment: false,
},
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
parser := protoparse.NewParserWithAccessor(func(string) (io.ReadCloser, error) {
return io.NopCloser(strings.NewReader(tc.protoIn)), nil
})
gotFopt, err := parser.ParseFile(filenameWithOpaqueDefault, false)
if err != nil {
t.Fatal(err)
}
// Flatten the tree of message options and wrap into []msgInfo to allow
// easy comparison. Check tree structure below.
var gotInfo []msgInfo
for _, optLoop := range gotFopt.MessageOpts {
traverseMsg(optLoop, func(opt *protoparse.MessageOpt) {
info := msgInfo{Opt: opt}
if opt.IsExplicit {
if opt.APIInfo == nil {
t.Fatalf("API flag is set explicitly, but APIInfo of message %q is nil", opt.Message)
}
info.HasLeadingComment = opt.APIInfo.HasLeadingComment
from, to, err := opt.APIInfo.TextRange.ToByteRange([]byte(tc.protoIn))
if err != nil {
t.Fatalf("TextRange.ToByteRange: %v", err)
}
info.APIString = string([]byte(tc.protoIn)[from:to])
}
gotInfo = append(gotInfo, info)
})
}
ingores := cmp.Options{
cmpopts.IgnoreFields(protoparse.MessageOpt{}, "APIInfo"),
cmpopts.IgnoreFields(protoparse.MessageOpt{}, "LocPath"),
cmpopts.IgnoreFields(protoparse.MessageOpt{}, "Parent"),
cmpopts.IgnoreFields(protoparse.MessageOpt{}, "Children"),
}
if diff := cmp.Diff(tc.want, gotInfo, ingores...); diff != "" {
t.Errorf("diff (-want +got):\n%v\n", diff)
}
// Check the tree structure of the message options. If I'm called A.A1.A2,
// is my parent called A.A1?
for _, optOuter := range gotFopt.MessageOpts {
traverseMsg(optOuter, func(opt *protoparse.MessageOpt) {
parts := strings.Split(opt.Message, ".")
if len(parts) == 1 {
if opt.Parent != nil {
t.Errorf("parent pointer of top-level message %q isn't nil", opt.Message)
}
return
}
if opt.Parent == nil {
t.Errorf("parent pointer of nested message %q is nil", opt.Message)
}
if got, want := opt.Parent.Message, strings.Join(parts[:len(parts)-1], "."); got != want {
t.Errorf("got name of parent message %q, want %q", got, want)
}
})
}
})
}
}