From 78d81e04404a06cacb237d9d16c2b136aaaac0bd Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Wed, 3 Apr 2024 17:50:58 -0400 Subject: [PATCH 1/6] feat: Add file and line numbers to exception traceback - CPython does not expose any sane way to set the traceback of an exception that does not orginate from a CPython exception. In particular, __traceback__ must be an internal Traceback type, and that Traceback type cannot be constructed without using internal frame and code types, which also cannot be constructed. - To get around this, we exploit the traceback module, which does not care about types and only care about interfaces. This allow us to use our own fake frame and code classes that match their interface. - We exploit object.__new__ to create an instance of TracebackException without calling its constructor, and set all its fields manually - To set the stack field, we use StackSummary.extract. We might be able to use StackSummary.from_list and avoid the fake frames and use FrameSummary directly --- ...ythonBytecodeToJavaBytecodeTranslator.java | 30 ++++++ .../jpyinterpreter/PythonClassTranslator.java | 38 ++++++- .../jpyinterpreter/PythonCompiledClass.java | 5 + .../PythonCompiledFunction.java | 15 +++ jpyinterpreter/src/main/python/conversions.py | 101 +++++++++++++----- jpyinterpreter/src/main/python/translator.py | 44 +++++++- jpyinterpreter/tests/conftest.py | 47 +++++++- jpyinterpreter/tests/test_builtins.py | 34 +++++- jpyinterpreter/tests/test_traceback.py | 28 +++++ 9 files changed, 305 insertions(+), 37 deletions(-) create mode 100644 jpyinterpreter/tests/test_traceback.py diff --git a/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/PythonBytecodeToJavaBytecodeTranslator.java b/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/PythonBytecodeToJavaBytecodeTranslator.java index e07e60bb..a6882037 100644 --- a/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/PythonBytecodeToJavaBytecodeTranslator.java +++ b/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/PythonBytecodeToJavaBytecodeTranslator.java @@ -245,6 +245,8 @@ public static Class translatePythonBytecodeToClass(PythonCompiledFunction final boolean isPythonLikeFunction = methodDescriptor.getDeclaringClassInternalName().equals(Type.getInternalName(PythonLikeFunction.class)); + classWriter.visitSource(pythonCompiledFunction.moduleFilePath, null); + createFields(classWriter); createConstructor(classWriter, internalClassName); @@ -289,6 +291,8 @@ public static Class translatePythonBytecodeToClass(PythonCompiledFunction final boolean isPythonLikeFunction = methodDescriptor.getDeclaringClassInternalName().equals(Type.getInternalName(PythonLikeFunction.class)); + classWriter.visitSource(pythonCompiledFunction.moduleFilePath, null); + createFields(classWriter); createConstructor(classWriter, internalClassName); @@ -308,6 +312,7 @@ public static Class translatePythonBytecodeToClass(PythonCompiledFunction null); methodVisitor.visitCode(); + visitGeneratedLineNumber(methodVisitor); methodVisitor.visitVarInsn(Opcodes.ALOAD, 0); for (int i = 0; i < methodWithoutGenerics.getParameterCount(); i++) { Type parameterType = Type.getType(methodWithoutGenerics.getParameterTypes()[i]); @@ -349,6 +354,8 @@ public static Class translatePythonBytecodeToPythonWrapperClass(PythonCom classWriter.visit(Opcodes.V11, Modifier.PUBLIC, internalClassName, null, Type.getInternalName(Object.class), new String[] { Type.getInternalName(PythonLikeFunction.class) }); + classWriter.visitSource(pythonCompiledFunction.moduleFilePath, null); + createFields(classWriter); classWriter.visitField(Modifier.PUBLIC | Modifier.STATIC, PYTHON_WRAPPER_CODE_STATIC_FIELD_NAME, Type.getDescriptor(OpaquePythonReference.class), @@ -368,6 +375,7 @@ public static Class translatePythonBytecodeToPythonWrapperClass(PythonCom null); methodVisitor.visitCode(); + visitGeneratedLineNumber(methodVisitor); methodVisitor.visitVarInsn(Opcodes.ALOAD, 0); methodVisitor.visitFieldInsn(Opcodes.GETFIELD, internalClassName, PYTHON_WRAPPER_FUNCTION_INSTANCE_FIELD_NAME, Type.getDescriptor(PythonObjectWrapper.class)); @@ -417,6 +425,8 @@ public static Class forceTranslatePythonBytecodeToGeneratorClass(PythonCo classWriter.visit(Opcodes.V11, Modifier.PUBLIC, internalClassName, null, Type.getInternalName(Object.class), new String[] { methodDescriptor.getDeclaringClassInternalName() }); + classWriter.visitSource(pythonCompiledFunction.moduleFilePath, null); + final boolean isPythonLikeFunction = methodDescriptor.getDeclaringClassInternalName().equals(Type.getInternalName(PythonLikeFunction.class)); @@ -460,6 +470,7 @@ public static Class forceTranslatePythonBytecodeToGeneratorClass(PythonCo null); methodVisitor.visitCode(); + visitGeneratedLineNumber(methodVisitor); methodVisitor.visitVarInsn(Opcodes.ALOAD, 0); for (int i = 0; i < methodWithoutGenerics.getParameterCount(); i++) { Type parameterType = Type.getType(methodWithoutGenerics.getParameterTypes()[i]); @@ -494,6 +505,7 @@ private static void createConstructor(ClassWriter classWriter, String className) null, null); methodVisitor.visitCode(); + visitGeneratedLineNumber(methodVisitor); methodVisitor.visitVarInsn(Opcodes.ALOAD, 0); methodVisitor.visitInsn(Opcodes.DUP); methodVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL, Type.getInternalName(Object.class), "", @@ -574,6 +586,8 @@ private static void createConstructor(ClassWriter classWriter, String className) Type.getType(PythonInterpreter.class)), null, null); methodVisitor.visitCode(); + + visitGeneratedLineNumber(methodVisitor); methodVisitor.visitVarInsn(Opcodes.ALOAD, 0); methodVisitor.visitInsn(Opcodes.DUP); methodVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL, Type.getInternalName(Object.class), "", @@ -647,6 +661,7 @@ private static void createPythonWrapperConstructor(ClassWriter classWriter, Stri null, null); methodVisitor.visitCode(); + visitGeneratedLineNumber(methodVisitor); methodVisitor.visitVarInsn(Opcodes.ALOAD, 0); methodVisitor.visitInsn(Opcodes.DUP); methodVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL, Type.getInternalName(Object.class), "", @@ -759,6 +774,8 @@ private static void createPythonWrapperConstructor(ClassWriter classWriter, Stri Type.getType(PythonInterpreter.class)), null, null); methodVisitor.visitCode(); + + visitGeneratedLineNumber(methodVisitor); methodVisitor.visitVarInsn(Opcodes.ALOAD, 0); methodVisitor.visitInsn(Opcodes.DUP); methodVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL, Type.getInternalName(Object.class), "", @@ -1007,6 +1024,7 @@ private static void translatePythonBytecodeToMethod(MethodDescriptor method, Str } methodVisitor.visitCode(); + visitGeneratedLineNumber(methodVisitor); Label start = new Label(); Label end = new Label(); @@ -1172,6 +1190,12 @@ public static void writeInstructionsForOpcodes(FunctionMetadata functionMetadata methodVisitor.visitLabel(label); } + if (instruction.startsLine().isPresent()) { + Label label = new Label(); + methodVisitor.visitLabel(label); + methodVisitor.visitLineNumber(instruction.startsLine().getAsInt(), label); + } + runAfterLabelAndBeforeArgumentors.accept(instruction); bytecodeIndexToArgumentorsMap.getOrDefault(instruction.offset(), Collections.emptyList()).forEach(Runnable::run); @@ -1361,4 +1385,10 @@ public static String getPythonBytecodeListing(PythonCompiledFunction pythonCompi out.append("\nco_exceptiontable = ").append(pythonCompiledFunction.co_exceptiontable).append("\n"); return out.toString(); } + + public static void visitGeneratedLineNumber(MethodVisitor methodVisitor) { + Label label = new Label(); + methodVisitor.visitLabel(label); + methodVisitor.visitLineNumber(0, label); + } } diff --git a/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/PythonClassTranslator.java b/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/PythonClassTranslator.java index 9daa15af..5d085691 100644 --- a/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/PythonClassTranslator.java +++ b/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/PythonClassTranslator.java @@ -168,6 +168,8 @@ public static PythonLikeType translatePythonClass(PythonCompiledClass pythonComp classWriter.visit(Opcodes.V11, Modifier.PUBLIC, internalClassName, null, superClassType.getJavaTypeInternalName(), interfaces); + classWriter.visitSource(pythonCompiledClass.moduleFilePath, null); + for (var annotation : pythonCompiledClass.annotations) { annotation.addAnnotationTo(classWriter); } @@ -236,6 +238,7 @@ public static PythonLikeType translatePythonClass(PythonCompiledClass pythonComp classWriter.visitMethod(Modifier.PUBLIC, "", Type.getMethodDescriptor(Type.VOID_TYPE), null, null); methodVisitor.visitCode(); + PythonBytecodeToJavaBytecodeTranslator.visitGeneratedLineNumber(methodVisitor); methodVisitor.visitVarInsn(Opcodes.ALOAD, 0); methodVisitor.visitFieldInsn(Opcodes.GETSTATIC, Type.getInternalName(PythonInterpreter.class), "DEFAULT", Type.getDescriptor(PythonInterpreter.class)); @@ -259,6 +262,7 @@ public static PythonLikeType translatePythonClass(PythonCompiledClass pythonComp methodVisitor.visitParameter("subclassType", 0); methodVisitor.visitCode(); + PythonBytecodeToJavaBytecodeTranslator.visitGeneratedLineNumber(methodVisitor); methodVisitor.visitVarInsn(Opcodes.ALOAD, 0); methodVisitor.visitVarInsn(Opcodes.ALOAD, 1); methodVisitor.visitVarInsn(Opcodes.ALOAD, 2); @@ -541,6 +545,8 @@ private static Class createPythonWrapperMethod(String methodName, PythonCompi classWriter.visit(Opcodes.V11, Modifier.PUBLIC, internalClassName, null, Type.getInternalName(Object.class), new String[] { interfaceDeclaration.interfaceName }); + classWriter.visitSource("", null); + classWriter.visitField(Modifier.PUBLIC | Modifier.FINAL, "$binaryType", Type.getDescriptor(PythonLikeType.class), null, null); classWriter.visitField(Modifier.STATIC | Modifier.PUBLIC, ARGUMENT_SPEC_INSTANCE_FIELD_NAME, @@ -554,7 +560,7 @@ private static Class createPythonWrapperMethod(String methodName, PythonCompi null, null); methodVisitor.visitCode(); - + PythonBytecodeToJavaBytecodeTranslator.visitGeneratedLineNumber(methodVisitor); methodVisitor.visitVarInsn(Opcodes.ALOAD, 0); methodVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL, Type.getInternalName(Object.class), "", Type.getMethodDescriptor(Type.VOID_TYPE), false); @@ -578,7 +584,7 @@ private static Class createPythonWrapperMethod(String methodName, PythonCompi } methodVisitor.visitCode(); - + PythonBytecodeToJavaBytecodeTranslator.visitGeneratedLineNumber(methodVisitor); methodVisitor.visitVarInsn(Opcodes.ALOAD, 0); methodVisitor.visitFieldInsn(Opcodes.GETFIELD, internalClassName, "$binaryType", Type.getDescriptor(PythonLikeType.class)); @@ -651,6 +657,8 @@ private static PythonLikeFunction createConstructor(String classInternalName, Type.getInternalName(PythonLikeFunction.class) }); + classWriter.visitSource(initFunction != null ? initFunction.moduleFilePath : "", null); + classWriter.visitField(Modifier.STATIC | Modifier.PUBLIC, ARGUMENT_SPEC_INSTANCE_FIELD_NAME, Type.getDescriptor(ArgumentSpec.class), null, null); @@ -659,6 +667,7 @@ private static PythonLikeFunction createConstructor(String classInternalName, classWriter.visitMethod(Modifier.PUBLIC, "", Type.getMethodDescriptor(Type.VOID_TYPE), null, null); methodVisitor.visitCode(); + PythonBytecodeToJavaBytecodeTranslator.visitGeneratedLineNumber(methodVisitor); methodVisitor.visitVarInsn(Opcodes.ALOAD, 0); methodVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL, Type.getInternalName(Object.class), "", Type.getMethodDescriptor(Type.VOID_TYPE), false); @@ -674,13 +683,16 @@ private static PythonLikeFunction createConstructor(String classInternalName, null, null); methodVisitor.visitCode(); - + PythonBytecodeToJavaBytecodeTranslator.visitGeneratedLineNumber(methodVisitor); methodVisitor.visitTypeInsn(Opcodes.NEW, classInternalName); methodVisitor.visitInsn(Opcodes.DUP); methodVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL, classInternalName, "", Type.getMethodDescriptor(Type.VOID_TYPE), false); if (initFunction != null) { + Label start = new Label(); + methodVisitor.visitLabel(start); + methodVisitor.visitLineNumber(initFunction.getFirstLine(), start); methodVisitor.visitInsn(Opcodes.DUP); methodVisitor.visitFieldInsn(Opcodes.GETSTATIC, constructorInternalClassName, ARGUMENT_SPEC_INSTANCE_FIELD_NAME, @@ -844,6 +856,10 @@ private static void createClassMethod(PythonLikeType pythonLikeType, ClassWriter addAnnotationsToMethod(function, methodVisitor); methodVisitor.visitCode(); + Label start = new Label(); + methodVisitor.visitLabel(start); + methodVisitor.visitLineNumber(function.getFirstLine(), start); + methodVisitor.visitFieldInsn(Opcodes.GETSTATIC, internalClassName, javaMethodName, interfaceDescriptor); for (int i = 0; i < function.totalArgCount(); i++) { @@ -880,6 +896,10 @@ private static void createInstanceOrStaticMethodBody(String internalClassName, S addAnnotationsToMethod(function, methodVisitor); methodVisitor.visitCode(); + Label start = new Label(); + methodVisitor.visitLabel(start); + methodVisitor.visitLineNumber(function.getFirstLine(), start); + methodVisitor.visitFieldInsn(Opcodes.GETSTATIC, internalClassName, javaMethodName, interfaceDescriptor); for (int i = 0; i < function.totalArgCount(); i++) { methodVisitor.visitVarInsn(Opcodes.ALOAD, i); @@ -929,6 +949,8 @@ public static void createGetAttribute(ClassWriter classWriter, String classInter methodVisitor.visitCode(); + PythonBytecodeToJavaBytecodeTranslator.visitGeneratedLineNumber(methodVisitor); + methodVisitor.visitVarInsn(Opcodes.ALOAD, 1); BytecodeSwitchImplementor.createStringSwitch(methodVisitor, instanceAttributes, 2, field -> { methodVisitor.visitVarInsn(Opcodes.ALOAD, 0); @@ -976,6 +998,8 @@ public static void createSetAttribute(ClassWriter classWriter, String classInter methodVisitor.visitCode(); + PythonBytecodeToJavaBytecodeTranslator.visitGeneratedLineNumber(methodVisitor); + methodVisitor.visitVarInsn(Opcodes.ALOAD, 1); BytecodeSwitchImplementor.createStringSwitch(methodVisitor, instanceAttributes, 3, field -> { var type = fieldToType.get(field); @@ -1023,6 +1047,8 @@ public static void createDeleteAttribute(ClassWriter classWriter, String classIn methodVisitor.visitCode(); + PythonBytecodeToJavaBytecodeTranslator.visitGeneratedLineNumber(methodVisitor); + methodVisitor.visitVarInsn(Opcodes.ALOAD, 1); BytecodeSwitchImplementor.createStringSwitch(methodVisitor, instanceAttributes, 2, field -> { methodVisitor.visitVarInsn(Opcodes.ALOAD, 0); @@ -1063,6 +1089,8 @@ public static void createReadFromCPythonReference(ClassWriter classWriter, Strin null); methodVisitor.visitCode(); + PythonBytecodeToJavaBytecodeTranslator.visitGeneratedLineNumber(methodVisitor); + methodVisitor.visitVarInsn(Opcodes.ALOAD, 0); methodVisitor.visitInsn(Opcodes.DUP); methodVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL, superClassInternalName, @@ -1157,6 +1185,8 @@ public static void createWriteToCPythonReference(ClassWriter classWriter, String null); methodVisitor.visitCode(); + PythonBytecodeToJavaBytecodeTranslator.visitGeneratedLineNumber(methodVisitor); + methodVisitor.visitVarInsn(Opcodes.ALOAD, 0); methodVisitor.visitInsn(Opcodes.DUP); methodVisitor.visitVarInsn(Opcodes.ALOAD, 1); @@ -1305,6 +1335,8 @@ public static InterfaceDeclaration createInterfaceForFunctionSignature(FunctionS classWriter.visit(Opcodes.V11, Modifier.PUBLIC | Modifier.INTERFACE | Modifier.ABSTRACT, internalClassName, null, Type.getInternalName(Object.class), null); + classWriter.visitSource("", null); + Type returnType = Type.getType(functionSignature.returnType); Type[] parameterTypes = new Type[functionSignature.parameterTypes.length]; for (int i = 0; i < parameterTypes.length; i++) { diff --git a/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/PythonCompiledClass.java b/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/PythonCompiledClass.java index ea08ac2a..3b0a6aa8 100644 --- a/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/PythonCompiledClass.java +++ b/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/PythonCompiledClass.java @@ -14,6 +14,11 @@ public class PythonCompiledClass { */ public String module; + /** + * The path to the file that defines the module. + */ + public String moduleFilePath; + /** * The qualified name of the class. Does not include module. */ diff --git a/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/PythonCompiledFunction.java b/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/PythonCompiledFunction.java index 923f4d45..35582847 100644 --- a/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/PythonCompiledFunction.java +++ b/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/PythonCompiledFunction.java @@ -22,6 +22,11 @@ public class PythonCompiledFunction { */ public String module; + /** + * The path to the file that defines the module. + */ + public String moduleFilePath; + /** * The qualified name of the function. Does not include module. */ @@ -128,6 +133,7 @@ public PythonCompiledFunction copy() { PythonCompiledFunction out = new PythonCompiledFunction(); out.module = module; + out.moduleFilePath = moduleFilePath; out.qualifiedName = qualifiedName; out.instructionList = List.copyOf(instructionList); out.closure = closure; @@ -282,4 +288,13 @@ public int totalArgCount() { return co_argcount + co_kwonlyargcount + extraArgs; } + + public int getFirstLine() { + for (var instruction : instructionList) { + if (instruction.startsLine().isPresent()) { + return instruction.startsLine().getAsInt(); + } + } + return -1; + } } diff --git a/jpyinterpreter/src/main/python/conversions.py b/jpyinterpreter/src/main/python/conversions.py index 1ba0e7f0..f381df3e 100644 --- a/jpyinterpreter/src/main/python/conversions.py +++ b/jpyinterpreter/src/main/python/conversions.py @@ -1,39 +1,77 @@ import builtins import inspect +import sys from dataclasses import dataclass -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional, Generator +from traceback import TracebackException, StackSummary, FrameSummary -from jpype import JLong, JDouble, JBoolean, JProxy, JImplementationFor +from jpype import JLong, JDouble, JBoolean, JProxy if TYPE_CHECKING: from java.util import IdentityHashMap + from java.lang import Throwable -# Workaround for https://github.com/jpype-project/jpype/issues/1178 -@JImplementationFor('java.lang.Throwable') -class _JavaException: - @staticmethod - def _get_exception_with_cause(exception): - if exception is None: - return None - try: - raise Exception(f'{exception.getClass().getSimpleName()}: {exception.getMessage()}') - except Exception as e: - cause = _JavaException._get_exception_with_cause(exception.getCause()) - if cause is not None: - try: - raise e from cause - except Exception as return_value: - return return_value - else: - return e - @property - def __cause__(self): - if self.getCause() is not None: - return _JavaException._get_exception_with_cause(self.getCause()) +@dataclass +class FakeFrame: + f_code: 'FakeCode' + f_globals: dict + f_locals: dict + + +@dataclass +class FakeCode: + co_filename: str + co_name: str + + +def extract_frames_from_java_error(java_error: 'Throwable'): + stack_trace = java_error.getStackTrace() + start_index = 0 + stop_index = len(stack_trace) + while start_index < stop_index and (stack_trace[start_index].getFileName() is None or + not stack_trace[start_index].getFileName().endswith('.py')): + start_index += 1 + + # If there is no python part, keep the entire exception + if start_index == stop_index: + start_index = 0 + + for i in range(start_index, stop_index): + stack_trace_element = stack_trace[stop_index - (i - start_index) - 1] + file_name = stack_trace_element.getFileName() or '' + if file_name.endswith('.py'): + class_name = stack_trace_element.getClassName() or '' + function_name = class_name.rsplit('.', 1)[-1].split('$', 1)[0] else: - return None + function_name = stack_trace_element.getMethodName() or '' + line_number = stack_trace_element.getLineNumber() or 0 + yield FakeFrame(FakeCode(file_name, function_name), {}, {}), line_number + + +def get_traceback_exception(java_error: 'Throwable', python_exception_type: type, clone_map: 'PythonCloneMap') -> TracebackException: + out = object.__new__(TracebackException) + if java_error.getCause() is not None: + out.__cause__ = get_traceback_exception(java_error, unwrap_python_like_object(java_error.getCause(), clone_map), + clone_map) + else: + out.__cause__ = None + + out.__suppress_context__ = False + out.__context__ = None + out.__notes__ = None + out.exceptions = None + out.exc_type = python_exception_type + out.lineno = 0 + out.end_lineno = 0 + out.offset = 0 + out.end_offset = 0 + out.text = '' + out.msg = java_error.getMessage() + out.stack = StackSummary.extract(extract_frames_from_java_error(java_error)) + out._str = java_error.getMessage() + return out def get_translated_java_system_error_message(error): @@ -624,7 +662,16 @@ def throw(self, thrown): exception_python_type = getattr(builtins, exception_name) args = unwrap_python_like_object(getattr(python_like_object, '$getArgs')(), clone_map, default) - return clone_map.add_clone(python_like_object, exception_python_type(*args)) + traceback_exception = get_traceback_exception(python_like_object, exception_python_type, clone_map) + + class WrappedException(exception_python_type): + def __init__(self, *args): + super().__init__(*args) + + def __str__(self): + return '\n'.join(traceback_exception.format()) + + return clone_map.add_clone(python_like_object, WrappedException(*args)) except AttributeError: return clone_map.add_clone(python_like_object, TranslatedJavaSystemError(python_like_object)) elif isinstance(python_like_object, PythonLikeType): @@ -700,4 +747,4 @@ def unwrap_python_like_builtin_module_object(python_like_object, clone_map, defa unwrap_python_like_object(python_like_object.seconds, clone_map, default), unwrap_python_like_object(python_like_object.microseconds, clone_map, default))) - return None \ No newline at end of file + return None diff --git a/jpyinterpreter/src/main/python/translator.py b/jpyinterpreter/src/main/python/translator.py index 5a599966..fc93f5fa 100644 --- a/jpyinterpreter/src/main/python/translator.py +++ b/jpyinterpreter/src/main/python/translator.py @@ -18,6 +18,23 @@ function_interface_pair_to_class = dict() +def get_file_for_module(module): + import pathlib + import sys + + if module is None: + return '' + + file_path = sys.modules[module].__file__ + if file_path is not None: + return file_path + + # Do not know file for module; predict file from module name + path_parts = module.split('.') + path_parts[-1] = f'{path_parts[-1]}.py' + return str(pathlib.Path(path_parts)) + + def is_python_version_supported(python_version): python_version_major_minor = python_version[0:2] return MINIMUM_SUPPORTED_PYTHON_VERSION <= python_version_major_minor <= MAXIMUM_SUPPORTED_PYTHON_VERSION @@ -213,6 +230,7 @@ def get_function_bytecode_object(python_function): instruction_list.add(java_instruction) python_compiled_function.module = python_function.__module__ + python_compiled_function.moduleFilePath = get_file_for_module(python_function.__module__) python_compiled_function.qualifiedName = python_function.__qualname__ python_compiled_function.instructionList = instruction_list python_compiled_function.co_exceptiontable = get_python_exception_table(python_function.__code__) @@ -386,10 +404,19 @@ def wrapped_function(*args, **kwargs): java_kwargs.put(convert_to_java_python_like_object(key, instance_map), convert_to_java_python_like_object(value, instance_map)) + out = None + error = None + try: - return unwrap_python_like_object(getattr(java_function, '$call')(java_args, java_kwargs, None)) + out = unwrap_python_like_object(getattr(java_function, '$call')(java_args, java_kwargs, None)) except Exception as e: - raise unwrap_python_like_object(e) + error = unwrap_python_like_object(e) + + if error is not None: + raise error + + return out + return wrapped_function @@ -402,10 +429,18 @@ def wrapped_function(*args): instance_map = HashMap() java_args = [convert_to_java_python_like_object(arg, instance_map) for arg in args] + out = None + error = None + try: - return unwrap_python_like_object(java_function.invoke(*java_args)) + out = unwrap_python_like_object(java_function.invoke(*java_args)) except Exception as e: - raise unwrap_python_like_object(e) + error = unwrap_python_like_object(e) + + if error is not None: + raise error + + return out return wrapped_function @@ -595,6 +630,7 @@ def translate_python_class_to_java_class(python_class): python_compiled_class.binaryType = CPythonType.getType(JProxy(OpaquePythonReference, inst=python_class, convert=True)) python_compiled_class.module = python_class.__module__ + python_compiled_class.moduleFilePath = get_file_for_module(python_class.__module__) python_compiled_class.qualifiedName = python_class.__qualname__ python_compiled_class.className = python_class.__name__ python_compiled_class.typeAnnotations = copy_type_annotations(python_class, diff --git a/jpyinterpreter/tests/conftest.py b/jpyinterpreter/tests/conftest.py index 4cd11a91..20ae99a4 100644 --- a/jpyinterpreter/tests/conftest.py +++ b/jpyinterpreter/tests/conftest.py @@ -67,7 +67,7 @@ def verify_property(self, *args, predicate, clone_arguments=True): f'the property ({inspect.getsource(predicate)}) ' f'for arguments {args}.') elif not predicate(untyped_java_result): - raise AssertionError(f'Untyped translated bytecode result ({java_result}) does not satisfy the ' + raise AssertionError(f'Untyped translated bytecode result ({untyped_java_result}) does not satisfy the ' f'property ({inspect.getsource(predicate)}) ' f'for arguments {args}.') else: @@ -75,6 +75,51 @@ def verify_property(self, *args, predicate, clone_arguments=True): f'property ({inspect.getsource(predicate)}) ' f'for arguments {args}.') + def verify_error_property(self, *args, predicate, clone_arguments=True): + cloner = get_argument_cloner(clone_arguments) + try: + python_result = self.python_function(*cloner(args)) + raise AssertionError(f'Python function did not raise an error and returned ({python_result})') + except AssertionError: + raise + except Exception as python_result: + if not predicate(python_result): + import inspect + raise AssertionError(f'Python function error ({python_result}) does not satisfy the property ' + f'({inspect.getsource(predicate).strip()}) ' + f'for arguments {args}.') + + try: + java_result = self.java_function(*cloner(args)) + raise AssertionError(f'Typed Java function did not raise an error and returned ({java_result})') + except AssertionError: + raise + except Exception as err: + java_result = err + + try: + untyped_java_result = self.untyped_java_function(*cloner(args)) + raise AssertionError(f'Untyped Java function did not raise an error and returned ({untyped_java_result})') + except AssertionError: + raise + except Exception as err: + untyped_java_result = err + + if not predicate(java_result) or not predicate(untyped_java_result): + import inspect + if not predicate(java_result) and not predicate(untyped_java_result): + raise AssertionError(f'Typed and untyped translated bytecode error ({java_result}) does not satisfy ' + f'the property ({inspect.getsource(predicate)}) ' + f'for arguments {args}.') + elif not predicate(untyped_java_result): + raise AssertionError(f'Untyped translated bytecode error ({untyped_java_result}) does not satisfy the ' + f'property ({inspect.getsource(predicate)}) ' + f'for arguments {args}.') + else: + raise AssertionError(f'Typed translated bytecode error ({java_result}) does not satisfy the ' + f'property ({inspect.getsource(predicate)}) ' + f'for arguments {args}.') + def expect(self, expected, *args, clone_arguments=True, type_check=True): cloner = get_argument_cloner(clone_arguments) java_result = self.java_function(*cloner(args)) diff --git a/jpyinterpreter/tests/test_builtins.py b/jpyinterpreter/tests/test_builtins.py index d39628f4..cf26682d 100644 --- a/jpyinterpreter/tests/test_builtins.py +++ b/jpyinterpreter/tests/test_builtins.py @@ -2,9 +2,38 @@ import pytest import sys from typing import SupportsAbs, Iterable, Callable, Sequence, Union, Iterator, Sized, Reversible, SupportsIndex + +from jpype import JImplementationFor + from .conftest import verifier_for +# Workaround for https://github.com/jpype-project/jpype/issues/1178 +@JImplementationFor('java.lang.Throwable') +class _JavaException: + @staticmethod + def _get_exception_with_cause(exception): + if exception is None: + return None + try: + raise Exception(f'{exception.getClass().getSimpleName()}: {exception.getMessage()}') + except Exception as e: + cause = _JavaException._get_exception_with_cause(exception.getCause()) + if cause is not None: + try: + raise e from cause + except Exception as return_value: + return return_value + else: + return e + @property + def __cause__(self): + if self.getCause() is not None: + return _JavaException._get_exception_with_cause(self.getCause()) + else: + return None + + def test_abs(): def my_function(x: SupportsAbs) -> object: return abs(x) @@ -377,7 +406,7 @@ def my_function(): with pytest.raises(ValueError) as excinfo: java_function() - assert 'builtin locals is not supported when executed in Java bytecode' == str(excinfo.value) + assert 'builtin locals is not supported when executed in Java bytecode' in str(excinfo.value) def test_map(): @@ -748,4 +777,5 @@ def my_function() -> exception_class: verifier = verifier_for(my_function) verifier.verify_property(predicate= - lambda error: type(error) == exception_class and error.args == ('my argument',)) + lambda error: isinstance(error, exception_class) and len(error.args) == 1 and + 'my argument' in error.args[0]) diff --git a/jpyinterpreter/tests/test_traceback.py b/jpyinterpreter/tests/test_traceback.py new file mode 100644 index 00000000..52e925e3 --- /dev/null +++ b/jpyinterpreter/tests/test_traceback.py @@ -0,0 +1,28 @@ +from .conftest import verifier_for + +def test_function_traceback(): + def my_function_1(): + my_function_2() + + def my_function_2(): + raise Exception('Message') + + def check_traceback(error: Exception): + from traceback import format_exception + traceback = '\n'.join(format_exception(type(error), error, error.__traceback__)) + if 'test_traceback.py", line 5, in my_function_1\n' not in traceback: + return False + + if 'test_traceback.py", line 8, in my_function_2\n' not in traceback: + return False + + if not traceback.strip().endswith('Exception: Message'): + return False + + if 'File "PythonException.java"' in traceback: + return False + + return True + + verifier = verifier_for(my_function_1) + verifier.verify_error_property(predicate=check_traceback) From 6d6c188b92818d3509e257a80c798dcf9e43077b Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Wed, 3 Apr 2024 21:16:08 -0400 Subject: [PATCH 2/6] chore: Use StackSummary.from_list --- jpyinterpreter/src/main/python/conversions.py | 20 +++---------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/jpyinterpreter/src/main/python/conversions.py b/jpyinterpreter/src/main/python/conversions.py index f381df3e..d8c6ef3a 100644 --- a/jpyinterpreter/src/main/python/conversions.py +++ b/jpyinterpreter/src/main/python/conversions.py @@ -1,8 +1,7 @@ import builtins import inspect -import sys from dataclasses import dataclass -from typing import TYPE_CHECKING, Optional, Generator +from typing import TYPE_CHECKING from traceback import TracebackException, StackSummary, FrameSummary from jpype import JLong, JDouble, JBoolean, JProxy @@ -13,19 +12,6 @@ from java.lang import Throwable -@dataclass -class FakeFrame: - f_code: 'FakeCode' - f_globals: dict - f_locals: dict - - -@dataclass -class FakeCode: - co_filename: str - co_name: str - - def extract_frames_from_java_error(java_error: 'Throwable'): stack_trace = java_error.getStackTrace() start_index = 0 @@ -47,7 +33,7 @@ def extract_frames_from_java_error(java_error: 'Throwable'): else: function_name = stack_trace_element.getMethodName() or '' line_number = stack_trace_element.getLineNumber() or 0 - yield FakeFrame(FakeCode(file_name, function_name), {}, {}), line_number + yield FrameSummary(file_name, line_number, function_name) def get_traceback_exception(java_error: 'Throwable', python_exception_type: type, clone_map: 'PythonCloneMap') -> TracebackException: @@ -69,7 +55,7 @@ def get_traceback_exception(java_error: 'Throwable', python_exception_type: type out.end_offset = 0 out.text = '' out.msg = java_error.getMessage() - out.stack = StackSummary.extract(extract_frames_from_java_error(java_error)) + out.stack = StackSummary.from_list(extract_frames_from_java_error(java_error)) out._str = java_error.getMessage() return out From 12df8fbbef737ec6a47fa7065e24e49b7efac02a Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Wed, 3 Apr 2024 21:54:12 -0400 Subject: [PATCH 3/6] fix: Make exceptions work in Python REPL, update format of exception --- jpyinterpreter/src/main/python/conversions.py | 2 +- jpyinterpreter/src/main/python/translator.py | 19 ++++++++++++------- .../src/main/python/timefold_api_wrappers.py | 4 +--- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/jpyinterpreter/src/main/python/conversions.py b/jpyinterpreter/src/main/python/conversions.py index d8c6ef3a..b44cda8c 100644 --- a/jpyinterpreter/src/main/python/conversions.py +++ b/jpyinterpreter/src/main/python/conversions.py @@ -655,7 +655,7 @@ def __init__(self, *args): super().__init__(*args) def __str__(self): - return '\n'.join(traceback_exception.format()) + return ''.join(traceback_exception.format()) return clone_map.add_clone(python_like_object, WrappedException(*args)) except AttributeError: diff --git a/jpyinterpreter/src/main/python/translator.py b/jpyinterpreter/src/main/python/translator.py index fc93f5fa..90c399e0 100644 --- a/jpyinterpreter/src/main/python/translator.py +++ b/jpyinterpreter/src/main/python/translator.py @@ -18,21 +18,26 @@ function_interface_pair_to_class = dict() -def get_file_for_module(module): +def get_file_for_module(module_name): import pathlib import sys - if module is None: + if module_name is None: return '' - file_path = sys.modules[module].__file__ - if file_path is not None: - return file_path + module = sys.modules[module_name] + if hasattr(module, '__file__'): + file_path = sys.modules[module_name].__file__ + if file_path is not None: + return file_path # Do not know file for module; predict file from module name - path_parts = module.split('.') + if module_name == '__main__': + return '' + + path_parts = module_name.split('.') path_parts[-1] = f'{path_parts[-1]}.py' - return str(pathlib.Path(path_parts)) + return str(pathlib.Path(*path_parts)) def is_python_version_supported(python_version): diff --git a/timefold-solver-python-core/src/main/python/timefold_api_wrappers.py b/timefold-solver-python-core/src/main/python/timefold_api_wrappers.py index c761471a..ac9f0549 100644 --- a/timefold-solver-python-core/src/main/python/timefold_api_wrappers.py +++ b/timefold-solver-python-core/src/main/python/timefold_api_wrappers.py @@ -306,9 +306,7 @@ def solve(self, problem: Solution_): try: java_solution = self._delegate.solve(java_problem) except PythonBaseException as e: - python_error = unwrap_python_like_object(e) - raise RuntimeError(f'Solving failed due to an error: {e.getMessage()}.\n' - f'Java stack trace: {e.stacktrace()}') from python_error + raise unwrap_python_like_object(e) except JavaException as e: raise RuntimeError(f'Solving failed due to an error: {e.getMessage()}.\n' f'Java stack trace: {e.stacktrace()}') from e From 440c021557c275bb3791365b5887323c2aebc9ff Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Thu, 4 Apr 2024 13:44:17 -0400 Subject: [PATCH 4/6] fix: Include cause infomation in stack trace - Python 3.10 has a quirk: it calls RERAISE in the finally block corresponding to an except block. That finally immediately calls RERAISE, but TOS is a type, so that exception loses both it cause and message. So work around this, we deviate a bit from how the code apparenty works - After finally branch is taken, stack is traceback, exeception, exception instead of traceback, exeception, type - JUMP_IF_NOT_EXEC_MATCH is now an instanceof instead of issubclass This works since Python pops off all these values when actually entering the code for an except block, and JUMP_IF_NOT_EXEC_MATCH is the only opcode that can be encountered. - Fix a bug in generating traceback --- .../implementors/ExceptionImplementor.java | 43 +++++----- .../implementors/JumpImplementor.java | 6 +- .../exceptions/SetupFinallyOpcode.java | 5 +- jpyinterpreter/src/main/python/conversions.py | 11 +-- .../jpyinterpreter/dag/FlowGraphTest.java | 81 ++++++++++--------- .../jpyinterpreter/util/ExceptBuilder.java | 3 - .../util/PythonFunctionBuilder.java | 2 +- jpyinterpreter/tests/test_traceback.py | 72 +++++++++++++++++ 8 files changed, 153 insertions(+), 70 deletions(-) diff --git a/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/implementors/ExceptionImplementor.java b/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/implementors/ExceptionImplementor.java index 04325f51..727b9fa2 100644 --- a/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/implementors/ExceptionImplementor.java +++ b/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/implementors/ExceptionImplementor.java @@ -141,11 +141,13 @@ public static void raiseWithOptionalExceptionAndCause(MethodVisitor methodVisito * are handled via the {@link ControlOpDescriptor#JUMP_IF_NOT_EXC_MATCH} instruction. * {@code instruction.arg} is the difference in bytecode offset to the first catch/finally block. */ - public static void createTryFinallyBlock(MethodVisitor methodVisitor, String className, - int handlerLocation, + public static void createTryFinallyBlock(FunctionMetadata functionMetadata, StackMetadata stackMetadata, + int handlerLocation, Map bytecodeCounterToLabelMap, BiConsumer bytecodeCounterCodeArgumentConsumer) { + var methodVisitor = functionMetadata.methodVisitor; + var className = functionMetadata.className; // Store the stack in local variables so the except block has access to them int[] stackLocals = StackManipulationImplementor.storeStack(methodVisitor, stackMetadata); @@ -159,7 +161,10 @@ public static void createTryFinallyBlock(MethodVisitor methodVisitor, String cla methodVisitor.visitLabel(tryStart); // At finallyStart, stack is expected to be: - // [(stack-before-try), instruction, level, label, tb, exception, exception_class] ; where: + // in Python 3.10 and below + // [(stack-before-try), instruction, level, label, tb, value, exception] + // or in Python 3.11 and above + // [(stack-before-try), instruction, level, label, tb, value, exception] ; where: // (stack-before-try) = the stack state before the try statement // (see https://github.com/python/cpython/blob/b6558d768f19584ad724be23030603280f9e6361/Python/compile.c#L3241-L3268 ) // instruction = instruction that created the block @@ -168,7 +173,11 @@ public static void createTryFinallyBlock(MethodVisitor methodVisitor, String cla // (see https://stackoverflow.com/a/66720684) // tb = stack trace // exception = exception instance - // exception_class = the exception class + // value = the exception instance again? + // Results from Python 3.10 seems to indicate both exception and value are exception + // instances, since Python 3.10 use RERAISE on exception (TOS) + // and stores value into the exception variable (TOS1) + // Python 3.11 and above use a different code path bytecodeCounterCodeArgumentConsumer.accept(handlerLocation, () -> { // Stack is exception // Duplicate exception to the current exception variable slot so we can reraise it if needed @@ -188,13 +197,12 @@ public static void createTryFinallyBlock(MethodVisitor methodVisitor, String cla "valueOf", Type.getMethodDescriptor(Type.getType(PythonInteger.class), Type.INT_TYPE), false); - // Stack is (stack-before-try), instruction, stack-size, exception + // Stack is (stack-before-try), instruction, stack-size // Label PythonConstantsImplementor.loadNone(methodVisitor); // We don't use it // Stack is (stack-before-try), instruction, stack-size, label - methodVisitor.visitVarInsn(Opcodes.ALOAD, 0); methodVisitor.visitTypeInsn(Opcodes.CHECKCAST, className); // needed cast; type confusion on this? methodVisitor.visitFieldInsn(Opcodes.GETFIELD, className, @@ -212,13 +220,9 @@ public static void createTryFinallyBlock(MethodVisitor methodVisitor, String cla // Stack is (stack-before-try), instruction, stack-size, label, traceback, exception - // Get exception class + // Get exception value methodVisitor.visitInsn(Opcodes.DUP); - methodVisitor.visitMethodInsn(Opcodes.INVOKEINTERFACE, Type.getInternalName(PythonLikeObject.class), - "$getType", Type.getMethodDescriptor(Type.getType(PythonLikeType.class)), - true); - - // Stack is (stack-before-try), instruction, stack-size, label, traceback, exception, exception_class + // Stack is (stack-before-try), instruction, stack-size, label, traceback, value, exception }); } @@ -285,8 +289,7 @@ public static void startWith(int jumpTarget, FunctionMetadata functionMetadata, .push(ValueSourceInfo.of(new OpcodeWithoutSource(), PythonLikeFunction.getFunctionType(), stackMetadata.getTOSValueSource())); - createTryFinallyBlock(methodVisitor, functionMetadata.className, jumpTarget, - currentStackMetadata, + createTryFinallyBlock(functionMetadata, currentStackMetadata, jumpTarget, functionMetadata.bytecodeCounterToLabelMap, (bytecodeIndex, runnable) -> { functionMetadata.bytecodeCounterToCodeArgumenterList @@ -351,16 +354,16 @@ public static void handleExceptionInWith(FunctionMetadata functionMetadata, Stac LocalVariableHelper localVariableHelper = stackMetadata.localVariableHelper; // First, store the top 7 items in the stack to be restored later - int exceptionType = localVariableHelper.newLocal(); int exception = localVariableHelper.newLocal(); + int exceptionArgs = localVariableHelper.newLocal(); int traceback = localVariableHelper.newLocal(); int label = localVariableHelper.newLocal(); int stackSize = localVariableHelper.newLocal(); int instruction = localVariableHelper.newLocal(); int exitFunction = localVariableHelper.newLocal(); - localVariableHelper.writeTemp(methodVisitor, Type.getType(PythonLikeObject.class), exceptionType); localVariableHelper.writeTemp(methodVisitor, Type.getType(PythonLikeObject.class), exception); + localVariableHelper.writeTemp(methodVisitor, Type.getType(PythonLikeObject.class), exceptionArgs); localVariableHelper.writeTemp(methodVisitor, Type.getType(PythonLikeObject.class), traceback); localVariableHelper.writeTemp(methodVisitor, Type.getType(PythonLikeObject.class), label); localVariableHelper.writeTemp(methodVisitor, Type.getType(PythonLikeObject.class), stackSize); @@ -381,7 +384,9 @@ public static void handleExceptionInWith(FunctionMetadata functionMetadata, Stac false); methodVisitor.visitInsn(Opcodes.DUP); - localVariableHelper.readTemp(methodVisitor, Type.getType(PythonLikeObject.class), exceptionType); + localVariableHelper.readTemp(methodVisitor, Type.getType(PythonLikeObject.class), exception); + methodVisitor.visitMethodInsn(Opcodes.INVOKEINTERFACE, Type.getInternalName(PythonLikeObject.class), + "$getType", Type.getMethodDescriptor(Type.getType(PythonLikeType.class)), true); methodVisitor.visitMethodInsn(Opcodes.INVOKEINTERFACE, Type.getInternalName(Collection.class), "add", Type.getMethodDescriptor(Type.BOOLEAN_TYPE, Type.getType(Object.class)), true); @@ -431,10 +436,10 @@ public static void handleExceptionInWith(FunctionMetadata functionMetadata, Stac localVariableHelper.readTemp(methodVisitor, Type.getType(PythonLikeObject.class), traceback); methodVisitor.visitInsn(Opcodes.SWAP); - localVariableHelper.readTemp(methodVisitor, Type.getType(PythonLikeObject.class), exception); + localVariableHelper.readTemp(methodVisitor, Type.getType(PythonLikeObject.class), exceptionArgs); methodVisitor.visitInsn(Opcodes.SWAP); - localVariableHelper.readTemp(methodVisitor, Type.getType(PythonLikeObject.class), exceptionType); + localVariableHelper.readTemp(methodVisitor, Type.getType(PythonLikeObject.class), exception); methodVisitor.visitInsn(Opcodes.SWAP); // Free the 7 temps diff --git a/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/implementors/JumpImplementor.java b/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/implementors/JumpImplementor.java index 587121ad..c37ab3e2 100644 --- a/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/implementors/JumpImplementor.java +++ b/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/implementors/JumpImplementor.java @@ -3,6 +3,7 @@ import java.util.Map; import ai.timefold.jpyinterpreter.FunctionMetadata; +import ai.timefold.jpyinterpreter.PythonLikeObject; import ai.timefold.jpyinterpreter.PythonUnaryOperator; import ai.timefold.jpyinterpreter.StackMetadata; import ai.timefold.jpyinterpreter.types.BuiltinTypes; @@ -83,7 +84,8 @@ public static void popAndJumpIfIsNone(FunctionMetadata functionMetadata, StackMe } /** - * TOS and TOS1 are an exception types. If TOS1 is not an instance of TOS, set the bytecode counter to the + * TOS is an exception type and TOS1 is an exception. + * If TOS1 is not an instance of TOS, set the bytecode counter to the * {@code instruction} argument. * Pop TOS and TOS1 off the stack. */ @@ -95,6 +97,8 @@ public static void popAndJumpIfExceptionDoesNotMatch(FunctionMetadata functionMe methodVisitor.visitTypeInsn(Opcodes.CHECKCAST, Type.getInternalName(PythonLikeType.class)); StackManipulationImplementor.swap(methodVisitor); + methodVisitor.visitMethodInsn(Opcodes.INVOKEINTERFACE, Type.getInternalName(PythonLikeObject.class), + "$getType", Type.getMethodDescriptor(Type.getType(PythonLikeType.class)), true); methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Type.getInternalName(PythonLikeType.class), "isSubclassOf", Type.getMethodDescriptor(Type.BOOLEAN_TYPE, Type.getType(PythonLikeType.class)), false); diff --git a/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/opcodes/exceptions/SetupFinallyOpcode.java b/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/opcodes/exceptions/SetupFinallyOpcode.java index 55ad3ad6..11f270e2 100644 --- a/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/opcodes/exceptions/SetupFinallyOpcode.java +++ b/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/opcodes/exceptions/SetupFinallyOpcode.java @@ -36,14 +36,13 @@ public List getStackMetadataAfterInstructionForBranches(FunctionM .pushTemp(BuiltinTypes.NONE_TYPE) .pushTemp(PythonTraceback.TRACEBACK_TYPE) .pushTemp(PythonBaseException.BASE_EXCEPTION_TYPE) - .pushTemp(BuiltinTypes.TYPE_TYPE)); + .pushTemp(PythonBaseException.BASE_EXCEPTION_TYPE)); } @Override public void implement(FunctionMetadata functionMetadata, StackMetadata stackMetadata) { - ExceptionImplementor.createTryFinallyBlock(functionMetadata.methodVisitor, functionMetadata.className, + ExceptionImplementor.createTryFinallyBlock(functionMetadata, stackMetadata, jumpTarget, - stackMetadata, functionMetadata.bytecodeCounterToLabelMap, (bytecodeIndex, runnable) -> { functionMetadata.bytecodeCounterToCodeArgumenterList diff --git a/jpyinterpreter/src/main/python/conversions.py b/jpyinterpreter/src/main/python/conversions.py index b44cda8c..6b36afec 100644 --- a/jpyinterpreter/src/main/python/conversions.py +++ b/jpyinterpreter/src/main/python/conversions.py @@ -39,7 +39,9 @@ def extract_frames_from_java_error(java_error: 'Throwable'): def get_traceback_exception(java_error: 'Throwable', python_exception_type: type, clone_map: 'PythonCloneMap') -> TracebackException: out = object.__new__(TracebackException) if java_error.getCause() is not None: - out.__cause__ = get_traceback_exception(java_error, unwrap_python_like_object(java_error.getCause(), clone_map), + out.__cause__ = get_traceback_exception(java_error.getCause(), + unwrap_python_like_object(java_error.getCause(), + clone_map).__class__.__bases__[0], clone_map) else: out.__cause__ = None @@ -49,12 +51,6 @@ def get_traceback_exception(java_error: 'Throwable', python_exception_type: type out.__notes__ = None out.exceptions = None out.exc_type = python_exception_type - out.lineno = 0 - out.end_lineno = 0 - out.offset = 0 - out.end_offset = 0 - out.text = '' - out.msg = java_error.getMessage() out.stack = StackSummary.from_list(extract_frames_from_java_error(java_error)) out._str = java_error.getMessage() return out @@ -651,6 +647,7 @@ def throw(self, thrown): traceback_exception = get_traceback_exception(python_like_object, exception_python_type, clone_map) class WrappedException(exception_python_type): + wrapped_type: type def __init__(self, *args): super().__init__(*args) diff --git a/jpyinterpreter/src/test/java/ai/timefold/jpyinterpreter/dag/FlowGraphTest.java b/jpyinterpreter/src/test/java/ai/timefold/jpyinterpreter/dag/FlowGraphTest.java index 9a082360..1c3468a2 100644 --- a/jpyinterpreter/src/test/java/ai/timefold/jpyinterpreter/dag/FlowGraphTest.java +++ b/jpyinterpreter/src/test/java/ai/timefold/jpyinterpreter/dag/FlowGraphTest.java @@ -217,16 +217,20 @@ public void testStackMetadataForExceptions() { new FrameData(9).stack(), // LOAD_ASSERTION_ERROR new FrameData(10).stack(PythonAssertionError.ASSERTION_ERROR_TYPE), // RAISE new FrameData(11).stack(BuiltinTypes.NONE_TYPE, BuiltinTypes.INT_TYPE, BuiltinTypes.NONE_TYPE, - PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE, BuiltinTypes.TYPE_TYPE), // except handler; DUP_TOP, + PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE, + PythonBaseException.BASE_EXCEPTION_TYPE), // except handler; DUP_TOP, new FrameData(12).stack(BuiltinTypes.NONE_TYPE, BuiltinTypes.INT_TYPE, BuiltinTypes.NONE_TYPE, - PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE, BuiltinTypes.TYPE_TYPE, - BuiltinTypes.TYPE_TYPE), // LOAD_CONSTANT + PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE, + PythonBaseException.BASE_EXCEPTION_TYPE, + PythonBaseException.BASE_EXCEPTION_TYPE), // LOAD_CONSTANT new FrameData(13).stack(BuiltinTypes.NONE_TYPE, BuiltinTypes.INT_TYPE, BuiltinTypes.NONE_TYPE, - PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE, BuiltinTypes.TYPE_TYPE, - BuiltinTypes.TYPE_TYPE, + PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE, + PythonBaseException.BASE_EXCEPTION_TYPE, + PythonBaseException.BASE_EXCEPTION_TYPE, BuiltinTypes.TYPE_TYPE), // JUMP_IF_NOT_EXC_MATCH new FrameData(14).stack(BuiltinTypes.NONE_TYPE, BuiltinTypes.INT_TYPE, BuiltinTypes.NONE_TYPE, - PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE, BuiltinTypes.TYPE_TYPE), // POP_TOP + PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE, + PythonBaseException.BASE_EXCEPTION_TYPE), // POP_TOP new FrameData(15).stack(BuiltinTypes.NONE_TYPE, BuiltinTypes.INT_TYPE, BuiltinTypes.NONE_TYPE, PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE), // POP_TOP new FrameData(16).stack(BuiltinTypes.NONE_TYPE, BuiltinTypes.INT_TYPE, BuiltinTypes.NONE_TYPE, @@ -236,13 +240,13 @@ public void testStackMetadataForExceptions() { new FrameData(19).stack(BuiltinTypes.STRING_TYPE), // RETURN new FrameData(20).stack(BuiltinTypes.NONE_TYPE, BuiltinTypes.INT_TYPE, BuiltinTypes.NONE_TYPE, PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE, - BuiltinTypes.TYPE_TYPE), // POP_TOP + PythonBaseException.BASE_EXCEPTION_TYPE), // POP_TOP new FrameData(21).stack(BuiltinTypes.NONE_TYPE, BuiltinTypes.INT_TYPE, BuiltinTypes.NONE_TYPE, - PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE, BuiltinTypes.TYPE_TYPE), // POP_TOP + PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE, + PythonBaseException.BASE_EXCEPTION_TYPE), // POP_TOP new FrameData(22).stack(BuiltinTypes.NONE_TYPE, BuiltinTypes.INT_TYPE, BuiltinTypes.NONE_TYPE, - PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE), // RERAISE - new FrameData(23).stack(BuiltinTypes.NONE_TYPE, BuiltinTypes.INT_TYPE, BuiltinTypes.NONE_TYPE, - PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE, BuiltinTypes.TYPE_TYPE) // RERAISE + PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE, + PythonBaseException.BASE_EXCEPTION_TYPE) // RERAISE ); } @@ -301,16 +305,20 @@ public void testStackMetadataForTryFinally() { new FrameData(15).stack(), // NOP new FrameData(16).stack(), // JUMP_ABSOLUTE new FrameData(17).stack(BuiltinTypes.NONE_TYPE, BuiltinTypes.INT_TYPE, BuiltinTypes.NONE_TYPE, - PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE, BuiltinTypes.TYPE_TYPE), // except handler; DUP_TOP, + PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE, + PythonBaseException.BASE_EXCEPTION_TYPE), // except handler; DUP_TOP, new FrameData(18).stack(BuiltinTypes.NONE_TYPE, BuiltinTypes.INT_TYPE, BuiltinTypes.NONE_TYPE, - PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE, BuiltinTypes.TYPE_TYPE, - BuiltinTypes.TYPE_TYPE), // LOAD_CONSTANT + PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE, + PythonBaseException.BASE_EXCEPTION_TYPE, + PythonBaseException.BASE_EXCEPTION_TYPE), // LOAD_CONSTANT new FrameData(19).stack(BuiltinTypes.NONE_TYPE, BuiltinTypes.INT_TYPE, BuiltinTypes.NONE_TYPE, - PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE, BuiltinTypes.TYPE_TYPE, - BuiltinTypes.TYPE_TYPE, + PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE, + PythonBaseException.BASE_EXCEPTION_TYPE, + PythonBaseException.BASE_EXCEPTION_TYPE, BuiltinTypes.TYPE_TYPE), // JUMP_IF_NOT_EXC_MATCH new FrameData(20).stack(BuiltinTypes.NONE_TYPE, BuiltinTypes.INT_TYPE, BuiltinTypes.NONE_TYPE, - PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE, BuiltinTypes.TYPE_TYPE), // POP_TOP + PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE, + PythonBaseException.BASE_EXCEPTION_TYPE), // POP_TOP new FrameData(21).stack(BuiltinTypes.NONE_TYPE, BuiltinTypes.INT_TYPE, BuiltinTypes.NONE_TYPE, PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE), // POP_TOP new FrameData(22).stack(BuiltinTypes.NONE_TYPE, BuiltinTypes.INT_TYPE, BuiltinTypes.NONE_TYPE, @@ -320,30 +328,31 @@ public void testStackMetadataForTryFinally() { new FrameData(25).stack(BuiltinTypes.STRING_TYPE), // STORE_GLOBAL new FrameData(26).stack(), // JUMP_ABSOLUTE new FrameData(27).stack(BuiltinTypes.NONE_TYPE, BuiltinTypes.INT_TYPE, BuiltinTypes.NONE_TYPE, - PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE, BuiltinTypes.TYPE_TYPE), // RERAISE + PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE, + PythonBaseException.BASE_EXCEPTION_TYPE), // RERAISE new FrameData(28).stack(BuiltinTypes.NONE_TYPE, BuiltinTypes.INT_TYPE, BuiltinTypes.NONE_TYPE, - PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE, BuiltinTypes.TYPE_TYPE), // POP_TOP - new FrameData(29).stack(BuiltinTypes.NONE_TYPE, BuiltinTypes.INT_TYPE, BuiltinTypes.NONE_TYPE, - PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE), // POP_TOP + PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE, + PythonBaseException.BASE_EXCEPTION_TYPE), // POP_TOP + new FrameData(29).stack(), // POP_TOP new FrameData(30).stack(), // POP_TOP - new FrameData(31).stack(), // Load constant - new FrameData(32).stack(BuiltinTypes.STRING_TYPE), // STORE - new FrameData(33).stack(), // JUMP_ABSOLUTE + new FrameData(31).stack(BuiltinTypes.STRING_TYPE), // STORE + new FrameData(32).stack(), // JUMP_ABSOLUTE + new FrameData(33).stack(BuiltinTypes.NONE_TYPE, BuiltinTypes.INT_TYPE, BuiltinTypes.NONE_TYPE, + PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE, + PythonBaseException.BASE_EXCEPTION_TYPE), // NO-OP; Uncaught exception handler new FrameData(34).stack(BuiltinTypes.NONE_TYPE, BuiltinTypes.INT_TYPE, BuiltinTypes.NONE_TYPE, - PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE, BuiltinTypes.TYPE_TYPE), // NO-OP; Uncaught exception handler + PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE, + PythonBaseException.BASE_EXCEPTION_TYPE), // LOAD-CONSTANT new FrameData(35).stack(BuiltinTypes.NONE_TYPE, BuiltinTypes.INT_TYPE, BuiltinTypes.NONE_TYPE, - PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE, BuiltinTypes.TYPE_TYPE), // LOAD-CONSTANT - new FrameData(36).stack(BuiltinTypes.NONE_TYPE, BuiltinTypes.INT_TYPE, BuiltinTypes.NONE_TYPE, - PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE, BuiltinTypes.TYPE_TYPE, + PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE, + PythonBaseException.BASE_EXCEPTION_TYPE, BuiltinTypes.STRING_TYPE), // STORE - new FrameData(37).stack(BuiltinTypes.NONE_TYPE, BuiltinTypes.INT_TYPE, BuiltinTypes.NONE_TYPE, - PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE, BuiltinTypes.TYPE_TYPE), // POP-TOP - new FrameData(38).stack(BuiltinTypes.NONE_TYPE, BuiltinTypes.INT_TYPE, BuiltinTypes.NONE_TYPE, - PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE), // RERAISE - new FrameData(39).stack(), // NO-OP; After try - new FrameData(40).stack(), // LOAD_CONSTANT - new FrameData(41).stack(BuiltinTypes.INT_TYPE) // RETURN - ); + new FrameData(36).stack(BuiltinTypes.NONE_TYPE, BuiltinTypes.INT_TYPE, BuiltinTypes.NONE_TYPE, + PythonTraceback.TRACEBACK_TYPE, PythonBaseException.BASE_EXCEPTION_TYPE, + PythonBaseException.BASE_EXCEPTION_TYPE), // POP-TOP + new FrameData(37).stack(), // RERAISE + new FrameData(38).stack(), // NO-OP; After try + new FrameData(39).stack(BuiltinTypes.INT_TYPE)); } @Test diff --git a/jpyinterpreter/src/test/java/ai/timefold/jpyinterpreter/util/ExceptBuilder.java b/jpyinterpreter/src/test/java/ai/timefold/jpyinterpreter/util/ExceptBuilder.java index aa7d1406..c2da00a4 100644 --- a/jpyinterpreter/src/test/java/ai/timefold/jpyinterpreter/util/ExceptBuilder.java +++ b/jpyinterpreter/src/test/java/ai/timefold/jpyinterpreter/util/ExceptBuilder.java @@ -96,7 +96,6 @@ public ExceptBuilder andFinally(Consumer finallyBuilder, .markAsJumpTarget(); delegate.instructionList.add(exceptGotoTarget); - delegate.op(StackOpDescriptor.POP_TOP); delegate.op(ExceptionOpDescriptor.RERAISE, 0); if (tryEndGoto != null) { @@ -127,7 +126,6 @@ public ExceptBuilder andFinally(Consumer finallyBuilder, finallyBuilder.accept(delegate); - delegate.op(StackOpDescriptor.POP_TOP); delegate.op(ExceptionOpDescriptor.RERAISE); delegate.update(finallyEndInstruction.withArg(delegate.instructionList.size())); @@ -150,7 +148,6 @@ public PythonFunctionBuilder tryEnd() { .markAsJumpTarget(); delegate.instructionList.add(exceptGotoTarget); - delegate.op(StackOpDescriptor.POP_TOP); delegate.op(ExceptionOpDescriptor.RERAISE, 0); } diff --git a/jpyinterpreter/src/test/java/ai/timefold/jpyinterpreter/util/PythonFunctionBuilder.java b/jpyinterpreter/src/test/java/ai/timefold/jpyinterpreter/util/PythonFunctionBuilder.java index 765fd7cf..bb2ca73d 100644 --- a/jpyinterpreter/src/test/java/ai/timefold/jpyinterpreter/util/PythonFunctionBuilder.java +++ b/jpyinterpreter/src/test/java/ai/timefold/jpyinterpreter/util/PythonFunctionBuilder.java @@ -321,7 +321,7 @@ public PythonFunctionBuilder with(Consumer blockBuilder) update(instruction.withArg(exceptionHandler.offset() - instruction.offset() - 1)); ifFalse(reraiseExceptionBlock -> reraiseExceptionBlock - .op(StackOpDescriptor.POP_TOP).op(ExceptionOpDescriptor.RERAISE)); + .op(ExceptionOpDescriptor.RERAISE)); op(StackOpDescriptor.POP_TOP); op(StackOpDescriptor.POP_TOP); diff --git a/jpyinterpreter/tests/test_traceback.py b/jpyinterpreter/tests/test_traceback.py index 52e925e3..4ac2ac6c 100644 --- a/jpyinterpreter/tests/test_traceback.py +++ b/jpyinterpreter/tests/test_traceback.py @@ -26,3 +26,75 @@ def check_traceback(error: Exception): verifier = verifier_for(my_function_1) verifier.verify_error_property(predicate=check_traceback) + + +def test_class_traceback(): + class A: + def my_function(self): + raise ValueError('Message') + + def check_traceback(error: Exception): + from traceback import format_exception + traceback = '\n'.join(format_exception(type(error), error, error.__traceback__)) + if 'test_traceback.py", line 34, in my_function\n' not in traceback: + return False + + if not traceback.strip().endswith('ValueError: Message'): + return False + + if 'File "PythonException.java"' in traceback: + return False + + return True + + def call_class_function(): + return A().my_function() + + verifier = verifier_for(call_class_function) + verifier.verify_error_property(predicate=check_traceback) + + +def test_chained_traceback(): + def first(): + return second() + + def second(): + try: + return third() + except ValueError as e: + raise RuntimeError('Consequence') from e + + def third(): + raise ValueError("Cause") + + def check_traceback(error: Exception): + from traceback import format_exception + traceback = '\n'.join(format_exception(type(error), error, error.__traceback__)) + if 'test_traceback.py", line 59, in first\n' not in traceback: + return False + + if 'test_traceback.py", line 63, in second\n' not in traceback: + return False + + if 'ValueError: Cause' not in traceback: + return False + + if 'The above exception was the direct cause of the following exception:\n' not in traceback: + return False + + if 'test_traceback.py", line 65, in second\n' not in traceback: + return False + + if 'test_traceback.py", line 68, in third\n' not in traceback: + return False + + if not traceback.strip().endswith('RuntimeError: Consequence'): + return False + + if 'File "PythonException.java"' in traceback: + return False + + return True + + verifier = verifier_for(first) + verifier.verify_error_property(predicate=check_traceback) From e11fbbb1b4a3bdabf68200e524bbea7013fdab96 Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Fri, 5 Apr 2024 13:13:27 -0400 Subject: [PATCH 5/6] chore: Make it more clear that locals refer to the built-in function --- .../ai/timefold/jpyinterpreter/builtins/GlobalBuiltins.java | 2 +- jpyinterpreter/tests/test_builtins.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/builtins/GlobalBuiltins.java b/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/builtins/GlobalBuiltins.java index e0721af4..38e7e248 100644 --- a/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/builtins/GlobalBuiltins.java +++ b/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/builtins/GlobalBuiltins.java @@ -1085,7 +1085,7 @@ public static PythonBoolean issubclass(List positionalArgs, public static PythonLikeDict locals(List positionalArgs, Map keywordArgs, PythonLikeObject instance) { - throw new ValueError("builtin locals is not supported when executed in Java bytecode"); + throw new ValueError("builtin locals() is not supported when executed in Java bytecode"); } public static PythonIterator map(List positionalArgs, diff --git a/jpyinterpreter/tests/test_builtins.py b/jpyinterpreter/tests/test_builtins.py index cf26682d..b210b846 100644 --- a/jpyinterpreter/tests/test_builtins.py +++ b/jpyinterpreter/tests/test_builtins.py @@ -406,7 +406,7 @@ def my_function(): with pytest.raises(ValueError) as excinfo: java_function() - assert 'builtin locals is not supported when executed in Java bytecode' in str(excinfo.value) + assert 'builtin locals() is not supported when executed in Java bytecode' in str(excinfo.value) def test_map(): From b91c031b6ca6ed92cdb09dfbad953251327c3f7d Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Fri, 5 Apr 2024 13:22:58 -0400 Subject: [PATCH 6/6] ci: Update CI after CI changes in Timefold Solver --- .github/workflows/pull_request.yml | 31 +++++++++++++----------------- .github/workflows/sonarcloud.yml | 31 +++++++++++++----------------- 2 files changed, 26 insertions(+), 36 deletions(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 48150795..79a66971 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -29,28 +29,23 @@ jobs: steps: # Need to check for stale repo, since Github is not aware of the build chain and therefore doesn't automate it. - - name: Checkout timefold-solver to access the scripts + - name: Checkout timefold-solver (PR) # Checkout the PR branch first, if it exists + id: checkout-solver uses: actions/checkout@v4 + continue-on-error: true with: - path: './timefold-solver' - repository: 'TimefoldAI/timefold-solver' - - name: Find the proper timefold-solver repo and branch - env: - CHAIN_USER: ${{ github.event.pull_request.head.repo.owner.login }} - CHAIN_BRANCH: ${{ github.head_ref }} - CHAIN_REPO: "timefold-solver" - CHAIN_DEFAULT_BRANCH: ${{ endsWith(github.head_ref, '.x') && github.head_ref || 'main' }} - shell: bash - run: | - ./timefold-solver/.github/scripts/check_chain_repo.sh - rm -rf ./timefold-solver - - name: Checkout the proper timefold-solver branch + repository: ${{ github.actor }}/timefold-solver + ref: ${{ github.head_ref }} + path: ./timefold-solver + fetch-depth: 0 # Otherwise merge will fail on account of not having history. + - name: Checkout timefold-solver (main) # Checkout the main branch if the PR branch does not exist + if: steps.checkout-solver.outcome != 'success' uses: actions/checkout@v4 with: - repository: ${{ env.TARGET_CHAIN_USER }}/${{ env.TARGET_CHAIN_REPO }} - ref: ${{ env.TARGET_CHAIN_BRANCH }} - path: './timefold-solver' - fetch-depth: 0 # Otherwise merge in the next step will fail on account of not having history. + repository: TimefoldAI/timefold-solver + ref: main + path: ./timefold-solver + fetch-depth: 0 # Otherwise merge will fail on account of not having history. - name: Prevent stale fork of timefold-solver env: BLESSED_REPO: "timefold-solver" diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index a71e9907..10e0c96f 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -23,28 +23,23 @@ jobs: steps: # Need to check for stale repo, since Github is not aware of the build chain and therefore doesn't automate it. - - name: Checkout timefold-solver to access the scripts + - name: Checkout timefold-solver (PR) # Checkout the PR branch first, if it exists + id: checkout-solver uses: actions/checkout@v4 + continue-on-error: true with: - path: './timefold-solver' - repository: 'TimefoldAI/timefold-solver' - - name: Find the proper timefold-solver repo and branch - env: - CHAIN_USER: ${{ github.event.pull_request.head.repo.owner.login }} - CHAIN_BRANCH: ${{ github.head_ref }} - CHAIN_REPO: "timefold-solver" - CHAIN_DEFAULT_BRANCH: ${{ endsWith(github.head_ref, '.x') && github.head_ref || 'main' }} - shell: bash - run: | - ./timefold-solver/.github/scripts/check_chain_repo.sh - rm -rf ./timefold-solver - - name: Checkout the proper timefold-solver branch + repository: ${{ github.actor }}/timefold-solver + ref: ${{ github.head_ref }} + path: ./timefold-solver + fetch-depth: 0 # Otherwise merge will fail on account of not having history. + - name: Checkout timefold-solver (main) # Checkout the main branch if the PR branch does not exist + if: steps.checkout-solver.outcome != 'success' uses: actions/checkout@v4 with: - repository: ${{ env.TARGET_CHAIN_USER }}/${{ env.TARGET_CHAIN_REPO }} - ref: ${{ env.TARGET_CHAIN_BRANCH }} - path: './timefold-solver' - fetch-depth: 0 # Otherwise merge in the next step will fail on account of not having history. + repository: TimefoldAI/timefold-solver + ref: main + path: ./timefold-solver + fetch-depth: 0 # Otherwise merge will fail on account of not having history. - name: Prevent stale fork of timefold-solver env: BLESSED_REPO: "timefold-solver"