From 6cee95b610c3e7f219cbc4003d732b1adae0d21a Mon Sep 17 00:00:00 2001 From: abhishrestha Date: Sun, 31 May 2026 21:03:18 +0530 Subject: [PATCH 1/2] cpython: replaced object.go files with 5 files --- cpython/object_attrs_test.go | 455 ++++++++++++++++++++++++++++++ cpython/object_call_test.go | 129 +++++++++ cpython/object_conversion_test.go | 353 +++++++++++++++++++++++ cpython/object_errors_test.go | 344 ++++++++++++++++++++++ cpython/object_lifecycle_test.go | 161 +++++++++++ 5 files changed, 1442 insertions(+) create mode 100644 cpython/object_attrs_test.go create mode 100644 cpython/object_call_test.go create mode 100644 cpython/object_conversion_test.go create mode 100644 cpython/object_errors_test.go create mode 100644 cpython/object_lifecycle_test.go diff --git a/cpython/object_attrs_test.go b/cpython/object_attrs_test.go new file mode 100644 index 0000000..68ea79e --- /dev/null +++ b/cpython/object_attrs_test.go @@ -0,0 +1,455 @@ +// MFP - Multi-Function Printers and scanners toolkit +// CPython binding. +// +// Copyright (C) 2024 and up by Alexander Pevzner (pzz@apevzner.com) +// See LICENSE for license terms and conditions +// +// Tests for attribute access (Get, Set, Del, HasAttr) and container +// operations (GetItem, SetItem, Del, ContainsItem, Len, Slice, Keys). + +package cpython + +import ( + "fmt" + "testing" + + "github.com/OpenPrinting/go-mfp/internal/assert" + "github.com/OpenPrinting/go-mfp/internal/testutils" +) + +// TestObjectAttributes tests Get, Set, DelAttr, HasAttr operations. +func TestObjectAttributes(t *testing.T) { + script := ` +class Dog: + species = "Canis familiaris" + + def __init__(self, name, age): + self.name = name + self.age = age + +dog = Dog("Archi", 4) +` + py, err := NewPython() + assert.NoError(err) + defer py.Close() + + err = py.Exec(script, "") + assert.NoError(err) + + obj := py.Eval("dog") + assert.NoError(obj.Err()) + + found, err := obj.HasAttr("name") + if err != nil { + t.Errorf("Unexpected error: %s", err) + } + if !found { + t.Errorf("Attribute %q expected but not found", "name") + } + + found, err = obj.HasAttr("unknown") + if err != nil { + t.Errorf("Unexpected error: %s", err) + } + if found { + t.Errorf("Attribute %q not expected but found", "unknown") + } + + attr := obj.Get("name") + if err := attr.Err(); err != nil { + t.Errorf("Unexpected error: %s", err) + } else { + s, err := attr.Unicode() + assert.NoError(err) + if s != "Archi" { + t.Errorf("obj.Get mismatch:\nexpected: %s\npresent: %s\n", "Archi", s) + } + } + + deleted, err := obj.DelAttr("name") + if err != nil { + t.Errorf("Unexpected error: %s", err) + } + if !deleted { + t.Errorf("obj.DelAttr: attribute %s not deleted", "name") + } + + found, err = obj.HasAttr("name") + if err != nil { + t.Errorf("Unexpected error: %s", err) + } + if found { + t.Errorf("Deleted attribute %q still present", "name") + } +} + +// TestObjectGetHasAttrError tests Get() when hasattr itself returns an error. +// A class with a raising __getattribute__ causes hasattr to propagate the +// error rather than returning (false, nil), covering the found=false, err!=nil +// branch in Get(). +func TestObjectGetHasAttrError(t *testing.T) { + py, err := NewPython() + assert.NoError(err) + defer py.Close() + + script := ` +class Exploding: + def __getattribute__(self, name): + raise RuntimeError("no attrs for you") + +exploding = Exploding() +` + err = py.Exec(script, "") + if err != nil { + t.Fatalf("setup failed: %s", err) + } + + obj := py.Eval("exploding") + if err := obj.Err(); err != nil { + t.Fatalf("Eval exploding: %s", err) + } + + result := obj.Get("anything") + if result.Err() == nil { + t.Error("Get on Exploding object: expected error, got nil") + } +} + +// TestObjectItems tests GetItem, SetItem, ContainsItem, Del on a dict. +func TestObjectItems(t *testing.T) { + py, err := NewPython() + assert.NoError(err) + defer py.Close() + + obj := py.Eval(`{1:"1", 2:"2", 3:"3"}`) + assert.NoError(obj.Err()) + + // Items 1-3 must exist with correct values. + for i := 1; i <= 3; i++ { + found, err := obj.ContainsItem(i) + if err != nil { + t.Errorf("Object.ContainsItem(%v): %s", i, err) + return + } + if !found { + t.Errorf("Object.ContainsItem(%v): item not found", i) + } + + item := obj.GetItem(i) + if err := item.Err(); err != nil { + t.Errorf("Object.GetItem(%v): %s", i, err) + return + } + + s, err := item.Str() + assert.NoError(err) + if expected := fmt.Sprintf("%d", i); s != expected { + t.Errorf("Object.GetItem(%v):\nexpected: %s\npresent: %s\n", i, expected, s) + } + } + + // Items 4-6 must not exist; GetItem must return ErrNotFound. + for i := 4; i <= 6; i++ { + found, err := obj.ContainsItem(i) + if err != nil { + t.Errorf("Object.ContainsItem(%v): %s", i, err) + return + } + if found { + t.Errorf("Object.ContainsItem(%v): item found unexpectedly", i) + } + + item := obj.GetItem(i) + if err := item.Err(); err != (ErrNotFound{}) { + t.Errorf("Object.GetItem(%v):\nexpected: (%s)\npresent: (%s)\n", + i, ErrNotFound{}, err) + } + } + + // Add items 4-6 and verify all 6 are now present. + for i := 4; i <= 6; i++ { + if err := obj.SetItem(i, fmt.Sprintf("%d", i)); err != nil { + t.Errorf("Object.SetItem(%v): %s", i, err) + return + } + } + for i := 1; i <= 6; i++ { + item := obj.GetItem(i) + if err := item.Err(); err != nil { + t.Errorf("Object.GetItem(%v): %s", i, err) + return + } + s, err := item.Str() + assert.NoError(err) + if expected := fmt.Sprintf("%d", i); s != expected { + t.Errorf("Object.GetItem(%v):\nexpected: %s\npresent: %s\n", i, expected, s) + } + } + + // Delete items 1-3; second delete must report not-found. + for i := 1; i <= 3; i++ { + found, err := obj.Del(i) + if err != nil { + t.Errorf("Object.Del(%v): %s", i, err) + return + } + if !found { + t.Errorf("Object.Del(%v): item not found on first delete", i) + } + + found, err = obj.Del(i) + if err != nil { + t.Errorf("Object.Del(%v): %s", i, err) + return + } + if found { + t.Errorf("Object.Del(%v): item still found after delete", i) + } + } + + // Items 1-3 must now be absent. + for i := 1; i <= 3; i++ { + found, err := obj.ContainsItem(i) + if err != nil { + t.Errorf("Object.ContainsItem(%v): %s", i, err) + return + } + if found { + t.Errorf("Object.ContainsItem(%v): deleted item still present", i) + } + } +} + +// TestObjectLen tests Object.Len on various container and scalar types. +func TestObjectLen(t *testing.T) { + type testData struct { + expr string + l int + err bool + } + + tests := []testData{ + {expr: `[1,2,3]`, l: 3}, + {expr: `(1,2,3)`, l: 3}, + {expr: `{1:"1", 2:"2", 3:"3"}`, l: 3}, + {expr: `[]`, l: 0}, + {expr: `()`, l: 0}, + {expr: `{}`, l: 0}, + {expr: `5`, l: 0, err: true}, + {expr: `"hello"`, l: 5}, + {expr: `"привет"`, l: 6}, + } + + py, err := NewPython() + assert.NoError(err) + defer py.Close() + + for _, test := range tests { + obj := py.Eval(test.expr) + assert.NoError(obj.Err()) + + l, err := obj.Len() + switch { + case err == nil && test.err: + t.Errorf("Object.Len(%s): error not occurred", test.expr) + case err != nil && !test.err: + t.Errorf("Object.Len(%s): %s", test.expr, err) + case l != test.l: + t.Errorf("Object.Len(%s):\nexpected: %d\npresent: %d\n", + test.expr, test.l, l) + } + } +} + +// TestObjectSlice tests Object.Slice on lists, tuples, and a non-sequence. +func TestObjectSlice(t *testing.T) { + type testData struct { + expr string + expected []string + mustfail bool + } + + tests := []testData{ + {expr: "()", expected: []string{}}, + {expr: "(1,2,3)", expected: []string{"1", "2", "3"}}, + {expr: "[]", expected: []string{}}, + {expr: "[1,2,3]", expected: []string{"1", "2", "3"}}, + {expr: "5", mustfail: true}, + } + + py, err := NewPython() + assert.NoError(err) + defer py.Close() + + for _, test := range tests { + obj := py.Eval(test.expr) + assert.NoError(obj.Err()) + + slice, err := obj.Slice() + if err != nil { + if !test.mustfail { + t.Errorf("%s: Object.Slice: %s", test.expr, err) + } + continue + } + if test.mustfail { + t.Errorf("%s: Object.Slice: expected error didn't occur", test.expr) + continue + } + + result := make([]string, len(slice)) + for i := range slice { + result[i], err = slice[i].Str() + assert.NoError(err) + } + if diff := testutils.Diff(test.expected, result); diff != "" { + t.Errorf("%s: Object.Slice:\n%s", test.expr, diff) + } + } +} + +// TestObjectKeys tests Object.Keys on dicts and non-mapping types. +func TestObjectKeys(t *testing.T) { + type testData struct { + expr string + expected []string + mustfail bool + } + + tests := []testData{ + {expr: `{1:"one",2:"two",3:"three"}`, expected: []string{"1", "2", "3"}}, + {expr: `{}`, expected: []string{}}, + {expr: "()", mustfail: true}, + {expr: "5", mustfail: true}, + } + + py, err := NewPython() + assert.NoError(err) + defer py.Close() + + for _, test := range tests { + obj := py.Eval(test.expr) + assert.NoError(obj.Err()) + + slice, err := obj.Keys() + if err != nil { + if !test.mustfail { + t.Errorf("%s: Object.Keys: %s", test.expr, err) + } + continue + } + if test.mustfail { + t.Errorf("%s: Object.Keys: expected error didn't occur", test.expr) + continue + } + + result := make([]string, len(slice)) + for i := range slice { + result[i], err = slice[i].Str() + assert.NoError(err) + } + if diff := testutils.Diff(test.expected, result); diff != "" { + t.Errorf("%s: Object.Keys:\n%s", test.expr, diff) + } + } +} + +// TestObjSliceNonSequence tests objSlice rejects a non-sequence object. +func TestObjSliceNonSequence(t *testing.T) { + py, err := NewPython() + assert.NoError(err) + defer py.Close() + + obj := py.NewObject(42) + assert.NoError(obj.Err()) + + _, err = obj.Slice() + if err == nil { + t.Error("Slice: expected error for non-sequence object") + } +} + +// TestObjSliceLengthError covers the gate.length failure path in objSlice. +// A list subclass whose __len__ raises passes isSeq() but fails at +// gate.length(), hitting the "return nil, err" branch. +func TestObjSliceLengthError(t *testing.T) { + py, err := NewPython() + assert.NoError(err) + defer py.Close() + + script := ` +class BadLen(list): + def __len__(self): + raise RuntimeError("len exploded") + +badlen = BadLen([1, 2, 3]) +` + err = py.Exec(script, "") + if err != nil { + t.Fatalf("setup failed: %s", err) + } + + obj := py.Eval("badlen") + if err := obj.Err(); err != nil { + t.Fatalf("Eval badlen: %s", err) + } + + _, err = obj.Slice() + if err == nil { + t.Error("Slice on BadLen: expected error from __len__, got nil") + } +} + +// TestObjSliceGetSeqItemError tests the mid-loop cleanup path in objSlice. +// A list subclass that raises on index >= 1 causes objSlice to fetch item[0] +// successfully, then fail, triggering the unref-cleanup loop. +func TestObjSliceGetSeqItemError(t *testing.T) { + py, err := NewPython() + assert.NoError(err) + defer py.Close() + + script := ` +class BrokenList(list): + def __getitem__(self, idx): + if idx >= 1: + raise IndexError("boom at index 1") + return super().__getitem__(idx) + +broken = BrokenList([10, 20, 30]) +` + err = py.Exec(script, "") + if err != nil { + t.Fatalf("setup failed: %s", err) + } + + obj := py.Eval("broken") + if err := obj.Err(); err != nil { + t.Fatalf("Eval broken: %s", err) + } + + _, err = obj.Slice() + if err == nil { + t.Error("Slice on BrokenList: expected error, got nil") + } +} + +// TestObjSliceGetItemErrorPath verifies Keys() works correctly on a populated +// dict, exercising the objSlice path via gate.keys(). +func TestObjSliceGetItemErrorPath(t *testing.T) { + py, err := NewPython() + assert.NoError(err) + defer py.Close() + + assert.NoError(py.Exec(`d = {"a": 1, "b": 2, "c": 3}`, "")) + obj := py.Get("d") + assert.NoError(obj.Err()) + + keys, err := obj.Keys() + if err != nil { + t.Fatalf("Keys: unexpected error: %v", err) + } + if len(keys) != 3 { + t.Errorf("Keys: expected 3 keys, got %d", len(keys)) + } +} + diff --git a/cpython/object_call_test.go b/cpython/object_call_test.go new file mode 100644 index 0000000..bd08b54 --- /dev/null +++ b/cpython/object_call_test.go @@ -0,0 +1,129 @@ +// MFP - Multi-Function Printers and scanners toolkit +// CPython binding. +// +// Copyright (C) 2024 and up by Alexander Pevzner (pzz@apevzner.com) +// See LICENSE for license terms and conditions +// +// Tests for callable detection and invocation: IsCallable, Call, CallKW. + +package cpython + +import ( + "testing" + + "github.com/OpenPrinting/go-mfp/internal/assert" +) + +// TestObjectCallable tests IsCallable on callable and non-callable objects. +func TestObjectCallable(t *testing.T) { + py, err := NewPython() + assert.NoError(err) + defer py.Close() + + obj := py.Eval("5") + assert.NoError(obj.Err()) + if obj.IsCallable() { + t.Errorf("Object.IsCallable: false positive on integer") + } + + obj = py.Eval("min") + assert.NoError(obj.Err()) + if !obj.IsCallable() { + t.Errorf("Object.IsCallable: false negative on builtin function") + } +} + +// TestObjectCall tests Call with positional args and CallKW with keyword args. +func TestObjectCall(t *testing.T) { + py, err := NewPython() + assert.NoError(err) + defer py.Close() + + obj := py.Eval("min") + assert.NoError(obj.Err()) + + // Positional args: min(1, 2) == 1 + res := obj.Call(1, 2) + if err := res.Err(); err != nil { + t.Errorf("Object.Call (positional): %s", err) + return + } + val, err := res.Int() + if err != nil { + t.Errorf("Object.Call (positional): Int: %s", err) + return + } + if val != 1 { + t.Errorf("Object.Call (positional):\nexpected: 1\npresent: %d\n", val) + } + + // Keyword args: min([], default=5) == 5 + res = obj.CallKW(map[string]any{"default": 5}, []int{}) + if err := res.Err(); err != nil { + t.Errorf("Object.Call (keyword): %s", err) + return + } + val, err = res.Int() + if err != nil { + t.Errorf("Object.Call (keyword): Int: %s", err) + return + } + if val != 5 { + t.Errorf("Object.Call (keyword):\nexpected: 5\npresent: %d\n", val) + } +} + +// TestObjectCallKWCoverage tests CallKW with an empty (non-nil) kw map, +// which exercises the len(kw)==0 branch where pykwargs stays nil. +func TestObjectCallKWCoverage(t *testing.T) { + py, err := NewPython() + assert.NoError(err) + defer py.Close() + + assert.NoError(py.Exec(`def f(*args, **kwargs): return len(args)`, "")) + fn := py.Get("f") + assert.NoError(fn.Err()) + + res := fn.CallKW(map[string]any{}, 1, 2, 3) + if err := res.Err(); err != nil { + t.Errorf("CallKW empty kw: unexpected error: %s", err) + } + v, err := res.Int() + if err != nil || v != 3 { + t.Errorf("CallKW empty kw: expected 3, got %v (err: %v)", v, err) + } +} + +// TestObjectCallKWArgConversionError tests CallKW when a positional arg +// cannot be converted to a Python object. +func TestObjectCallKWArgConversionError(t *testing.T) { + py, err := NewPython() + assert.NoError(err) + defer py.Close() + + assert.NoError(py.Exec(`def f(x): return x`, "")) + fn := py.Get("f") + assert.NoError(fn.Err()) + + result := fn.Call(make(chan int)) + if result.Err() == nil { + t.Error("CallKW: expected error for unconvertible positional arg") + } +} + +// TestObjectCallKWKwargConversionError tests CallKW when a keyword arg value +// cannot be converted to a Python object. +func TestObjectCallKWKwargConversionError(t *testing.T) { + py, err := NewPython() + assert.NoError(err) + defer py.Close() + + assert.NoError(py.Exec(`def f(x=None): return x`, "")) + fn := py.Get("f") + assert.NoError(fn.Err()) + + result := fn.CallKW(map[string]any{"x": make(chan int)}) + if result.Err() == nil { + t.Error("CallKW: expected error for unconvertible keyword arg") + } +} diff --git a/cpython/object_conversion_test.go b/cpython/object_conversion_test.go new file mode 100644 index 0000000..a556bba --- /dev/null +++ b/cpython/object_conversion_test.go @@ -0,0 +1,353 @@ +// MFP - Multi-Function Printers and scanners toolkit +// CPython binding. +// +// Copyright (C) 2024 and up by Alexander Pevzner (pzz@apevzner.com) +// See LICENSE for license terms and conditions +// +// Tests for Python <-> Go type conversions (Object.Bool, Int, Float, etc.) +// and type inspection (Object.IsBool, IsSeq, etc.) + +package cpython + +import ( + "fmt" + "math" + "math/big" + "reflect" + "testing" + + "github.com/OpenPrinting/go-mfp/internal/assert" +) + +// TestObjectFromPython tests decoding of Python values into Go types. +func TestObjectFromPython(t *testing.T) { + type testData struct { + expr string // Python expression + val any // Expected value + unbox func(*Object) (any, error) // Value unboxing function + mustfail bool // Error expected + } + + const verybig = "21267647892944572736998860269687930881" + + bigint := func(s string) *big.Int { + v := big.NewInt(0) + v.SetString(s, 0) + return v + } + + unboxNone := func(obj *Object) (any, error) { return obj.IsNone(), nil } + unboxBigint := func(obj *Object) (any, error) { return obj.Bigint() } + unboxBool := func(obj *Object) (any, error) { return obj.Bool() } + unboxBytes := func(obj *Object) (any, error) { return obj.Bytes() } + unboxComplex := func(obj *Object) (any, error) { return obj.Complex() } + unboxFloat := func(obj *Object) (any, error) { return obj.Float() } + unboxInt := func(obj *Object) (any, error) { return obj.Int() } + unboxUint := func(obj *Object) (any, error) { return obj.Uint() } + unboxUnicode := func(obj *Object) (any, error) { return obj.Unicode() } + + tests := []testData{ + {expr: `None`, val: true, unbox: unboxNone}, + {expr: `True`, val: true, unbox: unboxBool}, + {expr: `0`, val: false, unbox: unboxBool}, + {expr: `False`, val: false, unbox: unboxBool}, + {expr: `"hello"`, val: "hello", unbox: unboxUnicode}, + {expr: `"привет"`, val: "привет", unbox: unboxUnicode}, + {expr: `0`, val: "привет", unbox: unboxUnicode, mustfail: true}, + {expr: `""`, val: "", unbox: unboxUnicode}, + {expr: `0`, val: int64(0), unbox: unboxInt}, + {expr: `0`, val: uint64(0), unbox: unboxUint}, + {expr: `0x7fffffff`, val: int64(0x7fffffff), unbox: unboxInt}, + {expr: `0x7fffffff`, val: uint64(0x7fffffff), unbox: unboxUint}, + {expr: `-0x7fffffff`, val: int64(-0x7fffffff), unbox: unboxInt}, + {expr: `-0x7fffffff`, unbox: unboxUint, mustfail: true}, + {expr: `0xffffffff`, val: int64(0xffffffff), unbox: unboxInt}, + {expr: `0xffffffff`, val: uint64(0xffffffff), unbox: unboxUint}, + {expr: `0xffffffffffffffff`, val: uint64(0xffffffffffffffff), unbox: unboxUint}, + {expr: verybig, unbox: unboxInt, mustfail: true}, + {expr: verybig, unbox: unboxUint, mustfail: true}, + {expr: `0`, val: bigint("0"), unbox: unboxBigint}, + {expr: `1`, val: bigint("1"), unbox: unboxBigint}, + {expr: `-1`, val: bigint("-1"), unbox: unboxBigint}, + {expr: `0xffffffffffffffff`, val: bigint("0xffffffffffffffff"), unbox: unboxBigint}, + {expr: verybig, val: bigint(verybig), unbox: unboxBigint}, + {expr: `None`, unbox: unboxInt, mustfail: true}, + {expr: `None`, unbox: unboxUint, mustfail: true}, + {expr: `None`, unbox: unboxBigint, mustfail: true}, + {expr: `b'\x01\x02\x03'`, val: []byte{0x1, 0x2, 0x3}, unbox: unboxBytes}, + {expr: `bytearray(b'\x01\x02\x03')`, val: []byte{0x1, 0x2, 0x3}, unbox: unboxBytes}, + {expr: `None`, unbox: unboxBytes, mustfail: true}, + {expr: `0.5`, val: 0.5, unbox: unboxFloat}, + {expr: `None`, unbox: unboxFloat, mustfail: true}, + {expr: `complex(1,2)`, val: complex(1, 2), unbox: unboxComplex}, + {expr: `None`, unbox: unboxComplex, mustfail: true}, + + // Corner cases for integers + {expr: `-1`, val: int64(-1), unbox: unboxInt}, + {expr: `-9223372036854775808`, val: int64(-9223372036854775808), unbox: unboxInt}, + {expr: `9223372036854775807`, val: int64(9223372036854775807), unbox: unboxInt}, + {expr: `0xffffffffffffffff`, val: uint64(0xffffffffffffffff), unbox: unboxUint}, + {expr: `18446744073709551615`, val: uint64(18446744073709551615), unbox: unboxUint}, + + // Numerical conversions -> complex + {expr: `0`, val: complex(0, 0), unbox: unboxComplex}, + {expr: `1`, val: complex(1, 0), unbox: unboxComplex}, + {expr: `1.5`, val: complex(1.5, 0), unbox: unboxComplex}, + {expr: `"hello"`, unbox: unboxComplex, mustfail: true}, + + // Numerical conversions -> float + {expr: `0`, val: 0.0, unbox: unboxFloat}, + {expr: `1`, val: 1.0, unbox: unboxFloat}, + {expr: `1.5`, val: 1.5, unbox: unboxFloat}, + {expr: `0xffffffffffffffff`, val: 18446744073709551615., unbox: unboxFloat}, + {expr: `0xfffffffffffffffff`, unbox: unboxFloat, mustfail: true}, + {expr: `"hello"`, unbox: unboxFloat, mustfail: true}, + + // Numerical conversions -> int64 + {expr: `0`, val: int64(0), unbox: unboxInt}, + {expr: `0.0`, val: int64(0), unbox: unboxInt}, + {expr: `2.0`, val: int64(2), unbox: unboxInt}, + {expr: `1.5`, val: int64(1), unbox: unboxInt}, + {expr: `-1.5`, val: int64(-1), unbox: unboxInt}, + {expr: fmt.Sprintf("%f", maxInt64Float), val: int64(maxInt64Float), unbox: unboxInt}, + {expr: fmt.Sprintf("%f", minInt64Float), val: int64(minInt64Float), unbox: unboxInt}, + {expr: fmt.Sprintf("%f", maxUint64Float), mustfail: true, unbox: unboxInt}, + + // Numerical conversions -> uint64 + {expr: `0`, val: uint64(0), unbox: unboxUint}, + {expr: `0.0`, val: uint64(0), unbox: unboxUint}, + {expr: `2.0`, val: uint64(2), unbox: unboxUint}, + {expr: `1.5`, val: uint64(1), unbox: unboxUint}, + {expr: `-1.5`, mustfail: true, unbox: unboxUint}, + {expr: fmt.Sprintf("%f", maxInt64Float), val: uint64(maxInt64Float), unbox: unboxUint}, + {expr: fmt.Sprintf("%f", minInt64Float), mustfail: true, unbox: unboxUint}, + {expr: fmt.Sprintf("%f", maxUint64Float), val: uint64(maxUint64Float), unbox: unboxUint}, + {expr: fmt.Sprintf("%f", maxUint64Float*2), mustfail: true, unbox: unboxUint}, + } + + py, err := NewPython() + assert.NoError(err) + defer py.Close() + + for _, test := range tests { + obj := py.Eval(test.expr) + + if err := obj.Err(); err != nil { + t.Errorf("%s: unexpected error: %s", test.expr, err) + continue + } + + val, err := test.unbox(obj) + if err != nil { + if !test.mustfail { + t.Errorf("%s: object value error: %s", test.expr, err) + } + continue + } + + if test.mustfail { + t.Errorf("%s: error expected but didn't occur", test.expr) + continue + } + + if !reflect.DeepEqual(val, test.val) { + t.Errorf("%s: object value mismatch:\nexpected: %#v\npresent: %#v\n", + test.expr, test.val, val) + } + } +} + +// TestObjectIsMapSeq tests type-inspection methods: IsBool, IsBytes, IsSeq, etc. +func TestObjectIsMapSeq(t *testing.T) { + type testData struct { + expr string + isbool bool + isbytearray bool + isbytes bool + iscallable bool + iscomplex bool + isdict bool + isfloat bool + islong bool + isnone bool + isseq bool + isunicode bool + } + + tests := []testData{ + {expr: "True", isbool: true}, + {expr: "False", isbool: true}, + {expr: `bytearray(b'\x01\x02\x03')`, isbytearray: true}, + {expr: `bytes(b'\x01\x02\x03')`, isbytes: true}, + {expr: `min`, iscallable: true}, + {expr: `0.5 + 0.25j`, iscomplex: true}, + {expr: `{}`, isdict: true}, + {expr: `0.5`, isfloat: true}, + {expr: `5`, islong: true}, + {expr: `None`, isnone: true}, + {expr: `[]`, isseq: true}, + {expr: `()`, isseq: true}, + {expr: `"hello"`, isunicode: true}, + } + + py, err := NewPython() + assert.NoError(err) + defer py.Close() + + for _, test := range tests { + obj := py.Eval(test.expr) + assert.NoError(obj.Err()) + + if v := obj.IsBool(); v != test.isbool { + t.Errorf("%#v: Object.IsBool: expected %v, present %v", + test.expr, test.isbool, v) + } + if v := obj.IsByteArray(); v != test.isbytearray { + t.Errorf("%#v: Object.IsByteArray: expected %v, present %v", + test.expr, test.isbytearray, v) + } + if v := obj.IsBytes(); v != test.isbytes { + t.Errorf("%#v: Object.IsBytes: expected %v, present %v", + test.expr, test.isbytes, v) + } + if v := obj.IsCallable(); v != test.iscallable { + t.Errorf("%#v: Object.IsCallable: expected %v, present %v", + test.expr, test.iscallable, v) + } + if v := obj.IsComplex(); v != test.iscomplex { + t.Errorf("%#v: Object.IsComplex: expected %v, present %v", + test.expr, test.iscomplex, v) + } + if v := obj.IsDict(); v != test.isdict { + t.Errorf("%#v: Object.IsDict: expected %v, present %v", + test.expr, test.isdict, v) + } + if v := obj.IsFloat(); v != test.isfloat { + t.Errorf("%#v: Object.IsFloat: expected %v, present %v", + test.expr, test.isfloat, v) + } + if v := obj.IsLong(); v != test.islong { + t.Errorf("%#v: Object.IsLong: expected %v, present %v", + test.expr, test.islong, v) + } + if v := obj.IsNone(); v != test.isnone { + t.Errorf("%#v: Object.IsNone: expected %v, present %v", + test.expr, test.isnone, v) + } + if v := obj.IsSeq(); v != test.isseq { + t.Errorf("%#v: Object.IsSeq: expected %v, present %v", + test.expr, test.isseq, v) + } + if v := obj.IsUnicode(); v != test.isunicode { + t.Errorf("%#v: Object.IsUnicode: expected %v, present %v", + test.expr, test.isunicode, v) + } + } +} + +// TestNewObject tests Python.NewObject — encoding Go values to Python objects. +func TestNewObject(t *testing.T) { + type testData struct { + in any // Input value + out string // Expected output + } + + py, err := NewPython() + assert.NoError(err) + defer py.Close() + + bigint := func(s string) *big.Int { + v := big.NewInt(0) + v.SetString(s, 0) + return v + } + + eval := func(s string) *Object { + obj := py.Eval(s) + assert.NoError(obj.Err()) + return obj + } + + tests := []testData{ + {in: nil, out: `None`}, + {in: true, out: `True`}, + {in: false, out: `False`}, + {in: 0, out: `0`}, + {in: 1, out: `1`}, + {in: -1, out: `-1`}, + {in: int8(0), out: `0`}, + {in: int8(math.MaxInt8), out: fmt.Sprintf("%d", math.MaxInt8)}, + {in: int8(math.MinInt8), out: fmt.Sprintf("%d", math.MinInt8)}, + {in: int16(0), out: `0`}, + {in: int16(math.MaxInt16), out: fmt.Sprintf("%d", math.MaxInt16)}, + {in: int16(math.MinInt16), out: fmt.Sprintf("%d", math.MinInt16)}, + {in: int32(0), out: `0`}, + {in: int32(math.MaxInt32), out: fmt.Sprintf("%d", math.MaxInt32)}, + {in: int32(math.MinInt32), out: fmt.Sprintf("%d", math.MinInt32)}, + {in: int64(0), out: `0`}, + {in: int64(math.MaxInt64), out: fmt.Sprintf("%d", math.MaxInt64)}, + {in: int64(math.MinInt64), out: fmt.Sprintf("%d", math.MinInt64)}, + {in: uint(0), out: `0`}, + {in: uint(1), out: `1`}, + {in: uint8(0), out: `0`}, + {in: uint8(math.MaxUint8), out: fmt.Sprintf("%d", math.MaxUint8)}, + {in: uint16(0), out: `0`}, + {in: uint16(math.MaxUint16), out: fmt.Sprintf("%d", math.MaxUint16)}, + {in: uint32(0), out: `0`}, + {in: uint32(math.MaxUint32), out: fmt.Sprintf("%d", math.MaxUint32)}, + {in: uint64(0), out: `0`}, + {in: uint64(math.MaxUint64), out: fmt.Sprintf("%d", uint64(math.MaxUint64))}, + {in: bigint("0"), out: `0`}, + {in: bigint("1"), out: `1`}, + {in: bigint("-1"), out: `-1`}, + {in: bigint("340282366920938463426481119284349108225"), + out: `340282366920938463426481119284349108225`}, + {in: bigint("-340282366920938463426481119284349108225"), + out: `-340282366920938463426481119284349108225`}, + {in: 0.5, out: `0.5`}, + {in: -0.5, out: `-0.5`}, + {in: 0.25 + 0.25i, out: `(0.25+0.25j)`}, + {in: -0.25 - 0.25i, out: `(-0.25-0.25j)`}, + {in: "", out: ``}, + {in: "Hello, world!", out: `Hello, world!`}, + {in: "Здравствуй, мир!", out: `Здравствуй, мир!`}, + {in: eval("12345"), out: `12345`}, + {in: []int{1, 2, 3}, out: `[1, 2, 3]`}, + {in: [3]int{1, 2, 3}, out: `[1, 2, 3]`}, + {in: []byte("ABC"), out: `b'ABC'`}, + {in: [3]byte{'A', 'B', 'C'}, out: `b'ABC'`}, + {in: []byte{}, out: `b''`}, + {in: [0]byte{}, out: `b''`}, + {in: map[int]int{}, out: `{}`}, + {in: map[bool]string{true: "T", false: "F"}, out: `{False: 'F', True: 'T'}`}, + {in: map[int]string{1: "1", 2: "2", 3: "3"}, out: `{1: '1', 2: '2', 3: '3'}`}, + {in: map[uint]string{1: "1", 2: "2", 3: "3"}, out: `{1: '1', 2: '2', 3: '3'}`}, + {in: map[float64]string{0.25: "1/4", 0.5: "1/2"}, out: `{0.25: '1/4', 0.5: '1/2'}`}, + {in: map[string]int{"one": 1, "two": 2, "three": 3}, + out: `{'one': 1, 'three': 3, 'two': 2}`}, + {in: map[string]int{"раз": 1, "два": 2, "три": 3}, + out: `{'два': 2, 'раз': 1, 'три': 3}`}, + {in: map[int]int(nil), out: `{}`}, + {in: map[any]any(nil), out: `{}`}, + } + + for _, test := range tests { + obj := py.NewObject(test.in) + if err := obj.Err(); err != nil { + t.Errorf("%v: Python.NewObject: %s", test.in, err) + continue + } + + s, err := obj.Str() + if err != nil { + t.Errorf("%v: Object.Str: %s", test.in, err) + continue + } + + if s != test.out { + t.Errorf("%v: Python.NewObject:\nexpected: %s\npresent: %s\n", + test.in, test.out, s) + fmt.Printf("%v\n", obj) + } + } +} diff --git a/cpython/object_errors_test.go b/cpython/object_errors_test.go new file mode 100644 index 0000000..ad2a070 --- /dev/null +++ b/cpython/object_errors_test.go @@ -0,0 +1,344 @@ +// MFP - Multi-Function Printers and scanners toolkit +// CPython binding. +// +// Copyright (C) 2024 and up by Alexander Pevzner (pzz@apevzner.com) +// See LICENSE for license terms and conditions +// +// Tests for error propagation through Object methods, conversion errors, +// and miscellaneous methods: Py, Save, SaveTo, SaveItem, String, Repr, +// TypeName, TypeModuleName. + +package cpython + +import ( + "testing" + + "github.com/OpenPrinting/go-mfp/internal/assert" +) + +// TestObjectMiscMethods tests Py, Save, SaveTo, SaveItem, String, Repr, +// TypeName, TypeModuleName, and error-object behaviour. +func TestObjectMiscMethods(t *testing.T) { + py, err := NewPython() + assert.NoError(err) + defer py.Close() + + obj := py.Eval("42") + assert.NoError(obj.Err()) + + if obj.Py() != py { + t.Errorf("Object.Py: returned wrong interpreter") + } + + r, err := obj.Repr() + if err != nil { + t.Errorf("Object.Repr: %s", err) + } else if r != "42" { + t.Errorf("Object.Repr:\nexpected: 42\npresent: %s", r) + } + + if tn := obj.TypeName(); tn != "int" { + t.Errorf("Object.TypeName:\nexpected: int\npresent: %s", tn) + } + + if tmn := obj.TypeModuleName(); tmn != "builtins" { + t.Errorf("Object.TypeModuleName:\nexpected: builtins\npresent: %s", tmn) + } + + if s := obj.String(); s != "42" { + t.Errorf("Object.String:\nexpected: 42\npresent: %s", s) + } + + // String() on an error object returns the error message. + errObj := newErrorObject(py, ErrNotFound{}) + if s := errObj.String(); s != (ErrNotFound{}).Error() { + t.Errorf("Object.String (error):\nexpected: %s\npresent: %s", + ErrNotFound{}.Error(), s) + } + + // Save: store as a global variable. + num := py.Eval("100") + assert.NoError(num.Err()) + if err = num.Save("saved_num"); err != nil { + t.Errorf("Object.Save: %s", err) + } + got := py.Eval("saved_num") + assert.NoError(got.Err()) + if v, err := got.Int(); err != nil || v != 100 { + t.Errorf("Object.Save: expected 100, got %v (err: %v)", v, err) + } + + // SaveTo: store as an attribute of another object. + err = py.Exec("class Box:\n pass\nbox = Box()\n", "") + if err != nil { + t.Fatalf("Object.SaveTo setup: %s", err) + } + box := py.Eval("box") + if err := box.Err(); err != nil { + t.Fatalf("Object.SaveTo setup: Eval box: %s", err) + } + saveToVal := py.NewObject(999) + assert.NoError(saveToVal.Err()) + if err = saveToVal.SaveTo(box, "contents"); err != nil { + t.Errorf("Object.SaveTo: %s", err) + } + attr := box.Get("contents") + if err := attr.Err(); err != nil { + t.Errorf("Object.SaveTo: attribute not set: %s", err) + } else if n, err := attr.Int(); err != nil || n != 999 { + t.Errorf("Object.SaveTo: expected 999, got %v (err: %v)", n, err) + } + + // SaveItem: store as an item in a dict. + d := py.Eval("{}") + assert.NoError(d.Err()) + saveItemVal := py.NewObject(777) + assert.NoError(saveItemVal.Err()) + if err = saveItemVal.SaveItem(d, "key"); err != nil { + t.Errorf("Object.SaveItem: %s", err) + } + gotItem := d.GetItem("key") + if err := gotItem.Err(); err != nil { + t.Errorf("Object.SaveItem: item not set: %s", err) + } else if n, err := gotItem.Int(); err != nil || n != 777 { + t.Errorf("Object.SaveItem: expected 777, got %v (err: %v)", n, err) + } + + // Error-object: type/callable/seq checks return zero values. + e := newErrorObject(py, ErrNotFound{}) + if e.IsNone() { + t.Error("Object.IsNone on error obj: expected false") + } + if e.TypeName() != "" { + t.Error("Object.TypeName on error obj: expected empty string") + } + if e.TypeModuleName() != "" { + t.Error("Object.TypeModuleName on error obj: expected empty string") + } + if e.IsCallable() { + t.Error("Object.IsCallable on error obj: expected false") + } + if e.IsSeq() { + t.Error("Object.IsSeq on error obj: expected false") + } +} + +// TestObjectErrorPaths tests that every Object method propagates an error +// object's error unchanged, and covers begin() failure paths. +func TestObjectErrorPaths(t *testing.T) { + py, err := NewPython() + assert.NoError(err) + defer py.Close() + + sentinel := ErrNotFound{} + e := newErrorObject(py, sentinel) + + // Invalidate on a closed interpreter hits the gate() error path. + py2, err := NewPython() + assert.NoError(err) + obj2 := py2.Eval("1") + assert.NoError(obj2.Err()) + py2.Close() + obj2.Invalidate() + + check := func(name string, got error) { + t.Helper() + if got != sentinel { + t.Errorf("%s: expected sentinel error, got %v", name, got) + } + } + + _, err = e.Del("key") + check("Del", err) + + if got := e.GetItem("key"); got.Err() != sentinel { + t.Errorf("GetItem: expected sentinel error, got %v", got.Err()) + } + + _, err = e.ContainsItem("key") + check("ContainsItem", err) + + check("SetItem", e.SetItem("key", 1)) + + _, err = e.DelAttr("name") + check("DelAttr", err) + + if got := e.Get("name"); got.Err() != sentinel { + t.Errorf("Get: expected sentinel error, got %v", got.Err()) + } + + _, err = e.HasAttr("name") + check("HasAttr", err) + + check("Set", e.Set("name", 1)) + + if got := e.CallKW(nil, 1, 2); got.Err() != sentinel { + t.Errorf("CallKW: expected sentinel error, got %v", got.Err()) + } + + _, err = e.Bool() + check("Bool", err) + + _, err = e.Keys() + check("Keys", err) + + _, err = e.Slice() + check("Slice", err) + + // fastBool on a plain integer (not True/False) must return an error. + obj := py.Eval("5") + assert.NoError(obj.Err()) + if _, err = obj.fastBool(); err == nil { + t.Error("fastBool on int: expected error, got nil") + } +} + +// TestObjectBeginClosed tests begin() after the interpreter has been closed. +func TestObjectBeginClosed(t *testing.T) { + py, err := NewPython() + assert.NoError(err) + + obj := py.NewObject(42) + assert.NoError(obj.Err()) + py.Close() + + _, err = obj.Int() + if err == nil { + t.Error("begin: expected error after interpreter closed, got nil") + } +} + +// TestObjectBeginInvalidatedOID tests begin() after Invalidate removes the +// oid from the interpreter's map; lookupObjID returns nil and must error. +func TestObjectBeginInvalidatedOID(t *testing.T) { + py, err := NewPython() + assert.NoError(err) + defer py.Close() + + obj := py.Eval("999") + assert.NoError(obj.Err()) + obj.Invalidate() + + _, err = obj.Int() + if err == nil { + t.Error("begin after oid removal: expected error, got nil") + } + + if s := obj.String(); s == "" { + t.Error("String after oid removal: expected non-empty error string") + } +} + +// TestObjectGetErrorPropagation tests that Get on an error object propagates +// the error and NotFound() reports correctly. +func TestObjectGetErrorPropagation(t *testing.T) { + py, err := NewPython() + assert.NoError(err) + defer py.Close() + + errObj := newErrorObject(py, ErrNotFound{}) + result := errObj.Get("anything") + if result.Err() == nil { + t.Error("Get on error object: expected error propagation") + } + if !result.NotFound() { + t.Errorf("Get on error object: expected ErrNotFound, got %v", result.Err()) + } +} + +// TestObjectDelKeyConversionError tests Del when the key cannot be converted. +func TestObjectDelKeyConversionError(t *testing.T) { + py, err := NewPython() + assert.NoError(err) + defer py.Close() + + assert.NoError(py.Exec(`d = {}`, "")) + obj := py.Get("d") + assert.NoError(obj.Err()) + + _, err = obj.Del(make(chan int)) + if err == nil { + t.Error("Del: expected error for unconvertible key") + } +} + +// TestObjectGetItemKeyConversionError tests GetItem when the key cannot be converted. +func TestObjectGetItemKeyConversionError(t *testing.T) { + py, err := NewPython() + assert.NoError(err) + defer py.Close() + + assert.NoError(py.Exec(`d = {"a": 1}`, "")) + obj := py.Get("d") + assert.NoError(obj.Err()) + + if result := obj.GetItem(make(chan int)); result.Err() == nil { + t.Error("GetItem: expected error for unconvertible key") + } +} + +// TestObjectContainsItemKeyConversionError tests ContainsItem when the key +// cannot be converted. +func TestObjectContainsItemKeyConversionError(t *testing.T) { + py, err := NewPython() + assert.NoError(err) + defer py.Close() + + assert.NoError(py.Exec(`d = {"a": 1}`, "")) + obj := py.Get("d") + assert.NoError(obj.Err()) + + _, err = obj.ContainsItem(make(chan int)) + if err == nil { + t.Error("ContainsItem: expected error for unconvertible key") + } +} + +// TestObjectSetItemKeyConversionError tests SetItem when the key cannot be converted. +func TestObjectSetItemKeyConversionError(t *testing.T) { + py, err := NewPython() + assert.NoError(err) + defer py.Close() + + assert.NoError(py.Exec(`d = {}`, "")) + obj := py.Get("d") + assert.NoError(obj.Err()) + + if err = obj.SetItem(make(chan int), 1); err == nil { + t.Error("SetItem: expected error for unconvertible key") + } +} + +// TestObjectSetItemValConversionError tests SetItem when the value cannot be converted. +func TestObjectSetItemValConversionError(t *testing.T) { + py, err := NewPython() + assert.NoError(err) + defer py.Close() + + assert.NoError(py.Exec(`d = {}`, "")) + obj := py.Get("d") + assert.NoError(obj.Err()) + + if err = obj.SetItem("key", make(chan int)); err == nil { + t.Error("SetItem: expected error for unconvertible value") + } +} + +// TestObjectSetValConversionError tests Set when the value cannot be converted. +func TestObjectSetValConversionError(t *testing.T) { + py, err := NewPython() + assert.NoError(err) + defer py.Close() + + assert.NoError(py.Exec(` +class Obj: + pass +o = Obj() +`, "")) + obj := py.Get("o") + assert.NoError(obj.Err()) + + if err = obj.Set("attr", make(chan int)); err == nil { + t.Error("Set: expected error for unconvertible value") + } +} diff --git a/cpython/object_lifecycle_test.go b/cpython/object_lifecycle_test.go new file mode 100644 index 0000000..c1426ff --- /dev/null +++ b/cpython/object_lifecycle_test.go @@ -0,0 +1,161 @@ +// MFP - Multi-Function Printers and scanners toolkit +// CPython binding. +// +// Copyright (C) 2024 and up by Alexander Pevzner (pzz@apevzner.com) +// See LICENSE for license terms and conditions +// +// Tests for Bool/fastBool paths and object lifecycle: +// GC collection, Invalidate, finalizer after interpreter close. + +package cpython + +import ( + "runtime" + "testing" + + "github.com/OpenPrinting/go-mfp/internal/assert" +) + +// TestObjectBoolFallback tests Bool() via the __bool__ method fallback path. +func TestObjectBoolFallback(t *testing.T) { + py, err := NewPython() + assert.NoError(err) + defer py.Close() + + script := ` +class Truthy: + def __bool__(self): + return True + +class Falsy: + def __bool__(self): + return False + +truthy = Truthy() +falsy = Falsy() +` + err = py.Exec(script, "") + if err != nil { + t.Fatalf("setup failed: %s", err) + } + + obj := py.Eval("truthy") + if err := obj.Err(); err != nil { + t.Fatalf("Eval truthy: %s", err) + } + b, err := obj.Bool() + if err != nil { + t.Errorf("Object.Bool (Truthy): %s", err) + } else if !b { + t.Errorf("Object.Bool (Truthy): expected true, got false") + } + + obj = py.Eval("falsy") + if err := obj.Err(); err != nil { + t.Fatalf("Eval falsy: %s", err) + } + b, err = obj.Bool() + if err != nil { + t.Errorf("Object.Bool (Falsy): %s", err) + } else if b { + t.Errorf("Object.Bool (Falsy): expected false, got true") + } +} + +// TestObjectBoolNoMethod tests Bool() when __bool__ exists but is not callable +// (set to None). fastBool fails, toBool.Call() also fails, so Bool() must +// return the original fastBool error. +func TestObjectBoolNoMethod(t *testing.T) { + py, err := NewPython() + assert.NoError(err) + defer py.Close() + + script := ` +class NoBool: + __bool__ = None + +nobool = NoBool() +` + err = py.Exec(script, "") + if err != nil { + t.Fatalf("setup failed: %s", err) + } + + obj := py.Eval("nobool") + if err := obj.Err(); err != nil { + t.Fatalf("Eval nobool: %s", err) + } + + _, err = obj.Bool() + if err == nil { + t.Error("Object.Bool (NoBool): expected error, got nil") + } +} + +// TestObjectGC tests that Objects are properly tracked and collected by GC. +func TestObjectGC(t *testing.T) { + py, err := NewPython() + assert.NoError(err) + + runtime.GC() + base := py.countObjID() + + err = py.Eval("5").Err() + assert.NoError(err) + + if len(py.objects.mapped) != base+1 { + t.Errorf("TestObjectGC: object not properly mapped") + } + + runtime.GC() + runtime.GC() + + if py.countObjID() != base { + t.Errorf("TestObjectGC: GC did not collect object") + } +} + +// TestObjectInvalidateValid tests Invalidate on a live object. +// Verifies the gate-success branch of Invalidate, and that any subsequent +// operation on the invalidated object returns an error. +func TestObjectInvalidateValid(t *testing.T) { + py, err := NewPython() + assert.NoError(err) + defer py.Close() + + obj := py.Eval("123") + assert.NoError(obj.Err()) + + v, err := obj.Int() + if err != nil || v != 123 { + t.Fatalf("pre-Invalidate: expected 123, got %v (err: %v)", v, err) + } + + obj.Invalidate() + + _, err = obj.Int() + if err == nil { + t.Error("Int after Invalidate: expected error, got nil") + } +} + +// TestObjectFinalizerAfterClose tests that the GC finalizer handles the case +// where the Python interpreter is already closed when it fires. +// This covers the obj.py.closed() == true branch inside finalizer(). +func TestObjectFinalizerAfterClose(t *testing.T) { + py, err := NewPython() + assert.NoError(err) + + { + obj := py.Eval("42") + assert.NoError(obj.Err()) + py.Close() + _ = obj + } + + // Two GC passes: first enqueues finalizers, second runs them. + runtime.GC() + runtime.GC() + // Reaching here without panic means the branch is covered. +} + From 4bd8d8dcb1aff39c4e020b18893a50111b1d2026 Mon Sep 17 00:00:00 2001 From: abhishrestha Date: Thu, 4 Jun 2026 15:58:07 +0530 Subject: [PATCH 2/2] Add tests for callback functionality --- cpython/callback_test.go | 271 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 260 insertions(+), 11 deletions(-) diff --git a/cpython/callback_test.go b/cpython/callback_test.go index 5aa4341..6119634 100644 --- a/cpython/callback_test.go +++ b/cpython/callback_test.go @@ -9,27 +9,276 @@ package cpython import ( - "fmt" + "errors" "testing" "github.com/OpenPrinting/go-mfp/internal/assert" ) -// TestCallback tests calling Go from Python -func TestCallback(t *testing.T) { +// TestNewCallbackInvalidSignature tests that newCallback returns nil for +// functions with unsupported return signatures: +// - two return values where the second is not error +// - more than two return values +// +// NOTE: the following branches in callback.go are not tested because they +// are unreachable from Go-level tests: +// - callback.object: makeCapsule failure (requires CPython OOM) +// - callback.object: makeCfunction failure (requires CPython OOM) +// - callbackCall: p==nil branch (requires passing a non-capsule PyObject +// at the C level; calling callbackCall(nil,nil) directly causes SIGSEGV) +// - callbackDestroy: p==nil branch (same reason as above) +// - callbackDestroy: the function body itself is a CGo export attributed +// to C by the coverage tool; Delete() is its Go-side body and is tested +// directly by TestCallbackDeleteDirect. +// - callback.call: len(ret)==2, err==nil fallthrough — triggers a panic +// in callback.go due to nil interface assertion; not tested to avoid +// masking the underlying bug without modifying callback.go. +func TestNewCallbackInvalidSignature(t *testing.T) { + py, err := NewPython() + assert.NoError(err) + defer py.Close() + + // Two return values, second is NOT error. + obj := py.NewObject(func() (int, int) { return 1, 2 }) + if obj.Err() == nil { + t.Error("NewObject (int,int): expected error for invalid signature, got nil") + } + + // Three return values. + obj = py.NewObject(func() (int, int, int) { return 1, 2, 3 }) + if obj.Err() == nil { + t.Error("NewObject (int,int,int): expected error for invalid signature, got nil") + } +} + +// TestNewCallbackValidSignatures tests all accepted return-value combinations. +func TestNewCallbackValidSignatures(t *testing.T) { + py, err := NewPython() + assert.NoError(err) + defer py.Close() + + // 0 return values. + obj := py.NewObject(func() {}) + if err := obj.Err(); err != nil { + t.Errorf("0-return callback: unexpected error: %s", err) + } + + // 1 return value. + obj = py.NewObject(func() int { return 0 }) + if err := obj.Err(); err != nil { + t.Errorf("1-return callback: unexpected error: %s", err) + } + + // 2 return values (value + error). + obj = py.NewObject(func() (int, error) { return 0, errors.New("x") }) + if err := obj.Err(); err != nil { + t.Errorf("2-return (value+error) callback: unexpected error: %s", err) + } +} + +// TestCallbackVoidNoArgs covers the fast-path in callback.call: +// NumIn==0 && NumOut==0 — returns pyNone without entering the gate. +func TestCallbackVoidNoArgs(t *testing.T) { + py, err := NewPython() + assert.NoError(err) + defer py.Close() + + called := false + obj := py.NewObject(func() { called = true }) + assert.NoError(obj.Err()) + + ret := obj.Call() + if err := ret.Err(); err != nil { + t.Errorf("void callback: unexpected error: %s", err) + } + if !called { + t.Error("void callback: function was not called") + } + if !ret.IsNone() { + t.Errorf("void callback: expected None return, got %s", ret.String()) + } +} + +// TestCallbackSingleReturn covers the len(ret)==1 branch in callback.call. +func TestCallbackSingleReturn(t *testing.T) { + py, err := NewPython() + assert.NoError(err) + defer py.Close() + + obj := py.NewObject(func() int { return 42 }) + assert.NoError(obj.Err()) + + ret := obj.Call() + if err := ret.Err(); err != nil { + t.Errorf("single-return callback: unexpected error: %s", err) + } + + v, err := ret.Int() + if err != nil { + t.Errorf("single-return callback: Int: %s", err) + } + if v != 42 { + t.Errorf("single-return callback: expected 42, got %d", v) + } +} + +// TestCallbackValueAndError covers the len(ret)==2, err!=nil branch. +// The Go error propagates as a Python exception via callbackSetError. +func TestCallbackValueAndError(t *testing.T) { + py, err := NewPython() + assert.NoError(err) + defer py.Close() + + obj := py.NewObject(func() (int, error) { return 0, errors.New("go error") }) + assert.NoError(obj.Err()) + + ret := obj.Call() + if ret.Err() == nil { + t.Error("value+error callback: expected error to propagate, got nil") + } +} + +// TestCallbackSetErrorPython covers the ErrPython branch in callbackSetError. +func TestCallbackSetErrorPython(t *testing.T) { py, err := NewPython() assert.NoError(err) defer py.Close() - call := py.NewObject(func() { - fmt.Println("==== callbackCall ====") - }) - println(call.Str()) - ret := call.Call(5) + pyErr := ErrPython{ + except: Except("ValueError"), + msg: "python-style error", + } + obj := py.NewObject(func() (int, error) { return 0, pyErr }) + assert.NoError(obj.Err()) + + ret := obj.Call() + if ret.Err() == nil { + t.Error("ErrPython callback: expected error to propagate, got nil") + } +} + +// TestCallbackRoundTrip registers a void Go callback as a Python global and +// calls it via py.Eval, verifying the complete call chain end-to-end. +func TestCallbackRoundTrip(t *testing.T) { + py, err := NewPython() + assert.NoError(err) + defer py.Close() + + called := false + obj := py.NewObject(func() { called = true }) + assert.NoError(obj.Err()) + + if err = obj.Save("go_callback"); err != nil { + t.Fatalf("Save: %s", err) + } + + ret := py.Eval("go_callback()") + if err := ret.Err(); err != nil { + t.Errorf("round-trip call: %s", err) + } + if !called { + t.Error("round-trip call: Go function was not called") + } +} + +// TestCallbackRoundTripReturn verifies a callback returning a value can be +// called from Python and its return value retrieved. +func TestCallbackRoundTripReturn(t *testing.T) { + py, err := NewPython() + assert.NoError(err) + defer py.Close() + + obj := py.NewObject(func() int { return 7 }) + assert.NoError(obj.Err()) + + if err = obj.Save("go_fn"); err != nil { + t.Fatalf("Save: %s", err) + } + + ret := py.Eval("go_fn()") if err := ret.Err(); err != nil { - println(err.Error()) - } else { - println(ret.Str()) + t.Errorf("round-trip return: %s", err) + } + + v, err := ret.Int() + if err != nil || v != 7 { + t.Errorf("round-trip return: expected 7, got %v (err: %v)", v, err) } +} + +// TestCallbackDelete covers callback.Delete and callbackDestroy. +// The callback object is a PyCFunction wrapping an internal PyCapsule. +// When the PyCFunction's refcount drops to zero, CPython frees it, which +// decrefs the capsule, firing callbackDestroy → cb.Delete(). +// We delete the Python global and force a CPython GC cycle to ensure +// the destructor fires before the test returns. +func TestCallbackDelete(t *testing.T) { + py, err := NewPython() + assert.NoError(err) + defer py.Close() + + obj := py.NewObject(func() {}) + assert.NoError(obj.Err()) + + if err = obj.Save("cb_to_delete"); err != nil { + t.Fatalf("Save: %s", err) + } + + // Drop the Go-side reference first. + obj.Invalidate() + + // Delete the Python global and run CPython's cyclic GC to ensure + // the PyCFunction and its internal capsule are freed synchronously, + // firing callbackDestroy → cb.Delete(). + assert.NoError(py.Exec(` +import gc +del cb_to_delete +gc.collect() +`, "")) +} + +// TestCallbackDeleteDirect covers callback.Delete directly, exercising +// the C.free calls for ml_name and the PyMethodDef allocation. +// (callbackDestroy is a CGo export attributed to C by the coverage tool; +// Delete() is its Go-side body and is what actually needs coverage.) +func TestCallbackDeleteDirect(t *testing.T) { + py, err := NewPython() + assert.NoError(err) + defer py.Close() + cb := newCallback(py, "direct_delete", func() {}) + if cb == nil { + t.Fatal("newCallback returned nil unexpectedly") + } + // Call Delete directly — must not panic or crash. + cb.Delete() } + +// TestCallbackCallGateError covers the cb.py.gate() failure path in +// callback.call. This path is reached when the interpreter is closed +// between the time the callback object was created and when it is called. +// We simulate this by closing the interpreter and then directly invoking +// callback.call on a callback whose interpreter is gone. +func TestCallbackCallGateError(t *testing.T) { + py, err := NewPython() + assert.NoError(err) + + // Create a callback with NumIn>0 or NumOut>0 so it does NOT take the + // void fast-path and instead reaches the gate() call. + // A func() int has NumOut==1, NumIn==0 — it skips the void path and + // calls gate() before anything else. + cb := newCallback(py, "test", func() int { return 1 }) + if cb == nil { + t.Fatal("newCallback returned nil") + } + + // Close the interpreter so gate() will return an error. + py.Close() + + // Directly call cb.call — gate() must fail and return an error. + _, err = cb.call(nil) + if err == nil { + t.Error("call after interpreter close: expected error, got nil") + } +} +