/* BufferProviderProtocol(Test) - demonstration of buffer provider usage cycle Copyright (C) 2011, 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 buffer-provider-protocol-test.cpp ** unit test \ref BufferProviderProtocol_test */ #include "lib/error.hpp" #include "test/run.hpp" #include "test/test-frame.hpp" #include "test/test-helper.hpp" #include "test/tracking-dummy.hpp" #include "test/test-rand-ontology.hpp" #include "steam/engine/diagnostic-buffer-provider.hpp" #include "lib/iter-zip.hpp" #include using util::isSameObject; using test::TestFrame; using test::testData; using lib::HashVal; using lib::zip; namespace steam { namespace engine{ namespace test { namespace ont = ::test::ont; using LERR_(LIFECYCLE); namespace { // Test fixture const uint TEST_ELMS = 50; HashVal do_some_computation (TestFrame const& src, TestFrame& work, int64_t* data) { REQUIRE (data); ont::Param param = src.getChecksum(); ont::manipulateFrame (&work, &src, param); for (uint i=0; i(); CHECK (buff.isValid()); CHECK (sizeof(TestFrame) <= buff.size()); buff.accessAs() = testData(0); TestFrame& content = buff.accessAs(); CHECK (testData(0) == content); buff.emit(); buff.release(); CHECK (not buff.isValid()); VERIFY_ERROR (LIFECYCLE, buff.accessAs() ); auto inspect = watch(provider); CHECK (inspect.was_used (buff)); CHECK (inspect.was_released(buff)); CHECK (testData(0) == inspect.accessMemory(buff)); } /** @test demonstrate the full sequence of invocations encountered during rendering: * - as a first step, _buffer types_ are declared (usually by the Builder) * - before start of computations, required amount of buffers is _announced_ * - actual buffers are then claimed from within the ongoing computation * - buffer handles can be passed on and re-assigned * - buffers with computed results are _marked emitted_ * - each step not only creates new buffers, but also _releases_ old ones. * @remark while the control structure in this test is represented by a simple `for`-loop, * instead of the complicated recursive call-down found in the actual Render Engine, * the actual computations use a test-scheme with chained hash values, which allows * to prove that all computations happened exactly in the planned order. This * technique was developed for [verifying render nodes](\ref node-devel-test.cpp) * and relies on TestFrame objects representing a buffer filled with random yet * deterministic data. Each step in this [dummy computation](\ref do-some_computation) * - uses a source data buffer * - reads the _checksum_ of this source buffer * - uses this checksum as a _parameter_ for a »filtering operation« * - this filter or manipulation operation chains each source value with the parameter * - the resulting result values are then extracted from TestFrame and copied into * another data buffer (thereby widening from 8bit data to 64bit data) * - furthermore, each step computes the checksum of the result buffer * - and stores this checksum into a mark buffer * - note that the result buffer is passed on as source buffer to the next step. * Once this computation scheme is carried out, the checksum from the last result buffer * is retrieved. All buffers are already marked as discarded at that point, yet due to * the DiagnosticBufferProvider used for the test, the actual memory blocks were not * really deleted yet, and their contents can be inspected and verified after the fact. * This verification reinvokes the computation steps to validate the captured checksums, * which ensures that, overall * - buffers are not mixed up * - the buffer identities are maintained in the correct way * - data in buffers is not corrupted or re-used in uncontrolled ways * - all protocol steps (locking, emitting, releasing) happen in the expected order */ void verifyRenderingUse() { // Test fixture: allows to track/verify allocations after the fact DiagnosticBufferProvider provider; using DataBuff = std::array; constexpr auto BUFFSIZ = sizeof(DataBuff); BuffDescr desc1 = provider.getDescriptor(); BuffDescr desc2 = provider.getDescriptor(); // note: implies also sizeof(TestFrame) BuffDescr desc3 = provider.getDescriptorFor(BUFFSIZ); // Can also request fixed sized raw storage. CHECK (desc1.isValid()); CHECK (desc2.isValid()); CHECK (desc3.isValid()); uint num1 = desc1.announce(TEST_ELMS); uint num2 = desc2.announce(TEST_ELMS + 1); uint num3 = desc3.announce(TEST_ELMS * 2); // announce more than we actually use (which is tolerated) CHECK (num1 == TEST_ELMS); CHECK (num2 == TEST_ELMS + 1); CHECK (num3 == TEST_ELMS * 2); // emulate a progressive processing BuffHandle workBuff = desc2.lockBuffer(); workBuff.accessAs() = testData(0); for (uint i=0; i(); size_t& mark = markBuff.accessAs(); TestFrame& work = workBuff.accessAs(); DataBuff& data = dataBuff.accessAs(); mark = do_some_computation (src,work, &data[0]); srcBuff.emit(); dataBuff.emit(); markBuff.emit(); srcBuff.release(); dataBuff.release(); markBuff.release(); // note : workBuff passed to next iteration } CHECK (workBuff.isValid()); auto endSum = workBuff.accessAs().getChecksum(); workBuff.release(); // note: not every buffer need be emitted. CHECK (!workBuff); // Computation complete now — verify buffer management.... auto inspect = watch(provider); CHECK (inspect.created.cnt() == 3*TEST_ELMS + 1); CHECK (inspect.was_used (workBuff)); CHECK (inspect.was_released (workBuff)); CHECK (not inspect.was_emitted (workBuff)); CHECK (inspect.all_buffers_released()); // peek into buffer memory... // to prove that that the complete computation chain was // in fact performed, and was using the memory as intended uint last = inspect.released.cnt() - 1; diagn::Block lastWork = inspect.released[last]; diagn::Block lastMark = inspect.released[last-1]; diagn::Block lastData = inspect.released[last-2]; // due to the way this chained-hash dummy computation was defined, // the last data buffer should contain the same random numbers as the last work buffer... auto& workBuffContent = lastWork.accessAs().data(); auto& dataBuffContent = lastData.accessAs(); for (auto [c,v] : zip(workBuffContent,dataBuffContent)) CHECK (c == v); // furthermore, the last recorded checksum mark // should match the checksum of the last work buffer, // because that is where the last computation was carried out. CHECK (endSum == lastMark.accessAs()); // we can also verify that all data in emitted buffers is correct, // by re-computing the checksum for each step, using data in the buffers for (uint i=0; i(); // note const -- can not corrupt data in memory ont::Param param = src.getChecksum(); TestFrame newWorkBuff; ont::manipulateFrame (&newWorkBuff, &src, param); // the checksum recoded during the test run matches the re-computed sum, // based on combining data in memory for a second time with the same chained-hash function CHECK (markBuff.accessAs() == newWorkBuff.getChecksum()); } } /** @test automatic embedding of structures into a buffer * - contents of a raw buffer are never touched by the BufferProvider * - yet it is possible to define a _buffer type_ to _emplace_ * some arbitrary type into the buffer; such an embedded instance * is then managed alongside with the buffer state transitions * - we can even define custom handler functions to prepare * arbitrary structures within the buffer — or engage into * whatever kind of havoc we like (you have been warned) * - when invoking an _emergency clean-up_, destructor functors * can optionally invoked, or skipped (the latter is the default) * @remark this test uses the [tracking test-dummy](\ref lib::test::Tracker), * which records each ctor and dtor call into an lib::test::EventLog. * This allows us to play through several scenarios and verify * that objects are created and (not) destroyed, all as expected. * @see test-tracking-test.cpp * @see event-log-test.cpp */ void verifyObjectEmbedding() { DiagnosticBufferProvider provider; using ::test::Tracker; auto& log = Tracker::log; log.clear(); BuffDescr rawBuff = provider.getDescriptorFor(sizeof(Tracker)); BuffDescr objBuff = provider.getDescriptor(); CHECK (rawBuff.buffSize() == objBuff.buffSize()); { // Case-1 : Raw buffer does not create any embedded instance log.event("Case-1"); BuffHandle handle = provider.lockBuffer(rawBuff); CHECK (handle.size() >= sizeof(Tracker)); CHECK (handle.isAllotted()); // But no object has been created yet.... CHECK (log.ensureNot("ctor") .afterEvent("Case-1")); // explicitly placement-construct object into the buffer new(handle.rawStorage()) Tracker{55}; Tracker& emplaced = handle.accessAs(); CHECK (emplaced.val == 55); CHECK (log.verify("Case-1") .beforeCall("ctor").on(&emplaced).arg(55)); handle.release(); CHECK (not handle.isAllotted()); // Buffer is disposed now, // but emplaced object was not destroyed automatically CHECK (log.ensureNot("dtor") .afterCall("ctor").arg(55)); } { // Case-2 : Buffer with object instance automatically emplaced log.event("Case-2"); CHECK (log.ensureNot("ctor").after("Case-2")); // allot buffer and emplace Tracker instance automatically BuffHandle handle = provider.lockBuffer(objBuff); Tracker& emplaced = handle.accessAs(); CHECK (log.verify("Case-2") .beforeCall("ctor").on(&emplaced).arg()); int randomVal{emplaced.val}; CHECK (0 < randomVal and randomVal < 1000); // NOTE: buffer access is actually an unchecked force-cast! int something = handle.accessAs(); CHECK (something == randomVal); CHECK (std::is_standard_layout_v); CHECK (log.ensureNot("dtor").afterEvent("Case-2")); handle.emit(); CHECK (not handle); // no longer accessible CHECK (handle.isAllotted()); // while buffer contents are still alive CHECK (log.ensureNot("dtor").afterEvent("Case-2")); // Emplaced object is destroyed when releasing buffer handle.release(); CHECK (not handle.isAllotted()); CHECK (log.verify("Case-2") .beforeCall("ctor").arg() .beforeCall("dtor").arg(randomVal)); } { // Case-3 : Emergency clean-up log.event("Case-3"); BuffHandle buf1 = provider.lockBuffer(objBuff); BuffHandle buf2 = provider.lockBuffer(objBuff); auto& o1 = buf1.accessAs(); auto& o2 = buf2.accessAs(); CHECK (log.verify("Case-3") .beforeCall("ctor").on(&o1).arg() .beforeCall("ctor").on(&o2).arg()); int i1 = o1.val; CHECK (i1 > 0); o2.val = -1; // so we can tell them apart // Emergency clean-up can optionally invoke the dtor CHECK (buf1.isAllotted()); CHECK (buf2.isAllotted()); provider.emergencyCleanup(buf1, true); // invoke dtor buf2.emergencyCleanup(); // don't invoke dtor CHECK (not buf1.isAllotted()); CHECK (not buf2.isAllotted()); // only dtor for o1 was invoked CHECK (log.verify("Case-3") .beforeCall("ctor") .beforeCall("dtor").arg(i1)); // while buf2 was discarded without invoking dtor of o2 CHECK (log.ensureNot("Case-3") .beforeCall("ctor") .beforeCall("dtor").arg(-1)); } { // Case-4 : explicitly given ctor/dtor-λ log.event("Case-4"); using Arr = std::array; TypeHandler treat{[](void* buff){ // invoked on each buffer created from that type Tracker::log.event("create two..."); new(buff) Arr{}; } ,[](void* buff){ // invoked whenever releasing a buffer of that type static_cast(buff)->~Arr(); Tracker::log.event("both destroyed"); } }; BuffDescr special = provider.getDescriptorFor(sizeof(Arr), treat); CHECK (special.isValid()); CHECK (special.buffSize() == sizeof(Arr)); CHECK (HashVal(special) != HashVal(rawBuff)); CHECK (HashVal(special) != HashVal(objBuff)); // nothing happened yet... CHECK (log.ensureNot("create two...")); // allocate a buffer as »instance« of this buffer type... BuffHandle buff = special.lockBuffer(); CHECK (buff.isValid()); CHECK (log.verify("Case-4") .beforeEvent("create two...") .beforeCall("ctor").arg() .beforeCall("ctor").arg()); // so buffer was called and two objects created, but not destroyed yet CHECK (log.ensureNot("Case-4").before("ctor").before("dtor")); // can use buffer with embedded structure now... Arr& inlay = buff.accessAs(); CHECK (0 < inlay[0].val); CHECK (0 < inlay[1].val); int i1 = inlay[0].val; inlay[1].val = -55; // so we can identify the dtor call in the log // complete buffer usage cycle... buff.emit(); CHECK (not buff); CHECK (buff.isAllotted()); // objects are still alive.... CHECK (log.ensureNot("Case-4").before("both destroyed")); // buff.release(); CHECK (not buff.isAllotted()); CHECK (log.verify("Case-4") .beforeEvent("create two...") .before("ctor") .before("ctor") .before("dtor") .before("dtor") .beforeEvent("both destroyed")); // Order of individual ctor/dtor calls is implementation defined, // yet the overall sequence is guaranteed... CHECK (log.verify("Case-4") .before("ctor") .before("dtor").arg(i1) .beforeEvent("both destroyed")); CHECK (log.verify("Case-4") .before("ctor") .before("dtor").arg(-55) .beforeEvent("both destroyed")); } cout << "____Tracker-Log_______________\n" << util::join(Tracker::log, "\n") << "\n───╼━━━━━━━━━━━╾──────────────"<