LUMIERA.clone/tests/core/steam/engine/testframe.cpp
Ichthyostega 5a203513c9 Library: ensure automatic re-seeding for TestFrame
This closes a ''conceptual gap.''

There was a `lib::TestFrame::reseed()` function, which should be invoked
whenever a clean / deterministinc seed is required for computations involving
`TestFrame` data.

On occasion I noticed that most actual usages of `TestFrame`, which became quite common
lately, were missing this `reseed()` call, while the enclosing test
had indeed a proper Test::seedRand() call.

So my conclusion is: if even ''I myself'' miss that fine point frequently,
it is justified to automated this setup, even while `TestFrame` is right now
the only client of such a setup-facility.

There is a twist though.
`TestFrame` is part of the ''Engine'' test coverage, i.e. it is linked
into `libtest-core.so`. While the support for random numbers and reseeding
is obviously a very basic library feature, and linked into `liblumiera.so`

**Oh well**.
So now we have ''yet another'' static registration service.
I kept everything as simple as possible, which means, we continue
to use the ''common, global random generator'' from the library,
and there is no protection against some other code using those
library functions in a non-deterministic way.

`lib/random-reseed.cpp` is just a glorified hard-wired setup, beware!
2026-02-23 00:05:44 +01:00

443 lines
13 KiB
C++
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
TestFrame - test data frame (stub) for checking Render engine functionality
Copyright (C)
2011, 2024 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/random-reseed.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 <deque>
namespace steam {
namespace engine{
namespace test {
namespace err = lumiera::error;
using std::deque;
using util::unConst;
/** @note using a random-congruential engine to generate the payload data */
using PseudoRandom = lib::RandomSequencer<std::minstd_rand>;
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& distinction_;
public:
DistinctNucleus(uint64_t const& anchor)
: distinction_{anchor}
{ }
uint64_t
getSeed() override
{
return distinction_;
}
};
/** @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 const&
accessAsTestFrame (void const* memoryLocation)
{
REQUIRE (memoryLocation);
return *reinterpret_cast<TestFrame const*> (memoryLocation);
}
/* ======= static TestFrame repository ======= */
/** @internal table to hold test data frames in heap memory.
* 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.
* @note \ref TestFrame::reseed() also discards this storage, which
* otherwise is retained at stable location until process end.
*/
struct TestFrameTable
: deque<deque<TestFrame>>
{
TestFrameTable() = default;
TestFrame&
getFrame (uint seqNr, uint chanNr)
{
if (chanNr >= this->size())
resize(chanNr+1);
ENSURE (chanNr < this->size());
deque<TestFrame>& channel = at(chanNr);
if (seqNr >= channel.size())
{
INFO (test, "Growing channel #%d of test frames %d -> %d elements."
, chanNr, channel.size(), seqNr+1);
for (uint nr=channel.size(); nr<=seqNr; ++nr)
channel.emplace_back (nr, chanNr);
}
ENSURE (seqNr < channel.size());
return channel[seqNr];
}
};
//
std::unique_ptr<TestFrameTable> testFrames;
//
}// (End) hidden impl details
TestFrame&
testData (uint seqNr, uint chanNr)
{
if (not testFrames)
testFrames = std::make_unique<TestFrameTable>();
return testFrames->getFrame (seqNr, chanNr);
}
/**
* @remark this function can be invoked whenever either completely new
* or reproducible data values are required in TestFrame instances.
* 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.
* @note A default registration setup ensures this function is invoked
* from each call of test::Test::seedRand()
* @warning after invoking reseed(), the validity of previously generated
* frames can no longer be verified.
*/
void
TestFrame::reseed()
{
testFrames.reset(); // discard existing test data repository
dataSeed = drawSeed (lib::defaultGen);
}
namespace { // ---------------automatic-registration-for-uint-tests-----------------
const uint RESEED_SLOT{1};
static uint _registration_ = lib::register_at_seq (RESEED_SLOT, &TestFrame::reseed );
}
/* ======= 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 any given memory location
* just happens to hold our [marker word](\ref stampHeader()) by accident.
* @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::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::operator== (void const* memLocation) const
{
TestFrame const& candidate (accessAsTestFrame (memLocation));
return candidate.isSane()
and candidate == *this;
}
bool
TestFrame::contentEquals (TestFrame const& o) const
{
return 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 (uint64_t& dat : data64())
dat = gen.u64();
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 (uint64_t const& dat : data64())
if (dat != gen.u64())
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();
}
/** access current checksum without recomputing. */
HashVal
TestFrame::getChecksum() const
{
return unConst(this)->accessHeader().checksum;
}
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 const& candidate (accessAsTestFrame (memLocation));
return candidate.isAlive();
}
bool
TestFrame::isDead (void* memLocation)
{
TestFrame const& candidate (accessAsTestFrame (memLocation));
return candidate.isDead();
}
}}} // namespace steam::engine::test