-
Notifications
You must be signed in to change notification settings - Fork 317
Show favorite nodes in --nodes #888
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,221 @@ | ||
| """Meshtastic unit tests for showNodes favorite column feature""" | ||
|
|
||
| from unittest.mock import MagicMock | ||
|
|
||
| import pytest | ||
|
|
||
| from ..mesh_interface import MeshInterface | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def _iface_with_favorite_nodes(): | ||
| """Fixture to setup nodes with favorite flags.""" | ||
| nodesById = { | ||
| "!9388f81c": { | ||
| "num": 2475227164, | ||
| "user": { | ||
| "id": "!9388f81c", | ||
| "longName": "Favorite Node", | ||
| "shortName": "FAV1", | ||
| "macaddr": "RBeTiPgc", | ||
| "hwModel": "TBEAM", | ||
| }, | ||
| "position": {}, | ||
| "lastHeard": 1640204888, | ||
| "isFavorite": True, | ||
| }, | ||
| "!12345678": { | ||
| "num": 305419896, | ||
| "user": { | ||
| "id": "!12345678", | ||
| "longName": "Regular Node", | ||
| "shortName": "REG1", | ||
| "macaddr": "ABCDEFGH", | ||
| "hwModel": "TLORA_V2", | ||
| }, | ||
| "position": {}, | ||
| "lastHeard": 1640204999, | ||
| "isFavorite": False, | ||
| }, | ||
| "!abcdef00": { | ||
| "num": 2882400000, | ||
| "user": { | ||
| "id": "!abcdef00", | ||
| "longName": "Legacy Node", | ||
| "shortName": "LEG1", | ||
| "macaddr": "XYZABC00", | ||
| "hwModel": "HELTEC_V3", | ||
| }, | ||
| "position": {}, | ||
| "lastHeard": 1640205000, | ||
| # Note: No isFavorite field - testing backward compatibility | ||
| }, | ||
| } | ||
|
|
||
| nodesByNum = { | ||
| 2475227164: { | ||
| "num": 2475227164, | ||
| "user": { | ||
| "id": "!9388f81c", | ||
| "longName": "Favorite Node", | ||
| "shortName": "FAV1", | ||
| "macaddr": "RBeTiPgc", | ||
| "hwModel": "TBEAM", | ||
| }, | ||
| "position": {"time": 1640206266}, | ||
| "lastHeard": 1640206266, | ||
| "isFavorite": True, | ||
| }, | ||
| 305419896: { | ||
| "num": 305419896, | ||
| "user": { | ||
| "id": "!12345678", | ||
| "longName": "Regular Node", | ||
| "shortName": "REG1", | ||
| "macaddr": "ABCDEFGH", | ||
| "hwModel": "TLORA_V2", | ||
| }, | ||
| "position": {"time": 1640206200}, | ||
| "lastHeard": 1640206200, | ||
| "isFavorite": False, | ||
| }, | ||
| 2882400000: { | ||
| "num": 2882400000, | ||
| "user": { | ||
| "id": "!abcdef00", | ||
| "longName": "Legacy Node", | ||
| "shortName": "LEG1", | ||
| "macaddr": "XYZABC00", | ||
| "hwModel": "HELTEC_V3", | ||
| }, | ||
| "position": {"time": 1640206100}, | ||
| "lastHeard": 1640206100, | ||
| # Note: No isFavorite field - testing backward compatibility | ||
| }, | ||
| } | ||
|
|
||
| iface = MeshInterface(noProto=True) | ||
| iface.nodes = nodesById | ||
| iface.nodesByNum = nodesByNum | ||
| myInfo = MagicMock() | ||
| iface.myInfo = myInfo | ||
| iface.myInfo.my_node_num = 2475227164 | ||
| return iface | ||
|
|
||
|
|
||
| @pytest.mark.unit | ||
| def test_showNodes_favorite_column_header(capsys, _iface_with_favorite_nodes): | ||
| """Test that 'Fav' column header appears in showNodes output""" | ||
| iface = _iface_with_favorite_nodes | ||
| iface.showNodes() | ||
| out, err = capsys.readouterr() | ||
| assert "Fav" in out | ||
| assert err == "" | ||
|
|
||
|
|
||
| @pytest.mark.unit | ||
| def test_showNodes_favorite_asterisk_display(capsys, _iface_with_favorite_nodes): | ||
| """Test that favorite nodes show asterisk and non-favorites show empty""" | ||
| iface = _iface_with_favorite_nodes | ||
| iface.showNodes() | ||
| out, err = capsys.readouterr() | ||
|
|
||
| # Check that the output contains the "Fav" column | ||
| assert "Fav" in out | ||
|
|
||
| # Find lines containing our nodes | ||
| lines = out.split('\n') | ||
| favorite_line = None | ||
| regular_line = None | ||
| legacy_line = None | ||
| for line in lines: | ||
| if "Favorite Node" in line or "FAV1" in line: | ||
| favorite_line = line | ||
| if "Regular Node" in line or "REG1" in line: | ||
| regular_line = line | ||
| if "Legacy Node" in line or "LEG1" in line: | ||
| legacy_line = line | ||
|
|
||
| # Verify all nodes are present in the output | ||
| assert favorite_line is not None, "Favorite node should be in output" | ||
| assert regular_line is not None, "Regular node should be in output" | ||
| assert legacy_line is not None, "Legacy node should be in output" | ||
|
|
||
| # Verify the favorite node has an asterisk in its row | ||
| assert "*" in favorite_line, "Favorite node should have an asterisk" | ||
|
|
||
| # Verify the regular (non-favorite) node does NOT have an asterisk | ||
| assert regular_line.count("*") == 0, "Non-favorite node should not have an asterisk" | ||
|
|
||
| # Verify the legacy node (without isFavorite field) does NOT have an asterisk | ||
| assert legacy_line.count("*") == 0, "Legacy node without isFavorite field should not have an asterisk" | ||
|
|
||
| assert err == "" | ||
|
|
||
|
|
||
| @pytest.mark.unit | ||
| def test_showNodes_favorite_field_formatting(): | ||
| """Test the formatting logic for isFavorite field""" | ||
| # Test favorite node | ||
| raw_value = True | ||
| formatted_value = "*" if raw_value else "" | ||
| assert formatted_value == "*" | ||
|
|
||
| # Test non-favorite node | ||
| raw_value = False | ||
| formatted_value = "*" if raw_value else "" | ||
| assert formatted_value == "" | ||
|
|
||
| # Test None/missing value | ||
| raw_value = None | ||
| formatted_value = "*" if raw_value else "" | ||
| assert formatted_value == "" | ||
|
|
||
|
|
||
| @pytest.mark.unit | ||
| def test_showNodes_with_custom_fields_including_favorite(capsys, _iface_with_favorite_nodes): | ||
| """Test that isFavorite can be specified in custom showFields""" | ||
| iface = _iface_with_favorite_nodes | ||
| custom_fields = ["user.longName", "isFavorite"] | ||
| iface.showNodes(showFields=custom_fields) | ||
| out, err = capsys.readouterr() | ||
|
|
||
| # Should still show the Fav column when explicitly requested | ||
| assert "Fav" in out | ||
| assert err == "" | ||
|
|
||
|
|
||
| @pytest.mark.unit | ||
| def test_showNodes_default_fields_includes_favorite(_iface_with_favorite_nodes): | ||
| """Test that isFavorite is included in default fields""" | ||
| iface = _iface_with_favorite_nodes | ||
|
|
||
| # Call showNodes which uses default fields | ||
| result = iface.showNodes() | ||
|
|
||
| # The result should contain the formatted table as a string | ||
| assert "Fav" in result | ||
|
|
||
|
|
||
| @pytest.mark.unit | ||
| def test_showNodes_backward_compatibility_missing_field(capsys, _iface_with_favorite_nodes): | ||
| """Test that nodes without isFavorite field are handled gracefully""" | ||
| iface = _iface_with_favorite_nodes | ||
| iface.showNodes() | ||
| out, err = capsys.readouterr() | ||
|
|
||
| # Find the legacy node line | ||
| lines = out.split('\n') | ||
| legacy_line = None | ||
| for line in lines: | ||
| if "Legacy Node" in line or "LEG1" in line: | ||
| legacy_line = line | ||
| break | ||
|
|
||
| # Verify the legacy node appears in output | ||
| assert legacy_line is not None, "Legacy node without isFavorite field should appear in output" | ||
|
|
||
| # Verify it doesn't have an asterisk (should be treated as non-favorite) | ||
| assert legacy_line.count("*") == 0, "Legacy node should not have asterisk (treated as non-favorite)" | ||
|
|
||
| assert err == "" | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test fixture should include a node that doesn't have the isFavorite field at all to test backward compatibility with existing nodes. Currently, all test nodes explicitly have isFavorite set to either True or False. Adding a third node without the isFavorite field would ensure the implementation handles legacy nodes gracefully.