2010-03-13 00:04:21 +00:00
|
|
|
/*
|
|
|
|
---------------------------------------------------------------------------
|
2012-02-03 03:38:30 +00:00
|
|
|
Open Asset Import Library (assimp)
|
2010-03-13 00:04:21 +00:00
|
|
|
---------------------------------------------------------------------------
|
2009-06-29 14:58:06 +00:00
|
|
|
|
2012-02-03 03:38:30 +00:00
|
|
|
Copyright (c) 2006-2012, assimp team
|
2010-03-13 00:04:21 +00:00
|
|
|
|
|
|
|
All rights reserved.
|
|
|
|
|
|
|
|
Redistribution and use of this software in source and binary forms,
|
|
|
|
with or without modification, are permitted provided that the following
|
|
|
|
conditions are met:
|
|
|
|
|
|
|
|
* Redistributions of source code must retain the above
|
|
|
|
copyright notice, this list of conditions and the
|
|
|
|
following disclaimer.
|
|
|
|
|
|
|
|
* Redistributions in binary form must reproduce the above
|
|
|
|
copyright notice, this list of conditions and the
|
|
|
|
following disclaimer in the documentation and/or other
|
|
|
|
materials provided with the distribution.
|
|
|
|
|
2012-02-03 03:38:30 +00:00
|
|
|
* Neither the name of the assimp team, nor the names of its
|
2010-03-13 00:04:21 +00:00
|
|
|
contributors may be used to endorse or promote products
|
|
|
|
derived from this software without specific prior
|
2012-02-03 03:38:30 +00:00
|
|
|
written permission of the assimp team.
|
2010-03-13 00:04:21 +00:00
|
|
|
|
|
|
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
|
|
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
|
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
|
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
|
|
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
|
|
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
|
|
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
|
|
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
|
|
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
|
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
|
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
|
|
---------------------------------------------------------------------------
|
|
|
|
*/
|
|
|
|
|
|
|
|
/** @file CompareDump.cpp
|
|
|
|
* @brief Implementation of the 'assimp cmpdmp', which compares
|
|
|
|
* two model dumps for equality. It plays an important role
|
|
|
|
* in the regression test suite.
|
|
|
|
*/
|
|
|
|
|
|
|
|
#include "Main.h"
|
|
|
|
const char* AICMD_MSG_CMPDUMP_HELP =
|
|
|
|
"assimp cmpdump <actual> <expected>\n"
|
|
|
|
"\tCompare two short dumps produced with \'assimp dump <..> -s\' for equality.\n"
|
|
|
|
;
|
|
|
|
|
|
|
|
#include "../../code/assbin_chunks.h"
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
#include "generic_inserter.hpp"
|
|
|
|
|
|
|
|
// get << for aiString
|
|
|
|
template <typename char_t, typename traits_t>
|
|
|
|
void mysprint(std::basic_ostream<char_t, traits_t>& os, const aiString& vec) {
|
|
|
|
os << "[length: \'" << std::dec << vec.length << "\' content: \'" << vec.data << "\']";
|
|
|
|
}
|
|
|
|
|
|
|
|
template <typename char_t, typename traits_t>
|
|
|
|
std::basic_ostream<char_t, traits_t>& operator<< (std::basic_ostream<char_t, traits_t>& os, const aiString& vec) {
|
|
|
|
return generic_inserter(mysprint<char_t,traits_t>, os, vec);
|
|
|
|
}
|
|
|
|
|
|
|
|
class sliced_chunk_iterator;
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
/// @class compare_fails_exception
|
|
|
|
///
|
|
|
|
/// @brief Sentinel exception to return quickly from deeply nested control paths
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
class compare_fails_exception : public virtual std::exception {
|
|
|
|
public:
|
|
|
|
|
|
|
|
enum {MAX_ERR_LEN = 4096};
|
|
|
|
|
|
|
|
/* public c'tors */
|
|
|
|
compare_fails_exception(const char* msg) {
|
|
|
|
strncpy(mywhat,msg,MAX_ERR_LEN-1);
|
|
|
|
strcat(mywhat,"\n");
|
|
|
|
}
|
|
|
|
|
|
|
|
/* public member functions */
|
|
|
|
const char* what() const throw() {
|
|
|
|
return mywhat;
|
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
|
|
|
|
|
|
|
char mywhat[MAX_ERR_LEN+1];
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
#define MY_FLT_EPSILON 1e-1f
|
|
|
|
#define MY_DBL_EPSILON 1e-1
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
/// @class comparer_context
|
|
|
|
///
|
|
|
|
/// @brief Record our way through the files to be compared and dump useful information if we fail.
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
class comparer_context {
|
|
|
|
friend class sliced_chunk_iterator;
|
|
|
|
|
|
|
|
public:
|
|
|
|
|
|
|
|
/* construct given two file handles to compare */
|
|
|
|
comparer_context(FILE* actual,FILE* expect)
|
|
|
|
: actual(actual)
|
|
|
|
, expect(expect)
|
|
|
|
, cnt_chunks(0)
|
|
|
|
{
|
2012-04-22 23:22:40 +00:00
|
|
|
ai_assert(actual);
|
|
|
|
ai_assert(expect);
|
2010-03-13 00:04:21 +00:00
|
|
|
|
|
|
|
fseek(actual,0,SEEK_END);
|
|
|
|
lengths.push(std::make_pair(static_cast<uint32_t>(ftell(actual)),0));
|
|
|
|
fseek(actual,0,SEEK_SET);
|
|
|
|
|
|
|
|
history.push_back(HistoryEntry("---",PerChunkCounter()));
|
|
|
|
}
|
|
|
|
|
|
|
|
public:
|
|
|
|
|
|
|
|
|
|
|
|
/* set new scope */
|
|
|
|
void push_elem(const char* msg) {
|
|
|
|
const std::string s = msg;
|
|
|
|
|
|
|
|
PerChunkCounter::const_iterator it = history.back().second.find(s);
|
|
|
|
if(it != history.back().second.end()) {
|
|
|
|
++history.back().second[s];
|
|
|
|
}
|
2015-03-28 11:51:46 +00:00
|
|
|
else history.back().second[s] = 0;
|
2010-03-13 00:04:21 +00:00
|
|
|
|
|
|
|
history.push_back(HistoryEntry(s,PerChunkCounter()));
|
2015-03-28 11:46:15 +00:00
|
|
|
debug_trace.push_back("PUSH " + s);
|
2010-03-13 00:04:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/* leave current scope */
|
|
|
|
void pop_elem() {
|
2012-04-22 23:22:40 +00:00
|
|
|
ai_assert(history.size());
|
2015-03-28 11:46:15 +00:00
|
|
|
debug_trace.push_back("POP "+ history.back().first);
|
2010-03-13 00:04:21 +00:00
|
|
|
history.pop_back();
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* push current chunk length and start offset on top of stack */
|
|
|
|
void push_length(uint32_t nl, uint32_t start) {
|
|
|
|
lengths.push(std::make_pair(nl,start));
|
|
|
|
++cnt_chunks;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* pop the chunk length stack */
|
|
|
|
void pop_length() {
|
2012-04-22 23:22:40 +00:00
|
|
|
ai_assert(lengths.size());
|
2010-03-13 00:04:21 +00:00
|
|
|
lengths.pop();
|
|
|
|
}
|
|
|
|
|
|
|
|
/* access the current chunk length */
|
|
|
|
uint32_t get_latest_chunk_length() {
|
2012-04-22 23:22:40 +00:00
|
|
|
ai_assert(lengths.size());
|
2010-03-13 00:04:21 +00:00
|
|
|
return lengths.top().first;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* access the current chunk start offset */
|
|
|
|
uint32_t get_latest_chunk_start() {
|
2012-04-22 23:22:40 +00:00
|
|
|
ai_assert(lengths.size());
|
2010-03-13 00:04:21 +00:00
|
|
|
return lengths.top().second;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* total number of chunk headers passed so far*/
|
|
|
|
uint32_t get_num_chunks() {
|
|
|
|
return cnt_chunks;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* get ACTUAL file desc. != NULL */
|
|
|
|
FILE* get_actual() const {
|
|
|
|
return actual;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* get EXPECT file desc. != NULL */
|
|
|
|
FILE* get_expect() const {
|
|
|
|
return expect;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* compare next T from both streams, name occurs in error messages */
|
|
|
|
template<typename T> T cmp(const std::string& name) {
|
|
|
|
T a,e;
|
|
|
|
read(a,e);
|
|
|
|
|
|
|
|
if(a != e) {
|
|
|
|
std::stringstream ss;
|
|
|
|
failure((ss<< "Expected " << e << ", but actual is " << a,
|
|
|
|
ss.str()),name);
|
|
|
|
}
|
|
|
|
// std::cout << name << " " << std::hex << a << std::endl;
|
|
|
|
return a;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* compare next num T's from both streams, name occurs in error messages */
|
|
|
|
template<typename T> void cmp(size_t num,const std::string& name) {
|
|
|
|
for(size_t n = 0; n < num; ++n) {
|
|
|
|
std::stringstream ss;
|
|
|
|
cmp<T>((ss<<name<<"["<<n<<"]",ss.str()));
|
|
|
|
// std::cout << name << " " << std::hex << a << std::endl;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Bounds of an aiVector3D array (separate function
|
|
|
|
* because partial specializations of member functions are illegal--)*/
|
|
|
|
template<typename T> void cmp_bounds(const std::string& name) {
|
|
|
|
cmp<T> (name+".<minimum-value>");
|
|
|
|
cmp<T> (name+".<maximum-value>");
|
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
|
|
|
|
|
|
|
/* Report failure */
|
2015-02-13 12:45:36 +00:00
|
|
|
AI_WONT_RETURN void failure(const std::string& err, const std::string& name) AI_WONT_RETURN_SUFFIX {
|
2010-03-13 00:04:21 +00:00
|
|
|
std::stringstream ss;
|
|
|
|
throw compare_fails_exception((ss
|
|
|
|
<< "Files are different at "
|
|
|
|
<< history.back().first
|
|
|
|
<< "."
|
|
|
|
<< name
|
|
|
|
<< ".\nError is: "
|
|
|
|
<< err
|
|
|
|
<< ".\nCurrent position in scene hierarchy is "
|
|
|
|
<< print_hierarchy(),ss.str().c_str()
|
|
|
|
));
|
|
|
|
}
|
|
|
|
|
|
|
|
/** print our 'stack' */
|
|
|
|
std::string print_hierarchy() {
|
|
|
|
std::stringstream ss;
|
|
|
|
ss << std::endl;
|
|
|
|
|
|
|
|
const char* last = history.back().first.c_str();
|
|
|
|
std::string pad;
|
|
|
|
|
2015-03-28 11:51:46 +00:00
|
|
|
for(ChunkHistory::reverse_iterator rev = history.rbegin(),
|
|
|
|
end = history.rend(); rev != end; ++rev, pad += " ")
|
2010-03-13 00:04:21 +00:00
|
|
|
{
|
2015-03-28 11:51:46 +00:00
|
|
|
ss << pad << (*rev).first << "(Index: " << (*rev).second[last] << ")" << std::endl;
|
2010-03-13 00:04:21 +00:00
|
|
|
last = (*rev).first.c_str();
|
|
|
|
}
|
|
|
|
|
2015-03-28 11:46:15 +00:00
|
|
|
ss << std::endl << "Debug trace: "<< std::endl;
|
2015-03-28 12:10:34 +00:00
|
|
|
for (std::vector<std::string>::const_iterator it = debug_trace.begin(); it != debug_trace.end(); ++it) {
|
|
|
|
ss << *it << std::endl;
|
2015-03-28 11:46:15 +00:00
|
|
|
}
|
2010-03-13 00:04:21 +00:00
|
|
|
return ss.str();
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2015-03-28 11:46:15 +00:00
|
|
|
/* read from both streams at the same time */
|
2010-03-13 00:04:21 +00:00
|
|
|
template <typename T> void read(T& filla,T& fille) {
|
|
|
|
if(1 != fread(&filla,sizeof(T),1,actual)) {
|
2015-03-15 20:01:34 +00:00
|
|
|
EOFActual();
|
2010-03-13 00:04:21 +00:00
|
|
|
}
|
|
|
|
if(1 != fread(&fille,sizeof(T),1,expect)) {
|
2015-03-15 20:01:34 +00:00
|
|
|
EOFExpect();
|
2010-03-13 00:04:21 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
|
|
|
|
2015-03-15 20:01:34 +00:00
|
|
|
void EOFActual() {
|
|
|
|
std::stringstream ss;
|
|
|
|
throw compare_fails_exception((ss
|
|
|
|
<< "Unexpected EOF reading ACTUAL.\nCurrent position in scene hierarchy is "
|
|
|
|
<< print_hierarchy(),ss.str().c_str()
|
|
|
|
));
|
|
|
|
}
|
|
|
|
|
|
|
|
void EOFExpect() {
|
|
|
|
std::stringstream ss;
|
|
|
|
throw compare_fails_exception((ss
|
|
|
|
<< "Unexpected EOF reading EXPECT.\nCurrent position in scene hierarchy is "
|
|
|
|
<< print_hierarchy(),ss.str().c_str()
|
|
|
|
));
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2010-03-13 00:04:21 +00:00
|
|
|
FILE *const actual, *const expect;
|
|
|
|
|
|
|
|
typedef std::map<std::string,unsigned int> PerChunkCounter;
|
|
|
|
typedef std::pair<std::string,PerChunkCounter> HistoryEntry;
|
|
|
|
|
|
|
|
typedef std::deque<HistoryEntry> ChunkHistory;
|
|
|
|
ChunkHistory history;
|
|
|
|
|
2015-03-28 11:46:15 +00:00
|
|
|
std::vector<std::string> debug_trace;
|
|
|
|
|
2010-03-13 00:04:21 +00:00
|
|
|
typedef std::stack<std::pair<uint32_t,uint32_t> > LengthStack;
|
|
|
|
LengthStack lengths;
|
|
|
|
|
|
|
|
uint32_t cnt_chunks;
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
/* specialization for aiString (it needs separate handling because its on-disk representation
|
|
|
|
* differs from its binary representation in memory and can't be treated as an array of n T's.*/
|
|
|
|
template <> void comparer_context :: read<aiString>(aiString& filla,aiString& fille) {
|
|
|
|
uint32_t lena,lene;
|
|
|
|
read(lena,lene);
|
|
|
|
|
|
|
|
if(lena && 1 != fread(&filla.data,lena,1,actual)) {
|
2015-03-15 20:01:34 +00:00
|
|
|
EOFActual();
|
2010-03-13 00:04:21 +00:00
|
|
|
}
|
|
|
|
if(lene && 1 != fread(&fille.data,lene,1,expect)) {
|
2015-03-15 20:01:34 +00:00
|
|
|
EOFExpect();
|
2010-03-13 00:04:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
fille.data[fille.length=static_cast<unsigned int>(lene)] = '\0';
|
|
|
|
filla.data[filla.length=static_cast<unsigned int>(lena)] = '\0';
|
|
|
|
}
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
/* Specialization for float, uses epsilon for comparisons*/
|
|
|
|
template<> float comparer_context :: cmp<float>(const std::string& name)
|
|
|
|
{
|
|
|
|
float a,e,t;
|
|
|
|
read(a,e);
|
|
|
|
|
|
|
|
if((t=fabs(a-e)) > MY_FLT_EPSILON) {
|
|
|
|
std::stringstream ss;
|
|
|
|
failure((ss<< "Expected " << e << ", but actual is "
|
|
|
|
<< a << " (delta is " << t << ")", ss.str()),name);
|
|
|
|
}
|
|
|
|
return a;
|
|
|
|
}
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
/* Specialization for double, uses epsilon for comparisons*/
|
|
|
|
template<> double comparer_context :: cmp<double>(const std::string& name)
|
|
|
|
{
|
|
|
|
double a,e,t;
|
|
|
|
read(a,e);
|
|
|
|
|
|
|
|
if((t=fabs(a-e)) > MY_DBL_EPSILON) {
|
|
|
|
std::stringstream ss;
|
|
|
|
failure((ss<< "Expected " << e << ", but actual is "
|
|
|
|
<< a << " (delta is " << t << ")", ss.str()),name);
|
|
|
|
}
|
|
|
|
return a;
|
|
|
|
}
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
/* Specialization for aiVector3D */
|
|
|
|
template<> aiVector3D comparer_context :: cmp<aiVector3D >(const std::string& name)
|
|
|
|
{
|
|
|
|
const float x = cmp<float>(name+".x");
|
|
|
|
const float y = cmp<float>(name+".y");
|
|
|
|
const float z = cmp<float>(name+".z");
|
|
|
|
|
|
|
|
return aiVector3D(x,y,z);
|
|
|
|
}
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
/* Specialization for aiColor4D */
|
|
|
|
template<> aiColor4D comparer_context :: cmp<aiColor4D >(const std::string& name)
|
|
|
|
{
|
|
|
|
const float r = cmp<float>(name+".r");
|
|
|
|
const float g = cmp<float>(name+".g");
|
|
|
|
const float b = cmp<float>(name+".b");
|
|
|
|
const float a = cmp<float>(name+".a");
|
|
|
|
|
|
|
|
return aiColor4D(r,g,b,a);
|
|
|
|
}
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
/* Specialization for aiQuaternion */
|
|
|
|
template<> aiQuaternion comparer_context :: cmp<aiQuaternion >(const std::string& name)
|
|
|
|
{
|
|
|
|
const float w = cmp<float>(name+".w");
|
|
|
|
const float x = cmp<float>(name+".x");
|
|
|
|
const float y = cmp<float>(name+".y");
|
|
|
|
const float z = cmp<float>(name+".z");
|
|
|
|
|
|
|
|
return aiQuaternion(w,x,y,z);
|
|
|
|
}
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
/* Specialization for aiQuatKey */
|
|
|
|
template<> aiQuatKey comparer_context :: cmp<aiQuatKey >(const std::string& name)
|
|
|
|
{
|
|
|
|
const double mTime = cmp<double>(name+".mTime");
|
|
|
|
const aiQuaternion mValue = cmp<aiQuaternion>(name+".mValue");
|
|
|
|
|
|
|
|
return aiQuatKey(mTime,mValue);
|
|
|
|
}
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
/* Specialization for aiVectorKey */
|
|
|
|
template<> aiVectorKey comparer_context :: cmp<aiVectorKey >(const std::string& name)
|
|
|
|
{
|
|
|
|
const double mTime = cmp<double>(name+".mTime");
|
|
|
|
const aiVector3D mValue = cmp<aiVector3D>(name+".mValue");
|
|
|
|
|
|
|
|
return aiVectorKey(mTime,mValue);
|
|
|
|
}
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
/* Specialization for aiMatrix4x4 */
|
|
|
|
template<> aiMatrix4x4 comparer_context :: cmp<aiMatrix4x4 >(const std::string& name)
|
|
|
|
{
|
|
|
|
aiMatrix4x4 res;
|
|
|
|
for(unsigned int i = 0; i < 4; ++i) {
|
|
|
|
for(unsigned int j = 0; j < 4; ++j) {
|
|
|
|
std::stringstream ss;
|
|
|
|
res[i][j] = cmp<float>(name+(ss<<".m"<<i<<j,ss.str()));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return res;
|
|
|
|
}
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
/* Specialization for aiVertexWeight */
|
|
|
|
template<> aiVertexWeight comparer_context :: cmp<aiVertexWeight >(const std::string& name)
|
|
|
|
{
|
|
|
|
const unsigned int mVertexId = cmp<unsigned int>(name+".mVertexId");
|
|
|
|
const float mWeight = cmp<float>(name+".mWeight");
|
|
|
|
|
|
|
|
return aiVertexWeight(mVertexId,mWeight);
|
|
|
|
}
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
/// @class sliced_chunk_iterator
|
|
|
|
///
|
|
|
|
/// @brief Helper to iterate easily through corresponding chunks of two dumps simultaneously.
|
|
|
|
///
|
|
|
|
/// Not a *real* iterator, doesn't fully conform to the isocpp iterator spec
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
class sliced_chunk_iterator {
|
|
|
|
|
|
|
|
friend class sliced_chunk_reader;
|
|
|
|
sliced_chunk_iterator(comparer_context& ctx, long end)
|
|
|
|
: ctx(ctx)
|
|
|
|
, endit(false)
|
|
|
|
, next(std::numeric_limits<long>::max())
|
|
|
|
, end(end)
|
|
|
|
{
|
|
|
|
load_next();
|
|
|
|
}
|
|
|
|
|
|
|
|
public:
|
|
|
|
|
|
|
|
~sliced_chunk_iterator() {
|
|
|
|
fseek(ctx.get_actual(),end,SEEK_SET);
|
|
|
|
fseek(ctx.get_expect(),end,SEEK_SET);
|
|
|
|
}
|
|
|
|
|
|
|
|
public:
|
|
|
|
|
|
|
|
/* get current chunk head */
|
|
|
|
typedef std::pair<uint32_t,uint32_t> Chunk;
|
|
|
|
const Chunk& operator*() {
|
|
|
|
return current;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* get to next chunk head */
|
|
|
|
const sliced_chunk_iterator& operator++() {
|
|
|
|
cleanup();
|
|
|
|
load_next();
|
|
|
|
return *this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* */
|
|
|
|
bool is_end() const {
|
|
|
|
return endit;
|
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
|
|
|
|
|
|
|
/* get to the end of *this* chunk */
|
|
|
|
void cleanup() {
|
|
|
|
if(next != std::numeric_limits<long>::max()) {
|
|
|
|
fseek(ctx.get_actual(),next,SEEK_SET);
|
|
|
|
fseek(ctx.get_expect(),next,SEEK_SET);
|
|
|
|
|
|
|
|
ctx.pop_length();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/* advance to the next chunk */
|
|
|
|
void load_next() {
|
|
|
|
|
|
|
|
Chunk actual;
|
|
|
|
size_t res=0;
|
|
|
|
|
|
|
|
const long cur = ftell(ctx.get_expect());
|
|
|
|
if(end-cur<8) {
|
|
|
|
current = std::make_pair(0u,0u);
|
|
|
|
endit = true;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
res|=fread(¤t.first,4,1,ctx.get_expect());
|
|
|
|
res|=fread(¤t.second,4,1,ctx.get_expect()) <<1u;
|
|
|
|
res|=fread(&actual.first,4,1,ctx.get_actual()) <<2u;
|
|
|
|
res|=fread(&actual.second,4,1,ctx.get_actual()) <<3u;
|
|
|
|
|
|
|
|
if(res!=0xf) {
|
2015-03-15 20:01:34 +00:00
|
|
|
ctx.failure("IO Error reading chunk head, dumps are malformed","<ChunkHead>");
|
2010-03-13 00:04:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (current.first != actual.first) {
|
|
|
|
std::stringstream ss;
|
|
|
|
ctx.failure((ss
|
|
|
|
<<"Chunk headers do not match. EXPECT: "
|
|
|
|
<< std::hex << current.first
|
|
|
|
<<" ACTUAL: "
|
|
|
|
<< /*std::hex */actual.first,
|
|
|
|
ss.str()),
|
|
|
|
"<ChunkHead>");
|
|
|
|
}
|
|
|
|
|
|
|
|
if (current.first != actual.first) {
|
|
|
|
std::stringstream ss;
|
|
|
|
ctx.failure((ss
|
2015-03-15 20:01:34 +00:00
|
|
|
<<"Chunk lengths do not match. EXPECT: "
|
2010-03-13 00:04:21 +00:00
|
|
|
<<current.second
|
|
|
|
<<" ACTUAL: "
|
|
|
|
<< actual.second,
|
|
|
|
ss.str()),
|
|
|
|
"<ChunkHead>");
|
|
|
|
}
|
|
|
|
|
|
|
|
next = cur+current.second+8;
|
|
|
|
ctx.push_length(current.second,cur+8);
|
|
|
|
}
|
|
|
|
|
|
|
|
comparer_context& ctx;
|
|
|
|
Chunk current;
|
|
|
|
bool endit;
|
|
|
|
long next,end;
|
|
|
|
};
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
/// @class sliced_chunk_reader
|
|
|
|
///
|
|
|
|
/// @brief Helper to iterate easily through corresponding chunks of two dumps simultaneously.
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
class sliced_chunk_reader {
|
|
|
|
public:
|
|
|
|
|
|
|
|
//
|
|
|
|
sliced_chunk_reader(comparer_context& ctx)
|
|
|
|
: ctx(ctx)
|
|
|
|
{}
|
|
|
|
|
|
|
|
//
|
|
|
|
~sliced_chunk_reader() {
|
|
|
|
}
|
|
|
|
|
|
|
|
public:
|
|
|
|
|
|
|
|
sliced_chunk_iterator begin() const {
|
|
|
|
return sliced_chunk_iterator(ctx,ctx.get_latest_chunk_length()+
|
|
|
|
ctx.get_latest_chunk_start());
|
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
|
|
|
|
|
|
|
comparer_context& ctx;
|
|
|
|
};
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
/// @class scoped_chunk
|
|
|
|
///
|
|
|
|
/// @brief Utility to simplify usage of comparer_context.push_elem/pop_elem
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
class scoped_chunk {
|
|
|
|
public:
|
|
|
|
|
|
|
|
//
|
|
|
|
scoped_chunk(comparer_context& ctx,const char* msg)
|
|
|
|
: ctx(ctx)
|
|
|
|
{
|
|
|
|
ctx.push_elem(msg);
|
|
|
|
}
|
|
|
|
|
|
|
|
//
|
|
|
|
~scoped_chunk()
|
|
|
|
{
|
|
|
|
ctx.pop_elem();
|
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
|
|
|
|
|
|
|
comparer_context& ctx;
|
|
|
|
};
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
void CompareOnTheFlyMaterialProperty(comparer_context& comp) {
|
|
|
|
scoped_chunk chunk(comp,"aiMaterialProperty");
|
|
|
|
|
|
|
|
comp.cmp<aiString>("mKey");
|
|
|
|
comp.cmp<uint32_t>("mSemantic");
|
|
|
|
comp.cmp<uint32_t>("mIndex");
|
|
|
|
const uint32_t length = comp.cmp<uint32_t>("mDataLength");
|
|
|
|
const aiPropertyTypeInfo type = static_cast<aiPropertyTypeInfo>(
|
|
|
|
comp.cmp<uint32_t>("mType"));
|
|
|
|
|
|
|
|
switch (type)
|
|
|
|
{
|
|
|
|
case aiPTI_Float:
|
|
|
|
comp.cmp<float>(length/4,"mData");
|
|
|
|
break;
|
|
|
|
|
|
|
|
case aiPTI_String:
|
|
|
|
comp.cmp<aiString>("mData");
|
|
|
|
break;
|
|
|
|
|
|
|
|
case aiPTI_Integer:
|
|
|
|
comp.cmp<uint32_t>(length/4,"mData");
|
|
|
|
break;
|
|
|
|
|
|
|
|
case aiPTI_Buffer:
|
|
|
|
comp.cmp<uint8_t>(length,"mData");
|
|
|
|
break;
|
2011-06-09 14:29:32 +00:00
|
|
|
|
|
|
|
default:
|
|
|
|
break;
|
2010-03-13 00:04:21 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
void CompareOnTheFlyMaterial(comparer_context& comp) {
|
|
|
|
scoped_chunk chunk(comp,"aiMaterial");
|
|
|
|
|
|
|
|
comp.cmp<uint32_t>("aiMaterial::mNumProperties");
|
|
|
|
sliced_chunk_reader reader(comp);
|
|
|
|
for(sliced_chunk_iterator it = reader.begin(); !it.is_end(); ++it) {
|
|
|
|
if ((*it).first == ASSBIN_CHUNK_AIMATERIALPROPERTY) {
|
|
|
|
CompareOnTheFlyMaterialProperty(comp);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
void CompareOnTheFlyBone(comparer_context& comp) {
|
|
|
|
scoped_chunk chunk(comp,"aiBone");
|
|
|
|
comp.cmp<aiString>("mName");
|
|
|
|
comp.cmp<uint32_t>("mNumWeights");
|
|
|
|
comp.cmp<aiMatrix4x4>("mOffsetMatrix");
|
|
|
|
|
|
|
|
comp.cmp_bounds<aiVertexWeight>("mWeights");
|
|
|
|
}
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
void CompareOnTheFlyNodeAnim(comparer_context& comp) {
|
|
|
|
scoped_chunk chunk(comp,"aiNodeAnim");
|
|
|
|
|
|
|
|
comp.cmp<aiString>("mNodeName");
|
|
|
|
comp.cmp<uint32_t>("mNumPositionKeys");
|
|
|
|
comp.cmp<uint32_t>("mNumRotationKeys");
|
|
|
|
comp.cmp<uint32_t>("mNumScalingKeys");
|
|
|
|
comp.cmp<uint32_t>("mPreState");
|
|
|
|
comp.cmp<uint32_t>("mPostState");
|
|
|
|
|
2011-04-22 12:21:49 +00:00
|
|
|
comp.cmp_bounds<aiVectorKey>("mPositionKeys");
|
|
|
|
comp.cmp_bounds<aiQuatKey>("mRotationKeys");
|
|
|
|
comp.cmp_bounds<aiVectorKey>("mScalingKeys");
|
2010-03-13 00:04:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
void CompareOnTheFlyMesh(comparer_context& comp) {
|
|
|
|
scoped_chunk chunk(comp,"aiMesh");
|
|
|
|
|
|
|
|
comp.cmp<uint32_t>("mPrimitiveTypes");
|
|
|
|
comp.cmp<uint32_t>("mNumVertices");
|
|
|
|
const uint32_t nf = comp.cmp<uint32_t>("mNumFaces");
|
|
|
|
comp.cmp<uint32_t>("mNumBones");
|
|
|
|
comp.cmp<uint32_t>("mMaterialIndex");
|
|
|
|
|
|
|
|
const uint32_t present = comp.cmp<uint32_t>("<vertex-components-present>");
|
|
|
|
if(present & ASSBIN_MESH_HAS_POSITIONS) {
|
|
|
|
comp.cmp_bounds<aiVector3D>("mVertices");
|
|
|
|
}
|
|
|
|
|
|
|
|
if(present & ASSBIN_MESH_HAS_NORMALS) {
|
|
|
|
comp.cmp_bounds<aiVector3D>("mNormals");
|
|
|
|
}
|
|
|
|
|
|
|
|
if(present & ASSBIN_MESH_HAS_TANGENTS_AND_BITANGENTS) {
|
|
|
|
comp.cmp_bounds<aiVector3D>("mTangents");
|
|
|
|
comp.cmp_bounds<aiVector3D>("mBitangents");
|
|
|
|
}
|
|
|
|
|
|
|
|
for(unsigned int i = 0; present & ASSBIN_MESH_HAS_COLOR(i); ++i) {
|
|
|
|
std::stringstream ss;
|
|
|
|
comp.cmp_bounds<aiColor4D>((ss<<"mColors["<<i<<"]",ss.str()));
|
|
|
|
}
|
|
|
|
|
|
|
|
for(unsigned int i = 0; present & ASSBIN_MESH_HAS_TEXCOORD(i); ++i) {
|
|
|
|
std::stringstream ss;
|
|
|
|
comp.cmp<uint32_t>((ss<<"mNumUVComponents["<<i<<"]",ss.str()));
|
|
|
|
comp.cmp_bounds<aiVector3D>((ss.clear(),ss<<"mTextureCoords["<<i<<"]",ss.str()));
|
|
|
|
}
|
|
|
|
|
|
|
|
for(unsigned int i = 0; i< ((nf+511)/512); ++i) {
|
|
|
|
std::stringstream ss;
|
|
|
|
comp.cmp<uint32_t>((ss<<"mFaces["<<i*512<<"-"<<std::min(static_cast<
|
|
|
|
uint32_t>((i+1)*512),nf)<<"]",ss.str()));
|
|
|
|
}
|
|
|
|
|
|
|
|
sliced_chunk_reader reader(comp);
|
|
|
|
for(sliced_chunk_iterator it = reader.begin(); !it.is_end(); ++it) {
|
|
|
|
if ((*it).first == ASSBIN_CHUNK_AIBONE) {
|
|
|
|
CompareOnTheFlyBone(comp);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
void CompareOnTheFlyCamera(comparer_context& comp) {
|
|
|
|
scoped_chunk chunk(comp,"aiCamera");
|
|
|
|
|
|
|
|
comp.cmp<aiString>("mName");
|
|
|
|
|
|
|
|
comp.cmp<aiVector3D>("mPosition");
|
|
|
|
comp.cmp<aiVector3D>("mLookAt");
|
|
|
|
comp.cmp<aiVector3D>("mUp");
|
|
|
|
|
|
|
|
comp.cmp<float>("mHorizontalFOV");
|
|
|
|
comp.cmp<float>("mClipPlaneNear");
|
|
|
|
comp.cmp<float>("mClipPlaneFar");
|
|
|
|
comp.cmp<float>("mAspect");
|
|
|
|
}
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
void CompareOnTheFlyLight(comparer_context& comp) {
|
|
|
|
scoped_chunk chunk(comp,"aiLight");
|
|
|
|
|
|
|
|
comp.cmp<aiString>("mName");
|
|
|
|
const aiLightSourceType type = static_cast<aiLightSourceType>(
|
|
|
|
comp.cmp<uint32_t>("mType"));
|
|
|
|
|
2015-03-28 11:53:08 +00:00
|
|
|
if(type!=aiLightSource_DIRECTIONAL) {
|
2010-03-13 00:04:21 +00:00
|
|
|
comp.cmp<float>("mAttenuationConstant");
|
|
|
|
comp.cmp<float>("mAttenuationLinear");
|
|
|
|
comp.cmp<float>("mAttenuationQuadratic");
|
|
|
|
}
|
|
|
|
|
|
|
|
comp.cmp<aiVector3D>("mColorDiffuse");
|
|
|
|
comp.cmp<aiVector3D>("mColorSpecular");
|
|
|
|
comp.cmp<aiVector3D>("mColorAmbient");
|
|
|
|
|
|
|
|
if(type==aiLightSource_SPOT) {
|
|
|
|
comp.cmp<float>("mAngleInnerCone");
|
|
|
|
comp.cmp<float>("mAngleOuterCone");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
void CompareOnTheFlyAnimation(comparer_context& comp) {
|
|
|
|
scoped_chunk chunk(comp,"aiAnimation");
|
|
|
|
|
|
|
|
comp.cmp<aiString>("mName");
|
|
|
|
comp.cmp<double>("mDuration");
|
|
|
|
comp.cmp<double>("mTicksPerSecond");
|
|
|
|
comp.cmp<uint32_t>("mNumChannels");
|
|
|
|
|
|
|
|
sliced_chunk_reader reader(comp);
|
|
|
|
for(sliced_chunk_iterator it = reader.begin(); !it.is_end(); ++it) {
|
|
|
|
if ((*it).first == ASSBIN_CHUNK_AINODEANIM) {
|
|
|
|
CompareOnTheFlyNodeAnim(comp);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
void CompareOnTheFlyTexture(comparer_context& comp) {
|
|
|
|
scoped_chunk chunk(comp,"aiTexture");
|
|
|
|
|
|
|
|
const uint32_t w = comp.cmp<uint32_t>("mWidth");
|
|
|
|
const uint32_t h = comp.cmp<uint32_t>("mHeight");
|
2011-06-09 14:29:32 +00:00
|
|
|
(void)w; (void)h;
|
2010-03-13 00:04:21 +00:00
|
|
|
comp.cmp<char>("achFormatHint[0]");
|
|
|
|
comp.cmp<char>("achFormatHint[1]");
|
|
|
|
comp.cmp<char>("achFormatHint[2]");
|
|
|
|
comp.cmp<char>("achFormatHint[3]");
|
|
|
|
}
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
void CompareOnTheFlyNode(comparer_context& comp) {
|
|
|
|
scoped_chunk chunk(comp,"aiNode");
|
|
|
|
comp.cmp<aiString>("mName");
|
|
|
|
comp.cmp<aiMatrix4x4>("mTransformation");
|
|
|
|
comp.cmp<uint32_t>("mNumChildren");
|
|
|
|
comp.cmp<uint32_t>(comp.cmp<uint32_t>("mNumMeshes"),"mMeshes");
|
|
|
|
|
|
|
|
sliced_chunk_reader reader(comp);
|
|
|
|
for(sliced_chunk_iterator it = reader.begin(); !it.is_end(); ++it) {
|
|
|
|
if ((*it).first == ASSBIN_CHUNK_AINODE) {
|
|
|
|
CompareOnTheFlyNode(comp);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
void CompareOnTheFlyScene(comparer_context& comp) {
|
|
|
|
scoped_chunk chunk(comp,"aiScene");
|
|
|
|
|
|
|
|
comp.cmp<uint32_t>("mFlags");
|
|
|
|
comp.cmp<uint32_t>("mNumMeshes");
|
|
|
|
comp.cmp<uint32_t>("mNumMaterials");
|
|
|
|
comp.cmp<uint32_t>("mNumAnimations");
|
|
|
|
comp.cmp<uint32_t>("mNumTextures");
|
|
|
|
comp.cmp<uint32_t>("mNumLights");
|
|
|
|
comp.cmp<uint32_t>("mNumCameras");
|
|
|
|
|
|
|
|
sliced_chunk_reader reader(comp);
|
|
|
|
for(sliced_chunk_iterator it = reader.begin(); !it.is_end(); ++it) {
|
|
|
|
if ((*it).first == ASSBIN_CHUNK_AIMATERIAL) {
|
|
|
|
CompareOnTheFlyMaterial(comp);
|
|
|
|
}
|
|
|
|
else if ((*it).first == ASSBIN_CHUNK_AITEXTURE) {
|
|
|
|
CompareOnTheFlyTexture(comp);
|
|
|
|
}
|
|
|
|
else if ((*it).first == ASSBIN_CHUNK_AIMESH) {
|
|
|
|
CompareOnTheFlyMesh(comp);
|
|
|
|
}
|
|
|
|
else if ((*it).first == ASSBIN_CHUNK_AIANIMATION) {
|
|
|
|
CompareOnTheFlyAnimation(comp);
|
|
|
|
}
|
|
|
|
else if ((*it).first == ASSBIN_CHUNK_AICAMERA) {
|
|
|
|
CompareOnTheFlyCamera(comp);
|
|
|
|
}
|
|
|
|
else if ((*it).first == ASSBIN_CHUNK_AILIGHT) {
|
|
|
|
CompareOnTheFlyLight(comp);
|
|
|
|
}
|
|
|
|
else if ((*it).first == ASSBIN_CHUNK_AINODE) {
|
|
|
|
CompareOnTheFlyNode(comp);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
void CompareOnTheFly(comparer_context& comp)
|
|
|
|
{
|
|
|
|
sliced_chunk_reader reader(comp);
|
|
|
|
for(sliced_chunk_iterator it = reader.begin(); !it.is_end(); ++it) {
|
|
|
|
if ((*it).first == ASSBIN_CHUNK_AISCENE) {
|
|
|
|
CompareOnTheFlyScene(comp);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
void CheckHeader(comparer_context& comp)
|
|
|
|
{
|
|
|
|
fseek(comp.get_actual(),ASSBIN_HEADER_LENGTH,SEEK_CUR);
|
|
|
|
fseek(comp.get_expect(),ASSBIN_HEADER_LENGTH,SEEK_CUR);
|
|
|
|
}
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
int Assimp_CompareDump (const char* const* params, unsigned int num)
|
|
|
|
{
|
|
|
|
// --help
|
2011-06-09 14:29:32 +00:00
|
|
|
if ((num == 1 && !strcmp( params[0], "-h")) || !strcmp( params[0], "--help") || !strcmp( params[0], "-?") ) {
|
2010-03-13 00:04:21 +00:00
|
|
|
printf("%s",AICMD_MSG_CMPDUMP_HELP);
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// assimp cmpdump actual expected
|
|
|
|
if (num < 1) {
|
|
|
|
std::cout << "assimp cmpdump: Invalid number of arguments. "
|
|
|
|
"See \'assimp cmpdump --help\'\r\n" << std::endl;
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
if(!strcmp(params[0],params[1])) {
|
|
|
|
std::cout << "assimp cmpdump: same file, same content." << std::endl;
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
FILE* actual = fopen(params[0],"rb"), *expected = fopen(params[1],"rb");
|
|
|
|
if (!actual) {
|
|
|
|
std::cout << "assimp cmpdump: Failure reading ACTUAL data from " <<
|
|
|
|
params[0] << std::endl;
|
|
|
|
return -5;
|
|
|
|
}
|
|
|
|
if (!expected) {
|
|
|
|
std::cout << "assimp cmpdump: Failure reading EXPECT data from " <<
|
|
|
|
params[1] << std::endl;
|
|
|
|
return -6;
|
|
|
|
}
|
|
|
|
|
|
|
|
comparer_context comp(actual,expected);
|
|
|
|
try {
|
|
|
|
CheckHeader(comp);
|
|
|
|
CompareOnTheFly(comp);
|
|
|
|
}
|
|
|
|
catch(const compare_fails_exception& ex) {
|
|
|
|
printf("%s",ex.what());
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
catch(...) {
|
|
|
|
// we don't bother checking too rigourously here, so
|
|
|
|
// we might end up here ...
|
|
|
|
std::cout << "Unknown failure, are the input files well-defined?";
|
|
|
|
return -3;
|
|
|
|
}
|
|
|
|
|
|
|
|
std::cout << "Success (totally " << std::dec << comp.get_num_chunks() <<
|
|
|
|
" chunks)" << std::endl;
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|