diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f02d203..8f7a3de 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,14 +1,28 @@ -name: Build the examples +name: CI on: push: branches: - master pull_request: - branches: - - master jobs: + lint: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: ruff check + uses: astral-sh/ruff-action@v3 + with: + args: check + + - name: ruff format + uses: astral-sh/ruff-action@v3 + with: + args: format --check + build: runs-on: ubuntu-latest @@ -34,7 +48,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] + python-version: ["3.12", "3.13", "3.14"] steps: - name: Checkout repository @@ -46,12 +60,15 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install giws - run: python -m pip install . + run: python -m pip install . pytest + + - name: Run the test suite + run: python -m pytest - name: Smoke-test the installed script run: | giws --version cd examples/basic_example - giws -f MyComplexClass.giws.xml + giws -p -f MyComplexClass.giws.xml test -f basic_example.cpp test -f basic_example.hxx diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml deleted file mode 100644 index acf81a7..0000000 --- a/.github/workflows/lint.yml +++ /dev/null @@ -1,15 +0,0 @@ -name: Lint - -on: [push, pull_request] - -jobs: - ruff: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: astral-sh/ruff-action@v3 - with: - args: check - - uses: astral-sh/ruff-action@v3 - with: - args: format --check diff --git a/CHANGELOG b/CHANGELOG index eebc337..04d291c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -11,6 +11,12 @@ giws (3.1.0) --body-extension-file as intended * Fix the error message raised for an unknown datatype * CI: update actions, add a Python 3.9-3.14 install/smoke-test matrix + * Generated GiwsException: check for pending exceptions after each + CallObjectMethod (fixes -Xcheck:jni warnings while building the + C++ exception) + * Add a test suite (tests/) run by CI; no JDK needed + * New example bug_nonstatic_exception demonstrating the exception + fix on non-static methods * Modernize the Python code (f-strings, type hints, with-statement) * Add a pre-commit configuration running ruff diff --git a/CXXException.py b/CXXException.py index 5a0c684..bb387d9 100644 --- a/CXXException.py +++ b/CXXException.py @@ -410,6 +410,12 @@ def generateCXXBody(self, config): // call getLocalizedMessage jstring description = (jstring) curEnv->CallObjectMethod(javaException, getLocalizedMessageId); + if (curEnv->ExceptionCheck()) + { + curEnv->ExceptionClear(); + return ""; + } + if (description == NULL) { return ""; @@ -442,6 +448,12 @@ def generateCXXBody(self, config): // call getStackTrace jobjectArray stackTrace = (jobjectArray) curEnv->CallObjectMethod(javaException, getStackTraceId); + if (curEnv->ExceptionCheck()) + { + curEnv->ExceptionClear(); + return ""; + } + if (stackTrace == NULL) { return ""; @@ -463,6 +475,15 @@ def generateCXXBody(self, config): // call to string on the object jstring stackElementString = (jstring) curEnv->CallObjectMethod(curStackTraceElement, toStringId); + if (curEnv->ExceptionCheck()) + { + curEnv->ExceptionClear(); + curEnv->DeleteLocalRef(stackTraceElementClass); + curEnv->DeleteLocalRef(stackTrace); + curEnv->DeleteLocalRef(curStackTraceElement); + return res; + } + if (stackElementString == NULL) { curEnv->DeleteLocalRef(stackTraceElementClass); @@ -505,6 +526,14 @@ def generateCXXBody(self, config): // call the getName function jstring javaName = (jstring) curEnv->CallObjectMethod(exceptionClass, getNameId); + if (curEnv->ExceptionCheck()) + { + curEnv->ExceptionClear(); + curEnv->DeleteLocalRef(exceptionClass); + curEnv->DeleteLocalRef(classClass); + return ""; + } + if (javaName == NULL) { return ""; diff --git a/datatypes/dataGiws.py b/datatypes/dataGiws.py index 6d3c880..00e8326 100644 --- a/datatypes/dataGiws.py +++ b/datatypes/dataGiws.py @@ -38,11 +38,6 @@ from JNIFrameWork import JNIFrameWork -def abstractMethod(obj=None): - """Use this instead of 'pass' for the body of abstract methods.""" - raise Exception(f"Unimplemented abstract method: {obj}") - - # # This class intend to create a generic object for datatype # see http://en.wikipedia.org/wiki/Java_Native_Interface#Mapping_types @@ -126,11 +121,11 @@ def getCallStaticMethod(self): def getRealJavaType(self): """Returns the real datatype""" - abstractMethod(self) + raise NotImplementedError(type(self).__name__) def getDescription(self): """Returns the description""" - abstractMethod(self) + raise NotImplementedError(type(self).__name__) def setIsArray(self, isItAnArray): """Defines if we have to deal with an array or not""" diff --git a/examples/Makefile b/examples/Makefile index e304dd4..ae3d62c 100644 --- a/examples/Makefile +++ b/examples/Makefile @@ -9,7 +9,7 @@ SHELL = /bin/sh # list of buildable examples EX = basic_example example1 example2 example3 example4 example5 inherit bytebuffer \ -bug_no_param_int_array bug_disable_return bug_string_array_len +bug_no_param_int_array bug_disable_return bug_string_array_len bug_nonstatic_exception # verify that JAVA_HOME is set to something (should be the bare minimum) diff --git a/examples/bug_nonstatic_exception/Makefile b/examples/bug_nonstatic_exception/Makefile new file mode 100644 index 0000000..f96c1fb --- /dev/null +++ b/examples/bug_nonstatic_exception/Makefile @@ -0,0 +1,82 @@ +SHELL = /bin/sh + +# verify that JAVA_HOME is set to something (should be the bare minimum) +ifndef JAVA_HOME + $(error ERROR: Variable JAVA_HOME is not set!) +endif + + +# +# C++ compiler options +# +CC = g++ +CFLAGS = -g +INCLUDES = -I$(JAVA_HOME)/include -I$(JAVA_HOME)/include/linux +LIBS = -ljvm -L$(JAVA_HOME)/lib/server/ + +# +# Java compiler option +# +JCC = $(JAVA_HOME)/bin/javac +JFLAGS = + +# +# GIWS options +# +GIWS = ../../giws +GFLAGS = -p --throws-exception-on-error -g + +# +# GIWS PROJECT INFORMATION +# +PACKAGE_NAME = bug_nonstatic_exception +OBJECT_NAME = MyThrowingObject +BINARY = main + +GIWS_CPP_FILE = main.cpp +GIWS_DESC_FILE = $(OBJECT_NAME).giws.xml +GIWS_OUT_FILES = $(addprefix $(PACKAGE_NAME),.cpp .hxx) + + +# in case of --throws-exception-on-error, add 2 files to the list +# of the ones generated by GIWS. +ifneq (,$(findstring --throws-exception-on-error,$(GFLAGS))) + GIWS_OUT_FILES += GiwsException.cpp GiwsException.hxx +endif + + +######################################################################### +######################################################################### +######################################################################### + +# look for sources in current folder and in package name +VPATH = .:$(PACKAGE_NAME) + + +all: $(OBJECT_NAME).class $(BINARY) + + +# build java class file +$(OBJECT_NAME).class: $(OBJECT_NAME).java + $(JCC) $(JFLAGS) $< + + +# build output binary +$(BINARY): $(GIWS_OUT_FILES) + $(CC) $(GIWS_CPP_FILE) $(GIWS_OUT_FILES) $(CFLAGS) $(LIBS) $(INCLUDES) -o $(BINARY) + @if test $(MAKELEVEL) -eq 0; then \ + echo "==========================================================================="; \ + echo "Dont forget to set library path before running the program:"; \ + echo "# export LD_LIBRARY_PATH=$(LD_LIBRARY_PATH):$(JAVA_HOME)/lib/server/"; \ + echo "==========================================================================="; \ + fi + + +# run giws to generate cpp code +$(GIWS_OUT_FILES): + $(GIWS) -f $(GIWS_DESC_FILE) $(GFLAGS) + + +# cleanup giws generated files (cpp/hxx, bin) and .class +clean: + rm -f $(GIWS_OUT_FILES) $(PACKAGE_NAME)/$(OBJECT_NAME).class $(BINARY) diff --git a/examples/bug_nonstatic_exception/MyThrowingObject.giws.xml b/examples/bug_nonstatic_exception/MyThrowingObject.giws.xml new file mode 100644 index 0000000..6d7f518 --- /dev/null +++ b/examples/bug_nonstatic_exception/MyThrowingObject.giws.xml @@ -0,0 +1,12 @@ + + + + + + + diff --git a/examples/bug_nonstatic_exception/README b/examples/bug_nonstatic_exception/README new file mode 100644 index 0000000..f3e0e71 --- /dev/null +++ b/examples/bug_nonstatic_exception/README @@ -0,0 +1,16 @@ +Regression test for the missing exception check on non-static methods. + +Before giws 3.1.0 the generated code only checked for a pending Java +exception after calls to static methods. With +--throws-exception-on-error, an exception thrown by a non-static Java +method was never converted into a GiwsException::JniCallMethodException. + +main returns 0 only if the exception is caught in C++. + +Defines where is the JDK +# export JAVA_HOME=/path/to/java/ + +Build and run +# make +# export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$JAVA_HOME/lib/server/ +# ./main diff --git a/examples/bug_nonstatic_exception/bug_nonstatic_exception/MyThrowingObject.java b/examples/bug_nonstatic_exception/bug_nonstatic_exception/MyThrowingObject.java new file mode 100644 index 0000000..b649cd9 --- /dev/null +++ b/examples/bug_nonstatic_exception/bug_nonstatic_exception/MyThrowingObject.java @@ -0,0 +1,8 @@ +package bug_nonstatic_exception; + +public class MyThrowingObject { + + public void alwaysThrows() { + throw new IllegalStateException("thrown from a non-static method"); + } +} diff --git a/examples/bug_nonstatic_exception/main.cpp b/examples/bug_nonstatic_exception/main.cpp new file mode 100644 index 0000000..2702253 --- /dev/null +++ b/examples/bug_nonstatic_exception/main.cpp @@ -0,0 +1,52 @@ +/* +Regression test: before giws 3.1.0, the exception check after the Java +call was only generated for static methods. The Java exception thrown +by this non-static method was never converted into a C++ exception, so +the try/catch below did not catch anything and the program fell through +to the failure branch. +*/ + +#include +#include +#include "bug_nonstatic_exception.hxx" +#include "GiwsException.hxx" + +JavaVM* create_vm() { + JavaVM* jvm; + JNIEnv* env; + JavaVMInitArgs args; + JavaVMOption options[2]; + + args.version = JNI_VERSION_1_4; + + args.nOptions = 2; + options[0].optionString = const_cast("-Djava.class.path=."); + options[1].optionString = const_cast("-Xcheck:jni"); + args.options = options; + args.ignoreUnrecognized = JNI_FALSE; + + JNI_CreateJavaVM(&jvm, (void **)&env, &args); + return jvm; +} + +using namespace bug_nonstatic_exception; +using namespace std; + +int main(){ + JavaVM* jvm = create_vm(); + MyThrowingObject *obj = new MyThrowingObject(jvm); + + try { + obj->alwaysThrows(); + } catch (GiwsException::JniException & e) { + cout << "Exception caught from a non-static method: " + << e.getJavaDescription() << endl; + cout << "Exception name: " << e.getJavaExceptionName() << endl; + delete obj; + return 0; + } + + cerr << "FAILED: the Java exception was not converted into a C++ exception" << endl; + delete obj; + return 1; +} diff --git a/pyproject.toml b/pyproject.toml index 5a97319..9f74db4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,3 +31,6 @@ script-files = ["giws"] [tool.setuptools.dynamic] version = { attr = "configGiws.__version__" } + +[tool.pytest.ini_options] +testpaths = ["tests"] diff --git a/tests/test_codegen.py b/tests/test_codegen.py new file mode 100644 index 0000000..89b5639 --- /dev/null +++ b/tests/test_codegen.py @@ -0,0 +1,123 @@ +# Copyright or Copr. INRIA/Scilab - Sylvestre LEDRU +# +# This software is a computer program whose purpose is to generate C++ wrapper +# for Java objects/methods. +# +# This software is governed by the CeCILL license under French law. See the +# file COPYING for details. + +"""Tests for the code generator. + +Each case runs giws on an example XML declaration (with the same flags +as the example's Makefile) and checks the generated C++. No JDK needed. +""" + +import re +import shutil +import subprocess +import sys +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parent.parent +EXAMPLES = ROOT / "examples" +GIWS = ROOT / "giws" + +# (example directory, giws flags) - mirrors GFLAGS in examples/*/Makefile +CASES = [ + ("basic_example", ["-p"]), + ("bug_disable_return", ["-r"]), + ("bug_no_param_int_array", []), + ("bug_nonstatic_exception", ["-p", "--throws-exception-on-error", "-g"]), + ("bug_string_array_len", ["-p"]), + ("bytebuffer", ["-p", "--throws-exception-on-error", "-g"]), + ("example1", ["-p"]), + ("example2", ["-p", "--disable-return-size-array"]), + ("example3", ["-p", "--throws-exception-on-error", "-g"]), + ("example4", ["-p"]), + ("example5", ["-p"]), + ("inherit", ["-p"]), +] + + +def run_giws(args, cwd): + return subprocess.run( + [sys.executable, str(GIWS), *args], + cwd=cwd, + capture_output=True, + text=True, + ) + + +def generate(example, flags, destination): + """Run giws on an example and return the generated files by name.""" + xml = next((EXAMPLES / example).glob("*.giws.xml")) + shutil.copy(xml, destination / xml.name) + result = run_giws(["-f", xml.name, *flags], cwd=destination) + assert result.returncode == 0, result.stderr + generated = { + p.name: p.read_text() + for p in destination.iterdir() + if p.suffix in (".cpp", ".hxx") + } + assert generated, f"giws produced no output for {example}" + return generated + + +@pytest.mark.parametrize("example,flags", CASES, ids=[c[0] for c in CASES]) +def test_examples_generate(example, flags, tmp_path): + generated = generate(example, flags, tmp_path) + for name, content in generated.items(): + assert content.strip(), f"{example}/{name} is empty" + + +def test_nonstatic_method_checks_exceptions(tmp_path): + """A Java exception thrown by a non-static method must be detected. + + The check used to be generated for static methods only. + """ + generated = generate( + "bug_nonstatic_exception", + ["-p", "--throws-exception-on-error", "-g"], + tmp_path, + ) + body = generated["bug_nonstatic_exception.cpp"] + method = body.split("void MyThrowingObject::alwaysThrows")[1] + assert "ExceptionCheck()" in method + assert "JniCallMethodException" in method + + +def test_exception_class_checks_after_each_call(tmp_path): + """GiwsException helpers must not call JNI functions while an + exception raised by CallObjectMethod is pending (-Xcheck:jni).""" + generated = generate( + "example3", ["-p", "--throws-exception-on-error", "-g"], tmp_path + ) + body = generated["GiwsException.cpp"] + calls = list(re.finditer(r"CallObjectMethod\(", body)) + assert calls, "no CallObjectMethod in GiwsException.cpp" + for call in calls: + following = body[call.end() : call.end() + 400] + assert "ExceptionCheck()" in following, ( + "CallObjectMethod without a following ExceptionCheck near " + f"offset {call.start()}" + ) + + +def test_version_option(): + result = run_giws(["--version"], cwd=ROOT) + assert result.returncode == 0 + assert "GIWS " in result.stdout + + +def test_no_arguments_is_an_error(): + result = run_giws([], cwd=ROOT) + assert result.returncode == 2 + assert "description file" in result.stderr + + +def test_missing_description_file_is_an_error(tmp_path): + result = run_giws(["-f", "doesnotexist.xml"], cwd=tmp_path) + assert result.returncode == 2 + assert "doesnotexist.xml" in result.stderr