// Copyright 2015 The go-python 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 main import ( "bytes" "fmt" "log" "os" "os/exec" "path/filepath" "reflect" "sort" "strings" "testing" "github.com/go-python/gopy/bind" ) var ( testBackends = map[string]string{} features = map[string][]string{ "_examples/hi": []string{"py3"}, "_examples/gobytes": []string{"py3"}, "_examples/funcs": []string{"py3"}, "_examples/sliceptr": []string{"py3"}, "_examples/simple": []string{"py3"}, "_examples/empty": []string{"py3"}, "_examples/named": []string{"py3"}, "_examples/structs": []string{"py3"}, "_examples/consts": []string{"py3"}, "_examples/vars": []string{"py3"}, "_examples/seqs": []string{"py3"}, "_examples/cgo": []string{"py3"}, "_examples/pyerrors": []string{"py3"}, "_examples/iface": []string{"py3"}, "_examples/pointers": []string{"py3"}, "_examples/arrays": []string{"py3"}, "_examples/slices": []string{"py3"}, "_examples/maps": []string{"py3"}, "_examples/gostrings": []string{"py3"}, "_examples/rename": []string{"py3"}, "_examples/lot": []string{"py3"}, "_examples/unicode": []string{"py3"}, "_examples/osfile": []string{"py3"}, "_examples/gopygc": []string{"py3"}, "_examples/cstrings": []string{"py3"}, "_examples/pkgconflict": []string{"py3"}, "_examples/variadic": []string{"py3"}, } testEnvironment = os.Environ() ) func init() { os.Setenv("GOFLAGS", "-mod=mod") testEnvironment = append(testEnvironment, "GOFLAGS=-mod=mod") } func TestGovet(t *testing.T) { cmd := exec.Command("go", "vet", "./...") buf := new(bytes.Buffer) cmd.Stdout = buf cmd.Stderr = buf err := cmd.Run() if err != nil { t.Fatalf("error running %s:\n%s\n%v", "go vet", buf.String(), err) } } func TestGofmt(t *testing.T) { exe, err := exec.LookPath("goimports") if err != nil { switch e := err.(type) { case *exec.Error: if e.Err == exec.ErrNotFound { exe, err = exec.LookPath("gofmt") } } } if err != nil { t.Fatal(err) } cmd := exec.Command(exe, "-d", ".") buf := new(bytes.Buffer) cmd.Stdout = buf cmd.Stderr = buf err = cmd.Run() if err != nil { t.Fatalf("error running %s:\n%s\n%v", exe, buf.String(), err) } if len(buf.Bytes()) != 0 { t.Errorf("some files were not gofmt'ed:\n%s\n", buf.String()) } } func TestGoPyErrors(t *testing.T) { pyvm := testBackends["py3"] workdir, err := os.MkdirTemp("", "gopy-") if err != nil { t.Fatalf("could not create workdir: %v\n", err) } t.Logf("pyvm: %s making work dir: %s\n", pyvm, workdir) defer os.RemoveAll(workdir) curPkgPath := reflect.TypeOf(pkg{}).PkgPath() fpath := filepath.Join(curPkgPath, "_examples/gopyerrors") cmd := exec.Command("go", "run", ".", "gen", "-vm="+pyvm, "-output="+workdir, fpath) t.Logf("running: %v\n", cmd.Args) out, err := cmd.CombinedOutput() if err != nil { t.Fatalf("could not run %v: %+v\n", strings.Join(cmd.Args, " "), err) } contains := `--- Processing package: github.com/go-python/gopy/_examples/gopyerrors --- ignoring python incompatible function: .func github.com/go-python/gopy/_examples/gopyerrors.NotErrorMany() (int, int): func() (int, int): gopy: second result value must be of type error: func() (int, int) ignoring python incompatible method: gopyerrors.func (*github.com/go-python/gopy/_examples/gopyerrors.Struct).NotErrorMany() (int, string): func() (int, string): gopy: second result value must be of type error: func() (int, string) ignoring python incompatible method: gopyerrors.func (*github.com/go-python/gopy/_examples/gopyerrors.Struct).TooMany() (int, int, string): func() (int, int, string): gopy: too many results to return: func() (int, int, string) ignoring python incompatible function: .func github.com/go-python/gopy/_examples/gopyerrors.TooMany() (int, int, string): func() (int, int, string): gopy: too many results to return: func() (int, int, string) ` if got, want := string(out), contains; !strings.Contains(got, want) { t.Fatalf("%v does not contain\n%v\n", got, want) } } func TestHi(t *testing.T) { // t.Parallel() path := "_examples/hi" testPkg(t, pkg{ path: path, lang: features[path], cmd: "build", extras: nil, want: []byte(`hi from go hello you from go working... worked for 2 hours working... working... worked for 4 hours --- doc(hi)... package hi exposes a few Go functions to be wrapped and used from Python. --- hi.Universe: 42 --- hi.Version: 0.1 --- hi.Debug(): False --- hi.Set_Debug(true) --- hi.Debug(): True --- hi.Set_Debug(false) --- hi.Debug(): False --- hi.Anon(): hi.Person{Name="", Age=1} --- new anon: hi.Person{Name="you", Age=24} --- hi.Set_Anon(hi.NewPerson('you', 24))... --- hi.Anon(): hi.Person{Name="you", Age=24} --- doc(hi.Hi)... Hi() Hi prints hi from Go --- hi.Hi()... --- doc(hi.Hello)... Hello(str s) Hello prints a greeting from Go --- hi.Hello('you')... --- doc(hi.Add)... Add(int i, int j) int Add returns the sum of its arguments. --- hi.Add(1, 41)... 42 --- hi.Concat('4', '2')... 42 --- hi.LookupQuestion(42)... Life, the Universe and Everything --- hi.LookupQuestion(12)... caught: Wrong answer: 12 != 42 --- doc(hi.Person): Person is a simple struct --- p = hi.Person()... --- p: hi.Person{Name="", Age=0} --- p.Name: --- p.Age: 0 --- doc(hi.Greet): Greet() str Greet sends greetings --- p.Greet()... Hello, I am --- p.String()... hi.Person{Name="", Age=0} --- doc(p): Person is a simple struct --- p.Name = "foo"... --- p.Age = 42... --- p.String()... hi.Person{Name="foo", Age=42} --- p.Age: 42 --- p.Name: foo --- p.Work(2)... --- p.Work(24)... caught: can't work for 24 hours! --- p.Salary(2): 20 --- p.Salary(24): caught: can't work for 24 hours! --- Person.__init__ caught: argument 2 must be str, not int | err-type: caught: 'str' object cannot be interpreted as an integer | err-type: *ERROR* no exception raised! hi.Person{Name="name", Age=0} hi.Person{Name="name", Age=42} hi.Person{Name="name", Age=42} hi.Person{Name="name", Age=42} --- hi.NewPerson('me', 666): hi.Person{Name="me", Age=666} --- hi.NewPersonWithAge(666): hi.Person{Name="stranger", Age=666} --- hi.NewActivePerson(4): hi.Person{Name="", Age=0} --- c = hi.Couple()... hi.Couple{P1=hi.Person{Name="", Age=0}, P2=hi.Person{Name="", Age=0}} --- c.P1: hi.Person{Name="", Age=0} --- c: hi.Couple{P1=hi.Person{Name="tom", Age=5}, P2=hi.Person{Name="bob", Age=2}} --- c = hi.NewCouple(tom, bob)... hi.Couple{P1=hi.Person{Name="tom", Age=50}, P2=hi.Person{Name="bob", Age=41}} hi.Couple{P1=hi.Person{Name="mom", Age=50}, P2=hi.Person{Name="bob", Age=51}} --- Couple.__init__ hi.Couple{P1=hi.Person{Name="p1", Age=42}, P2=hi.Person{Name="p2", Age=52}} hi.Couple{P1=hi.Person{Name="p1", Age=42}, P2=hi.Person{Name="p2", Age=52}} hi.Couple{P1=hi.Person{Name="p2", Age=52}, P2=hi.Person{Name="p1", Age=42}} caught: supplied argument type is not a go.GoClass | err-type: caught: supplied argument type is not a go.GoClass | err-type: caught: supplied argument type is not a go.GoClass | err-type: --- testing GC... --- len(objs): 100000 --- len(vs): 100000 --- testing GC... [ok] --- testing array... arr: hi.Array_2_int len: 2 handle: 300036 [1, 2] len(arr): 2 arr[0]: 1 arr[1]: 2 arr[2]: caught: slice index out of range arr: hi.Array_2_int len: 2 handle: 300036 [1, 2] len(arr): 2 mem(arr): caught: memoryview: a bytes-like object is required, not 'Array_2_int' --- testing slice... slice: go.Slice_int len: 2 handle: 300037 [1, 2] len(slice): 2 slice[0]: 1 slice[1]: 2 slice[2]: caught: slice index out of range slice: go.Slice_int len: 2 handle: 300037 [1, 42] slice repr: go.Slice_int([1, 42]) len(slice): 2 mem(slice): caught: memoryview: a bytes-like object is required, not 'Slice_int' OK `), }) } func TestBytes(t *testing.T) { // t.Parallel() path := "_examples/gobytes" testPkg(t, pkg{ path: path, lang: features[path], cmd: "build", extras: nil, want: []byte(`Python bytes: b'\x00\x01\x02\x03' Go slice: go.Slice_byte len: 10 handle: 1 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] gobytes.HashBytes from Go bytes: gobytes.Array_4_byte len: 4 handle: 2 [12, 13, 81, 81] Python bytes to Go: go.Slice_byte len: 4 handle: 3 [0, 1, 2, 3] Go bytes to Python: b'\x03\x04\x05' OK `), }) } func TestBindFuncs(t *testing.T) { // t.Parallel() path := "_examples/funcs" testPkg(t, pkg{ path: path, lang: features[path], cmd: "build", extras: nil, want: []byte(`got return value: true got nil ofs FieldI: 42 FieldS: str field fs.CallBack(22, cbfun)... in python cbfun: FieldI: 42 FieldS: str field ival: 22 sval: str field fs.CallBackIf(22, cbfunif)... in python cbfunif: FieldI: 42 FieldS: str field ival: 22 ifval: str field fs.CallBackRval(22, cbfunrval)... in python cbfunrval: FieldI: 42 FieldS: str field ival: 22 ifval: str field fs.CallBack(32, cls.ClassFun)... in python class fun: FieldI: 42 FieldS: str field ival: 32 sval: str field cls.CallSelf... in python class fun: FieldI: 42 FieldS: str field ival: 77 sval: str field fs.ObjArg with nil fs.ObjArg with fs OK `), }) } func TestBindSimple(t *testing.T) { // t.Parallel() path := "_examples/simple" testPkg(t, pkg{ path: path, lang: features[path], cmd: "build", extras: nil, want: []byte(`doc(pkg): '\nsimple is a simple package.\n\n' pkg.Func()... fct = pkg.Func... fct()... pkg.Add(1,2)= 3 pkg.Bool(True)= True pkg.Bool(False)= False pkg.Comp64Add((3+4j), (2+5j)) = (5+9j) pkg.Comp128Add((3+4j), (2+5j)) = (5+9j) OK `), }) } func TestBindEmpty(t *testing.T) { // t.Parallel() path := "_examples/empty" testPkg(t, pkg{ path: path, lang: features[path], cmd: "build", extras: nil, want: []byte(`doc(pkg): '\nPackage empty does not expose anything.\nWe may want to wrap and import it just for its side-effects.\n\n' OK `), }) } func TestBindPointers(t *testing.T) { // t.Parallel() path := "_examples/pointers" testPkg(t, pkg{ path: path, lang: features[path], cmd: "build", extras: nil, want: []byte(`s = pointers.S(2) s = pointers.S{Value=2, handle=1} s.Value = 2 OK `), }) } func TestBindNamed(t *testing.T) { // t.Parallel() path := "_examples/named" testPkg(t, pkg{ path: path, lang: features[path], cmd: "build", extras: nil, want: []byte(`doc(named): '\npackage named tests various aspects of named types.\n\n' s = named.Slice() s = named.named_Slice len: 0 handle: 1 [] s = named.Slice([1,2]) s = named.named_Slice len: 2 handle: 2 [1.0, 2.0] s = named.Slice(range(10)) s = named.named_Slice len: 10 handle: 3 [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0] s = named.Slice(xrange(10)) s = named.named_Slice len: 10 handle: 4 [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0] OK `), }) } func TestBindStructs(t *testing.T) { // t.Parallel() path := "_examples/structs" testPkg(t, pkg{ path: path, lang: features[path], cmd: "build", extras: nil, want: []byte(`s = structs.S() s = structs.S{handle=1} s.Init() s.Upper('boo')= 'BOO' s1 = structs.S1() s1 = structs.S1{handle=2} caught error: 'S1' object has no attribute 'private' s2 = structs.S2() s2 = structs.S2{Public=1, handle=5} s2 = structs.S2{Public=42, handle=7} s2.Public = 42 caught error: 'S2' object has no attribute 'private' s2child = S2Child{S2: structs.S2{Public=42, handle=8, local=123}, local: 123} s2child.Public = 42 s2child.local = 123 caught error: 'S2Child' object has no attribute 'private' s3.X,Y = 3,4 OK `), }) } func TestBindConsts(t *testing.T) { // t.Parallel() path := "_examples/consts" // NOTE: python2 does not properly output .666 decimals for unknown reasons. testPkg(t, pkg{ path: path, lang: features[path], cmd: "build", extras: nil, want: []byte(`c1 = c1 c2 = 42 c3 = 666.666 c4 = c4 c5 = 42 c6 = 42 c7 = 666.666 k1 = 1 k2 = 2 OK `), }) } func TestBindVars(t *testing.T) { // t.Parallel() path := "_examples/vars" testPkg(t, pkg{ path: path, lang: features[path], cmd: "build", extras: nil, want: []byte(`doc(vars): None doc(vars.V1()): '\n\tV1 Gets Go Variable: vars.V1\n\t\n\t' doc(vars.Set_V1()): '\n\tSet_V1 Sets Go Variable: vars.V1\n\t\n\t' Initial values v1 = v1 v2 = 42 v3 = 666.666 v4 = c4 v5 = 42 v6 = 42 v7 = 666.666 k1 = 1 k2 = 2 New values v1 = test1 v2 = 90 v3 = 1111.1111 v4 = test2 v5 = 50 v6 = 50 v7 = 1111.1111 k1 = 123 k2 = 456 vars.Doc() = 'A variable with some documentation' doc of vars.Doc = '\n\tDoc Gets Go Variable: vars.Doc\n\tDoc is a top-level string with some documentation attached.\n\t\n\t' doc of vars.Set_Doc = '\n\tSet_Doc Sets Go Variable: vars.Doc\n\tDoc is a top-level string with some documentation attached.\n\t\n\t' OK `), }) } func TestBindSeqs(t *testing.T) { // t.Parallel() path := "_examples/seqs" testPkg(t, pkg{ path: path, lang: features[path], cmd: "build", extras: nil, want: []byte(`doc(seqs): '\npackage seqs tests various aspects of sequence types.\n\n' s = seqs.Slice() s = seqs.seqs_Slice len: 0 handle: 1 [] s = seqs.Slice([1,2]) s = seqs.seqs_Slice len: 2 handle: 2 [1.0, 2.0] s = seqs.Slice(range(10)) s = seqs.seqs_Slice len: 10 handle: 3 [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0] s = seqs.Slice(xrange(10)) s = seqs.seqs_Slice len: 10 handle: 4 [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0] s = seqs.Slice() s = seqs.seqs_Slice len: 0 handle: 5 [] s += [1,2] s = seqs.seqs_Slice len: 2 handle: 5 [1.0, 2.0] s += [10,20] s = seqs.seqs_Slice len: 4 handle: 5 [1.0, 2.0, 10.0, 20.0] OK `), }) } func TestBindInterfaces(t *testing.T) { // t.Parallel() path := "_examples/iface" // NOTE: python2 outputs this in a different order, so test fails, but results are the same testPkg(t, pkg{ path: path, lang: features[path], cmd: "build", extras: nil, want: []byte(`t.F [CALLED] iface.CallIface... t.F [CALLED] iface.CallIface... [DONE] iface as string: test string iface as string: 42 iface as handle: &{0 } iface as handle: doc(iface): '\npackage iface tests various aspects of interfaces.\n\n' t = iface.T() t.F() iface.CallIface(t) iface.IfaceString("test string" iface.IfaceString(str(42)) iface.IfaceHandle(t) iface.IfaceHandle(go.nil) OK `), }) } func TestBindCgoPackage(t *testing.T) { // t.Parallel() path := "_examples/cgo" testPkg(t, pkg{ path: path, lang: features[path], cmd: "build", extras: nil, want: []byte(`cgo.doc: '\nPackage cgo tests bindings of CGo-based packages.\n\n' cgo.Hi()= 'hi from go\n' cgo.Hello(you)= 'hello you from go\n' OK `), }) } func TestPyErrors(t *testing.T) { // t.Parallel() path := "_examples/pyerrors" testPkg(t, pkg{ path: path, lang: features[path], cmd: "build", extras: nil, want: []byte(`Divide by zero. pyerrors.Div(5, 2) = 2 Empty string value. pyerrors.NewMyString("hello") = "hello" OK `), }) } func TestBuiltinArrays(t *testing.T) { // t.Parallel() path := "_examples/arrays" testPkg(t, pkg{ path: path, lang: features[path], cmd: "build", extras: nil, want: []byte(`Python list: [1, 2, 3, 4] Go array: arrays.Array_4_int len: 4 handle: 1 [1, 2, 3, 4] arrays.IntSum from Go array: 10 OK `), }) } func TestBuiltinSlices(t *testing.T) { // t.Parallel() path := "_examples/slices" testPkg(t, pkg{ path: path, lang: features[path], cmd: "build", extras: nil, want: []byte(`0: S0 1: S1 2: S2 S0 S1 Python list: [1, 2, 3, 4] Go slice: go.Slice_int len: 4 handle: 1 [1, 2, 3, 4] slices.IntSum from Python list: 10 slices.IntSum from Go slice: 10 unsigned slice elements: 1 2 3 4 signed slice elements: -1 -2 -3 -4 struct slice: slices.Slice_Ptr_slices_S len: 3 handle: 11 [slices.S{Name=S0, handle=12}, slices.S{Name=S1, handle=13}, slices.S{Name=S2, handle=14}] struct slice[0]: slices.S{Name=S0, handle=15} struct slice[1]: slices.S{Name=S1, handle=16} struct slice[2].Name: S2 [][]bool working as expected OK `), }) } func TestBuiltinMaps(t *testing.T) { // t.Parallel() path := "_examples/maps" testPkg(t, pkg{ path: path, lang: features[path], cmd: "build", extras: nil, want: []byte(`map a[1]: 3.0 map a[2]: 5.0 2 in map: True 3 in map: False maps.Sum from Go map: 8.0 map b: {1: 3.0, 2: 5.0} maps.Sum from Python dictionary: 8.0 maps.Keys from Go map: go.Slice_int len: 2 handle: 3 [1, 2] maps.Values from Go map: go.Slice_float64 len: 2 handle: 4 [3.0, 5.0] maps.Keys from Python dictionary: go.Slice_int len: 2 handle: 6 [1, 2] maps.Values from Python dictionary: go.Slice_float64 len: 2 handle: 8 [3.0, 5.0] deleted 1 from a: maps.Map_int_float64 len: 1 handle: 1 {2=5.0, } OK `), }) } func TestBindStrings(t *testing.T) { // t.Parallel() path := "_examples/gostrings" testPkg(t, pkg{ path: path, lang: features[path], cmd: "build", extras: nil, want: []byte(`S1 = S1 GetString() = MyString OK `), }) } func TestBindRename(t *testing.T) { // t.Parallel() path := "_examples/rename" testPkg(t, pkg{ path: path, lang: features[path], cmd: "build", extras: []string{"-rename"}, want: []byte(`say_hi_fn(): hi MyStruct().say_something(): something MyStruct.auto_renamed_property.__doc__: I should be renamed to auto_renamed_property when generated with -rename flag MyStruct.custom_name.__doc__: I should be renamed to custom_name with the custom option OK `), }) } func TestLot(t *testing.T) { // t.Parallel() path := "_examples/lot" testPkg(t, pkg{ path: path, lang: features[path], cmd: "build", extras: nil, want: []byte(`l.SomeString : some string l.SomeInt : 1337 l.SomeFloat : 1337.1337 l.SomeBool : True l.SomeListOfStrings: go.Slice_string len: 4 handle: 2 [some, list, of, strings] l.SomeListOfInts: go.Slice_int64 len: 4 handle: 3 [6, 2, 9, 1] l.SomeListOfFloats: go.Slice_float64 len: 4 handle: 4 [6.6, 2.2, 9.9, 1.1] l.SomeListOfBools: go.Slice_bool len: 4 handle: 5 [True, False, True, False] OK `), }) } func TestSlicePtr(t *testing.T) { // t.Parallel() path := "_examples/sliceptr" testPkg(t, pkg{ path: path, lang: features[path], cmd: "build", extras: nil, want: []byte(`sliceptr.sliceptr_IntVector len: 3 handle: 1 [1, 2, 3] sliceptr.sliceptr_IntVector len: 4 handle: 1 [1, 2, 3, 4] sliceptr.sliceptr_StrVector len: 4 handle: 2 [1, 2, 3, 4] OK `), }) } func TestUnicode(t *testing.T) { // t.Parallel() path := "_examples/unicode" testPkg(t, pkg{ path: path, lang: features[path], cmd: "build", extras: nil, want: []byte(`encoding.HandleString(unicodestr) -> Python Unicode string 🐱 encoding.GetString() -> Go Unicode string 🐱 OK `), }) } func TestPYGC(t *testing.T) { // t.Parallel() path := "_examples/gopygc" testPkg(t, pkg{ path: path, lang: features[path], cmd: "build", extras: nil, want: []byte(`0 3 0 3 5 6 7 8 5 3 2 1 0 1 0 1 1 1 0 OK `), }) } func TestCStrings(t *testing.T) { // t.Parallel() path := "_examples/cstrings" testPkg(t, pkg{ path: path, lang: features[path], cmd: "build", extras: nil, // todo: this test on mac leaks everything except String want: []byte(`gofnString leaked: False gofnStruct leaked: False gofnNestedStruct leaked: False gofnSlice leaked: False gofnMap leaked: False OK `), }) } func TestPackagePrefix(t *testing.T) { // t.Parallel() path := "_examples/package/mypkg" testPkg(t, pkg{ path: path, lang: features[path], cmd: "build", outputdir: "mypkg", pkgprefix: ".", testdir: ".", want: []byte(`mypkg.mypkg.SayHello()... Hello OK `), }) } func TestPkgConflict(t *testing.T) { // t.Parallel() path := "_examples/pkgconflict" testPkg(t, pkg{ path: path, lang: features[path], cmd: "build", extras: nil, want: []byte(`17 items are made of wool 42 items are made of hyperlinks 2 items are made of fuzz `), }) } // see notes in _examples/osfile/test.py for why this doesn't work.. // leaving here for now in case someone wants to follow-up and make it work.. // // func TestOsFile(t *testing.T) { // // t.Parallel() // path := "_examples/osfile" // testPkg(t, pkg{ // path: path, // lang: features[path], // cmd: "pkg", // extras: []string{"os"}, // want: []byte(`yeah.. // OK // `), // }) // } func TestBindVariadic(t *testing.T) { // t.Parallel() path := "_examples/variadic" testPkg(t, pkg{ path: path, lang: features[path], cmd: "build", extras: nil, want: []byte(`NonVariadic 1+[2+3+4]+5 = 15 Variadic 1+2+3+4+5 = 15 Variadic Struct s(1)+s(2)+s(3) = 6 Variadic InterFace i(1)+i(2)+i(3) = 6 Type OK `), }) } // Generate / verify SUPPORT_MATRIX.md from features map. func TestCheckSupportMatrix(t *testing.T) { var buf bytes.Buffer buf.WriteString("# Support matrix\n") buf.WriteString(` NOTE: File auto-generated by TestCheckSupportMatrix in main_test.go. Please don't modify manually. `) // Generate // - sorted list of features // - sorted list of backends // - a map of feature to available backends var featuresSorted []string var allBackendsSorted []string featureToBackendMap := make(map[string]map[string]bool) allBackends := make(map[string]bool) for feature, backends := range features { featuresSorted = append(featuresSorted, feature) featureToBackendMap[feature] = make(map[string]bool) for _, backend := range backends { featureToBackendMap[feature][backend] = true allBackends[backend] = true } } for backend, _ := range allBackends { allBackendsSorted = append(allBackendsSorted, backend) } sort.Strings(featuresSorted) sort.Strings(allBackendsSorted) // Write the table header and the line separating header and rows. fmt.Fprintf(&buf, "Feature |%s\n", strings.Join(allBackendsSorted, " | ")) var tableDelimiters []string for i := 0; i <= len(allBackendsSorted); i++ { tableDelimiters = append(tableDelimiters, "---") } buf.WriteString(strings.Join(tableDelimiters, " | ")) buf.WriteString("\n") // Write the actual rows of the support matrix. for _, feature := range featuresSorted { var cells []string cells = append(cells, feature) for _, backend := range allBackendsSorted { if featureToBackendMap[feature][backend] { cells = append(cells, "yes") } else { cells = append(cells, "no") } } buf.WriteString(strings.Join(cells, " | ")) buf.WriteString("\n") } if os.Getenv("GOPY_GENERATE_SUPPORT_MATRIX") == "1" { err := os.WriteFile("SUPPORT_MATRIX.md", buf.Bytes(), 0644) if err != nil { log.Fatalf("Unable to write SUPPORT_MATRIX.md") } return } src, err := os.ReadFile("SUPPORT_MATRIX.md") if err != nil { log.Fatalf("Unable to read SUPPORT_MATRIX.md") } msg := ` This is a test case to verify the support matrix. This test is likely failing because the map features has been updated and the auto-generated file SUPPORT_MATRIX.md hasn't been updated. Please run 'go test' with environment variable GOPY_GENERATE_SUPPORT_MATRIX=1 to regenerate SUPPORT_MATRIX.md and commit the changes to SUPPORT_MATRIX.md onto git. ` if bytes.Compare(buf.Bytes(), src) != 0 { t.Fatalf(msg) } } type pkg struct { path string lang []string cmd string outputdir string pkgprefix string testdir string extras []string want []byte } func testPkg(t *testing.T, table pkg) { backends := []string{"py3"} // backends := table.lang // todo: enabling py2 testing requires separate "want" output for _, be := range backends { fmt.Printf("looping over backends: %s in %s\n", be, backends) vm, ok := testBackends[be] if !ok || vm == "" { // backend not available. t.Logf("Skipped testing backend %s for %s\n", be, table.path) continue } switch be { case "py3": t.Run(be, func(t *testing.T) { // t.Parallel() testPkgBackend(t, vm, table) }) default: t.Errorf("invalid backend name %q", be) } } } func writeGoMod(t *testing.T, pkgDir, tstDir string) { template := ` module dummy require github.com/go-python/gopy v0.0.0 replace github.com/go-python/gopy => %s ` contents := fmt.Sprintf(template, pkgDir) if err := os.WriteFile(filepath.Join(tstDir, "go.mod"), []byte(contents), 0666); err != nil { t.Fatalf("failed to write go.mod file: %v", err) } } func testPkgBackend(t *testing.T, pyvm string, table pkg) { curPkgPath := reflect.TypeOf(table).PkgPath() _, pkgNm := filepath.Split(table.path) cwd, _ := os.Getwd() workdir, err := os.MkdirTemp("", "gopy-") if err != nil { t.Fatalf("[%s:%s]: could not create workdir: %v\n", pyvm, table.path, err) } genPkgDir := workdir if table.outputdir != "" { genPkgDir = filepath.Join(workdir, table.outputdir) fmt.Printf("pyvm: %s making temp output dir: %s\n", pyvm, genPkgDir) err = os.MkdirAll(genPkgDir, 0700) if err != nil { t.Fatalf("[%s:%s]: could not create temp output dir: %v\n", pyvm, table.path, err) } } defer os.RemoveAll(workdir) defer bind.ResetPackages() env := make([]string, len(testEnvironment)) copy(env, testEnvironment) env = append(env, fmt.Sprintf("PYTHONPATH=%s", workdir)) writeGoMod(t, cwd, genPkgDir) // fmt.Printf("building in work dir: %s\n", workdir) fpath := "./" + table.path if table.cmd != "build" { // non-build cases end up inside the working dir -- need a global import path fpath = filepath.Join(curPkgPath, table.path) } args := []string{table.cmd, "-vm=" + pyvm, "-output=" + genPkgDir, "-package-prefix", table.pkgprefix} if table.extras != nil { args = append(args, table.extras...) } args = append(args, fpath) fmt.Printf("run cmd: %s\n", args) err = run(args) if err != nil { t.Fatalf("[%s:%s]: error running gopy-build: %v\n", pyvm, table.path, err) } // fmt.Printf("copying test.py\n") tstDir := genPkgDir if table.cmd != "build" { tstDir = filepath.Join(workdir, pkgNm) } if table.testdir != "" { tstDir = filepath.Join(workdir, table.testdir) } tstSrc := filepath.Join(filepath.Join(cwd, table.path), "test.py") tstDst := filepath.Join(tstDir, "test.py") err = copyCmd(tstSrc, tstDst) if err != nil { t.Fatalf("[%s:%s]: error copying 'test.py' from: %s to: %s: %v\n", pyvm, table.path, tstSrc, tstDst, err) } fmt.Printf("running %s test.py\n", pyvm) cmd := exec.Command(pyvm, "./test.py") cmd.Env = env cmd.Dir = tstDir cmd.Stdin = os.Stdin buf, err := cmd.CombinedOutput() if err != nil { t.Fatalf( "[%s:%s]: error running python module: err=%v\n%v\n", pyvm, table.path, err, string(buf), ) } var ( got = strings.Replace(string(buf), "\r\n", "\n", -1) want = strings.Replace(string(table.want), "\r\n", "\n", -1) ) if !reflect.DeepEqual(got, want) { diffTxt := "" diffBin, diffErr := exec.LookPath("diff") if diffErr == nil { wantFile, wantErr := os.Create(filepath.Join(workdir, "want.txt")) if wantErr == nil { wantFile.Write([]byte(want)) wantFile.Close() } gotFile, gotErr := os.Create(filepath.Join(workdir, "got.txt")) if gotErr == nil { gotFile.Write([]byte(got)) gotFile.Close() } if gotErr == nil && wantErr == nil { cmd = exec.Command(diffBin, "-urN", wantFile.Name(), gotFile.Name(), ) diff, _ := cmd.CombinedOutput() diffTxt = string(diff) + "\n" } t.Fatalf("[%s:%s]: error running python module:\n%s", pyvm, table.path, diffTxt, ) } t.Fatalf("[%s:%s]: error running python module:\ngot:\n%s\n\nwant:\n%s\n[%s:%s] diff:\n%s", pyvm, table.path, got, want, pyvm, table.path, diffTxt, ) } }