Repository: PalisadoesFoundation/switchmap-ng
Pull request #319: Test/rows 100 coverage
Author: login: Abhi-MS
Created: 2025-04-11T06:11:58Z | Merged: 2025-04-17T16:53:20Z

<!--
This section can be deleted after reading.

We employ the following branching strategy to simplify the development process and to ensure that only stable code is pushed to the `master` branch:

- `develop`: For unstable code: New features and bug fixes.
- `master`: Where the stable production ready code lies. Only security related bugs.

NOTE!!!

ONLY SUBMIT PRS AGAINST OUR `DEVELOP` BRANCH. THE DEFAULT IS `MAIN`, SO YOU WILL HAVE TO MODIFY THIS BEFORE SUBMITTING YOUR PR FOR REVIEW. PRS MADE AGAINST `MAIN` WILL BE CLOSED.

-->

<!--
Thanks for submitting a pull request! Please provide enough information so that others can review your pull request.
-->

**What kind of change does this PR introduce?**

Added unit tests for coverage on the rows.py file.

**Issue Number:**

Fixes #195 


**Snapshots/Videos:**

![image](https://github.com/user-attachments/assets/01199fcb-7686-4577-84d8-fc9c47c97d33)

**If relevant, did you update the documentation?**

Not relevant

**Summary**

This PR introduces unit tests for all functions in the rows.py file to achieve 100% test coverage.
The tests ensure that the following functions correctly convert database rows to tuples:

- device()
- root()
- event()
- l1interface()
- mac()
- macip()
- macport()
- oui()
- vlan()
- vlanport()
- zone()
- ip()
- ipport()

Additionally, edge cases where the input is None are tested to ensure that each function properly raises exceptions when provided with invalid input.

The tests verify the following:
- Correct tuple conversion for each function.
- Correct tuple length and data type.
- Correct value in the tuple for each entry in the row
- Handling of invalid (None) inputs with appropriate exceptions.

**Does this PR introduce a breaking change?**

No

## Checklist

### CodeRabbit AI Review
- [ ] I have reviewed and addressed all critical issues flagged by CodeRabbit AI
- [ ] I have implemented or provided justification for each non-critical suggestion
- [ ] I have documented my reasoning in the PR comments where CodeRabbit AI suggestions were not implemented

### Test Coverage
- [ ] I have written tests for all new changes/features
- [ ] I have verified that test coverage meets or exceeds 95%
- [ ] I have run the test suite locally and all tests pass


**Other information**

<!--Add extra information about this PR here-->

**Have you read the [contributing guide](https://github.com/PalisadoesFoundation/switchmap-ng/blob/main/CONTRIBUTING.md)?**

Yes

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **Tests**
  - Introduced a new automated test module to verify that data conversion processes work as expected.
  - The tests ensure accurate formatting and proper error handling when invalid inputs are encountered, contributing to overall system reliability.
  - Added comprehensive unit tests for various entity conversions, including edge case validations.  
  - Improved error detection by refining pattern matching for numeric codes in function argument parsing.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Linked issue #195: Create unittests for `switchmap/server/db/misc/rows.py`
We will need unittests done for all methods, classes and functions found in this file.

### PR Acceptance Criteria

1. When complete this file must show **100%** coverage when merged into the code base.
1. [The current code coverage for the file can be found here](https://app.codecov.io/gh/PalisadoesFoundation/switchmap-ng/tree/develop/?displayType=list)
1. CodeRabbit must approve all your changes to the test file(s) and the file(s) under test
1. The PR will show a report for the code coverage for the file you have added. You can use that as a guide
1. No functionality must be removed to achieve this goal
1. All tests must be valid and truly test the code base
1. The tests must be placed in the equivalent folder under `tests/switchmap_` therefore:
    1. a file named `switchmap/x/y/z/file.py` must have a test file named `tests/switchmap_/x/y/z/test_file.py`
    1. file `tests/switchmap_/x/y/z/test_file.py` must have 755 permissions so it can be easily executable
    1. take guidance on the formatting of the file from other `tests/switchmap_/x/y/z/test_*.py` files or others along the path or in adjacent directories
    
### Other Criteria

1. If this file isn't referenced in the code base then remove it. We don't want tests run on unused code

### Potential internship candidates

Please read this if you are planning to apply for a Palisadoes Foundation internship 
- https://github.com/PalisadoesFoundation/talawa/issues/359
Linked issue comments: user: login: github-actions[bot]
created_at: 2025-01-13T00:13:37Z
body: This issue did not get any activity in the past 10 days and will be closed in 180 days if no update occurs. Please check if the develop branch has fixed it and report again or close the issue.
author_association: NONE
user: login: srinitha709
created_at: 2025-01-14T10:30:20Z
body: @palisadoes i would like to work on this issue

author_association: NONE
user: login: 404Nikhil
created_at: 2025-01-19T12:47:28Z
body: If this is not assigned can I work on this? @palisadoes 
author_association: CONTRIBUTOR
user: login: palisadoes
created_at: 2025-02-09T00:43:52Z
body: Unassigned. Inactivity
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-02-21T00:12:31Z
body: This issue did not get any activity in the past 10 days and will be closed in 180 days if no update occurs. Please check if the develop branch has fixed it and report again or close the issue.
author_association: NONE
user: login: Nivedita-Chhokar
created_at: 2025-03-02T13:25:08Z
body: @palisadoes I have solved it, can i be assigned?
author_association: NONE
user: login: Cioppolo14
created_at: 2025-03-02T14:27:47Z
body: Our policy is to assign no more than two issues to each contributor across all repositories. This way everyone gets a chance to participate in the projects. We sometimes give exceptions for more urgent cases and sometimes we lose track, but the policy stands. You have reached your limit, please wait until your existing issues are closed before requesting more issues. You could unassign yourself from one of the other issues too. @Nivedita-Chhokar 
author_association: NONE
user: login: Nivedita-Chhokar
created_at: 2025-03-11T05:44:26Z
body: @Cioppolo14 can i be assigned?

author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-22T00:12:41Z
body: This issue did not get any activity in the past 10 days and will be closed in 180 days if no update occurs. Please check if the develop branch has fixed it and report again or close the issue.
author_association: NONE
user: login: Cioppolo14
created_at: 2025-03-22T00:43:42Z
body: Unassigning due to inactivity and/or no submitted PR. Others need to get a chance. Please let us know whether you want to be reassigned before someone else requests assignment or select another issue.
author_association: NONE
user: login: aakarshgopishetty
created_at: 2025-03-23T13:32:49Z
body: Hi, I would like to work on this issue. I understand that my task is to write unittests for the rows.py file to ensure 100% code coverage. I will follow the provided guidelines and ensure the tests are valid and formatted correctly. Please assign this issue to me, and feel free to provide any additional instructions or resources that may be helpful. Thank you!
author_association: NONE
user: login: Nivedita-Chhokar
created_at: 2025-03-24T09:52:52Z
body: @Cioppolo14 I've resolved the issue and will be submitting the PR as soon as possible.

author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-04T00:12:58Z
body: This issue did not get any activity in the past 10 days and will be closed in 180 days if no update occurs. Please check if the develop branch has fixed it and report again or close the issue.
author_association: NONE
user: login: Cioppolo14
created_at: 2025-04-04T00:28:30Z
body: Unassigning due to inactivity and/or no submitted PR. Others need to get a chance. Please let us know whether you want to be reassigned before someone else requests assignment or select another issue.
author_association: NONE
user: login: Abhi-MS
created_at: 2025-04-06T11:46:28Z
body: If no one is working on this, please assign me
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-17T00:13:16Z
body: This issue did not get any activity in the past 10 days and will be closed in 180 days if no update occurs. Please check if the develop branch has fixed it and report again or close the issue.
author_association: NONE

Diff:
diff --git a/tests/switchmap_/server/db/misc/test_rows.py b/tests/switchmap_/server/db/misc/test_rows.py
new file mode 100755
index 00000000..98fabac2
--- /dev/null
+++ b/tests/switchmap_/server/db/misc/test_rows.py
@@ -0,0 +1,427 @@
+#!/usr/bin/env python3
+"""Test the rows module.
+
+This module contains unit tests for the functions in the `rows.py` file.
+The tests ensure that the following functions correctly convert
+database rows to tuples:
+
+- device()
+- root()
+- event()
+- l1interface()
+- mac()
+- macip()
+- macport()
+- oui()
+- vlan()
+- vlanport()
+- zone()
+- ip()
+- ipport()
+
+Additionally, edge cases where the input is None are tested to ensure that
+each function properly raises AttributeErrors when provided with invalid input.
+
+The tests verify the following:
+- Correct tuple conversion for each function.
+- Correct tuple length and data type.
+- Correct value in the tuple for each entry in the row
+- Handling of invalid (None) inputs with appropriate AttributeErrors.
+"""
+
+import os
+import sys
+
+import unittest
+from switchmap.server.db.misc import rows
+from switchmap.server.db.models import (
+    Device,
+    Event,
+    Ip,
+    IpPort,
+    L1Interface,
+    Mac,
+    MacIp,
+    MacPort,
+    Oui,
+    Root,
+    Vlan,
+    VlanPort,
+    Zone,
+)
+
+from switchmap.server.db import SCOPED_SESSION
+from tests.testlib_ import setup
+from tests.testlib_ import db
+
+# Try to create a working PYTHONPATH
+EXEC_DIR = os.path.dirname(os.path.realpath(__file__))
+ROOT_DIR = os.path.abspath(
+    os.path.join(
+        os.path.abspath(
+            os.path.join(
+                os.path.abspath(
+                    os.path.join(
+                        os.path.abspath(
+                            os.path.join(
+                                os.path.abspath(
+                                    os.path.join(EXEC_DIR, os.pardir)
+                                ),
+                                os.pardir,
+                            )
+                        ),
+                        os.pardir,
+                    )
+                ),
+                os.pardir,
+            )
+        ),
+        os.pardir,
+    )
+)
+_EXPECTED = """\
+{0}switchmap-ng{0}tests{0}switchmap_{0}server{0}db{0}misc""".format(
+    os.sep
+)
+if EXEC_DIR.endswith(_EXPECTED) is True:
+    # We need to prepend the path in case the repo has been installed
+    # elsewhere on the system using PIP. This could corrupt expected results
+    sys.path.insert(0, ROOT_DIR)
+else:
+    print(
+        """This script is not installed in the
+        "{0}" directory. Please fix.""".format(
+            _EXPECTED
+        )
+    )
+    sys.exit(2)
+
+
+class TestRowsMethods(unittest.TestCase):
+    """Test suite for functions in rows.py."""
+
+    @classmethod
+    def setUpClass(cls):
+        """Set up test environment before running tests."""
+        # Load the configuration
+        cls.config = setup.config()
+        cls.config.save()
+        # Drop tables
+        cls.database = db.Database()
+        cls.database.drop()
+        cls.database.create()
+        # Populate the database
+        db.populate()
+        cls.session = SCOPED_SESSION()
+
+    @classmethod
+    def tearDownClass(cls):
+        """Clean up test environment after tests."""
+        if cls.session:
+            cls.session.close()
+        cls.database.drop()
+        cls.config.cleanup()
+
+    # ----------------------------------------------------------------------
+    # Standard conversion tests
+    # ----------------------------------------------------------------------
+
+    def compare_row_to_expected(self, row, expected_dict):
+        """Compare the attributes of a row with expected values.
+
+        Iterates through the `expected_dict`, comparing each value with the
+        corresponding attribute of the `row` object, handling byte string
+        normalization, and reporting mismatches using assertions.
+
+        Args:
+            row: The database row object.
+            expected_dict: A dictionary of expected attribute values.
+
+        Raises:
+            AttributeError: If an attribute is missing in the row.
+            AssertionError: If an attribute value does not match.
+
+        Returns:
+            None
+        """
+        for key, expected_value in expected_dict.items():
+            actual_value = getattr(row, key)
+
+            # Normalize byte strings
+            if isinstance(actual_value, bytes):
+                actual_value = actual_value.decode()
+            if isinstance(expected_value, bytes):
+                expected_value = expected_value.decode()
+
+            with self.subTest(attribute=key):
+                self.assertEqual(actual_value, expected_value)
+
+    def test_device(self):
+        """Test the device() function for correct tuple conversion.
+
+        Verifies that the function returns a tuple of length 12
+        when given a Device row from the database.
+        """
+        row = self.session.query(Device).first()
+        self.assertIsNotNone(row, "No Device row found in the test database.")
+        result = rows.device(row)
+
+        self.assertIsInstance(result, tuple)
+        self.assertEqual(len(result), 12)
+
+        self.compare_row_to_expected(row, result._asdict())
+
+    def test_root(self):
+        """Test the root() function for correct tuple conversion.
+
+        Verifies that the function returns a tuple of length 6
+        when given a Root row from the database.
+        """
+        row = self.session.query(Root).first()
+        self.assertIsNotNone(row, "No Root row found in the test database.")
+        result = rows.root(row)
+
+        self.assertIsInstance(result, tuple)
+        self.assertEqual(len(result), 6)
+
+        self.compare_row_to_expected(row, result._asdict())
+
+    def test_event(self):
+        """Test the event() function for correct tuple conversion.
+
+        Verifies that the function returns a tuple of length 6
+        when given an Event row from the database.
+        """
+        row = self.session.query(Event).first()
+        self.assertIsNotNone(row, "No Event row found in the test database.")
+        result = rows.event(row)
+
+        self.assertIsInstance(result, tuple)
+        self.assertEqual(len(result), 6)
+
+        self.compare_row_to_expected(row, result._asdict())
+
+    def test_l1interface(self):
+        """Test the l1interface() function for correct tuple conversion.
+
+        Verifies that the function returns a tuple of length 25
+        when given an L1Interface row from the database.
+        """
+        row = self.session.query(L1Interface).first()
+        self.assertIsNotNone(
+            row, "No L1Interface row found in the test database."
+        )
+
+        result = rows.l1interface(row)
+        self.assertIsInstance(result, tuple)
+        self.assertEqual(len(result), 25)
+
+        self.compare_row_to_expected(row, result._asdict())
+
+    def test_mac(self):
+        """Test the mac() function for correct tuple conversion.
+
+        Verifies that the function returns a tuple of length 7
+        when given a Mac row from the database.
+        """
+        row = self.session.query(Mac).first()
+        self.assertIsNotNone(row, "No Mac row found in the test database.")
+        result = rows.mac(row)
+
+        self.assertIsInstance(result, tuple)
+        self.assertEqual(len(result), 7)
+
+        self.compare_row_to_expected(row, result._asdict())
+
+    def test_macip(self):
+        """Test the macip() function for correct tuple conversion.
+
+        Verifies that the function returns a tuple of length 6
+        when given a MacIp row from the database.
+        """
+        row = self.session.query(MacIp).first()
+        self.assertIsNotNone(row, "No MacIp row found in the test database.")
+        result = rows.macip(row)
+
+        self.assertIsInstance(result, tuple)
+        self.assertEqual(len(result), 6)
+
+        self.compare_row_to_expected(row, result._asdict())
+
+    def test_macport(self):
+        """Test the macport() function for correct tuple conversion.
+
+        Verifies that the function returns a tuple of length 6
+        when given a MacPort row from the database.
+        """
+        row = self.session.query(MacPort).first()
+        self.assertIsNotNone(row, "No MacPort row found in the test database.")
+        result = rows.macport(row)
+
+        self.assertIsInstance(result, tuple)
+        self.assertEqual(len(result), 6)
+
+        self.compare_row_to_expected(row, result._asdict())
+
+    def test_oui(self):
+        """Test the oui() function for correct tuple conversion.
+
+        Verifies that the function returns a tuple of length 6
+        when given a Oui row from the database.
+        """
+        row = self.session.query(Oui).first()
+        self.assertIsNotNone(row, "No Oui row found in the test database.")
+        result = rows.oui(row)
+
+        self.assertIsInstance(result, tuple)
+        self.assertEqual(len(result), 6)
+
+        self.compare_row_to_expected(row, result._asdict())
+
+    def test_vlan(self):
+        """Test the vlan() function for correct tuple conversion.
+
+        Verifies that the function returns a tuple of length 8
+        when given a Vlan row from the database.
+        """
+        row = self.session.query(Vlan).first()
+        self.assertIsNotNone(row, "No Vlan row found in the test database.")
+        result = rows.vlan(row)
+
+        self.assertIsInstance(result, tuple)
+        self.assertEqual(len(result), 8)
+
+        self.compare_row_to_expected(row, result._asdict())
+
+    def test_vlanport(self):
+        """Test the vlanport() function for correct tuple conversion.
+
+        Verifies that the function returns a tuple of length 6
+        when given a VlanPort row from the database.
+        """
+        row = self.session.query(VlanPort).first()
+        self.assertIsNotNone(row, "No VlanPort row found in the test database.")
+        result = rows.vlanport(row)
+
+        self.assertIsInstance(result, tuple)
+        self.assertEqual(len(result), 6)
+
+        self.compare_row_to_expected(row, result._asdict())
+
+    def test_zone(self):
+        """Test the zone() function for correct tuple conversion.
+
+        Verifies that the function returns a tuple of length 7
+        when given a Zone row from the database.
+        """
+        row = self.session.query(Zone).first()
+        self.assertIsNotNone(row, "No Zone row found in the test database.")
+        result = rows.zone(row)
+
+        self.assertIsInstance(result, tuple)
+        self.assertEqual(len(result), 7)
+
+        self.compare_row_to_expected(row, result._asdict())
+
+    def test_ip(self):
+        """Test the ip() function for correct tuple conversion.
+
+        Verifies that the function returns a tuple of length 8
+        when given a Ip row from the database.
+        """
+        row = self.session.query(Ip).first()
+        self.assertIsNotNone(row, "No Ip row found in the test database.")
+        result = rows.ip(row)
+
+        self.assertIsInstance(result, tuple)
+        self.assertEqual(len(result), 8)
+
+        self.compare_row_to_expected(row, result._asdict())
+
+    def test_ipport(self):
+        """Test the ipport() function for correct tuple conversion.
+
+        Verifies that the function returns a tuple of length 6
+        when given a IpPort row from the database.
+        """
+        row = self.session.query(IpPort).first()
+        self.assertIsNotNone(row, "No IpPort row found in the test database.")
+        result = rows.ipport(row)
+
+        self.assertIsInstance(result, tuple)
+        self.assertEqual(len(result), 6)
+
+        self.compare_row_to_expected(row, result._asdict())
+
+    # ----------------------------------------------------------------------
+    # --- Edge case: None input tests ---
+    # ----------------------------------------------------------------------
+
+    def test_device_edge_none(self):
+        """Edge Case: Test device() with None input."""
+        with self.assertRaises(AttributeError):
+            rows.device(None)
+
+    def test_root_edge_none(self):
+        """Edge Case: Test root() with None input."""
+        with self.assertRaises(AttributeError):
+            rows.root(None)
+
+    def test_event_edge_none(self):
+        """Edge Case: Test event() with None input."""
+        with self.assertRaises(AttributeError):
+            rows.event(None)
+
+    def test_l1interface_edge_none(self):
+        """Edge Case: Test l1interface() with None input."""
+        with self.assertRaises(AttributeError):
+            rows.l1interface(None)
+
+    def test_mac_edge_none(self):
+        """Edge Case: Test mac() with None input."""
+        with self.assertRaises(AttributeError):
+            rows.mac(None)
+
+    def test_macip_edge_none(self):
+        """Edge Case: Test macip() with None input."""
+        with self.assertRaises(AttributeError):
+            rows.macip(None)
+
+    def test_macport_edge_none(self):
+        """Edge Case: Test macport() with None input."""
+        with self.assertRaises(AttributeError):
+            rows.macport(None)
+
+    def test_oui_edge_none(self):
+        """Edge Case: Test oui() with None input."""
+        with self.assertRaises(AttributeError):
+            rows.oui(None)
+
+    def test_vlan_edge_none(self):
+        """Edge Case: Test vlan() with None input."""
+        with self.assertRaises(AttributeError):
+            rows.vlan(None)
+
+    def test_vlanport_edge_none(self):
+        """Edge Case: Test vlanport() with None input."""
+        with self.assertRaises(AttributeError):
+            rows.vlanport(None)
+
+    def test_zone_edge_none(self):
+        """Edge Case: Test zone() with None input."""
+        with self.assertRaises(AttributeError):
+            rows.zone(None)
+
+    def test_ip_edge_none(self):
+        """Edge Case: Test ip() with None input."""
+        with self.assertRaises(AttributeError):
+            rows.ip(None)
+
+    def test_ipport_edge_none(self):
+        """Edge Case: Test ipport() with None input."""
+        with self.assertRaises(AttributeError):
+            rows.ipport(None)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tests/testlib_/errors.py b/tests/testlib_/errors.py
index 12718a4b..f68a29e7 100644
--- a/tests/testlib_/errors.py
+++ b/tests/testlib_/errors.py
@@ -223,7 +223,7 @@ def _codes(filename, to_find):
 
     """
     # Initalize key variables
-    digits = re.compile(r"^.*?(\d+).*?$")
+    digits = re.compile(r"^.*?(\d+),.*?$")
     metadata = []
     Metadata = collections.namedtuple(
         "Metadata", "filename, line, number, code"


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>tests/switchmap_/server/db/misc/test_rows.py (1)</summary><blockquote>

`56-96`: **Consider simplifying the PYTHONPATH setup.**

The current implementation uses multiple nested `os.path.abspath` and `os.path.join` calls, which is difficult to read and maintain. Consider simplifying this using a more direct approach.

```diff
-ROOT_DIR = os.path.abspath(
-    os.path.join(
-        os.path.abspath(
-            os.path.join(
-                os.path.abspath(
-                    os.path.join(
-                        os.path.abspath(
-                            os.path.join(
-                                os.path.abspath(
-                                    os.path.join(EXEC_DIR, os.pardir)
-                                ),
-                                os.pardir,
-                            )
-                        ),
-                        os.pardir,
-                    )
-                ),
-                os.pardir,
-            )
-        ),
-        os.pardir,
-    )
-)
+# Navigate up six directory levels to reach the root
+ROOT_DIR = os.path.abspath(os.path.join(EXEC_DIR, *([os.pardir] * 6)))
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 3f966e905696f9cf08a73e086e86985bf3b78c9a and 3b9eb9d88a154bc9f6fcec781baf99936f547051.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `tests/switchmap_/server/db/misc/test_rows.py` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>tests/switchmap_/server/db/misc/test_rows.py (2)</summary><blockquote>

<details>
<summary>tests/testlib_/setup.py (1)</summary>

* `save` (87-106)

</details>
<details>
<summary>switchmap/server/db/misc/rows.py (3)</summary>

* `root` (58-77)
* `event` (80-99)
* `oui` (234-257)

</details>

</blockquote></details>

</details><details>
<summary>🪛 Ruff (0.8.2)</summary>

<details>
<summary>tests/switchmap_/server/db/misc/test_rows.py</summary>

595-595: `assertRaises(Exception)` should be considered evil

(B017)

---

600-600: `assertRaises(Exception)` should be considered evil

(B017)

---

605-605: `assertRaises(Exception)` should be considered evil

(B017)

---

610-610: `assertRaises(Exception)` should be considered evil

(B017)

---

615-615: `assertRaises(Exception)` should be considered evil

(B017)

---

620-620: `assertRaises(Exception)` should be considered evil

(B017)

---

625-625: `assertRaises(Exception)` should be considered evil

(B017)

---

630-630: `assertRaises(Exception)` should be considered evil

(B017)

---

635-635: `assertRaises(Exception)` should be considered evil

(B017)

---

640-640: `assertRaises(Exception)` should be considered evil

(B017)

---

645-645: `assertRaises(Exception)` should be considered evil

(B017)

---

650-650: `assertRaises(Exception)` should be considered evil

(B017)

---

655-655: `assertRaises(Exception)` should be considered evil

(B017)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: Test Application

</details>

<details>
<summary>🔇 Additional comments (5)</summary><blockquote>

<details>
<summary>tests/switchmap_/server/db/misc/test_rows.py (5)</summary>

`1-30`: **Well-structured module docstring.**

The file begins with a comprehensive docstring that clearly explains the purpose of the test module and outlines the functions being tested. This follows best practices for documentation.

---

`102-123`: **Good test setup and teardown implementation.**

The `setUpClass` and `tearDownClass` methods appropriately initialize and clean up the test environment, including database creation, population, and cleanup. This ensures isolated test runs and prevents test pollution.

---

`128-166`: **Comprehensive test for device row conversion.**

This test thoroughly validates the `device()` function by checking tuple conversion, proper length, and value correctness. The method effectively decodes byte strings when necessary.

---

`167-198`: **Consistent and thorough test implementations across all row types.**

The tests for all row types follow a consistent pattern and thoroughly validate the conversion functionality. Each test verifies tuple conversion, length, and value correctness. The pattern is replicated effectively across all row types, ensuring comprehensive coverage.



Also applies to: 200-231, 233-284, 285-319, 320-352, 353-385, 386-418, 419-453, 454-486, 487-520, 521-555, 556-588

---

`659-661`: **Good test module entry point.**

The standard `if __name__ == "__main__"` pattern allows for direct execution of the test module, which is a good practice.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: CHANGES_REQUESTED
author_association: NONE
submitted_at: 2025-04-11T06:15:11Z
user: login: Abhi-MS
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-11T06:34:13Z
user: login: coderabbitai[bot]
state: DISMISSED
author_association: NONE
submitted_at: 2025-04-11T06:34:35Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-11T06:35:51Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>tests/switchmap_/server/db/misc/test_rows.py (3)</summary><blockquote>

`56-96`: **Consider simplifying the PYTHONPATH setup**

The current setup uses multiple nested `os.path.abspath` and `os.path.join` calls to determine the root directory. This approach is complex and might be difficult to maintain.

```diff
-ROOT_DIR = os.path.abspath(
-    os.path.join(
-        os.path.abspath(
-            os.path.join(
-                os.path.abspath(
-                    os.path.join(
-                        os.path.abspath(
-                            os.path.join(
-                                os.path.abspath(
-                                    os.path.join(EXEC_DIR, os.pardir)
-                                ),
-                                os.pardir,
-                            )
-                        ),
-                        os.pardir,
-                    )
-                ),
-                os.pardir,
-            )
-        ),
-        os.pardir,
-    )
-)
+# Navigate up 6 directories to reach the root
+ROOT_DIR = os.path.abspath(os.path.join(EXEC_DIR, *(os.pardir for _ in range(6))))
```

---

`128-587`: **Consider refactoring repetitive test code**

There's significant code duplication across all the test functions for different row types. They follow the same pattern: get a row, convert it, assert instance type, assert length, and check each attribute value.

Consider creating a helper method to reduce duplication:

```python
def _test_row_conversion(self, model_class, converter_func, row_attributes, expected_length):
    """Helper to test row conversion functions.
    
    Args:
        model_class: The model class to query (e.g., Device)
        converter_func: The rows module function to test (e.g., rows.device)
        row_attributes: List of attribute names to check
        expected_length: Expected length of the resulting tuple
    """
    row = self.session.query(model_class).first()
    result = converter_func(row)
    self.assertIsInstance(result, tuple)
    self.assertEqual(len(result), expected_length)
    
    # Check each value in the tuple
    for i, attr in enumerate(row_attributes):
        row_value = getattr(row, attr)
        
        # Decode byte string if necessary
        if isinstance(row_value, bytes):
            row_value = row_value.decode()
            
        self.assertEqual(
            row_value,
            result[i],
            f"Mismatch at index {i} for attribute '{attr}'"
        )
```

Then each test could be simplified to:

```python
def test_device(self):
    """Test the device() function for correct tuple conversion."""
    row_attributes = [
        "idx_device", "idx_zone", "sys_name", "hostname", "name", 
        "sys_description", "sys_objectid", "sys_uptime", "last_polled", 
        "enabled", "ts_modified", "ts_created"
    ]
    self._test_row_conversion(Device, rows.device, row_attributes, 12)
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Actions: PR Workflow</summary>

[error] 267-267: Extremely small error code 1 found. Must be greater than 1000. Please fix.

</details>

</details>

---

`593-656`: **Consider additional edge cases**

The tests currently verify behavior when passing None as input. Consider adding tests for other edge cases:

1. Rows with missing required attributes
2. Rows with null values in optional fields
3. Rows with attributes of unexpected types

This would make the test suite more robust against potential issues.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 3b9eb9d88a154bc9f6fcec781baf99936f547051 and 4d1a681dd203ad973c228cb2d734ab42da1fe38d.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `tests/switchmap_/server/db/misc/test_rows.py` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧠 Learnings (1)</summary>

<details>
<summary>tests/switchmap_/server/db/misc/test_rows.py (1)</summary>

```
Learnt from: Abhi-MS
PR: PalisadoesFoundation/switchmap-ng#319
File: tests/switchmap_/server/db/misc/test_rows.py:593-656
Timestamp: 2025-04-11T06:35:50.860Z
Learning: In the switchmap-ng project, when passing None to the row conversion functions in rows.py, an AttributeError is raised because these functions attempt to access attributes of None (e.g., row.idx_root). Tests for these functions should use assertRaises(AttributeError) rather than a generic Exception or TypeError.
```

</details>

</details><details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>tests/switchmap_/server/db/misc/test_rows.py (1)</summary><blockquote>

<details>
<summary>switchmap/server/db/misc/rows.py (3)</summary>

* `root` (58-77)
* `event` (80-99)
* `oui` (234-257)

</details>

</blockquote></details>

</details><details>
<summary>🪛 GitHub Actions: PR Workflow</summary>

<details>
<summary>tests/switchmap_/server/db/misc/test_rows.py</summary>

[error] 267-267: Extremely small error code 1 found. Must be greater than 1000. Please fix.

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (5)</summary><blockquote>

<details>
<summary>tests/switchmap_/server/db/misc/test_rows.py (5)</summary>

`1-30`: **Great comprehensive test module introduction!**

The module docstring is excellent - clearly states the purpose of the tests, lists all functions being tested, and outlines the test approach including edge cases. This level of documentation makes the test suite purpose immediately clear.

---

`102-122`: **Well-structured test setup and teardown**

The setup and teardown methods properly manage the test environment by configuring, creating, and cleaning up the database resources, ensuring tests run in isolation.

---

`128-166`: **Thorough test for device() function**

The test verifies all aspects of the conversion: tuple type, length, and value equality for each attribute. The byte string handling is properly implemented.

---

`593-656`: **Correct exception testing for None inputs**

Your tests correctly use `assertRaises(AttributeError)` when passing None to the row conversion functions, which aligns with how these functions behave (they attempt to access attributes of None).

---

`267-267`: <details>
<summary>❓ Verification inconclusive</summary>

**Fix error code issue flagged by pipeline**

The pipeline reports: "Extremely small error code 1 found. Must be greater than 1000. Please fix." This could be related to a project-specific convention or linting rule. Check if there are implicit error codes in this line that need to be adjusted.

---

🏁 Script executed:

```shell
#!/bin/bash
# Search for patterns of error codes in the codebase

# Look for error code patterns or constants
rg "error.*code|err.*code" --no-filename
```

Length of output: 123514

---



</details>

**Review: Verify Error Code Convention Usage**

After carefully searching the codebase for error code references, we did not find any evidence that the test line
```python
row = self.session.query(L1Interface).first()
```
directly uses or returns an error code of “1”. The pipeline’s message — "Extremely small error code 1 found. Must be greater than 1000. Please fix." — appears to be triggered by our internal error handling or logging routines elsewhere in the code (for example, within functions that call `log.log2die()`) rather than from this query statement.

• **Action Required:** Confirm whether the violation is indirectly triggered by this test file or by another module that defines the error code conventions.  
• **Recommendation:** Manually verify the error code assignment logic (e.g., in our logging or error management helpers) to ensure that all error codes meet the minimum threshold of 1000, and update them if needed.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Actions: PR Workflow</summary>

[error] 267-267: Extremely small error code 1 found. Must be greater than 1000. Please fix.

</details>

</details>

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-11T06:40:09Z
user: login: coderabbitai[bot]
state: DISMISSED
author_association: NONE
submitted_at: 2025-04-11T06:40:12Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>📜 Review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 4d1a681dd203ad973c228cb2d734ab42da1fe38d and 0a073175b8c62e1f13ac4b707cbc799e8c3ef3f8.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `tests/switchmap_/server/db/misc/test_rows.py` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>tests/switchmap_/server/db/misc/test_rows.py (1)</summary><blockquote>

<details>
<summary>switchmap/server/db/misc/rows.py (3)</summary>

* `root` (58-77)
* `event` (80-99)
* `oui` (234-257)

</details>

</blockquote></details>

</details><details>
<summary>🪛 GitHub Actions: PR Workflow</summary>

<details>
<summary>tests/switchmap_/server/db/misc/test_rows.py</summary>

[error] 271-271: Extremely small error code 1 found. Must be greater than 1000. Please fix.

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (6)</summary><blockquote>

<details>
<summary>tests/switchmap_/server/db/misc/test_rows.py (6)</summary>

`1-30`: **Great comprehensive docstring!**

The module docstring clearly explains the purpose, scope, and expectations of these tests. It's well-structured and provides a solid overview of what's being tested.

---

`32-55`: **Appropriate imports and dependencies.**

All necessary modules are imported correctly, including the rows module being tested, the database models, and testing utilities.

---

`99-123`: **Well-structured test setup and teardown.**

The `setUpClass` and `tearDownClass` methods properly initialize and clean up the test environment, ensuring isolated test runs with proper database setup and teardown.

---

`128-167`: **Good test implementation for the device() function.**

This test thoroughly validates the device() function by checking the tuple length and comparing each value against the expected database row attributes.

---

`168-604`: **Thorough testing of all conversion functions.**

The test implementations for all the remaining functions (root(), event(), l1interface(), etc.) follow a consistent and thorough pattern, validating tuple conversion, length, and value accuracy.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Actions: PR Workflow</summary>

[error] 271-271: Extremely small error code 1 found. Must be greater than 1000. Please fix.

</details>

</details>

---

`610-673`: **Correct exception handling for None inputs.**

These edge case tests correctly use `assertRaises(AttributeError)` which is the specific exception that occurs when attributes are accessed on None. This is an improvement over using a generic Exception as noted in the past review comments.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: CHANGES_REQUESTED
author_association: NONE
submitted_at: 2025-04-12T17:52:41Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>♻️ Duplicate comments (2)</summary><blockquote>

<details>
<summary>tests/switchmap_/server/db/misc/test_rows.py (2)</summary><blockquote>

`388-451`: **Properly implemented edge case tests with specific exception types**

The tests correctly use `assertRaises(AttributeError)` instead of generic `Exception` type, which aligns with the project's standards and previous review feedback.

---

`240-242`: _⚠️ Potential issue_

**Update error message to include proper error code**

According to previous review comments, error codes must be greater than 1000. The assertion message here should include an appropriate error code.



```diff
-        self.assertIsNotNone(row, "No Event row found in the test database.")
+        self.assertIsNotNone(row, "Error 1011: No Event row found in the test database.")
```

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>tests/switchmap_/server/db/misc/test_rows.py (2)</summary><blockquote>

`57-96`: **Consider simplifying the path calculation logic**

The current implementation uses multiple nested calls to `os.path.abspath` and `os.path.join` which makes the code harder to read and maintain.


```diff
-ROOT_DIR = os.path.abspath(
-    os.path.join(
-        os.path.abspath(
-            os.path.join(
-                os.path.abspath(
-                    os.path.join(
-                        os.path.abspath(
-                            os.path.join(
-                                os.path.abspath(
-                                    os.path.join(EXEC_DIR, os.pardir)
-                                ),
-                                os.pardir,
-                            )
-                        ),
-                        os.pardir,
-                    )
-                ),
-                os.pardir,
-            )
-        ),
-        os.pardir,
-    )
-)
+# Navigate up 7 directories from the current script's directory
+ROOT_DIR = os.path.abspath(os.path.join(EXEC_DIR, *([os.pardir] * 7)))
```

---

`213-239`: **Test data creation approach is inconsistent with other tests**

While most tests query existing database records, this test creates new data directly. Consider using a consistent approach across all tests, either by pre-populating all test data in the `populate()` function or creating test data inline for all tests.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 8399a70b79591ba3d9a9977b62467b1fa701a700 and 28eb350818d4924ecabd3068a5fc62a971c5dc77.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `tests/switchmap_/server/db/misc/test_rows.py` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>tests/switchmap_/server/db/misc/test_rows.py (1)</summary><blockquote>

<details>
<summary>switchmap/server/db/misc/rows.py (3)</summary>

* `root` (58-77)
* `event` (80-99)
* `oui` (234-257)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>🔇 Additional comments (2)</summary><blockquote>

<details>
<summary>tests/switchmap_/server/db/misc/test_rows.py (2)</summary>

`1-30`: **Comprehensive docstring with clear test objectives**

The docstring provides an excellent overview of what's being tested and why. It clearly lists each function under test and explains the aspects being validated (tuple conversion, length, data types, and exception handling).

---

`128-161`: **Well-implemented helper method for row comparison**

The `compare_row_to_expected` method is a good approach to reduce code duplication across test methods. It properly handles byte string normalization and uses subTest to provide better context in case of failures.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-12T21:45:52Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>tests/switchmap_/server/db/misc/test_rows.py (1)</summary><blockquote>

`60-98`: **Consider simplifying the path manipulation code.**

While the current implementation works, the nested `os.path.abspath` and `os.path.join` calls create significant complexity. Consider using a more direct approach like:

```diff
- ROOT_DIR = os.path.abspath(
-     os.path.join(
-         os.path.abspath(
-             os.path.join(
-                 os.path.abspath(
-                     os.path.join(
-                         os.path.abspath(
-                             os.path.join(
-                                 os.path.abspath(
-                                     os.path.join(EXEC_DIR, os.pardir)
-                                 ),
-                                 os.pardir,
-                             )
-                         ),
-                         os.pardir,
-                     )
-                 ),
-                 os.pardir,
-             )
-         ),
-         os.pardir,
-     )
- )
+ # Navigate up 6 directories from current directory to reach project root
+ ROOT_DIR = os.path.abspath(os.path.join(EXEC_DIR, *([os.pardir] * 6)))
```

This achieves the same result more concisely and with less nesting.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 3f966e905696f9cf08a73e086e86985bf3b78c9a and effb40eaf39fee4d6f79971ae08eb85810ff888c.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `tests/switchmap_/server/db/misc/test_rows.py` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>tests/switchmap_/server/db/misc/test_rows.py (1)</summary><blockquote>

<details>
<summary>switchmap/server/db/misc/rows.py (3)</summary>

* `root` (58-77)
* `event` (80-99)
* `oui` (234-257)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>🔇 Additional comments (7)</summary><blockquote>

<details>
<summary>tests/switchmap_/server/db/misc/test_rows.py (7)</summary>

`1-30`: **Excellent test file organization and documentation.**

The module docstring is comprehensive and clearly outlines the purpose of the test suite, the functions being tested, and the aspects being verified. This makes it easy for other developers to understand the test coverage and purpose.

---

`52-54`: **Good workaround for error.py issue with L1Interface.**

The renaming of L1Interface to LOneInterface is a clever solution to avoid errors, with a clear comment explaining the rationale.

---

`105-126`: **Well-structured setup and teardown methods.**

The class-level setup and teardown methods properly initialize and clean up the test environment, ensuring tests run in isolation. Good practice to use `@classmethod` for efficiency.

---

`131-164`: **Excellent helper method implementation.**

The `compare_row_to_expected` method is well-designed:
1. It handles byte string normalization correctly
2. Uses `subTest` for clearer assertion failure messages
3. Provides detailed error messages with the attribute name

This greatly reduces code duplication across test cases.

---

`366-429`: **Good implementation of edge case tests.**

The edge case tests correctly use `assertRaises(AttributeError)` instead of generic `Exception`, addressing the previous review feedback. This is more precise and better validates the expected behavior when `None` is passed to these functions.

---

`167-178`: **Comprehensive test for device() function.**

The test correctly verifies:
1. The function returns a tuple
2. The tuple has the expected length
3. Each element in the tuple matches the corresponding attribute in the row

This pattern is consistently followed throughout the test suite, which is excellent for maintainability.

---

`165-271`: <details>
<summary>✅ Verification successful</summary>

**Ensure error code meets project requirements.**

There was a pipeline failure indicating an extremely small error code on or around line 271. While no explicit error code is visible in this segment, ensure any error codes used (possibly in error messages or assertions) are greater than 1000 as per project standards.

---

🏁 Script executed:

```shell
#!/bin/bash
# Check for any error codes around line 271
grep -n -A 2 -B 2 '[0-9][0-9][0-9][0-9]' tests/switchmap_/server/db/misc/test_rows.py | grep -E '26[0-9]|27[0-9]|28[0-9]'
```

Length of output: 123

---



</details>

**Pipeline Verification Complete: No error code issues found near line 271.**

After running a grep search for four-digit error codes around the reported area, there's no evidence in the file at `tests/switchmap_/server/db/misc/test_rows.py` that any error codes (or numeric error messages) are defined or used, and none appear to be below the 1000 threshold. The pipeline failure error indication seems unrelated to this segment.

- Verified that no four-digit error codes are present near line 271.
- The functions in the file consistently perform tuple conversions and standard assertions, without involving explicit error codes.

Please confirm if there might be related issues in other parts of the code, though no adjustments are necessary here based on current evidence.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: CHANGES_REQUESTED
author_association: NONE
submitted_at: 2025-04-13T22:27:58Z
user: login: coderabbitai[bot]
state: DISMISSED
author_association: NONE
submitted_at: 2025-04-13T23:06:48Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>♻️ Duplicate comments (1)</summary><blockquote>

<details>
<summary>tests/switchmap_/server/db/misc/test_rows.py (1)</summary><blockquote>

`364-428`: **Correct implementation of edge case tests using specific exception type.**

The edge case tests correctly use `assertRaises(AttributeError)` when testing with None input, in accordance with the project's requirements and previous feedback. This verifies that the functions fail appropriately when given invalid inputs.

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>tests/switchmap_/server/db/misc/test_rows.py (1)</summary><blockquote>

`58-96`: **Consider simplifying the path construction logic.**

While functional, the nested `os.path.join` and `os.path.abspath` calls make the path construction difficult to read and maintain. Consider refactoring to a more straightforward approach.


```diff
-EXEC_DIR = os.path.dirname(os.path.realpath(__file__))
-ROOT_DIR = os.path.abspath(
-    os.path.join(
-        os.path.abspath(
-            os.path.join(
-                os.path.abspath(
-                    os.path.join(
-                        os.path.abspath(
-                            os.path.join(
-                                os.path.abspath(
-                                    os.path.join(EXEC_DIR, os.pardir)
-                                ),
-                                os.pardir,
-                            )
-                        ),
-                        os.pardir,
-                    )
-                ),
-                os.pardir,
-            )
-        ),
-        os.pardir,
-    )
-)
+EXEC_DIR = os.path.dirname(os.path.realpath(__file__))
+# Navigate up 6 directory levels to reach the root
+ROOT_DIR = os.path.abspath(os.path.join(EXEC_DIR, *([os.pardir] * 6)))
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between d1119b8d800f649059acbac59495efdb857ec193 and 8a7b2410f7ba5f97f231487b2942a716c0e56ea3.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `tests/switchmap_/server/db/misc/test_rows.py` (1 hunks)
* `tests/testlib_/errors.py` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧠 Learnings (1)</summary>

<details>
<summary>tests/switchmap_/server/db/misc/test_rows.py (1)</summary>

```
Learnt from: Abhi-MS
PR: PalisadoesFoundation/switchmap-ng#319
File: tests/switchmap_/server/db/misc/test_rows.py:593-656
Timestamp: 2025-04-11T06:35:50.860Z
Learning: In the switchmap-ng project, when passing None to the row conversion functions in rows.py, an AttributeError is raised because these functions attempt to access attributes of None (e.g., row.idx_root). Tests for these functions should use assertRaises(AttributeError) rather than a generic Exception or TypeError.
```

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: Test Application

</details>

<details>
<summary>🔇 Additional comments (5)</summary><blockquote>

<details>
<summary>tests/testlib_/errors.py (1)</summary>

`226-226`: <details>
<summary>✅ Verification successful</summary>

**Well-implemented regex pattern refinement.**

The change from `r"^.*?(\d+).*?$"` to `r"^.*?(\d+),.*?$"` improves error code extraction by only matching digit sequences followed by a comma. This prevents standalone numbers from being incorrectly identified as error codes.


---

🏁 Script executed:

```shell
#!/bin/bash
# Verify if the regex pattern change resolves false positives in error code detection
# Pattern now requires the digit to be followed by a comma

# Check for potential false positives with the old pattern (any standalone numbers)
echo "Testing string with standalone number: log.debug(1)"
echo "log.debug(1)" | grep -P "^.*?(\d+).*?$" || echo "No match with old pattern"

# Check if the new pattern correctly ignores standalone numbers
echo "Testing string with standalone number: log.debug(1)"
echo "log.debug(1)" | grep -P "^.*?(\d+),.*?$" || echo "No match with new pattern"

# Check if the new pattern correctly matches valid error codes
echo "Testing string with error code: log.debug(1091, 'message')"
echo "log.debug(1091, 'message')" | grep -P "^.*?(\d+),.*?$" || echo "No match with new pattern for valid code"
```

Length of output: 625

---



</details>

**Regex Pattern Verification Approved**

- The update from `r"^.*?(\d+).*?$"` to `r"^.*?(\d+),.*?$"` in `tests/testlib_/errors.py` (line 226) is correctly implemented.
- The executed tests confirm that the new regex pattern no longer matches standalone numbers (e.g., `log.debug(1)`) while successfully matching error codes (e.g., `log.debug(1091, 'message')`).

</details>
<details>
<summary>tests/switchmap_/server/db/misc/test_rows.py (4)</summary>

`1-30`: **Excellent documentation and test coverage for rows.py module.**

The comprehensive docstring clearly outlines the test suite's purpose and the specific functions being tested. The inclusion of edge cases and validation criteria demonstrates a thorough testing approach.

---

`100-124`: **Well-structured test class with proper setup and teardown.**

The test class follows best practices by properly setting up and tearing down the test environment. Database creation and cleanup are handled appropriately to ensure test isolation.

---

`129-162`: **Robust implementation of the comparison helper method.**

The `compare_row_to_expected` method is well-designed with:
1. Clear documentation explaining its purpose
2. Proper handling of byte string normalization
3. Effective use of `subTest` for granular test reporting
4. Inclusion of error code (1091) in assertion message, meeting the project's requirement for error codes to be >1000

---

`163-359`: **Comprehensive test coverage for all row conversion functions.**

Each test method follows a consistent pattern:
1. Retrieving a row from the database
2. Verifying non-null results
3. Converting to a tuple
4. Validating the tuple type and length
5. Comparing all attributes against expected values

The consistent structure makes the test suite easy to understand and maintain.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-15T03:25:34Z
user: login: coderabbitai[bot]
state: DISMISSED
author_association: NONE
submitted_at: 2025-04-15T03:25:37Z
user: login: coderabbitai[bot]
state: APPROVED
author_association: NONE
submitted_at: 2025-04-15T03:49:45Z
user: login: palisadoes
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T16:53:13Z

Review comments:
diff_hunk: @@ -0,0 +1,660 @@
+#!/usr/bin/env python3
+"""Test the rows module.
+
+This module contains unit tests for the functions in the `rows.py` file.
+The tests ensure that the following functions correctly convert
+database rows to tuples:
+
+- device()
+- root()
+- event()
+- l1interface()
+- mac()
+- macip()
+- macport()
+- oui()
+- vlan()
+- vlanport()
+- zone()
+- ip()
+- ipport()
+
+Additionally, edge cases where the input is None are tested to ensure that
+each function properly raises exceptions when provided with invalid input.
+
+The tests verify the following:
+- Correct tuple conversion for each function.
+- Correct tuple length and data type.
+- Correct value in the tuple for each entry in the row
+- Handling of invalid (None) inputs with appropriate exceptions.
+"""
+
+import os
+import sys
+
+import unittest
+from switchmap.server.db.misc import rows
+from switchmap.server.db.models import (
+    Device,
+    Event,
+    Ip,
+    IpPort,
+    L1Interface,
+    Mac,
+    MacIp,
+    MacPort,
+    Oui,
+    Root,
+    Vlan,
+    VlanPort,
+    Zone,
+)
+from switchmap.server.db import SCOPED_SESSION
+from tests.testlib_ import setup
+from tests.testlib_ import db
+
+# Try to create a working PYTHONPATH
+EXEC_DIR = os.path.dirname(os.path.realpath(__file__))
+ROOT_DIR = os.path.abspath(
+    os.path.join(
+        os.path.abspath(
+            os.path.join(
+                os.path.abspath(
+                    os.path.join(
+                        os.path.abspath(
+                            os.path.join(
+                                os.path.abspath(
+                                    os.path.join(EXEC_DIR, os.pardir)
+                                ),
+                                os.pardir,
+                            )
+                        ),
+                        os.pardir,
+                    )
+                ),
+                os.pardir,
+            )
+        ),
+        os.pardir,
+    )
+)
+_EXPECTED = """\
+{0}switchmap-ng{0}tests{0}switchmap_{0}server{0}db{0}misc""".format(
+    os.sep
+)
+if EXEC_DIR.endswith(_EXPECTED) is True:
+    # We need to prepend the path in case the repo has been installed
+    # elsewhere on the system using PIP. This could corrupt expected results
+    sys.path.insert(0, ROOT_DIR)
+else:
+    print(
+        """This script is not installed in the
+        "{0}" directory. Please fix.""".format(
+            _EXPECTED
+        )
+    )
+    sys.exit(2)
+
+
+class TestRowsMethods(unittest.TestCase):
+    """Test suite for functions in rows.py."""
+
+    @classmethod
+    def setUpClass(cls):
+        """Set up test environment before running tests."""
+        # Load the configuration
+        cls.config = setup.config()
+        cls.config.save()
+        # Drop tables
+        cls.database = db.Database()
+        cls.database.drop()
+        cls.database.create()
+        # Populate the database
+        db.populate()
+        cls.session = SCOPED_SESSION()
+
+    @classmethod
+    def tearDownClass(cls):
+        """Clean up test environment after tests."""
+        if cls.session:
+            cls.session.close()
+        cls.database.drop()
+        cls.config.cleanup()
+
+    # ----------------------------------------------------------------------
+    # Standard conversion tests
+    # ----------------------------------------------------------------------
+
+    def test_device(self):
+        """Test the device() function for correct tuple conversion.
+
+        Verifies that the function returns a tuple of length 12
+        when given a Device row from the database.
+        """
+        # List of row attributes to match with the tuple values
+        row_attributes = [
+            "idx_device",
+            "idx_zone",
+            "sys_name",
+            "hostname",
+            "name",
+            "sys_description",
+            "sys_objectid",
+            "sys_uptime",
+            "last_polled",
+            "enabled",
+            "ts_modified",
+            "ts_created",
+        ]
+        row = self.session.query(Device).first()
+        result = rows.device(row)
+        self.assertIsInstance(result, tuple)
+        self.assertEqual(len(result), 12)
+        # Check each values in the tuple
+        for i, attr in enumerate(row_attributes):
+            row_value = getattr(row, attr)
+
+            # Decode byte string if necessary
+            if isinstance(row_value, bytes):
+                row_value = row_value.decode()
+
+            self.assertEqual(
+                row_value,
+                result[i],
+                f"Mismatch at index {i} for attribute '{attr}'",
+            )
+
+    def test_root(self):
+        """Test the root() function for correct tuple conversion.
+
+        Verifies that the function returns a tuple of length 6
+        when given a Root row from the database.
+        """
+        # List of row attributes to match with the tuple values
+        row_attributes = [
+            "idx_root",
+            "idx_event",
+            "name",
+            "enabled",
+            "ts_modified",
+            "ts_created",
+        ]
+        row = self.session.query(Root).first()
+        result = rows.root(row)
+        self.assertIsInstance(result, tuple)
+        self.assertEqual(len(result), 6)
+        # Check each values in the tuple
+        for i, attr in enumerate(row_attributes):
+            row_value = getattr(row, attr)
+
+            # Decode byte string if necessary
+            if isinstance(row_value, bytes):
+                row_value = row_value.decode()
+
+            self.assertEqual(
+                row_value,
+                result[i],
+                f"Mismatch at index {i} for attribute '{attr}'",
+            )
+
+    def test_event(self):
+        """Test the event() function for correct tuple conversion.
+
+        Verifies that the function returns a tuple of length 6
+        when given an Event row from the database.
+        """
+        # List of row attributes to match with the tuple values
+        row_attributes = [
+            "idx_event",
+            "name",
+            "epoch_utc",
+            "enabled",
+            "ts_modified",
+            "ts_created",
+        ]
+        row = self.session.query(Event).first()
+        result = rows.event(row)
+        self.assertIsInstance(result, tuple)
+        self.assertEqual(len(result), 6)
+        # Check each values in the tuple
+        for i, attr in enumerate(row_attributes):
+            row_value = getattr(row, attr)
+
+            # Decode byte string if necessary
+            if isinstance(row_value, bytes):
+                row_value = row_value.decode()
+
+            self.assertEqual(
+                row_value,
+                result[i],
+                f"Mismatch at index {i} for attribute '{attr}'",
+            )
+
+    def test_l1interface(self):
+        """Test the l1interface() function for correct tuple conversion.
+
+        Verifies that the function returns a tuple of length 25
+        when given a L1Interface row from the database.
+        """
+        # List of row attributes to match with the tuple values
+        row_attributes = [
+            "idx_l1interface",
+            "idx_device",
+            "ifindex",
+            "duplex",
+            "ethernet",
+            "nativevlan",
+            "trunk",
+            "ifspeed",
+            "iftype",
+            "ifalias",
+            "ifdescr",
+            "ifname",
+            "ifadminstatus",
+            "ifoperstatus",
+            "ts_idle",
+            "cdpcachedeviceid",
+            "cdpcachedeviceport",
+            "cdpcacheplatform",
+            "lldpremportdesc",
+            "lldpremsyscapenabled",
+            "lldpremsysdesc",
+            "lldpremsysname",
+            "enabled",
+            "ts_modified",
+            "ts_created",
+        ]
+        row = self.session.query(L1Interface).first()
+        result = rows.l1interface(row)
+        self.assertIsInstance(result, tuple)
+        self.assertEqual(len(result), 25)
+        # Check each values in the tuple
+        for i, attr in enumerate(row_attributes):
+            row_value = getattr(row, attr)
+
+            # Decode byte string if necessary
+            if isinstance(row_value, bytes):
+                row_value = row_value.decode()
+
+            self.assertEqual(
+                row_value,
+                result[i],
+                f"Mismatch at index {i} for attribute '{attr}'",
+            )
+
+    def test_mac(self):
+        """Test the mac() function for correct tuple conversion.
+
+        Verifies that the function returns a tuple of length 7
+        when given a Mac row from the database.
+        """
+        # List of row attributes to match with the tuple values
+        row_attributes = [
+            "idx_mac",
+            "idx_oui",
+            "idx_zone",
+            "mac",
+            "enabled",
+            "ts_modified",
+            "ts_created",
+        ]
+        row = self.session.query(Mac).first()
+        result = rows.mac(row)
+        self.assertIsInstance(result, tuple)
+        self.assertEqual(len(result), 7)
+
+        # Check each values in the tuple
+        for i, attr in enumerate(row_attributes):
+            row_value = getattr(row, attr)
+
+            # Decode byte string if necessary
+            if isinstance(row_value, bytes):
+                row_value = row_value.decode()
+
+            self.assertEqual(
+                row_value,
+                result[i],
+                f"Mismatch at index {i} for attribute '{attr}'",
+            )
+
+    def test_macip(self):
+        """Test the macip() function for correct tuple conversion.
+
+        Verifies that the function returns a tuple of length 6
+        when given a MacIp row from the database.
+        """
+        # List of row attributes to match with the tuple values
+        row_attributes = [
+            "idx_macip",
+            "idx_ip",
+            "idx_mac",
+            "enabled",
+            "ts_modified",
+            "ts_created",
+        ]
+        row = self.session.query(MacIp).first()
+        result = rows.macip(row)
+        self.assertIsInstance(result, tuple)
+        self.assertEqual(len(result), 6)
+        # Check each values in the tuple
+        for i, attr in enumerate(row_attributes):
+            row_value = getattr(row, attr)
+
+            # Decode byte string if necessary
+            if isinstance(row_value, bytes):
+                row_value = row_value.decode()
+
+            self.assertEqual(
+                row_value,
+                result[i],
+                f"Mismatch at index {i} for attribute '{attr}'",
+            )
+
+    def test_macport(self):
+        """Test the macport() function for correct tuple conversion.
+
+        Verifies that the function returns a tuple of length 6
+        when given a MacPort row from the database.
+        """
+        # List of row attributes to match with the tuple values
+        row_attributes = [
+            "idx_macport",
+            "idx_l1interface",
+            "idx_mac",
+            "enabled",
+            "ts_modified",
+            "ts_created",
+        ]
+        row = self.session.query(MacPort).first()
+        result = rows.macport(row)
+        self.assertIsInstance(result, tuple)
+        self.assertEqual(len(result), 6)
+        # Check each values in the tuple
+        for i, attr in enumerate(row_attributes):
+            row_value = getattr(row, attr)
+
+            # Decode byte string if necessary
+            if isinstance(row_value, bytes):
+                row_value = row_value.decode()
+
+            self.assertEqual(
+                row_value,
+                result[i],
+                f"Mismatch at index {i} for attribute '{attr}'",
+            )
+
+    def test_oui(self):
+        """Test the oui() function for correct tuple conversion.
+
+        Verifies that the function returns a tuple of length 6
+        when given a Oui row from the database.
+        """
+        # List of row attributes to match with the tuple values
+        row_attributes = [
+            "idx_oui",
+            "oui",
+            "organization",
+            "enabled",
+            "ts_modified",
+            "ts_created",
+        ]
+        row = self.session.query(Oui).first()
+        result = rows.oui(row)
+        self.assertIsInstance(result, tuple)
+        self.assertEqual(len(result), 6)
+        # Check each values in the tuple
+        for i, attr in enumerate(row_attributes):
+            row_value = getattr(row, attr)
+
+            # Decode byte string if necessary
+            if isinstance(row_value, bytes):
+                row_value = row_value.decode()
+
+            self.assertEqual(
+                row_value,
+                result[i],
+                f"Mismatch at index {i} for attribute '{attr}'",
+            )
+
+    def test_vlan(self):
+        """Test the vlan() function for correct tuple conversion.
+
+        Verifies that the function returns a tuple of length 8
+        when given a Vlan row from the database.
+        """
+        # List of row attributes to match with the tuple values
+        row_attributes = [
+            "idx_vlan",
+            "idx_device",
+            "vlan",
+            "name",
+            "state",
+            "enabled",
+            "ts_modified",
+            "ts_created",
+        ]
+        row = self.session.query(Vlan).first()
+        result = rows.vlan(row)
+        self.assertIsInstance(result, tuple)
+        self.assertEqual(len(result), 8)
+        # Check each values in the tuple
+        for i, attr in enumerate(row_attributes):
+            row_value = getattr(row, attr)
+
+            # Decode byte string if necessary
+            if isinstance(row_value, bytes):
+                row_value = row_value.decode()
+
+            self.assertEqual(
+                row_value,
+                result[i],
+                f"Mismatch at index {i} for attribute '{attr}'",
+            )
+
+    def test_vlanport(self):
+        """Test the vlanport() function for correct tuple conversion.
+
+        Verifies that the function returns a tuple of length 6
+        when given a VlanPort row from the database.
+        """
+        # List of row attributes to match with the tuple values
+        row_attributes = [
+            "idx_vlanport",
+            "idx_l1interface",
+            "idx_vlan",
+            "enabled",
+            "ts_modified",
+            "ts_created",
+        ]
+        row = self.session.query(VlanPort).first()
+        result = rows.vlanport(row)
+        self.assertIsInstance(result, tuple)
+        self.assertEqual(len(result), 6)
+        # Check each values in the tuple
+        for i, attr in enumerate(row_attributes):
+            row_value = getattr(row, attr)
+
+            # Decode byte string if necessary
+            if isinstance(row_value, bytes):
+                row_value = row_value.decode()
+
+            self.assertEqual(
+                row_value,
+                result[i],
+                f"Mismatch at index {i} for attribute '{attr}'",
+            )
+
+    def test_zone(self):
+        """Test the zone() function for correct tuple conversion.
+
+        Verifies that the function returns a tuple of length 7
+        when given a Zone row from the database.
+        """
+        # List of row attributes to match with the tuple values
+        row_attributes = [
+            "idx_zone",
+            "idx_event",
+            "name",
+            "notes",
+            "enabled",
+            "ts_modified",
+            "ts_created",
+        ]
+        row = self.session.query(Zone).first()
+        result = rows.zone(row)
+        self.assertIsInstance(result, tuple)
+        self.assertEqual(len(result), 7)
+        # Check each values in the tuple
+        for i, attr in enumerate(row_attributes):
+            row_value = getattr(row, attr)
+
+            # Decode byte string if necessary
+            if isinstance(row_value, bytes):
+                row_value = row_value.decode()
+
+            self.assertEqual(
+                row_value,
+                result[i],
+                f"Mismatch at index {i} for attribute '{attr}'",
+            )
+
+    def test_ip(self):
+        """Test the ip() function for correct tuple conversion.
+
+        Verifies that the function returns a tuple of length 8
+        when given a Ip row from the database.
+        """
+        # List of row attributes to match with the tuple values
+        row_attributes = [
+            "idx_ip",
+            "idx_zone",
+            "address",
+            "version",
+            "hostname",
+            "enabled",
+            "ts_modified",
+            "ts_created",
+        ]
+        row = self.session.query(Ip).first()
+        result = rows.ip(row)
+        self.assertIsInstance(result, tuple)
+        self.assertEqual(len(result), 8)
+        # Check each values in the tuple
+        for i, attr in enumerate(row_attributes):
+            row_value = getattr(row, attr)
+
+            # Decode byte string if necessary
+            if isinstance(row_value, bytes):
+                row_value = row_value.decode()
+
+            self.assertEqual(
+                row_value,
+                result[i],
+                f"Mismatch at index {i} for attribute '{attr}'",
+            )
+
+    def test_ipport(self):
+        """Test the ipport() function for correct tuple conversion.
+
+        Verifies that the function returns a tuple of length 6
+        when given a IpPort row from the database.
+        """
+        # List of row attributes to match with the tuple values
+        row_attributes = [
+            "idx_ipport",
+            "idx_l1interface",
+            "idx_ip",
+            "enabled",
+            "ts_modified",
+            "ts_created",
+        ]
+        row = self.session.query(IpPort).first()
+        result = rows.ipport(row)
+        self.assertIsInstance(result, tuple)
+        self.assertEqual(len(result), 6)
+        # Check each values in the tuple
+        for i, attr in enumerate(row_attributes):
+            row_value = getattr(row, attr)
+
+            # Decode byte string if necessary
+            if isinstance(row_value, bytes):
+                row_value = row_value.decode()
+
+            self.assertEqual(
+                row_value,
+                result[i],
+                f"Mismatch at index {i} for attribute '{attr}'",
+            )
+
+    # ----------------------------------------------------------------------
+    # --- Edge case: None input tests ---
+    # ----------------------------------------------------------------------
+
+    def test_device_edge_none(self):
+        """Edge Case: Test device() with None input."""
+        with self.assertRaises(Exception):
+            rows.device(None)
+
+    def test_root_edge_none(self):
+        """Edge Case: Test root() with None input."""
+        with self.assertRaises(Exception):
+            rows.root(None)
+
+    def test_event_edge_none(self):
+        """Edge Case: Test event() with None input."""
+        with self.assertRaises(Exception):
+            rows.event(None)
+
+    def test_l1interface_edge_none(self):
+        """Edge Case: Test l1interface() with None input."""
+        with self.assertRaises(Exception):
+            rows.l1interface(None)
+
+    def test_mac_edge_none(self):
+        """Edge Case: Test mac() with None input."""
+        with self.assertRaises(Exception):
+            rows.mac(None)
+
+    def test_macip_edge_none(self):
+        """Edge Case: Test macip() with None input."""
+        with self.assertRaises(Exception):
+            rows.macip(None)
+
+    def test_macport_edge_none(self):
+        """Edge Case: Test macport() with None input."""
+        with self.assertRaises(Exception):
+            rows.macport(None)
+
+    def test_oui_edge_none(self):
+        """Edge Case: Test oui() with None input."""
+        with self.assertRaises(Exception):
+            rows.oui(None)
+
+    def test_vlan_edge_none(self):
+        """Edge Case: Test vlan() with None input."""
+        with self.assertRaises(Exception):
+            rows.vlan(None)
+
+    def test_vlanport_edge_none(self):
+        """Edge Case: Test vlanport() with None input."""
+        with self.assertRaises(Exception):
+            rows.vlanport(None)
+
+    def test_zone_edge_none(self):
+        """Edge Case: Test zone() with None input."""
+        with self.assertRaises(Exception):
+            rows.zone(None)
+
+    def test_ip_edge_none(self):
+        """Edge Case: Test ip() with None input."""
+        with self.assertRaises(Exception):
+            rows.ip(None)
+
+    def test_ipport_edge_none(self):
+        """Edge Case: Test ipport() with None input."""
+        with self.assertRaises(Exception):
+            rows.ipport(None)
path: tests/switchmap_/server/db/misc/test_rows.py
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Use specific exception types instead of generic Exception.**

The static analysis tool correctly flagged that using `assertRaises(Exception)` is too broad. If you know which specific exception types should be raised, use those instead of the generic `Exception` class.

---

🏁 Script executed:

```shell
#!/bin/bash
# Description: Find what specific exceptions are raised by the rows functions when given None input

# Look for exception handling in the rows.py file
rg -A 2 -B 2 "def \w+\(" switchmap/server/db/misc/rows.py
```

Length of output: 800

---



</details>

**Use Specific Exception Types for None Input Edge Cases**

The test suite currently uses `assertRaises(Exception)` for all the rows functions when `None` is passed. However, the rows conversion functions in `switchmap/server/db/misc/rows.py` do not implement custom error handling for `None` input and rely on Python’s built-in errors (e.g., when attempting operations on `None`). Typically, passing `None` to these functions (which likely perform operations such as iterating or key access) will result in a `TypeError`.

**Action Required:**
- Replace each instance of `assertRaises(Exception)` with `assertRaises(TypeError)` in tests (e.g., for `device()`, `root()`, `event()`, etc.) to ensure the tests verify the expected, more specific exception. For example:
  ```diff
  -    with self.assertRaises(Exception):
  -        rows.device(None)
  +    with self.assertRaises(TypeError):
  +        rows.device(None)
  ```
- Apply similar changes to all tests in the block (lines 593–656 of `tests/switchmap_/server/db/misc/test_rows.py`).

This change narrows the exception check to what is actually raised, as flagged by the static analysis tool.



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
    def test_device_edge_none(self):
        """Edge Case: Test device() with None input."""
        with self.assertRaises(TypeError):
            rows.device(None)

    def test_root_edge_none(self):
        """Edge Case: Test root() with None input."""
        with self.assertRaises(TypeError):
            rows.root(None)

    def test_event_edge_none(self):
        """Edge Case: Test event() with None input."""
        with self.assertRaises(TypeError):
            rows.event(None)

    def test_l1interface_edge_none(self):
        """Edge Case: Test l1interface() with None input."""
        with self.assertRaises(TypeError):
            rows.l1interface(None)

    def test_mac_edge_none(self):
        """Edge Case: Test mac() with None input."""
        with self.assertRaises(TypeError):
            rows.mac(None)

    def test_macip_edge_none(self):
        """Edge Case: Test macip() with None input."""
        with self.assertRaises(TypeError):
            rows.macip(None)

    def test_macport_edge_none(self):
        """Edge Case: Test macport() with None input."""
        with self.assertRaises(TypeError):
            rows.macport(None)

    def test_oui_edge_none(self):
        """Edge Case: Test oui() with None input."""
        with self.assertRaises(TypeError):
            rows.oui(None)

    def test_vlan_edge_none(self):
        """Edge Case: Test vlan() with None input."""
        with self.assertRaises(TypeError):
            rows.vlan(None)

    def test_vlanport_edge_none(self):
        """Edge Case: Test vlanport() with None input."""
        with self.assertRaises(TypeError):
            rows.vlanport(None)

    def test_zone_edge_none(self):
        """Edge Case: Test zone() with None input."""
        with self.assertRaises(TypeError):
            rows.zone(None)

    def test_ip_edge_none(self):
        """Edge Case: Test ip() with None input."""
        with self.assertRaises(TypeError):
            rows.ip(None)

    def test_ipport_edge_none(self):
        """Edge Case: Test ipport() with None input."""
        with self.assertRaises(TypeError):
            rows.ipport(None)
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

595-595: `assertRaises(Exception)` should be considered evil

(B017)

---

600-600: `assertRaises(Exception)` should be considered evil

(B017)

---

605-605: `assertRaises(Exception)` should be considered evil

(B017)

---

610-610: `assertRaises(Exception)` should be considered evil

(B017)

---

615-615: `assertRaises(Exception)` should be considered evil

(B017)

---

620-620: `assertRaises(Exception)` should be considered evil

(B017)

---

625-625: `assertRaises(Exception)` should be considered evil

(B017)

---

630-630: `assertRaises(Exception)` should be considered evil

(B017)

---

635-635: `assertRaises(Exception)` should be considered evil

(B017)

---

640-640: `assertRaises(Exception)` should be considered evil

(B017)

---

645-645: `assertRaises(Exception)` should be considered evil

(B017)

---

650-650: `assertRaises(Exception)` should be considered evil

(B017)

---

655-655: `assertRaises(Exception)` should be considered evil

(B017)

</details>

</details>

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-04-11T06:15:11Z
author_association: NONE
diff_hunk: @@ -0,0 +1,660 @@
+#!/usr/bin/env python3
+"""Test the rows module.
+
+This module contains unit tests for the functions in the `rows.py` file.
+The tests ensure that the following functions correctly convert
+database rows to tuples:
+
+- device()
+- root()
+- event()
+- l1interface()
+- mac()
+- macip()
+- macport()
+- oui()
+- vlan()
+- vlanport()
+- zone()
+- ip()
+- ipport()
+
+Additionally, edge cases where the input is None are tested to ensure that
+each function properly raises exceptions when provided with invalid input.
+
+The tests verify the following:
+- Correct tuple conversion for each function.
+- Correct tuple length and data type.
+- Correct value in the tuple for each entry in the row
+- Handling of invalid (None) inputs with appropriate exceptions.
+"""
+
+import os
+import sys
+
+import unittest
+from switchmap.server.db.misc import rows
+from switchmap.server.db.models import (
+    Device,
+    Event,
+    Ip,
+    IpPort,
+    L1Interface,
+    Mac,
+    MacIp,
+    MacPort,
+    Oui,
+    Root,
+    Vlan,
+    VlanPort,
+    Zone,
+)
+from switchmap.server.db import SCOPED_SESSION
+from tests.testlib_ import setup
+from tests.testlib_ import db
+
+# Try to create a working PYTHONPATH
+EXEC_DIR = os.path.dirname(os.path.realpath(__file__))
+ROOT_DIR = os.path.abspath(
+    os.path.join(
+        os.path.abspath(
+            os.path.join(
+                os.path.abspath(
+                    os.path.join(
+                        os.path.abspath(
+                            os.path.join(
+                                os.path.abspath(
+                                    os.path.join(EXEC_DIR, os.pardir)
+                                ),
+                                os.pardir,
+                            )
+                        ),
+                        os.pardir,
+                    )
+                ),
+                os.pardir,
+            )
+        ),
+        os.pardir,
+    )
+)
+_EXPECTED = """\
+{0}switchmap-ng{0}tests{0}switchmap_{0}server{0}db{0}misc""".format(
+    os.sep
+)
+if EXEC_DIR.endswith(_EXPECTED) is True:
+    # We need to prepend the path in case the repo has been installed
+    # elsewhere on the system using PIP. This could corrupt expected results
+    sys.path.insert(0, ROOT_DIR)
+else:
+    print(
+        """This script is not installed in the
+        "{0}" directory. Please fix.""".format(
+            _EXPECTED
+        )
+    )
+    sys.exit(2)
+
+
+class TestRowsMethods(unittest.TestCase):
+    """Test suite for functions in rows.py."""
+
+    @classmethod
+    def setUpClass(cls):
+        """Set up test environment before running tests."""
+        # Load the configuration
+        cls.config = setup.config()
+        cls.config.save()
+        # Drop tables
+        cls.database = db.Database()
+        cls.database.drop()
+        cls.database.create()
+        # Populate the database
+        db.populate()
+        cls.session = SCOPED_SESSION()
+
+    @classmethod
+    def tearDownClass(cls):
+        """Clean up test environment after tests."""
+        if cls.session:
+            cls.session.close()
+        cls.database.drop()
+        cls.config.cleanup()
+
+    # ----------------------------------------------------------------------
+    # Standard conversion tests
+    # ----------------------------------------------------------------------
+
+    def test_device(self):
+        """Test the device() function for correct tuple conversion.
+
+        Verifies that the function returns a tuple of length 12
+        when given a Device row from the database.
+        """
+        # List of row attributes to match with the tuple values
+        row_attributes = [
+            "idx_device",
+            "idx_zone",
+            "sys_name",
+            "hostname",
+            "name",
+            "sys_description",
+            "sys_objectid",
+            "sys_uptime",
+            "last_polled",
+            "enabled",
+            "ts_modified",
+            "ts_created",
+        ]
+        row = self.session.query(Device).first()
+        result = rows.device(row)
+        self.assertIsInstance(result, tuple)
+        self.assertEqual(len(result), 12)
+        # Check each values in the tuple
+        for i, attr in enumerate(row_attributes):
+            row_value = getattr(row, attr)
+
+            # Decode byte string if necessary
+            if isinstance(row_value, bytes):
+                row_value = row_value.decode()
+
+            self.assertEqual(
+                row_value,
+                result[i],
+                f"Mismatch at index {i} for attribute '{attr}'",
+            )
+
+    def test_root(self):
+        """Test the root() function for correct tuple conversion.
+
+        Verifies that the function returns a tuple of length 6
+        when given a Root row from the database.
+        """
+        # List of row attributes to match with the tuple values
+        row_attributes = [
+            "idx_root",
+            "idx_event",
+            "name",
+            "enabled",
+            "ts_modified",
+            "ts_created",
+        ]
+        row = self.session.query(Root).first()
+        result = rows.root(row)
+        self.assertIsInstance(result, tuple)
+        self.assertEqual(len(result), 6)
+        # Check each values in the tuple
+        for i, attr in enumerate(row_attributes):
+            row_value = getattr(row, attr)
+
+            # Decode byte string if necessary
+            if isinstance(row_value, bytes):
+                row_value = row_value.decode()
+
+            self.assertEqual(
+                row_value,
+                result[i],
+                f"Mismatch at index {i} for attribute '{attr}'",
+            )
+
+    def test_event(self):
+        """Test the event() function for correct tuple conversion.
+
+        Verifies that the function returns a tuple of length 6
+        when given an Event row from the database.
+        """
+        # List of row attributes to match with the tuple values
+        row_attributes = [
+            "idx_event",
+            "name",
+            "epoch_utc",
+            "enabled",
+            "ts_modified",
+            "ts_created",
+        ]
+        row = self.session.query(Event).first()
+        result = rows.event(row)
+        self.assertIsInstance(result, tuple)
+        self.assertEqual(len(result), 6)
+        # Check each values in the tuple
+        for i, attr in enumerate(row_attributes):
+            row_value = getattr(row, attr)
+
+            # Decode byte string if necessary
+            if isinstance(row_value, bytes):
+                row_value = row_value.decode()
+
+            self.assertEqual(
+                row_value,
+                result[i],
+                f"Mismatch at index {i} for attribute '{attr}'",
+            )
+
+    def test_l1interface(self):
+        """Test the l1interface() function for correct tuple conversion.
+
+        Verifies that the function returns a tuple of length 25
+        when given a L1Interface row from the database.
+        """
+        # List of row attributes to match with the tuple values
+        row_attributes = [
+            "idx_l1interface",
+            "idx_device",
+            "ifindex",
+            "duplex",
+            "ethernet",
+            "nativevlan",
+            "trunk",
+            "ifspeed",
+            "iftype",
+            "ifalias",
+            "ifdescr",
+            "ifname",
+            "ifadminstatus",
+            "ifoperstatus",
+            "ts_idle",
+            "cdpcachedeviceid",
+            "cdpcachedeviceport",
+            "cdpcacheplatform",
+            "lldpremportdesc",
+            "lldpremsyscapenabled",
+            "lldpremsysdesc",
+            "lldpremsysname",
+            "enabled",
+            "ts_modified",
+            "ts_created",
+        ]
+        row = self.session.query(L1Interface).first()
+        result = rows.l1interface(row)
+        self.assertIsInstance(result, tuple)
+        self.assertEqual(len(result), 25)
+        # Check each values in the tuple
+        for i, attr in enumerate(row_attributes):
+            row_value = getattr(row, attr)
+
+            # Decode byte string if necessary
+            if isinstance(row_value, bytes):
+                row_value = row_value.decode()
+
+            self.assertEqual(
+                row_value,
+                result[i],
+                f"Mismatch at index {i} for attribute '{attr}'",
+            )
+
+    def test_mac(self):
+        """Test the mac() function for correct tuple conversion.
+
+        Verifies that the function returns a tuple of length 7
+        when given a Mac row from the database.
+        """
+        # List of row attributes to match with the tuple values
+        row_attributes = [
+            "idx_mac",
+            "idx_oui",
+            "idx_zone",
+            "mac",
+            "enabled",
+            "ts_modified",
+            "ts_created",
+        ]
+        row = self.session.query(Mac).first()
+        result = rows.mac(row)
+        self.assertIsInstance(result, tuple)
+        self.assertEqual(len(result), 7)
+
+        # Check each values in the tuple
+        for i, attr in enumerate(row_attributes):
+            row_value = getattr(row, attr)
+
+            # Decode byte string if necessary
+            if isinstance(row_value, bytes):
+                row_value = row_value.decode()
+
+            self.assertEqual(
+                row_value,
+                result[i],
+                f"Mismatch at index {i} for attribute '{attr}'",
+            )
+
+    def test_macip(self):
+        """Test the macip() function for correct tuple conversion.
+
+        Verifies that the function returns a tuple of length 6
+        when given a MacIp row from the database.
+        """
+        # List of row attributes to match with the tuple values
+        row_attributes = [
+            "idx_macip",
+            "idx_ip",
+            "idx_mac",
+            "enabled",
+            "ts_modified",
+            "ts_created",
+        ]
+        row = self.session.query(MacIp).first()
+        result = rows.macip(row)
+        self.assertIsInstance(result, tuple)
+        self.assertEqual(len(result), 6)
+        # Check each values in the tuple
+        for i, attr in enumerate(row_attributes):
+            row_value = getattr(row, attr)
+
+            # Decode byte string if necessary
+            if isinstance(row_value, bytes):
+                row_value = row_value.decode()
+
+            self.assertEqual(
+                row_value,
+                result[i],
+                f"Mismatch at index {i} for attribute '{attr}'",
+            )
+
+    def test_macport(self):
+        """Test the macport() function for correct tuple conversion.
+
+        Verifies that the function returns a tuple of length 6
+        when given a MacPort row from the database.
+        """
+        # List of row attributes to match with the tuple values
+        row_attributes = [
+            "idx_macport",
+            "idx_l1interface",
+            "idx_mac",
+            "enabled",
+            "ts_modified",
+            "ts_created",
+        ]
+        row = self.session.query(MacPort).first()
+        result = rows.macport(row)
+        self.assertIsInstance(result, tuple)
+        self.assertEqual(len(result), 6)
+        # Check each values in the tuple
+        for i, attr in enumerate(row_attributes):
+            row_value = getattr(row, attr)
+
+            # Decode byte string if necessary
+            if isinstance(row_value, bytes):
+                row_value = row_value.decode()
+
+            self.assertEqual(
+                row_value,
+                result[i],
+                f"Mismatch at index {i} for attribute '{attr}'",
+            )
+
+    def test_oui(self):
+        """Test the oui() function for correct tuple conversion.
+
+        Verifies that the function returns a tuple of length 6
+        when given a Oui row from the database.
+        """
+        # List of row attributes to match with the tuple values
+        row_attributes = [
+            "idx_oui",
+            "oui",
+            "organization",
+            "enabled",
+            "ts_modified",
+            "ts_created",
+        ]
+        row = self.session.query(Oui).first()
+        result = rows.oui(row)
+        self.assertIsInstance(result, tuple)
+        self.assertEqual(len(result), 6)
+        # Check each values in the tuple
+        for i, attr in enumerate(row_attributes):
+            row_value = getattr(row, attr)
+
+            # Decode byte string if necessary
+            if isinstance(row_value, bytes):
+                row_value = row_value.decode()
+
+            self.assertEqual(
+                row_value,
+                result[i],
+                f"Mismatch at index {i} for attribute '{attr}'",
+            )
+
+    def test_vlan(self):
+        """Test the vlan() function for correct tuple conversion.
+
+        Verifies that the function returns a tuple of length 8
+        when given a Vlan row from the database.
+        """
+        # List of row attributes to match with the tuple values
+        row_attributes = [
+            "idx_vlan",
+            "idx_device",
+            "vlan",
+            "name",
+            "state",
+            "enabled",
+            "ts_modified",
+            "ts_created",
+        ]
+        row = self.session.query(Vlan).first()
+        result = rows.vlan(row)
+        self.assertIsInstance(result, tuple)
+        self.assertEqual(len(result), 8)
+        # Check each values in the tuple
+        for i, attr in enumerate(row_attributes):
+            row_value = getattr(row, attr)
+
+            # Decode byte string if necessary
+            if isinstance(row_value, bytes):
+                row_value = row_value.decode()
+
+            self.assertEqual(
+                row_value,
+                result[i],
+                f"Mismatch at index {i} for attribute '{attr}'",
+            )
+
+    def test_vlanport(self):
+        """Test the vlanport() function for correct tuple conversion.
+
+        Verifies that the function returns a tuple of length 6
+        when given a VlanPort row from the database.
+        """
+        # List of row attributes to match with the tuple values
+        row_attributes = [
+            "idx_vlanport",
+            "idx_l1interface",
+            "idx_vlan",
+            "enabled",
+            "ts_modified",
+            "ts_created",
+        ]
+        row = self.session.query(VlanPort).first()
+        result = rows.vlanport(row)
+        self.assertIsInstance(result, tuple)
+        self.assertEqual(len(result), 6)
+        # Check each values in the tuple
+        for i, attr in enumerate(row_attributes):
+            row_value = getattr(row, attr)
+
+            # Decode byte string if necessary
+            if isinstance(row_value, bytes):
+                row_value = row_value.decode()
+
+            self.assertEqual(
+                row_value,
+                result[i],
+                f"Mismatch at index {i} for attribute '{attr}'",
+            )
+
+    def test_zone(self):
+        """Test the zone() function for correct tuple conversion.
+
+        Verifies that the function returns a tuple of length 7
+        when given a Zone row from the database.
+        """
+        # List of row attributes to match with the tuple values
+        row_attributes = [
+            "idx_zone",
+            "idx_event",
+            "name",
+            "notes",
+            "enabled",
+            "ts_modified",
+            "ts_created",
+        ]
+        row = self.session.query(Zone).first()
+        result = rows.zone(row)
+        self.assertIsInstance(result, tuple)
+        self.assertEqual(len(result), 7)
+        # Check each values in the tuple
+        for i, attr in enumerate(row_attributes):
+            row_value = getattr(row, attr)
+
+            # Decode byte string if necessary
+            if isinstance(row_value, bytes):
+                row_value = row_value.decode()
+
+            self.assertEqual(
+                row_value,
+                result[i],
+                f"Mismatch at index {i} for attribute '{attr}'",
+            )
+
+    def test_ip(self):
+        """Test the ip() function for correct tuple conversion.
+
+        Verifies that the function returns a tuple of length 8
+        when given a Ip row from the database.
+        """
+        # List of row attributes to match with the tuple values
+        row_attributes = [
+            "idx_ip",
+            "idx_zone",
+            "address",
+            "version",
+            "hostname",
+            "enabled",
+            "ts_modified",
+            "ts_created",
+        ]
+        row = self.session.query(Ip).first()
+        result = rows.ip(row)
+        self.assertIsInstance(result, tuple)
+        self.assertEqual(len(result), 8)
+        # Check each values in the tuple
+        for i, attr in enumerate(row_attributes):
+            row_value = getattr(row, attr)
+
+            # Decode byte string if necessary
+            if isinstance(row_value, bytes):
+                row_value = row_value.decode()
+
+            self.assertEqual(
+                row_value,
+                result[i],
+                f"Mismatch at index {i} for attribute '{attr}'",
+            )
+
+    def test_ipport(self):
+        """Test the ipport() function for correct tuple conversion.
+
+        Verifies that the function returns a tuple of length 6
+        when given a IpPort row from the database.
+        """
+        # List of row attributes to match with the tuple values
+        row_attributes = [
+            "idx_ipport",
+            "idx_l1interface",
+            "idx_ip",
+            "enabled",
+            "ts_modified",
+            "ts_created",
+        ]
+        row = self.session.query(IpPort).first()
+        result = rows.ipport(row)
+        self.assertIsInstance(result, tuple)
+        self.assertEqual(len(result), 6)
+        # Check each values in the tuple
+        for i, attr in enumerate(row_attributes):
+            row_value = getattr(row, attr)
+
+            # Decode byte string if necessary
+            if isinstance(row_value, bytes):
+                row_value = row_value.decode()
+
+            self.assertEqual(
+                row_value,
+                result[i],
+                f"Mismatch at index {i} for attribute '{attr}'",
+            )
+
+    # ----------------------------------------------------------------------
+    # --- Edge case: None input tests ---
+    # ----------------------------------------------------------------------
+
+    def test_device_edge_none(self):
+        """Edge Case: Test device() with None input."""
+        with self.assertRaises(Exception):
+            rows.device(None)
+
+    def test_root_edge_none(self):
+        """Edge Case: Test root() with None input."""
+        with self.assertRaises(Exception):
+            rows.root(None)
+
+    def test_event_edge_none(self):
+        """Edge Case: Test event() with None input."""
+        with self.assertRaises(Exception):
+            rows.event(None)
+
+    def test_l1interface_edge_none(self):
+        """Edge Case: Test l1interface() with None input."""
+        with self.assertRaises(Exception):
+            rows.l1interface(None)
+
+    def test_mac_edge_none(self):
+        """Edge Case: Test mac() with None input."""
+        with self.assertRaises(Exception):
+            rows.mac(None)
+
+    def test_macip_edge_none(self):
+        """Edge Case: Test macip() with None input."""
+        with self.assertRaises(Exception):
+            rows.macip(None)
+
+    def test_macport_edge_none(self):
+        """Edge Case: Test macport() with None input."""
+        with self.assertRaises(Exception):
+            rows.macport(None)
+
+    def test_oui_edge_none(self):
+        """Edge Case: Test oui() with None input."""
+        with self.assertRaises(Exception):
+            rows.oui(None)
+
+    def test_vlan_edge_none(self):
+        """Edge Case: Test vlan() with None input."""
+        with self.assertRaises(Exception):
+            rows.vlan(None)
+
+    def test_vlanport_edge_none(self):
+        """Edge Case: Test vlanport() with None input."""
+        with self.assertRaises(Exception):
+            rows.vlanport(None)
+
+    def test_zone_edge_none(self):
+        """Edge Case: Test zone() with None input."""
+        with self.assertRaises(Exception):
+            rows.zone(None)
+
+    def test_ip_edge_none(self):
+        """Edge Case: Test ip() with None input."""
+        with self.assertRaises(Exception):
+            rows.ip(None)
+
+    def test_ipport_edge_none(self):
+        """Edge Case: Test ipport() with None input."""
+        with self.assertRaises(Exception):
+            rows.ipport(None)
path: tests/switchmap_/server/db/misc/test_rows.py
user: login: Abhi-MS
body: 
assertRaises(Exception) is changed to assertRaises(AttributeError)
created_at: 2025-04-11T06:34:11Z
author_association: NONE
diff_hunk: @@ -0,0 +1,660 @@
+#!/usr/bin/env python3
+"""Test the rows module.
+
+This module contains unit tests for the functions in the `rows.py` file.
+The tests ensure that the following functions correctly convert
+database rows to tuples:
+
+- device()
+- root()
+- event()
+- l1interface()
+- mac()
+- macip()
+- macport()
+- oui()
+- vlan()
+- vlanport()
+- zone()
+- ip()
+- ipport()
+
+Additionally, edge cases where the input is None are tested to ensure that
+each function properly raises exceptions when provided with invalid input.
+
+The tests verify the following:
+- Correct tuple conversion for each function.
+- Correct tuple length and data type.
+- Correct value in the tuple for each entry in the row
+- Handling of invalid (None) inputs with appropriate exceptions.
+"""
+
+import os
+import sys
+
+import unittest
+from switchmap.server.db.misc import rows
+from switchmap.server.db.models import (
+    Device,
+    Event,
+    Ip,
+    IpPort,
+    L1Interface,
+    Mac,
+    MacIp,
+    MacPort,
+    Oui,
+    Root,
+    Vlan,
+    VlanPort,
+    Zone,
+)
+from switchmap.server.db import SCOPED_SESSION
+from tests.testlib_ import setup
+from tests.testlib_ import db
+
+# Try to create a working PYTHONPATH
+EXEC_DIR = os.path.dirname(os.path.realpath(__file__))
+ROOT_DIR = os.path.abspath(
+    os.path.join(
+        os.path.abspath(
+            os.path.join(
+                os.path.abspath(
+                    os.path.join(
+                        os.path.abspath(
+                            os.path.join(
+                                os.path.abspath(
+                                    os.path.join(EXEC_DIR, os.pardir)
+                                ),
+                                os.pardir,
+                            )
+                        ),
+                        os.pardir,
+                    )
+                ),
+                os.pardir,
+            )
+        ),
+        os.pardir,
+    )
+)
+_EXPECTED = """\
+{0}switchmap-ng{0}tests{0}switchmap_{0}server{0}db{0}misc""".format(
+    os.sep
+)
+if EXEC_DIR.endswith(_EXPECTED) is True:
+    # We need to prepend the path in case the repo has been installed
+    # elsewhere on the system using PIP. This could corrupt expected results
+    sys.path.insert(0, ROOT_DIR)
+else:
+    print(
+        """This script is not installed in the
+        "{0}" directory. Please fix.""".format(
+            _EXPECTED
+        )
+    )
+    sys.exit(2)
+
+
+class TestRowsMethods(unittest.TestCase):
+    """Test suite for functions in rows.py."""
+
+    @classmethod
+    def setUpClass(cls):
+        """Set up test environment before running tests."""
+        # Load the configuration
+        cls.config = setup.config()
+        cls.config.save()
+        # Drop tables
+        cls.database = db.Database()
+        cls.database.drop()
+        cls.database.create()
+        # Populate the database
+        db.populate()
+        cls.session = SCOPED_SESSION()
+
+    @classmethod
+    def tearDownClass(cls):
+        """Clean up test environment after tests."""
+        if cls.session:
+            cls.session.close()
+        cls.database.drop()
+        cls.config.cleanup()
+
+    # ----------------------------------------------------------------------
+    # Standard conversion tests
+    # ----------------------------------------------------------------------
+
+    def test_device(self):
+        """Test the device() function for correct tuple conversion.
+
+        Verifies that the function returns a tuple of length 12
+        when given a Device row from the database.
+        """
+        # List of row attributes to match with the tuple values
+        row_attributes = [
+            "idx_device",
+            "idx_zone",
+            "sys_name",
+            "hostname",
+            "name",
+            "sys_description",
+            "sys_objectid",
+            "sys_uptime",
+            "last_polled",
+            "enabled",
+            "ts_modified",
+            "ts_created",
+        ]
+        row = self.session.query(Device).first()
+        result = rows.device(row)
+        self.assertIsInstance(result, tuple)
+        self.assertEqual(len(result), 12)
+        # Check each values in the tuple
+        for i, attr in enumerate(row_attributes):
+            row_value = getattr(row, attr)
+
+            # Decode byte string if necessary
+            if isinstance(row_value, bytes):
+                row_value = row_value.decode()
+
+            self.assertEqual(
+                row_value,
+                result[i],
+                f"Mismatch at index {i} for attribute '{attr}'",
+            )
+
+    def test_root(self):
+        """Test the root() function for correct tuple conversion.
+
+        Verifies that the function returns a tuple of length 6
+        when given a Root row from the database.
+        """
+        # List of row attributes to match with the tuple values
+        row_attributes = [
+            "idx_root",
+            "idx_event",
+            "name",
+            "enabled",
+            "ts_modified",
+            "ts_created",
+        ]
+        row = self.session.query(Root).first()
+        result = rows.root(row)
+        self.assertIsInstance(result, tuple)
+        self.assertEqual(len(result), 6)
+        # Check each values in the tuple
+        for i, attr in enumerate(row_attributes):
+            row_value = getattr(row, attr)
+
+            # Decode byte string if necessary
+            if isinstance(row_value, bytes):
+                row_value = row_value.decode()
+
+            self.assertEqual(
+                row_value,
+                result[i],
+                f"Mismatch at index {i} for attribute '{attr}'",
+            )
+
+    def test_event(self):
+        """Test the event() function for correct tuple conversion.
+
+        Verifies that the function returns a tuple of length 6
+        when given an Event row from the database.
+        """
+        # List of row attributes to match with the tuple values
+        row_attributes = [
+            "idx_event",
+            "name",
+            "epoch_utc",
+            "enabled",
+            "ts_modified",
+            "ts_created",
+        ]
+        row = self.session.query(Event).first()
+        result = rows.event(row)
+        self.assertIsInstance(result, tuple)
+        self.assertEqual(len(result), 6)
+        # Check each values in the tuple
+        for i, attr in enumerate(row_attributes):
+            row_value = getattr(row, attr)
+
+            # Decode byte string if necessary
+            if isinstance(row_value, bytes):
+                row_value = row_value.decode()
+
+            self.assertEqual(
+                row_value,
+                result[i],
+                f"Mismatch at index {i} for attribute '{attr}'",
+            )
+
+    def test_l1interface(self):
+        """Test the l1interface() function for correct tuple conversion.
+
+        Verifies that the function returns a tuple of length 25
+        when given a L1Interface row from the database.
+        """
+        # List of row attributes to match with the tuple values
+        row_attributes = [
+            "idx_l1interface",
+            "idx_device",
+            "ifindex",
+            "duplex",
+            "ethernet",
+            "nativevlan",
+            "trunk",
+            "ifspeed",
+            "iftype",
+            "ifalias",
+            "ifdescr",
+            "ifname",
+            "ifadminstatus",
+            "ifoperstatus",
+            "ts_idle",
+            "cdpcachedeviceid",
+            "cdpcachedeviceport",
+            "cdpcacheplatform",
+            "lldpremportdesc",
+            "lldpremsyscapenabled",
+            "lldpremsysdesc",
+            "lldpremsysname",
+            "enabled",
+            "ts_modified",
+            "ts_created",
+        ]
+        row = self.session.query(L1Interface).first()
+        result = rows.l1interface(row)
+        self.assertIsInstance(result, tuple)
+        self.assertEqual(len(result), 25)
+        # Check each values in the tuple
+        for i, attr in enumerate(row_attributes):
+            row_value = getattr(row, attr)
+
+            # Decode byte string if necessary
+            if isinstance(row_value, bytes):
+                row_value = row_value.decode()
+
+            self.assertEqual(
+                row_value,
+                result[i],
+                f"Mismatch at index {i} for attribute '{attr}'",
+            )
+
+    def test_mac(self):
+        """Test the mac() function for correct tuple conversion.
+
+        Verifies that the function returns a tuple of length 7
+        when given a Mac row from the database.
+        """
+        # List of row attributes to match with the tuple values
+        row_attributes = [
+            "idx_mac",
+            "idx_oui",
+            "idx_zone",
+            "mac",
+            "enabled",
+            "ts_modified",
+            "ts_created",
+        ]
+        row = self.session.query(Mac).first()
+        result = rows.mac(row)
+        self.assertIsInstance(result, tuple)
+        self.assertEqual(len(result), 7)
+
+        # Check each values in the tuple
+        for i, attr in enumerate(row_attributes):
+            row_value = getattr(row, attr)
+
+            # Decode byte string if necessary
+            if isinstance(row_value, bytes):
+                row_value = row_value.decode()
+
+            self.assertEqual(
+                row_value,
+                result[i],
+                f"Mismatch at index {i} for attribute '{attr}'",
+            )
+
+    def test_macip(self):
+        """Test the macip() function for correct tuple conversion.
+
+        Verifies that the function returns a tuple of length 6
+        when given a MacIp row from the database.
+        """
+        # List of row attributes to match with the tuple values
+        row_attributes = [
+            "idx_macip",
+            "idx_ip",
+            "idx_mac",
+            "enabled",
+            "ts_modified",
+            "ts_created",
+        ]
+        row = self.session.query(MacIp).first()
+        result = rows.macip(row)
+        self.assertIsInstance(result, tuple)
+        self.assertEqual(len(result), 6)
+        # Check each values in the tuple
+        for i, attr in enumerate(row_attributes):
+            row_value = getattr(row, attr)
+
+            # Decode byte string if necessary
+            if isinstance(row_value, bytes):
+                row_value = row_value.decode()
+
+            self.assertEqual(
+                row_value,
+                result[i],
+                f"Mismatch at index {i} for attribute '{attr}'",
+            )
+
+    def test_macport(self):
+        """Test the macport() function for correct tuple conversion.
+
+        Verifies that the function returns a tuple of length 6
+        when given a MacPort row from the database.
+        """
+        # List of row attributes to match with the tuple values
+        row_attributes = [
+            "idx_macport",
+            "idx_l1interface",
+            "idx_mac",
+            "enabled",
+            "ts_modified",
+            "ts_created",
+        ]
+        row = self.session.query(MacPort).first()
+        result = rows.macport(row)
+        self.assertIsInstance(result, tuple)
+        self.assertEqual(len(result), 6)
+        # Check each values in the tuple
+        for i, attr in enumerate(row_attributes):
+            row_value = getattr(row, attr)
+
+            # Decode byte string if necessary
+            if isinstance(row_value, bytes):
+                row_value = row_value.decode()
+
+            self.assertEqual(
+                row_value,
+                result[i],
+                f"Mismatch at index {i} for attribute '{attr}'",
+            )
+
+    def test_oui(self):
+        """Test the oui() function for correct tuple conversion.
+
+        Verifies that the function returns a tuple of length 6
+        when given a Oui row from the database.
+        """
+        # List of row attributes to match with the tuple values
+        row_attributes = [
+            "idx_oui",
+            "oui",
+            "organization",
+            "enabled",
+            "ts_modified",
+            "ts_created",
+        ]
+        row = self.session.query(Oui).first()
+        result = rows.oui(row)
+        self.assertIsInstance(result, tuple)
+        self.assertEqual(len(result), 6)
+        # Check each values in the tuple
+        for i, attr in enumerate(row_attributes):
+            row_value = getattr(row, attr)
+
+            # Decode byte string if necessary
+            if isinstance(row_value, bytes):
+                row_value = row_value.decode()
+
+            self.assertEqual(
+                row_value,
+                result[i],
+                f"Mismatch at index {i} for attribute '{attr}'",
+            )
+
+    def test_vlan(self):
+        """Test the vlan() function for correct tuple conversion.
+
+        Verifies that the function returns a tuple of length 8
+        when given a Vlan row from the database.
+        """
+        # List of row attributes to match with the tuple values
+        row_attributes = [
+            "idx_vlan",
+            "idx_device",
+            "vlan",
+            "name",
+            "state",
+            "enabled",
+            "ts_modified",
+            "ts_created",
+        ]
+        row = self.session.query(Vlan).first()
+        result = rows.vlan(row)
+        self.assertIsInstance(result, tuple)
+        self.assertEqual(len(result), 8)
+        # Check each values in the tuple
+        for i, attr in enumerate(row_attributes):
+            row_value = getattr(row, attr)
+
+            # Decode byte string if necessary
+            if isinstance(row_value, bytes):
+                row_value = row_value.decode()
+
+            self.assertEqual(
+                row_value,
+                result[i],
+                f"Mismatch at index {i} for attribute '{attr}'",
+            )
+
+    def test_vlanport(self):
+        """Test the vlanport() function for correct tuple conversion.
+
+        Verifies that the function returns a tuple of length 6
+        when given a VlanPort row from the database.
+        """
+        # List of row attributes to match with the tuple values
+        row_attributes = [
+            "idx_vlanport",
+            "idx_l1interface",
+            "idx_vlan",
+            "enabled",
+            "ts_modified",
+            "ts_created",
+        ]
+        row = self.session.query(VlanPort).first()
+        result = rows.vlanport(row)
+        self.assertIsInstance(result, tuple)
+        self.assertEqual(len(result), 6)
+        # Check each values in the tuple
+        for i, attr in enumerate(row_attributes):
+            row_value = getattr(row, attr)
+
+            # Decode byte string if necessary
+            if isinstance(row_value, bytes):
+                row_value = row_value.decode()
+
+            self.assertEqual(
+                row_value,
+                result[i],
+                f"Mismatch at index {i} for attribute '{attr}'",
+            )
+
+    def test_zone(self):
+        """Test the zone() function for correct tuple conversion.
+
+        Verifies that the function returns a tuple of length 7
+        when given a Zone row from the database.
+        """
+        # List of row attributes to match with the tuple values
+        row_attributes = [
+            "idx_zone",
+            "idx_event",
+            "name",
+            "notes",
+            "enabled",
+            "ts_modified",
+            "ts_created",
+        ]
+        row = self.session.query(Zone).first()
+        result = rows.zone(row)
+        self.assertIsInstance(result, tuple)
+        self.assertEqual(len(result), 7)
+        # Check each values in the tuple
+        for i, attr in enumerate(row_attributes):
+            row_value = getattr(row, attr)
+
+            # Decode byte string if necessary
+            if isinstance(row_value, bytes):
+                row_value = row_value.decode()
+
+            self.assertEqual(
+                row_value,
+                result[i],
+                f"Mismatch at index {i} for attribute '{attr}'",
+            )
+
+    def test_ip(self):
+        """Test the ip() function for correct tuple conversion.
+
+        Verifies that the function returns a tuple of length 8
+        when given a Ip row from the database.
+        """
+        # List of row attributes to match with the tuple values
+        row_attributes = [
+            "idx_ip",
+            "idx_zone",
+            "address",
+            "version",
+            "hostname",
+            "enabled",
+            "ts_modified",
+            "ts_created",
+        ]
+        row = self.session.query(Ip).first()
+        result = rows.ip(row)
+        self.assertIsInstance(result, tuple)
+        self.assertEqual(len(result), 8)
+        # Check each values in the tuple
+        for i, attr in enumerate(row_attributes):
+            row_value = getattr(row, attr)
+
+            # Decode byte string if necessary
+            if isinstance(row_value, bytes):
+                row_value = row_value.decode()
+
+            self.assertEqual(
+                row_value,
+                result[i],
+                f"Mismatch at index {i} for attribute '{attr}'",
+            )
+
+    def test_ipport(self):
+        """Test the ipport() function for correct tuple conversion.
+
+        Verifies that the function returns a tuple of length 6
+        when given a IpPort row from the database.
+        """
+        # List of row attributes to match with the tuple values
+        row_attributes = [
+            "idx_ipport",
+            "idx_l1interface",
+            "idx_ip",
+            "enabled",
+            "ts_modified",
+            "ts_created",
+        ]
+        row = self.session.query(IpPort).first()
+        result = rows.ipport(row)
+        self.assertIsInstance(result, tuple)
+        self.assertEqual(len(result), 6)
+        # Check each values in the tuple
+        for i, attr in enumerate(row_attributes):
+            row_value = getattr(row, attr)
+
+            # Decode byte string if necessary
+            if isinstance(row_value, bytes):
+                row_value = row_value.decode()
+
+            self.assertEqual(
+                row_value,
+                result[i],
+                f"Mismatch at index {i} for attribute '{attr}'",
+            )
+
+    # ----------------------------------------------------------------------
+    # --- Edge case: None input tests ---
+    # ----------------------------------------------------------------------
+
+    def test_device_edge_none(self):
+        """Edge Case: Test device() with None input."""
+        with self.assertRaises(Exception):
+            rows.device(None)
+
+    def test_root_edge_none(self):
+        """Edge Case: Test root() with None input."""
+        with self.assertRaises(Exception):
+            rows.root(None)
+
+    def test_event_edge_none(self):
+        """Edge Case: Test event() with None input."""
+        with self.assertRaises(Exception):
+            rows.event(None)
+
+    def test_l1interface_edge_none(self):
+        """Edge Case: Test l1interface() with None input."""
+        with self.assertRaises(Exception):
+            rows.l1interface(None)
+
+    def test_mac_edge_none(self):
+        """Edge Case: Test mac() with None input."""
+        with self.assertRaises(Exception):
+            rows.mac(None)
+
+    def test_macip_edge_none(self):
+        """Edge Case: Test macip() with None input."""
+        with self.assertRaises(Exception):
+            rows.macip(None)
+
+    def test_macport_edge_none(self):
+        """Edge Case: Test macport() with None input."""
+        with self.assertRaises(Exception):
+            rows.macport(None)
+
+    def test_oui_edge_none(self):
+        """Edge Case: Test oui() with None input."""
+        with self.assertRaises(Exception):
+            rows.oui(None)
+
+    def test_vlan_edge_none(self):
+        """Edge Case: Test vlan() with None input."""
+        with self.assertRaises(Exception):
+            rows.vlan(None)
+
+    def test_vlanport_edge_none(self):
+        """Edge Case: Test vlanport() with None input."""
+        with self.assertRaises(Exception):
+            rows.vlanport(None)
+
+    def test_zone_edge_none(self):
+        """Edge Case: Test zone() with None input."""
+        with self.assertRaises(Exception):
+            rows.zone(None)
+
+    def test_ip_edge_none(self):
+        """Edge Case: Test ip() with None input."""
+        with self.assertRaises(Exception):
+            rows.ip(None)
+
+    def test_ipport_edge_none(self):
+        """Edge Case: Test ipport() with None input."""
+        with self.assertRaises(Exception):
+            rows.ipport(None)
path: tests/switchmap_/server/db/misc/test_rows.py
user: login: coderabbitai[bot]
body: <details>
<summary>🧩 Analysis chain</summary>

@Abhi-MS I'll check the implementation to verify if `AttributeError` is indeed the correct exception type for these test cases.

---

🏁 Script executed:
```
#!/bin/bash
# Look at the rows.py file to understand what exception would be raised when passing None
cat switchmap/server/db/misc/rows.py
```

Length of output: 8986

---
</details>

@Abhi-MS Your change from `assertRaises(Exception)` to `assertRaises(AttributeError)` is correct. After examining the rows.py file, I can confirm that when None is passed to any of these functions, an AttributeError would be raised because the functions immediately try to access attributes of the row parameter (e.g., `row.idx_root`).

When None is passed, operations like `None.idx_root` would raise an AttributeError rather than TypeError. This makes your change more precise than the previous suggestion and ensures the tests correctly verify the exact exception that would occur in practice.

---
<details>
<summary>🧬 Code Graph Analysis Results</summary>

### Relevant Code Snippets

#### From `tests/testlib_/setup.py`
- **Line 87-106**: `save(self)`
    ```python
    def save(self):
        """Save a good config and set the SWITCHMAP_CONFIGDIR variable.

        Args:
            None

        Returns:
            config_directory: Directory where the config is placed

        """
        # Initialize key variables
        config_file = "{}/config.yaml".format(self.metadata.config_directory)

        # Write good_config to file
        with open(config_file, "w") as f_handle:
            yaml.dump(self.metadata.config, f_handle, default_flow_style=False)

        # Return
        return self.metadata.config_directory
    ```

#### From `switchmap/server/db/misc/rows.py`
- **Line 58-77**: `root(row)`
    ```python
    def root(row):
        """Convert table row to tuple.

        Args:
            row: Root row

        Returns:
            result: RRoot tuple

        """
        # Initialize key variables
        result = RRoot(
            idx_root=row.idx_root,
            idx_event=row.idx_event,
            name=row.name.decode(),
            enabled=int(bool(row.enabled) is True),
            ts_created=row.ts_created,
            ts_modified=row.ts_modified,
        )
        return result
    ```

- **Line 80-99**: `event(row)`
    ```python
    def event(row):
        """Convert table row to tuple.

        Args:
            row: Event row

        Returns:
            result: REvent tuple

        """
        # Initialize key variables
        result = REvent(
            idx_event=row.idx_event,
            name=row.name.decode(),
            epoch_utc=row.epoch_utc,
            enabled=int(bool(row.enabled) is True),
            ts_created=row.ts_created,
            ts_modified=row.ts_modified,
        )
        return result
    ```

- **Line 234-257**: `oui(row)`
    ```python
    def oui(row):
        """Convert table row to tuple.

        Args:
            row: Oui row

        Returns:
            result: ROui tuple

        """
        # Initialize key variables
        result = ROui(
            idx_oui=row.idx_oui,
            oui=(None if bool(row.oui) is False else row.oui.decode()),
            organization=(
                None
                if bool(row.organization) is False
                else row.organization.decode()
            ),
            enabled=int(bool(row.enabled) is True),
            ts_created=row.ts_created,
            ts_modified=row.ts_modified,
        )
        return result
    ```

#### From `tests/testlib_/db.py`
- **Line 49-98**: `class Database`
    ```python
    class Database:
        """Database class."""

        def __init__(self):
            """Initialize the class.

            Args:
                None

            Returns:
                None

            """
            # Make sure we are doing operations only on a test database
            expected = "switchmap_unittest"
            config = ConfigServer()
            if config.db_name() != expected:
                log_message = """\
    The database under test must be named {}""".format(
                    expected
                )
                log.log2die(1174, log_message)

        def drop(self):
            """Drop database.

            Args:
                None

            Returns:
                None

            """
            # Drop all the tables
            with ENGINE.connect() as connection:
                with Session(bind=connection) as session:
                    models.BASE.metadata.drop_all(session.get_bind())

        def create(self):
            """Create database.

            Args:
                None

            Returns:
                None

            """
            # Initialize key values
            models.create_all_tables()
    ```

#### From `tests/testlib_/db.py`
- **Line 101-381**: `populate()`
    ```python
    def populate():
        """Create prerequisite rows.

        Args:
            None

        Returns:
            None

        """
        # Initialize key variables
        macresult = {}
        vlanresult = {}
        Result = namedtuple(
            "Result",
            "idx_mac idx_l1interface interfaces devices macs ips idx_event ipports",
        )

        # Code for populating the database with test data...
        # (omitted for brevity)
        
        # Return
        result = Result(
            idx_mac=macresult,
            idx_l1interface=vlanresult,
            interfaces=r_interfaces,
            devices=r_devices,
            macs=r_macs,
            ips=r_ips,
            idx_event=r_zone.idx_event,
            ipports=r_ipports,
        )
        return result
    ```
</details>
---
<details>
<summary>✏️ Learnings added</summary>

```
Learnt from: Abhi-MS
PR: PalisadoesFoundation/switchmap-ng#319
File: tests/switchmap_/server/db/misc/test_rows.py:593-656
Timestamp: 2025-04-11T06:35:50.675Z
Learning: In the switchmap-ng project, when passing None to the row conversion functions in rows.py, an AttributeError is raised because these functions attempt to access attributes of None (e.g., row.idx_root). Tests for these functions should use assertRaises(AttributeError) rather than a generic Exception or TypeError.
```

> Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).

</details>



<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer><details>
<summary>🧩 Analysis chain</summary>

@Abhi-MS I'll check the implementation to verify if `AttributeError` is indeed the correct exception type for these test cases.

---

🏁 Script executed:
```
#!/bin/bash
# Look at the rows.py file to understand what exception would be raised when passing None
cat switchmap/server/db/misc/rows.py
```

Length of output: 8986

---



</details>

@Abhi-MS Your change from `assertRaises(Exception)` to `assertRaises(AttributeError)` is correct. After examining the rows.py file, I can confirm that when None is passed to any of these functions, an AttributeError would be raised because the functions immediately try to access attributes of the row parameter (e.g., `row.idx_root`).

When None is passed, operations like `None.idx_root` would raise an AttributeError rather than TypeError. This makes your change more precise than the previous suggestion and ensures the tests correctly verify the exact exception that would occur in practice.

---

<details>
<summary>🧬 Code Graph Analysis Results</summary>

### Relevant Code Snippets

#### From `tests/testlib_/setup.py`
- **Line 87-106**: `save(self)`
    ```python
    def save(self):
        """Save a good config and set the SWITCHMAP_CONFIGDIR variable.

        Args:
            None

        Returns:
            config_directory: Directory where the config is placed

        """
        # Initialize key variables
        config_file = "{}/config.yaml".format(self.metadata.config_directory)

        # Write good_config to file
        with open(config_file, "w") as f_handle:
            yaml.dump(self.metadata.config, f_handle, default_flow_style=False)

        # Return
        return self.metadata.config_directory
    ```

#### From `switchmap/server/db/misc/rows.py`
- **Line 58-77**: `root(row)`
    ```python
    def root(row):
        """Convert table row to tuple.

        Args:
            row: Root row

        Returns:
            result: RRoot tuple

        """
        # Initialize key variables
        result = RRoot(
            idx_root=row.idx_root,
            idx_event=row.idx_event,
            name=row.name.decode(),
            enabled=int(bool(row.enabled) is True),
            ts_created=row.ts_created,
            ts_modified=row.ts_modified,
        )
        return result
    ```

- **Line 80-99**: `event(row)`
    ```python
    def event(row):
        """Convert table row to tuple.

        Args:
            row: Event row

        Returns:
            result: REvent tuple

        """
        # Initialize key variables
        result = REvent(
            idx_event=row.idx_event,
            name=row.name.decode(),
            epoch_utc=row.epoch_utc,
            enabled=int(bool(row.enabled) is True),
            ts_created=row.ts_created,
            ts_modified=row.ts_modified,
        )
        return result
    ```

- **Line 234-257**: `oui(row)`
    ```python
    def oui(row):
        """Convert table row to tuple.

        Args:
            row: Oui row

        Returns:
            result: ROui tuple

        """
        # Initialize key variables
        result = ROui(
            idx_oui=row.idx_oui,
            oui=(None if bool(row.oui) is False else row.oui.decode()),
            organization=(
                None
                if bool(row.organization) is False
                else row.organization.decode()
            ),
            enabled=int(bool(row.enabled) is True),
            ts_created=row.ts_created,
            ts_modified=row.ts_modified,
        )
        return result
    ```

#### From `tests/testlib_/db.py`
- **Line 49-98**: `class Database`
    ```python
    class Database:
        """Database class."""

        def __init__(self):
            """Initialize the class.

            Args:
                None

            Returns:
                None

            """
            # Make sure we are doing operations only on a test database
            expected = "switchmap_unittest"
            config = ConfigServer()
            if config.db_name() != expected:
                log_message = """\
    The database under test must be named {}""".format(
                    expected
                )
                log.log2die(1174, log_message)

        def drop(self):
            """Drop database.

            Args:
                None

            Returns:
                None

            """
            # Drop all the tables
            with ENGINE.connect() as connection:
                with Session(bind=connection) as session:
                    models.BASE.metadata.drop_all(session.get_bind())

        def create(self):
            """Create database.

            Args:
                None

            Returns:
                None

            """
            # Initialize key values
            models.create_all_tables()
    ```

#### From `tests/testlib_/db.py`
- **Line 101-381**: `populate()`
    ```python
    def populate():
        """Create prerequisite rows.

        Args:
            None

        Returns:
            None

        """
        # Initialize key variables
        macresult = {}
        vlanresult = {}
        Result = namedtuple(
            "Result",
            "idx_mac idx_l1interface interfaces devices macs ips idx_event ipports",
        )

        # Code for populating the database with test data...
        # (omitted for brevity)
        
        # Return
        result = Result(
            idx_mac=macresult,
            idx_l1interface=vlanresult,
            interfaces=r_interfaces,
            devices=r_devices,
            macs=r_macs,
            ips=r_ips,
            idx_event=r_zone.idx_event,
            ipports=r_ipports,
        )
        return result
    ```

</details>


---

<details>
<summary>✏️ Learnings added</summary>

```
Learnt from: Abhi-MS
PR: PalisadoesFoundation/switchmap-ng#319
File: tests/switchmap_/server/db/misc/test_rows.py:593-656
Timestamp: 2025-04-11T06:35:50.675Z
Learning: In the switchmap-ng project, when passing None to the row conversion functions in rows.py, an AttributeError is raised because these functions attempt to access attributes of None (e.g., row.idx_root). Tests for these functions should use assertRaises(AttributeError) rather than a generic Exception or TypeError.
```

> Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).

</details></answer></rawResChunk> -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-04-11T06:35:51Z
author_association: NONE
diff_hunk: @@ -0,0 +1,677 @@
+#!/usr/bin/env python3
+"""Test the rows module.
+
+This module contains unit tests for the functions in the `rows.py` file.
+The tests ensure that the following functions correctly convert
+database rows to tuples:
+
+- device()
+- root()
+- event()
+- l1interface()
+- mac()
+- macip()
+- macport()
+- oui()
+- vlan()
+- vlanport()
+- zone()
+- ip()
+- ipport()
+
+Additionally, edge cases where the input is None are tested to ensure that
+each function properly raises AttributeErrors when provided with invalid input.
+
+The tests verify the following:
+- Correct tuple conversion for each function.
+- Correct tuple length and data type.
+- Correct value in the tuple for each entry in the row
+- Handling of invalid (None) inputs with appropriate AttributeErrors.
+"""
+
+import os
+import sys
+
+import unittest
+from switchmap.server.db.misc import rows
+from switchmap.server.db.models import (
+    Device,
+    Event,
+    Ip,
+    IpPort,
+    L1Interface,
+    Mac,
+    MacIp,
+    MacPort,
+    Oui,
+    Root,
+    Vlan,
+    VlanPort,
+    Zone,
+)
+from switchmap.server.db import SCOPED_SESSION
+from tests.testlib_ import setup
+from tests.testlib_ import db
+
+# Try to create a working PYTHONPATH
+EXEC_DIR = os.path.dirname(os.path.realpath(__file__))
+ROOT_DIR = os.path.abspath(
+    os.path.join(
+        os.path.abspath(
+            os.path.join(
+                os.path.abspath(
+                    os.path.join(
+                        os.path.abspath(
+                            os.path.join(
+                                os.path.abspath(
+                                    os.path.join(EXEC_DIR, os.pardir)
+                                ),
+                                os.pardir,
+                            )
+                        ),
+                        os.pardir,
+                    )
+                ),
+                os.pardir,
+            )
+        ),
+        os.pardir,
+    )
+)
+_EXPECTED = """\
+{0}switchmap-ng{0}tests{0}switchmap_{0}server{0}db{0}misc""".format(
+    os.sep
+)
+if EXEC_DIR.endswith(_EXPECTED) is True:
+    # We need to prepend the path in case the repo has been installed
+    # elsewhere on the system using PIP. This could corrupt expected results
+    sys.path.insert(0, ROOT_DIR)
+else:
+    print(
+        """This script is not installed in the
+        "{0}" directory. Please fix.""".format(
+            _EXPECTED
+        )
+    )
+    sys.exit(2)
+
+
+class TestRowsMethods(unittest.TestCase):
+    """Test suite for functions in rows.py."""
+
+    @classmethod
+    def setUpClass(cls):
+        """Set up test environment before running tests."""
+        # Load the configuration
+        cls.config = setup.config()
+        cls.config.save()
+        # Drop tables
+        cls.database = db.Database()
+        cls.database.drop()
+        cls.database.create()
+        # Populate the database
+        db.populate()
+        cls.session = SCOPED_SESSION()
+
+    @classmethod
+    def tearDownClass(cls):
+        """Clean up test environment after tests."""
+        if cls.session:
+            cls.session.close()
+        cls.database.drop()
+        cls.config.cleanup()
+
+    # ----------------------------------------------------------------------
+    # Standard conversion tests
+    # ----------------------------------------------------------------------
+
+    def test_device(self):
+        """Test the device() function for correct tuple conversion.
+
+        Verifies that the function returns a tuple of length 12
+        when given a Device row from the database.
+        """
+        # List of row attributes to match with the tuple values
+        row_attributes = [
+            "idx_device",
+            "idx_zone",
+            "sys_name",
+            "hostname",
+            "name",
+            "sys_description",
+            "sys_objectid",
+            "sys_uptime",
+            "last_polled",
+            "enabled",
+            "ts_modified",
+            "ts_created",
+        ]
+        row = self.session.query(Device).first()
+        self.assertIsNotNone(row, "No Device row found in the test database.")
+        result = rows.device(row)
+        self.assertIsInstance(result, tuple)
+        self.assertEqual(len(result), 12)
+        # Check each value in the tuple
+        for i, attr in enumerate(row_attributes):
+            row_value = getattr(row, attr)
+
+            # Decode byte string if necessary
+            if isinstance(row_value, bytes):
+                row_value = row_value.decode()
+
+            self.assertEqual(
+                row_value,
+                result[i],
+                f"Mismatch at index {i} for attribute '{attr}'",
+            )
+
+    def test_root(self):
+        """Test the root() function for correct tuple conversion.
+
+        Verifies that the function returns a tuple of length 6
+        when given a Root row from the database.
+        """
+        # List of row attributes to match with the tuple values
+        row_attributes = [
+            "idx_root",
+            "idx_event",
+            "name",
+            "enabled",
+            "ts_modified",
+            "ts_created",
+        ]
+        row = self.session.query(Root).first()
+        self.assertIsNotNone(row, "No Root row found in the test database.")
+        result = rows.root(row)
+        self.assertIsInstance(result, tuple)
+        self.assertEqual(len(result), 6)
+        # Check each value in the tuple
+        for i, attr in enumerate(row_attributes):
+            row_value = getattr(row, attr)
+
+            # Decode byte string if necessary
+            if isinstance(row_value, bytes):
+                row_value = row_value.decode()
+
+            self.assertEqual(
+                row_value,
+                result[i],
+                f"Mismatch at index {i} for attribute '{attr}'",
+            )
+
+    def test_event(self):
+        """Test the event() function for correct tuple conversion.
+
+        Verifies that the function returns a tuple of length 6
+        when given an Event row from the database.
+        """
+        # List of row attributes to match with the tuple values
+        row_attributes = [
+            "idx_event",
+            "name",
+            "epoch_utc",
+            "enabled",
+            "ts_modified",
+            "ts_created",
+        ]
+        row = self.session.query(Event).first()
+        self.assertIsNotNone(row, "No Event row found in the test database.")
+        result = rows.event(row)
+        self.assertIsInstance(result, tuple)
+        self.assertEqual(len(result), 6)
+        # Check each value in the tuple
+        for i, attr in enumerate(row_attributes):
+            row_value = getattr(row, attr)
+
+            # Decode byte string if necessary
+            if isinstance(row_value, bytes):
+                row_value = row_value.decode()
+
+            self.assertEqual(
+                row_value,
+                result[i],
+                f"Mismatch at index {i} for attribute '{attr}'",
+            )
+
+    def test_l1interface(self):
+        """Test the l1interface() function for correct tuple conversion.
+
+        Verifies that the function returns a tuple of length 25
+        when given a L1Interface row from the database.
+        """
+        # List of row attributes to match with the tuple values
+        row_attributes = [
+            "idx_l1interface",
+            "idx_device",
+            "ifindex",
+            "duplex",
+            "ethernet",
+            "nativevlan",
+            "trunk",
+            "ifspeed",
+            "iftype",
+            "ifalias",
+            "ifdescr",
+            "ifname",
+            "ifadminstatus",
+            "ifoperstatus",
+            "ts_idle",
+            "cdpcachedeviceid",
+            "cdpcachedeviceport",
+            "cdpcacheplatform",
+            "lldpremportdesc",
+            "lldpremsyscapenabled",
+            "lldpremsysdesc",
+            "lldpremsysname",
+            "enabled",
+            "ts_modified",
+            "ts_created",
+        ]
+
+        row = self.session.query(L1Interface).first()
path: tests/switchmap_/server/db/misc/test_rows.py
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Fix the extremely small error code.**

The pipeline failure indicates an error code issue on this line.




Based on the pipeline error, it appears that your project requires error codes to be greater than 1000. This line or the surrounding assertion likely needs to include a proper error code in its error message.

---

🏁 Script executed:

```shell
#!/bin/bash
# Check for error code policy in the project
rg -A 2 -B 2 "log\.\w+\([0-9]+" switchmap
```

Length of output: 27365

---



</details>

**Update error code to conform with project standards**

It appears that the test in `tests/switchmap_/server/db/misc/test_rows.py` is triggering a pipeline failure because an error code being used (or defaulted) is smaller than the minimum required (i.e. error codes must be >1000). Please update the error code in the failing assertion or logging call around line 271 to a proper value—similar to those used elsewhere in the project (e.g., 1011, 1012, etc.). Verify that any errors raised from this test incorporate an error code that meets our project policy.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Actions: PR Workflow</summary>

[error] 271-271: Extremely small error code 1 found. Must be greater than 1000. Please fix.

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit d1119b8
created_at: 2025-04-12T17:52:40Z
author_association: NONE
diff_hunk: @@ -0,0 +1,433 @@
+#!/usr/bin/env python3
+"""Test the rows module.
+
+This module contains unit tests for the functions in the `rows.py` file.
+The tests ensure that the following functions correctly convert
+database rows to tuples:
+
+- device()
+- root()
+- event()
+- l1interface()
+- mac()
+- macip()
+- macport()
+- oui()
+- vlan()
+- vlanport()
+- zone()
+- ip()
+- ipport()
+
+Additionally, edge cases where the input is None are tested to ensure that
+each function properly raises AttributeErrors when provided with invalid input.
+
+The tests verify the following:
+- Correct tuple conversion for each function.
+- Correct tuple length and data type.
+- Correct value in the tuple for each entry in the row
+- Handling of invalid (None) inputs with appropriate AttributeErrors.
+"""
+
+import os
+import sys
+
+import unittest
+from switchmap.server.db.misc import rows
+from switchmap.server.db.models import (
+    Device,
+    Event,
+    Ip,
+    IpPort,
+    Mac,
+    MacIp,
+    MacPort,
+    Oui,
+    Root,
+    Vlan,
+    VlanPort,
+    Zone,
+)
+
+# Renaming L1Interface model as LOneInterface
+# as error.py is throwing error for L1Interface
+from switchmap.server.db.models import L1Interface as LOneInterface
+from switchmap.server.db import SCOPED_SESSION
+from tests.testlib_ import setup
+from tests.testlib_ import db
+
+# Try to create a working PYTHONPATH
+EXEC_DIR = os.path.dirname(os.path.realpath(__file__))
+ROOT_DIR = os.path.abspath(
+    os.path.join(
+        os.path.abspath(
+            os.path.join(
+                os.path.abspath(
+                    os.path.join(
+                        os.path.abspath(
+                            os.path.join(
+                                os.path.abspath(
+                                    os.path.join(EXEC_DIR, os.pardir)
+                                ),
+                                os.pardir,
+                            )
+                        ),
+                        os.pardir,
+                    )
+                ),
+                os.pardir,
+            )
+        ),
+        os.pardir,
+    )
+)
+_EXPECTED = """\
+{0}switchmap-ng{0}tests{0}switchmap_{0}server{0}db{0}misc""".format(
+    os.sep
+)
+if EXEC_DIR.endswith(_EXPECTED) is True:
+    # We need to prepend the path in case the repo has been installed
+    # elsewhere on the system using PIP. This could corrupt expected results
+    sys.path.insert(0, ROOT_DIR)
+else:
+    print(
+        """This script is not installed in the
+        "{0}" directory. Please fix.""".format(
+            _EXPECTED
+        )
+    )
+    sys.exit(2)
+
+
+class TestRowsMethods(unittest.TestCase):
+    """Test suite for functions in rows.py."""
+
+    @classmethod
+    def setUpClass(cls):
+        """Set up test environment before running tests."""
+        # Load the configuration
+        cls.config = setup.config()
+        cls.config.save()
+        # Drop tables
+        cls.database = db.Database()
+        cls.database.drop()
+        cls.database.create()
+        # Populate the database
+        db.populate()
+        cls.session = SCOPED_SESSION()
+
+    @classmethod
+    def tearDownClass(cls):
+        """Clean up test environment after tests."""
+        if cls.session:
+            cls.session.close()
+        cls.database.drop()
+        cls.config.cleanup()
+
+    # ----------------------------------------------------------------------
+    # Standard conversion tests
+    # ----------------------------------------------------------------------
+
+    def compare_row_to_expected(self, row, expected_dict):
+        """Compare the attributes of a row with expected values.
+
+        Iterates through the `expected_dict`, comparing each value with the
+        corresponding attribute of the `row` object, handling byte string
+        normalization, and reporting mismatches using assertions.
+
+        Args:
+            row: The database row object.
+            expected_dict: A dictionary of expected attribute values.
+
+        Raises:
+            AttributeError: If an attribute is missing in the row.
+            AssertionError: If an attribute value does not match.
+
+        Returns:
+            None
+        """
+        for key, expected_value in expected_dict.items():
+            actual_value = getattr(row, key)
+
+            # Normalize byte strings
+            if isinstance(actual_value, bytes):
+                actual_value = actual_value.decode()
+            if isinstance(expected_value, bytes):
+                expected_value = expected_value.decode()
+
+            with self.subTest(attribute=key):
+                self.assertEqual(
+                    actual_value,
+                    expected_value,
+                    f"Mismatch for attribute '{key}'",
+                )
path: tests/switchmap_/server/db/misc/test_rows.py
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Consider adding an explicit error code to assertion messages.**

The error message in the assertEqual statement should include an error code greater than 1000 to comply with project standards. This might be the source of the pipeline failure mentioned in previous comments.


```diff
- self.assertEqual(
-     actual_value,
-     expected_value,
-     f"Mismatch for attribute '{key}'",
- )
+ self.assertEqual(
+     actual_value, 
+     expected_value,
+     f"1001: Mismatch for attribute '{key}'",
+ )
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
                self.assertEqual(
                    actual_value, 
                    expected_value,
                    f"1001: Mismatch for attribute '{key}'",
                )
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit d1119b8
created_at: 2025-04-13T22:27:56Z
author_association: NONE

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-04-11T06:12:05Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

"""
## Walkthrough
A new test module has been added in `tests/switchmap_/server/db/misc/test_rows.py`. This module, using the `unittest` framework, validates the conversion of database rows from `rows.py` into tuples for various entity types. It tests expected tuple lengths, data type conversions, and proper exception handling when provided with invalid input. Setup and teardown routines manage database initialization and cleanup, ensuring isolated test runs. Detailed docstrings are included to explain the purpose and outcomes of each test.

## Changes
| File(s)                                                       | Change Summary                                                                                                                                                                                      |
|---------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| tests/switchmap_/server/db/misc/test_rows.py                  | Added new unit tests in the `TestRowsMethods` class to verify correct conversion of database rows into tuples for multiple entities, including tests for edge cases (e.g., `None` input).      |
| tests/testlib_/errors.py                                       | Modified regex pattern in `_codes` function to require digits to be followed by a comma, narrowing the matching criteria for numeric codes in function arguments.                                  |

## Assessment against linked issues
| Objective                                                     | Addressed | Explanation |
|---------------------------------------------------------------|-----------|-------------|
| Create unittests for `switchmap/server/db/misc/rows.py` (#195) | ✅        |             |

## Suggested reviewers
- palisadoes

## Poem
> I'm a rabbit, leaping with delight,  
> Testing rows in code by day and night.  
> Tuples line up neatly, each test a happy hop,  
> No row misbehaves, no bug can stop.  
> With carrot cheer, I celebrate this spree—  
> CodeRabbi-approved, as nimble as can be!  
> 🥕🐇
"""

<!-- walkthrough_end -->


---

<details>
<summary>📜 Recent review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 8a7b2410f7ba5f97f231487b2942a716c0e56ea3 and 5570d6984de7fd431fdceb481df693f5d65b41be.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `tests/switchmap_/server/db/misc/test_rows.py` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* tests/switchmap_/server/db/misc/test_rows.py

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: Test Application

</details>

</details>
<!-- internal state start -->


<!-- DwQgtGAEAqAWCWBnSTIEMB26CuAXA9mAOYCmGJATmriQCaQDG+Ats2bgFyQAOFk+AIwBWJBrngA3EsgEBPRvlqU0AgfFwA6NPEgQAfACgjoCEYDEZyAAUASpETZWaCrKNwSPbABsvkCiQBHbGlcSHFcLzpIACJoEIB6CnwAd2QARgAGDIUpKlJoyGS0ZAcBZnUaejlIbERKSABBAQQwAFkAZXRkW0gMRwF6gGY0gE5+LABxdQAJbAENGFgPZm0sInw0X3wAMzCEbrtUAnRaemwMdTCQ5G38Pk3fbfOxeHwMZHgsXCXIAAMk1IabiyX6QbbwSJhfDoBgIEhSSCZDIAUiuiFCTFyaFICzgqDQp38iDqH2JwV6/XqowArBo3D8ep8GF5sEpkDR0Tc7nsPLcfClPkQwc9xG9EFxfkoJPAGCQABQASl+ABo/kl8LhFSq/vD2FrVb8vGlPjQKNs0LL9X8Vgwrb8bfBuHabdw7pqlQb8Nh4HaJF5ML7/RhXRR3drfgAvN7yj1/R1WzD0X6OkNh3FLOpo3DIXLwbbyb7USAkC2wYUYF5vBQUfxiLzyJgYXLZyC0agqYoeAEfDDHXDYbiRGQNpYMADWgrCA8hjdyiFeWAtDGwVAYslV/cHHkiGCI31VbdwaDCsm40lViZ51druHIxP4uwkm2CyGS6gQXx+m6H6Y8HJbmyINCmL1HQpCMJ2r5LP4V6fNweAoMgvwAHLRtqZAOBQk6FqEaDcLw+C8PA1AeCQAAesrcKK7zoDBVBIFEtx8J8T5ePA9BwXgiB0gYqGQAI/hoBOu6MLAmCkMgzgeCaSS0NgspVAW+zWDYF4YPQtD4MubC9tQC41Nwh7SLRHgYBqfgkJET69r+ODfNyjbghQzDIEwzCDsRFYeG+3xXo2uBYQIeCTkQ3pKGxd7oOpKAViybLoKJohjmx6IxbQMp6SJOGMFh4gMJsiEOMZ2z+kQpCKZAADCigkDYKhqKEDQAJKQGJCIDGQFnSiQyR0Bu1ytWg7UkJ1yS5TQWBMegPi9D1YIlv2RL9alIF5MsI0tty5GynQyAjNSyJqfQDxZsg3DFMgXhaQ8sg8e4KkQVgAxdfAPVRNQXCwLguDcOK8TxEQ75zBobnxFYmxIAS+DSAAYl66l6W88SID5sIrNwYC7vE8E+PEwwjOYljVaw7AlI4KwuPSXbwq9yQKCTvbIFW3yoIyTb4F4CLbCNtACBaY5RfQ/hBANREUxC8jbEkzBXv43W9XwvznSlUPSKCl5Eq67zFdLV5oHgsDcr8TQtB0vw8U1FziDdBrK5DmlqxZIvolE3y8hzV1viJFrUeKBhpAsADyAhHp86D4UkrFgrr1VKHVqiXM1YLcm79hzOU31RMkdxjnSABMCw2NIHMIpgDZvCVMriCJ4JDjFV62HSgwLAAou8K4eCd/5nRdgsmZArHsXSAAsCwNLCr0Iv+ChKDkyjgcUjBXZmxxIqii+usS8ACJEPHtHMdQi729a2xDiCq4goKuiaURerhiXjlCxbtzB2WUEkfBMPFYeGvgRAaEoIKRBQSaV6OZfAuR/TcD7sUMcrsfj63snwY4uZ8x7HxEQVYd0EEGzuAaE28A2jtG1JrN4Sh6DZTdvIBWHg2qTnStscEy4vC4HkOxdgeZZDYR+EBFcsoHxRWLDWPBYDq5CkuOlegZlQh3HgIDDAJFWzwBvPWaOLArzT1riQI69gxL+GOvYBg/gMKG1CBCFk6IqBiNgmSEgPFfhf2UAnUOoI5Sp0QSwEiVQNQKh4JQJibADFPBmnLWmYQsJlUoFEao7jcEUEAJgEyBhYvk0EYAAMiRCg+DmiELNhZVM8Ciypx6EUHuW9vYzVhElbiiwuz4HMvlWo3lF7sN7HmV6x12Q/D6MwAYis0igjDseRAZ4GDtIYJACKHgBiTiZHcFRbDeyUF4CQSoXRBHvwcjVfiSkPC/E2RQAA+o4w5/hUxAhBIYrCVFbKIMNnwRAeioiICMSNd4pima7FTgRHeJAVh5UmZ8Tu0VU5PArNRCyIyxTb0hFNFmMgSB0JEQ4SJ6JuFFJ+GCysWAmDeCqNJdykdXbQiGvgdiadYRgkAh4Te6hJDGTDqCvAHdblxIfNzfRT9XmKMwIVckxwCS0CJF0pA9i7bnwdpfSFWtyE7Kdik9FoRQXwDIvxaSsVWRREZT8ZcNZ2APSsdBPYvLuXWOGSQc6VjpK2J4oHLEPgNw6pYDpFs/gSqiHvkwHwKg7h6QRKEuaBFZT3luMuOo9AqwYRXNhEIc81r0y3BNaQiBVREhLpOA5STLJeKfgcmeHglA0GxTolYJpViTkcZAIIENWFqqYutCg8j/5GH0MYcAUAyARt2B44gZBlBrLci6rgvB+DCA9fS4c+aqDOK0DoVtJgoB4lJIIntpByBWvoIO9gXAqB0wcE4FwcrHHToarO3QYBDBttMAYbuyNUawHRoc5GlBcjxF5vEcoLz4j/lOSkbiwIOAGGiMBgwFhGhNV7eunN+6KbyB2KJcS0gjANFmnTaezBFDeD2belG6g0Z4SfXUCgr732foYN+kIv7ATAlBGJBFnVBXEp4JHdhCb/BLHePSmoVtTrJ2QU6pslB5xVixb7euqd/h/ouaCLRv5MyiYXK5N4zYB7OFeLUVs7Y+aZgBKOkQYgex9mnMZM5RIOEiQ4eIYyDhKWL1+AAERprKcMNh6m4HDC3KQvZwxpLSJbU05pnMGlaBacMIWGBNW4GFi0Vg3ThkDt6cMAA1IMyWgyxdDOGAAWmhA0kXtSXl+JFjL7nbLTwcOobRKB3KRBdVEWok4rCyHshgAA5Ehc4FQQgyaoGwbOFAxyqjqC2AcCVp6Hg7JmMg0okgYBdaqV0OMSIfFCD5MsE2dEiywsZZVFBUq6fgyWSllmCynj/NCVBuzrwervJ8jRxmFCCb2wuHiLdSxZkgGweym7Rxji6Rijws4hP6QU1Wfwi0aLHm/B4eDqdyKjLWTuPcZZLyUK/MZ9rj2Jotn+ZS9x31Ap4GMrDn4sj5EFQm9pupyQx6tUsmePgX3DZJjcpakgVHDkEEOfDj1dBQSs6ksgI7KOCfbyJwI1OunsSrFSnDsiCOoisRfKqMyzkIYRknHIGg9gAqCnZNCSxeu+Mwl1YogXWEgIYDKwNQC0ImRxVIrQcC+USSFGgns1C5BBllPDesziSroTC8e3OEHIoFwXmJJQax2VeXG1F0FGgLdhEUG934bQvvtXSA8C8sgan8A1PutPZkvcVgKIkvYVZo3UclgoJpZIWAScFq052Pujk5ErkRl8QPL9rUc0763yImAq/bFNKdHiYGGgsP7Ypp+qclDF6sTPw78u3S3z4PBHeMpn7hFeogFtkBkJzSL/6e8jHaASmL/eOI6JXOpFaKs5nUqf44fvY+59xHKBvoEB+pA5Gf3dmkyAxAwMAgDACMFvX/DYgECfUzQuUA2A2iFA0sGakg37WeXJmcDg12FhEQz3wMHun8FCn9D4Hh2FX0nOkzgoCwCaQoUDzIgCh9iUUBhbClnUVB0XEbUcFJnEx+F+GORqilXYMKEXgwwYQ6QWCsFCS9EQBPllhIFIFVX+GiAAD0NAAAqAAfjlAAB1aAABqBUdQjQgAEmiFBFxyWGOgwHkHSmYMkmsOSHdx4Oz1113AWCalW0XhwN3CYyUNUM0J0P0IVGVCMNMO1EcJlDLDMjpmSWUR2zEiVR+FsPUARWTn5F6gqmPEHTQHTFQG8PAgUWEVSCvAsIrVykoGIifmSJbBj3whrzrTuE7nphWGcNolChdR1ywl3AWw1A4RummlNEnAiMpR6QqImUcUkhgkLV53oFYJllTl4QoH4SmW4kJkaCn0XzFFnySNEGIM7zu3h1XwjXXzmDYgmUs13yME9zsSAMQJAIvSvQ7Wing1XT7Q3WaO3TTz3QwMPWqGPXqnUDPXnXbWaPUEOXYkQFORpjeloEOXRGcFCFbQMAXUgEGAEBGBIHRNoAAA5sS0A0hqRh4BAGARhtgAA2bYWUBgAAdmxLSD5m2BGCZMGApMJOpIyGpDSHPUMBROHloDSDQDJLpNODzgyEGAJBGGpMGAYDzjzmxIYAEDzloClOHhUGHiVPxO5kGGxPoGBIgEgAyDQAyClLSGpOpAEHlLJLzhIDSG2DSHFIYCJPZOpIVJpKZJIHlMGBIG2EGG2GxO5ORJBMGDzmpO2GpA5LznDOHkGE5NlGJOVJGAyBJLSDSGHgyBTP5JGApLJJIElK5L1KgEpJlJGCUDNI9OpOpIEGpGpLbBUBDNoDJLQFpPtNoGDLzj5LQBIDzi9IDJRIyEZLSFzI5OpG9IpIyEbLzj5mDIJKbIEFbIyDnIYGxLTNxMdMGF7JBPVLQDtLJMTLnO2GHj9OpMnMFUYQyEjONN9LQBGGJNJMyArImQLLBDTOpAYCTOLOpBDJGDpPDMZMGFoAYBJPlPVKyAyGVKNP5LtI3P1MdOHhGFFOpPgtUBTNlDznpOpPxIJPZJGgJJpOHltOpPwupFoGyCfO1OHmXLSGxMbMxIGAYF3PNKzI5KlP7OpDxMlIXLQuXPorJOgqgAyGxLtLSFoGHhzPPIFItGpLJIyEPN9LJOpLoFoHgpIHDJIHIsnOVL4sgGxMGCZKbIXMQupBGHpLQH/JGGvMlMrKtNEsrNtP0oFPZNIoeJRLlMxJjIEqopEvguHlEBUHnKFLQGpEpKtOvOpLSAYGIppOpK0rAuxNfOHlVLfPovCulIFJzLDK9KZONKMv/PHLzlMoYEHK0u9O2AEDTJLB9NJJGj5IpMQrzM7IEsxLirpP7L9NXK0v5JTJvJ1OxKyHJLgo5PMoVItCMrgpHO2F5jioUsKpGHXKfLxMrPbMyG2ErMCsZNDO7NTOxMWpGC3LCrJLfJUpzNMq0upGrLAt3OXNLImujNtIAsxIouEvJNmrDIbPNPwueiRJRMHTBIhKhPljoG52in0CAA -->

<!-- internal state end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=PalisadoesFoundation/switchmap-ng&utm_content=319):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-11T06:12:09Z
body: ## Our Pull Request Approval Process

We have these basic policies to make the approval process smoother for our volunteer team.

### Testing Your Code

Please make sure your code passes all tests. Our test code coverage system will fail if either of these two conditions occur:

1.  The overall code coverage drops below the target threshold of the repository
2.  Any file in the pull request has code coverage levels below the repository threshold

The process helps maintain the overall reliability of the code base and is a prerequisite for getting your PR approved. Assigned reviewers regularly review the PR queue and tend to focus on PRs that are passing.

### Reviewers

When your PR has been assigned reviewers contact them to get your code reviewed and approved via:

1. comments in this PR or
1. our slack channel

#### Reviewing Your Code

Your reviewer(s) will have the following roles:

1.  arbitrators of future discussions with other contributors about the validity of your changes
2.  point of contact for evaluating the validity of your work
3.  person who verifies matching issues by others that should be closed.
4.  person who gives general guidance in fixing your tests

:dart: Contacting the person who assigned the mentors is not advised unless they make a request. Do not @ the person who did the assignment otherwise.
<!-- thollander/actions-comment-pull-request "pr_review_policy" -->
author_association: NONE
user: login: palisadoes
created_at: 2025-04-13T13:05:23Z
body: 1. Please make coderabbit.ai approves your work.
2. Resolve any conflicting files
3. Make sure all tests pass and are valid.
4. Ensure the test code coverage for your patch reaches as close to **100%** as possible.

author_association: CONTRIBUTOR
user: login: palisadoes
created_at: 2025-04-13T13:06:02Z
body: 1. There is a check to make sure the error codes in log.debug don't overlap.
2. Please check again
author_association: CONTRIBUTOR
user: login: Abhi-MS
created_at: 2025-04-13T21:35:04Z
body: @palisadoes Thank you for the feedback. I am having difficulty pinpointing where this error is coming from; it is not directly from the test file.
![image](https://github.com/user-attachments/assets/71dc4dd7-95e1-4ce4-a709-7b09c86c5d3a)

author_association: NONE
user: login: Abhi-MS
created_at: 2025-04-13T22:25:57Z
body: @coderabbitai full review
author_association: NONE
user: login: coderabbitai[bot]
created_at: 2025-04-13T22:26:02Z
body: <!-- This is an auto-generated reply by CodeRabbit -->
<details>
<summary>:white_check_mark: Actions performed</summary>

Full review triggered.

</details>
author_association: NONE
user: login: Abhi-MS
created_at: 2025-04-13T23:36:49Z
body: @palisadoes This PR is now passing all checks. The issue was due to the number 1 in the following line being incorrectly interpreted as an error code by the error_code_report.py script:
![image](https://github.com/user-attachments/assets/dd0283c3-5210-4c61-b37e-d8889576117f)

This behavior stems from the following function, which could be improved to avoid such false positives in the future:
![image](https://github.com/user-attachments/assets/caf28505-ed2e-4cb1-ad23-a8bcdf13c31e)

Would you like me to create an issue addressing this?
author_association: NONE
user: login: palisadoes
created_at: 2025-04-14T14:40:43Z
body: > @palisadoes This PR is now passing all checks. The issue was due to the number 1 in the following line being incorrectly interpreted as an error code by the error_code_report.py script: ![image](https://private-user-images.githubusercontent.com/113867502/433175640-dd0283c3-5210-4c61-b37e-d8889576117f.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDQ2NDE5MTUsIm5iZiI6MTc0NDY0MTYxNSwicGF0aCI6Ii8xMTM4Njc1MDIvNDMzMTc1NjQwLWRkMDI4M2MzLTUyMTAtNGM2MS1iMzdlLWQ4ODg5NTc2MTE3Zi5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjUwNDE0JTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI1MDQxNFQxNDQwMTVaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT04ZDE2NDFhZDZhOTZhZTdkOGU2MDIwNTc3ZTFhYzc5NzA5MTY1ZjM2Yjg2NDgxZmQwZWRjY2M3ODk3NWNiNDllJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.zcG983MZK4iea5n3I45vAfC9M9uQ1vphQArHzf0wrEU)
> 
> This behavior stems from the following function, which could be improved to avoid such false positives in the future: ![image](https://private-user-images.githubusercontent.com/113867502/433175718-caf28505-ed2e-4cb1-ad23-a8bcdf13c31e.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDQ2NDE5MTUsIm5iZiI6MTc0NDY0MTYxNSwicGF0aCI6Ii8xMTM4Njc1MDIvNDMzMTc1NzE4LWNhZjI4NTA1LWVkMmUtNGNiMS1hZDIzLWE4YmNkZjEzYzMxZS5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjUwNDE0JTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI1MDQxNFQxNDQwMTVaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT1mYWY5NjcyNGIxMmY1YjUxNzZkNDkxMjU1Mjc4YzcyNTM3MWYzZjg5YmM1MGUwNTQxOTA0ZTY4YjAxMTlmNGM1JlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.qKgfUdGjeRieCQ9GjYE45Ys80KMIByt4DnZK8HAyerg)
> 
> Would you like me to create an issue addressing this?

Fix it in this PR
author_association: CONTRIBUTOR
user: login: Abhi-MS
created_at: 2025-04-15T04:00:58Z
body: > > @palisadoes This PR is now passing all checks. The issue was due to the number 1 in the following line being incorrectly interpreted as an error code by the error_code_report.py script: ![image](https://private-user-images.githubusercontent.com/113867502/433175640-dd0283c3-5210-4c61-b37e-d8889576117f.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDQ2NDE5MTUsIm5iZiI6MTc0NDY0MTYxNSwicGF0aCI6Ii8xMTM4Njc1MDIvNDMzMTc1NjQwLWRkMDI4M2MzLTUyMTAtNGM2MS1iMzdlLWQ4ODg5NTc2MTE3Zi5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjUwNDE0JTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI1MDQxNFQxNDQwMTVaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT04ZDE2NDFhZDZhOTZhZTdkOGU2MDIwNTc3ZTFhYzc5NzA5MTY1ZjM2Yjg2NDgxZmQwZWRjY2M3ODk3NWNiNDllJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.zcG983MZK4iea5n3I45vAfC9M9uQ1vphQArHzf0wrEU)
> > This behavior stems from the following function, which could be improved to avoid such false positives in the future: ![image](https://private-user-images.githubusercontent.com/113867502/433175718-caf28505-ed2e-4cb1-ad23-a8bcdf13c31e.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDQ2NDE5MTUsIm5iZiI6MTc0NDY0MTYxNSwicGF0aCI6Ii8xMTM4Njc1MDIvNDMzMTc1NzE4LWNhZjI4NTA1LWVkMmUtNGNiMS1hZDIzLWE4YmNkZjEzYzMxZS5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjUwNDE0JTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI1MDQxNFQxNDQwMTVaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT1mYWY5NjcyNGIxMmY1YjUxNzZkNDkxMjU1Mjc4YzcyNTM3MWYzZjg5YmM1MGUwNTQxOTA0ZTY4YjAxMTlmNGM1JlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.qKgfUdGjeRieCQ9GjYE45Ys80KMIByt4DnZK8HAyerg)
> > Would you like me to create an issue addressing this?
> 
> Fix it in this PR

I’ve fixed errors.py by adding a comma to the regex pattern, since all the detected lines follow that structure (as shown in the screenshot). This should prevent cases like 1 in L1Interface from being mistakenly identified as error codes.
![image](https://github.com/user-attachments/assets/2c1ba9f3-a9f3-4627-9fa7-7f99b8a2da3b)
author_association: NONE