//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2012, 2013 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#if 0
namespace ripple {
namespace PeerFinder {
namespace Sim {
class Link;
class Message;
class Network;
class Node;
// Maybe this should be std::set
typedef std::list Links;
//------------------------------------------------------------------------------
class Network
{
public:
typedef std::list Peers;
typedef hash_map > Table;
explicit Network (Params const& params,
Journal journal = Journal());
~Network ();
Params const& params() const { return m_params; }
void prepare ();
Journal journal () const;
int next_node_id ();
clock_type::time_point now ();
Peers& nodes();
Peers const& nodes() const;
Node* find (IP::Endpoint const& address);
void step ();
template
void post (Function f)
{ m_queue.post (f); }
private:
Params m_params;
Journal m_journal;
int m_next_node_id;
manual_clock m_clock;
Peers m_nodes;
Table m_table;
FunctionQueue m_queue;
};
//------------------------------------------------------------------------------
class Node;
// Represents a link between two peers.
// The link holds the messages the local node will receive.
//
class Link
{
public:
typedef std::vector Messages;
Link (
Node& local_node,
SlotImp::ptr const& slot,
IP::Endpoint const& local_endpoint,
Node& remote_node,
IP::Endpoint const& remote_endpoint,
bool inbound)
: m_local_node (&local_node)
, m_slot (slot)
, m_local_endpoint (local_endpoint)
, m_remote_node (&remote_node)
, m_remote_endpoint (remote_endpoint)
, m_inbound (inbound)
, m_closed (false)
{
}
// Indicates that the remote closed their end
bool closed () const { return m_closed; }
bool inbound () const { return m_inbound; }
bool outbound () const { return ! m_inbound; }
IP::Endpoint const& remote_endpoint() const { return m_remote_endpoint; }
IP::Endpoint const& local_endpoint() const { return m_local_endpoint; }
SlotImp::ptr const& slot () const { return m_slot; }
Node& remote_node () { return *m_remote_node; }
Node const& remote_node () const { return *m_remote_node; }
Node& local_node () { return *m_local_node; }
Node const& local_node () const { return *m_local_node; }
void post (Message const& m)
{
m_pending.push_back (m);
}
bool pending () const
{
return m_pending.size() > 0;
}
void close ()
{
m_closed = true;
}
void pre_step ()
{
std::swap (m_current, m_pending);
}
void step ();
private:
Node* m_local_node;
SlotImp::ptr m_slot;
IP::Endpoint m_local_endpoint;
Node* m_remote_node;
IP::Endpoint m_remote_endpoint;
bool m_inbound;
bool m_closed;
Messages m_current;
Messages m_pending;
};
//--------------------------------------------------------------------------
class Node
: public Callback
, public Store
, public Checker
{
private:
typedef std::vector SavedBootstrapAddresses;
public:
struct Config
{
Config ()
: canAccept (true)
{
}
bool canAccept;
IP::Endpoint listening_endpoint;
IP::Endpoint well_known_endpoint;
PeerFinder::Config config;
};
Links m_links;
std::vector m_livecache_history;
Node (
Network& network,
Config const& config,
clock_type& clock,
Journal journal)
: m_network (network)
, m_id (network.next_node_id())
, m_config (config)
, m_node_id (RipplePublicKey::createFromInteger (m_id))
, m_sink (prefix(), journal.sink())
, m_journal (Journal (m_sink, journal.severity()), Reporting::node)
, m_next_port (m_config.listening_endpoint.port() + 1)
, m_logic (boost::in_place (
std::ref (clock), std::ref (*this), std::ref (*this), std::ref (*this), m_journal))
, m_when_expire (m_network.now() + std::chrono::seconds (1))
{
logic().setConfig (m_config.config);
logic().load ();
}
~Node ()
{
// Have to destroy the logic early because it calls back into us
m_logic = boost::none;
}
void dump (Journal::ScopedStream& ss) const
{
ss << listening_endpoint();
logic().dump (ss);
}
Links& links()
{
return m_links;
}
Links const& links() const
{
return m_links;
}
int id () const
{
return m_id;
}
RipplePublicKey const& node_id () const
{
return m_node_id;
}
Logic& logic ()
{
return m_logic.get();
}
Logic const& logic () const
{
return m_logic.get();
}
IP::Endpoint const& listening_endpoint () const
{
return m_config.listening_endpoint;
}
bool canAccept () const
{
return m_config.canAccept;
}
void receive (Link const& c, Message const& m)
{
logic().on_endpoints (c.slot (), m.payload());
}
void pre_step ()
{
for (Links::iterator iter (links().begin());
iter != links().end();)
{
Links::iterator cur (iter++);
cur->pre_step ();
}
}
void step ()
{
for (Links::iterator iter (links().begin());
iter != links().end();)
{
Links::iterator cur (iter++);
//Link& link (*cur);
cur->step ();
#if 0
if (iter->closed ())
{
// Post notification?
iter->local_node().logic().on_closed (
iter->remote_endpoint());
iter = links().erase (iter);
}
else
#endif
}
logic().makeOutgoingConnections ();
logic().sendEndpoints ();
if (m_network.now() >= m_when_expire)
{
logic().expire();
m_when_expire = m_network.now() + std::chrono::seconds (1);
}
m_livecache_history.emplace_back (
logic().state().livecache.histogram());
logic().periodicActivity();
}
//----------------------------------------------------------------------
//
// Callback
//
//----------------------------------------------------------------------
void sendEndpoints (IP::Endpoint const& remote_endpoint,
Endpoints const& endpoints)
{
m_network.post (std::bind (&Node::doSendEndpoints, this,
remote_endpoint, endpoints));
}
void connectPeers (IPAddresses const& addresses)
{
m_network.post (std::bind (&Node::doConnectPeers, this,
addresses));
}
void disconnectPeer (IP::Endpoint const& remote_endpoint, bool graceful)
{
m_network.post (std::bind (&Node::doDisconnectPeer, this,
remote_endpoint, graceful));
}
void activatePeer (IP::Endpoint const& remote_endpoint)
{
/* no underlying peer to activate */
}
void doSendEndpoints (IP::Endpoint const& remote_endpoint,
Endpoints const& endpoints)
{
Links::iterator const iter1 (std::find_if (
links().begin (), links().end(),
is_remote_endpoint (remote_endpoint)));
if (iter1 != links().end())
{
// Drop the message if they closed their end
if (iter1->closed ())
return;
Node& remote_node (iter1->remote_node());
// Find their link to us
Links::iterator const iter2 (std::find_if (
remote_node.links().begin(), remote_node.links().end(),
is_remote_endpoint (iter1->local_endpoint ())));
consistency_check (iter2 != remote_node.links().end());
//
// VFALCO NOTE This looks wrong! Shouldn't it call receive()
// on the link and not the Peer?
//
Message const m (endpoints);
iter2->local_node().receive (*iter2, m);
//iter2->post (m);
}
}
void doCheckAccept (Node& remote_node, IP::Endpoint const& remote_endpoint)
{
// Find our link to the remote node
Links::iterator iter (std::find_if (m_links.begin (),
m_links.end(), is_remote_endpoint (remote_endpoint)));
// See if the logic closed the connection
if (iter == m_links.end())
return;
// Post notifications
m_network.post (std::bind (&Logic::on_handshake,
&remote_node.logic(), iter->local_endpoint(), node_id(), false));
m_network.post (std::bind (&Logic::on_handshake,
&logic(), remote_endpoint, remote_node.node_id(), false));
}
void doConnectPeers (IPAddresses const& addresses)
{
for (IPAddresses::const_iterator iter (addresses.begin());
iter != addresses.end(); ++iter)
{
IP::Endpoint const& remote_endpoint (*iter);
Node* const remote_node (m_network.find (remote_endpoint));
// Acquire slot
Slot::ptr const local_slot (
m_logic->new_outbound_slot (remote_endpoint));
if (! local_slot)
continue;
// See if the address is connectible
if (remote_node == nullptr || ! remote_node->canAccept())
{
// Firewalled or no one listening
// Post notification
m_network.post (std::bind (&Logic::on_closed,
&logic(), local_slot));
continue;
}
IP::Endpoint const local_endpoint (
listening_endpoint().at_port (m_next_port++));
// Acquire slot
Slot::ptr const remote_slot (
remote_node->logic().new_inbound_slot (
remote_endpoint, local_endpoint));
if (! remote_slot)
continue;
// Connection established, create links
m_links.emplace_back (*this, local_slot, local_endpoint,
*remote_node, remote_endpoint, false);
remote_node->m_links.emplace_back (*remote_node, remote_slot,
remote_endpoint, *this, local_endpoint, true);
// Post notifications
m_network.post (std::bind (&Logic::on_connected,
&logic(), local_endpoint, remote_endpoint));
m_network.post (std::bind (&Node::doCheckAccept,
remote_node, std::ref (*this), local_endpoint));
}
}
void doClosed (IP::Endpoint const& remote_endpoint, bool graceful)
{
// Find our link to them
Links::iterator const iter (std::find_if (
m_links.begin(), m_links.end(),
is_remote_endpoint (remote_endpoint)));
// Must be connected!
check_invariant (iter != m_links.end());
// Must be closed!
check_invariant (iter->closed());
// Remove our link to them
m_links.erase (iter);
// Notify
m_network.post (std::bind (&Logic::on_closed,
&logic(), remote_endpoint));
}
void doDisconnectPeer (IP::Endpoint const& remote_endpoint, bool graceful)
{
// Find our link to them
Links::iterator const iter1 (std::find_if (
m_links.begin(), m_links.end(),
is_remote_endpoint (remote_endpoint)));
if (iter1 == m_links.end())
return;
Node& remote_node (iter1->remote_node());
IP::Endpoint const local_endpoint (iter1->local_endpoint());
// Find their link to us
Links::iterator const iter2 (std::find_if (
remote_node.links().begin(), remote_node.links().end(),
is_remote_endpoint (local_endpoint)));
if (iter2 != remote_node.links().end())
{
// Notify the remote that we closed
check_invariant (! iter2->closed());
iter2->close();
m_network.post (std::bind (&Node::doClosed,
&remote_node, local_endpoint, graceful));
}
if (! iter1->closed ())
{
// Remove our link to them
m_links.erase (iter1);
// Notify
m_network.post (std::bind (&Logic::on_closed,
&logic(), remote_endpoint));
}
/*
if (! graceful || ! iter2->pending ())
{
remote_node.links().erase (iter2);
remote_node.logic().on_closed (local_endpoint);
}
*/
}
//----------------------------------------------------------------------
//
// Store
//
//----------------------------------------------------------------------
std::vector loadBootstrapCache ()
{
std::vector result;
SavedBootstrapAddress item;
item.address = m_config.well_known_endpoint;
item.cumulativeUptime = std::chrono::seconds (0);
item.connectionValence = 0;
result.push_back (item);
return result;
}
void updateBootstrapCache (
std::vector const& list)
{
m_bootstrap_cache = list;
}
//
// Checker
//
void cancel ()
{
}
void async_connect (IP::Endpoint const& address,
asio::shared_handler handler)
{
Node* const node (m_network.find (address));
Checker::Result result;
result.address = address;
if (node != nullptr)
result.canAccept = node->canAccept();
else
result.canAccept = false;
handler (result);
}
private:
std::string prefix()
{
int const width (5);
std::stringstream ss;
ss << "#" << m_id << " ";
std::string s (ss.str());
s.insert (0, std::max (
0, width - int(s.size())), ' ');
return s;
}
Network& m_network;
int const m_id;
Config const m_config;
RipplePublicKey m_node_id;
WrappedSink m_sink;
Journal m_journal;
IP::Port m_next_port;
boost::optional m_logic;
clock_type::time_point m_when_expire;
SavedBootstrapAddresses m_bootstrap_cache;
};
//------------------------------------------------------------------------------
void Link::step ()
{
for (Messages::const_iterator iter (m_current.begin());
iter != m_current.end(); ++iter)
m_local_node->receive (*this, *iter);
m_current.clear();
}
//------------------------------------------------------------------------------
static IP::Endpoint next_endpoint (IP::Endpoint address)
{
if (address.is_v4())
{
do
{
address = IP::Endpoint (IP::AddressV4 (
address.to_v4().value + 1)).at_port (address.port());
}
while (! is_public (address));
return address;
}
bassert (address.is_v6());
// unimplemented
bassertfalse;
return IP::Endpoint();
}
Network::Network (
Params const& params,
Journal journal)
: m_params (params)
, m_journal (journal)
, m_next_node_id (1)
{
}
void Network::prepare ()
{
IP::Endpoint const well_known_endpoint (
IP::Endpoint::from_string ("1.0.0.1").at_port (1));
IP::Endpoint address (well_known_endpoint);
for (int i = 0; i < params().nodes; ++i )
{
if (i == 0)
{
Node::Config config;
config.canAccept = true;
config.listening_endpoint = address;
config.well_known_endpoint = well_known_endpoint;
config.config.maxPeers = params().maxPeers;
config.config.outPeers = params().outPeers;
config.config.wantIncoming = true;
config.config.autoConnect = true;
config.config.listeningPort = address.port();
m_nodes.emplace_back (
*this,
config,
m_clock,
m_journal);
m_table.emplace (address, std::ref (m_nodes.back()));
address = next_endpoint (address);
}
if (i != 0)
{
Node::Config config;
config.canAccept = Random::getSystemRandom().nextInt (100) >=
(m_params.firewalled * 100);
config.listening_endpoint = address;
config.well_known_endpoint = well_known_endpoint;
config.config.maxPeers = params().maxPeers;
config.config.outPeers = params().outPeers;
config.config.wantIncoming = true;
config.config.autoConnect = true;
config.config.listeningPort = address.port();
m_nodes.emplace_back (
*this,
config,
m_clock,
m_journal);
m_table.emplace (address, std::ref (m_nodes.back()));
address = next_endpoint (address);
}
}
}
Network::~Network ()
{
}
Journal Network::journal () const
{
return m_journal;
}
int Network::next_node_id ()
{
return m_next_node_id++;
}
clock_type::time_point Network::now ()
{
return m_clock.now();
}
Network::Peers& Network::nodes()
{
return m_nodes;
}
#if 0
Network::Peers const& Network::nodes() const
{
return m_nodes;
}
#endif
Node* Network::find (IP::Endpoint const& address)
{
Table::iterator iter (m_table.find (address));
if (iter != m_table.end())
return iter->second.get_pointer();
return nullptr;
}
void Network::step ()
{
for (Peers::iterator iter (m_nodes.begin());
iter != m_nodes.end();)
(iter++)->pre_step();
for (Peers::iterator iter (m_nodes.begin());
iter != m_nodes.end();)
(iter++)->step();
m_queue.run ();
// Advance the manual clock so that
// messages are broadcast at every step.
//
//m_clock += Tuning::secondsPerConnect;
++m_clock;
}
//------------------------------------------------------------------------------
template <>
struct VertexTraits
{
typedef Links Edges;
typedef Link Edge;
static Edges& edges (Node& node)
{ return node.links(); }
static Node* vertex (Link& l)
{ return &l.remote_node(); }
};
//------------------------------------------------------------------------------
struct PeerStats
{
PeerStats ()
: inboundActive (0)
, out_active (0)
, inboundSlotsFree (0)
, outboundSlotsFree (0)
{
}
template
explicit PeerStats (Peer const& peer)
{
inboundActive = peer.logic().counts().inboundActive();
out_active = peer.logic().counts().out_active();
inboundSlotsFree = peer.logic().counts().inboundSlotsFree();
outboundSlotsFree = peer.logic().counts().outboundSlotsFree();
}
PeerStats& operator+= (PeerStats const& rhs)
{
inboundActive += rhs.inboundActive;
out_active += rhs.out_active;
inboundSlotsFree += rhs.inboundSlotsFree;
outboundSlotsFree += rhs.outboundSlotsFree;
return *this;
}
int totalActive () const
{ return inboundActive + out_active; }
int inboundActive;
int out_active;
int inboundSlotsFree;
int outboundSlotsFree;
};
//------------------------------------------------------------------------------
inline PeerStats operator+ (PeerStats const& lhs, PeerStats& rhs)
{
PeerStats result (lhs);
result += rhs;
return result;
}
//------------------------------------------------------------------------------
/** Aggregates statistics on the connected network. */
class CrawlState
{
public:
explicit CrawlState (std::size_t step)
: m_step (step)
, m_size (0)
, m_diameter (0)
{
}
std::size_t step () const
{ return m_step; }
std::size_t size () const
{ return m_size; }
int diameter () const
{ return m_diameter; }
PeerStats const& stats () const
{ return m_stats; }
// network wide average
double outPeers () const
{
if (m_size > 0)
return double (m_stats.out_active) / m_size;
return 0;
}
// Histogram, shows the number of peers that have a specific number of
// active connections. The index into the array is the number of connections,
// and the value is the number of peers.
//
std::vector totalActiveHistogram;
template
void operator() (Peer const& peer, int diameter)
{
++m_size;
PeerStats const stats (peer);
int const bucket (stats.totalActive ());
if (totalActiveHistogram.size() < bucket + 1)
totalActiveHistogram.resize (bucket + 1);
++totalActiveHistogram [bucket];
m_stats += stats;
m_diameter = diameter;
}
private:
std::size_t m_step;
std::size_t m_size;
PeerStats m_stats;
int m_diameter;
};
//------------------------------------------------------------------------------
/** Report the results of a network crawl. */
template
void report_crawl (Stream const& stream, Crawl const& c)
{
if (! stream)
return;
stream
<< std::setw (6) << c.step()
<< std::setw (6) << c.size()
<< std::setw (6) << std::fixed << std::setprecision(2) << c.outPeers()
<< std::setw (6) << c.diameter()
//<< to_string (c.totalActiveHistogram)
;
}
template
void report_crawls (Stream const& stream, CrawlSequence const& c)
{
if (! stream)
return;
stream
<< "Crawl Report"
<< std::endl
<< std::setw (6) << "Step"
<< std::setw (6) << "Size"
<< std::setw (6) << "Out"
<< std::setw (6) << "Hops"
//<< std::setw (6) << "Count"
;
for (typename CrawlSequence::const_iterator iter (c.begin());
iter != c.end(); ++iter)
report_crawl (stream, *iter);
stream << std::endl;
}
/** Report a table with aggregate information on each node. */
template
void report_nodes (NodeSequence const& nodes, Journal::Stream const& stream)
{
stream <<
divider() << std::endl <<
"Nodes Report" << std::endl <<
rfield ("ID") <<
rfield ("Total") <<
rfield ("In") <<
rfield ("Out") <<
rfield ("Tries") <<
rfield ("Live") <<
rfield ("Boot")
;
for (typename NodeSequence::const_iterator iter (nodes.begin());
iter != nodes.end(); ++iter)
{
typename NodeSequence::value_type const& node (*iter);
Logic const& logic (node.logic());
Logic::State const& state (logic.state());
stream <<
rfield (node.id ()) <<
rfield (state.counts.totalActive ()) <<
rfield (state.counts.inboundActive ()) <<
rfield (state.counts.out_active ()) <<
rfield (state.counts.connectCount ()) <<
rfield (state.livecache.size ()) <<
rfield (state.bootcache.size ())
;
}
}
//------------------------------------------------------------------------------
/** Convert a sequence into a formatted delimited string.
The range is [first, last)
*/
/** @{ */
template
std::basic_string
sequence_to_string (InputIterator first, InputIterator last,
std::basic_string const& sep = ",", int width = -1)
{
std::basic_stringstream ss;
while (first != last)
{
InputIterator const iter (first++);
if (width > 0)
ss << std::setw (width) << *iter;
else
ss << *iter;
if (first != last)
ss << sep;
}
return ss.str();
}
template
std::string sequence_to_string (InputIterator first, InputIterator last,
char const* sep, int width = -1)
{
return sequence_to_string (first, last, std::string (sep), width);
}
/** @} */
/** Report the time-evolution of a specified node. */
template
void report_node_timeline (Node const& node, Stream const& stream)
{
typename Livecache::Histogram::size_type const histw (
3 * Livecache::Histogram::size() - 1);
// Title
stream <<
divider () << std::endl <<
"Node #" << node.id() << " History" << std::endl <<
divider ();
// Legend
stream <<
fpad (4) << fpad (2) <<
fpad (2) << field ("Livecache entries by hops", histw) << fpad (2)
;
{
Journal::ScopedStream ss (stream);
ss <<
rfield ("Step",4) << fpad (2);
ss << "[ ";
for (typename Livecache::Histogram::size_type i (0);
i < Livecache::Histogram::size(); ++i)
{
ss << rfield (i,2);
if (i != Livecache::Histogram::size() - 1)
ss << fpad (1);
}
ss << " ]";
}
// Entries
typedef std::vector History;
History const& h (node.m_livecache_history);
std::size_t step (0);
for (typename History::const_iterator iter (h.begin());
iter != h.end(); ++iter)
{
++step;
Livecache::Histogram const& t (*iter);
stream <<
rfield (step,4) << fpad (2) <<
fpad (2) <<
field (sequence_to_string (t.begin (), t.end(), " ", 2), histw) <<
fpad (2)
;
}
}
//------------------------------------------------------------------------------
class PeerFinderTests : public UnitTest
{
public:
void runTest ()
{
//Debug::setAlwaysCheckHeap (true);
beginTestCase ("network");
Params p;
p.steps = 200;
p.nodes = 1000;
p.outPeers = 9.5;
p.maxPeers = 200;
p.firewalled = 0.80;
Network n (p, Journal (journal(), Reporting::network));
// Report network parameters
if (Reporting::params)
{
Journal::Stream const stream (journal().info);
if (stream)
{
stream
<< "Network parameters"
<< std::endl
<< std::setw (6) << "Steps"
<< std::setw (6) << "Nodes"
<< std::setw (6) << "Out"
<< std::setw (6) << "Max"
<< std::setw (6) << "Fire"
;
stream
<< std::setw (6) << p.steps
<< std::setw (6) << p.nodes
<< std::setw (6) << std::fixed << std::setprecision (1) << p.outPeers
<< std::setw (6) << p.maxPeers
<< std::setw (6) << int (p.firewalled * 100)
;
stream << std::endl;
}
}
//
// Run the simulation
//
n.prepare ();
{
// Note that this stream is only for the crawl,
// The network has its own journal.
Journal::Stream const stream (
journal().info, Reporting::crawl);
std::vector crawls;
if (Reporting::crawl)
crawls.reserve (p.steps);
// Iterate the network
for (std::size_t step (0); step < p.steps; ++step)
{
if (Reporting::crawl)
{
crawls.emplace_back (step);
CrawlState& c (crawls.back ());
breadth_first_traverse (
n.nodes().front(), c);
}
n.journal().info <<
divider () << std::endl <<
"Time " << n.now ().time_since_epoch () << std::endl <<
divider ()
;
n.step();
n.journal().info << std::endl;
}
n.journal().info << std::endl;
// Report the crawls
report_crawls (stream, crawls);
}
// Run detailed nodes dump report
if (Reporting::dump_nodes)
{
Journal::Stream const stream (journal().info);
for (Network::Peers::const_iterator iter (n.nodes().begin());
iter != n.nodes().end(); ++iter)
{
Journal::ScopedStream ss (stream);
Node const& node (*iter);
ss << std::endl <<
"--------------" << std::endl <<
"#" << node.id() <<
" at " << node.listening_endpoint ();
node.logic().dump (ss);
}
}
// Run aggregate nodes report
if (Reporting::nodes)
{
Journal::Stream const stream (journal().info);
report_nodes (n.nodes (), stream);
stream << std::endl;
}
// Run Node report
{
Journal::Stream const stream (journal().info);
report_node_timeline (n.nodes().front(), stream);
stream << std::endl;
}
pass();
}
PeerFinderTests () : UnitTest ("PeerFinder", "ripple", runManual)
{
}
};
static PeerFinderTests peerFinderTests;
}
}
}
#endif