create the NCC trie, which holds name claims, and create a test for it

This commit is contained in:
Jimmy Kiselak 2015-01-30 10:10:36 -05:00
parent 5be46df3ed
commit fd35060a6f
4 changed files with 764 additions and 0 deletions

View file

@ -55,6 +55,7 @@ BITCOIN_TESTS =\
test/miner_tests.cpp \
test/mruset_tests.cpp \
test/multisig_tests.cpp \
test/ncctrie_tests.cpp \
test/netbase_tests.cpp \
test/pmt_tests.cpp \
test/rpc_tests.cpp \

View file

@ -64,3 +64,514 @@ CScript StripNCCScriptPrefix(const CScript& scriptIn)
return CScript(pc, scriptIn.end());
}
std::string CNodeValue::ToString()
{
std::stringstream ss;
ss << nOut;
return txhash.ToString() + ss.str();
}
bool CNCCTrieNode::insertValue(CNodeValue val, bool * pfChanged)
{
bool fChanged = false;
if (values.empty())
{
values.push_back(val);
fChanged = true;
}
else
{
CNodeValue currentTop = values.front();
values.push_back(val);
std::make_heap(values.begin(), values.end());
if (currentTop != values.front())
fChanged = true;
}
if (pfChanged)
*pfChanged = fChanged;
return true;
}
bool CNCCTrieNode::removeValue(CNodeValue val, bool * pfChanged)
{
bool fChanged = false;
CNodeValue currentTop = values.front();
std::vector<CNodeValue>::iterator position = std::find(values.begin(), values.end(), val);
if (position != values.end())
values.erase(position);
else
{
LogPrintf("CNCCTrieNode::removeValue() : asked to remove a value that doesn't exist");
return false;
}
if (!values.empty())
{
std::make_heap(values.begin(), values.end());
if (currentTop != values.front())
fChanged = true;
}
else
fChanged = true;
if (pfChanged)
*pfChanged = fChanged;
return true;
}
bool CNCCTrieNode::getValue(CNodeValue& value)
{
if (values.empty())
return false;
else
{
value = values.front();
return true;
}
}
uint256 CNCCTrie::getMerkleHash()
{
return root.hash;
}
bool CNCCTrie::empty() const
{
return root.empty();
}
bool CNCCTrie::checkConsistency()
{
return recursiveCheckConsistency(&root);
}
bool CNCCTrie::recursiveCheckConsistency(CNCCTrieNode* node)
{
std::string stringToHash;
CNodeValue val;
bool hasValue = node->getValue(val);
if (hasValue)
{
stringToHash += val.ToString();
}
for (nodeMapType::iterator it = node->children.begin(); it != node->children.end(); ++it)
{
std::stringstream ss;
ss << it->first;
if (recursiveCheckConsistency(it->second))
{
stringToHash += ss.str();
stringToHash += it->second->hash.ToString();
}
else
return false;
}
CHash256 hasher;
std::vector<unsigned char> vchHash(hasher.OUTPUT_SIZE);
hasher.Write((const unsigned char*) stringToHash.data(), stringToHash.size());
hasher.Finalize(&(vchHash[0]));
uint256 calculatedHash(vchHash);
return calculatedHash == node->hash;
}
/*bool cachesort (std::pair<std::string, CNCCTrieNode*>& i, std::pair<std::string, CNCCTrieNode*>& j)
{
return i.first.size() < j.first.size();
}*/
bool CNCCTrie::update(nodeCacheType& cache, hashMapType& hashes)
{
// General strategy: the cache is ordered by length, ensuring child
// nodes are always inserted after their parents. Insert each node
// one at a time. When updating a node, swap its values with those
// of the cached node and delete all characters (and their children
// and so forth) which don't exist in the updated node. When adding
// a new node, make sure that its <character, CNCCTrieNode*> pair
// gets into the parent's children.
// Then, update all of the given hashes.
// This can probably be optimized by checking each substring against
// the caches each time, but that will come after this is shown to
// work correctly.
if (cache.empty())
{
return true;
}
//std::sort(cache.begin(), cache.end(), cachesort);
bool success = true;
for (nodeCacheType::iterator itcache = cache.begin(); itcache != cache.end(); ++itcache)
{
success = updateName(itcache->first, itcache->second);
if (!success)
return false;
}
for (hashMapType::iterator ithash = hashes.begin(); ithash != hashes.end(); ++ithash)
{
success = updateHash(ithash->first, ithash->second);
if (!success)
return false;
}
return true;
}
bool CNCCTrie::updateName(const std::string &name, CNCCTrieNode* updatedNode)
{
CNCCTrieNode* current = &root;
for (std::string::const_iterator itname = name.begin(); itname != name.end(); ++itname)
{
nodeMapType::iterator itchild = current->children.find(*itname);
if (itchild == current->children.end())
{
if (itname + 1 == name.end())
{
CNCCTrieNode* newNode = new CNCCTrieNode();
current->children[*itname] = newNode;
current = newNode;
}
else
return false;
}
else
{
current = itchild->second;
}
}
assert(current != NULL);
bool success = true;
current->values.swap(updatedNode->values);
for (nodeMapType::iterator itchild = current->children.begin(); itchild != current->children.end();)
{
nodeMapType::iterator itupdatechild = updatedNode->children.find(itchild->first);
if (itupdatechild == updatedNode->children.end())
{
// This character has apparently been deleted, so delete
// all descendents from this child.
success = recursiveNullify(itchild->second);
if (!success)
return false;
current->children.erase(itchild++);
}
else
++itchild;
}
return success;
}
bool CNCCTrie::recursiveNullify(CNCCTrieNode* node)
{
assert(node != NULL);
for (nodeMapType::iterator itchild = node->children.begin(); itchild != node->children.end(); ++itchild)
recursiveNullify(itchild->second);
node->children.clear();
delete node;
return true;
}
bool CNCCTrie::updateHash(const std::string& name, uint256& hash)
{
CNCCTrieNode* current = &root;
for (std::string::const_iterator itname = name.begin(); itname != name.end(); ++itname)
{
nodeMapType::iterator itchild = current->children.find(*itname);
if (itchild == current->children.end())
return false;
current = itchild->second;
}
assert(current != NULL);
current->hash = hash;
return true;
}
bool CNCCTrieCache::recursiveComputeMerkleHash(CNCCTrieNode* tnCurrent, std::string sPos) const
{
std::string stringToHash;
CNodeValue val;
bool hasValue = tnCurrent->getValue(val);
if (hasValue)
{
stringToHash += val.ToString();
}
nodeCacheType::iterator cachedNode;
for (nodeMapType::iterator it = tnCurrent->children.begin(); it != tnCurrent->children.end(); ++it)
{
std::stringstream ss;
ss << it->first;
std::string sNextPos = sPos + ss.str();
if (dirtyHashes.count(sNextPos) != 0)
{
// the child might be in the cache, so look for it there
cachedNode = cache.find(sNextPos);
if (cachedNode != cache.end())
recursiveComputeMerkleHash(cachedNode->second, sNextPos);
else
recursiveComputeMerkleHash(it->second, sNextPos);
}
stringToHash += ss.str();
hashMapType::iterator ithash = cacheHashes.find(sNextPos);
if (ithash != cacheHashes.end())
stringToHash += ithash->second.ToString();
else
stringToHash += it->second->hash.ToString();
}
CHash256 hasher;
std::vector<unsigned char> vchHash(hasher.OUTPUT_SIZE);
hasher.Write((const unsigned char*) stringToHash.data(), stringToHash.size());
hasher.Finalize(&(vchHash[0]));
cacheHashes[sPos] = uint256(vchHash);
std::set<std::string>::iterator itDirty = dirtyHashes.find(sPos);
if (itDirty != dirtyHashes.end())
dirtyHashes.erase(itDirty);
return true;
}
uint256 CNCCTrieCache::getMerkleHash() const
{
if (empty())
{
uint256 one(uint256S("0000000000000000000000000000000000000000000000000000000000000001"));
return one;
}
if (isDirty())
{
nodeCacheType::iterator cachedNode = cache.find("");
if (cachedNode != cache.end())
recursiveComputeMerkleHash(cachedNode->second, "");
else
recursiveComputeMerkleHash(&(base->root), "");
}
hashMapType::iterator ithash = cacheHashes.find("");
if (ithash != cacheHashes.end())
return ithash->second;
else
return base->root.hash;
}
bool CNCCTrieCache::empty() const
{
return base->empty() && cache.empty();
}
bool CNCCTrieCache::insertName(const std::string name, uint256 txhash, int nOut, CAmount nAmount, int nHeight) const
{
CNCCTrieNode* currentNode = &(base->root);
nodeCacheType::iterator cachedNode;
cachedNode = cache.find("");
if (cachedNode != cache.end())
currentNode = cachedNode->second;
if (currentNode == NULL)
{
currentNode = new CNCCTrieNode();
cache[""] = currentNode;
}
for (std::string::const_iterator itCur = name.begin(); itCur != name.end(); ++itCur)
{
std::string sCurrentSubstring(name.begin(), itCur);
std::string sNextSubstring(name.begin(), itCur + 1);
cachedNode = cache.find(sNextSubstring);
if (cachedNode != cache.end())
{
currentNode = cachedNode->second;
continue;
}
nodeMapType::iterator childNode = currentNode->children.find(*itCur);
if (childNode != currentNode->children.end())
{
currentNode = childNode->second;
continue;
}
// This next substring doesn't exist in the cache and the next
// character doesn't exist in current node's children, so check
// if the current node is in the cache, and if it's not, copy
// it and stick it in the cache, and then create a new node as
// its child and stick that in the cache. We have to have both
// this node and its child in the cache so that the current
// node's child map will contain the next letter, which will be
// used to find the child in the cache. This is necessary in
// order to calculate the merkle hash.
cachedNode = cache.find(sCurrentSubstring);
if (cachedNode != cache.end())
{
assert(cachedNode->second == currentNode);
}
else
{
currentNode = new CNCCTrieNode(*currentNode);
cache[sCurrentSubstring] = currentNode;
}
CNCCTrieNode* newNode = new CNCCTrieNode();
currentNode->children[*itCur] = newNode;
cache[sNextSubstring] = newNode;
currentNode = newNode;
}
cachedNode = cache.find(name);
if (cachedNode != cache.end())
{
assert(cachedNode->second == currentNode);
}
else
{
currentNode = new CNCCTrieNode(*currentNode);
cache[name] = currentNode;
}
bool fChanged = false;
currentNode->insertValue(CNodeValue(txhash, nOut, nAmount, nHeight), &fChanged);
if (fChanged)
{
for (std::string::const_iterator itCur = name.begin(); itCur != name.end(); ++itCur)
{
std::string sub(name.begin(), itCur);
dirtyHashes.insert(sub);
}
dirtyHashes.insert(name);
}
return true;
}
bool CNCCTrieCache::removeName(const std::string name, uint256 txhash, int nOut, CAmount nAmount, int nHeight) const
{
CNCCTrieNode* currentNode = &(base->root);
nodeCacheType::iterator cachedNode;
cachedNode = cache.find("");
if (cachedNode != cache.end())
currentNode = cachedNode->second;
assert(currentNode != NULL); // If there is no root in either the trie or the cache, how can there be any names to remove?
for (std::string::const_iterator itCur = name.begin(); itCur != name.end(); ++itCur)
{
std::string sCurrentSubstring(name.begin(), itCur);
std::string sNextSubstring(name.begin(), itCur + 1);
cachedNode = cache.find(sNextSubstring);
if (cachedNode != cache.end())
{
currentNode = cachedNode->second;
continue;
}
nodeMapType::iterator childNode = currentNode->children.find(*itCur);
if (childNode != currentNode->children.end())
{
currentNode = childNode->second;
continue;
}
// The name doesn't exist in either the trie or the cache, so how can we remove it?
return false;
}
cachedNode = cache.find(name);
if (cachedNode != cache.end())
assert(cachedNode->second == currentNode);
else
{
currentNode = new CNCCTrieNode(*currentNode);
cache[name] = currentNode;
}
bool fChanged = false;
assert(currentNode != NULL);
bool success = currentNode->removeValue(CNodeValue(txhash, nOut, nAmount, nHeight), &fChanged);
assert(success);
if (fChanged)
{
for (std::string::const_iterator itCur = name.begin(); itCur != name.end(); ++itCur)
{
std::string sub(name.begin(), itCur);
dirtyHashes.insert(sub);
}
dirtyHashes.insert(name);
}
CNCCTrieNode* rootNode = &(base->root);
cachedNode = cache.find("");
if (cachedNode != cache.end())
rootNode = cachedNode->second;
return recursivePruneName(rootNode, 0, name);
}
bool CNCCTrieCache::recursivePruneName(CNCCTrieNode* tnCurrent, unsigned int nPos, std::string sName, bool* pfNullified) const
{
bool fNullified = false;
std::string sCurrentSubstring = sName.substr(0, nPos);
if (nPos < sName.size())
{
std::string sNextSubstring = sName.substr(0, nPos + 1);
unsigned char cNext = sName.at(nPos);
CNCCTrieNode* tnNext = NULL;
nodeCacheType::iterator cachedNode = cache.find(sNextSubstring);
if (cachedNode != cache.end())
tnNext = cachedNode->second;
else
{
nodeMapType::iterator childNode = tnCurrent->children.find(cNext);
if (childNode != tnCurrent->children.end())
tnNext = childNode->second;
}
if (tnNext == NULL)
return false;
bool fChildNullified = false;
if (!recursivePruneName(tnNext, nPos + 1, sName, &fChildNullified))
return false;
if (fChildNullified)
{
// If the child nullified itself, the child should already be
// out of the cache, and the character must now be removed
// from the current node's map of child nodes to ensure that
// it isn't found when calculating the merkle hash. But
// tnCurrent isn't necessarily in the cache. If it's not, it
// has to be added to the cache, so nothing is changed in the
// trie. If the current node is added to the cache, however,
// that does not imply that the parent node must be altered to
// reflect that its child is now in the cache, since it
// already has a character in its child map which will be used
// when calculating the merkle root.
// First, find out if this node is in the cache.
cachedNode = cache.find(sCurrentSubstring);
if (cachedNode == cache.end())
{
// it isn't, so make a copy, stick it in the cache,
// and make it the new current node
tnCurrent = new CNCCTrieNode(*tnCurrent);
cache[sCurrentSubstring] = tnCurrent;
}
// erase the character from the current node, which is
// now guaranteed to be in the cache
nodeMapType::iterator childNode = tnCurrent->children.find(cNext);
if (childNode != tnCurrent->children.end())
tnCurrent->children.erase(childNode);
else
return false;
}
}
if (sCurrentSubstring.size() != 0 && tnCurrent->empty())
{
// If the current node is in the cache, remove it from there
nodeCacheType::iterator cachedNode = cache.find(sCurrentSubstring);
if (cachedNode != cache.end())
{
assert(tnCurrent == cachedNode->second);
delete tnCurrent;
cache.erase(cachedNode);
}
fNullified = true;
}
if (pfNullified)
*pfNullified = fNullified;
return true;
}
bool CNCCTrieCache::Flush()
{
if (isDirty())
getMerkleHash();
return base->update(cache, cacheHashes);
}

125
src/ncc.h
View file

@ -2,10 +2,135 @@
#define BITCOIN_NCC_H
#include "script/script.h"
#include "amount.h"
#include "primitives/transaction.h"
#include "coins.h"
#include "hash.h"
#include "uint256.h"
#include "util.h"
#include <iostream>
#include <string>
#include <algorithm>
#include <vector>
bool DecodeNCCScript(const CScript& scriptIn, int& op, std::vector<std::vector<unsigned char> >& vvchParams);
bool DecodeNCCScript(const CScript& scriptIn, int& op, std::vector<std::vector<unsigned char> >& vvchParams, CScript::const_iterator& pc);
CScript StripNCCScriptPrefix(const CScript& scriptIn);
class CNodeValue
{
public:
uint256 txhash;
uint32_t nOut;
CAmount nAmount;
int nHeight;
CNodeValue() {};
CNodeValue(uint256 txhash, uint32_t nOut, CAmount nAmount, int nHeight) : txhash(txhash), nOut(nOut), nAmount(nAmount), nHeight(nHeight) {}
std::string ToString();
bool operator<(const CNodeValue& other)
{
if (nAmount < other.nAmount)
return true;
else if (nAmount == other.nAmount)
{
if (nHeight > other.nHeight)
return true;
else if (nHeight == other.nHeight)
{
if (txhash.GetHex() > other.txhash.GetHex())
return true;
else if (txhash == other.txhash)
if (nOut > other.nOut)
return true;
}
}
return false;
}
bool operator==(const CNodeValue& other)
{
return txhash == other.txhash && nOut == other.nOut && nAmount == other.nAmount && nHeight == other.nHeight;
}
bool operator!=(const CNodeValue& other)
{
return !(*this == other);
}
};
class CNCCTrieNode;
class CNCCTrie;
typedef std::map<unsigned char, CNCCTrieNode*> nodeMapType;
class CNCCTrieNode
{
public:
CNCCTrieNode() {}
CNCCTrieNode(uint256 hash) : hash(hash) {}
uint256 hash;
uint256 bestBlock;
nodeMapType children;
bool insertValue(CNodeValue val, bool * fChanged);
bool removeValue(CNodeValue val, bool * fChanged);
bool getValue(CNodeValue& val);
bool empty() const {return children.empty() && values.empty();}
friend class CNCCTrie;
private:
std::vector<CNodeValue> values;
};
struct nodenamecompare
{
bool operator() (const std::string& i, const std::string& j) const
{
if (i.size() == j.size())
return i < j;
return i.size() < j.size();
}
};
typedef std::map<std::string, CNCCTrieNode*, nodenamecompare> nodeCacheType;
typedef std::map<std::string, uint256> hashMapType;
class CNCCTrieCache;
class CNCCTrie
{
public:
CNCCTrie(CCoinsViewCache* coins) : coins(coins), root(uint256S("0000000000000000000000000000000000000000000000000000000000000001")) {}
uint256 getMerkleHash();
bool empty() const;
bool checkConsistency();
friend class CNCCTrieCache;
private:
CCoinsViewCache* coins;
bool update(nodeCacheType& cache, hashMapType& hashes);
bool updateName(const std::string& name, CNCCTrieNode* updatedNode);
bool updateHash(const std::string& name, uint256& hash);
bool recursiveNullify(CNCCTrieNode* node);
bool recursiveCheckConsistency(CNCCTrieNode* node);
CNCCTrieNode root;
};
class CNCCTrieCache
{
public:
CNCCTrieCache(CNCCTrie* base): base(base) {}
uint256 getMerkleHash() const;
bool empty() const;
bool Flush();
bool isDirty() const { return !dirtyHashes.empty(); }
bool insertName (const std::string name, uint256 txhash, int nOut, CAmount nAmount, int nDepth) const;
bool removeName (const std::string name, uint256 txhash, int nOut, CAmount nAmount, int nDepth) const;
private:
CNCCTrie* base;
mutable nodeCacheType cache;
mutable std::set<std::string> dirtyHashes;
mutable hashMapType cacheHashes;
uint256 computeHash() const;
bool recursiveComputeMerkleHash(CNCCTrieNode* tnCurrent, std::string sPos) const;
bool recursivePruneName(CNCCTrieNode* tnCurrent, unsigned int nPos, std::string sName, bool* pfNullified = NULL) const;
};
#endif // BITCOIN_NCC_H

127
src/test/ncctrie_tests.cpp Normal file
View file

@ -0,0 +1,127 @@
// Copyright (c) 2015 The LBRY Foundation
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://opensource.org/licenses/mit-license.php
#include "primitives/transaction.h"
#include "ncc.h"
#include "coins.h"
#include <boost/test/unit_test.hpp>
#include <iostream>
using namespace std;
BOOST_AUTO_TEST_SUITE(ncctrie_tests)
CMutableTransaction BuildTransaction(const uint256& prevhash)
{
CMutableTransaction tx;
tx.nVersion = 1;
tx.nLockTime = 0;
tx.vin.resize(1);
tx.vout.resize(1);
tx.vin[0].prevout.hash = prevhash;
tx.vin[0].prevout.n = 0;
tx.vin[0].scriptSig = CScript();
tx.vin[0].nSequence = std::numeric_limits<unsigned int>::max();
tx.vout[0].scriptPubKey = CScript();
tx.vout[0].nValue = 0;
return tx;
}
BOOST_AUTO_TEST_CASE(ncctrie_create_insert_remov)
{
CMutableTransaction tx1 = BuildTransaction(uint256S("0000000000000000000000000000000000000000000000000000000000000001"));
CMutableTransaction tx2 = BuildTransaction(tx1.GetHash());
CMutableTransaction tx3 = BuildTransaction(tx2.GetHash());
CMutableTransaction tx4 = BuildTransaction(tx3.GetHash());
CMutableTransaction tx5 = BuildTransaction(tx4.GetHash());
CMutableTransaction tx6 = BuildTransaction(tx5.GetHash());
uint256 hash1;
hash1.SetHex("81727ef4dd44787941b9bba2c143a3607d92ee1f0d1d24d0da5aac6aa44ae12f");
uint256 hash2;
hash2.SetHex("653304cb35e66280b52ee6c52fcf2b8b1032ced6fea7a6e1548f24c47b1a3910");
uint256 hash3;
hash3.SetHex("48c0f04ab06338b25c66a6f237084eec49a5d761b5bfbe125d213fce33948242");
uint256 hash4;
hash4.SetHex("a79e8a5b28f7fa5e8836a4b48da9988bdf56ce749f81f413cb754f963a516200");
CCoinsView coinsDummy;
CCoinsViewCache coins(&coinsDummy);
CNCCTrie trie(&coins);
BOOST_CHECK(trie.empty());
CNCCTrieCache ntState(&trie);
ntState.insertName(std::string("test"), tx1.GetHash(), 0, 50, 100);
ntState.insertName(std::string("test2"), tx2.GetHash(), 0, 50, 100);
BOOST_CHECK(trie.empty());
BOOST_CHECK(!ntState.empty());
BOOST_CHECK(ntState.getMerkleHash() == hash1);
ntState.insertName(std::string("test"), tx3.GetHash(), 0, 50, 101);
BOOST_CHECK(ntState.getMerkleHash() == hash1);
ntState.insertName(std::string("tes"), tx4.GetHash(), 0, 50, 100);
BOOST_CHECK(ntState.getMerkleHash() == hash2);
ntState.insertName(std::string("testtesttesttest"), tx5.GetHash(), 0, 50, 100);
ntState.removeName(std::string("testtesttesttest"), tx5.GetHash(), 0, 50, 100);
BOOST_CHECK(ntState.getMerkleHash() == hash2);
ntState.Flush();
BOOST_CHECK(!trie.empty());
BOOST_CHECK(trie.getMerkleHash() == hash2);
BOOST_CHECK(trie.checkConsistency());
CNCCTrieCache ntState2(&trie);
ntState2.insertName(std::string("abab"), tx6.GetHash(), 0, 50, 100);
ntState2.removeName(std::string("test"), tx1.GetHash(), 0, 50, 100);
BOOST_CHECK(ntState2.getMerkleHash() == hash3);
ntState2.Flush();
BOOST_CHECK(!trie.empty());
BOOST_CHECK(trie.getMerkleHash() == hash3);
BOOST_CHECK(trie.checkConsistency());
CNCCTrieCache ntState3(&trie);
ntState3.insertName(std::string("test"), tx1.GetHash(), 0, 50, 100);
BOOST_CHECK(ntState3.getMerkleHash() == hash4);
ntState3.Flush();
BOOST_CHECK(!trie.empty());
BOOST_CHECK(trie.getMerkleHash() == hash4);
BOOST_CHECK(trie.checkConsistency());
CNCCTrieCache ntState4(&trie);
ntState4.removeName(std::string("abab"), tx6.GetHash(), 0, 50, 100);
BOOST_CHECK(ntState4.getMerkleHash() == hash2);
ntState4.Flush();
BOOST_CHECK(!trie.empty());
BOOST_CHECK(trie.getMerkleHash() == hash2);
BOOST_CHECK(trie.checkConsistency());
CNCCTrieCache ntState5(&trie);
ntState5.removeName(std::string("test"), tx3.GetHash(), 0, 50, 101);
BOOST_CHECK(ntState5.getMerkleHash() == hash2);
ntState5.Flush();
BOOST_CHECK(!trie.empty());
BOOST_CHECK(trie.getMerkleHash() == hash2);
BOOST_CHECK(trie.checkConsistency());
CNCCTrieCache ntState6(&trie);
ntState6.insertName(std::string("test"), tx3.GetHash(), 0, 50, 101);
BOOST_CHECK(ntState6.getMerkleHash() == hash2);
ntState6.Flush();
BOOST_CHECK(!trie.empty());
BOOST_CHECK(trie.getMerkleHash() == hash2);
BOOST_CHECK(trie.checkConsistency());
}
BOOST_AUTO_TEST_SUITE_END()