It seemed that the integration test will end up as a dull repetition of already coded stuff, just with more ports and thus more boilerplate; and so I reconsidered what an actually relevant integration test might encompass - getting parameters from the invocation - translating and wiring parameters - which entails to adapt / partially close a processing function! Thus — surprise — there is a new feature not yet supported by the `NodeBuilder`, which would be very likely to be used in many real-world use cases: which is to adapt the parameter tuple expected by the binding from the library. Obviously we want this, since many »raw« processing functions will expose a mix of technical and artistic parameters; and we'd like to ''close'' the technical ones. Such a feature ''should be implementable,'' based on the already developed technique with the »cross builder«, which implies to switch the template arguments from within a builder expression. We already do this very thing for adapting parameter functor, and thus the main difficulty would be to compose an adaptor functor to the correct argument of the processing functor... Which is... (well, it is nasty and technical, yet feasible).
399 lines
18 KiB
C++
399 lines
18 KiB
C++
/*
|
||
NodeLink(Test) - render node connectivity and collaboration
|
||
|
||
Copyright (C)
|
||
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 node-link-test.cpp
|
||
** The \ref NodeLink_test covers the essence of connected render nodes.
|
||
*/
|
||
|
||
|
||
#include "lib/test/run.hpp"
|
||
#include "steam/engine/proc-node.hpp"
|
||
#include "steam/engine/node-builder.hpp"
|
||
#include "steam/engine/test-rand-ontology.hpp"
|
||
#include "steam/engine/diagnostic-buffer-provider.hpp"
|
||
#include "lib/test/diagnostic-output.hpp"/////////////////TODO
|
||
#include "lib/util.hpp"
|
||
|
||
#include <array>
|
||
|
||
using std::array;
|
||
using util::isnil;
|
||
using util::isSameObject;
|
||
|
||
|
||
namespace steam {
|
||
namespace engine{
|
||
namespace test {
|
||
|
||
|
||
|
||
|
||
/***************************************************************//**
|
||
* @test demonstrate and document how [render nodes](\ref proc-node.hpp)
|
||
* are connected into a processing network, allowing to _invoke_
|
||
* a \ref Port on a node to pull-generate a render result.
|
||
* - the foundation layer is formed by the nodes as linked into a network
|
||
* - starting from any Port, a TurnoutSystem can be established
|
||
* - which in turn allows _turn out_ a render result from this port.
|
||
*/
|
||
class NodeLink_test : public Test
|
||
{
|
||
virtual void
|
||
run (Arg)
|
||
{
|
||
seedRand();
|
||
|
||
build_simple_node();
|
||
build_connected_nodes();
|
||
trigger_node_port_invocation();
|
||
}
|
||
|
||
|
||
|
||
|
||
/** @test Build Node Port for simple function
|
||
* and verify observable properties of a Render Node
|
||
* @todo 7/24 ✔ define ⟶ ✔ implement
|
||
*/
|
||
void
|
||
build_simple_node()
|
||
{
|
||
// use some dummy specs and a dummy operation....
|
||
StrView nodeID{ont::DUMMY_NODE_ID};
|
||
StrView procID{ont::DUMMY_PROC_ID};
|
||
CHECK (nodeID == "Test:dummy"_expect);
|
||
CHECK (procID == "op(int)"_expect);
|
||
|
||
// use the NodeBuilder to construct a simple source-node connectivity
|
||
auto con = prepareNode(nodeID)
|
||
.preparePort()
|
||
.invoke(procID, ont::dummyOp)
|
||
.completePort()
|
||
.build();
|
||
CHECK (isnil (con.leads));
|
||
CHECK (1 == con.ports.size());
|
||
|
||
// can build a ProcNode with this connectivity
|
||
ProcNode n1{move(con)};
|
||
CHECK (watch(n1).isValid());
|
||
CHECK (watch(n1).leads().empty());
|
||
CHECK (watch(n1).ports().size() == 1);
|
||
|
||
// can generate a symbolic spec to describe the Port's processing functionality...
|
||
CHECK (watch(n1).getPortSpec(0) == "dummy.op(int)"_expect);
|
||
CHECK (watch(n1).getPortSpec(1) == "↯"_expect);
|
||
|
||
// such a symbolic spec is actually generated by a deduplicated metadata descriptor
|
||
auto& meta1 = ProcID::describe("N1","(arg)");
|
||
auto& meta1b = ProcID::describe("N1","(arg)");
|
||
auto& meta2 = ProcID::describe("N2","(arg)");
|
||
auto& meta3 = ProcID::describe("N1","uga()");
|
||
CHECK ( isSameObject (meta1,meta1b));
|
||
CHECK (not isSameObject (meta1,meta2));
|
||
CHECK (not isSameObject (meta1,meta3));
|
||
CHECK (hash_value(meta1) == hash_value(meta1b));
|
||
CHECK (hash_value(meta1) != hash_value(meta2));
|
||
CHECK (hash_value(meta1) != hash_value(meta3));
|
||
|
||
CHECK (meta1.genProcSpec() == "N1(arg)"_expect);
|
||
CHECK (meta2.genProcSpec() == "N2(arg)"_expect);
|
||
CHECK (meta3.genProcSpec() == "N1.uga()"_expect);
|
||
|
||
// re-generate the descriptor for the source node (n1)
|
||
auto& metaN1 = ProcID::describe("Test:dummy","op(int)");
|
||
CHECK (metaN1.genProcSpec() == "dummy.op(int)"_expect);
|
||
CHECK (metaN1.genProcName() == "dummy.op"_expect);
|
||
CHECK (metaN1.genNodeName() == "Test:dummy"_expect);
|
||
CHECK (metaN1.genNodeSpec(con.leads) == "Test:dummy-◎"_expect);
|
||
}
|
||
|
||
|
||
/** @test Build more elaborate Render Nodes linked into a connectivity network
|
||
* - verify nodes with several ports; at exit-level, 3 ports are available
|
||
* - using two different source nodes, one with two, one with three ports
|
||
* - the 2-port source is linearly chained to a 2-port filter node
|
||
* - the exit-level is a mix node, combining data from both chains
|
||
* - apply the automatic wiring of ports with the same number, whereby
|
||
* the first port connects to the first port on the lead, and so on.
|
||
* - yet for the 3rd port at the mix node, on one side the port number
|
||
* must be given explicitly, since the »A-side« chain offers only
|
||
* two ports.
|
||
* @todo 1/25 ✔ define ⟶ ✔ implement
|
||
*/
|
||
void
|
||
build_connected_nodes()
|
||
{
|
||
// This operation emulates a data source
|
||
auto src_op = [](int param, int* res){ *res = param; };
|
||
|
||
// A Node with two (source) ports
|
||
ProcNode n1s{prepareNode("srcA")
|
||
.preparePort()
|
||
.invoke("a(int)", src_op)
|
||
.setParam(5)
|
||
.completePort()
|
||
.preparePort()
|
||
.invoke("b(int)", src_op)
|
||
.setParam(23)
|
||
.completePort()
|
||
.build()};
|
||
|
||
// A node to add some "processing" to each data chain
|
||
auto add1_op = [](int* src, int* res){ *res = 1 + *src; };
|
||
ProcNode n1f{prepareNode("filterA")
|
||
.preparePort()
|
||
.invoke("a+1(int)(int)", add1_op)
|
||
.connectLead(n1s)
|
||
.completePort()
|
||
.preparePort()
|
||
.invoke("b+1(int)(int)", add1_op)
|
||
.connectLead(n1s)
|
||
.completePort()
|
||
.build()};
|
||
|
||
// Need a secondary source, this time with three ports
|
||
ProcNode n2s{prepareNode("srcB")
|
||
.preparePort()
|
||
.invoke("a(int)", src_op)
|
||
.setParam(7)
|
||
.completePort()
|
||
.preparePort()
|
||
.invoke("b(int)", src_op)
|
||
.setParam(13)
|
||
.completePort()
|
||
.preparePort()
|
||
.invoke("c(int)", src_op)
|
||
.setParam(17)
|
||
.completePort()
|
||
.build()};
|
||
|
||
// This operation emulates mixing of two source chains
|
||
auto mix_op = [](array<int*,2> src, int* res){ *res = (*src[0] + *src[1]) / 2; };
|
||
|
||
// Wiring for the Mix, building up three ports
|
||
// Since the first source-chain has only two ports,
|
||
// for the third result port we'll re-use the second source
|
||
ProcNode mix{prepareNode("mix")
|
||
.preparePort()
|
||
.invoke("a-mix(int/2)(int)", mix_op)
|
||
.connectLead(n1f)
|
||
.connectLead(n2s)
|
||
.completePort()
|
||
.preparePort()
|
||
.invoke("b-mix(int/2)(int)", mix_op)
|
||
.connectLead(n1f)
|
||
.connectLead(n2s)
|
||
.completePort()
|
||
.preparePort()
|
||
.invoke("c-mix(int/2)(int)", mix_op)
|
||
.connectLeadPort(n1f,1)
|
||
.connectLead(n2s)
|
||
.completePort()
|
||
.build()};
|
||
|
||
// verify Node-level connectivity
|
||
CHECK ( is_linked(n1f).to(n1s));
|
||
CHECK (not is_linked(n2s).to(n1s));
|
||
|
||
CHECK (not is_linked(mix).to(n1s));
|
||
CHECK ( is_linked(mix).to(n2s));
|
||
CHECK ( is_linked(mix).to(n1f));
|
||
|
||
CHECK (watch(n1s).leads().size() == 0 );
|
||
CHECK (watch(n1f).leads().size() == 1 );
|
||
CHECK (watch(n2s).leads().size() == 0 );
|
||
CHECK (watch(mix).leads().size() == 2 );
|
||
|
||
// verify Node and connectivity spec
|
||
CHECK (watch(n1s).getNodeSpec() == "srcA-◎"_expect );
|
||
CHECK (watch(n1f).getNodeSpec() == "filterA◁—srcA-◎"_expect );
|
||
CHECK (watch(n2s).getNodeSpec() == "srcB-◎"_expect );
|
||
CHECK (watch(mix).getNodeSpec() == "mix┉┉{srcA, srcB}"_expect);
|
||
|
||
// verify setup of the source nodes
|
||
CHECK (watch(n1s).ports().size() == 2 );
|
||
CHECK (watch(n1s).watchPort(0).isSrc());
|
||
CHECK (watch(n1s).watchPort(1).isSrc());
|
||
CHECK (watch(n1s).watchPort(0).getProcSpec() == "srcA.a(int)"_expect );
|
||
CHECK (watch(n1s).watchPort(1).getProcSpec() == "srcA.b(int)"_expect );
|
||
CHECK (watch(n1s).getPortSpec(0) == "srcA.a(int)"_expect );
|
||
CHECK (watch(n1s).getPortSpec(1) == "srcA.b(int)"_expect );
|
||
|
||
// second source node has 3 ports....
|
||
CHECK (watch(n2s).ports().size() == 3 );
|
||
CHECK (watch(n2s).watchPort(0).isSrc());
|
||
CHECK (watch(n2s).watchPort(1).isSrc());
|
||
CHECK (watch(n2s).watchPort(2).isSrc());
|
||
CHECK (watch(n2s).watchPort(0).getProcSpec() == "srcB.a(int)"_expect );
|
||
CHECK (watch(n2s).watchPort(1).getProcSpec() == "srcB.b(int)"_expect );
|
||
CHECK (watch(n2s).watchPort(2).getProcSpec() == "srcB.c(int)"_expect );
|
||
CHECK (watch(n2s).getPortSpec(0) == "srcB.a(int)"_expect );
|
||
CHECK (watch(n2s).getPortSpec(1) == "srcB.b(int)"_expect );
|
||
CHECK (watch(n2s).getPortSpec(2) == "srcB.c(int)"_expect );
|
||
|
||
// verify 2-chain
|
||
CHECK (watch(n1f).leads().size() == 1 );
|
||
CHECK (watch(n1f).ports().size() == 2 );
|
||
CHECK (watch(n1f).watchPort(0).srcPorts().size() == 1 );
|
||
CHECK (watch(n1f).watchLead(0).ports().size() == 2 );
|
||
CHECK (watch(n1f).watchLead(0).getNodeName() == "srcA"_expect);
|
||
CHECK (watch(n1f).watchPort(0).watchLead(0).getProcSpec() == "srcA.a(int)"_expect );
|
||
CHECK (watch(n1f).watchLead(0).watchPort(0).getProcSpec() == "srcA.a(int)"_expect );
|
||
CHECK (watch(n1f).watchPort(0).srcPorts()[0] == watch(n1f).watchLead(0).ports()[0]);
|
||
CHECK (watch(n1f).watchPort(1).srcPorts()[0] == watch(n1f).watchLead(0).ports()[1]);
|
||
|
||
// verify mix with 3 ports
|
||
CHECK (watch(mix).leads().size() == 2);
|
||
CHECK (watch(mix).leads()[0] == n1f );
|
||
CHECK (watch(mix).leads()[1] == n2s );
|
||
CHECK (watch(mix).ports().size() == 3);
|
||
CHECK (watch(mix).watchPort(0).srcPorts().size() == 2 );
|
||
CHECK (watch(mix).watchPort(1).srcPorts().size() == 2 );
|
||
CHECK (watch(mix).watchPort(2).srcPorts().size() == 2 );
|
||
CHECK (watch(mix).watchLead(0).ports().size() == 2 );
|
||
CHECK (watch(mix).watchLead(1).ports().size() == 3 );
|
||
CHECK (watch(mix).watchPort(0).watchLead(0).getProcName() == "filterA.a+1"_expect );
|
||
CHECK (watch(mix).watchLead(0).watchPort(0).getProcName() == "filterA.a+1"_expect );
|
||
CHECK (watch(mix).watchPort(1).watchLead(0).getProcName() == "filterA.b+1"_expect );
|
||
CHECK (watch(mix).watchLead(0).watchPort(1).getProcName() == "filterA.b+1"_expect );
|
||
CHECK (watch(mix).watchPort(2).watchLead(0).getProcName() == "filterA.b+1"_expect ); // special connection to port 1 on lead
|
||
CHECK (watch(mix).watchLead(0).watchPort(1).getProcName() == "filterA.b+1"_expect );
|
||
CHECK (watch(mix).watchPort(0).srcPorts()[0] == watch(mix).watchLead(0).ports()[0]);
|
||
CHECK (watch(mix).watchPort(1).srcPorts()[0] == watch(mix).watchLead(0).ports()[1]);
|
||
CHECK (watch(mix).watchPort(2).srcPorts()[0] == watch(mix).watchLead(0).ports()[1]);
|
||
CHECK (watch(mix).watchPort(0).watchLead(1).getProcName() == "srcB.a"_expect );
|
||
CHECK (watch(mix).watchLead(1).watchPort(0).getProcName() == "srcB.a"_expect );
|
||
CHECK (watch(mix).watchPort(1).watchLead(1).getProcName() == "srcB.b"_expect );
|
||
CHECK (watch(mix).watchLead(1).watchPort(1).getProcName() == "srcB.b"_expect );
|
||
CHECK (watch(mix).watchPort(2).watchLead(1).getProcName() == "srcB.c"_expect );
|
||
CHECK (watch(mix).watchLead(1).watchPort(2).getProcName() == "srcB.c"_expect );
|
||
CHECK (watch(mix).watchPort(0).srcPorts()[1] == watch(mix).watchLead(1).ports()[0]);
|
||
CHECK (watch(mix).watchPort(1).srcPorts()[1] == watch(mix).watchLead(1).ports()[1]);
|
||
CHECK (watch(mix).watchPort(2).srcPorts()[1] == watch(mix).watchLead(1).ports()[2]);
|
||
|
||
|
||
//________________________________________________________
|
||
// for sake of completeness: all these nodes can be invoked
|
||
|
||
BufferProvider& provider = DiagnosticBufferProvider::build();
|
||
auto invoke = [&](ProcNode& node, uint port)
|
||
{ // Sequence to invoke a Node...
|
||
BuffHandle buff = provider.lockBufferFor<int> (-55);
|
||
CHECK (-55 == buff.accessAs<int>());
|
||
buff = node.pull (port, buff, Time::ZERO, ProcessKey{0});
|
||
int result = buff.accessAs<int>();
|
||
buff.release();
|
||
return result;
|
||
};
|
||
|
||
// node|port
|
||
CHECK (invoke (n1s, 0 ) == 5);
|
||
CHECK (invoke (n1s, 1 ) == 23);
|
||
|
||
CHECK (invoke (n1f, 0 ) == 5+1);
|
||
CHECK (invoke (n1f, 1 ) == 23+1);
|
||
|
||
CHECK (invoke (n2s, 0 ) == 7);
|
||
CHECK (invoke (n2s, 1 ) == 13);
|
||
CHECK (invoke (n2s, 2 ) == 17);
|
||
|
||
CHECK (invoke (mix, 0 ) == (5+1 + 7 )/2);
|
||
CHECK (invoke (mix, 1 ) == (23+1 + 13)/2);
|
||
CHECK (invoke (mix, 2 ) == (23+1 + 17)/2);
|
||
}
|
||
|
||
|
||
/** @test TODO Invoke some render nodes as linked together
|
||
* @todo WIP 12/24 🔁 define ⟶ implement
|
||
*/
|
||
void
|
||
trigger_node_port_invocation()
|
||
{
|
||
auto testGen = testRand().setupGenerator();
|
||
auto testMan = testRand().setupManipulator();
|
||
auto testMix = testRand().setupCombinator();
|
||
|
||
/*
|
||
// A Node with two (source) ports
|
||
ProcNode n1s{prepareNode("srcA")
|
||
.preparePort()
|
||
.invoke("a(int)", src_op)
|
||
.setParam(5)
|
||
.completePort()
|
||
.preparePort()
|
||
.invoke("b(int)", src_op)
|
||
.setParam(23)
|
||
.completePort()
|
||
.build()};
|
||
|
||
// A node to add some "processing" to each data chain
|
||
auto add1_op = [](int* src, int* res){ *res = 1 + *src; };
|
||
ProcNode n1f{prepareNode("filterA")
|
||
.preparePort()
|
||
.invoke("a+1(int)(int)", add1_op)
|
||
.connectLead(n1s)
|
||
.completePort()
|
||
.preparePort()
|
||
.invoke("b+1(int)(int)", add1_op)
|
||
.connectLead(n1s)
|
||
.completePort()
|
||
.build()};
|
||
|
||
// Need a secondary source, this time with three ports
|
||
ProcNode n2s{prepareNode("srcB")
|
||
.preparePort()
|
||
.invoke("a(int)", src_op)
|
||
.setParam(7)
|
||
.completePort()
|
||
.preparePort()
|
||
.invoke("b(int)", src_op)
|
||
.setParam(13)
|
||
.completePort()
|
||
.preparePort()
|
||
.invoke("c(int)", src_op)
|
||
.setParam(17)
|
||
.completePort()
|
||
.build()};
|
||
|
||
// Wiring for the Mix, building up three ports
|
||
// Since the first source-chain has only two ports,
|
||
// for the third result port we'll re-use the second source
|
||
ProcNode mix{prepareNode("mix")
|
||
.preparePort()
|
||
.invoke("a-mix(int/2)(int)", mix_op)
|
||
.connectLead(n1f)
|
||
.connectLead(n2s)
|
||
.completePort()
|
||
.preparePort()
|
||
.invoke("b-mix(int/2)(int)", mix_op)
|
||
.connectLead(n1f)
|
||
.connectLead(n2s)
|
||
.completePort()
|
||
.preparePort()
|
||
.invoke("c-mix(int/2)(int)", mix_op)
|
||
.connectLeadPort(n1f,1)
|
||
.connectLead(n2s)
|
||
.completePort()
|
||
.build()};
|
||
*/
|
||
UNIMPLEMENTED ("operate some render nodes as linked together");
|
||
}
|
||
};
|
||
|
||
|
||
/** Register this test class... */
|
||
LAUNCHER (NodeLink_test, "unit node");
|
||
|
||
|
||
|
||
}}} // namespace steam::engine::test
|