Changeset View
Changeset View
Standalone View
Standalone View
src/avalanche/test/processor_tests.cpp
| Show All 21 Lines | |||||
| #include <avalanche/test/util.h> | #include <avalanche/test/util.h> | ||||
| #include <test/util/setup_common.h> | #include <test/util/setup_common.h> | ||||
| #include <boost/mpl/list.hpp> | #include <boost/mpl/list.hpp> | ||||
| #include <boost/test/unit_test.hpp> | #include <boost/test/unit_test.hpp> | ||||
| #include <functional> | #include <functional> | ||||
| #include <type_traits> | |||||
| #include <vector> | #include <vector> | ||||
| using namespace avalanche; | using namespace avalanche; | ||||
| namespace avalanche { | namespace avalanche { | ||||
| namespace { | namespace { | ||||
| struct AvalancheTest { | struct AvalancheTest { | ||||
| static void runEventLoop(avalanche::Processor &p) { p.runEventLoop(); } | static void runEventLoop(avalanche::Processor &p) { p.runEventLoop(); } | ||||
| ▲ Show 20 Lines • Show All 215 Lines • ▼ Show 20 Lines | std::vector<Vote> buildVotesForItems(uint32_t error, | ||||
| // Votes are sorted by most work first | // Votes are sorted by most work first | ||||
| std::sort(items.begin(), items.end(), CBlockIndexWorkComparator()); | std::sort(items.begin(), items.end(), CBlockIndexWorkComparator()); | ||||
| for (auto &item : reverse_iterate(items)) { | for (auto &item : reverse_iterate(items)) { | ||||
| votes.emplace_back(error, item->GetBlockHash()); | votes.emplace_back(error, item->GetBlockHash()); | ||||
| } | } | ||||
| return votes; | return votes; | ||||
| } | } | ||||
| void invalidateItem(CBlockIndex *pindex) { | |||||
| pindex->nStatus = pindex->nStatus.withFailed(); | |||||
| } | |||||
| }; | }; | ||||
| struct ProofProvider { | struct ProofProvider { | ||||
| AvalancheTestingSetup *fixture; | AvalancheTestingSetup *fixture; | ||||
| std::vector<ProofUpdate> updates; | std::vector<ProofUpdate> updates; | ||||
| uint32_t invType; | uint32_t invType; | ||||
| Show All 20 Lines | bool registerVotes(NodeId nodeid, const avalanche::Response &response, | ||||
| nodeid, response, blockUpdates, updates, banscore, error); | nodeid, response, blockUpdates, updates, banscore, error); | ||||
| } | } | ||||
| bool registerVotes(NodeId nodeid, const avalanche::Response &response) { | bool registerVotes(NodeId nodeid, const avalanche::Response &response) { | ||||
| std::string error; | std::string error; | ||||
| return registerVotes(nodeid, response, error); | return registerVotes(nodeid, response, error); | ||||
| } | } | ||||
| bool addToReconcile(const ProofRef &proof) { | bool addToReconcile(const ProofRef &proof) { | ||||
| fixture->m_processor->addProofToReconcile(proof); | return fixture->m_processor->addProofToReconcile(proof); | ||||
| return true; | |||||
| } | } | ||||
| std::vector<Vote> buildVotesForItems(uint32_t error, | std::vector<Vote> buildVotesForItems(uint32_t error, | ||||
| std::vector<ProofRef> &&items) { | std::vector<ProofRef> &&items) { | ||||
| size_t numItems = items.size(); | size_t numItems = items.size(); | ||||
| std::vector<Vote> votes; | std::vector<Vote> votes; | ||||
| votes.reserve(numItems); | votes.reserve(numItems); | ||||
| // Votes are sorted by high score first | // Votes are sorted by high score first | ||||
| std::sort(items.begin(), items.end(), ProofComparatorByScore()); | std::sort(items.begin(), items.end(), ProofComparatorByScore()); | ||||
| for (auto &item : items) { | for (auto &item : items) { | ||||
| votes.emplace_back(error, item->getId()); | votes.emplace_back(error, item->getId()); | ||||
| } | } | ||||
| return votes; | return votes; | ||||
| } | } | ||||
| void invalidateItem(const ProofRef &proof) { | |||||
| fixture->m_processor->withPeerManager([&](avalanche::PeerManager &pm) { | |||||
| pm.rejectProof(proof->getId(), | |||||
| avalanche::PeerManager::RejectionMode::INVALIDATE); | |||||
| }); | |||||
| } | |||||
| }; | }; | ||||
| } // namespace | } // namespace | ||||
| BOOST_FIXTURE_TEST_SUITE(processor_tests, AvalancheTestingSetup) | BOOST_FIXTURE_TEST_SUITE(processor_tests, AvalancheTestingSetup) | ||||
| // FIXME A std::tuple can be used instead of boost::mpl::list after boost 1.67 | // FIXME A std::tuple can be used instead of boost::mpl::list after boost 1.67 | ||||
| using VoteItemProviders = boost::mpl::list<BlockProvider, ProofProvider>; | using VoteItemProviders = boost::mpl::list<BlockProvider, ProofProvider>; | ||||
| Show All 11 Lines | for (auto s : status) { | ||||
| BlockUpdate abu(pindex, s); | BlockUpdate abu(pindex, s); | ||||
| // The use of BOOST_CHECK instead of BOOST_CHECK_EQUAL prevents from | // The use of BOOST_CHECK instead of BOOST_CHECK_EQUAL prevents from | ||||
| // having to define operator<<() for each argument type. | // having to define operator<<() for each argument type. | ||||
| BOOST_CHECK(abu.getVoteItem() == pindex); | BOOST_CHECK(abu.getVoteItem() == pindex); | ||||
| BOOST_CHECK(abu.getStatus() == s); | BOOST_CHECK(abu.getStatus() == s); | ||||
| } | } | ||||
| } | } | ||||
| BOOST_AUTO_TEST_CASE(block_reconcile_twice) { | BOOST_AUTO_TEST_CASE_TEMPLATE(item_reconcile_twice, P, VoteItemProviders) { | ||||
| CBlock block = CreateAndProcessBlock({}, CScript()); | P provider(this); | ||||
| const BlockHash blockHash = block.GetHash(); | |||||
| CBlockIndex *pindex; | |||||
| { | |||||
| LOCK(cs_main); | |||||
| pindex = g_chainman.m_blockman.LookupBlockIndex(blockHash); | |||||
| } | |||||
| // Adding the block twice does nothing. | auto item = provider.buildVoteItem(); | ||||
| BOOST_CHECK(m_processor->addBlockToReconcile(pindex)); | |||||
| BOOST_CHECK(!m_processor->addBlockToReconcile(pindex)); | // Adding the item twice does nothing. | ||||
| BOOST_CHECK(m_processor->isAccepted(pindex)); | BOOST_CHECK(provider.addToReconcile(item)); | ||||
| BOOST_CHECK(!provider.addToReconcile(item)); | |||||
| BOOST_CHECK(m_processor->isAccepted(item)); | |||||
| } | } | ||||
| BOOST_AUTO_TEST_CASE(block_null) { | BOOST_AUTO_TEST_CASE_TEMPLATE(item_null, P, VoteItemProviders) { | ||||
| P provider(this); | |||||
| // Check that null case is handled on the public interface | // Check that null case is handled on the public interface | ||||
| BOOST_CHECK(!m_processor->isAccepted(nullptr)); | BOOST_CHECK(!m_processor->isAccepted(nullptr)); | ||||
| BOOST_CHECK_EQUAL(m_processor->getConfidence(nullptr), -1); | BOOST_CHECK_EQUAL(m_processor->getConfidence(nullptr), -1); | ||||
| BOOST_CHECK(!m_processor->addBlockToReconcile(nullptr)); | auto item = decltype(provider.buildVoteItem())(); | ||||
| BOOST_CHECK(item == nullptr); | |||||
| BOOST_CHECK(!provider.addToReconcile(item)); | |||||
| // Check that adding blocks to vote on doesn't change the outcome. A | // Check that adding item to vote on doesn't change the outcome. A | ||||
| // comparator is used under the hood, and this is skipped if there are no | // comparator is used under the hood, and this is skipped if there are no | ||||
| // vote records. | // vote records. | ||||
| CBlock block = CreateAndProcessBlock({}, CScript()); | item = provider.buildVoteItem(); | ||||
| const BlockHash blockHash = block.GetHash(); | BOOST_CHECK(provider.addToReconcile(item)); | ||||
| CBlockIndex *pindex; | |||||
| { | |||||
| LOCK(cs_main); | |||||
| pindex = g_chainman.m_blockman.LookupBlockIndex(blockHash); | |||||
| } | |||||
| BOOST_CHECK(m_processor->addBlockToReconcile(pindex)); | |||||
| BOOST_CHECK(!m_processor->isAccepted(nullptr)); | BOOST_CHECK(!m_processor->isAccepted(nullptr)); | ||||
| BOOST_CHECK_EQUAL(m_processor->getConfidence(nullptr), -1); | BOOST_CHECK_EQUAL(m_processor->getConfidence(nullptr), -1); | ||||
| } | } | ||||
| namespace { | namespace { | ||||
| Response next(Response &r) { | Response next(Response &r) { | ||||
| auto copy = r; | auto copy = r; | ||||
| ▲ Show 20 Lines • Show All 355 Lines • ▼ Show 20 Lines | BOOST_AUTO_TEST_CASE_TEMPLATE(poll_and_response, P, VoteItemProviders) { | ||||
| // But they are accepted in order. | // But they are accepted in order. | ||||
| resp = {getRound(), 0, votes}; | resp = {getRound(), 0, votes}; | ||||
| runEventLoop(); | runEventLoop(); | ||||
| BOOST_CHECK(provider.registerVotes(avanodeid, resp)); | BOOST_CHECK(provider.registerVotes(avanodeid, resp)); | ||||
| BOOST_CHECK_EQUAL(updates.size(), 0); | BOOST_CHECK_EQUAL(updates.size(), 0); | ||||
| BOOST_CHECK_EQUAL(getSuitableNodeToQuery(), avanodeid); | BOOST_CHECK_EQUAL(getSuitableNodeToQuery(), avanodeid); | ||||
| } | } | ||||
| BOOST_AUTO_TEST_CASE(dont_poll_invalid_block) { | BOOST_AUTO_TEST_CASE_TEMPLATE(dont_poll_invalid_item, P, VoteItemProviders) { | ||||
| std::vector<BlockUpdate> updates; | P provider(this); | ||||
| auto &updates = provider.updates; | |||||
| const uint32_t invType = provider.invType; | |||||
| CBlock blockA = CreateAndProcessBlock({}, CScript()); | auto itemA = provider.buildVoteItem(); | ||||
| CBlock blockB = CreateAndProcessBlock({}, CScript()); | auto itemB = provider.buildVoteItem(); | ||||
| const BlockHash blockHashA = blockA.GetHash(); | |||||
| const BlockHash blockHashB = blockB.GetHash(); | |||||
| const CBlockIndex *pindexA; | |||||
| CBlockIndex *pindexB; | |||||
| { | |||||
| LOCK(cs_main); | |||||
| pindexA = g_chainman.m_blockman.LookupBlockIndex(blockHashA); | |||||
| pindexB = g_chainman.m_blockman.LookupBlockIndex(blockHashB); | |||||
| } | |||||
| auto avanodes = ConnectNodes(); | auto avanodes = ConnectNodes(); | ||||
| // Register the blocks and check they are added to the list of elements to | // Build votes to get proper ordering | ||||
| std::vector<Vote> votes = provider.buildVotesForItems(0, {itemA, itemB}); | |||||
| // Register the items and check they are added to the list of elements to | |||||
| // poll. | // poll. | ||||
| BOOST_CHECK(m_processor->addBlockToReconcile(pindexA)); | BOOST_CHECK(provider.addToReconcile(itemA)); | ||||
| BOOST_CHECK(m_processor->addBlockToReconcile(pindexB)); | BOOST_CHECK(provider.addToReconcile(itemB)); | ||||
| auto invs = getInvsForNextPoll(); | auto invs = getInvsForNextPoll(); | ||||
| BOOST_CHECK_EQUAL(invs.size(), 2); | BOOST_CHECK_EQUAL(invs.size(), 2); | ||||
| BOOST_CHECK_EQUAL(invs[0].type, MSG_BLOCK); | for (size_t i = 0; i < invs.size(); i++) { | ||||
| BOOST_CHECK(invs[0].hash == blockHashB); | BOOST_CHECK_EQUAL(invs[i].type, invType); | ||||
| BOOST_CHECK_EQUAL(invs[1].type, MSG_BLOCK); | BOOST_CHECK(invs[i].hash == votes[i].GetHash()); | ||||
| BOOST_CHECK(invs[1].hash == blockHashA); | } | ||||
| // When a block is marked invalid, stop polling. | // When an item is marked invalid, stop polling. | ||||
| pindexB->nStatus = pindexB->nStatus.withFailed(); | provider.invalidateItem(itemB); | ||||
sdulfari: After invalidating, it doesn't look like the test is actually checking to see if the polling… | |||||
FabienAuthorUnsubmitted Done Inline Actionsit does indirectly. It it was expecting the itemB being voted on then registerVotes would have returned an error Fabien: it does indirectly. It it was expecting the itemB being voted on then registerVotes would have… | |||||
| Response resp{getRound(), 0, {Vote(0, blockHashA)}}; | Response resp{getRound(), 0, {Vote(0, provider.getVoteItemId(itemA))}}; | ||||
| runEventLoop(); | runEventLoop(); | ||||
| BOOST_CHECK(registerVotes(avanodes[0]->GetId(), resp, updates)); | BOOST_CHECK(provider.registerVotes(avanodes[0]->GetId(), resp)); | ||||
| BOOST_CHECK_EQUAL(updates.size(), 0); | BOOST_CHECK_EQUAL(updates.size(), 0); | ||||
| } | } | ||||
| BOOST_TEST_DECORATOR(*boost::unit_test::timeout(60)) | BOOST_TEST_DECORATOR(*boost::unit_test::timeout(60)) | ||||
| BOOST_AUTO_TEST_CASE_TEMPLATE(poll_inflight_timeout, P, VoteItemProviders) { | BOOST_AUTO_TEST_CASE_TEMPLATE(poll_inflight_timeout, P, VoteItemProviders) { | ||||
| P provider(this); | P provider(this); | ||||
| const auto item = provider.buildVoteItem(); | const auto item = provider.buildVoteItem(); | ||||
| ▲ Show 20 Lines • Show All 261 Lines • ▼ Show 20 Lines | BOOST_AUTO_TEST_CASE(destructor) { | ||||
| schedulerThread.join(); | schedulerThread.join(); | ||||
| } | } | ||||
| BOOST_AUTO_TEST_CASE(add_proof_to_reconcile) { | BOOST_AUTO_TEST_CASE(add_proof_to_reconcile) { | ||||
| uint32_t score = MIN_VALID_PROOF_SCORE; | uint32_t score = MIN_VALID_PROOF_SCORE; | ||||
| auto addProofToReconcile = [&](uint32_t proofScore) { | auto addProofToReconcile = [&](uint32_t proofScore) { | ||||
| auto proof = buildRandomProof(proofScore); | auto proof = buildRandomProof(proofScore); | ||||
| m_processor->addProofToReconcile(proof); | m_processor->withPeerManager([&](avalanche::PeerManager &pm) { | ||||
| BOOST_CHECK(pm.registerProof(proof)); | |||||
| }); | |||||
| BOOST_CHECK(m_processor->addProofToReconcile(proof)); | |||||
| return proof; | return proof; | ||||
| }; | }; | ||||
| for (size_t i = 0; i < AVALANCHE_MAX_ELEMENT_POLL; i++) { | for (size_t i = 0; i < AVALANCHE_MAX_ELEMENT_POLL; i++) { | ||||
| auto proof = addProofToReconcile(++score); | auto proof = addProofToReconcile(++score); | ||||
| auto invs = AvalancheTest::getInvsForNextPoll(*m_processor); | auto invs = AvalancheTest::getInvsForNextPoll(*m_processor); | ||||
| BOOST_CHECK_EQUAL(invs.size(), i + 1); | BOOST_CHECK_EQUAL(invs.size(), i + 1); | ||||
| Show All 28 Lines | BOOST_AUTO_TEST_CASE(add_proof_to_reconcile) { | ||||
| auto proof = addProofToReconcile(--score); | auto proof = addProofToReconcile(--score); | ||||
| auto invs = AvalancheTest::getInvsForNextPoll(*m_processor); | auto invs = AvalancheTest::getInvsForNextPoll(*m_processor); | ||||
| for (auto &inv : invs) { | for (auto &inv : invs) { | ||||
| BOOST_CHECK_NE(inv.hash, proof->getId()); | BOOST_CHECK_NE(inv.hash, proof->getId()); | ||||
| } | } | ||||
| } | } | ||||
| BOOST_AUTO_TEST_CASE(proof_record) { | BOOST_AUTO_TEST_CASE(proof_record) { | ||||
| gArgs.ForceSetArg("-enableavalancheproofreplacement", "1"); | |||||
| gArgs.ForceSetArg("-avalancheconflictingproofcooldown", "0"); | |||||
| BOOST_CHECK(!m_processor->isAccepted(nullptr)); | BOOST_CHECK(!m_processor->isAccepted(nullptr)); | ||||
| BOOST_CHECK_EQUAL(m_processor->getConfidence(nullptr), -1); | BOOST_CHECK_EQUAL(m_processor->getConfidence(nullptr), -1); | ||||
| auto proofA = GetProof(); | const CKey key = CKey::MakeCompressedKey(); | ||||
| auto proofB = GetProof(); | |||||
| BOOST_CHECK(!m_processor->isAccepted(proofA)); | const COutPoint conflictingOutpoint{TxId(GetRandHash()), 0}; | ||||
| BOOST_CHECK(!m_processor->isAccepted(proofB)); | { | ||||
| BOOST_CHECK_EQUAL(m_processor->getConfidence(proofA), -1); | CScript script = GetScriptForDestination(PKHash(key.GetPubKey())); | ||||
| BOOST_CHECK_EQUAL(m_processor->getConfidence(proofB), -1); | |||||
| LOCK(cs_main); | |||||
| m_processor->addProofToReconcile(proofA); | CCoinsViewCache &coins = ::ChainstateActive().CoinsTip(); | ||||
| BOOST_CHECK(!m_processor->isAccepted(proofA)); | coins.AddCoin(conflictingOutpoint, | ||||
| BOOST_CHECK(!m_processor->isAccepted(proofB)); | Coin(CTxOut(10 * COIN, script), 10, false), false); | ||||
| BOOST_CHECK_EQUAL(m_processor->getConfidence(proofA), 0); | } | ||||
| BOOST_CHECK_EQUAL(m_processor->getConfidence(proofB), -1); | const COutPoint missingOutpoint{TxId(GetRandHash()), 0}; | ||||
| auto buildProof = [&](uint64_t sequence, const COutPoint &outpoint) { | |||||
sdulfariUnsubmitted Not Done Inline Actionsnit: other tests organize buildProof args as (outpoint, sequence) sdulfari: nit: other tests organize buildProof args as (outpoint, sequence) | |||||
| ProofBuilder pb(sequence, 0, key); | |||||
| BOOST_CHECK(pb.addUTXO(outpoint, 10 * COIN, 10, false, key)); | |||||
| return pb.build(); | |||||
| }; | |||||
| auto conflictingProof = buildProof(1, conflictingOutpoint); | |||||
| auto validProof = buildProof(2, conflictingOutpoint); | |||||
| auto orphanProof = buildProof(3, missingOutpoint); | |||||
| ; | |||||
sdulfariUnsubmitted Not Done Inline Actionsremove sdulfari: remove | |||||
| BOOST_CHECK(!m_processor->isAccepted(conflictingProof)); | |||||
| BOOST_CHECK(!m_processor->isAccepted(validProof)); | |||||
| BOOST_CHECK(!m_processor->isAccepted(orphanProof)); | |||||
| BOOST_CHECK_EQUAL(m_processor->getConfidence(conflictingProof), -1); | |||||
| BOOST_CHECK_EQUAL(m_processor->getConfidence(validProof), -1); | |||||
| BOOST_CHECK_EQUAL(m_processor->getConfidence(orphanProof), -1); | |||||
| // Registering proofs that don't exist will fail | |||||
sdulfariUnsubmitted Not Done Inline ActionsThis isn't registering proofs, but attempting to poll sdulfari: This isn't registering proofs, but attempting to poll | |||||
| BOOST_CHECK(!m_processor->addProofToReconcile(conflictingProof)); | |||||
| BOOST_CHECK(!m_processor->addProofToReconcile(validProof)); | |||||
| BOOST_CHECK(!m_processor->addProofToReconcile(orphanProof)); | |||||
| m_processor->withPeerManager([&](avalanche::PeerManager &pm) { | m_processor->withPeerManager([&](avalanche::PeerManager &pm) { | ||||
| BOOST_CHECK(pm.registerProof(proofB)); | ProofRegistrationState state; | ||||
| BOOST_CHECK(pm.registerProof(conflictingProof)); | |||||
| BOOST_CHECK(pm.registerProof(validProof, state)); | |||||
sdulfariUnsubmitted Not Done Inline ActionsDid you intend to check the state? sdulfari: Did you intend to check the state? | |||||
FabienAuthorUnsubmitted Done Inline ActionsA debugging leftover that helped me figure out that I needed to disable the replacement cooldown Fabien: A debugging leftover that helped me figure out that I needed to disable the replacement cooldown | |||||
| BOOST_CHECK(!pm.registerProof(orphanProof)); | |||||
| BOOST_CHECK(pm.isBoundToPeer(validProof->getId())); | |||||
| BOOST_CHECK(pm.isInConflictingPool(conflictingProof->getId())); | |||||
| BOOST_CHECK(pm.isOrphan(orphanProof->getId())); | |||||
| }); | }); | ||||
| m_processor->addProofToReconcile(proofB); | |||||
| BOOST_CHECK(!m_processor->isAccepted(proofA)); | BOOST_CHECK(m_processor->addProofToReconcile(conflictingProof)); | ||||
| BOOST_CHECK(m_processor->isAccepted(proofB)); | BOOST_CHECK(!m_processor->isAccepted(conflictingProof)); | ||||
| BOOST_CHECK_EQUAL(m_processor->getConfidence(proofA), 0); | BOOST_CHECK(!m_processor->isAccepted(validProof)); | ||||
| BOOST_CHECK_EQUAL(m_processor->getConfidence(proofB), 0); | BOOST_CHECK(!m_processor->isAccepted(orphanProof)); | ||||
| BOOST_CHECK_EQUAL(m_processor->getConfidence(conflictingProof), 0); | |||||
| BOOST_CHECK_EQUAL(m_processor->getConfidence(validProof), -1); | |||||
| BOOST_CHECK_EQUAL(m_processor->getConfidence(orphanProof), -1); | |||||
| BOOST_CHECK(m_processor->addProofToReconcile(validProof)); | |||||
| BOOST_CHECK(!m_processor->isAccepted(conflictingProof)); | |||||
| BOOST_CHECK(m_processor->isAccepted(validProof)); | |||||
| BOOST_CHECK(!m_processor->isAccepted(orphanProof)); | |||||
| BOOST_CHECK_EQUAL(m_processor->getConfidence(conflictingProof), 0); | |||||
| BOOST_CHECK_EQUAL(m_processor->getConfidence(validProof), 0); | |||||
| BOOST_CHECK_EQUAL(m_processor->getConfidence(orphanProof), -1); | |||||
| BOOST_CHECK(!m_processor->addProofToReconcile(orphanProof)); | |||||
| BOOST_CHECK(!m_processor->isAccepted(conflictingProof)); | |||||
| BOOST_CHECK(m_processor->isAccepted(validProof)); | |||||
| BOOST_CHECK(!m_processor->isAccepted(orphanProof)); | |||||
| BOOST_CHECK_EQUAL(m_processor->getConfidence(conflictingProof), 0); | |||||
| BOOST_CHECK_EQUAL(m_processor->getConfidence(validProof), 0); | |||||
| BOOST_CHECK_EQUAL(m_processor->getConfidence(orphanProof), -1); | |||||
| gArgs.ClearForcedArg("-avalancheconflictingproofcooldown"); | |||||
| gArgs.ClearForcedArg("-enableavalancheproofreplacement"); | |||||
| } | } | ||||
| BOOST_AUTO_TEST_CASE(quorum_detection) { | BOOST_AUTO_TEST_CASE(quorum_detection) { | ||||
| // Set min quorum parameters for our test | // Set min quorum parameters for our test | ||||
| int minStake = 4'000'000; | int minStake = 4'000'000; | ||||
| gArgs.ForceSetArg("-avaminquorumstake", ToString(minStake)); | gArgs.ForceSetArg("-avaminquorumstake", ToString(minStake)); | ||||
| gArgs.ForceSetArg("-avaminquorumconnectedstakeratio", "0.5"); | gArgs.ForceSetArg("-avaminquorumconnectedstakeratio", "0.5"); | ||||
| ▲ Show 20 Lines • Show All 318 Lines • Show Last 20 Lines | |||||
After invalidating, it doesn't look like the test is actually checking to see if the polling for itemB stopped. Only that further votes on itemA succeed. In the least, shouldn't this check that votes on itemB fail?