From 07192f8151d519fc7ac92678f27a3a168fd3d247 Mon Sep 17 00:00:00 2001 From: Lars Asplund Date: Fri, 9 Oct 2026 11:59:18 +0200 Subject: [PATCH] Added support for and/or expressions and simulator version comparisons in vunit_pkg.toml --- docs/vunit_packages.rst | 45 ++++- tests/unit/test_activehdl_interface.py | 164 ++++++++++------ tests/unit/test_builtins.py | 168 ++++++++++++++++ tests/unit/test_ghdl_interface.py | 32 ++- tests/unit/test_modelsim_interface.py | 49 ++++- tests/unit/test_rivierapro_interface.py | 36 +++- tests/unit/test_sim_if_hooks.py | 4 +- vunit/builtins.py | 248 +++++++++++++++++++++--- vunit/sim_if/activehdl.py | 118 ++++++----- vunit/sim_if/ghdl.py | 68 ++++++- vunit/sim_if/modelsim.py | 66 ++++++- vunit/sim_if/nvc.py | 74 +++++-- vunit/sim_if/rivierapro.py | 89 +++++++-- 13 files changed, 960 insertions(+), 201 deletions(-) diff --git a/docs/vunit_packages.rst b/docs/vunit_packages.rst index 87b3c770e..5abe22525 100644 --- a/docs/vunit_packages.rst +++ b/docs/vunit_packages.rst @@ -30,6 +30,7 @@ and compilation defaults. For example: [package] requires-vunit = ">=5.0.0.dev14" requires-vhdl = ">=2008,<2019" + requires = 'simulator != "xsim" or vhdl >= "2019"' library = "foo_lib" compile_option = [["", [""]]] @@ -41,10 +42,20 @@ and compilation defaults. For example: 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``. +Each ``[[package.sources]]`` table defines a source entry. Its ``files`` +patterns (or the backwards-compatible ``include`` key) 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``. + +A source entry can be restricted to matching environments: + +.. code-block:: toml + + [[package.sources]] + library = "foo_lib" + files = ["hdl/vhdl2019/*.vhd"] + when = 'vhdl >= "2019"' Replace the compile-option placeholders with names and values supported by the project. Each option is a two-element array containing the option name @@ -67,14 +78,21 @@ The supported manifest fields are: * - ``requires-vhdl`` - ``[package]`` - Constraints on the VHDL standards supported by the package. + * - ``requires`` + - ``[package]`` + - Environment expression that must match before the package can be used. * - ``library`` - ``[package]`` - Default library for package sources. Required when ``sources`` is present. VUnit creates the library for the package. * - ``sources`` - ``[[package.sources]]`` - - Repeated source tables. Each table must contain ``include``, an array - of source patterns relative to the package root. + - Repeated source tables. Each table must contain ``files`` or + ``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 @@ -98,6 +116,21 @@ 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. +The optional ``requires`` and ``when`` expressions support marker names +``vunit``, ``vhdl``, ``simulator``, and the simulator version markers +``activehdl_version``, ``ghdl_version``, ``modelsim_version``, +``nvc_version``, and ``rivierapro_version``. Version markers are available +only when their corresponding simulator is selected. Equality and inequality +checks on an unavailable version marker are supported, but ordering checks +are unavailable. With no simulator selected, equality and inequality checks +on ``simulator`` are supported, but ordering checks are unavailable. Values +are quoted strings, comparisons use +``<=``, ``<``, ``!=``, ``==``, ``>=``, or ``>``, and ``and``, ``or``, and +parentheses group comparisons. ``and`` has higher precedence than ``or``. +The legacy ``requires-vunit`` and ``requires-vhdl`` constraints remain in +force alongside ``requires``. A source entry without ``when`` is always +included for a usable package. + 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 abc33ec56..9c06e0b34 100644 --- a/tests/unit/test_builtins.py +++ b/tests/unit/test_builtins.py @@ -17,9 +17,14 @@ 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.nvc import NVCVersion from tests.common import create_tempdir from contextlib import contextmanager from importlib.machinery import ModuleSpec @@ -241,6 +246,169 @@ 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 = 'vhdl >= \"2019\"'\n") + self.builtins.add_package("foo") + + 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]] +files = ["common.vhd"] +[[package.sources]] +library = "osvvm" +files = ["matching.vhd"] +when = 'vhdl == "2002"' +[[package.sources]] +library = "unused" +files = ["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 22a56f4f9..e70020c17 100644 --- a/tests/unit/test_sim_if_hooks.py +++ b/tests/unit/test_sim_if_hooks.py @@ -19,7 +19,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 @@ -180,7 +180,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): hooks.register_hooks( "nvc", diff --git a/vunit/builtins.py b/vunit/builtins.py index 240177e70..baf1af911 100755 --- a/vunit/builtins.py +++ b/vunit/builtins.py @@ -17,7 +17,7 @@ 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 +29,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 +50,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 +196,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 +232,67 @@ def _meets_required_version( return True + def _evaluate_marker_expression(self, expression: str) -> bool: + """Evaluate an expression against the explicitly supported environment markers.""" + syntax = _MarkerExpressionParser(expression).parse() + marker_values = { + "vunit": (version(), VUnitVersion), + "vhdl": (str(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 +400,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 +413,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 +421,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, "files": list, "library": str, "compile_option": list, "when": str}, path=f"package.sources[{idx}]", ) ) @@ -252,18 +429,30 @@ 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" in source: - for list_idx, path in enumerate(source["include"]): - if not isinstance(path, str): - errors.append( - ValidationError( - path=f"package.sources[{idx}].include[{list_idx}]", - message="Path must be a string.", + if "include" not in source and "files" not in source: + errors.append( + ValidationError( + path=f"package.sources[{idx}]", + message="Missing mandatory array 'include' or 'files'.", + ) + ) + if "include" in source and "files" in source: + errors.append( + ValidationError( + path=f"package.sources[{idx}]", + message="Specify only one of 'include' or 'files'.", + ) + ) + for file_list_key in ("include", "files"): + if file_list_key in source: + for list_idx, path in enumerate(source[file_list_key]): + if not isinstance(path, str): + errors.append( + ValidationError( + path=f"package.sources[{idx}].{file_list_key}[{list_idx}]", + message="Path must be a string.", + ) ) - ) if isinstance(package.get("setup"), str) and not RE_SETUP.match(package["setup"]): errors.append( @@ -330,16 +519,26 @@ 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}." ) + if "requires" in package and not self._evaluate_marker_expression(package["requires"]): + raise RuntimeError( + f"Package {package_name} does not satisfy requires expression {package['requires']!r} " + "in the current environment." + ) + 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 +565,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("files", source.get("include", [])): source_files = source_library.add_source_files( package_root / include, vhdl_standard=use_vhdl_standard ) @@ -403,14 +602,14 @@ 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): + if self._meets_version_specifier(VHDLStandard, str(self._vhdl_standard), package_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) + if self._meets_version_specifier(VHDLStandard, str(standard), package_vhdl_standard) ), None, ) @@ -552,6 +751,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})"