Open Christopher-Chianelli opened 7 months ago
This should be simple to fix. The problem should be in native/common/jp_exception.cpp where we set the exception information. tb_lineno is one of the fields we set. My guess it that we didn’t have a line number in the compiled code so we left it as NULL. The PyTest code may incorrect as it is blindly assuming that the result can’t be None, which I don’t see as a requirement in the spec. I will need to investigate further and read the some PEPs.
I was unable to replicate the issue from your example. I get
______________________________________________ test_exception_with_cause _______________________________________________
java.lang.java.lang.IllegalStateException: java.lang.IllegalStateException: cause
The above exception was the direct cause of the following exception:
> throw new IllegalStateException("test", new IllegalStateException("cause"));
E Exception: Java Exception
Example.java:4: Exception
The above exception was the direct cause of the following exception:
def test_exception_with_cause():
jpype.startJVM(classpath='.')
example = jpype.JClass('Example')
> example.throwingMethod()
E java.lang.java.lang.IllegalStateException: java.lang.IllegalStateException: test
test.py:5: java.lang.IllegalStateException
At this point, I am almost certain this is a pytest bug, since this code still cause the internal error:
def test_reproducer():
try:
code_that_cause_exception()
except:
raise Exception
Forgot that raise Exception
is a short for raise Exception from last_raise_exception
. Managed to get a minimal reproducer; it has to do with generated classes;
Reproducer is https://github.com/Christopher-Chianelli/issue-reproducer/tree/jpype-1178
If the generated class has source information
(i.e. classWriter.visitSource("MyFile.py", "debug");
and methodVisitor.visitLineNumber(0, line);
),
then the exception does not occur.
Unfortunately I got
[INFO] Scanning for projects...
[ERROR] [ERROR] Some problems were encountered while processing the POMs:
[FATAL] Non-resolvable parent POM for org.optaplanner:DroolsExecutableReproducer:8.0.0-SNAPSHOT: Could not find artifact org.optaplanner:optaplanner-build-parent:pom:8.0.0-SNAPSHOT and 'parent.relativePath' points at wrong local POM @ line 7, column 11
@
[ERROR] The build could not read 1 project -> [Help 1]
[ERROR]
[ERROR] The project org.optaplanner:DroolsExecutableReproducer:8.0.0-SNAPSHOT (/mnt/c/Users/nelson85/Documents/devel/open/jpype/issue/pom.xml) has 1 error
[ERROR] Non-resolvable parent POM for org.optaplanner:DroolsExecutableReproducer:8.0.0-SNAPSHOT: Could not find artifact org.optaplanner:optaplanner-build-parent:pom:8.0.0-SNAPSHOT and 'parent.relativePath' points at wrong local POM @ line 7, column 11 -> [Help 2]
[ERROR]
So I was unable to build the reproducer to verify the test. Sorry! I will try again next weekend to see if I can sort out what is going on. Unfortunately, as I don't use maven it doesn't make much sense to me.
The specific call where we mess with linenum is in native/common/jp_exception.cpp
PyObject *tb_create(
PyObject *last_traceback,
PyObject *dict,
const char* filename,
const char* funcname,
int linenum)
{
// Create a code for this frame. (ref count is 1)
JPPyObject code = JPPyObject::accept((PyObject*)PyCode_NewEmpty(filename, funcname, linenum));
// If we don't get the code object there is no point
if (code.get() == nullptr)
return nullptr;
// Create a frame for the traceback.
PyThreadState *state = PyThreadState_GET();
PyFrameObject *pframe = PyFrame_New(state, (PyCodeObject*) code.get(), dict, NULL);
JPPyObject frame = JPPyObject::accept((PyObject*)pframe);
// If we don't get the frame object there is no point
if (frame.get() == nullptr)
return nullptr;
// Create a traceback
#if PY_MINOR_VERSION<11
JPPyObject lasti = JPPyObject::claim(PyLong_FromLong(pframe->f_lasti));
#else
JPPyObject lasti = JPPyObject::claim(PyLong_FromLong(PyFrame_GetLasti(pframe)));
#endif
JPPyObject linenuma = JPPyObject::claim(PyLong_FromLong(linenum));
JPPyObject tuple = JPPyObject::call(PyTuple_Pack(4, Py_None, frame.get(), lasti.get(), linenuma.get()));
JPPyObject traceback = JPPyObject::accept(PyObject_Call((PyObject*) &PyTraceBack_Type, tuple.get(), NULL));
// We could fail in process
if (traceback.get() == nullptr)
{
return nullptr;
}
return traceback.keep();
}
As you can see we are calling the constructor with a valid traceback linenum. If there is a problem then most likely traceback changed its signature starting in 3.11 and we would need to adjust the constructor call to account for it. I scanned the Python code base and don't see an errors in the handoff (though admittedly the code is very ugly....)
PyObject *argsbuf[4];
PyObject * const *fastargs;
Py_ssize_t nargs = PyTuple_GET_SIZE(args);
PyObject *tb_next;
PyFrameObject *tb_frame;
int tb_lasti;
int tb_lineno;
fastargs = _PyArg_UnpackKeywords(_PyTuple_CAST(args)->ob_item, nargs, kwargs, NULL, &_parser, 4, 4, 0, argsbuf);
if (!fastargs) {
goto exit;
}
tb_next = fastargs[0];
if (!PyObject_TypeCheck(fastargs[1], &PyFrame_Type)) {
_PyArg_BadArgument("TracebackType", "argument 'tb_frame'", (&PyFrame_Type)->tp_name, fastargs[1]);
goto exit;
}
tb_frame = (PyFrameObject *)fastargs[1];
tb_lasti = _PyLong_AsInt(fastargs[2]);
if (tb_lasti == -1 && PyErr_Occurred()) { <=== BAD THING HAPPENS HERE IF THERE IS ALREADY AN EXCEPTION
goto exit;
}
tb_lineno = _PyLong_AsInt(fastargs[3]);
if (tb_lineno == -1 && PyErr_Occurred()) {
goto exit;
}
return_value = tb_new_impl(type, tb_next, tb_frame, tb_lasti, tb_lineno);
As you can see form this code it may be the same error as in the PR. There is a potential the lost exception is tripping a problem in the copied traceback. But if that was the case the traceback would have failed without producing, and not one with Py_None in the linenum slot. Thus I still don't have a good explanation for what could be happening here.
I think you checked out the wrong branch; the branch should be jpype-1178
not drools-classloader-reproducer
(which is the default branch)
Ah... will try again later them.
I got
(python3.12-wsl) nelson85@wl-5520983:~/devel/open/jpype/issue$ pytest
======================================================================================= test session starts ========================================================================================
platform linux -- Python 3.12.0, pytest-8.1.1, pluggy-1.4.0 -- /home/nelson85/env/python3.12-wsl/bin/python
cachedir: .pytest_cache
rootdir: /mnt/c/Users/nelson85/Documents/devel/open/jpype
configfile: setup.cfg
collected 2 items
tests/test_reproducer.py::test_throwing FAILED [ 50%]
tests/test_reproducer.py::test_b PASSED [100%]
============================================================================================= FAILURES =============================================================================================
__________________________________________________________________________________________ test_throwing ___________________________________________________________________________________________
> ???
E Exception: Java Exception
org.acme.GeneratedClass.java:-1: Exception
The above exception was the direct cause of the following exception:
def test_throwing():
import jpype
import jpype.imports
jpype.startJVM(classpath=[
'target/issue-reproducer-1.0.0-SNAPSHOT.jar',
'target/dependency/asm-9.7.jar'
])
from org.acme import MyClass
consumer = MyClass.getConsumer()
> consumer.accept('My Error')
E java.lang.java.lang.RuntimeException: java.lang.RuntimeException: My Error
tests/test_reproducer.py:10: java.lang.RuntimeException
===================================================================================== short test summary info ======================================================================================
FAILED tests/test_reproducer.py::test_throwing - java.lang.java.lang.RuntimeException: java.lang.RuntimeException: My Error
=================================================================================== 1 failed, 1 passed in 0.49s ====================================================================================
Is this correct or incorrect?
This is odd,
pytest
without mvn clean install
, got the above result on both this and #1180 (i.e. what is expected if the issue was fixed)mvn clean install
and recreated venvTypeError: unsupported operand type(s) for -: 'NoneType' and 'int'
on both this and #1180I don't know why it randomly worked then failed after a mvn clean install
.
This is what it looks like when the internal error happens:
mvn clean install
python -m venv venv
. venv/bin/activate
pip install JPype1 pytest
pytest
===================================================================================== test session starts ======================================================================================
platform linux -- Python 3.12.2, pytest-8.1.1, pluggy-1.4.0
rootdir: .../issue-reproducer
collected 2 items
tests/test_reproducer.py
INTERNALERROR> Traceback (most recent call last):
...
Note the second test is not even run.
any lead on a fix here? I can't reproduce this locally but fails on github actions https://github.com/omercnet/palgate-py/actions/runs/11260802234/job/31312822917?pr=2
I haven't touched it as I have no reproducing method. Could it be a particular version of Python 3.11. They backported some broken behavior in. It was the stuff of nightmares when I was dealing with 3.13 as what was ported in had wotk arounds only in later versions. Hence the comment... "Python 3.11 is dead to me."
This problem has cropped up again in my Python 3.12 testing for release. Not sure yet how to deal with it.
I have identified this issue. It is maddening. We are setting the line number to -1 on an integer field. Depending on the system and how it is called -1 will be changed to None. This is actually correct behavior because None means don't print a meaningless line number. But the pytest is trying to do math with it. We are once again the victim of random changes in Python code base.
Currently, to see results from PyTest failures caused by an Exception with a cause in Python 3.11 and above, I need to have this
@JImplementationFor
:To reproduce:
Python test:
Java class:
Exception from PyTest: