Since each `TestFrame` now has a metadata header, we can store an additional data checksum there, so that it is now possible both to detect if data is in pristine state, or if it matches a changed state recorded in the additional checksum. So we have now three different levels of verification isSane:: consistent metadata header found isValid:: metadata header found and checksum there matches data isPristine:: in addition, the data is exactly as generated from the `(frameNr,family)`
466 lines
14 KiB
C++
466 lines
14 KiB
C++
/*
|
||
TestFrame - test data frame (stub) for checking Render engine functionality
|
||
|
||
Copyright (C)
|
||
2011, Hermann Vosseler <Ichthyostega@web.de>
|
||
|
||
**Lumiera** is free software; you can redistribute it and/or modify it
|
||
under the terms of the GNU General Public License as published by the
|
||
Free Software Foundation; either version 2 of the License, or (at your
|
||
option) any later version. See the file COPYING for further details.
|
||
|
||
* *****************************************************************/
|
||
|
||
/** @file testframe.cpp
|
||
** Implementation of fake data frames to support unit testing.
|
||
** The data generation is based on a _discriminator seed value,_
|
||
** which is computed as a linear combination of a statically fixed anchor-seed
|
||
** combined with the family-number and sequence number. Based on this seed,
|
||
** the contents are then filled by a pseudo-random sequence.
|
||
** @note while initially drawn from real entropy, the anchor-seed can be
|
||
** reset from the default PRNG, which allows to establish a totally
|
||
** deterministically setup from test code, because the test itself
|
||
** can seed the default PRNG and thus establish a reproducible state.
|
||
**
|
||
** Additionally, beyond this basic test-data feature, the contents can be
|
||
** manipulated freely, and a new checksum can be stored in the metadata,
|
||
** which allows to build pseudo media computation functions with a
|
||
** reproducible effect — so that the proper invocation of several
|
||
** computation steps invoked deep down in the render engine can
|
||
** be verified after completing a test invocation.
|
||
*/
|
||
|
||
|
||
#include "lib/error.hpp"
|
||
#include "lib/random.hpp"
|
||
#include "lib/hash-standard.hpp"
|
||
#include "lib/hash-combine.hpp"
|
||
#include "steam/engine/testframe.hpp"
|
||
#include "lib/nocopy.hpp"
|
||
#include "lib/util.hpp"
|
||
|
||
#include <climits>
|
||
#include <memory>
|
||
#include <vector>
|
||
|
||
|
||
|
||
namespace steam {
|
||
namespace engine{
|
||
namespace test {
|
||
namespace err = lumiera::error;
|
||
|
||
using util::unConst;
|
||
using std::vector;
|
||
|
||
/** @note using a random-congruential engine to generate the payload data */
|
||
using PseudoRandom = lib::RandomSequencer<std::minstd_rand>;
|
||
|
||
|
||
namespace error = lumiera::error;
|
||
|
||
namespace { // hidden local support facilities....
|
||
|
||
/**
|
||
* Offset to set the seed values of »families« apart.
|
||
* The data in the test frames is generated from a distinctive ID-seed,
|
||
* which is controlled by the _family_ and the _seq-No_ within each family.
|
||
* The seeds for consecutive frames are spread apart by the #dataSeed,
|
||
* and the SEQUENCE_SPREAD constant acts as minimum spread. While seed
|
||
* values can wrap within the 64bit number range, this generation scheme
|
||
* makes it very unlikely that neighbouring frames end up with the same seed.
|
||
*/
|
||
const size_t SEQUENCE_SPREAD = 100;
|
||
|
||
HashVal
|
||
drawSeed (lib::Random& srcGen)
|
||
{
|
||
return srcGen.distribute(
|
||
std::uniform_int_distribution<HashVal>{SEQUENCE_SPREAD
|
||
,std::numeric_limits<HashVal>::max()-SEQUENCE_SPREAD});
|
||
}
|
||
|
||
/** @internal a static seed hash used to anchor the data distinction ID-seeds */
|
||
HashVal dataSeed{drawSeed(lib::entropyGen)};
|
||
|
||
/** @internal helper for generating unique test frames.
|
||
* This »discriminator« is used as a random seed when filling the test frame data buffers.
|
||
* It is generated to be very likely different on adjacent frames of the same series,
|
||
* as well as to differ to all nearby neighbouring channels.
|
||
* @note the #dataSeed hash is limited by #SEQUENCE_SPREAD to prevent „risky“ families;
|
||
* the extreme case would be dataSeed+family ≡ 0 (all frames would be equal then)
|
||
* @param seq the sequence number of the frame within the channel
|
||
* @param family the channel this frame belongs to
|
||
*/
|
||
uint64_t
|
||
generateDiscriminator(uint seq, uint family)
|
||
{
|
||
// use the family as stepping
|
||
return (seq+1) * (dataSeed+family);
|
||
}
|
||
|
||
class DistinctNucleus
|
||
: public lib::SeedNucleus
|
||
, util::MoveOnly
|
||
{
|
||
uint64_t const& fixPoint_;
|
||
public:
|
||
DistinctNucleus(uint64_t const& anchor)
|
||
: fixPoint_{anchor}
|
||
{ }
|
||
|
||
uint64_t
|
||
getSeed() override
|
||
{
|
||
return fixPoint_;
|
||
}
|
||
};
|
||
|
||
/** @return a stable characteristic memory marker for the metadata record */
|
||
HashVal
|
||
stampHeader()
|
||
{
|
||
static const HashVal MARK = lib::entropyGen.hash()
|
||
| 0b1000'1000'1000'1000'1000'1000'1000'1000; //////////////////////////////TICKET #722 : not portable because HashVal ≡ size_t — should it be?
|
||
return MARK;
|
||
}
|
||
|
||
/** @internal build a PRNG starting from the referred fixed seed */
|
||
auto
|
||
buildDataGenFrom (uint64_t const& anchor)
|
||
{
|
||
DistinctNucleus seed{anchor};
|
||
return PseudoRandom{seed};
|
||
}
|
||
|
||
|
||
TestFrame&
|
||
accessAsTestFrame (void* memoryLocation)
|
||
{
|
||
REQUIRE (memoryLocation);
|
||
return *reinterpret_cast<TestFrame*> (memoryLocation);
|
||
}
|
||
|
||
|
||
/**
|
||
* @internal table to hold test data frames.
|
||
* These frames are built on demand, but retained thereafter.
|
||
* Some tests might rely on the actual memory locations, using the
|
||
* test frames to simulate a real input frame data stream.
|
||
* @param CHA the maximum number of channels to expect
|
||
* @param FRA the maximum number of frames to expect per channel
|
||
* @warning choose the maximum number parameters wisely.
|
||
* We're allocating memory to hold a table of test frames
|
||
* e.g. sizeof(TestFrame) * 20channels * 100frames ≈ 2 MiB
|
||
* The table uses vectors, and thus will grow on demand,
|
||
* but this might cause existing frames to be relocated in memory;
|
||
* some tests might rely on fixed memory locations. Just be cautious!
|
||
*/
|
||
template<uint CHA, uint FRA>
|
||
struct TestFrameTable
|
||
: vector<vector<TestFrame>>
|
||
{
|
||
typedef vector<vector<TestFrame>> VECT;
|
||
|
||
TestFrameTable()
|
||
: VECT(CHA)
|
||
{
|
||
for (uint i=0; i<CHA; ++i)
|
||
at(i).reserve(FRA);
|
||
}
|
||
|
||
TestFrame&
|
||
getFrame (uint seqNr, uint chanNr=0)
|
||
{
|
||
if (chanNr >= this->size())
|
||
{
|
||
WARN (test, "Growing table of test frames to %d channels, "
|
||
"which is > the default (%d)", chanNr, CHA);
|
||
resize(chanNr+1);
|
||
}
|
||
ENSURE (chanNr < this->size());
|
||
vector<TestFrame>& channel = at(chanNr);
|
||
|
||
if (seqNr >= channel.size())
|
||
{
|
||
WARN_IF (seqNr >= FRA, test,
|
||
"Growing channel #%d of test frames to %d elements, "
|
||
"which is > the default (%d)", chanNr, seqNr, FRA);
|
||
for (uint i=channel.size(); i<=seqNr; ++i)
|
||
channel.push_back (TestFrame (i,chanNr));
|
||
}
|
||
ENSURE (seqNr < channel.size());
|
||
|
||
return channel[seqNr];
|
||
}
|
||
};
|
||
|
||
const uint INITIAL_CHAN = 20;
|
||
const uint INITIAL_FRAMES = 100;
|
||
|
||
typedef TestFrameTable<INITIAL_CHAN,INITIAL_FRAMES> TestFrames;
|
||
|
||
std::unique_ptr<TestFrames> testFrames;
|
||
|
||
|
||
TestFrame&
|
||
accessTestFrame (uint seqNr, uint chanNr)
|
||
{
|
||
if (!testFrames) testFrames.reset (new TestFrames);
|
||
|
||
return testFrames->getFrame(seqNr,chanNr);
|
||
}
|
||
|
||
} // (End) hidden impl details
|
||
|
||
|
||
|
||
|
||
TestFrame&
|
||
testData (uint seqNr, uint chanNr)
|
||
{
|
||
return accessTestFrame (seqNr,chanNr);
|
||
}
|
||
|
||
|
||
|
||
/**
|
||
* @remark this function should be invoked at the start of any test
|
||
* which requires reproducible data values in the TestFrame.
|
||
* It generates a new base seed to distinguish individual data frames.
|
||
* The seed is drawn from the \ref lib::defaultGen, and thus will be
|
||
* reproducible if the latter has been reseeded beforehand.
|
||
* @warning after invoking reseed(), the validity of previously generated
|
||
* frames can no longer be verified.
|
||
*/
|
||
void
|
||
TestFrame::reseed()
|
||
{
|
||
testFrames.reset();
|
||
dataSeed = drawSeed (lib::defaultGen);
|
||
}
|
||
|
||
|
||
|
||
|
||
/* ===== TestFrame class ===== */
|
||
|
||
TestFrame::Meta::Meta (uint seq, uint family)
|
||
: _MARK_{stampHeader()}
|
||
, checksum{0}
|
||
, distinction{generateDiscriminator (seq,family)}
|
||
, stage{CREATED}
|
||
{ }
|
||
|
||
TestFrame::~TestFrame()
|
||
{
|
||
header_.stage = DISCARDED;
|
||
}
|
||
|
||
|
||
TestFrame::TestFrame (uint seq, uint family)
|
||
: header_{seq,family}
|
||
{
|
||
buildData();
|
||
ASSERT (0 < header_.distinction);
|
||
ENSURE (CREATED == header_.stage);
|
||
ENSURE (isPristine());
|
||
}
|
||
|
||
|
||
TestFrame::TestFrame (TestFrame const& o)
|
||
: header_{o.header_}
|
||
{
|
||
data() = o.data();
|
||
header_.stage = CREATED;
|
||
}
|
||
|
||
TestFrame&
|
||
TestFrame::operator= (TestFrame const& o)
|
||
{
|
||
if (not isAlive())
|
||
throw err::Logic ("target TestFrame already dead or unaccessible");
|
||
if (not util::isSameAdr (this, o))
|
||
{
|
||
data() = o.data();
|
||
header_ = o.header_;
|
||
header_.stage = CREATED;
|
||
}
|
||
return *this;
|
||
}
|
||
|
||
|
||
/**
|
||
* Sanity check on the metadata header.
|
||
* @remark Relevant to detect memory corruption or when accessing some
|
||
* arbitrary memory location, which may or may not actually hold a TestFrame.
|
||
* Based on the assumption that it is unlikely that some random memory location
|
||
* just happens to hold our [marker word](\ref stampHeader()).
|
||
* @note this is only the base level of verification, because in addition
|
||
* \ref isValid verifies the checksum and \ref isPristine additionally
|
||
* recomputes the data generation to see if it matches the Meta::distinction
|
||
*/
|
||
bool
|
||
TestFrame::Meta::isPlausible() const
|
||
{
|
||
return _MARK_ == stampHeader()
|
||
and stage <= DISCARDED;
|
||
}
|
||
|
||
TestFrame::Meta&
|
||
TestFrame::accessHeader()
|
||
{
|
||
if (not header_.isPlausible())
|
||
throw err::Invalid{"TestFrame: missing or corrupted metadata"};
|
||
return header_;
|
||
}
|
||
TestFrame::Meta const&
|
||
TestFrame::accessHeader() const
|
||
{
|
||
return unConst(this)->accessHeader();
|
||
}
|
||
|
||
TestFrame::StageOfLife
|
||
TestFrame::currStage() const
|
||
{
|
||
return header_.isPlausible()? header_.stage
|
||
: DISCARDED;
|
||
}
|
||
|
||
bool
|
||
TestFrame::operator== (void* memLocation) const
|
||
{
|
||
TestFrame& candidate (accessAsTestFrame (memLocation));
|
||
return candidate.isSane()
|
||
&& candidate == *this;
|
||
}
|
||
|
||
bool
|
||
TestFrame::Meta::operator== (Meta const&o) const
|
||
{
|
||
return isPlausible() and o.isPlausible()
|
||
and stage == o.stage
|
||
and checksum == o.checksum
|
||
and distinction == o.distinction;
|
||
}
|
||
|
||
bool
|
||
TestFrame::contentEquals (TestFrame const& o) const
|
||
{
|
||
return header_ == o.header_
|
||
and data() == o.data();
|
||
}
|
||
|
||
|
||
/**
|
||
* Generate baseline data content based on the Meta::distinction seed.
|
||
* @remark the seed is a [discriminator](\ref buildDiscriminator) based
|
||
* on both the »family« and the frameNo within this family;
|
||
* thus closely related frames are very unlikely to hold the same
|
||
* baseline data. Of course, follow-up manipulations could change
|
||
* the data, which should be documented by \ref markChecksum().
|
||
*/
|
||
void
|
||
TestFrame::buildData()
|
||
{
|
||
auto gen = buildDataGenFrom (accessHeader().distinction);
|
||
for (char& dat : data())
|
||
dat = char(gen.i(CHAR_MAX));
|
||
markChecksum();
|
||
}
|
||
|
||
/** verify the current data was not touched since initialisation
|
||
* @remark implemented by regenerating the data sequence deterministically,
|
||
* based on the Meta::distinction mark recorded in the metadata.
|
||
*/
|
||
bool
|
||
TestFrame::matchDistinction() const
|
||
{
|
||
auto gen = buildDataGenFrom (accessHeader().distinction);
|
||
for (char const& dat : data())
|
||
if (dat != char(gen.i(CHAR_MAX)))
|
||
return false;
|
||
return true;
|
||
}
|
||
|
||
/** @return a hash checksum computed over current data content */
|
||
HashVal
|
||
TestFrame::computeChecksum() const
|
||
{
|
||
HashVal checksum{0};
|
||
std::hash<char> getHash;
|
||
for (char const& dat : data())
|
||
lib::hash::combine (checksum, getHash (dat));
|
||
return checksum;
|
||
}
|
||
|
||
/** @remark can be used to mark a manipulated new content as _valid_ */
|
||
HashVal
|
||
TestFrame::markChecksum()
|
||
{
|
||
return accessHeader().checksum = computeChecksum();
|
||
}
|
||
|
||
|
||
bool
|
||
TestFrame::hasValidChecksum() const
|
||
{
|
||
return accessHeader().checksum == computeChecksum();
|
||
}
|
||
|
||
bool
|
||
TestFrame::isSane() const
|
||
{
|
||
return header_.isPlausible();
|
||
}
|
||
|
||
bool
|
||
TestFrame::isValid() const
|
||
{
|
||
return isSane()
|
||
and hasValidChecksum();
|
||
}
|
||
|
||
bool
|
||
TestFrame::isPristine() const
|
||
{
|
||
return isValid()
|
||
and matchDistinction();
|
||
}
|
||
|
||
bool
|
||
TestFrame::isAlive() const
|
||
{
|
||
return isSane()
|
||
and not isDead();
|
||
}
|
||
|
||
bool
|
||
TestFrame::isDead() const
|
||
{
|
||
return isSane()
|
||
and (DISCARDED == currStage());
|
||
}
|
||
|
||
/** @note performing an unchecked conversion of the given
|
||
* memory location to be accessed as TestFrame.
|
||
* The sanity of the data found at that location
|
||
* is checked as well, not only the lifecycle flag.
|
||
*/
|
||
bool
|
||
TestFrame::isAlive (void* memLocation)
|
||
{
|
||
TestFrame& candidate (accessAsTestFrame (memLocation));
|
||
return candidate.isAlive();
|
||
}
|
||
|
||
bool
|
||
TestFrame::isDead (void* memLocation)
|
||
{
|
||
TestFrame& candidate (accessAsTestFrame (memLocation));
|
||
return candidate.isDead();
|
||
}
|
||
|
||
|
||
|
||
}}} // namespace steam::engine::test
|