fbas_analyzer 0.7.4

Library and tools for analyzing FBASs like the Stellar network
Documentation
#!/usr/bin/env python3

import subprocess
import tempfile


def main():

    cargo_debug_build()
    cargo_test()
    test_example()

    cargo_build()
    test_fbas_analyzer()
    test_bulk_fbas_analyzer()
    test_qsc_simulator()

    print("All tests completed successfully!")


def cargo_test():
    run_and_check_return('cargo test --no-default-features', 'Running unit tests with minimal feature set')
    run_and_check_return('cargo test --all-features', 'Running unit tests with full feature set')
    run_and_check_return('cargo test -- --ignored', 'Running slow unit tests')


def cargo_build():
    run_and_check_return('cargo build --release --all-features', 'Building project to make sure we have up-to-date binaries')


def cargo_debug_build():
    run_and_check_return('cargo build', 'Building project to make sure we have up-to-date debug binaries for some tests')


def test_example():
    command = "cargo run --release --example results_reuse"
    expected_strings = [
        'c6602f930734bf9eb3dd35387aa0e8d0a31438ef57dbb4745e3ddfe6acf2b073', # standard form hash after reducing to core and removing inactive nodes
        'GA35T3723UP2XJLC2H7MNL6VMKZZIFL2VW7XHMFFJKKIA2FJCYTLKFBW', # a minimal blocking set member
    ]
    run_and_check_output(command, expected_strings=expected_strings)


def test_fbas_analyzer():
    test_fbas_analyzer_with_ids()
    test_fbas_analyzer_on_broken()
    test_fbas_analyzer_on_mobilecoin()
    test_fbas_analyzer_with_organizations()


def test_fbas_analyzer_with_organizations():
    command = "target/release/fbas_analyzer test_data/stellarbeat_nodes_2019-09-17.json --merge-by-org test_data/stellarbeat_organizations_2019-09-17.json -a -p -S --only-core-nodes"
    expected_strings = [
        'has_quorum_intersection: true',
        'minimal_quorums: [["Stellar Development Foundation","LOBSTR","SatoshiPay","COINQVEST Limited"],["Stellar Development Foundation","LOBSTR","SatoshiPay","Keybase"],["Stellar Development Foundation","LOBSTR","COINQVEST Limited","Keybase"],["Stellar Development Foundation","SatoshiPay","COINQVEST Limited","Keybase"],["LOBSTR","SatoshiPay","COINQVEST Limited","Keybase"]]',
        'minimal_blocking_sets: [["Stellar Development Foundation","LOBSTR"],["Stellar Development Foundation","SatoshiPay"],["Stellar Development Foundation","COINQVEST Limited"],["Stellar Development Foundation","Keybase"],["LOBSTR","SatoshiPay"],["LOBSTR","COINQVEST Limited"],["LOBSTR","Keybase"],["SatoshiPay","COINQVEST Limited"],["SatoshiPay","Keybase"],["COINQVEST Limited","Keybase"]]',
        'minimal_splitting_sets_with_affected_quorums:',
        '- ["Stellar Development Foundation","LOBSTR","COINQVEST Limited"]: [["Stellar Development Foundation","LOBSTR","SatoshiPay","COINQVEST Limited"],["Stellar Development Foundation","LOBSTR","COINQVEST Limited","Keybase"]]',
        'top_tier: ["Stellar Development Foundation","LOBSTR","SatoshiPay","COINQVEST Limited","Keybase"]',
        ]
    run_and_check_output(command, expected_strings=expected_strings)


def test_fbas_analyzer_with_ids():
    command = "target/release/fbas_analyzer test_data/stellarbeat_nodes_2019-09-17.json -q"
    expected_strings = [
        'top_tier: [1,4,8,23,29,36,37,43,44,52,56,69,86,105,167,168,171]',
    ]
    run_and_check_output(command, expected_strings=expected_strings)


def test_fbas_analyzer_on_broken():
    command = "target/release/fbas_analyzer test_data/broken.json -a"
    expected_strings = [
        'has_quorum_intersection: false',
        'minimal_blocking_sets: [[3,4],[4,10],[3,6,10]]',
        'minimal_splitting_sets: [[]]',
        'top_tier: [3,4,6,10]',
    ]
    run_and_check_output(command, expected_strings=expected_strings)


def test_fbas_analyzer_on_mobilecoin():
    command = "target/release/fbas_analyzer test_data/mobilecoin_nodes_2021-10-22.json"
    expected_strings = [
        'symmetric_clusters: [{"threshold":8,"validators":[0,1,2,3,4,5,6,7,8,9]}]',
    ]
    run_and_check_output(command, expected_strings=expected_strings)


def test_bulk_fbas_analyzer():
    test_bulk_fbas_analyzer_to_stdout()
    test_bulk_fbas_analyzer_update_flag()


def test_bulk_fbas_analyzer_to_stdout():
    input_files = ['test_data/' + x for x in [
        'broken.json',
        'correct.json',
        'stellarbeat_nodes_2019-09-17.json',
        'stellarbeat_nodes_2020-01-16_broken_by_hand.json',
        'stellarbeat_organizations_2019-09-17.json',
    ]]
    command = 'target/release/bulk_fbas_analyzer --only-core-nodes ' + ' '.join(input_files)

    expected_strings = [
        'label,has_quorum_intersection,top_tier_size,mbs_min,mbs_max,mbs_mean,mss_min,mss_max,mss_mean,mq_min,mq_max,mq_mean,orgs_top_tier_size,orgs_mbs_min,orgs_mbs_max,orgs_mbs_mean,orgs_mss_min,orgs_mss_max,orgs_mss_mean,orgs_mq_min,orgs_mq_max,orgs_mq_mean,isps_top_tier_size,isps_mbs_min,isps_mbs_max,isps_mbs_mean,isps_mss_min,isps_mss_max,isps_mss_mean,isps_mq_min,isps_mq_max,isps_mq_mean,ctries_top_tier_size,ctries_mbs_min,ctries_mbs_max,ctries_mbs_mean,ctries_mss_min,ctries_mss_max,ctries_mss_mean,ctries_mq_min,ctries_mq_max,ctries_mq_mean,standard_form_hash,analysis_duration_mq,analysis_duration_mbs,analysis_duration_mss,analysis_duration_total',
        'broken,false,4,2,3',
        'correct,true,3,2,2,2.0,1,1,1.0,2,2,2.0,,,,,,,,,,,',
        '2019-09-17,true,17,4,5,4.689655172413793,3,3,3.0,8,9,8.930232558139535,5,2,2,2.0,3,3,3.0,4,4,4.0,,,,,,,,,,,3,1,1,1.0,1,1,1.0,1,1,1.0,6f73c7787f38fdde66470cc3b2e469e092c70f52823396ae13e52c9a561b20f5,0.',
        '2020-01-16_broken_by_hand,false,22,5,6,5.625,0,0,0.0,2,11,10.9413',
        ]
    run_and_check_output(command, expected_strings=expected_strings)

def test_bulk_fbas_analyzer_update_flag():
    input_files = ['test_data/' + x for x in [
        'broken.json',
        'correct.json',
        'stellarbeat_nodes_2020-01-16_broken_by_hand.json',
    ]]
    update_files = ['test_data/' + x for x in [
        'broken.json',
        'correct.json',
        'stellarbeat_nodes_2019-09-17.json',
        'stellarbeat_nodes_2020-01-16_broken_by_hand.json',
        'stellarbeat_organizations_2019-09-17.json',
    ]]
    tf = tempfile.NamedTemporaryFile('r+')
    daily_csv = tf.name

    command = 'target/release/bulk_fbas_analyzer --only-core-nodes ' + ' '.join(input_files)
    run_redirect_stdout_to_file_and_check_return(command, tf)

    command = 'target/release/bulk_fbas_analyzer --only-core-nodes ' + ' '.join(update_files) + ' -u -o ' + daily_csv
    expected_strings  = [
        'label,has_quorum_intersection,top_tier_size,mbs_min,mbs_max,mbs_mean,mss_min,mss_max,mss_mean,mq_min,mq_max,mq_mean,orgs_top_tier_size,orgs_mbs_min,orgs_mbs_max,orgs_mbs_mean,orgs_mss_min,orgs_mss_max,orgs_mss_mean,orgs_mq_min,orgs_mq_max,orgs_mq_mean,isps_top_tier_size,isps_mbs_min,isps_mbs_max,isps_mbs_mean,isps_mss_min,isps_mss_max,isps_mss_mean,isps_mq_min,isps_mq_max,isps_mq_mean,ctries_top_tier_size,ctries_mbs_min,ctries_mbs_max,ctries_mbs_mean,ctries_mss_min,ctries_mss_max,ctries_mss_mean,ctries_mq_min,ctries_mq_max,ctries_mq_mean,standard_form_hash,analysis_duration_mq,analysis_duration_mbs,analysis_duration_mss,analysis_duration_total',
        'broken,false,4,2,3',
        'correct,true,3,2,2,2.0,1,1,1.0,2,2,2.0,,,,,,,,,,,',
        '2020-01-16_broken_by_hand,false,22,5,6,5.625,0,0,0.0,2,11,10.9413',
        '2019-09-17,true,17,4,5,4.689655172413793,3,3,3.0,8,9,8.930232558139535,5,2,2,2.0,3,3,3.0,4,4,4.0,,,,,,,,,,,3,1,1,1.0,1,1,1.0,1,1,1.0,6f73c7787f38fdde66470cc3b2e469e092c70f52823396ae13e52c9a561b20f5,0.',
        ]
    run_redirect_stdout_to_file_and_check_output(command, tf, expected_strings=expected_strings)
    tf.close()

def test_qsc_simulator():
    graph = '0|1|0\n0|2|0\n1|0|0\n1|2|0\n2|0|0\n2|1|0'
    command = 'target/release/qsc_simulator AllNeighbors -'

    expected = '\n'.join([
        '[',
        '  {',
        '    "publicKey": "n0",',
        '    "quorumSet": {',
        '      "threshold": 3,',
        '      "validators": [',
        '        "n0",',
        '        "n1",',
        '        "n2"',
        '      ]',
        '    }',
        '  },',
        '  {',
        '    "publicKey": "n1",',
        '    "quorumSet": {',
        '      "threshold": 3,',
        '      "validators": [',
        '        "n0",',
        '        "n1",',
        '        "n2"',
        '      ]',
        '    }',
        '  },',
        '  {',
        '    "publicKey": "n2",',
        '    "quorumSet": {',
        '      "threshold": 3,',
        '      "validators": [',
        '        "n0",',
        '        "n1",',
        '        "n2"',
        '      ]',
        '    }',
        '  }',
        ']',
        ])

    run_and_check_output(command, expected_strings=[expected], stdin=graph)


def run_and_check_return(command, log_message, expected_returncode=0):
    print("%s: `%s`" % (log_message, command))
    completed_process = subprocess.run(command, shell=True)
    assert completed_process.returncode == expected_returncode,\
        "Expected return code '%d', got '%d'." % (expected_returncode, completed_process.returncode)


def run_and_check_output(command, log_message='Running command', expected_strings=[], stdin=''):
    print("%s: `%s`" % (log_message, command))
    if stdin:
        print("Feeding in via STDIN:\n'''\n%s\n'''" % stdin)
    completed_process = subprocess.run(command, input=stdin, capture_output=True, universal_newlines=True, shell=True)
    stdout = completed_process.stdout
    stderr = completed_process.stderr

    print("Checking output for expected strings...")
    for expected in expected_strings:
        assert expected in stdout, '\n'.join([
            "Missing expected output string:",
            "'''",
            expected,
            "'''",
            "Full output:",
            "'''",
            stdout + "'''",
            "STDERR: '%s'" % stderr,
        ])

def run_redirect_stdout_to_file_and_check_return(command, file_descriptor, expected_returncode=0):
    completed_process = subprocess.run(command, universal_newlines=True, shell=True, stdout=file_descriptor)
    assert completed_process.returncode == expected_returncode,\
        "Expected return code '%d', got '%d'." % (expected_returncode, completed_process.returncode)

def run_redirect_stdout_to_file_and_check_output(command, file_descriptor, log_message='Running command', expected_strings=[]):
    print("%s: `%s`" % (log_message, command))
    completed_process = subprocess.run(command, universal_newlines=True, shell=True, stdout=file_descriptor)
    stderr = completed_process.stderr

    file_descriptor.seek(0)
    actual_strings = file_descriptor.read()
    print("Checking output for expected strings...")
    for expected in expected_strings:
        assert expected in actual_strings, '\n'.join([
            "Missing expected output string:",
            "'''",
            expected,
            "'''",
            "Full output:",
            "'''",
            str(actual_strings) + "\n" + "'''"
            "STDERR: '%s'" % stderr,
        ])

if __name__ == "__main__":
    main()