diff --git a/.github/workflows/test-and-publish.yml b/.github/workflows/test-and-publish.yml index 35f7160..6ca0789 100644 --- a/.github/workflows/test-and-publish.yml +++ b/.github/workflows/test-and-publish.yml @@ -8,11 +8,16 @@ on: jobs: test: - runs-on: ubuntu-latest strategy: fail-fast: false matrix: python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + os: ["ubuntu-latest"] + include: + - python-version: "3.8" + os: "windows-latest" + + runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 @@ -30,7 +35,8 @@ jobs: - name: Run tests run: | - python -m pytest + # Disable the fault handler to fix https://stackoverflow.com/questions/57523762/pytest-windows-fatal-exception-code-0x8001010d + pytest -p no:faulthandler # ------------------------------------------------------------ # Build the distribution and publish (on release tag). diff --git a/pyproject.toml b/pyproject.toml index 27e9167..a11dd51 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,10 +29,22 @@ dynamic = ["version"] # Version is read from rhodium/__init__.py "Bug Tracker" = "https://github.com/Project-Platypus/Rhodium/issues" [project.optional-dependencies] -test = ["pytest", "mock", "rhodium[examples]"] -openmdao = ["openmdao"] -windows = ["win32com"] -examples = ["pandas[excel]", "pyper<=1.1.2"] +test = [ + "pytest", + "mock", + "rhodium[examples]" +] +openmdao = [ + "openmdao" +] +examples = [ + "pandas[excel]", + "pyper<=1.1.2", + "rhodium[windows]" +] +windows = [ + "pywin32 ; platform_system == 'Windows'" +] [tool.setuptools.dynamic] version = {attr = "rhodium.__version__"} diff --git a/rhodium/excel.py b/rhodium/excel.py index 5542668..679aaac 100644 --- a/rhodium/excel.py +++ b/rhodium/excel.py @@ -17,13 +17,20 @@ # along with Rhodium. If not, see . import win32com.client from win32com.universal import com_error -from .model import Model +from .model import Model, RhodiumError class ExcelHelper: def __init__(self, filename, sheet=1, visible=False): - self.xl = win32com.client.Dispatch("Excel.Application") - self.wb = self.xl.Workbooks.Open(filename) + try: + self.xl = win32com.client.Dispatch("Excel.Application") + except com_error as e: + raise RhodiumError("Failed to load Excel application", e) + + try: + self.wb = self.xl.Workbooks.Open(filename) + except com_error as e: + raise RhodiumError("Failed to open Excel file", e) # ensure auto-calculations is enabled sheets = self.xl.Worksheets diff --git a/rhodium/test/excel_test.py b/rhodium/test/excel_test.py index a851dbd..37c11f2 100644 --- a/rhodium/test/excel_test.py +++ b/rhodium/test/excel_test.py @@ -18,14 +18,27 @@ import os import sys import unittest -from ..model import Parameter, Response, IntegerUncertainty, \ +from ..model import Parameter, Response, RhodiumError, IntegerUncertainty, \ UniformUncertainty from ..optimization import evaluate from ..sampling import sample_lhs +# Since Excel is typically not installed on hosted CI, skip test failures. +def skipErrorsOnCI(test): + def wrapper(self): + try: + test(self) + except RhodiumError as e: + if os.getenv("CI"): + self.skipTest(f"Excel test failed, ignoring. Reason: {e}") + else: + raise + return wrapper + class TestExcelHelper(unittest.TestCase): @unittest.skipUnless(sys.platform.startswith("win"), "requires Windows") + @skipErrorsOnCI def testGetItem(self): from ..excel import ExcelHelper file = os.path.join(os.path.dirname(__file__), "TestGetItem.xlsx") @@ -42,6 +55,7 @@ def testGetItem(self): self.assertEqual(u"sheet 2", helper["B2"]) @unittest.skipUnless(sys.platform.startswith("win"), "requires Windows") + @skipErrorsOnCI def testSetItem(self): from ..excel import ExcelHelper file = os.path.join(os.path.dirname(__file__), "TestSetItem.xlsx") @@ -62,10 +76,20 @@ def testSetItem(self): helper["B2"] = "world" helper.set_sheet(2) self.assertEqual(u"hello", helper["B2"]) + + @unittest.skipUnless(sys.platform.startswith("win"), "requires Windows") + @skipErrorsOnCI + def testInvalidFile(self): + from ..excel import ExcelHelper + file = os.path.join(os.path.dirname(__file__), "Missing.xlsx") + with self.assertRaises(RhodiumError) as context: + with ExcelHelper(file) as helper: + pass class TestExcelModel(unittest.TestCase): @unittest.skipUnless(sys.platform.startswith("win"), "requires Windows") + @skipErrorsOnCI def testEvaluate(self): from ..excel import ExcelModel file = os.path.join(os.path.dirname(__file__), "TestModel.xlsx") @@ -79,6 +103,7 @@ def testEvaluate(self): self.assertEqual(8, result["Y"]) @unittest.skipUnless(sys.platform.startswith("win"), "requires Windows") + @skipErrorsOnCI def testSample(self): from ..excel import ExcelModel file = os.path.join(os.path.dirname(__file__), "TestModel.xlsx")