diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 885d5876f4..4d19ef2b6f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -38,11 +38,14 @@ jobs: - name: Upgrade pip run: python3 -m pip install -U pip setuptools wheel - name: Install Python dependencies - run: python3 -m pip install ruamel.yaml scons numpy cython h5py pandas + run: python3 -m pip install ruamel.yaml scons numpy cython h5py pandas pytest + pytest-github-actions-annotate-failures - name: Build Cantera - run: python3 `which scons` build -j2 debug=n + run: python3 `which scons` build env_vars=all -j2 debug=n --debug=time - name: Test Cantera - run: python3 `which scons` test + run: python3 `which scons` test --debug=time + env: + GITHUB_ACTIONS: "true" macos-multiple-pythons: name: macOS with Python ${{ matrix.python-version }} @@ -75,11 +78,14 @@ jobs: - name: Upgrade pip run: python3 -m pip install -U pip 'setuptools>=47.0.0,<48' wheel - name: Install Python dependencies - run: python3 -m pip install ruamel.yaml scons numpy cython h5py pandas + run: python3 -m pip install ruamel.yaml scons numpy cython h5py pandas pytest + pytest-github-actions-annotate-failures - name: Build Cantera - run: python3 `which scons` build -j2 debug=n + run: python3 `which scons` build env_vars=all -j3 debug=n --debug=time - name: Test Cantera - run: python3 `which scons` test + run: python3 `which scons` test --debug=time + env: + GITHUB_ACTIONS: "true" # Coverage is its own job because macOS builds of the samples # use Homebrew gfortran which is not compatible for coverage @@ -88,7 +94,6 @@ jobs: coverage: name: Coverage runs-on: ubuntu-latest - needs: [ubuntu-multiple-pythons] steps: - uses: actions/checkout@v2 name: Checkout the repository @@ -105,13 +110,16 @@ jobs: - name: Upgrade pip run: python3 -m pip install -U pip setuptools wheel - name: Install Python dependencies - run: python3 -m pip install ruamel.yaml scons numpy cython h5py pandas + run: python3 -m pip install ruamel.yaml scons numpy cython h5py pandas pytest + pytest-github-actions-annotate-failures - name: Build Cantera run: | python3 `which scons` build blas_lapack_libs=lapack,blas coverage=y \ - optimize=n no_optimize_flags=-DNDEBUG -j2 + optimize=n no_optimize_flags=-DNDEBUG env_vars=all -j2 --debug=time - name: Test Cantera - run: python3 `which scons` test + run: python3 `which scons` test --debug=time + env: + GITHUB_ACTIONS: "true" - name: Upload Coverage to Codecov run: bash <(curl -s https://codecov.io/bash) env: @@ -180,7 +188,8 @@ jobs: python3-ruamel.yaml python-numpy cython3 libsundials-dev liblapack-dev \ libblas-dev - name: Build Cantera - run: scons build python_cmd=/usr/bin/python3 blas_lapack_libs=lapack,blas -j2 debug=n + run: scons build python_cmd=/usr/bin/python3 blas_lapack_libs=lapack,blas -j2 + debug=n --debug=time - name: Test Cantera run: scons test @@ -326,7 +335,7 @@ jobs: - name: Install Python dependencies run: | python -m pip install -U pip 'setuptools>=47.0.0,<48' - python -m pip install scons pypiwin32 numpy ruamel.yaml cython h5py pandas + python -m pip install scons pypiwin32 numpy ruamel.yaml cython h5py pandas pytest pytest-github-actions-annotate-failures - name: Restore Boost cache uses: actions/cache@v2 id: cache-boost @@ -345,9 +354,11 @@ jobs: rm $BOOST_ROOT/download.7z shell: bash - name: Build Cantera - run: | - scons build -j2 boost_inc_dir=%BOOST_ROOT% debug=n VERBOSE=y python_package=full ^ - msvc_version=${{ matrix.vs-toolset }} f90_interface=n + run: scons build -j2 boost_inc_dir=%BOOST_ROOT% debug=n VERBOSE=y + python_package=full env_vars=PYTHONPATH,GITHUB_ACTIONS + msvc_version=${{ matrix.vs-toolset }} f90_interface=n --debug=time shell: cmd - name: Test Cantera - run: scons test + run: scons test --debug=time + env: + GITHUB_ACTIONS: "true" diff --git a/interfaces/cython/cantera/onedim.py b/interfaces/cython/cantera/onedim.py index 294c8b9a3c..c3bc10285b 100644 --- a/interfaces/cython/cantera/onedim.py +++ b/interfaces/cython/cantera/onedim.py @@ -1613,6 +1613,8 @@ def set_initial_guess(self, data=None, group=None): `FlameBase.set_initial_guess`). """ super().set_initial_guess(data=data, group=group) + if data: + return Yu = self.reactants.Y Tu = self.reactants.T diff --git a/interfaces/cython/cantera/test/test_onedim.py b/interfaces/cython/cantera/test/test_onedim.py index 3d6b999afb..a4b858f43a 100644 --- a/interfaces/cython/cantera/test/test_onedim.py +++ b/interfaces/cython/cantera/test/test_onedim.py @@ -823,7 +823,7 @@ def time_step_func(dt): def run_extinction(self, mdot_fuel, mdot_ox, T_ox, width, P): self.create_sim(fuel='H2:1.0', oxidizer='O2:1.0', p=ct.one_atm*P, - mdot_fuel=mdot_fuel, mdot_ox=mdot_ox, width=width) + mdot_fuel=mdot_fuel, mdot_ox=mdot_ox, T_ox=T_ox, width=width) self.sim.solve(loglevel=0, auto=True) self.assertFalse(self.sim.extinct()) diff --git a/interfaces/cython/cantera/test/test_reactor.py b/interfaces/cython/cantera/test/test_reactor.py index 7669554cfe..0d4884646e 100644 --- a/interfaces/cython/cantera/test/test_reactor.py +++ b/interfaces/cython/cantera/test/test_reactor.py @@ -1442,6 +1442,8 @@ def calc_dtdh(self, species): dtdp = ((t[-1] - tig)*S[-1,:]*Tf - np.trapz(S*T[:,None], t, axis=0))/(Tf-To) return dtdp + # See https://github.com/Cantera/enhancements/issues/55 + @unittest.skip("Integration of sensitivity ODEs is unreliable") def test_ignition_delay_sensitivity(self): species = ('H2', 'H', 'O2', 'H2O2', 'H2O', 'OH', 'HO2') dtigdh_cvodes = self.calc_dtdh(species) diff --git a/interfaces/cython/cantera/test/utilities.py b/interfaces/cython/cantera/test/utilities.py index 5f2f20edf2..c5a2f9fa33 100644 --- a/interfaces/cython/cantera/test/utilities.py +++ b/interfaces/cython/cantera/test/utilities.py @@ -20,6 +20,7 @@ def setUpClass(cls): '..', '..', '..', '..')) if os.path.exists(os.path.join(root_dir, 'SConstruct')): cls.test_work_dir = os.path.join(root_dir, 'test', 'work', 'python') + cls.using_tempfile = False try: os.makedirs(cls.test_work_dir) except OSError as e: @@ -31,6 +32,7 @@ def setUpClass(cls): raise else: cls.test_work_dir = tempfile.mkdtemp() + cls.using_tempfile = True cantera.make_deprecation_warnings_fatal() cantera.add_directory(cls.test_work_dir) @@ -41,7 +43,7 @@ def setUpClass(cls): @classmethod def tearDownClass(cls): # Remove the working directory after testing, but only if its a temp directory - if tempfile.tempdir is not None: + if getattr(cls, "using_tempfile", False): try: shutil.rmtree(cls.test_work_dir) except OSError: diff --git a/src/base/AnyMap.cpp b/src/base/AnyMap.cpp index fabbdb3d5d..fcdcc99271 100644 --- a/src/base/AnyMap.cpp +++ b/src/base/AnyMap.cpp @@ -39,6 +39,7 @@ bool isFloat(const std::string& val) int numDot = 0; int numExp = 0; int istart = 0; + int numDigit = 0; char ch = str[0]; if (ch == '+' || ch == '-') { istart = 1; @@ -49,6 +50,7 @@ bool isFloat(const std::string& val) for (size_t i = istart; i < str.size(); i++) { ch = str[i]; if (isdigit(ch)) { + numDigit++; } else if (ch == '.') { numDot++; if (numDot > 1) { @@ -59,7 +61,7 @@ bool isFloat(const std::string& val) } } else if (ch == 'e' || ch == 'E') { numExp++; - if (numExp > 1 || i == str.size() - 1) { + if (numExp > 1 || numDigit == 0 || i == str.size() - 1) { return false; } ch = str[i+1]; diff --git a/src/base/units.h b/src/base/units.h index 6712ee2f31..08e6dc554a 100644 --- a/src/base/units.h +++ b/src/base/units.h @@ -3,15 +3,13 @@ * Header for units conversion utilities, which are used to translate * user input from input files (See \ref inputfiles and * class \link Cantera::Unit Unit\endlink). - * - * This header is included only by file misc.cpp. */ // This file is part of Cantera. See License.txt in the top-level directory or // at https://cantera.org/license.txt for license and copyright information. -#ifndef CT_UNITS_H -#define CT_UNITS_H +#ifndef CT_LEGACY_UNITS_H +#define CT_LEGACY_UNITS_H #include "cantera/base/ct_defs.h" #include "cantera/base/ctexceptions.h" diff --git a/src/pch/system.h b/src/pch/system.h index 868ecdcaf6..6c277b279e 100644 --- a/src/pch/system.h +++ b/src/pch/system.h @@ -15,5 +15,6 @@ #include #include "cantera/base/fmt.h" +#include "cantera/base/AnyMap.h" #endif diff --git a/test/SConscript b/test/SConscript index 6d501f15d4..c5392b7a16 100644 --- a/test/SConscript +++ b/test/SConscript @@ -115,14 +115,18 @@ def addPythonTest(testname, subdir, script, interpreter, outfile, Create targets for running and resetting a test script. """ def scriptRunner(target, source, env): + unittest_outfile = File("#test/work/python-results.txt").abspath + pytest_outfile = File("#test/work/pytest.xml").abspath """Scons Action to run a test script using the specified interpreter""" workDir = Dir('#test/work').abspath passedFile = target[0] testResults.tests.pop(passedFile.name, None) if not os.path.isdir(workDir): os.mkdir(workDir) - if os.path.exists(outfile): - os.remove(outfile) + if os.path.exists(unittest_outfile): + os.remove(unittest_outfile) + if os.path.exists(pytest_outfile): + os.remove(pytest_outfile) environ = dict(env['ENV']) for k,v in env_vars.items(): @@ -145,7 +149,7 @@ def addPythonTest(testname, subdir, script, interpreter, outfile, sys.exit(1) failures = 0 - if os.path.exists(outfile): + if os.path.exists(unittest_outfile): # Determine individual test status for line in open(outfile): status, name = line.strip().split(': ', 1) @@ -154,6 +158,18 @@ def addPythonTest(testname, subdir, script, interpreter, outfile, elif status in ('FAIL', 'ERROR'): testResults.failed[':'.join((testname,name))] = 1 failures += 1 + elif os.path.exists(pytest_outfile): + results = ElementTree.parse(pytest_outfile) + for test in results.findall('.//testcase'): + class_name = test.get('classname') + if class_name.startswith("build.python.cantera.test."): + class_name = class_name[26:] + test_name = "python: {}.{}".format(class_name, test.get('name')) + if test.findall('failure'): + testResults.failed[test_name] = 1 + failures += 1 + else: + testResults.passed[test_name] = 1 if code and failures == 0: # Failure, but unable to determine status of individual tests. This diff --git a/test/python/runCythonTests.py b/test/python/runCythonTests.py index 54534f2806..3105ba3241 100644 --- a/test/python/runCythonTests.py +++ b/test/python/runCythonTests.py @@ -20,19 +20,16 @@ import sys import os +from pathlib import Path cantera_root = os.path.relpath(__file__).split(os.sep)[:-1] + ['..', '..'] -module_path = os.path.abspath(os.sep.join(cantera_root + ['build'])) - -if 'PYTHONPATH' in os.environ: - os.environ['PYTHONPATH'] = module_path + os.path.pathsep + os.environ['PYTHONPATH'] -else: - os.environ['PYTHONPATH'] = module_path - -sys.path.insert(0, module_path) os.chdir(os.sep.join(cantera_root + ['test', 'work'])) -from cantera.test.utilities import unittest +import unittest +try: + import pytest +except ImportError: + pytest = None import cantera import cantera.test @@ -77,19 +74,36 @@ def addError(self, test, err): else: fast_fail = False subset_start = 1 - loader = unittest.TestLoader() - runner = unittest.TextTestRunner( - verbosity=2, resultclass=TestResult, failfast=fast_fail - ) - suite = unittest.TestSuite() - subsets = [] - for name in sys.argv[subset_start:]: - subsets.append('cantera.test.test_' + name) - - if not subsets: - subsets.append('cantera.test') - - suite = loader.loadTestsFromNames(subsets) - - results = runner.run(suite) - sys.exit(len(results.errors) + len(results.failures)) + + if pytest is not None: + base = Path(cantera.__file__).parent.joinpath('test') + subsets = [] + for name in sys.argv[subset_start:]: + subsets.append(str(base.joinpath(f"test_{name}.py"))) + + if not subsets: + subsets.append(str(base)) + + pytest_args = ["-raP", "--durations=50", "--junitxml=pytest.xml"] + if fast_fail: + pytest_args.insert(0, "-x") + + ret_code = pytest.main(pytest_args + subsets) + sys.exit(ret_code) + else: + loader = unittest.TestLoader() + runner = unittest.TextTestRunner( + verbosity=2, resultclass=TestResult, failfast=fast_fail + ) + suite = unittest.TestSuite() + subsets = [] + for name in sys.argv[subset_start:]: + subsets.append('cantera.test.test_' + name) + + if not subsets: + subsets.append('cantera.test') + + suite = loader.loadTestsFromNames(subsets) + + results = runner.run(suite) + sys.exit(len(results.errors) + len(results.failures))