--- name: ue-testing-debugging description: Use when writing automation tests, functional tests, or any test in Unreal Engine. Also use when the user asks about "UE_LOG", logging, log categories, assertion, check, ensure, verify, DrawDebug, debug draw, console command, profiling, Unreal Insights, stat commands, or debugging techniques. See ue-module-build-system for test module setup, and ue-cpp-foundations for general C++ logging patterns. metadata: version: 1.0.0 --- # UE Testing & Debugging You are an expert in testing, debugging, and profiling Unreal Engine C++ projects. ## Context Read `.agents/ue-project-context.md` for engine version, existing log categories, test infrastructure (automation modules, test maps), and project-specific conventions before providing guidance. ## Before You Start Ask which area the user needs help with if unclear: - **Automation tests** — unit/integration tests using IMPLEMENT_SIMPLE_AUTOMATION_TEST - **Functional tests** — actor-based AFunctionalTest in maps - **Logging** — UE_LOG, custom categories, verbosity filtering - **Assertions** — check, ensure, verify and when to use each - **Debug drawing** — DrawDebug helpers for runtime visualization - **Console commands** — UFUNCTION(Exec), FAutoConsoleCommand, CVars - **Profiling** — Unreal Insights, stat commands, SCOPE_CYCLE_COUNTER --- ## Automation Framework Automation tests live in a dedicated module (e.g., `MyGameTests`) that depends on `"AutomationController"`. Include the module in the editor target via `ExtraModuleNames` and conditionally in the game target via `if (bWithAutomationTests)`. ### Simple Tests ```cpp // Source/MyGameTests/Private/MyFeature.spec.cpp #include "Misc/AutomationTest.h" // Must specify one application context flag AND exactly one filter flag IMPLEMENT_SIMPLE_AUTOMATION_TEST(FMyInventoryTest, "MyGame.Inventory.AddItem", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) bool FMyInventoryTest::RunTest(const FString& Parameters) { UInventoryComponent* Inv = NewObject(); Inv->AddItem(FName("Sword"), 1); TestEqual(TEXT("Item count after add"), Inv->GetItemCount(FName("Sword")), 1); TestTrue(TEXT("Has sword"), Inv->HasItem(FName("Sword"))); TestFalse(TEXT("No axe"), Inv->HasItem(FName("Axe"))); TestNotNull(TEXT("Inv valid"), Inv); return true; } ``` ### Complex / Parameterized Tests `IMPLEMENT_COMPLEX_AUTOMATION_TEST` requires overriding `GetTests()` to populate the parameter list, and `RunTest(Parameters)` receives each entry in turn. ```cpp IMPLEMENT_COMPLEX_AUTOMATION_TEST(FMyAssetLoadTest, "MyGame.Assets.LoadByPath", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) void FMyAssetLoadTest::GetTests(TArray& OutBeautifiedNames, TArray& OutTestCommands) const { OutBeautifiedNames.Add(TEXT("Sword")); OutTestCommands.Add(TEXT("/Game/Items/BP_Sword")); OutBeautifiedNames.Add(TEXT("Shield")); OutTestCommands.Add(TEXT("/Game/Items/BP_Shield")); } bool FMyAssetLoadTest::RunTest(const FString& Parameters) { UObject* Asset = StaticLoadObject(UObject::StaticClass(), nullptr, *Parameters); if (!TestNotNull(TEXT("Asset loaded"), Asset)) { return false; } TestTrue(TEXT("Is DataAsset"), Asset->IsA()); return true; } ``` ### Test Assertion Methods ```cpp // Equality — overloaded for int32, int64, float, double, FVector, FRotator, // FTransform, FColor, FLinearColor, FString, FStringView, FText, FName TestEqual(TEXT("Label"), Actual, Expected); TestEqual(TEXT("Float approx"), ActualF, ExpectedF, 0.001f); // tolerance overload // Boolean TestTrue(TEXT("Label"), bCondition); TestFalse(TEXT("Label"), bCondition); // Pointer / validity TestNotNull(TEXT("Label"), Ptr); // fails if Ptr == nullptr TestNull(TEXT("Label"), Ptr); // fails if Ptr != nullptr TestValid(TEXT("Label"), WeakPtr); // IsValid() check on TWeakObjectPtr etc. // Custom failure messages AddError(FString::Printf(TEXT("Unexpected damage %d"), Damage)); AddWarning(TEXT("Deprecated path hit")); // Add error and bail — returns false from RunTest if condition fails UE_RETURN_ON_ERROR(Ptr != nullptr, TEXT("Ptr must not be null")); ``` ### Test Flags Reference | Flag | Meaning | |---|---| | `EditorContext` | Runs in the editor process | | `ClientContext` | Runs in game client | | `ServerContext` | Runs on dedicated server | | `SmokeFilter` | Fast; runs on every CI check-in | | `ProductFilter` | Project/game-level tests | --- ## Latent Commands (Async Testing) Use latent commands when the test must wait for an async operation. `Update()` returns `true` when done, `false` to retry next frame. ```cpp DEFINE_LATENT_AUTOMATION_COMMAND_ONE_PARAMETER(FWaitSecondsCommand, float, Duration); bool FWaitSecondsCommand::Update() { return GetCurrentRunTime() >= Duration; } // Enqueue inside RunTest; commands drain sequentially after RunTest returns ADD_LATENT_AUTOMATION_COMMAND(FWaitSecondsCommand(2.0f)); ``` See `references/automation-test-patterns.md` for delegate-wait, timeout, and post-async assertion patterns. --- ## Functional Tests `AFunctionalTest` is a `UCLASS` actor placed in a test map. Override `StartTest()` and call `FinishTest()` when done. ```cpp // MyFunctionalTest.h UCLASS() class MYGAMETESTS_API AMyFunctionalTest : public AFunctionalTest { GENERATED_BODY() public: virtual void StartTest() override; private: UFUNCTION() void OnTimerExpired(); FTimerHandle TimerHandle; }; // MyFunctionalTest.cpp void AMyFunctionalTest::StartTest() { Super::StartTest(); GetWorldTimerManager().SetTimer(TimerHandle, this, &AMyFunctionalTest::OnTimerExpired, 1.0f, false); } void AMyFunctionalTest::OnTimerExpired() { bool bPassed = /* verify world state */ true; FinishTest(bPassed ? EFunctionalTestResult::Succeeded : EFunctionalTestResult::Failed, TEXT("Timer-based check")); } ``` Place actors in `Maps/Test_MyFeature.umap`. Run via `RunAutomationTest "MyGame.Functional.MyFeature"` or the Session Frontend. ### TimeLimit ```cpp // Timeout — auto-fail if test exceeds time limit void AMyFunctionalTest::PrepareTest() { Super::PrepareTest(); TimeLimit = 10.0f; // seconds; 0 = no timeout (default) } ``` --- ## Logging ### Declaring and Defining a Category ```cpp // MyModule.h — visible across the module DECLARE_LOG_CATEGORY_EXTERN(LogMyModule, Log, All); // ^Name ^Default ^CompileTime max // MyModule.cpp DEFINE_LOG_CATEGORY(LogMyModule); // Single-file static category (no extern needed) DEFINE_LOG_CATEGORY_STATIC(LogMyHelper, Warning, All); ``` ### UE_LOG Usage ```cpp // UE_LOG(CategoryName, Verbosity, Format, ...) UE_LOG(LogMyModule, Log, TEXT("Player spawned: %s"), *PlayerName); UE_LOG(LogMyModule, Warning, TEXT("Inventory full, dropping item %s"), *ItemName); UE_LOG(LogMyModule, Error, TEXT("Failed to load asset at %s"), *AssetPath); UE_LOG(LogMyModule, Verbose, TEXT("Tick called, dt=%.4f"), DeltaTime); UE_LOG(LogMyModule, Fatal, TEXT("Unrecoverable save corruption")); // crashes // Conditional log — condition evaluated only if category/verbosity is active UE_CLOG(Health <= 0.f, LogMyModule, Warning, TEXT("Actor %s has zero health"), *GetName()); ``` ### Verbosity Levels (highest to lowest severity) | Level | When to use | |---|---| | `Fatal` | Crash-worthy unrecoverable errors | | `Error` | Operation failed, needs developer attention | | `Warning` | Unexpected but recoverable condition | | `Display` | User-visible output (always shown) | | `Log` | Standard development info | | `Verbose` | Detailed per-frame or per-call info | | `VeryVerbose` | Trace-level; very high frequency | ### Structured Logging (UE 5.2+) ```cpp #include "Logging/StructuredLog.h" // Named fields — order does not matter; extra fields beyond the format string are allowed UE_LOGFMT(LogMyModule, Warning, "Loading '{Name}' failed with error {Error}", ("Name", AssetName), ("Error", ErrorCode), ("Flags", LoadFlags)); ``` ### Runtime Log Filtering ``` # Command line: set a category to Verbose at startup -LogCmds="LogMyModule Verbose, LogAI Warning" # Console at runtime Log LogMyModule Verbose Log LogAI Warning Log reset # restore defaults ``` --- ## Assertions Assertions are defined in `Misc/AssertionMacros.h`. Understand the build-configuration behaviour before choosing one. ### check / checkf ```cpp // Active in Debug, Development, Test. REMOVED in Shipping (expression not evaluated). check(Ptr != nullptr); checkf(Index >= 0 && Index < Array.Num(), TEXT("Index %d out of range [0,%d)"), Index, Array.Num()); // Debug-only (DO_GUARD_SLOW — only Debug builds) checkSlow(ExpensiveValidationFn()); // Marks unreachable code paths checkNoEntry(); // Marks code that must only execute once checkNoReentry(); ``` ### ensure / ensureMsgf ```cpp // Non-fatal. Logs callstack and submits crash report. Execution continues. // Fires only ONCE per call site per session (subsequent failures are silent). if (ensure(Component != nullptr)) { Component->DoWork(); // safe to call here } // With a message ensureMsgf(Health > 0.f, TEXT("Actor %s has non-positive health %.1f"), *GetName(), Health); // Always fires (not just once per session) ensureAlways(bInitialized); ensureAlwaysMsgf(bInitialized, TEXT("System not initialized before use")); ``` ### verify / verifyf ```cpp // Expression ALWAYS evaluated (even in Shipping), but only halts in non-Shipping. // Use when expression has side effects you always need. verify(Manager->Init()); verifyf(Count++ < MaxCount, TEXT("Exceeded max count %d"), MaxCount); ``` ### Decision Guide | Situation | Macro | |---|---| | Class invariant, programmer error | `check` / `checkf` | | Recoverable condition, want to continue | `ensure` / `ensureMsgf` | | Expression has side effects always needed | `verify` / `verifyf` | | Debug-build heavy validation | `checkSlow` | | User-facing input validation | **none** — use explicit if/return | --- ## Debug Drawing Debug draw functions from `DrawDebugHelpers.h` render geometry directly in the world viewport during PIE or standalone builds. They are stripped by `#if ENABLE_DRAW_DEBUG` in Shipping. ```cpp #include "DrawDebugHelpers.h" // Duration > 0 = seconds visible; bPersistentLines = true for permanent. // Duration 0 or -1 both show for one frame. UWorld* World = GetWorld(); DrawDebugLine(World, StartLoc, EndLoc, FColor::Red, false, 2.0f, 0, 2.0f); DrawDebugSphere(World, Center, Radius, 12, FColor::Green, false, 2.0f); DrawDebugBox(World, Center, Extent, FColor::Blue, false, 2.0f); DrawDebugCapsule(World, Center, HalfHeight, Radius, FQuat::Identity, FColor::Yellow, false, 2.0f); DrawDebugPoint(World, Location, 8.0f, FColor::White, false, 2.0f); DrawDebugDirectionalArrow(World, Start, End, 40.0f, FColor::Cyan, false, 2.0f); DrawDebugString(World, Location, TEXT("Label"), nullptr, FColor::White, 2.0f, true); // Clear all persistent debug geometry FlushPersistentDebugLines(World); // Guard persistent draws for shipping: #if ENABLE_DRAW_DEBUG ... #endif // UKismetSystemLibrary::DrawDebugSphere (and siblings) auto-gate behind ENABLE_DRAW_DEBUG ``` --- ## Console Commands ### Exec Functions ```cpp // AMyPlayerController.h UFUNCTION(Exec) void ToggleGodMode(); // AMyPlayerController.cpp void AMyPlayerController::ToggleGodMode() { bGodMode = !bGodMode; UE_LOG(LogMyModule, Display, TEXT("GodMode: %s"), bGodMode ? TEXT("ON") : TEXT("OFF")); } ``` Exec functions work when typed in the console (~) if on a `PlayerController`, `Pawn`, `HUD`, `GameMode`, `GameState`, `CheatManager`, or `GameInstance`. ### FAutoConsoleCommand and CVars ```cpp // Registers at static init; no world context needed static FAutoConsoleCommand GDumpStatsCmd( TEXT("MyGame.DumpStats"), TEXT("Dump gameplay stats"), FConsoleCommandDelegate::CreateLambda([]() { UE_LOG(LogMyModule, Display, TEXT("=== GameStats ===")); })); // With world + args (PIE-safe; prefer this for gameplay commands) static FAutoConsoleCommandWithWorldAndArgs GSpawnItemCmd( TEXT("MyGame.SpawnItem"), TEXT("Usage: MyGame.SpawnItem "), FConsoleCommandWithWorldAndArgsDelegate::CreateLambda( [](const TArray& Args, UWorld* World) { /* spawn */ })); // CVar — declare in .cpp to avoid ODR issues static TAutoConsoleVariable CVarDamageScale( TEXT("MyGame.DamageScale"), 1.0f, TEXT("Scales all outgoing damage."), ECVF_Cheat); float Scale = CVarDamageScale.GetValueOnGameThread(); // Attach CVar to an existing variable float GDrawDistance = 5000.0f; static FAutoConsoleVariableRef CVarDrawDistance( TEXT("MyGame.DrawDistance"), GDrawDistance, TEXT("Draw distance for gameplay objects"), ECVF_Default); ``` ### Runtime Command Registration ```cpp // Runtime registration — call from StartupModule, Initialize, or BeginPlay IConsoleCommand* Cmd = IConsoleManager::Get().RegisterConsoleCommand( TEXT("MyGame.ReloadConfig"), TEXT("Reloads runtime config"), FConsoleCommandDelegate::CreateUObject(this, &UMySubsystem::ReloadConfig)); // Unregister when the owning object is destroyed: IConsoleManager::Get().UnregisterConsoleObject(Cmd); ``` Unlike `FAutoConsoleCommand` (static init registration), `RegisterConsoleCommand` registers at runtime and returns a handle for explicit cleanup. --- ## Custom Stat Groups & Profiling Markers ```cpp // Declare in a .cpp — feeds stat commands and Unreal Insights DECLARE_STATS_GROUP(TEXT("MyGame"), STATGROUP_MyGame, STATCAT_Advanced); DECLARE_CYCLE_STAT(TEXT("MySystem Tick"), STAT_MySystemTick, STATGROUP_MyGame); DECLARE_CYCLE_STAT(TEXT("PathFind"), STAT_PathFind, STATGROUP_MyGame); void UMySystem::Tick(float DeltaTime) { SCOPE_CYCLE_COUNTER(STAT_MySystemTick); } FPath UMySystem::FindPath(...) { SCOPE_CYCLE_COUNTER(STAT_PathFind); } // Named events — colored blocks in Insights, no stat overhead #include "HAL/PlatformMisc.h" void UMySystem::HeavyOperation() { SCOPED_NAMED_EVENT(MyHeavyOp, FColor::Orange); } // CSV profiling — lightweight always-on telemetry written to Saved/Profiling/CSVStats/ #include "ProfilingDebugging/CsvProfiler.h" CSV_DEFINE_CATEGORY(MyGame, true); void UMySystem::Tick(float DeltaTime) { CSV_SCOPED_TIMING_STAT(MyGame, SystemTick); } ``` --- ## Debugging Techniques ### Visual Logger ```cpp #include "VisualLogger/VisualLogger.h" // Window > Visual Logger shows a timeline of shapes and log events per actor UE_VLOG_SPHERE(this, LogMyModule, Verbose, GetActorLocation(), 50.0f, FColor::Green, TEXT("Patrol point")); UE_VLOG_SEGMENT(this, LogMyModule, Log, From, To, FColor::Red, TEXT("Path")); UE_VLOG(this, LogMyModule, Log, TEXT("State: %s"), *StateName); ``` ### Gameplay Debugger Press `'` (apostrophe) in PIE to open the Gameplay Debugger. It shows AI, EQS, Ability System, and custom categories. Register a custom category in module startup: ```cpp IGameplayDebugger::Get().RegisterCategory("MySystem", IGameplayDebugger::FOnGetCategory::CreateStatic(&FMyDebugCategory::MakeInstance), EGameplayDebuggerCategoryState::EnabledInGame); ``` ### IDE Breakpoint Debugging Attach Visual Studio or Rider to the running `UnrealEditor` process (Debug > Attach to Process). Use `DebugGame` or `Debug` configuration for full symbol resolution -- `Development` strips many symbols. ```cpp // Break on assertion failures -- set breakpoints on these functions: // FDebug::AssertFailed (check/checkf) // FDebug::EnsureFailed (ensure/ensureMsgf) // FDebug::OptionallyLogFormattedEnsureMessageReturningFalse // Conditional breakpoint example (VS/Rider): // Condition: Actor->GetName().Contains(TEXT("Enemy")) // Hit count: break on 5th hit ``` For `check()` and `ensure()` failures, set breakpoints on the handler functions above -- they fire before crash/log, letting you inspect the call stack. ### Crash Analysis - Minidumps land in `Saved/Crashes/`. Open with `UnrealEditor-Win64-DebugGame` PDB in WinDbg or Rider. - `FDebug::DumpStackTraceToLog(ELogVerbosity::Error)` prints the current callstack to the log. - `ensure` submits a callstack to the Crash Reporter without crashing the process. **Network**: `-NetTrace` for replication capture, `stat net` for live bandwidth, `net.ListActorChannels` for actor channels. **Key profiling commands**: `stat startfile`/`stat stopfile` (.uestats capture for Insights), `stat gpu`/`ProfileGPU` (GPU timing), `stat memoryplatform`/`memreport -full` (memory), `-trace=cpu,gpu,frame,memory` (Insights launch args). See `references/profiling-commands.md`. --- ## Common Mistakes **check() in shipping** — `check()` expressions are compiled out of Shipping builds. Never put required logic inside a check expression; use `verify()` if the expression must always evaluate. **ensure fires only once** — After the first ensure failure at a call site, subsequent calls at that site are silent. Use `ensureAlways` if you need every failure reported. **DrawDebug in shipping** — DrawDebug calls do not exist in Shipping without `ENABLE_DRAW_DEBUG`. Wrap persistent draws with `#if ENABLE_DRAW_DEBUG`. **Missing log category** — Defining `UE_LOG` with a category not visible in the current translation unit causes a linker error. Include the header that declares the category. **Automation test without a filter flag** — Every `IMPLEMENT_SIMPLE_AUTOMATION_TEST` must have exactly one filter flag (Smoke, Engine, Product, Perf, Stress, or Negative). Missing it is a compile-time `static_assert` failure. **Latent command after `return true`** — Latent commands are enqueued before the function returns. Do not enqueue them after the `return true;` statement. **Log spam in multiplayer** — Identical log calls fire from both server and each client. Prefix messages with `GetWorld()->GetNetMode()` or use `UE_CLOG(HasAuthority(), ...)` to reduce noise. --- ## Related Skills - `ue-cpp-foundations` — UE macro system, delegates, FString, UE_LOG basics - `ue-module-build-system` — setting up a dedicated test module and target inclusion - `ue-actor-component-architecture` — AFunctionalTest placement and world interaction --- ## References - `references/automation-test-patterns.md` — test setup patterns, latent commands, common scenarios - `references/profiling-commands.md` — stat commands, Insights capture, analysis workflow