From 5a203513c95e9ddb9d18ad47f366a2fe801f779d Mon Sep 17 00:00:00 2001 From: Ichthyostega Date: Sun, 22 Feb 2026 21:59:43 +0100 Subject: [PATCH] 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! --- src/lib/random-reseed.cpp | 95 +++++ src/lib/random-reseed.hpp | 76 ++++ src/lib/test/suite.cpp | 3 +- tests/core/steam/engine/node-devel-test.cpp | 1 - .../engine/output-proxy-provider-test.cpp | 1 - tests/core/steam/engine/testframe-test.cpp | 3 +- tests/core/steam/engine/testframe.cpp | 12 +- wiki/thinkPad.ichthyo.mm | 363 ++++++++++++++++++ 8 files changed, 547 insertions(+), 7 deletions(-) create mode 100644 src/lib/random-reseed.cpp create mode 100644 src/lib/random-reseed.hpp diff --git a/src/lib/random-reseed.cpp b/src/lib/random-reseed.cpp new file mode 100644 index 000000000..5d571ef15 --- /dev/null +++ b/src/lib/random-reseed.cpp @@ -0,0 +1,95 @@ +/* + RandomReseed - establish controlled seed for deterministic randomness + + Copyright (C) + 2026, Hermann Vosseler + +  **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 random-reseed.cpp + ** Implementation of coordinated reseeding of random number generators. + ** @todo 3/2024 work out how some executions or performances are configured reproducibly. + */ + + +#include "lib/error.hpp" +#include "lib/random.hpp" +#include "lib/random-reseed.hpp" +#include "lib/format-string.hpp" +#include "lib/iter-adapter-stl.hpp" +#include "lib/util.hpp" + +#include + +namespace lib { + namespace err = lumiera::error; + + using util::_Fmt; + using util::contains; + using iter_stl::eachVal; + + typedef void (*Hook)(); + + + namespace {// Implementation: a central registry for callbacks... + + class RandomReseed + : util::NonCopyable + { + std::map registry_; + + public: + void + enrol (uint idx, Hook callback) + { + if (contains (registry_, idx)) + throw err::Fatal{_Fmt{"Duplicate registration for random-reseeding-slot %d"} % idx}; + + registry_[idx] = callback; + } + + void + triggerAll() + { + for (Hook hook : eachVal (registry_)) + (*hook)(); + } + }; + + /** + * Static internal access to a global (hidden) registry. + * @note use Meyer's Singleton to allow registration from + * a static-init context, without prerequisites. + */ + inline RandomReseed& + randomReseedService() + { + static RandomReseed registry; + return registry; + } + }//(End)hidden impl. + + + + uint + register_at_seq (uint idx, Hook callback) + { + randomReseedService().enrol (idx, callback); + return idx; + } + + void + reseed_for_test (SeedNucleus& reseedingNucleus) + { + lib::defaultGen.reseed (reseedingNucleus); + randomReseedService().triggerAll(); + } + + +} // namespace lib diff --git a/src/lib/random-reseed.hpp b/src/lib/random-reseed.hpp new file mode 100644 index 000000000..96b886619 --- /dev/null +++ b/src/lib/random-reseed.hpp @@ -0,0 +1,76 @@ +/* + RANDOM-RESEED.hpp - establish controlled seed for deterministic randomness + + Copyright (C) + 2026, Hermann Vosseler + +  **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 random-reseed.hpp + ** A setup to allow for coordinated reseeding of otherwise independent components. + ** The primary purpose of this setup is to allow for probabilistic unit-testing, + ** yet including some facilities which are more intricate to setup. These will have + ** to configure themselves to be registered with this random-reseeding service. + ** Each registration-»slot« allocates an unique index number, and for each such + ** index, some random values will be drawn from the global lib::defaultGen. + ** @remark reseeding for »deterministic randomness« is an intricate topic. + ** This setup is intended to be used as "one-stop solution" for all basic stuff, + ** commonly used in unit tests whenever "some random XYZ" seems appropriate. + ** Obviously, such a solution can only be provisional, and must be augmented, + ** once new requirements arise. In 2026, using pseudo-random values and chained + ** hash computations has become quite commonplace in the Lumiera test suite; + ** since 2024, there is a framework in place to allow for controlled re-seeding + ** and deterministic computation. However, in practice, the necessary setup + ** and initialisation was overlooked frequently, and thus we now have to raise + ** to the next level, by providing this one and central implementation function. + ** @warning _deliberately_ there is no coordination in place to ensure consistency + ** of registration or initialisation. This is only a convenient front-end, while + ** the actual registration and setup remains essentially hard-wired. + ** @see Test::seedRand() + ** @see Random_test + */ + + +#ifndef LIB_RANDOM_RESEED_H +#define LIB_RANDOM_RESEED_H + + +#include "lib/integral.hpp" + + +namespace lib { + + class SeedNucleus; // see lib/random.hpp + + + /** + * Install a callback \a Hook to be invoked whenever + * _reseeding_ of random number generators becomes necessary. + * @param idx number of a distinct »slot« to allocate; + * invocation will happen in ascending order + * @remark this function can be used to plant a static variable + * to ensure registration happens early and once within + * some implementation translation unit. The callback + * is called way later, typically when the first test + * case invokes test::Test::seedRand() + */ + uint register_at_seq (uint idx, void(*)(void) ); + + /** + * Reseed generic randomness used for probabilistic unit-testing. + * After calling this function, the default random generator in the Lumiera library + * is reconfigured and reseeded based on the given seeding nucleus; furthermore, a + * small, hard-wired collection of test facilities will be re-set and re-seeded + * from this newly configured random source. + */ + void reseed_for_test (SeedNucleus&); + + +} // namespace lib +#endif /*LIB_RANDOM_RESEED_H*/ diff --git a/src/lib/test/suite.cpp b/src/lib/test/suite.cpp index d0b6af2b4..2f1a096d9 100644 --- a/src/lib/test/suite.cpp +++ b/src/lib/test/suite.cpp @@ -20,6 +20,7 @@ #include "lib/error.hpp" #include "lib/symbol.hpp" +#include "lib/random-reseed.hpp" #include "lib/format-cout.hpp" #include "lib/test/suite.hpp" #include "lib/test/run.hpp" @@ -210,7 +211,7 @@ namespace test { void Test::seedRand() { - lib::defaultGen.reseed (suiteSeed); + lib::reseed_for_test (suiteSeed); } diff --git a/tests/core/steam/engine/node-devel-test.cpp b/tests/core/steam/engine/node-devel-test.cpp index 3f5a81ed9..d86e3e364 100644 --- a/tests/core/steam/engine/node-devel-test.cpp +++ b/tests/core/steam/engine/node-devel-test.cpp @@ -78,7 +78,6 @@ namespace test { run (Arg) { seedRand(); - TestFrame::reseed(); processing_generateFrame(); processing_generateMultichan(); diff --git a/tests/core/steam/engine/output-proxy-provider-test.cpp b/tests/core/steam/engine/output-proxy-provider-test.cpp index c59484a3d..14cef882e 100644 --- a/tests/core/steam/engine/output-proxy-provider-test.cpp +++ b/tests/core/steam/engine/output-proxy-provider-test.cpp @@ -54,7 +54,6 @@ namespace test { // Assuming some data block is »given« seedRand(); - TestFrame::reseed(); size_t frameNr = defaultGen.u64(); TestFrame dataBlock (frameNr); CHECK ( dataBlock.isPristine()); diff --git a/tests/core/steam/engine/testframe-test.cpp b/tests/core/steam/engine/testframe-test.cpp index eba99406c..45699b6e9 100644 --- a/tests/core/steam/engine/testframe-test.cpp +++ b/tests/core/steam/engine/testframe-test.cpp @@ -70,8 +70,7 @@ namespace test { virtual void run (Arg) { - seedRand(); - TestFrame::reseed(); + seedRand(); // ◁—————————————————————————— Note: TestFrame::reseed() automatically invoked here simpleUsage(); verifyDataContent(); diff --git a/tests/core/steam/engine/testframe.cpp b/tests/core/steam/engine/testframe.cpp index 31e618c24..09c8adee3 100644 --- a/tests/core/steam/engine/testframe.cpp +++ b/tests/core/steam/engine/testframe.cpp @@ -33,6 +33,7 @@ #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" @@ -192,11 +193,13 @@ namespace test { /** - * @remark this function should be invoked at the start of any test - * which requires reproducible data values in the TestFrame. + * @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. */ @@ -207,6 +210,11 @@ namespace test { 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 ); + } + diff --git a/wiki/thinkPad.ichthyo.mm b/wiki/thinkPad.ichthyo.mm index 2bc700a06..a82906033 100644 --- a/wiki/thinkPad.ichthyo.mm +++ b/wiki/thinkPad.ichthyo.mm @@ -60350,6 +60350,32 @@ + + + + +

+ Das ist nicht eigentlich ein Bug, aber etwas, was man natürlicherweise erwarten würde; bisher habe ich mich auf den Standpunkt gestellt, daß man sich da schon selber darum kümmern sollte. Wenn ich jetzt aber durch alle Tests suche, die TestFrame verwenden, finde ich fast keinen, der ein korrektes re-Seeding macht, aber die meisten haben immerhin ein Test::seedRand() +

+ +
+ + + + + + +

+ das muß nach dem Herstellen eines deterministischen Master-Seed passieren +

+ +
+
+ + + + +
@@ -61649,6 +61675,343 @@ + + + + + + + +

+ Es wird in Zukunft immer wieder Einrichtungen geben, die neu re-seeded werden müssen, um deterministisch zu sein. Dieses re-Seeding muß zuverlässig und reproduzierbar erfolgen, nachdem der globale PRNG neu initialisiert wurde. +

+ + +
+ + + + + + +

+ ...jede Art von Einrichtung, die in komplexer Weise vom Random-Seed abhängt, und bei korrektem Setup deterministisch gemacht werden kann; also immer dann, wenn dieses Setup »schwierig« wird! Als Kontrast: für einen banalen Test zieht man sich einfach eine Zufallszahl und gut is; inwiefern das Test-Ergabnis von dieser Zufallszahl abhängt, ist in einem solchen Fall komplett offensichtlich. +

+ + +
+
+ + + + + + +

+ TestFrame definiert nicht nur einen Mechanismus, einen Datenblock von Zufallszahlen zu erzeugen, sondern er definiert auch deterministische Sequenzen und Familien solcher Sequenzen. Diese werden aus einem »Diskriminator« gebildet. Per TestFrame::reseed() verwirft man alle bestehenden Testframes und zieht einen neuen Diskriminator. Sofern ein Test mit Test-Frames einzeln reproduzierbar sein soll, muß dieres reseed() aufgerufen werden. Das übersieht man natürlich leicht, denn normalerweise will man solche Tests nicht deterministisch wiederholen (was solange gilt, bis es mal ein ernstes Problem gibt) +

+ + +
+ + + + +
+
+ + + + + + + + + + +

+ Wenn die Lumiera-Applikation mal etwas Vergleichbares braucht, muß es anderweitig an den Application-Lifecycle gebunden werden +

+ + +
+
+ + + + + + +

+ soll aber ohne Weiteres stets für jeden Test verfügbar sein +

+ + +
+
+ + + + + + + + + + + + + + +

+ Explizit ausformuliert: +

+
    +
  • + in der Tat sich Probleme absehbar +
  • +
  • + aber wir warten mit der Lösung bis sich diese erwartbaren Probleme tatsächlich manifestieren +
  • +
+ + +
+
+ + + + + + + +

+ Jede Einrichtung, die am re-Seeding teilnehmen muß, hat sich da selbst in den Code zu integrieren. Sobald sich diese Integrierung ändert, ändert sich der Seed für alle Beteiligten. Aufgrund der Layer-Ordnung können nur Library-Einrichtungen an einem solchen Setup teilnehmen; alles oberhalb muß dann auf weitere, verteilte Random-Generatoren aufbauen +

+ + +
+
+ + + + + + +

+ irgend ein Koordiniertungs-System, entweder compile-time oder run-time. Es muß sicherstellen, daß Partizipatoren leicht hinzukonfiguriert werden können, idealerweise automatisch, indem man einen Header includiert. Weiterhin muß aber sichergestellt werden, daß die Ordnung der Zuteilung stabil bleibt. Konsequenz: das wird aufwendiger als es auf den ersten Blick aussieht; dafür ist die Aufgabe für die Konsumenten des Random-Seed sehr einfach +

+ + +
+
+ + + + + + +

+ Auch bei dieser Variante gibt es einen Registrierungs-Hook, der sehr bequem automatisiert wird. Allerdings liefert er nur einen einzigen Seed-Wert identisch an alle Konsumenten. Jeder Konsument muß daher zwingend seinen eigenen, lokalen Random-Generator verwalten und darf nirgends lib::defaultGen verwenden. Wenn mehr als ein einziger Client diese Regel mißachtet, bricht das System zusammen +

+ + +
+
+ + + + + + +

+ Dieser Ansatz ist mit den beiden zuvor genannten Varianten kombinierbar. Es erlaubt dann beliebigen Komponenten in verschiedensten Setups, automatisch am re-Seeding teilzunehmen. Effektiv läuft das aber darauf hinaus, auch getrennte Generatoren für jeden Konsumenten zu verlangen, also keine bequeme Library-Funktion mehr! +

+ + +
+
+
+
+ + + + + + + +

+ Vorteil: in den meisten Tests ist die Situation so einfach, daß sich all die schwierigeren Fragen überhaupt nicht stellen +

+

+ Nachteil: das Thema ist nicht offensichtlich und wird deshalb leicht übersehen, so daß man dann im Ernstfall vor einem nicht-reproduzierbaren Problem steht +

+ + +
+ + + +
+ + + + + + +

+ Vorteil: man kümmert sich nur um das, was wirklich ein Problem darstellt, und läßt alles andere laufen +

+

+ Nachteil: das Problembewußtsein muß da sein, sonst funktioniert der Ansatz nicht +

+ + +
+ +
+ + + + + +
+
+ + + + + + + + + + + + + + + +

+ tja ... alles schön gesültzt bis jetzt — geht aber am Problem vorbei +

+ + +
+ + + + + + + + + +

+ und der paßt nicht ins Schema: der sitzt nämlich NICHT in der lib-Lumiera +

+ + +
+
+ + + + + + + + +

+ wir haben auch schon ein App-Event-System — das ist aber leider zu knapp definiert +

+ + +
+ + + + + +

+ ...es kann nur einfachste void(void)-Funktionen aufrufen, und auch nicht in definierter Reihenfolge. Zudem gibt es keine Scopes, d.h. wir müßten ein globales Lifecycle-Event definieren ON_RESEED. Das ist genau das was ich definitiv nicht will — weil wir damit in ein viel anspruchsvolleres Feature einsteigen +

+ + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

+ Explizit nicht sinnvoll hierfür einen Unit-Test zu schreiben! +

+
    +
  • + bin mit dem Debugger durch eine komplette Verwendungs-Sequenz gesteppt +
  • +
  • + hab das gleiche mit einem komplett anderen Test wiederholt +
  • +
  • + habe zusätzliche Test-Hooks installiert +
  • +
  • + verifiziert daß duplikate Slot-Nummern eine Exception werfen +
  • +
  • + verifiziert daß Slots in aufsteigender Sortier-Reihenfolge aufgerufen werden +
  • +
  • + verifiziert daß die absolute Nummer keine Rolle spielt +
  • +
+ + +
+
+
+