From 8103be3f96cefe5bb07a42938118ecf5dcc71660 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Thu, 10 Sep 2026 19:09:50 +0200 Subject: [PATCH] test: cover testBehavior --- tests/testBehavior.cpp | 157 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) diff --git a/tests/testBehavior.cpp b/tests/testBehavior.cpp index 4b51140..c07c083 100644 --- a/tests/testBehavior.cpp +++ b/tests/testBehavior.cpp @@ -156,3 +156,160 @@ VF_TEST(aRunningNodeResumesNextTickInsteadOfRestartingTheSequence) { int slowRuns = 0; int slowProgress = 0; + auto root = std::unique_ptr(new SequenceNode("sequence")); + root->add(constantAction("first", BehaviorStatus::Success, &firstRuns)); + root->add(slowAction("slow", 2, &slowRuns, &slowProgress)); + + BehaviorTree tree(std::move(root)); + BehaviorContext ctx; + + CHECK(tree.tick(ctx) == BehaviorStatus::Running); + CHECK_EQ(firstRuns, 1); + CHECK_EQ(slowRuns, 1); + + // The whole point: the first child must not run again while the second is + // still Running. + CHECK(tree.tick(ctx) == BehaviorStatus::Running); + CHECK_EQ(firstRuns, 1); + CHECK_EQ(slowRuns, 2); + + CHECK(tree.tick(ctx) == BehaviorStatus::Success); + CHECK_EQ(firstRuns, 1); + CHECK_EQ(slowRuns, 3); + + // Once settled the sequence starts over from the top. + CHECK(tree.tick(ctx) == BehaviorStatus::Running); + CHECK_EQ(firstRuns, 2); + CHECK_EQ(slowRuns, 4); +} + +VF_TEST(selectorResumesTheRunningBranchWithoutRecheckingEarlierOnes) { + int guardChecks = 0; + int slowRuns = 0; + int slowProgress = 0; + + auto root = std::unique_ptr(new SelectorNode("selector")); + root->add(std::unique_ptr( + new ConditionNode("guard", [&guardChecks](BehaviorContext&) { + ++guardChecks; + return false; + }))); + root->add(slowAction("slow", 3, &slowRuns, &slowProgress)); + + BehaviorTree tree(std::move(root)); + BehaviorContext ctx; + + tree.tick(ctx); + CHECK_EQ(guardChecks, 1); + tree.tick(ctx); + tree.tick(ctx); + CHECK_EQ(guardChecks, 1); + CHECK_EQ(slowRuns, 3); + CHECK_EQ(tree.root()->childAt(1)->name(), std::string("slow")); +} + +VF_TEST(cooldownBlocksForExactlyTheRequestedNumberOfTicks) { + int runs = 0; + auto gated = std::unique_ptr( + new CooldownNode("cooldown", constantAction("fire", BehaviorStatus::Success, &runs), 3)); + BehaviorTree tree(std::move(gated)); + BehaviorContext ctx; + + CHECK(tree.tick(ctx) == BehaviorStatus::Success); + CHECK_EQ(runs, 1); + + for (int blocked = 0; blocked < 3; ++blocked) { + CHECK(tree.tick(ctx) == BehaviorStatus::Failure); + CHECK_EQ(runs, 1); + } + + CHECK(tree.tick(ctx) == BehaviorStatus::Success); + CHECK_EQ(runs, 2); + + // And the block starts again immediately after the second firing. + CHECK(tree.tick(ctx) == BehaviorStatus::Failure); + CHECK_EQ(runs, 2); +} + +VF_TEST(resettingTheTreeClearsALiveCooldown) { + int runs = 0; + auto gated = std::unique_ptr( + new CooldownNode("cooldown", constantAction("fire", BehaviorStatus::Success, &runs), 5)); + BehaviorTree tree(std::move(gated)); + BehaviorContext ctx; + + tree.tick(ctx); + CHECK(tree.tick(ctx) == BehaviorStatus::Failure); + tree.reset(); + CHECK(tree.tick(ctx) == BehaviorStatus::Success); + CHECK_EQ(runs, 2); +} + +VF_TEST(inverterFlipsSuccessAndFailureButLetsRunningThrough) { + int progress = 0; + int runs = 0; + + BehaviorContext ctx; + BehaviorTree yes(std::unique_ptr(new InverterNode( + "invert", constantAction("succeeds", BehaviorStatus::Success, nullptr)))); + CHECK(yes.tick(ctx) == BehaviorStatus::Failure); + + BehaviorTree no(std::unique_ptr( + new InverterNode("invert", constantAction("fails", BehaviorStatus::Failure, nullptr)))); + CHECK(no.tick(ctx) == BehaviorStatus::Success); + + BehaviorTree waiting(std::unique_ptr( + new InverterNode("invert", slowAction("slow", 2, &runs, &progress)))); + CHECK(waiting.tick(ctx) == BehaviorStatus::Running); +} + +VF_TEST(repeaterSucceedsOnlyAfterTheRequestedNumberOfCompletions) { + int runs = 0; + auto repeat = std::unique_ptr(new RepeaterNode( + "repeat", constantAction("beat", BehaviorStatus::Success, &runs), 3)); + BehaviorTree tree(std::move(repeat)); + BehaviorContext ctx; + + CHECK(tree.tick(ctx) == BehaviorStatus::Running); + CHECK(tree.tick(ctx) == BehaviorStatus::Running); + CHECK(tree.tick(ctx) == BehaviorStatus::Success); + CHECK_EQ(runs, 3); + + int failingRuns = 0; + auto stopping = std::unique_ptr(new RepeaterNode( + "repeat", constantAction("beat", BehaviorStatus::Failure, &failingRuns), 3, true)); + BehaviorTree strict(std::move(stopping)); + CHECK(strict.tick(ctx) == BehaviorStatus::Failure); + CHECK_EQ(failingRuns, 1); +} + +VF_TEST(retryUntilSuccessGivesUpAfterTheAttemptBudget) { + int runs = 0; + auto retry = std::unique_ptr(new RetryUntilSuccessNode( + "retry", constantAction("attempt", BehaviorStatus::Failure, &runs), 3)); + BehaviorTree tree(std::move(retry)); + BehaviorContext ctx; + + CHECK(tree.tick(ctx) == BehaviorStatus::Running); + CHECK(tree.tick(ctx) == BehaviorStatus::Running); + CHECK(tree.tick(ctx) == BehaviorStatus::Failure); + CHECK_EQ(runs, 3); +} + +VF_TEST(timeLimitCutsOffAChildThatNeverSettles) { + int runs = 0; + auto limited = std::unique_ptr(new TimeLimitNode( + "window", constantAction("stall", BehaviorStatus::Running, &runs), 3)); + BehaviorTree tree(std::move(limited)); + BehaviorContext ctx; + + CHECK(tree.tick(ctx) == BehaviorStatus::Running); + CHECK(tree.tick(ctx) == BehaviorStatus::Running); + CHECK(tree.tick(ctx) == BehaviorStatus::Failure); + CHECK_EQ(runs, 3); + + // The budget is refilled once the decorator gave its verdict. + CHECK(tree.tick(ctx) == BehaviorStatus::Running); + CHECK_EQ(runs, 4); +} +