diff --git a/docs/conf.py b/docs/conf.py index ac5492f4e..2e3c06554 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -6,7 +6,6 @@ from pathlib import Path from shutil import copyfile - ROOT = Path(__file__).resolve().parent sys_path.insert(0, abspath(".")) @@ -52,7 +51,7 @@ master_doc = "index" project = "VUnit" -copyright = "2014-2024, Lars Asplund" +copyright = "2014-2026, Lars Asplund" author = "LarsAsplund, kraigher and contributors" version = "" diff --git a/docs/create_release_notes.py b/docs/create_release_notes.py index eaa103569..b4c413eec 100644 --- a/docs/create_release_notes.py +++ b/docs/create_release_notes.py @@ -8,6 +8,7 @@ Create monolithic release notes file from several input files """ +import sys from pathlib import Path from subprocess import check_call, check_output, CalledProcessError from shutil import which @@ -21,7 +22,7 @@ def create_release_notes(): docsroot = Path(__file__).parent root = docsroot.parent - check_call(["towncrier", "build", "--keep"], cwd=root) + check_call([sys.executable, "-m", "towncrier", "build", "--keep"], cwd=root) # Get all releases defined by release note files releases = [ diff --git a/docs/examples.py b/docs/examples.py index b45369374..e754efdf7 100644 --- a/docs/examples.py +++ b/docs/examples.py @@ -19,7 +19,6 @@ from textwrap import indent - ROOT = Path(__file__).parent.parent / "docs" @@ -74,7 +73,7 @@ def _get_eg_doc(location: Path, ref): f".. _examples:{location.parent.name}:{location.name}:\n", title, "-" * len(title), - f":vunit_example:`➚ examples/{ref} <{ref!s}>`\n", + f":vunit_example:`examples/{ref} <{ref!s}>`\n", eg_doc.split("---\n", 1)[1], "\n", ] diff --git a/docs/vunit_packages.rst b/docs/vunit_packages.rst index f7549538a..791028c8c 100644 --- a/docs/vunit_packages.rst +++ b/docs/vunit_packages.rst @@ -22,35 +22,55 @@ To use a package, add it in the project's run script: vu.main() The manifest specifies the package's source files, version requirements, -and compilation defaults. For example: +and compilation options. For example: .. code-block:: toml :caption: vunit_pkg.toml [package] - requires-vunit = ">=5.0.0.dev14" - requires-vhdl = ">=2008,<2019" + # The package requires VUnit 5.0.0.dev15 or later and VHDL 2008 or later. + requires = 'vunit >= "5.0.0.dev15" and vhdl >= "2008"' + + # The default library for the package's sources. library = "foo_lib" - compile_option = [["", [""]]] + + # Default compile options for all sources in the package. These options are optional and can also be overridden for + # individual source entries. + compile_option = [ + ["ghdl.a_flags", ["-fpsl"]], + ["nvc.a_flags", ["--psl"]] + ] [[package.sources]] include = ["hdl/src/*.vhd"] [[package.sources]] include = ["hdl/compat/*.vhd"] + # Overrides the default library foo_lib for this source entry. library = "foo_compat" - compile_option = [["", [""]]] -Each ``[[package.sources]]`` table defines a source entry. Its ``include`` -patterns are relative to the package root. In this example, files matching -``hdl/src/*.vhd`` use the default library, ``foo_lib``, while files matching -``hdl/compat/*.vhd`` use ``foo_compat``. + [[package.sources]] + include = ["hdl/ghdl_extras_pkg.vhd", "hdl/ghdl_extras_pkg-body.vhd"] + # Conditionally includes this source entry based on the environment. + when = 'vhdl >= "2019" and simulator == "ghdl" and ghdl_version >= "6.0.0"' + -Replace the compile-option placeholders with names and values supported by -the project. Each option is a two-element array containing the option name -and an array of string values. Package-level options apply to every source -entry. A source-level option overrides the package-level option with the -same name for that entry. +The ``requires`` expression in the example allows the package to be used with +VUnit 5.0.0.dev15 or later and VHDL 2008 or later. Each ``[[package.sources]]`` +table defines a source entry, and its ``include`` patterns are relative to the +package root. The first entry adds files matching ``hdl/src/*.vhd`` to the +default library, ``foo_lib``. The second adds ``hdl/compat/*.vhd`` to +``foo_compat`` instead. + +The final source entry is added only when all parts of its ``when`` condition +match: the selected VHDL standard is 2019 or later, the simulator is GHDL, and +the GHDL version is 6.0.0 or later. Otherwise, that entry's source files are +skipped. + +The package-level ``compile_option`` values shown apply to every source entry. +Each option is a two-element array containing an option name and an array of +string values. A source-level option, if specified, overrides the package-level +option with the same name for that entry. The supported manifest fields are: @@ -61,12 +81,9 @@ The supported manifest fields are: * - Field - Location - Meaning - * - ``requires-vunit`` - - ``[package]`` - - VUnit version constraints that the project must satisfy. - * - ``requires-vhdl`` + * - ``requires`` - ``[package]`` - - Constraints on the VHDL standards supported by the package. + - Environment expression that must match before the package can be used. * - ``library`` - ``[package]`` - Default library for package sources. Required when ``sources`` is @@ -75,6 +92,10 @@ The supported manifest fields are: - ``[[package.sources]]`` - Repeated source tables. Each table must contain ``include``, an array of source patterns relative to the package root. + * - ``when`` + - ``[[package.sources]]`` + - Optional environment expression that controls whether this source + entry is included. * - ``library`` - ``[[package.sources]]`` - Optional library override for this source entry. VUnit creates the @@ -87,16 +108,48 @@ The supported manifest fields are: - ``[package]`` - Optional Python setup function, specified as ``"module:function"``. See :ref:`package_setup_function`. - -The ``requires-vunit`` and ``requires-vhdl`` fields accept comma-separated -comparisons using ``<=``, ``<``, ``!=``, ``==``, ``>=``, or ``>``. All -comparisons must be satisfied. For example, ``">=2008,<2019"`` allows -VHDL 2008 but excludes VHDL 2019 and later standards. - -If the project's VHDL standard does not satisfy ``requires-vhdl``, VUnit -selects a compatible supported standard and issues a warning. If no such -standard is available, adding the package fails. A VUnit version that does -not satisfy ``requires-vunit`` always causes an error. + * - ``requires-vunit`` + - ``[package]`` + - Deprecated. Use ``requires`` to specify a VUnit version constraint. + * - ``requires-vhdl`` + - ``[package]`` + - Deprecated. Use ``requires`` to specify a VHDL standard constraint. + +The ``requires`` and ``when`` expressions support the markers ``vunit``, +``vhdl``, ``simulator``, and the simulator version markers +``activehdl_version``, ``ghdl_version``, ``modelsim_version``, +``nvc_version``, and ``rivierapro_version``. Values must be quoted strings. +Comparisons use ``==``, ``!=``, ``<``, ``<=``, ``>``, or ``>=``. A simulator +version marker has a value only when its corresponding simulator is selected. +If a marker has no value, equality and inequality comparisons are supported +(``==`` is false and ``!=`` is true); ordering comparisons are unavailable. + +Comparisons can be combined with ``and`` and ``or``, and parentheses can group +them. ``and`` has higher precedence than ``or``; parentheses can override +that precedence. These operators short-circuit: ``and`` skips its second +comparison when the first is false, and ``or`` skips it when the first is +true. For example, in ``simulator == "ghdl" and ghdl_version >= "6.0.0"``, +the GHDL version is checked only if GHDL is selected. If another simulator is +selected, the first comparison is false, so the version marker is not +evaluated while it has no value. + +``requires`` determines whether the package can be used in the current +environment. ``when`` determines whether a particular source entry is +included. A source entry without ``when`` is included whenever the package +can be used. + +The deprecated ``requires-vunit`` and ``requires-vhdl`` fields are still +supported, but new manifests should use ``requires``. + +VUnit uses the project's VHDL standard when it satisfies ``requires``. If not, +VUnit selects the closest supported VHDL standard that makes the full +``requires`` expression match. A warning is issued when a different standard +is selected, and the package is compiled with that standard. For example, if +the project uses VHDL 2008 and the manifest has +``requires = 'vhdl == "1993"'``, the package is compiled with VHDL 1993. If the +project uses VHDL 2019 and the manifest has ``requires = 'vhdl <= "2008"'``, +VUnit selects VHDL 2008, the closest supported standard that satisfies the +requirement. Manifest validation is strict: unknown fields and values of the wrong type are rejected. diff --git a/tests/unit/test_activehdl_interface.py b/tests/unit/test_activehdl_interface.py index 647f0f00a..e1f8f2c47 100644 --- a/tests/unit/test_activehdl_interface.py +++ b/tests/unit/test_activehdl_interface.py @@ -8,13 +8,12 @@ Test the ActiveHDL interface """ - import unittest from pathlib import Path import os from shutil import rmtree from unittest import mock -from vunit.sim_if.activehdl import ActiveHDLInterface, VersionConsumer, Version +from vunit.sim_if.activehdl import ActiveHDLInterface, ActiveHDLVersion, VersionConsumer from vunit.project import Project from vunit.ostools import renew_path, write_file from vunit.vhdl_standard import VHDL @@ -347,125 +346,166 @@ def _assert_version_correct(self, version_line, expected_major, expected_minor, consumer(version_line) self.assertEqual( consumer.version, - Version(expected_major, expected_minor, expected_minor_letter), + ActiveHDLVersion(f"{expected_major}.{expected_minor}{expected_minor_letter}"), ) - def test_vendor_version_without_letters(self): + def test_version_string_without_letters(self): self._assert_version_correct( - "Aldec, Inc. VHDL compiler version 10.5.216.6767 built for Windows on January 20, 2018.", + "10.5.216.6767", 10, 5, "", ) - def test_vendor_version_with_letters(self): + def test_version_string_with_letters(self): self._assert_version_correct( - "Aldec, Inc. VHDL compiler version 10.5a.12.6914 built for Windows on June 06, 2018.", + "10.5a.12.6914", 10, 5, "a", ) -class TestVersion(unittest.TestCase): +class TestActiveHDLVersion(unittest.TestCase): """ - Test the Version class. + Test ActiveHDL version comparisons. Test cases = (assert true, assert false) x (with letters, without letters, mixed) x number_of_operations Where number_of_operations = 1 for <, and 2 for <=, etc """ - high_version_letter = Version(10, 5, "b") - low_version_letter = Version(10, 5, "a") - high_version_no_letter = Version(10, 6) - low_version_no_letter = Version(10, 5) - high_version_letter_for_mixed = Version(10, 6, "") + high_version_letter = ActiveHDLVersion("10.5b") + low_version_letter = ActiveHDLVersion("10.5a") + high_version_no_letter = ActiveHDLVersion("10.6") + low_version_no_letter = ActiveHDLVersion("10.5") + high_version_letter_for_mixed = ActiveHDLVersion("10.6") + + def test_versions_are_ordered_by_major_minor_and_minor_letter(self): + self.assertLess(ActiveHDLVersion("10.5"), ActiveHDLVersion("10.5a")) + self.assertLess(ActiveHDLVersion("10.5a"), ActiveHDLVersion("10.5b")) + self.assertLess(ActiveHDLVersion("10.5b"), ActiveHDLVersion("10.6")) + self.assertEqual(ActiveHDLVersion("10.5a.12.6914"), ActiveHDLVersion("10.5a")) + self.assertEqual(str(ActiveHDLVersion("10.5a")), "10.5a") + + def test_from_output_extracts_versions_from_vendor_output(self): + self.assertEqual( + ActiveHDLVersion.from_output( + "Aldec, Inc. VHDL compiler version 10.5.216.6767 built for Windows on January 20, 2018." + ), + ActiveHDLVersion("10.5"), + ) + self.assertEqual( + ActiveHDLVersion.from_output( + "Aldec, Inc. VHDL compiler version 10.5a.12.6914 built for Windows on June 06, 2018." + ), + ActiveHDLVersion("10.5a"), + ) + + def test_from_output_returns_unknown_when_version_cannot_be_parsed(self): + with self.assertWarnsRegex(RuntimeWarning, "version is unknown"): + version = ActiveHDLVersion.from_output("Active-HDL version unavailable") + + self.assertFalse(version.is_valid) + self.assertEqual(str(version), "unknown") + + def test_unknown_versions_cannot_be_ordered(self): + with self.assertWarns(RuntimeWarning): + version = ActiveHDLVersion("unknown") + + with self.assertRaisesRegex(ValueError, "Cannot order unknown ActiveHDL versions"): + version < ActiveHDLVersion("10.5") def test_lt(self): # Test with letters - self.assertTrue(TestVersion.low_version_letter < TestVersion.high_version_letter) - self.assertFalse(TestVersion.high_version_letter < TestVersion.low_version_letter) + self.assertTrue(TestActiveHDLVersion.low_version_letter < TestActiveHDLVersion.high_version_letter) + self.assertFalse(TestActiveHDLVersion.high_version_letter < TestActiveHDLVersion.low_version_letter) # Test without letters - self.assertTrue(TestVersion.low_version_no_letter < TestVersion.high_version_no_letter) - self.assertFalse(TestVersion.high_version_no_letter < TestVersion.low_version_no_letter) + self.assertTrue(TestActiveHDLVersion.low_version_no_letter < TestActiveHDLVersion.high_version_no_letter) + self.assertFalse(TestActiveHDLVersion.high_version_no_letter < TestActiveHDLVersion.low_version_no_letter) # Both - self.assertTrue(TestVersion.low_version_letter < TestVersion.high_version_no_letter) - self.assertFalse(TestVersion.high_version_letter < TestVersion.low_version_no_letter) + self.assertTrue(TestActiveHDLVersion.low_version_letter < TestActiveHDLVersion.high_version_no_letter) + self.assertFalse(TestActiveHDLVersion.high_version_letter < TestActiveHDLVersion.low_version_no_letter) def test_le(self): # Test equal # Test with letters - self.assertTrue(TestVersion.high_version_letter <= TestVersion.high_version_letter) - self.assertFalse(TestVersion.high_version_letter <= TestVersion.low_version_letter) + self.assertTrue(TestActiveHDLVersion.high_version_letter <= TestActiveHDLVersion.high_version_letter) + self.assertFalse(TestActiveHDLVersion.high_version_letter <= TestActiveHDLVersion.low_version_letter) # Test without letters - self.assertTrue(TestVersion.high_version_no_letter <= TestVersion.high_version_no_letter) - self.assertFalse(TestVersion.high_version_no_letter <= TestVersion.low_version_no_letter) + self.assertTrue(TestActiveHDLVersion.high_version_no_letter <= TestActiveHDLVersion.high_version_no_letter) + self.assertFalse(TestActiveHDLVersion.high_version_no_letter <= TestActiveHDLVersion.low_version_no_letter) # Both - self.assertTrue(TestVersion.high_version_letter <= TestVersion.high_version_no_letter) - self.assertFalse(TestVersion.high_version_letter <= TestVersion.low_version_no_letter) + self.assertTrue(TestActiveHDLVersion.high_version_letter <= TestActiveHDLVersion.high_version_no_letter) + self.assertFalse(TestActiveHDLVersion.high_version_letter <= TestActiveHDLVersion.low_version_no_letter) # Test less than # Test with letters - self.assertTrue(TestVersion.low_version_letter <= TestVersion.high_version_letter) - self.assertFalse(TestVersion.high_version_letter <= TestVersion.low_version_letter) + self.assertTrue(TestActiveHDLVersion.low_version_letter <= TestActiveHDLVersion.high_version_letter) + self.assertFalse(TestActiveHDLVersion.high_version_letter <= TestActiveHDLVersion.low_version_letter) # Test without letters - self.assertTrue(TestVersion.low_version_no_letter <= TestVersion.high_version_no_letter) - self.assertFalse(TestVersion.high_version_no_letter <= TestVersion.low_version_no_letter) + self.assertTrue(TestActiveHDLVersion.low_version_no_letter <= TestActiveHDLVersion.high_version_no_letter) + self.assertFalse(TestActiveHDLVersion.high_version_no_letter <= TestActiveHDLVersion.low_version_no_letter) # Both - self.assertTrue(TestVersion.low_version_letter <= TestVersion.high_version_no_letter) - self.assertFalse(TestVersion.high_version_letter <= TestVersion.low_version_no_letter) + self.assertTrue(TestActiveHDLVersion.low_version_letter <= TestActiveHDLVersion.high_version_no_letter) + self.assertFalse(TestActiveHDLVersion.high_version_letter <= TestActiveHDLVersion.low_version_no_letter) def test_gt(self): # Test with letters - self.assertTrue(TestVersion.high_version_letter > TestVersion.low_version_letter) - self.assertFalse(TestVersion.low_version_letter > TestVersion.high_version_letter) + self.assertTrue(TestActiveHDLVersion.high_version_letter > TestActiveHDLVersion.low_version_letter) + self.assertFalse(TestActiveHDLVersion.low_version_letter > TestActiveHDLVersion.high_version_letter) # Test without letters - self.assertTrue(TestVersion.high_version_no_letter > TestVersion.low_version_no_letter) - self.assertFalse(TestVersion.low_version_no_letter > TestVersion.high_version_no_letter) + self.assertTrue(TestActiveHDLVersion.high_version_no_letter > TestActiveHDLVersion.low_version_no_letter) + self.assertFalse(TestActiveHDLVersion.low_version_no_letter > TestActiveHDLVersion.high_version_no_letter) # Both - self.assertTrue(TestVersion.high_version_letter > TestVersion.low_version_no_letter) - self.assertFalse(TestVersion.low_version_letter > TestVersion.high_version_no_letter) + self.assertTrue(TestActiveHDLVersion.high_version_letter > TestActiveHDLVersion.low_version_no_letter) + self.assertFalse(TestActiveHDLVersion.low_version_letter > TestActiveHDLVersion.high_version_no_letter) def test_ge(self): # Test equal # Test with letters - self.assertTrue(TestVersion.high_version_letter >= TestVersion.high_version_letter) - self.assertFalse(TestVersion.low_version_letter >= TestVersion.high_version_letter) + self.assertTrue(TestActiveHDLVersion.high_version_letter >= TestActiveHDLVersion.high_version_letter) + self.assertFalse(TestActiveHDLVersion.low_version_letter >= TestActiveHDLVersion.high_version_letter) # Test without letters - self.assertTrue(TestVersion.high_version_no_letter >= TestVersion.high_version_no_letter) - self.assertFalse(TestVersion.low_version_no_letter >= TestVersion.high_version_no_letter) + self.assertTrue(TestActiveHDLVersion.high_version_no_letter >= TestActiveHDLVersion.high_version_no_letter) + self.assertFalse(TestActiveHDLVersion.low_version_no_letter >= TestActiveHDLVersion.high_version_no_letter) # Both - self.assertTrue(TestVersion.high_version_letter_for_mixed >= TestVersion.high_version_no_letter) - self.assertFalse(TestVersion.low_version_letter >= TestVersion.high_version_no_letter) + self.assertTrue( + TestActiveHDLVersion.high_version_letter_for_mixed >= TestActiveHDLVersion.high_version_no_letter + ) + self.assertFalse(TestActiveHDLVersion.low_version_letter >= TestActiveHDLVersion.high_version_no_letter) # Test greater than # Test with letters - self.assertTrue(TestVersion.high_version_letter >= TestVersion.low_version_letter) - self.assertFalse(TestVersion.low_version_letter >= TestVersion.high_version_letter) + self.assertTrue(TestActiveHDLVersion.high_version_letter >= TestActiveHDLVersion.low_version_letter) + self.assertFalse(TestActiveHDLVersion.low_version_letter >= TestActiveHDLVersion.high_version_letter) # Test without letters - self.assertTrue(TestVersion.high_version_no_letter >= TestVersion.low_version_no_letter) - self.assertFalse(TestVersion.low_version_no_letter >= TestVersion.high_version_no_letter) + self.assertTrue(TestActiveHDLVersion.high_version_no_letter >= TestActiveHDLVersion.low_version_no_letter) + self.assertFalse(TestActiveHDLVersion.low_version_no_letter >= TestActiveHDLVersion.high_version_no_letter) # Both - self.assertTrue(TestVersion.high_version_letter >= TestVersion.low_version_no_letter) - self.assertFalse(TestVersion.low_version_letter >= TestVersion.high_version_no_letter) + self.assertTrue(TestActiveHDLVersion.high_version_letter >= TestActiveHDLVersion.low_version_no_letter) + self.assertFalse(TestActiveHDLVersion.low_version_letter >= TestActiveHDLVersion.high_version_no_letter) def test_eq(self): # Test with letters - self.assertTrue(TestVersion.high_version_letter == TestVersion.high_version_letter) - self.assertFalse(TestVersion.high_version_letter == TestVersion.low_version_letter) + self.assertTrue(TestActiveHDLVersion.high_version_letter == TestActiveHDLVersion.high_version_letter) + self.assertFalse(TestActiveHDLVersion.high_version_letter == TestActiveHDLVersion.low_version_letter) # Test without letters - self.assertTrue(TestVersion.high_version_no_letter == TestVersion.high_version_no_letter) - self.assertFalse(TestVersion.high_version_no_letter == TestVersion.low_version_no_letter) + self.assertTrue(TestActiveHDLVersion.high_version_no_letter == TestActiveHDLVersion.high_version_no_letter) + self.assertFalse(TestActiveHDLVersion.high_version_no_letter == TestActiveHDLVersion.low_version_no_letter) # Both - self.assertTrue(TestVersion.high_version_letter_for_mixed == TestVersion.high_version_no_letter) - self.assertFalse(TestVersion.high_version_letter == TestVersion.low_version_no_letter) + self.assertTrue( + TestActiveHDLVersion.high_version_letter_for_mixed == TestActiveHDLVersion.high_version_no_letter + ) + self.assertFalse(TestActiveHDLVersion.high_version_letter == TestActiveHDLVersion.low_version_no_letter) def test_ne(self): # Test with letters - self.assertTrue(TestVersion.high_version_letter != TestVersion.low_version_letter) - self.assertFalse(TestVersion.high_version_letter != TestVersion.high_version_letter) + self.assertTrue(TestActiveHDLVersion.high_version_letter != TestActiveHDLVersion.low_version_letter) + self.assertFalse(TestActiveHDLVersion.high_version_letter != TestActiveHDLVersion.high_version_letter) # Test without letters - self.assertTrue(TestVersion.high_version_no_letter != TestVersion.low_version_no_letter) - self.assertFalse(TestVersion.high_version_no_letter != TestVersion.high_version_no_letter) + self.assertTrue(TestActiveHDLVersion.high_version_no_letter != TestActiveHDLVersion.low_version_no_letter) + self.assertFalse(TestActiveHDLVersion.high_version_no_letter != TestActiveHDLVersion.high_version_no_letter) # Both - self.assertTrue(TestVersion.high_version_letter != TestVersion.low_version_no_letter) - self.assertFalse(TestVersion.high_version_letter_for_mixed != TestVersion.high_version_no_letter) + self.assertTrue(TestActiveHDLVersion.high_version_letter != TestActiveHDLVersion.low_version_no_letter) + self.assertFalse( + TestActiveHDLVersion.high_version_letter_for_mixed != TestActiveHDLVersion.high_version_no_letter + ) diff --git a/tests/unit/test_builtins.py b/tests/unit/test_builtins.py index d289f53a3..d265a6b53 100644 --- a/tests/unit/test_builtins.py +++ b/tests/unit/test_builtins.py @@ -17,10 +17,15 @@ from vunit import VUnit from vunit.builtins import Builtins, BuiltinsAdder from vunit.about import version +from vunit.sim_if.activehdl import ActiveHDLVersion +from vunit.sim_if.ghdl import GHDLVersion +from vunit.sim_if.modelsim import ModelSimVersion +from vunit.sim_if.rivierapro import RivieraProVersion from vunit.vhdl_standard import VHDL, VHDLStandard from vunit.project import Project from vunit.sim_if import hooks from vunit.sim_if.hooks import SimulatorHooks +from vunit.sim_if.nvc import NVCVersion from tests.common import create_tempdir from contextlib import contextmanager from importlib.machinery import ModuleSpec @@ -243,6 +248,227 @@ def test_raises_if_incompatible_vunit_version(self): ) self.builtins.add_package("foo") + def test_marker_expression_comparison_operators(self): + for expression, expected in ( + (f'vunit < "{version()}"', False), + (f'vunit <= "{version()}"', True), + (f'vunit == "{version()}"', True), + (f'vunit != "{version()}"', False), + (f'vunit >= "{version()}"', True), + (f'vunit > "{version()}"', False), + ): + with self.subTest(expression=expression): + self.assertEqual(self.builtins._evaluate_marker_expression(expression), expected) + + def test_marker_expression_logical_precedence_and_parentheses(self): + self.assertTrue(self.builtins._evaluate_marker_expression('vhdl == "2002" or vunit == "0" and vhdl == "2019"')) + self.assertFalse( + self.builtins._evaluate_marker_expression('(vhdl == "2002" or vunit == "0") and vhdl == "2019"') + ) + self.assertTrue( + self.builtins._evaluate_marker_expression(" ( vhdl == '2002' ) and vunit >= \"5.0.0.dev14\" ") + ) + + def test_marker_expression_simulator_marker(self): + simulator_class = mock.Mock() + simulator_class.name = "questa" + builtins = Builtins(self.vu, VHDL.STD_2008, simulator_class) + self.assertTrue(builtins._evaluate_marker_expression('simulator == "questa"')) + self.assertTrue(builtins._evaluate_marker_expression('simulator != "xsim"')) + self.assertTrue(self.builtins._evaluate_marker_expression('simulator != "xsim"')) + self.assertFalse(self.builtins._evaluate_marker_expression('simulator == "xsim"')) + + def test_marker_expression_nvc_version_marker(self): + simulator_class = mock.Mock() + simulator_class.name = "nvc" + simulator_class.find_prefix.return_value = "nvc_prefix" + simulator_class.determine_version.return_value = NVCVersion("1.16.2") + builtins = Builtins(self.vu, VHDL.STD_2008, simulator_class) + + self.assertTrue(builtins._evaluate_marker_expression('nvc_version >= "1.16"')) + self.assertTrue(builtins._evaluate_marker_expression('nvc_version == "1.16.2"')) + self.assertFalse(builtins._evaluate_marker_expression('nvc_version < "1.16"')) + simulator_class.determine_version.assert_called_with("nvc_prefix") + + def test_nvc_version_marker_is_unavailable_without_nvc(self): + self.assertFalse(self.builtins._evaluate_marker_expression('nvc_version == "1.16"')) + self.assertTrue(self.builtins._evaluate_marker_expression('nvc_version != "1.16"')) + + def test_nvc_version_marker_in_short_circuited_expression_without_nvc(self): + simulator_class = mock.Mock() + simulator_class.name = "questa" + builtins = Builtins(self.vu, VHDL.STD_2008, simulator_class) + + self.assertFalse(builtins._evaluate_marker_expression('simulator == "nvc" and nvc_version < "1.20"')) + self.assertTrue(builtins._evaluate_marker_expression('simulator != "nvc" or nvc_version >= "1.20"')) + + def test_simulator_version_markers(self): + marker_versions = ( + ("activehdl", "activehdl_version", "10.5a", "10.5"), + ("ghdl", "ghdl_version", "5.0.1", "5.0"), + ("modelsim", "modelsim_version", "2024.3", "2024.2"), + ("nvc", "nvc_version", "1.20", "1.19"), + ("rivierapro", "rivierapro_version", "2023.10", "2023.4"), + ) + version_classes = { + "activehdl": ActiveHDLVersion, + "ghdl": GHDLVersion, + "modelsim": ModelSimVersion, + "nvc": NVCVersion, + "rivierapro": RivieraProVersion, + } + + for simulator_name, marker, installed_version, older_version in marker_versions: + with self.subTest(marker=marker): + simulator_class = mock.Mock() + simulator_class.name = simulator_name + simulator_class.find_prefix.return_value = "simulator_prefix" + simulator_class.determine_version.return_value = version_classes[simulator_name](installed_version) + builtins = Builtins(self.vu, VHDL.STD_2008, simulator_class) + + self.assertTrue(builtins._evaluate_marker_expression(f'{marker} >= "{older_version}"')) + simulator_class.determine_version.assert_called_with("simulator_prefix") + + def test_simulator_version_marker_is_unavailable_on_other_simulator(self): + simulator_class = mock.Mock() + simulator_class.name = "ghdl" + builtins = Builtins(self.vu, VHDL.STD_2008, simulator_class) + + self.assertFalse(builtins._evaluate_marker_expression('nvc_version == "1.20"')) + self.assertTrue(builtins._evaluate_marker_expression('nvc_version != "1.20"')) + + def test_marker_expression_reports_invalid_syntax_and_values(self): + for expression, error in ( + ("vhdl >=", "Invalid marker expression syntax"), + ('vhdl >= "2019" and', "Invalid marker expression syntax"), + ('(vhdl >= "2019"', "Invalid marker expression syntax"), + ('vhdl "2019"', "Invalid marker expression syntax"), + ('vhdl => "2019"', "Unsupported marker comparison operator: =>"), + ('unknown_marker == "foo"', "Unknown environment marker 'unknown_marker'"), + ('vhdl == "not-a-standard"', "Invalid value 'not-a-standard' for environment marker 'vhdl'"), + ): + with self.subTest(expression=expression), self.assertRaisesRegex(RuntimeError, error): + self.builtins._evaluate_marker_expression(expression) + + def test_package_requires_expression(self): + with create_tempdir() as tempdir, pkg_env(tempdir): + self._write_toml( + tempdir, + """\ +[package] +requires = 'vunit >= "5.0.0.dev14" and vhdl == "2002"' +""", + ) + self.builtins.add_package("foo") + + with ( + create_tempdir() as tempdir, + pkg_env(tempdir), + self.assertRaisesRegex(RuntimeError, "does not satisfy requires expression"), + ): + self._write_toml(tempdir, "[package]\nrequires = 'vunit == \"0\"'\n") + self.builtins.add_package("foo") + + def test_package_requires_selects_compatible_vhdl_standard(self): + builtins = Builtins(self.vu, VHDLStandard("2008"), None) + with ( + create_tempdir() as tempdir, + pkg_env(tempdir), + self.assertLogs("vunit.builtins", "WARNING") as mock_warning, + ): + self._write_toml( + tempdir, + """\ +[package] +library = "bar" +requires = 'vhdl == "1993"' +[[package.sources]] +include = ["source.vhd"] +""", + ) + builtins.add_package("foo") + + self.library_mock.add_source_files.assert_called_once_with( + tempdir / "source.vhd", vhdl_standard=str(VHDL.STD_1993) + ) + self._assertLogContent( + mock_warning, + "WARNING", + f"Package foo requires a VHDL standard matching requires expression 'vhdl == \"1993\"' " + "but current standard is 2008. Proceeding with mixed-language compilation using VHDL standard " + f"{VHDL.STD_1993} for the package.", + ) + + def test_package_requires_selects_closest_vhdl_standard(self): + builtins = Builtins(self.vu, VHDL.STD_2019, None) + with ( + create_tempdir() as tempdir, + pkg_env(tempdir), + self.assertLogs("vunit.builtins", "WARNING") as mock_warning, + ): + self._write_toml( + tempdir, + """\ +[package] +library = "bar" +requires = 'vhdl <= "2008"' +[[package.sources]] +include = ["source.vhd"] +""", + ) + builtins.add_package("foo") + + self.library_mock.add_source_files.assert_called_once_with(tempdir / "source.vhd", vhdl_standard="2008") + self._assertLogContent( + mock_warning, + "WARNING", + "Package foo requires a VHDL standard matching requires expression 'vhdl <= \"2008\"' " + "but current standard is 2019. Proceeding with mixed-language compilation using VHDL standard " + "2008 for the package.", + ) + + def test_package_requires_combines_with_legacy_constraints(self): + builtins = Builtins(self.vu, VHDL.STD_2008, None) + manifests = ( + '[package]\nrequires-vunit = ">=5.0.0.dev14"\nrequires = \'vunit >= "5.0.0.dev14"\'\n', + '[package]\nrequires-vhdl = ">=2008"\nrequires = \'vhdl >= "2008"\'\n', + '[package]\nrequires-vunit = ">=5.0.0.dev14"\nrequires-vhdl = ">=2008"\n' + 'requires = \'vunit >= "5.0.0.dev14" and vhdl >= "2008"\'\n', + ) + for manifest in manifests: + with self.subTest(manifest=manifest), create_tempdir() as tempdir, pkg_env(tempdir): + self._write_toml(tempdir, manifest) + builtins.add_package("foo") + + def test_source_when_filters_source_sets(self): + with create_tempdir() as tempdir, pkg_env(tempdir): + self._write_toml( + tempdir, + """\ +[package] +library = "bar" +[[package.sources]] +include = ["common.vhd"] +[[package.sources]] +library = "osvvm" +include = ["matching.vhd"] +when = 'vhdl == "2002"' +[[package.sources]] +library = "unused" +include = ["excluded.vhd"] +when = 'vhdl == "2019"' +""", + ) + self.builtins.add_package("foo") + + self.library_mock.add_source_files.assert_has_calls( + [ + mock.call(tempdir / "common.vhd", vhdl_standard=None), + mock.call(tempdir / "matching.vhd", vhdl_standard=None), + ] + ) + self.assertNotIn("unused", self.vu._project._libraries) + def test_warns_if_multi_vhdl_standard(self): with ( create_tempdir() as tempdir, diff --git a/tests/unit/test_ghdl_interface.py b/tests/unit/test_ghdl_interface.py index e72fa8967..fb8193120 100644 --- a/tests/unit/test_ghdl_interface.py +++ b/tests/unit/test_ghdl_interface.py @@ -14,7 +14,7 @@ from shutil import rmtree from unittest import mock from tests.unit.test_test_bench import Entity -from vunit.sim_if.ghdl import GHDLInterface +from vunit.sim_if.ghdl import GHDLInterface, GHDLVersion from vunit.project import Project from vunit.ostools import renew_path, write_file from vunit.exceptions import CompileError @@ -22,6 +22,34 @@ from vunit.vhdl_standard import VHDL +class TestGHDLVersion(unittest.TestCase): + def test_versions_are_ordered_by_major_minor_and_patch(self): + self.assertLess(GHDLVersion("0.33dev"), GHDLVersion("3.0.0-dev")) + self.assertLess(GHDLVersion("5.0"), GHDLVersion("5.0.1")) + self.assertEqual(GHDLVersion("5.0"), GHDLVersion("5.0.0")) + self.assertEqual(str(GHDLVersion("5.0")), "5.0.0") + + def test_from_output_extracts_ghdl_version(self): + output = "GHDL 3.0.0-dev (2.0.0.r1369) [Dunoon edition]" + self.assertEqual(GHDLVersion.from_output(output), GHDLVersion("3.0.0-dev")) + + def test_unrecognized_version_warns_and_is_unknown(self): + with self.assertWarnsRegex(RuntimeWarning, "version is unknown"): + version = GHDLVersion.from_output("unrecognized GHDL output") + + self.assertFalse(version.is_valid) + self.assertEqual(str(version), "unknown") + with self.assertRaisesRegex(ValueError, "Cannot order unknown GHDL versions"): + version < GHDLVersion("5.0") + + @mock.patch.object(GHDLInterface, "_get_version_output", return_value="unrecognized GHDL output") + def test_determine_version_returns_unknown_on_unrecognized_output(self, _get_version_output): + with self.assertWarnsRegex(RuntimeWarning, "version is unknown"): + version = GHDLInterface.determine_version("prefix") + + self.assertFalse(version.is_valid) + + class TestGHDLInterface(unittest.TestCase): """ Test the GHDL interface @@ -138,7 +166,7 @@ def test_assertion_on_unknown_backend(self, check_output): self.assertRaises(AssertionError, GHDLInterface.determine_backend, "prefix") @mock.patch("vunit.sim_if.check_output", autospec=True, return_value="") # pylint: disable=no-self-use - @mock.patch.object(GHDLInterface, "determine_version", return_value=6.0) + @mock.patch.object(GHDLInterface, "determine_version", return_value=GHDLVersion("6.0")) def test_compile_project_2019(self, determine_version, check_output): simif = GHDLInterface(prefix="prefix", output_path="") write_file("file.vhd", "") diff --git a/tests/unit/test_modelsim_interface.py b/tests/unit/test_modelsim_interface.py index 248d03126..93b70899e 100644 --- a/tests/unit/test_modelsim_interface.py +++ b/tests/unit/test_modelsim_interface.py @@ -8,20 +8,51 @@ Test the ModelSim interface """ - import unittest from pathlib import Path import os from shutil import rmtree from unittest import mock from tests.common import set_env -from vunit.sim_if.modelsim import ModelSimInterface +from vunit.sim_if.modelsim import ModelSimInterface, ModelSimVersion from vunit.project import Project from vunit.ostools import renew_path, write_file from vunit.test.bench import Configuration from vunit.vhdl_standard import VHDL +class TestModelSimVersion(unittest.TestCase): + def test_versions_are_ordered_by_numeric_components(self): + self.assertLess(ModelSimVersion("10.7"), ModelSimVersion("10.10")) + self.assertLess(ModelSimVersion("2024.2"), ModelSimVersion("2024.3")) + self.assertEqual(ModelSimVersion("2024.3"), ModelSimVersion("2024.3.0")) + self.assertEqual(str(ModelSimVersion("2024.3")), "2024.3.0") + + def test_from_output_extracts_modelsims_version(self): + output = "Model Technology ModelSim SE-64 vcom 2024.3" + self.assertEqual(ModelSimVersion.from_output(output), ModelSimVersion("2024.3")) + + def test_from_output_extracts_questa_version(self): + output = "QuestaSim-64 vcom 2024.3 Compiler 2024.10 Oct 18 2024" + self.assertEqual(ModelSimVersion.from_output(output), ModelSimVersion("2024.3")) + + def test_unrecognized_version_warns_and_is_unknown(self): + with self.assertWarnsRegex(RuntimeWarning, "version is unknown"): + version = ModelSimVersion.from_output("unrecognized simulator output") + + self.assertFalse(version.is_valid) + self.assertEqual(str(version), "unknown") + with self.assertRaisesRegex(ValueError, "Cannot order unknown ModelSim versions"): + version < ModelSimVersion("2024.3") + + @mock.patch("vunit.sim_if.modelsim.check_output", return_value="unrecognized simulator output") + def test_determine_version_returns_unknown_on_unrecognized_output(self, _check_output): + with self.assertWarnsRegex(RuntimeWarning, "version is unknown"): + version = ModelSimInterface.determine_version("prefix") + + self.assertFalse(version.is_valid) + + class TestModelSimInterface(unittest.TestCase): """ Test the ModelSim interface @@ -266,7 +297,7 @@ def _get_inis(self, name="modelsim"): @mock.patch("vunit.sim_if.modelsim.check_output", autospec=True, return_value="") def test_copies_modelsim_ini_file_from_install(self, _check_output): - (modelsim_ini, installed_modelsim_ini, user_modelsim_ini) = self._get_inis() + modelsim_ini, installed_modelsim_ini, user_modelsim_ini = self._get_inis() with open(installed_modelsim_ini, "w") as fptr: fptr.write("[Library]\ninstalled=installed") @@ -280,7 +311,7 @@ def test_copies_modelsim_ini_file_from_install(self, _check_output): @mock.patch("vunit.sim_if.modelsim.check_output", autospec=True, return_value="") def test_copies_modelsim_ini_file_from_user(self, _check_output): - (modelsim_ini, installed_modelsim_ini, user_modelsim_ini) = self._get_inis() + modelsim_ini, installed_modelsim_ini, user_modelsim_ini = self._get_inis() with open(installed_modelsim_ini, "w") as fptr: fptr.write("[Library]\ninstalled=installed") @@ -296,7 +327,7 @@ def test_copies_modelsim_ini_file_from_user(self, _check_output): @mock.patch("vunit.sim_if.modelsim.check_output", autospec=True, return_value="") def test_overwrites_modelsim_ini_file_from_install(self, _check_output): - (modelsim_ini, installed_modelsim_ini, user_modelsim_ini) = self._get_inis() + modelsim_ini, installed_modelsim_ini, user_modelsim_ini = self._get_inis() with open(modelsim_ini, "w") as fptr: fptr.write("[Library]\nexisting=existing") @@ -313,7 +344,7 @@ def test_overwrites_modelsim_ini_file_from_install(self, _check_output): @mock.patch("vunit.sim_if.modelsim.check_output", autospec=True, return_value="") def test_overwrites_modelsim_ini_file_from_user(self, _check_output): - (modelsim_ini, installed_modelsim_ini, user_modelsim_ini) = self._get_inis() + modelsim_ini, installed_modelsim_ini, user_modelsim_ini = self._get_inis() with open(modelsim_ini, "w") as fptr: fptr.write("[Library]\nexisting=existing") @@ -332,7 +363,7 @@ def test_overwrites_modelsim_ini_file_from_user(self, _check_output): @mock.patch("vunit.sim_if.vsim_simulator_mixin.Process", autospec=True) def test_modelsim_ini_file_detection(self, vsim_simulator_mixin_process): - (modelsim_ini, _, user_modelsim_ini) = self._get_inis() + modelsim_ini, _, user_modelsim_ini = self._get_inis() with open(user_modelsim_ini, "w") as fptr: fptr.write("[Library]\nuser=user") @@ -357,7 +388,7 @@ def check_output(*args, **kwargs): @mock.patch("vunit.sim_if.vsim_simulator_mixin.Process", autospec=True) def test_questa_ini_file_detection(self, vsim_simulator_mixin_process): - (questa_ini, installed_questa_ini, user_questa_ini) = self._get_inis("questa") + questa_ini, installed_questa_ini, user_questa_ini = self._get_inis("questa") with open(user_questa_ini, "w") as fptr: fptr.write("[Library]\nuser=user") @@ -386,7 +417,7 @@ def test_non_standard_ini_name(self, vsim_simulator_mixin_process): # We need to remove the INI file created in setUp since we testing a non-standard INI file name. (Path(self.prefix_path) / ".." / "modelsim.ini").unlink() - (questasim_ini, installed_questasim_ini, user_questasim_ini) = self._get_inis("questasim") + questasim_ini, installed_questasim_ini, user_questasim_ini = self._get_inis("questasim") with open(user_questasim_ini, "w") as fptr: fptr.write("[Library]\nuser=user") diff --git a/tests/unit/test_rivierapro_interface.py b/tests/unit/test_rivierapro_interface.py index 62a11b30d..b881a7431 100644 --- a/tests/unit/test_rivierapro_interface.py +++ b/tests/unit/test_rivierapro_interface.py @@ -8,18 +8,47 @@ Test the RivieraPro interface """ - import unittest from pathlib import Path import os from shutil import rmtree from unittest import mock -from vunit.sim_if.rivierapro import RivieraProInterface +from vunit.sim_if.rivierapro import RivieraProInterface, RivieraProVersion from vunit.project import Project from vunit.ostools import renew_path, write_file from vunit.vhdl_standard import VHDL +class TestRivieraProVersion(unittest.TestCase): + def test_versions_are_ordered_by_year_and_release_month(self): + self.assertLess(RivieraProVersion("2022.10"), RivieraProVersion("2023.4")) + self.assertLess(RivieraProVersion("2023.4"), RivieraProVersion("2023.10.123")) + self.assertEqual(RivieraProVersion("2023.04.123"), RivieraProVersion("2023.4")) + self.assertEqual(str(RivieraProVersion("2023.04")), "2023.4") + + def test_from_output_extracts_rivierapro_version(self): + output = "Aldec Riviera-PRO version 2023.04.123" + self.assertEqual(RivieraProVersion.from_output(output), RivieraProVersion("2023.4")) + + def test_unrecognized_version_warns_and_is_unknown(self): + with self.assertWarnsRegex(RuntimeWarning, "version is unknown"): + version = RivieraProVersion.from_output("unrecognized simulator output") + + self.assertFalse(version.is_valid) + self.assertEqual(str(version), "unknown") + with self.assertRaisesRegex(ValueError, "Cannot order unknown Riviera-PRO versions"): + version < RivieraProVersion("2023.4") + + @mock.patch("vunit.sim_if.rivierapro.Process") + def test_determine_version_returns_unknown_on_unrecognized_output(self, process): + process.return_value.consume_output.side_effect = lambda callback: callback("unrecognized Riviera-PRO output") + + with self.assertWarnsRegex(RuntimeWarning, "version is unknown"): + version = RivieraProInterface.determine_version("prefix") + + self.assertFalse(version.is_valid) + + class TestRivieraProInterface(unittest.TestCase): """ Test the RivieraPro interface @@ -28,7 +57,8 @@ class TestRivieraProInterface(unittest.TestCase): @mock.patch("vunit.sim_if.check_output", autospec=True, return_value="") @mock.patch("vunit.sim_if.rivierapro.Process", autospec=True) @mock.patch("vunit.sim_if.rivierapro.RivieraProInterface.find_prefix", return_value="prefix") - def test_compile_project_vhdl_2019(self, _find_prefix, process, check_output): + @mock.patch.object(RivieraProInterface, "_get_version", return_value=RivieraProVersion("2023.4")) + def test_compile_project_vhdl_2019(self, _get_version, _find_prefix, process, check_output): simif = RivieraProInterface(prefix="prefix", output_path=self.output_path) project = Project() project.add_library("lib", "lib_path") diff --git a/tests/unit/test_sim_if_hooks.py b/tests/unit/test_sim_if_hooks.py index d00695a74..3cf3de80a 100644 --- a/tests/unit/test_sim_if_hooks.py +++ b/tests/unit/test_sim_if_hooks.py @@ -20,7 +20,7 @@ from vunit.sim_if.activehdl import ActiveHDLInterface from vunit.sim_if.ghdl import GHDLInterface from vunit.sim_if.modelsim import ModelSimInterface -from vunit.sim_if.nvc import NVCInterface +from vunit.sim_if.nvc import NVCInterface, NVCVersion from vunit.sim_if.rivierapro import RivieraProInterface from vunit.sim_if.vsim_simulator_mixin import VsimSimulatorMixin from vunit.vhdl_standard import VHDL @@ -210,7 +210,7 @@ def test_ghdl_uses_hooks(self, determine_version): self.assertEqual(process.call_args[1]["env"]["FOO"], "1") - @mock.patch.object(NVCInterface, "determine_version", return_value=(1, 16)) + @mock.patch.object(NVCInterface, "determine_version", return_value=NVCVersion("1.16")) def test_nvc_uses_hooks(self, determine_version): self.hooks.register( "nvc", diff --git a/vunit/builtins.py b/vunit/builtins.py index 240177e70..8674bed4c 100755 --- a/vunit/builtins.py +++ b/vunit/builtins.py @@ -12,12 +12,11 @@ from glob import glob import logging import importlib -import importlib.resources import importlib.util import re import operator from dataclasses import dataclass -from typing import TypeVar, Any, Optional, Tuple +from typing import Any, Optional, Tuple try: # Python 3.11+ @@ -29,6 +28,11 @@ from vunit.ui.common import get_checked_file_names_from_globs from vunit.about import version, VUnitVersion from vunit.package_context import PackageContext +from vunit.sim_if.activehdl import ActiveHDLVersion +from vunit.sim_if.ghdl import GHDLVersion +from vunit.sim_if.modelsim import ModelSimVersion +from vunit.sim_if.nvc import NVCVersion +from vunit.sim_if.rivierapro import RivieraProVersion LOGGER = logging.getLogger(__name__) @@ -45,7 +49,116 @@ class ValidationError: message: str -VersionT = TypeVar("VersionT", VHDLStandard, VUnitVersion) +SIMULATOR_VERSION_MARKERS = { + "activehdl_version": ("activehdl", ActiveHDLVersion), + "ghdl_version": ("ghdl", GHDLVersion), + "modelsim_version": ("modelsim", ModelSimVersion), + "nvc_version": ("nvc", NVCVersion), + "rivierapro_version": ("rivierapro", RivieraProVersion), +} + + +class _MarkerExpressionParser: + """Parse the supported subset of PEP 508 marker expressions.""" + + _TOKEN_RE = re.compile( + r"(?P\s+)|(?P===|~=|=>|<=|>=|==|!=|[<>=])|(?P\()|(?P\))|" + r"(?Pand\b|or\b)|(?P[A-Za-z_][A-Za-z0-9_]*)|" + r"(?P\"(?:\\.|[^\"\\])*\"|'(?:\\.|[^'\\])*')" + ) + _SUPPORTED_OPERATORS = {"<", "<=", "==", "!=", ">=", ">"} + + def __init__(self, expression: str): + """Initialize the parser and tokenize the marker expression.""" + self._expression = expression + self._tokens = self._tokenize(expression) + self._index = 0 + + @classmethod + def _tokenize(cls, expression: str) -> list[tuple[str, str]]: + """Tokenize the marker expression according to the supported grammar.""" + tokens = [] + position = 0 + while position < len(expression): + match = cls._TOKEN_RE.match(expression, position) + if match is None: + raise RuntimeError(f"Invalid marker expression syntax at position {position}: {expression!r}.") + position = match.end() + token_type = match.lastgroup + if token_type is not None and token_type != "space": + tokens.append((token_type, match.group(token_type))) + return tokens + + def _peek(self) -> Optional[tuple[str, str]]: + """Return the next token without consuming it.""" + return self._tokens[self._index] if self._index < len(self._tokens) else None + + def _take(self, token_type: str) -> Optional[str]: + """Consume and return a token of the requested type when present.""" + token = self._peek() + if token is not None and token[0] == token_type: + self._index += 1 + return token[1] + return None + + def parse(self) -> tuple: + """Parse the full marker expression and return the syntax tree.""" + if not self._tokens: + raise RuntimeError(f"Invalid marker expression syntax: {self._expression!r} is empty.") + result = self._parse_or() + if self._peek() is not None: + self._syntax_error("unexpected token") + return result + + def _parse_or(self) -> tuple: + """Parse an OR expression.""" + result = self._parse_and() + while self._peek() == ("keyword", "or"): + self._index += 1 + result = ("or", result, self._parse_and()) + return result + + def _parse_and(self) -> tuple: + """Parse an AND expression.""" + result = self._parse_comparison() + while self._peek() == ("keyword", "and"): + self._index += 1 + result = ("and", result, self._parse_comparison()) + return result + + def _parse_comparison(self) -> tuple: + """Parse a single comparison expression.""" + if self._take("left") is not None: + result = self._parse_or() + if self._take("right") is None: + self._syntax_error("expected ')'") + return result + + marker = self._take("identifier") + if marker is None: + self._syntax_error("expected a marker name") + + operator_token = self._take("operator") + if operator_token is None: + self._syntax_error("expected a comparison operator") + if operator_token not in self._SUPPORTED_OPERATORS: + raise RuntimeError(f"Unsupported marker comparison operator: {operator_token}.") + + value_token = self._take("string") + if value_token is None: + self._syntax_error("expected a quoted string value") + # This is to satisfy the type checker. _syntax_error always raises an exception, so the following line is + # never reached. + assert value_token is not None + + value = re.sub(r"\\([\\'\"])", r"\1", value_token[1:-1]) + return ("compare", marker, operator_token, value) + + def _syntax_error(self, reason: str) -> None: + """Raise a syntax error for the current parser state.""" + token = self._peek() + location = f" near {token[1]!r}" if token is not None else " at end of expression" + raise RuntimeError(f"Invalid marker expression syntax: {reason}{location} in {self._expression!r}.") class Builtins(object): @@ -82,16 +195,16 @@ def add(self, name, args=None): "<": operator.lt, } - def _meets_required_version( + def _meets_version_specifier( self, - version_class: type[VersionT], + version_class: type, current_version_str: str, - required_version_specifier: str, + version_specifier: str, ) -> bool: - """Check if the current VUnit version meets the required version.""" + """Check whether a version satisfies a comma-separated specifier.""" current_version = version_class(current_version_str) - requirement_list = required_version_specifier.split(",") + requirement_list = version_specifier.split(",") for requirement in requirement_list: requirement = requirement.strip() # Skip empty requirements since trailing commas are allowed @@ -118,6 +231,67 @@ def _meets_required_version( return True + def _evaluate_marker_expression(self, expression: str, vhdl_standard: Optional[VHDLStandard] = None) -> bool: + """Evaluate an expression against the explicitly supported environment markers.""" + syntax = _MarkerExpressionParser(expression).parse() + marker_values = { + "vunit": (version(), VUnitVersion), + "vhdl": (str(vhdl_standard if vhdl_standard is not None else self._vhdl_standard), VHDLStandard), + "simulator": (getattr(self._simulator_class, "name", None), None), + } + marker_values.update( + {marker: (None, version_class) for marker, (_, version_class) in SIMULATOR_VERSION_MARKERS.items()} + ) + + def evaluate(node: tuple) -> bool: + kind = node[0] + if kind == "compare": + _, marker, version_operator, required_value = node + if marker not in marker_values: + raise RuntimeError(f"Unknown environment marker {marker!r}.") + current_value, version_class = marker_values[marker] + if marker in SIMULATOR_VERSION_MARKERS: + simulator_name, _ = SIMULATOR_VERSION_MARKERS[marker] + if getattr(self._simulator_class, "name", None) == simulator_name: + current_value = str( + self._simulator_class.determine_version(self._simulator_class.find_prefix()) + ) + if current_value is None: + if version_operator == "==": + return False + if version_operator == "!=": + return True + if marker in SIMULATOR_VERSION_MARKERS: + simulator_name, _ = SIMULATOR_VERSION_MARKERS[marker] + raise RuntimeError( + f"The {marker} marker is unavailable because {simulator_name} was not selected." + ) + raise RuntimeError("The simulator marker is unavailable because no simulator was selected.") + try: + if version_class is not None: + return self._meets_version_specifier( + version_class, current_value, f"{version_operator}{required_value}" + ) + return self._OPERATORS[version_operator](current_value, required_value) + except (TypeError, ValueError) as exc: + raise RuntimeError(f"Invalid value {required_value!r} for environment marker {marker!r}.") from exc + if kind == "and": + return evaluate(node[1]) and evaluate(node[2]) + if kind == "or": + return evaluate(node[1]) or evaluate(node[2]) + raise AssertionError(f"Unknown expression node: {kind}") + + def validate_markers(node: tuple) -> None: + if node[0] == "compare": + if node[1] not in marker_values: + raise RuntimeError(f"Unknown environment marker {node[1]!r}.") + return + validate_markers(node[1]) + validate_markers(node[2]) + + validate_markers(syntax) + return evaluate(syntax) + @staticmethod def _to_toml_type(python_type: type) -> Tuple[str, str]: """Translate Python type to TOML type terminology.""" @@ -225,6 +399,7 @@ def _validate_toml(self, data: dict) -> None: valid={ "requires-vunit": str, "requires-vhdl": str, + "requires": str, "library": str, "sources": list, "setup": str, @@ -237,6 +412,7 @@ def _validate_toml(self, data: dict) -> None: if isinstance(package.get("compile_option"), list): errors.extend(self._check_compile_options(package["compile_option"], "package.compile_option")) + # pylint: disable=too-many-nested-blocks if "sources" in package: errors.extend(self._check_mandatory_keys(package, mandatory={"library": str}, path="package")) @@ -244,7 +420,7 @@ def _validate_toml(self, data: dict) -> None: errors.extend( self._check_valid_keys( source, - valid={"include": list, "library": str, "compile_option": list}, + valid={"include": list, "library": str, "compile_option": list, "when": str}, path=f"package.sources[{idx}]", ) ) @@ -252,9 +428,13 @@ def _validate_toml(self, data: dict) -> None: errors.extend( self._check_compile_options(source["compile_option"], f"package.sources[{idx}].compile_option") ) - errors.extend( - self._check_mandatory_keys(source, mandatory={"include": list}, path=f"package.sources[{idx}]") - ) + if "include" not in source: + errors.append( + ValidationError( + path=f"package.sources[{idx}]", + message="Missing mandatory array 'include'.", + ) + ) if "include" in source: for list_idx, path in enumerate(source["include"]): if not isinstance(path, str): @@ -330,7 +510,7 @@ def add_package(self, package_name: str, allow_setup: bool = False) -> None: # package = data["package"] vunit_version = version() - if not self._meets_required_version(VUnitVersion, vunit_version, package.get("requires-vunit", "")): + if not self._meets_version_specifier(VUnitVersion, vunit_version, package.get("requires-vunit", "")): raise RuntimeError( f"Package {package_name} requires VUnit version " f"{package['requires-vunit']} but current version is {vunit_version}." @@ -339,7 +519,11 @@ def add_package(self, package_name: str, allow_setup: bool = False) -> None: # use_vhdl_standard = self._find_vhdl_standard(package_name, package) library = None - sources = package.get("sources", []) + sources = [ + source + for source in package.get("sources", []) + if "when" not in source or self._evaluate_marker_expression(source["when"]) + ] if sources: package_compile_options = dict(package.get("compile_option", [])) library_name = package.get("library") @@ -366,7 +550,7 @@ def add_package(self, package_name: str, allow_setup: bool = False) -> None: # compile_options = package_compile_options.copy() compile_options.update(source.get("compile_option", [])) - for include in source["include"]: + for include in source.get("include", []): source_files = source_library.add_source_files( package_root / include, vhdl_standard=use_vhdl_standard ) @@ -403,32 +587,81 @@ def _find_vhdl_standard(self, package_name: str, package: dict) -> Optional[str] standard the package supports, and None stands for the standard of the project. """ package_vhdl_standard = package.get("requires-vhdl", "") - if self._meets_required_version(VHDLStandard, str(self._vhdl_standard), package_vhdl_standard): + requires_expression = package.get("requires") + standard_order = {standard: index for index, standard in enumerate(VHDL.STANDARDS)} + current_standard_index = standard_order[self._vhdl_standard] + candidate_standards = [self._vhdl_standard] + [ + *sorted( + (standard for standard in VHDL.STANDARDS if standard != self._vhdl_standard), + key=lambda standard: abs(standard_order[standard] - current_standard_index), + ) + ] + unavailable_marker_errors = [] + + def meets_requires(standard: VHDLStandard) -> bool: + if requires_expression is None: + return True + try: + return self._evaluate_marker_expression(requires_expression, vhdl_standard=standard) + except RuntimeError as exc: + if "marker is unavailable" not in str(exc): + raise + unavailable_marker_errors.append(exc) + return False + + def meets_vhdl_requirement(standard: VHDLStandard) -> bool: + return self._meets_version_specifier(VHDLStandard, str(standard), package_vhdl_standard) + + if meets_requires(self._vhdl_standard) and meets_vhdl_requirement(self._vhdl_standard): return None use_vhdl_standard = next( ( str(standard) - for standard in VHDL.STANDARDS - if self._meets_required_version(VHDLStandard, str(standard), package_vhdl_standard) + for standard in candidate_standards[1:] + if meets_requires(standard) and meets_vhdl_requirement(standard) ), None, ) if not use_vhdl_standard: + if requires_expression is not None: + requires_matches_any_standard = any(meets_requires(standard) for standard in candidate_standards) + if not requires_matches_any_standard: + if unavailable_marker_errors: + raise unavailable_marker_errors[0] + raise RuntimeError( + f"Package {package_name} does not satisfy requires expression {requires_expression!r} " + "in the current environment." + ) + if package_vhdl_standard: + raise RuntimeError( + f"Package {package_name} requires VHDL standard " + f"{package_vhdl_standard}. Failed to find a compatible standard." + ) raise RuntimeError( - f"Package {package_name} requires VHDL standard " - f"{package['requires-vhdl']}. Failed to find a compatible standard." + f"Package {package_name} does not satisfy requires expression {requires_expression!r} " + "in the current environment." ) - LOGGER.warning( - "Package %s requires VHDL standard %s but current standard is %s. " - "Proceeding with mixed-language compilation using VHDL standard %s for the package.", - package_name, - package["requires-vhdl"], - self._vhdl_standard, - use_vhdl_standard, - ) + if package_vhdl_standard and not meets_vhdl_requirement(self._vhdl_standard): + LOGGER.warning( + "Package %s requires VHDL standard %s but current standard is %s. " + "Proceeding with mixed-language compilation using VHDL standard %s for the package.", + package_name, + package_vhdl_standard, + self._vhdl_standard, + use_vhdl_standard, + ) + else: + LOGGER.warning( + "Package %s requires a VHDL standard matching requires expression %r but current standard is %s. " + "Proceeding with mixed-language compilation using VHDL standard %s for the package.", + package_name, + requires_expression, + self._vhdl_standard, + use_vhdl_standard, + ) return use_vhdl_standard @@ -552,6 +785,7 @@ def _add_osvvm(self): """ Add osvvm library """ + library = self._add_library_if_not_exist( "osvvm", "Library 'OSVVM' previously defined. Skipping addition of builtin OSVVM (2023.04)." ) diff --git a/vunit/sim_if/activehdl.py b/vunit/sim_if/activehdl.py index 41d2a832a..29c021597 100644 --- a/vunit/sim_if/activehdl.py +++ b/vunit/sim_if/activehdl.py @@ -14,6 +14,7 @@ import re import logging import sys +import warnings from ..exceptions import CompileError from ..ostools import Process, write_file, file_exists, renew_path from ..test.suites import get_result_file_name @@ -54,6 +55,14 @@ def from_args(cls, args, output_path, **kwargs): def find_prefix_from_path(cls): return cls.find_toolchain(["vsim", "avhdl"]) + @classmethod + def determine_version(cls, prefix): + """Determine the ActiveHDL version.""" + proc = Process([str(Path(prefix) / "vcom"), "-version"], env=cls.get_env()) + consumer = VersionConsumer() + proc.consume_output(consumer) + return consumer.version + @classmethod def supports_vhdl_call_paths(cls): """ @@ -69,8 +78,8 @@ def supports_vhdl_package_generics(cls): proc = Process([str(Path(cls.find_prefix()) / "vcom"), "-version"], env=cls.get_env()) consumer = VersionConsumer() proc.consume_output(consumer) - if consumer.version is not None: - return consumer.version >= Version(10, 1) + if consumer.version.is_valid: + return consumer.version >= ActiveHDLVersion("10.1") return False @@ -546,60 +555,73 @@ def simulate(self, output_path, test_suite_name, config, elaborate_only): return self._run_batch_file(str(batch_file_name), gui=False, cwd=str(Path(self._library_cfg).parent)) -@total_ordering -class Version(object): +class VersionConsumer(object): """ - Simulator version + Consume version information """ - def __init__(self, major=0, minor=0, minor_letter=""): - self.major = major - self.minor = minor - self.minor_letter = minor_letter - - def _compare(self, other, greater_than, less_than, equal_to): - """ - Compares this object with another - """ - if self.major > other.major: - result = greater_than - elif self.major < other.major: - result = less_than - elif self.minor > other.minor: - result = greater_than - elif self.minor < other.minor: - result = less_than - elif self.minor_letter > other.minor_letter: - result = greater_than - elif self.minor_letter < other.minor_letter: - result = less_than - else: - result = equal_to + def __init__(self): + self.version = None - return result + def __call__(self, line): + self.version = ActiveHDLVersion.from_output(line) - def __lt__(self, other): - return self._compare(other, greater_than=False, less_than=True, equal_to=False) + return True - def __eq__(self, other): - return self._compare(other, greater_than=False, less_than=False, equal_to=True) +@total_ordering +class ActiveHDLVersion: + """ActiveHDL version number, possibly unknown.""" -class VersionConsumer(object): - """ - Consume version information - """ + _VERSION_RE = re.compile(r"(?P\d+)\.(?P\d+)(?P[a-zA-Z]?)(?:\.\d+\.\d+)?") - def __init__(self): - self.version = None + def __init__(self, version_string): + self.version_string = version_string + self._version = None - _version_re = re.compile(r"(?P\d+)\.(?P\d+)(?P[a-zA-Z]?)\.\d+\.\d+") + match = self._VERSION_RE.fullmatch(version_string) + if not match: + warnings.warn( + f"Cannot parse ActiveHDL version: {version_string!r}; version is unknown", + RuntimeWarning, + stacklevel=2, + ) + return - def __call__(self, line): - match = self._version_re.search(line) - if match is not None: - major = int(match.group("major")) - minor = int(match.group("minor")) - minor_letter = match.group("minor_letter") - self.version = Version(major, minor, minor_letter) - return True + self._version = (int(match.group("major")), int(match.group("minor")), match.group("minor_letter")) + + @classmethod + def from_output(cls, output): + """Extract the first version from tool output. + + If none is found, return an unknown version and issue a warning. + """ + match = cls._VERSION_RE.search(output) + return cls(match.group(0) if match else output) + + @property + def is_valid(self): + return self._version is not None + + def __eq__(self, other): + if not isinstance(other, ActiveHDLVersion): + return NotImplemented + if not self.is_valid or not other.is_valid: + raise ValueError("Cannot compare unknown ActiveHDL versions") + return self._version == other._version + + def __lt__(self, other): + if not isinstance(other, ActiveHDLVersion): + return NotImplemented + if not self.is_valid or not other.is_valid: + raise ValueError("Cannot order unknown ActiveHDL versions") + return self._version < other._version + + def __str__(self): + if not self.is_valid: + return "unknown" + major, minor, minor_letter = self._version + return f"{major}.{minor}{minor_letter}" + + def __repr__(self): + return f"ActiveHDLVersion({self.version_string!r})" diff --git a/vunit/sim_if/ghdl.py b/vunit/sim_if/ghdl.py index bb28a5c85..d2801b34a 100644 --- a/vunit/sim_if/ghdl.py +++ b/vunit/sim_if/ghdl.py @@ -9,12 +9,14 @@ """ from pathlib import Path +from functools import total_ordering from os import environ, makedirs, remove import logging import subprocess import shlex import re import shutil +import warnings from json import dump from sys import stdout # To avoid output catched in non-verbose mode from ..exceptions import CompileError @@ -191,12 +193,7 @@ def determine_version(cls, prefix): """ Determine the GHDL version """ - return float( - re.match( - r"GHDL ([0-9]*\.[0-9]*).*\(.*\) \[Dunoon edition\]", - cls._get_version_output(prefix), - ).group(1) - ) + return GHDLVersion.from_output(cls._get_version_output(prefix)) @classmethod def supports_vhdl_call_paths(cls): @@ -217,8 +214,9 @@ def supports_vhpi(cls): """ Returns True when the simulator supports VHPI """ - return (cls.determine_backend(cls.find_prefix()) != "mcode") or ( - cls.determine_version(cls.find_prefix()) > 0.36 + version = cls.determine_version(cls.find_prefix()) + return cls.determine_backend(cls.find_prefix()) != "mcode" or ( + version.is_valid and version > GHDLVersion("0.36") ) @classmethod @@ -272,7 +270,8 @@ def _std_str(self, vhdl_standard): Convert standard to format of GHDL command line flag """ if vhdl_standard == VHDL.STD_2019: - if self._version >= 6.0: + version = self._version + if version.is_valid and (version >= GHDLVersion("6.0")): return "19" raise ValueError("VHDL-2019 requires GHDL >=6.0.0.") @@ -518,3 +517,54 @@ def merge_coverage(self, file_name, args=None): self._merge_coverage_gcc(output_dir, args) else: self._merge_coverage_jit(output_dir, args) + + +@total_ordering +class GHDLVersion: + """GHDL version number.""" + + _VERSION_RE = re.compile(r"([0-9]+)\.([0-9]+)(?:\.([0-9]+))?(?:-?dev)?") + + def __init__(self, version_string): + self.version_string = version_string + self._version = None + match = self._VERSION_RE.fullmatch(version_string) + if not match: + warnings.warn( + f"Cannot parse GHDL version: {version_string!r}; version is unknown", + RuntimeWarning, + stacklevel=2, + ) + return + + self._version = (int(match.group(1)), int(match.group(2)), int(match.group(3) or 0)) + + @classmethod + def from_output(cls, output): + """Extract the GHDL version from command output, or return an unknown version.""" + match = cls._VERSION_RE.search(output) + return cls(match.group(0) if match else output) + + @property + def is_valid(self): + return self._version is not None + + def __eq__(self, other): + if isinstance(other, GHDLVersion): + if not self.is_valid or not other.is_valid: + raise ValueError("Cannot compare unknown GHDL versions") + return self._version == other._version # pylint: disable=protected-access + return NotImplemented + + def __lt__(self, other): + if isinstance(other, GHDLVersion): + if not self.is_valid or not other.is_valid: + raise ValueError("Cannot order unknown GHDL versions") + return self._version < other._version # pylint: disable=protected-access + return NotImplemented + + def __str__(self): + return ".".join(str(component) for component in self._version) if self.is_valid else "unknown" + + def __repr__(self): + return f"GHDLVersion({self.version_string!r})" diff --git a/vunit/sim_if/modelsim.py b/vunit/sim_if/modelsim.py index 576bbb5b2..6553f74c6 100644 --- a/vunit/sim_if/modelsim.py +++ b/vunit/sim_if/modelsim.py @@ -8,9 +8,14 @@ Interface towards Mentor Graphics/Siemens ModelSim/Questa simulator. """ +# pylint: disable=too-many-lines + from pathlib import Path +from functools import total_ordering import os import logging +import re +import warnings from threading import Lock, Event from time import sleep from configparser import RawConfigParser, ParsingError @@ -153,6 +158,12 @@ def has_ini(path): return cls.find_toolchain(["vsim"], constraints=[has_ini]) + @classmethod + def determine_version(cls, prefix): + """Determine the ModelSim/Questa version.""" + output = check_output([str(Path(prefix) / "vcom"), "-version"], env=cls.get_env()) + return ModelSimVersion.from_output(output) + @classmethod def supports_vhdl_call_paths(cls): """ @@ -440,9 +451,7 @@ def _create_optimize_function(self, config): return false }} -""".format( - vopt_flags=" ".join(vopt_flags) - ) +""".format(vopt_flags=" ".join(vopt_flags)) return tcl @@ -950,3 +959,54 @@ def write_ini(cfg, file_name): """ with Path(file_name).open("w", encoding="utf-8") as optr: cfg.write(optr) + + +@total_ordering +class ModelSimVersion: + """ModelSim/Questa version number.""" + + _VERSION_RE = re.compile(r"([0-9]+)\.([0-9]+)(?:\.([0-9]+))?") + + def __init__(self, version_string): + self.version_string = version_string + self._version = None + match = self._VERSION_RE.fullmatch(version_string) + if not match: + warnings.warn( + f"Cannot parse ModelSim version: {version_string!r}; version is unknown", + RuntimeWarning, + stacklevel=2, + ) + return + + self._version = (int(match.group(1)), int(match.group(2)), int(match.group(3) or 0)) + + @classmethod + def from_output(cls, output): + """Extract the ModelSim/Questa version from command output, or return an unknown version.""" + match = cls._VERSION_RE.search(output) + return cls(match.group(0) if match else output) + + @property + def is_valid(self): + return self._version is not None + + def __eq__(self, other): + if isinstance(other, ModelSimVersion): + if not self.is_valid or not other.is_valid: + raise ValueError("Cannot compare unknown ModelSim versions") + return self._version == other._version # pylint: disable=protected-access + return NotImplemented + + def __lt__(self, other): + if isinstance(other, ModelSimVersion): + if not self.is_valid or not other.is_valid: + raise ValueError("Cannot order unknown ModelSim versions") + return self._version < other._version # pylint: disable=protected-access + return NotImplemented + + def __str__(self): + return ".".join(str(component) for component in self._version) if self.is_valid else "unknown" + + def __repr__(self): + return f"ModelSimVersion({self.version_string!r})" diff --git a/vunit/sim_if/nvc.py b/vunit/sim_if/nvc.py index cc2880384..b1d742b72 100644 --- a/vunit/sim_if/nvc.py +++ b/vunit/sim_if/nvc.py @@ -15,6 +15,8 @@ import subprocess import shlex import re +import warnings +from functools import total_ordering from sys import stdout # To avoid output catched in non-verbose mode from ..exceptions import CompileError from ..ostools import Process, file_exists @@ -90,10 +92,10 @@ def __init__( # pylint: disable=too-many-arguments self._vhdl_standard = None self._coverage_files = set() - (major, minor) = self.determine_version(prefix) - self._supports_jit = major > 1 or (major == 1 and minor >= 9) - self._ieee_warnings_global = major > 1 or (major == 1 and minor >= 16) - self._supports_coverage_merge = major > 1 or (major == 1 and minor >= 15) + version = self.determine_version(prefix) + self._supports_jit = version.is_valid and version >= NVCVersion("1.9") + self._ieee_warnings_global = version.is_valid and version >= NVCVersion("1.16") + self._supports_coverage_merge = version.is_valid and version >= NVCVersion("1.15") if self.use_color: environ["NVC_COLORS"] = "always" @@ -120,12 +122,7 @@ def determine_version(cls, prefix): """ Determine the NVC version """ - raw = cls._get_version_output(prefix) - match = re.match(r"nvc ([0-9]+)\.([0-9]+).*", raw) - if not match: - raise RuntimeError(f"Cannot determine NVC version: {raw}") - - return (int(match.group(1)), int(match.group(2))) + return NVCVersion.from_output(cls._get_version_output(prefix)) @classmethod def supports_vhpi(cls): @@ -305,7 +302,7 @@ def simulate( " option vhdl_assert_stop_level, which is set to '%s'. See" " https://vunit.github.io/py/opts.html#simulation-options for further details", self.name, - config.vhdl_assert_stop_level + config.vhdl_assert_stop_level, ) cmd += config_sim_options cmd += hooks.get_flags(self, "run_flags") @@ -358,9 +355,7 @@ def merge_coverage(self, file_name, args=None): """ if not self._supports_coverage_merge: - LOGGER.error( - "Current nvc version does not support coverage database merge." - ) + LOGGER.error("Current nvc version does not support coverage database merge.") return coverage_files = [] @@ -384,3 +379,54 @@ def merge_coverage(self, file_name, args=None): nvc_coverage_merge_process = Process(nvc_coverage_merge_cmd, env=self.get_env()) nvc_coverage_merge_process.consume_output() print("Done merging coverage files") + + +@total_ordering +class NVCVersion: + """NVC version number.""" + + _VERSION_RE = re.compile(r"([0-9]+)\.([0-9]+)(?:\.([0-9]+))?") + + def __init__(self, version_string): + self.version_string = version_string + self._version = None + match = self._VERSION_RE.fullmatch(version_string) + if not match: + warnings.warn( + f"Cannot parse NVC version: {version_string!r}; version is unknown", + RuntimeWarning, + stacklevel=2, + ) + return + + self._version = (int(match.group(1)), int(match.group(2)), int(match.group(3) or 0)) + + @classmethod + def from_output(cls, output): + """Extract the NVC version from command output, or return an unknown version.""" + match = cls._VERSION_RE.search(output) + return cls(match.group(0) if match else output) + + @property + def is_valid(self): + return self._version is not None + + def __eq__(self, other): + if isinstance(other, NVCVersion): + if not self.is_valid or not other.is_valid: + raise ValueError("Cannot compare unknown NVC versions") + return self._version == other._version # pylint: disable=protected-access + return NotImplemented + + def __lt__(self, other): + if isinstance(other, NVCVersion): + if not self.is_valid or not other.is_valid: + raise ValueError("Cannot order unknown NVC versions") + return self._version < other._version # pylint: disable=protected-access + return NotImplemented + + def __str__(self): + return ".".join(str(component) for component in self._version) if self.is_valid else "unknown" + + def __repr__(self): + return f"NVCVersion({self.version_string!r})" diff --git a/vunit/sim_if/rivierapro.py b/vunit/sim_if/rivierapro.py index 20f4431ff..b70b89150 100644 --- a/vunit/sim_if/rivierapro.py +++ b/vunit/sim_if/rivierapro.py @@ -9,9 +9,11 @@ """ from pathlib import Path +from functools import total_ordering import os import re import logging +import warnings from ..exceptions import CompileError from ..ostools import Process, file_exists from ..vhdl_standard import VHDL @@ -74,13 +76,18 @@ def no_avhdl(path): @classmethod def _get_version(cls): """ - Return a VersionConsumer object containing the simulator version. + Return a RivieraProVersion object representing the simulator version. """ - proc = Process([str(Path(cls.find_prefix()) / "vcom"), "-version"], env=cls.get_env()) + + return cls.determine_version(cls.find_prefix()) + + @classmethod + def determine_version(cls, prefix): + """Determine the Riviera-PRO version.""" + proc = Process([str(Path(prefix) / "vcom"), "-version"], env=cls.get_env()) consumer = VersionConsumer() proc.consume_output(consumer) - - return consumer + return consumer.version @classmethod def get_osvvm_coverage_api(cls): @@ -88,8 +95,8 @@ def get_osvvm_coverage_api(cls): Returns simulator name when OSVVM coverage API is supported, None otherwise. """ version = cls._get_version() - if version.year is not None: - if (version.year == 2016 and version.month >= 10) or (version.year > 2016): + if version.is_valid: + if version >= RivieraProVersion("2016.10"): return cls.name return None @@ -169,8 +176,8 @@ def _std_str(self, vhdl_standard): Convert standard to format of Riviera-PRO command line flag """ if vhdl_standard == VHDL.STD_2019: - if self._version.year is not None: - if (self._version.year == 2020 and self._version.month < 4) or (self._version.year < 2020): + if self._version.is_valid: + if self._version < RivieraProVersion("2020.4"): return "-2018" return "-2019" @@ -353,9 +360,7 @@ def _create_load_function( return false }} -""".format( - vsim_flags=" ".join(vsim_flags), break_level=config.vhdl_assert_stop_level - ) +""".format(vsim_flags=" ".join(vsim_flags), break_level=config.vhdl_assert_stop_level) return tcl @@ -459,14 +464,60 @@ class VersionConsumer(object): """ def __init__(self): - self.year = None - self.month = None - - _version_re = re.compile(r"(?P\d+)\.(?P\d+)\.\d+") + self.version = None def __call__(self, line): - match = self._version_re.search(line) - if match is not None: - self.year = int(match.group("year")) - self.month = int(match.group("month")) + self.version = RivieraProVersion.from_output(line) + return True + + +@total_ordering +class RivieraProVersion: + """Riviera-PRO version.""" + + _VERSION_RE = re.compile(r"(\d+)\.(\d+)(?:\.\d+){0,2}") + + def __init__(self, version_string): + self.version_string = version_string + self._version = None + match = self._VERSION_RE.fullmatch(version_string) + if not match: + warnings.warn( + f"Cannot parse Riviera-PRO version: {version_string!r}; version is unknown", + RuntimeWarning, + stacklevel=2, + ) + return + + self._version = (int(match.group(1)), int(match.group(2))) + + @classmethod + def from_output(cls, output): + """Extract the Riviera-PRO version from command output, or return an unknown version.""" + match = cls._VERSION_RE.search(output) + return cls(match.group(0) if match else output) + + @property + def is_valid(self): + return self._version is not None + + def __eq__(self, other): + if isinstance(other, RivieraProVersion): + if not self.is_valid or not other.is_valid: + raise ValueError("Cannot compare unknown Riviera-PRO versions") + return self._version == other._version # pylint: disable=protected-access + return NotImplemented + + def __lt__(self, other): + if isinstance(other, RivieraProVersion): + if not self.is_valid or not other.is_valid: + raise ValueError("Cannot order unknown Riviera-PRO versions") + return self._version < other._version # pylint: disable=protected-access + return NotImplemented + + def __str__(self): + return ".".join(str(component) for component in self._version) if self.is_valid else "unknown" + + def __repr__(self): + return f"RivieraProVersion({self.version_string!r})"