Repository: aristanetworks/anta (python)

Issue:
bug(anta): TypeError: '<' not supported between instances of 'NoneType' and 'str'
The following error can occur if we try to sort a `ResultManager` by `custom_field` with results having `custom_field` set to `None`.
```
  File "/home/cbaillar/git_projects/anta/anta/result_manager/__init__.py", line 298, in sort
    self._result_entries.sort(key=lambda result: [getattr(result, field) for field in sort_by])
    ~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: '<' not supported between instances of 'NoneType' and 'str'
```
I think something like `self._result_entries.sort(key=lambda result: [getattr(result, field) or "" for field in sort_by])` could work.

Pull request:
fix(anta): Fixing the TypeError: '<' not supported between instances of 'NoneType' and 'str'
# Description

anta/anta/result_manager/__init__.py", line 298, in sort
    self._result_entries.sort(key=lambda result: [getattr(result, field) for field in sort_by])
    ~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: '<' not supported between instances of 'NoneType' and 'str'

Fixes #1110 

# Checklist:

- [X] My code follows the style guidelines of this project
- [X] I have performed a self-review of my code
- [X] I have run pre-commit for code linting and typing (`pre-commit run`)
- [ ] I have made corresponding changes to the documentation
- [ ] I have added tests that prove my fix is effective or that my feature works
- [X] New and existing unit tests pass locally with my changes (`tox -e testenv`)


Patch:
diff --git a/anta/result_manager/__init__.py b/anta/result_manager/__init__.py
index 39ed3644..6f0f9a2e 100644
--- a/anta/result_manager/__init__.py
+++ b/anta/result_manager/__init__.py
@@ -253,7 +253,7 @@ class ResultManager:
             if not set(sort_by).issubset(set(accepted_fields)):
                 msg = f"Invalid sort_by fields: {sort_by}. Accepted fields are: {list(accepted_fields)}"
                 raise ValueError(msg)
-            results = sorted(results, key=lambda result: [getattr(result, field) for field in sort_by])
+            results = sorted(results, key=lambda result: [getattr(result, field) or "" for field in sort_by])
 
         return results
 
@@ -295,7 +295,7 @@ class ResultManager:
         if not set(sort_by).issubset(set(accepted_fields)):
             msg = f"Invalid sort_by fields: {sort_by}. Accepted fields are: {list(accepted_fields)}"
             raise ValueError(msg)
-        self._result_entries.sort(key=lambda result: [getattr(result, field) for field in sort_by])
+        self._result_entries.sort(key=lambda result: [getattr(result, field) or "" for field in sort_by])
         return self
 
     def filter(self, hide: set[AntaTestStatus]) -> ResultManager:


Test patch:
diff --git a/tests/data/test_md_report.md b/tests/data/test_md_report.md
index 1b1acd1a..8b79283c 100644
--- a/tests/data/test_md_report.md
+++ b/tests/data/test_md_report.md
@@ -70,7 +70,7 @@
 | s1-spine1 | Field Notices | VerifyFieldNotice44Resolution | Verifies that the device is using the correct Aboot version per FN0044. | - | skipped | VerifyFieldNotice44Resolution test is not supported on cEOSLab. |
 | s1-spine1 | Hardware | VerifyTemperature | Verifies if the device temperature is within acceptable limits. | - | skipped | VerifyTemperature test is not supported on cEOSLab. |
 | s1-spine1 | Interfaces | VerifyIPProxyARP | Verifies if Proxy ARP is enabled. | - | failure | Interface: Ethernet1 - Proxy-ARP disabled<br>Interface: Ethernet2 - Proxy-ARP disabled |
-| s1-spine1 | ISIS | VerifyISISNeighborState | Verifies the health of IS-IS neighbors. | - | skipped | IS-IS not configured |
+| s1-spine1 | ISIS | VerifyISISNeighborState | Verifies the health of IS-IS neighbors. | custom | skipped | IS-IS not configured |
 | s1-spine1 | LANZ | VerifyLANZ | Verifies if LANZ is enabled. | - | skipped | VerifyLANZ test is not supported on cEOSLab. |
 | s1-spine1 | Logging | VerifyLoggingHosts | Verifies logging hosts (syslog servers) for a specified VRF. | - | failure | Syslog servers 1.1.1.1, 2.2.2.2 are not configured in VRF default |
 | s1-spine1 | MLAG | VerifyMlagDualPrimary | Verifies the MLAG dual-primary detection parameters. | - | failure | Dual-primary detection is disabled |
diff --git a/tests/units/result_manager/test__init__.py b/tests/units/result_manager/test__init__.py
index e70dbf9e..15ece728 100644
--- a/tests/units/result_manager/test__init__.py
+++ b/tests/units/result_manager/test__init__.py
@@ -225,6 +225,11 @@ class TestResultManager:
         assert success_skipped_results[0].categories == ["avt"]
         assert success_skipped_results[-1].categories == ["vxlan"]
 
+        # Check multiple statuses with sort_by custom_field
+        success_skipped_results = result_manager.get_results(status={AntaTestStatus.SUCCESS, AntaTestStatus.SKIPPED}, sort_by=["custom_field"])
+        assert len(success_skipped_results) == 13
+        assert success_skipped_results[-1].test == "VerifyISISNeighborState"
+
         # Check all results with bad sort_by
         with pytest.raises(
             ValueError,
@@ -546,6 +551,32 @@ class TestResultManager:
         assert results[2].result == "failure"
         assert results[2].test == "Test2"
 
+    def test_sort_fields_as_none(self, test_result_factory: Callable[[], TestResult]) -> None:
+        """Test sorting by multiple fields."""
+        result_manager = ResultManager()
+        test1 = test_result_factory()
+        test1.result = AntaTestStatus.ERROR
+        test1.test = "Test3"
+        test1.custom_field = "custom"
+        test2 = test_result_factory()
+        test2.result = AntaTestStatus.ERROR
+        test2.test = "Test1"
+        test3 = test_result_factory()
+        test3.result = AntaTestStatus.FAILURE
+        test3.test = "Test2"
+
+        result_manager.results = [test1, test2, test3]
+        sorted_manager = result_manager.sort(["custom_field"])
+        results = sorted_manager.results
+
+        assert results[0].result == "error"
+        assert results[0].test == "Test1"
+        assert results[1].result == "failure"
+        assert results[1].test == "Test2"
+        assert results[2].result == "error"
+        assert results[2].test == "Test3"
+        assert results[2].custom_field == "custom"
+
     def test_sort_invalid_field(self) -> None:
         """Test that sort method raises ValueError for invalid sort_by fields."""
         result_manager = ResultManager()
diff --git a/tests/units/result_manager/test_files/test_md_report_results.json b/tests/units/result_manager/test_files/test_md_report_results.json
index ab932dc5..25282b16 100644
--- a/tests/units/result_manager/test_files/test_md_report_results.json
+++ b/tests/units/result_manager/test_files/test_md_report_results.json
@@ -75,7 +75,7 @@
     "messages": [
       "IS-IS not configured"
     ],
-    "custom_field": null
+    "custom_field": "custom"
   },
   {
     "name": "s1-spine1",