#include #include #include "procgen/PointGraph.h" #include "procgen/Procgen.h" #include "procgen/RuntimeGeneration.h" #include "zeroerr/unittest.h" using namespace eve; using namespace eve::procgen; namespace { void configure(PointGraph& graph) { REQUIRE(graph.addNode("source", "input")); REQUIRE(graph.addNode("out", "transform")); REQUIRE(graph.connect("source", "out")); REQUIRE(graph.setComputePolicy("cpu")); PointSet points; points.add(1.f, 0.f, 2.f); REQUIRE(graph.setNodePoints("source", &points)); } template void requireFailure(const Result& result, DiagnosticCode code, const std::string& path) { REQUIRE(!result.ok()); const auto* diagnostic = result.status().primaryDiagnostic(); REQUIRE(diagnostic != nullptr); REQUIRE(diagnostic->code() == code); REQUIRE_EQ(diagnostic->path(), path); } } // namespace TEST_CASE("procgen.result.executionOwnsDiagnosticsAndOutput") { PointGraph graph; configure(graph); auto missing = graph.executeResult("missing"); requireFailure(missing, DiagnosticCode::NotFound, "missing"); auto output = graph.executeResult("out"); REQUIRE(output.ok()); auto points = std::move(output).takeValue(); REQUIRE_EQ(points.getCount(), 1); REQUIRE(graph.removeNode("source")); auto invalid = graph.executeResult("out"); requireFailure(invalid, DiagnosticCode::InvalidArgument, "out"); requireFailure(missing, DiagnosticCode::NotFound, "missing"); REQUIRE_EQ(points.getX(0), 1.f); std::unique_ptr legacy(graph.execute("out")); REQUIRE(!legacy); REQUIRE(!graph.getError().empty()); } TEST_CASE("procgen.result.validationCycleCancellationAndBudgets") { PointGraph graph; configure(graph); auto valid = graph.validateResult(); REQUIRE(valid.ok()); graph.requestCancel(); auto cancelled = graph.executeResult("out"); requireFailure(cancelled, DiagnosticCode::Cancelled, "out"); graph.resetCancellation(); graph.setExecutionNodeBudget(1); auto budget = graph.executeResult("out"); requireFailure(budget, DiagnosticCode::Cancelled, "out"); graph.setExecutionNodeBudget(0); REQUIRE(graph.addNode("loop", "transform")); REQUIRE(graph.connect("out", "loop")); REQUIRE(graph.connect("loop", "out")); auto cycle = graph.executeResult("out"); requireFailure(cycle, DiagnosticCode::Conflict, "out"); auto invalid = graph.validateResult(); requireFailure(invalid, DiagnosticCode::Conflict, "out"); } TEST_CASE("procgen.result.nodeFailuresAndPointBudgetHavePaths") { PointGraph graph; REQUIRE(graph.addNode("source", "input")); auto unset = graph.executeResult("source"); requireFailure(unset, DiagnosticCode::InvalidArgument, "source"); PointSet points; points.add(1.f, 0.f, 0.f); points.add(2.f, 0.f, 0.f); REQUIRE(graph.setNodePoints("source", &points)); graph.setMaxNodeOutputPoints(1); auto budget = graph.executeResult("source"); requireFailure(budget, DiagnosticCode::InvalidArgument, "source"); graph.setMaxNodeOutputPoints(0); auto recovered = graph.executeResult("source"); REQUIRE(recovered.ok()); REQUIRE_EQ(recovered.value().getCount(), 2); } TEST_CASE("procgen.result.cleanupDistinguishesEmptyThreadAndStale") { RuntimeGeneration runtime(17), other(17); auto empty = runtime.nextCleanupRequest(); REQUIRE(empty.ok()); REQUIRE(!empty.value().has_value()); const int level = runtime.addLevel(10.f, 4.f, 2.f); REQUIRE_EQ(level, 0); runtime.updateSource(5.f, 5.f, 1.f, 0.f); auto issued = runtime.nextGenerationJob(); REQUIRE(issued.ok()); REQUIRE(issued.value().has_value()); auto completed = runtime.completeGenerationJob({*issued.value(), PointSet{}}); REQUIRE(completed.ok()); runtime.updateSource(1000.f, 1000.f, 1.f, 0.f); auto wrongThread = std::async(std::launch::async, [&] { return runtime.nextCleanupRequest(); }); auto rejected = wrongThread.get(); requireFailure(rejected, DiagnosticCode::Conflict, "thread"); auto cleanup = runtime.nextCleanupRequest(); REQUIRE(cleanup.ok()); REQUIRE(cleanup.value().has_value()); const auto request = *cleanup.value(); auto foreign = other.completeCleanupRequest(request); REQUIRE(!foreign.ok()); auto cleaned = runtime.completeCleanupRequest(request); REQUIRE(cleaned.ok()); REQUIRE_EQ(cleaned.value(), uint64_t(1)); auto replay = runtime.completeCleanupRequest(request); REQUIRE(!replay.ok()); } TEST_CASE("procgen.result.nestedFailurePreservesNodePath") { PointGraph nested; configure(nested); REQUIRE(nested.addNode("math", "density.remap")); REQUIRE(nested.connect("out", "math")); REQUIRE(nested.setNodeFloat("math", "inputMax", 0.f)); PointGraph graph; configure(graph); REQUIRE(graph.addNode("nested", "subgraph")); REQUIRE(graph.connect("out", "nested")); REQUIRE(graph.setNodeSubgraph("nested", &nested, "source", "math")); auto failed = graph.executeResult("nested"); requireFailure(failed, DiagnosticCode::InvalidArgument, "nested/math"); } TEST_CASE("procgen.result.workerGraphResultCommitsOnOwner") { RuntimeGeneration runtime(41); const int level = runtime.addLevel(10.f, 4.f, 2.f); REQUIRE_EQ(level, 0); runtime.updateSource(5.f, 5.f, 1.f, 0.f); auto issued = runtime.nextGenerationJob(); REQUIRE(issued.ok()); REQUIRE(issued.value().has_value()); const auto job = *issued.value(); auto worker = std::async(std::launch::async, [job] { PointGraph graph; const bool configured = graph.addNode("source", "input") && graph.setComputePolicy("cpu"); if (!configured) return Result::failure(Diagnostic::error(DiagnosticCode::Failed, "worker setup failed")); PointSet points; points.add(job.getMinX(), 0.f, job.getMinZ()); if (!graph.setNodePoints("source", &points)) return Result::failure(Diagnostic::error(DiagnosticCode::Failed, "worker input failed")); return graph.executeResult("source"); }); auto generated = worker.get(); REQUIRE(generated.ok()); auto committed = runtime.completeGenerationJob({job, std::move(generated).takeValue()}); REQUIRE(committed.ok()); REQUIRE_EQ(committed.value(), uint64_t(1)); REQUIRE_EQ(runtime.getActiveCellCount(), 1); } TEST_CASE("procgen.result.squirrelExecutionAndSchedulerShareProjection") { ssq::VM vm(1024, ssq::Libs::ALL); eve::ModuleManager::expose(vm); vm.run(vm.compileSource(R"( local procgen = eve.Procgen(); local graph = procgen.newPointGraph().value; local points = procgen.sampleGrid(1, 1, 1.0, 18, 0.0).value; assert(graph.addNode("source", "input")); assert(graph.setNodePoints("source", points)); assert(graph.validateResult().ok); local result = graph.executeResult("source"); assert(result.ok && result.hasValue && result.value.getCount() == 1); local objectLayer = eve.ProcgenObjectBuildLayer(); assert(objectLayer.addAsset("props/rock", 1.0).ok); objectLayer.setSeed(99); assert(objectLayer.setRandomScale(0.8, 1.2, 0.8, 1.2, 0.8, 1.2, true).ok); assert(objectLayer.addChild("props/rubble", 2, 0.5, 0.2, 0.4, -180.0, 180.0).ok); local builtObjects = objectLayer.build(result.value); assert(builtObjects.ok && builtObjects.value.getCount() == 3); assert(builtObjects.value.getStringAttribute(0, "asset", "") == "props/rock"); assert(builtObjects.value.getStringAttribute(1, "object_role", "") == "child"); assert(builtObjects.value.getIntAttribute(1, "parent_index", -2) == 0); local buildStack = eve.ProcgenBuildLayerStack(); assert(buildStack.addTileLayer("tiles", true, 1.0, 0.25, "script").ok); assert(buildStack.addObjectLayer("objects", true, objectLayer).ok); local stackDefinition = buildStack.serializeDefinition(); local restoredStack = eve.ProcgenBuildLayerStack(); assert(restoredStack.deserializeDefinition(stackDefinition).ok); local stackGrid = procgen.newGrid(1, 1).value; assert(stackGrid.setCell(0, 0, 2).ok); local stackResult = restoredStack.execute(stackGrid, result.value); assert(stackResult.ok && stackResult.value.getCount() == 2); assert(stackResult.value.getType(0) == "mesh" && stackResult.value.getMesh(0).ok); assert(stackResult.value.getType(1) == "points" && stackResult.value.getPoints(1).ok); local incremental = eve.ProcgenIncrementalBuildExecutor(); local firstDelta = incremental.update(restoredStack, stackGrid, result.value, 1, 1.0); assert(firstDelta.ok && firstDelta.value.getCount() == 1); assert(firstDelta.value.getClusterX(0) == 0 && !firstDelta.value.isRemoved(0)); assert(firstDelta.value.getArtifacts(0).ok); local stableDelta = incremental.update(restoredStack, stackGrid, result.value, 1, 1.0); assert(stableDelta.ok && stableDelta.value.getCount() == 0); assert(incremental.getCachedClusterCount() == 1); assert(incremental.getCachedArtifacts(0, 0).ok); local missing = graph.executeResult("missing"); assert(!missing.ok && missing.value == null && missing.diagnostics[0].path == "missing"); local gridGraph = procgen.newGridGraph().value; assert(gridGraph.addNode("noise", "generate.random_noise").ok); assert(gridGraph.setNodeInt("noise", "width", 4).ok); assert(gridGraph.setNodeInt("noise", "height", 4).ok); assert(gridGraph.setNodeInt("noise", "seed", 7).ok); assert(gridGraph.setNodeFloat("noise", "x", 1.0).ok); local generatedGrid = gridGraph.execute("noise"); assert(generatedGrid.ok && generatedGrid.value.getWidth() == 4); local ownedGrid = procgen.newGrid(3, 2); assert(ownedGrid.ok && ownedGrid.value.fill(0).ok); assert(ownedGrid.value.setCell(1, 1, 1).ok); local ownedInputGraph = procgen.newGridGraph().value; assert(ownedInputGraph.addNode("input", "grid.input").ok); assert(ownedInputGraph.setNodeGrid("input", ownedGrid.value).ok); local ownedGridCopy = ownedInputGraph.execute("input"); assert(ownedGridCopy.ok && ownedGridCopy.value.getCell(1, 1) == 1); local registryGraph = procgen.newGridGraph().value; assert(registryGraph.addNode("level", "generate.registry").ok); assert(registryGraph.setNodeString("level", "algorithm", "level.roguelike").ok); assert(registryGraph.setNodeInt("level", "width", 25).ok); assert(registryGraph.setNodeInt("level", "height", 19).ok); assert(registryGraph.setNodeInt("level", "seed", 435).ok); assert(registryGraph.addNode("floors", "select.semantic").ok); assert(registryGraph.setNodeInt("floors", "semantic", 2).ok); assert(registryGraph.connect("level", "floors", 0).ok); local semanticFloors = registryGraph.execute("floors"); assert(semanticFloors.ok && semanticFloors.value.getWidth() == 25); local meshGraph = procgen.newMeshGraph().value; assert(meshGraph.addNode("grid", "grid.input").ok); assert(meshGraph.addNode("tiles", "mesh.grid_tiles").ok); assert(meshGraph.setNodeGrid("grid", generatedGrid.value).ok); assert(meshGraph.connect("grid", "tiles", 0).ok); local generatedMesh = meshGraph.execute("tiles"); assert(generatedMesh.ok && generatedMesh.value.getIndexCount() > 0); local runtime = procgen.newRuntimeGeneration(19).value; local empty = runtime.nextCleanupRequest(); assert(empty.ok && empty.value == null); runtime.addLevel(10.0, 4.0, 2.0); runtime.updateSource(5.0, 5.0, 1.0, 0.0); local job = runtime.nextGenerationJob(); assert(job.ok && job.value != null); assert(runtime.completeGenerationJob(job.value, result.value).ok); assert(!runtime.completeGenerationJob(job.value, result.value).ok); runtime.updateSource(1000.0, 1000.0, 1.0, 0.0); local cleanup = runtime.nextCleanupRequest(); assert(cleanup.ok && cleanup.value != null); assert(runtime.completeCleanupRequest(cleanup.value).ok); assert(!runtime.completeCleanupRequest(cleanup.value).ok); runtime.updateSource(1005.0, 1005.0, 1.0, 0.0); local retry = runtime.nextGenerationJob(); assert(retry.ok && retry.value != null); assert(runtime.failGenerationJob(retry.value).ok); assert(!runtime.failGenerationJob(retry.value).ok); )")); }