1*1c12ee1eSDan Willemsen// Copyright 2018 The Go Authors. All rights reserved. 2*1c12ee1eSDan Willemsen// Use of this source code is governed by a BSD-style 3*1c12ee1eSDan Willemsen// license that can be found in the LICENSE file. 4*1c12ee1eSDan Willemsen 5*1c12ee1eSDan Willemsenpackage protogen 6*1c12ee1eSDan Willemsen 7*1c12ee1eSDan Willemsenimport ( 8*1c12ee1eSDan Willemsen "flag" 9*1c12ee1eSDan Willemsen "fmt" 10*1c12ee1eSDan Willemsen "testing" 11*1c12ee1eSDan Willemsen 12*1c12ee1eSDan Willemsen "github.com/google/go-cmp/cmp" 13*1c12ee1eSDan Willemsen 14*1c12ee1eSDan Willemsen "google.golang.org/protobuf/proto" 15*1c12ee1eSDan Willemsen "google.golang.org/protobuf/reflect/protoreflect" 16*1c12ee1eSDan Willemsen 17*1c12ee1eSDan Willemsen "google.golang.org/protobuf/types/descriptorpb" 18*1c12ee1eSDan Willemsen "google.golang.org/protobuf/types/pluginpb" 19*1c12ee1eSDan Willemsen) 20*1c12ee1eSDan Willemsen 21*1c12ee1eSDan Willemsenfunc TestPluginParameters(t *testing.T) { 22*1c12ee1eSDan Willemsen var flags flag.FlagSet 23*1c12ee1eSDan Willemsen value := flags.Int("integer", 0, "") 24*1c12ee1eSDan Willemsen const params = "integer=2" 25*1c12ee1eSDan Willemsen _, err := Options{ 26*1c12ee1eSDan Willemsen ParamFunc: flags.Set, 27*1c12ee1eSDan Willemsen }.New(&pluginpb.CodeGeneratorRequest{ 28*1c12ee1eSDan Willemsen Parameter: proto.String(params), 29*1c12ee1eSDan Willemsen }) 30*1c12ee1eSDan Willemsen if err != nil { 31*1c12ee1eSDan Willemsen t.Errorf("New(generator parameters %q): %v", params, err) 32*1c12ee1eSDan Willemsen } 33*1c12ee1eSDan Willemsen if *value != 2 { 34*1c12ee1eSDan Willemsen t.Errorf("New(generator parameters %q): integer=%v, want 2", params, *value) 35*1c12ee1eSDan Willemsen } 36*1c12ee1eSDan Willemsen} 37*1c12ee1eSDan Willemsen 38*1c12ee1eSDan Willemsenfunc TestPluginParameterErrors(t *testing.T) { 39*1c12ee1eSDan Willemsen for _, parameter := range []string{ 40*1c12ee1eSDan Willemsen "unknown=1", 41*1c12ee1eSDan Willemsen "boolean=error", 42*1c12ee1eSDan Willemsen } { 43*1c12ee1eSDan Willemsen var flags flag.FlagSet 44*1c12ee1eSDan Willemsen flags.Bool("boolean", false, "") 45*1c12ee1eSDan Willemsen _, err := Options{ 46*1c12ee1eSDan Willemsen ParamFunc: flags.Set, 47*1c12ee1eSDan Willemsen }.New(&pluginpb.CodeGeneratorRequest{ 48*1c12ee1eSDan Willemsen Parameter: proto.String(parameter), 49*1c12ee1eSDan Willemsen }) 50*1c12ee1eSDan Willemsen if err == nil { 51*1c12ee1eSDan Willemsen t.Errorf("New(generator parameters %q): want error, got nil", parameter) 52*1c12ee1eSDan Willemsen } 53*1c12ee1eSDan Willemsen } 54*1c12ee1eSDan Willemsen} 55*1c12ee1eSDan Willemsen 56*1c12ee1eSDan Willemsenfunc TestNoGoPackage(t *testing.T) { 57*1c12ee1eSDan Willemsen _, err := Options{}.New(&pluginpb.CodeGeneratorRequest{ 58*1c12ee1eSDan Willemsen ProtoFile: []*descriptorpb.FileDescriptorProto{ 59*1c12ee1eSDan Willemsen { 60*1c12ee1eSDan Willemsen Name: proto.String("testdata/go_package/no_go_package.proto"), 61*1c12ee1eSDan Willemsen Syntax: proto.String(protoreflect.Proto3.String()), 62*1c12ee1eSDan Willemsen Package: proto.String("goproto.testdata"), 63*1c12ee1eSDan Willemsen }, 64*1c12ee1eSDan Willemsen }, 65*1c12ee1eSDan Willemsen }) 66*1c12ee1eSDan Willemsen if err == nil { 67*1c12ee1eSDan Willemsen t.Fatalf("missing go_package option: New(req) = nil, want error") 68*1c12ee1eSDan Willemsen } 69*1c12ee1eSDan Willemsen} 70*1c12ee1eSDan Willemsen 71*1c12ee1eSDan Willemsenfunc TestInvalidImportPath(t *testing.T) { 72*1c12ee1eSDan Willemsen _, err := Options{}.New(&pluginpb.CodeGeneratorRequest{ 73*1c12ee1eSDan Willemsen ProtoFile: []*descriptorpb.FileDescriptorProto{ 74*1c12ee1eSDan Willemsen { 75*1c12ee1eSDan Willemsen Name: proto.String("testdata/go_package/no_go_package.proto"), 76*1c12ee1eSDan Willemsen Syntax: proto.String(protoreflect.Proto3.String()), 77*1c12ee1eSDan Willemsen Package: proto.String("goproto.testdata"), 78*1c12ee1eSDan Willemsen Options: &descriptorpb.FileOptions{ 79*1c12ee1eSDan Willemsen GoPackage: proto.String("foo"), 80*1c12ee1eSDan Willemsen }, 81*1c12ee1eSDan Willemsen }, 82*1c12ee1eSDan Willemsen }, 83*1c12ee1eSDan Willemsen }) 84*1c12ee1eSDan Willemsen if err == nil { 85*1c12ee1eSDan Willemsen t.Fatalf("missing go_package option: New(req) = nil, want error") 86*1c12ee1eSDan Willemsen } 87*1c12ee1eSDan Willemsen} 88*1c12ee1eSDan Willemsen 89*1c12ee1eSDan Willemsenfunc TestPackageNamesAndPaths(t *testing.T) { 90*1c12ee1eSDan Willemsen const ( 91*1c12ee1eSDan Willemsen filename = "dir/filename.proto" 92*1c12ee1eSDan Willemsen protoPackageName = "proto.package" 93*1c12ee1eSDan Willemsen ) 94*1c12ee1eSDan Willemsen for _, test := range []struct { 95*1c12ee1eSDan Willemsen desc string 96*1c12ee1eSDan Willemsen parameter string 97*1c12ee1eSDan Willemsen goPackageOption string 98*1c12ee1eSDan Willemsen generate bool 99*1c12ee1eSDan Willemsen wantPackageName GoPackageName 100*1c12ee1eSDan Willemsen wantImportPath GoImportPath 101*1c12ee1eSDan Willemsen wantFilename string 102*1c12ee1eSDan Willemsen }{ 103*1c12ee1eSDan Willemsen { 104*1c12ee1eSDan Willemsen desc: "go_package option sets import path", 105*1c12ee1eSDan Willemsen goPackageOption: "golang.org/x/foo", 106*1c12ee1eSDan Willemsen generate: true, 107*1c12ee1eSDan Willemsen wantPackageName: "foo", 108*1c12ee1eSDan Willemsen wantImportPath: "golang.org/x/foo", 109*1c12ee1eSDan Willemsen wantFilename: "golang.org/x/foo/filename", 110*1c12ee1eSDan Willemsen }, 111*1c12ee1eSDan Willemsen { 112*1c12ee1eSDan Willemsen desc: "go_package option sets import path without slashes", 113*1c12ee1eSDan Willemsen goPackageOption: "golang.org;foo", 114*1c12ee1eSDan Willemsen generate: true, 115*1c12ee1eSDan Willemsen wantPackageName: "foo", 116*1c12ee1eSDan Willemsen wantImportPath: "golang.org", 117*1c12ee1eSDan Willemsen wantFilename: "golang.org/filename", 118*1c12ee1eSDan Willemsen }, 119*1c12ee1eSDan Willemsen { 120*1c12ee1eSDan Willemsen desc: "go_package option sets import path and package", 121*1c12ee1eSDan Willemsen goPackageOption: "golang.org/x/foo;bar", 122*1c12ee1eSDan Willemsen generate: true, 123*1c12ee1eSDan Willemsen wantPackageName: "bar", 124*1c12ee1eSDan Willemsen wantImportPath: "golang.org/x/foo", 125*1c12ee1eSDan Willemsen wantFilename: "golang.org/x/foo/filename", 126*1c12ee1eSDan Willemsen }, 127*1c12ee1eSDan Willemsen { 128*1c12ee1eSDan Willemsen desc: "command line sets import path for a file", 129*1c12ee1eSDan Willemsen parameter: "Mdir/filename.proto=golang.org/x/bar", 130*1c12ee1eSDan Willemsen goPackageOption: "golang.org/x/foo", 131*1c12ee1eSDan Willemsen generate: true, 132*1c12ee1eSDan Willemsen wantPackageName: "foo", 133*1c12ee1eSDan Willemsen wantImportPath: "golang.org/x/bar", 134*1c12ee1eSDan Willemsen wantFilename: "golang.org/x/bar/filename", 135*1c12ee1eSDan Willemsen }, 136*1c12ee1eSDan Willemsen { 137*1c12ee1eSDan Willemsen desc: "command line sets import path for a file with package name specified", 138*1c12ee1eSDan Willemsen parameter: "Mdir/filename.proto=golang.org/x/bar;bar", 139*1c12ee1eSDan Willemsen goPackageOption: "golang.org/x/foo", 140*1c12ee1eSDan Willemsen generate: true, 141*1c12ee1eSDan Willemsen wantPackageName: "bar", 142*1c12ee1eSDan Willemsen wantImportPath: "golang.org/x/bar", 143*1c12ee1eSDan Willemsen wantFilename: "golang.org/x/bar/filename", 144*1c12ee1eSDan Willemsen }, 145*1c12ee1eSDan Willemsen { 146*1c12ee1eSDan Willemsen desc: "module option set", 147*1c12ee1eSDan Willemsen parameter: "module=golang.org/x", 148*1c12ee1eSDan Willemsen goPackageOption: "golang.org/x/foo", 149*1c12ee1eSDan Willemsen generate: false, 150*1c12ee1eSDan Willemsen wantPackageName: "foo", 151*1c12ee1eSDan Willemsen wantImportPath: "golang.org/x/foo", 152*1c12ee1eSDan Willemsen wantFilename: "foo/filename", 153*1c12ee1eSDan Willemsen }, 154*1c12ee1eSDan Willemsen { 155*1c12ee1eSDan Willemsen desc: "paths=import uses import path from command line", 156*1c12ee1eSDan Willemsen parameter: "paths=import,Mdir/filename.proto=golang.org/x/bar", 157*1c12ee1eSDan Willemsen goPackageOption: "golang.org/x/foo", 158*1c12ee1eSDan Willemsen generate: true, 159*1c12ee1eSDan Willemsen wantPackageName: "foo", 160*1c12ee1eSDan Willemsen wantImportPath: "golang.org/x/bar", 161*1c12ee1eSDan Willemsen wantFilename: "golang.org/x/bar/filename", 162*1c12ee1eSDan Willemsen }, 163*1c12ee1eSDan Willemsen { 164*1c12ee1eSDan Willemsen desc: "module option implies paths=import", 165*1c12ee1eSDan Willemsen parameter: "module=golang.org/x,Mdir/filename.proto=golang.org/x/foo", 166*1c12ee1eSDan Willemsen generate: false, 167*1c12ee1eSDan Willemsen wantPackageName: "foo", 168*1c12ee1eSDan Willemsen wantImportPath: "golang.org/x/foo", 169*1c12ee1eSDan Willemsen wantFilename: "foo/filename", 170*1c12ee1eSDan Willemsen }, 171*1c12ee1eSDan Willemsen } { 172*1c12ee1eSDan Willemsen context := fmt.Sprintf(` 173*1c12ee1eSDan WillemsenTEST: %v 174*1c12ee1eSDan Willemsen --go_out=%v:. 175*1c12ee1eSDan Willemsen file %q: generate=%v 176*1c12ee1eSDan Willemsen option go_package = %q; 177*1c12ee1eSDan Willemsen 178*1c12ee1eSDan Willemsen `, 179*1c12ee1eSDan Willemsen test.desc, test.parameter, filename, test.generate, test.goPackageOption) 180*1c12ee1eSDan Willemsen 181*1c12ee1eSDan Willemsen req := &pluginpb.CodeGeneratorRequest{ 182*1c12ee1eSDan Willemsen Parameter: proto.String(test.parameter), 183*1c12ee1eSDan Willemsen ProtoFile: []*descriptorpb.FileDescriptorProto{ 184*1c12ee1eSDan Willemsen { 185*1c12ee1eSDan Willemsen Name: proto.String(filename), 186*1c12ee1eSDan Willemsen Package: proto.String(protoPackageName), 187*1c12ee1eSDan Willemsen Options: &descriptorpb.FileOptions{ 188*1c12ee1eSDan Willemsen GoPackage: proto.String(test.goPackageOption), 189*1c12ee1eSDan Willemsen }, 190*1c12ee1eSDan Willemsen }, 191*1c12ee1eSDan Willemsen }, 192*1c12ee1eSDan Willemsen } 193*1c12ee1eSDan Willemsen if test.generate { 194*1c12ee1eSDan Willemsen req.FileToGenerate = []string{filename} 195*1c12ee1eSDan Willemsen } 196*1c12ee1eSDan Willemsen gen, err := Options{}.New(req) 197*1c12ee1eSDan Willemsen if err != nil { 198*1c12ee1eSDan Willemsen t.Errorf("%vNew(req) = %v", context, err) 199*1c12ee1eSDan Willemsen continue 200*1c12ee1eSDan Willemsen } 201*1c12ee1eSDan Willemsen gotFile, ok := gen.FilesByPath[filename] 202*1c12ee1eSDan Willemsen if !ok { 203*1c12ee1eSDan Willemsen t.Errorf("%v%v: missing file info", context, filename) 204*1c12ee1eSDan Willemsen continue 205*1c12ee1eSDan Willemsen } 206*1c12ee1eSDan Willemsen if got, want := gotFile.GoPackageName, test.wantPackageName; got != want { 207*1c12ee1eSDan Willemsen t.Errorf("%vGoPackageName=%v, want %v", context, got, want) 208*1c12ee1eSDan Willemsen } 209*1c12ee1eSDan Willemsen if got, want := gotFile.GoImportPath, test.wantImportPath; got != want { 210*1c12ee1eSDan Willemsen t.Errorf("%vGoImportPath=%v, want %v", context, got, want) 211*1c12ee1eSDan Willemsen } 212*1c12ee1eSDan Willemsen gen.NewGeneratedFile(gotFile.GeneratedFilenamePrefix, "") 213*1c12ee1eSDan Willemsen resp := gen.Response() 214*1c12ee1eSDan Willemsen if got, want := resp.File[0].GetName(), test.wantFilename; got != want { 215*1c12ee1eSDan Willemsen t.Errorf("%vgenerated filename=%v, want %v", context, got, want) 216*1c12ee1eSDan Willemsen } 217*1c12ee1eSDan Willemsen } 218*1c12ee1eSDan Willemsen} 219*1c12ee1eSDan Willemsen 220*1c12ee1eSDan Willemsenfunc TestPackageNameInference(t *testing.T) { 221*1c12ee1eSDan Willemsen gen, err := Options{}.New(&pluginpb.CodeGeneratorRequest{ 222*1c12ee1eSDan Willemsen Parameter: proto.String("Mdir/file1.proto=path/to/file1"), 223*1c12ee1eSDan Willemsen ProtoFile: []*descriptorpb.FileDescriptorProto{ 224*1c12ee1eSDan Willemsen { 225*1c12ee1eSDan Willemsen Name: proto.String("dir/file1.proto"), 226*1c12ee1eSDan Willemsen Package: proto.String("proto.package"), 227*1c12ee1eSDan Willemsen }, 228*1c12ee1eSDan Willemsen { 229*1c12ee1eSDan Willemsen Name: proto.String("dir/file2.proto"), 230*1c12ee1eSDan Willemsen Package: proto.String("proto.package"), 231*1c12ee1eSDan Willemsen Options: &descriptorpb.FileOptions{ 232*1c12ee1eSDan Willemsen GoPackage: proto.String("path/to/file2"), 233*1c12ee1eSDan Willemsen }, 234*1c12ee1eSDan Willemsen }, 235*1c12ee1eSDan Willemsen }, 236*1c12ee1eSDan Willemsen FileToGenerate: []string{"dir/file1.proto", "dir/file2.proto"}, 237*1c12ee1eSDan Willemsen }) 238*1c12ee1eSDan Willemsen if err != nil { 239*1c12ee1eSDan Willemsen t.Fatalf("New(req) = %v", err) 240*1c12ee1eSDan Willemsen } 241*1c12ee1eSDan Willemsen if f1, ok := gen.FilesByPath["dir/file1.proto"]; !ok { 242*1c12ee1eSDan Willemsen t.Errorf("missing file info for dir/file1.proto") 243*1c12ee1eSDan Willemsen } else if f1.GoPackageName != "file1" { 244*1c12ee1eSDan Willemsen t.Errorf("dir/file1.proto: GoPackageName=%v, want foo; package name should be derived from dir/file2.proto", f1.GoPackageName) 245*1c12ee1eSDan Willemsen } 246*1c12ee1eSDan Willemsen} 247*1c12ee1eSDan Willemsen 248*1c12ee1eSDan Willemsenfunc TestInconsistentPackageNames(t *testing.T) { 249*1c12ee1eSDan Willemsen _, err := Options{}.New(&pluginpb.CodeGeneratorRequest{ 250*1c12ee1eSDan Willemsen ProtoFile: []*descriptorpb.FileDescriptorProto{ 251*1c12ee1eSDan Willemsen { 252*1c12ee1eSDan Willemsen Name: proto.String("dir/file1.proto"), 253*1c12ee1eSDan Willemsen Package: proto.String("proto.package"), 254*1c12ee1eSDan Willemsen Options: &descriptorpb.FileOptions{ 255*1c12ee1eSDan Willemsen GoPackage: proto.String("golang.org/x/foo"), 256*1c12ee1eSDan Willemsen }, 257*1c12ee1eSDan Willemsen }, 258*1c12ee1eSDan Willemsen { 259*1c12ee1eSDan Willemsen Name: proto.String("dir/file2.proto"), 260*1c12ee1eSDan Willemsen Package: proto.String("proto.package"), 261*1c12ee1eSDan Willemsen Options: &descriptorpb.FileOptions{ 262*1c12ee1eSDan Willemsen GoPackage: proto.String("golang.org/x/foo;bar"), 263*1c12ee1eSDan Willemsen }, 264*1c12ee1eSDan Willemsen }, 265*1c12ee1eSDan Willemsen }, 266*1c12ee1eSDan Willemsen FileToGenerate: []string{"dir/file1.proto", "dir/file2.proto"}, 267*1c12ee1eSDan Willemsen }) 268*1c12ee1eSDan Willemsen if err == nil { 269*1c12ee1eSDan Willemsen t.Fatalf("inconsistent package names for the same import path: New(req) = nil, want error") 270*1c12ee1eSDan Willemsen } 271*1c12ee1eSDan Willemsen} 272*1c12ee1eSDan Willemsen 273*1c12ee1eSDan Willemsenfunc TestImports(t *testing.T) { 274*1c12ee1eSDan Willemsen gen, err := Options{}.New(&pluginpb.CodeGeneratorRequest{}) 275*1c12ee1eSDan Willemsen if err != nil { 276*1c12ee1eSDan Willemsen t.Fatal(err) 277*1c12ee1eSDan Willemsen } 278*1c12ee1eSDan Willemsen g := gen.NewGeneratedFile("foo.go", "golang.org/x/foo") 279*1c12ee1eSDan Willemsen g.P("package foo") 280*1c12ee1eSDan Willemsen g.P() 281*1c12ee1eSDan Willemsen for _, importPath := range []GoImportPath{ 282*1c12ee1eSDan Willemsen "golang.org/x/foo", 283*1c12ee1eSDan Willemsen // Multiple references to the same package. 284*1c12ee1eSDan Willemsen "golang.org/x/bar", 285*1c12ee1eSDan Willemsen "golang.org/x/bar", 286*1c12ee1eSDan Willemsen // Reference to a different package with the same basename. 287*1c12ee1eSDan Willemsen "golang.org/y/bar", 288*1c12ee1eSDan Willemsen "golang.org/x/baz", 289*1c12ee1eSDan Willemsen // Reference to a package conflicting with a predeclared identifier. 290*1c12ee1eSDan Willemsen "golang.org/z/string", 291*1c12ee1eSDan Willemsen } { 292*1c12ee1eSDan Willemsen g.P("var _ = ", GoIdent{GoName: "X", GoImportPath: importPath}, " // ", importPath) 293*1c12ee1eSDan Willemsen } 294*1c12ee1eSDan Willemsen want := `package foo 295*1c12ee1eSDan Willemsen 296*1c12ee1eSDan Willemsenimport ( 297*1c12ee1eSDan Willemsen bar "golang.org/x/bar" 298*1c12ee1eSDan Willemsen baz "golang.org/x/baz" 299*1c12ee1eSDan Willemsen bar1 "golang.org/y/bar" 300*1c12ee1eSDan Willemsen string1 "golang.org/z/string" 301*1c12ee1eSDan Willemsen) 302*1c12ee1eSDan Willemsen 303*1c12ee1eSDan Willemsenvar _ = X // "golang.org/x/foo" 304*1c12ee1eSDan Willemsenvar _ = bar.X // "golang.org/x/bar" 305*1c12ee1eSDan Willemsenvar _ = bar.X // "golang.org/x/bar" 306*1c12ee1eSDan Willemsenvar _ = bar1.X // "golang.org/y/bar" 307*1c12ee1eSDan Willemsenvar _ = baz.X // "golang.org/x/baz" 308*1c12ee1eSDan Willemsenvar _ = string1.X // "golang.org/z/string" 309*1c12ee1eSDan Willemsen` 310*1c12ee1eSDan Willemsen got, err := g.Content() 311*1c12ee1eSDan Willemsen if err != nil { 312*1c12ee1eSDan Willemsen t.Fatalf("g.Content() = %v", err) 313*1c12ee1eSDan Willemsen } 314*1c12ee1eSDan Willemsen if diff := cmp.Diff(string(want), string(got)); diff != "" { 315*1c12ee1eSDan Willemsen t.Fatalf("content mismatch (-want +got):\n%s", diff) 316*1c12ee1eSDan Willemsen } 317*1c12ee1eSDan Willemsen} 318*1c12ee1eSDan Willemsen 319*1c12ee1eSDan Willemsenfunc TestImportRewrites(t *testing.T) { 320*1c12ee1eSDan Willemsen gen, err := Options{ 321*1c12ee1eSDan Willemsen ImportRewriteFunc: func(i GoImportPath) GoImportPath { 322*1c12ee1eSDan Willemsen return "prefix/" + i 323*1c12ee1eSDan Willemsen }, 324*1c12ee1eSDan Willemsen }.New(&pluginpb.CodeGeneratorRequest{}) 325*1c12ee1eSDan Willemsen if err != nil { 326*1c12ee1eSDan Willemsen t.Fatal(err) 327*1c12ee1eSDan Willemsen } 328*1c12ee1eSDan Willemsen g := gen.NewGeneratedFile("foo.go", "golang.org/x/foo") 329*1c12ee1eSDan Willemsen g.P("package foo") 330*1c12ee1eSDan Willemsen g.P("var _ = ", GoIdent{GoName: "X", GoImportPath: "golang.org/x/bar"}) 331*1c12ee1eSDan Willemsen want := `package foo 332*1c12ee1eSDan Willemsen 333*1c12ee1eSDan Willemsenimport ( 334*1c12ee1eSDan Willemsen bar "prefix/golang.org/x/bar" 335*1c12ee1eSDan Willemsen) 336*1c12ee1eSDan Willemsen 337*1c12ee1eSDan Willemsenvar _ = bar.X 338*1c12ee1eSDan Willemsen` 339*1c12ee1eSDan Willemsen got, err := g.Content() 340*1c12ee1eSDan Willemsen if err != nil { 341*1c12ee1eSDan Willemsen t.Fatalf("g.Content() = %v", err) 342*1c12ee1eSDan Willemsen } 343*1c12ee1eSDan Willemsen if diff := cmp.Diff(string(want), string(got)); diff != "" { 344*1c12ee1eSDan Willemsen t.Fatalf("content mismatch (-want +got):\n%s", diff) 345*1c12ee1eSDan Willemsen } 346*1c12ee1eSDan Willemsen} 347