Repository: cocotb/cocotb

Issue:
CI: Do not run simulator tests on pure documentation changes
Noted in #4637 that the whole simulator test suite was running although all changes were in the documentation rst files. Would save a bit of computrons not doing that.

Discussion:
@ktbarrett: https://github.com/prisma/prisma/pull/6612 shows an example of how this could be done, but we would probably integrate that into the existing `generate-envs.py`.@cmarqu: Relevant new blog post: https://nedbatchelder.com/blog/202505/filtering_github_actions_by_changed_files.html

Solution patch:
diff --git a/.github/workflows/build-test-dev.yml b/.github/workflows/build-test-dev.yml
index ae8b500b..306f9a43 100644
--- a/.github/workflows/build-test-dev.yml
+++ b/.github/workflows/build-test-dev.yml
@@ -14,6 +14,22 @@ on:
     branches:
       - master
       - 'stable/**'
+    paths-ignore:
+      # Skip running tests for changes only in:
+      # Documentation
+      - 'docs/**'
+      # Dot-files not related to running tests
+      - '.backportrc.json'
+      - '.clang-format'
+      - '.git-blame-ignore-revs'
+      - '.gitignore'
+      - '.pre-commit-config.yml'
+      - '.readthedocs.yml'
+      - '.pylintrc'
+      # Information files
+      - 'LICENSE'
+      - 'README.md'
+      - 'CONTRIBUTING.md'
 
 jobs:
   test_dev:
diff --git a/docs/source/building.rst b/docs/source/building.rst
index b547660c..f60b9c5c 100644
--- a/docs/source/building.rst
+++ b/docs/source/building.rst
@@ -278,8 +278,7 @@ Regression Manager
 
     Enable to collect Python coverage data for user code.
     For some simulators, this will also report :term:`HDL` coverage.
-    If :envvar:`COCOTB_COVERAGE_RCFILE` is not set,
-    branch coverage is collected
+    If :envvar:`COCOTB_COVERAGE_RCFILE` is not set, branch coverage is collected
     and files in the cocotb package directory are excluded.
 
     This needs the :mod:`coverage` Python module to be installed.
diff --git a/docs/source/contributing.rst b/docs/source/contributing.rst
index 285918c2..96c22492 100644
--- a/docs/source/contributing.rst
+++ b/docs/source/contributing.rst
@@ -100,8 +100,7 @@ Finally, they should notify the maintainers that they are able and willing to ac
 
 To be able to add labels and close issues and PRs you will need special permissions.
 Contact a `maintainer <#maintainer>`__ if you are interested in receiving these permissions.
-They will be granted according to the project's need and the requestor's familiarity with
-cocotb.
+They will be granted according to the project's need and the requestor's familiarity with cocotb.
 Once you have those permissions, see the guidelines on `managing issues and pull requests <#Managing-of-Issues-and-Pull-Requests>`__.
 
 This task can also be done without special repo permissions, by just commenting on the issue or PR. This is especially helpful for Github issues about bugs.
diff --git a/docs/source/newsfragments/3792.change.rst b/docs/source/newsfragments/3792.change.rst
deleted file mode 100644
index 8826fa50..00000000
--- a/docs/source/newsfragments/3792.change.rst
+++ /dev/null
@@ -1 +0,0 @@
-When constructing a :class:`~cocotb.types.LogicArray`, if the given value cannot fit in the given range, an :exc:`OverflowError` is now thrown, instead of a :exc:`ValueError`.
diff --git a/docs/source/newsfragments/3792.removal.3.rst b/docs/source/newsfragments/3792.removal.3.rst
deleted file mode 100644
index 7091bded..00000000
--- a/docs/source/newsfragments/3792.removal.3.rst
+++ /dev/null
@@ -1 +0,0 @@
-Constructing a :class:`~cocotb.types.LogicArray` from an :class:`int` is deprecated. Use :meth:`LogicArray.from_unsigned() <cocotb.types.LogicArray.from_unsigned>` and :meth:`LogicArray.from_signed() <cocotb.types.LogicArray.from_signed>` instead.
diff --git a/src/cocotb/types/_array.py b/src/cocotb/types/_array.py
index 0fd5d852..03a18a95 100644
--- a/src/cocotb/types/_array.py
+++ b/src/cocotb/types/_array.py
@@ -187,8 +187,10 @@ class Array(AbstractArray[T]):
     def __eq__(self, other: object) -> bool:
         if isinstance(other, Array):
             return self._value == other._value
-        elif isinstance(other, (list, tuple)):
-            return self == Array(other)
+        elif isinstance(other, list):
+            return self._value == other
+        elif isinstance(other, tuple):
+            return tuple(self._value) == other
         else:
             return NotImplemented
 
diff --git a/src/cocotb/types/_logic_array.py b/src/cocotb/types/_logic_array.py
index df132390..5606c63b 100644
--- a/src/cocotb/types/_logic_array.py
+++ b/src/cocotb/types/_logic_array.py
@@ -3,7 +3,6 @@
 # SPDX-License-Identifier: BSD-3-Clause
 import os
 import random
-import warnings
 from math import ceil
 from typing import (
     TYPE_CHECKING,
@@ -274,7 +273,7 @@ class LogicArray(AbstractArray[Logic]):
             if range is not None:
                 if len(value) != len(range):
                     raise ValueError(
-                        f"Value of length {len(self._value_as_str)} will not fit in {range}"
+                        f"Value of length {len(self._value_as_str)} will not fit in {range!r}"
                     )
                 self._range = range
             else:
@@ -296,7 +295,7 @@ class LogicArray(AbstractArray[Logic]):
             if range is not None:
                 if len(self._value_as_array) != len(range):
                     raise ValueError(
-                        f"Value of length {len(self._value_as_array)} will not fit in {range}"
+                        f"Value of length {len(self._value_as_array)} will not fit in {range!r}"
                     )
                 self._range = range
             else:
@@ -365,6 +364,8 @@ class LogicArray(AbstractArray[Logic]):
             TypeError: When invalid argument types are used.
             ValueError: When a :class:`LogicArray` of the given *range* can't hold the *value*, or *value* is negative.
         """
+        if value < 0:
+            raise ValueError("Expected unsigned integer, got negative value.")
         return LogicArray(value, range)
 
     @classmethod
@@ -396,13 +397,20 @@ class LogicArray(AbstractArray[Logic]):
                 f"Expected Range or int for parameter 'range', not {type(range).__qualname__}"
             )
 
+        # Prevent null range from blowing up the below code.
+        if len(range) == 0:
+            raise ValueError(
+                f"Signed integer {value!r} will not fit in a LogicArray with bounds: {range!r}."
+            )
+
         limit = 1 << (len(range) - 1)
         if value < -limit or limit <= value:
             raise ValueError(
-                f"{value!r} will not fit in a LogicArray with bounds: {range!r}."
+                f"Signed integer {value!r} will not fit in a LogicArray with bounds: {range!r}."
             )
+        value %= 2 * limit
 
-        return LogicArray(value % (2 * limit), range)
+        return LogicArray(value, range)
 
     @classmethod
     def from_bytes(
@@ -482,6 +490,9 @@ class LogicArray(AbstractArray[Logic]):
         other: object,
     ) -> bool:
         if isinstance(other, int):
+            if len(self) == 0:
+                # Null arrays don't have a value and thus always compare False.
+                return False
             try:
                 return self.to_unsigned() == other
             except ValueError:
@@ -614,8 +625,7 @@ class LogicArray(AbstractArray[Logic]):
             An integer equivalent to the value by interpreting it using unsigned representation.
         """
         if len(self) == 0:
-            warnings.warn("Converting a LogicArray of length 0 to integer")
-            return 0
+            raise ValueError("Cannot convert null vector to integer")
         return self._get_int(resolve)
 
     def to_signed(
@@ -638,11 +648,11 @@ class LogicArray(AbstractArray[Logic]):
             An integer equivalent to the value by interpreting it using two's complement representation.
         """
         if len(self) == 0:
-            warnings.warn("Converting a LogicArray of length 0 to integer")
-            return 0
+            raise ValueError("Cannot convert null vector to integer")
         value = self._get_int(resolve)
-        if value >= (1 << (len(self) - 1)):
-            value -= 1 << len(self)
+        limit = 1 << (len(self) - 1)
+        if value >= limit:
+            value -= 2 * limit
         return value
 
     def to_bytes(


Test patch:
diff --git a/tests/pytest/test_logic_array.py b/tests/pytest/test_logic_array.py
index 9a858458..1ef6e931 100644
--- a/tests/pytest/test_logic_array.py
+++ b/tests/pytest/test_logic_array.py
@@ -384,20 +384,21 @@ def test_null_vector():
     assert len(null_vector) == 0
     assert list(null_vector) == []
     assert str(null_vector) == ""
-    with pytest.warns(UserWarning):
-        assert int(null_vector) == 0
-    with pytest.warns(UserWarning):
-        assert null_vector.to_signed() == 0
-    with pytest.warns(UserWarning):
-        assert null_vector.to_unsigned() == 0
+    with pytest.raises(ValueError):
+        int(null_vector)
+    with pytest.raises(ValueError):
+        null_vector.to_signed()
+    with pytest.raises(ValueError):
+        null_vector.to_unsigned()
 
     # test comparison
     assert null_vector == LogicArray("")
     assert null_vector == LogicArray("", null_range)
     assert null_vector == LogicArray([])
     assert null_vector == LogicArray([], null_range)
-    with pytest.warns(UserWarning):
-        assert null_vector == 0
+    assert null_vector != 0
+    assert null_vector != 1
+    assert null_vector != -1
     assert null_vector == ""
     assert null_vector == []