|
15 | 15 | PubsubFactory,
|
16 | 16 | )
|
17 | 17 | from tests.utils.pubsub.utils import (
|
| 18 | + connect_some, |
18 | 19 | dense_connect,
|
19 | 20 | one_to_all_connect,
|
20 | 21 | sparse_connect,
|
@@ -590,3 +591,166 @@ async def test_sparse_connect():
|
590 | 591 | f"received the message. Ideally all nodes should receive it, but at "
|
591 | 592 | f"minimum {min_required} required for sparse network scalability."
|
592 | 593 | )
|
| 594 | + |
| 595 | + |
| 596 | +@pytest.mark.trio |
| 597 | +async def test_connect_some_with_fewer_hosts_than_degree(): |
| 598 | + """Test connect_some when there are fewer hosts than degree.""" |
| 599 | + # Create 3 hosts with degree=5 |
| 600 | + async with PubsubFactory.create_batch_with_floodsub(3) as pubsubs_fsub: |
| 601 | + hosts = [pubsub.host for pubsub in pubsubs_fsub] |
| 602 | + degree = 5 |
| 603 | + |
| 604 | + await connect_some(hosts, degree) |
| 605 | + await trio.sleep(0.1) # Allow connections to establish |
| 606 | + |
| 607 | + # Each host should connect to all other hosts (since there are only 2 others) |
| 608 | + for i, pubsub in enumerate(pubsubs_fsub): |
| 609 | + connected_peers = len(pubsub.peers) |
| 610 | + expected_max_connections = len(hosts) - 1 # All others |
| 611 | + assert connected_peers <= expected_max_connections, ( |
| 612 | + f"Host {i} has {connected_peers} connections, " |
| 613 | + f"but can only connect to {expected_max_connections} others" |
| 614 | + ) |
| 615 | + |
| 616 | + |
| 617 | +@pytest.mark.trio |
| 618 | +async def test_connect_some_degree_limit_enforced(): |
| 619 | + """Test that connect_some enforces degree limits and creates expected topology.""" |
| 620 | + # Test with small network where we can verify exact behavior |
| 621 | + async with PubsubFactory.create_batch_with_floodsub(6) as pubsubs_fsub: |
| 622 | + hosts = [pubsub.host for pubsub in pubsubs_fsub] |
| 623 | + degree = 2 |
| 624 | + |
| 625 | + await connect_some(hosts, degree) |
| 626 | + await trio.sleep(0.1) |
| 627 | + |
| 628 | + # With 6 hosts and degree=2, expected connections: |
| 629 | + # Host 0 → connects to hosts 1,2 (2 peers total) |
| 630 | + # Host 1 → connects to hosts 2,3 (3 peers: 0,2,3) |
| 631 | + # Host 2 → connects to hosts 3,4 (4 peers: 0,1,3,4) |
| 632 | + # Host 3 → connects to hosts 4,5 (3 peers: 1,2,4,5) - wait, that's 4! |
| 633 | + # Host 4 → connects to host 5 (3 peers: 2,3,5) |
| 634 | + # Host 5 → (2 peers: 3,4) |
| 635 | + |
| 636 | + peer_counts = [len(pubsub.peers) for pubsub in pubsubs_fsub] |
| 637 | + |
| 638 | + # First and last hosts should have exactly degree connections |
| 639 | + assert peer_counts[0] == degree, ( |
| 640 | + f"Host 0 should have {degree} peers, got {peer_counts[0]}" |
| 641 | + ) |
| 642 | + assert peer_counts[-1] <= degree, ( |
| 643 | + f"Last host should have ≤ {degree} peers, got {peer_counts[-1]}" |
| 644 | + ) |
| 645 | + |
| 646 | + # Middle hosts may have more due to bidirectional connections |
| 647 | + # but the pattern should be consistent with degree limit |
| 648 | + total_connections = sum(peer_counts) |
| 649 | + |
| 650 | + # Should be less than full mesh (each host connected to all others) |
| 651 | + full_mesh_connections = len(hosts) * (len(hosts) - 1) |
| 652 | + assert total_connections < full_mesh_connections, ( |
| 653 | + f"Got {total_connections} total connections, " |
| 654 | + f"but full mesh would be {full_mesh_connections}" |
| 655 | + ) |
| 656 | + |
| 657 | + # Should be more than just a chain (each host connected to next only) |
| 658 | + chain_connections = 2 * (len(hosts) - 1) # bidirectional chain |
| 659 | + assert total_connections > chain_connections, ( |
| 660 | + f"Got {total_connections} total connections, which is too few " |
| 661 | + f"(chain would be {chain_connections})" |
| 662 | + ) |
| 663 | + |
| 664 | + |
| 665 | +@pytest.mark.trio |
| 666 | +async def test_connect_some_degree_zero(): |
| 667 | + """Test edge case: degree=0 should result in no connections.""" |
| 668 | + # Create 5 hosts with degree=0 |
| 669 | + async with PubsubFactory.create_batch_with_floodsub(5) as pubsubs_fsub: |
| 670 | + hosts = [pubsub.host for pubsub in pubsubs_fsub] |
| 671 | + degree = 0 |
| 672 | + |
| 673 | + await connect_some(hosts, degree) |
| 674 | + await trio.sleep(0.1) # Allow any potential connections to establish |
| 675 | + |
| 676 | + # Verify no connections were made |
| 677 | + for i, pubsub in enumerate(pubsubs_fsub): |
| 678 | + connected_peers = len(pubsub.peers) |
| 679 | + assert connected_peers == 0, ( |
| 680 | + f"Host {i} has {connected_peers} connections, " |
| 681 | + f"but degree=0 should result in no connections" |
| 682 | + ) |
| 683 | + |
| 684 | + |
| 685 | +@pytest.mark.trio |
| 686 | +async def test_connect_some_negative_degree(): |
| 687 | + """Test edge case: negative degree should be handled gracefully.""" |
| 688 | + # Create 5 hosts with degree=-1 |
| 689 | + async with PubsubFactory.create_batch_with_floodsub(5) as pubsubs_fsub: |
| 690 | + hosts = [pubsub.host for pubsub in pubsubs_fsub] |
| 691 | + degree = -1 |
| 692 | + |
| 693 | + await connect_some(hosts, degree) |
| 694 | + await trio.sleep(0.1) # Allow any potential connections to establish |
| 695 | + |
| 696 | + # Verify no connections were made (negative degree should behave like 0) |
| 697 | + for i, pubsub in enumerate(pubsubs_fsub): |
| 698 | + connected_peers = len(pubsub.peers) |
| 699 | + assert connected_peers == 0, ( |
| 700 | + f"Host {i} has {connected_peers} connections, " |
| 701 | + f"but negative degree should result in no connections" |
| 702 | + ) |
| 703 | + |
| 704 | + |
| 705 | +@pytest.mark.trio |
| 706 | +async def test_sparse_connect_degree_zero(): |
| 707 | + """Test sparse_connect with degree=0.""" |
| 708 | + async with PubsubFactory.create_batch_with_floodsub(8) as pubsubs_fsub: |
| 709 | + hosts = [pubsub.host for pubsub in pubsubs_fsub] |
| 710 | + degree = 0 |
| 711 | + |
| 712 | + await sparse_connect(hosts, degree) |
| 713 | + await trio.sleep(0.1) # Allow connections to establish |
| 714 | + |
| 715 | + # With degree=0, sparse_connect should still create neighbor connections |
| 716 | + # for connectivity (this is part of the algorithm design) |
| 717 | + for i, pubsub in enumerate(pubsubs_fsub): |
| 718 | + connected_peers = len(pubsub.peers) |
| 719 | + # Should have some connections due to neighbor connectivity |
| 720 | + # (each node connects to immediate neighbors) |
| 721 | + expected_neighbors = 2 # previous and next in ring |
| 722 | + assert connected_peers >= expected_neighbors, ( |
| 723 | + f"Host {i} has {connected_peers} connections, " |
| 724 | + f"expected at least {expected_neighbors} neighbor connections" |
| 725 | + ) |
| 726 | + |
| 727 | + |
| 728 | +@pytest.mark.trio |
| 729 | +async def test_empty_host_list(): |
| 730 | + """Test edge case: empty host list should be handled gracefully.""" |
| 731 | + hosts = [] |
| 732 | + |
| 733 | + # All functions should handle empty lists gracefully |
| 734 | + await connect_some(hosts, 5) |
| 735 | + await sparse_connect(hosts, 3) |
| 736 | + await dense_connect(hosts) |
| 737 | + |
| 738 | + # If we reach here without exceptions, the test passes |
| 739 | + |
| 740 | + |
| 741 | +@pytest.mark.trio |
| 742 | +async def test_single_host(): |
| 743 | + """Test edge case: single host should be handled gracefully.""" |
| 744 | + async with PubsubFactory.create_batch_with_floodsub(1) as pubsubs_fsub: |
| 745 | + hosts = [pubsub.host for pubsub in pubsubs_fsub] |
| 746 | + |
| 747 | + # All functions should handle single host gracefully |
| 748 | + await connect_some(hosts, 5) |
| 749 | + await sparse_connect(hosts, 3) |
| 750 | + await dense_connect(hosts) |
| 751 | + |
| 752 | + # Single host should have no connections |
| 753 | + connected_peers = len(pubsubs_fsub[0].peers) |
| 754 | + assert connected_peers == 0, ( |
| 755 | + f"Single host has {connected_peers} connections, expected 0" |
| 756 | + ) |
0 commit comments