Download

Pass by Reference to Latent Command Odd Behaviour (Automation Testing)

Hi, this is sillly, but I think that something is odd with how pass by reference is treated in latent commands.

Consider this test:



DEFINE_LATENT_AUTOMATION_COMMAND_ONE_PARAMETER(FLatentCommandCounterCommand,int&, latentCounter);

bool FLatentCommandCounterCommand::Update()
{
    latentCounter += 1;

    GEngine->AddOnScreenDebugMessage(-1, 50.0f, FColor::Orange, FString::Printf(TEXT("latentCounter: %d"), latentCounter));
    
    if(latentCounter > 5)
    {
        GEngine->AddOnScreenDebugMessage(-1, 50.0f, FColor::Green, FString::Printf(TEXT("latentCounter reached: %d"), latentCounter));
        return true;
    }
    
    return false;
}

IMPLEMENT_SIMPLE_AUTOMATION_TEST(FLatentCounterTest, "tests.LatentCounterTest", EAutomationTestFlags::ApplicationContextMask | EAutomationTestFlags::ProductFilter)

bool FLatentCounterTest::RunTest(const FString& Parameters)
{
    int latentCounter = 0;
    ADD_LATENT_AUTOMATION_COMMAND(FLatentCommandCounterCommand(latentCounter));
    return true;
}


I pass an int as reference and the latent command should modify that variable each frame until it reaches 6, because we are modifying the real variable, not a copy.

The problem is that it doesn’t. It looks like it treats as if we were working with addresses when we run it:

passByReference.png

But it should behave as if we were working with pointers:



DEFINE_LATENT_AUTOMATION_COMMAND_ONE_PARAMETER(FLatentCommandCounterCommand, int*, latentCounter);

bool FLatentCommandCounterCommand::Update()
{
    *latentCounter = *latentCounter + 1;

    GEngine->AddOnScreenDebugMessage(-1, 50.0f, FColor::Orange, FString::Printf(TEXT("latentCounter: %d"), *latentCounter));
    
    if(*latentCounter > 5)
    {
        GEngine->AddOnScreenDebugMessage(-1, 50.0f, FColor::Green, FString::Printf(TEXT("latentCounter reached: %d"), *latentCounter));
        return true;
    }
    
    return false;
}

IMPLEMENT_SIMPLE_AUTOMATION_TEST(FLatentCounterTest, "tests.LatentCounterTest", EAutomationTestFlags::ApplicationContextMask | EAutomationTestFlags::ProductFilter)

bool FLatentCounterTest::RunTest(const FString& Parameters)
{
    int* latentCounter = new int{0};
    ADD_LATENT_AUTOMATION_COMMAND(FLatentCommandCounterCommand(latentCounter));
    return true;
}


Which results in the desired behaviour:

pointer.png

Passing by reference would be nicer because I shouldn’t have to use ‘*’ and it doesn’t have a memory leak like the pointer implementation.

I don’t know, maybe I’m tired, but shouldn’t these two tests reach the same result?

Thanks!

Looks like dangling reference.

Take a look at \Engine\Source\Runtime\Core\Public\Misc\AutomationTest.h where DEFINE_LATENT_AUTOMATION_COMMAND* is defined.

This macro expand to class definition with ParamType ParamName as a member, so anytime the Update() method is called you working with the same variable.

Mm yeah, you’re right. What a shame.

Well, it looks like I’ll need to delete that pointer explicitly or use a shared pointer instead.

Thanks!!

Is there any reason you can’t use the value type (not ref/ptr)?

Lol, it’s the first thing I should’ve tried.

For some reason I thought it wasn’t possible in the first place, so I didn’t try.

I’m a fool haha

Many thanks!!

Edit: I’ll leave here what @Emaer refers to.



#define DEFINE_LATENT_AUTOMATION_COMMAND_ONE_PARAMETER(CommandName,ParamType,ParamName) \
class CommandName : public IAutomationLatentCommand \
{ \
public: \
CommandName(ParamType InputParam) \
: ParamName(InputParam) \
{} \
virtual ~CommandName() \
{} \
virtual bool Update() override; \
private: \
ParamType ParamName; \
}


When you define a Latent Command with parameters, those parameters are used as member variables for the class that’s being created, instead of just passing the parameters as arguments of a member method of said class (which was what I thought it did).
That’s why you’re able to use them as constructors of that class when calling the latent command with
ADD_LATENT_AUTOMATION_COMMAND().

So, the test could be like this:



DEFINE_LATENT_AUTOMATION_COMMAND_ONE_PARAMETER(FLatentCommandCounterCommand,int, latentCounter);

bool FLatentCommandCounterCommand::Update()
{
    ++latentCounter;
    GEngine->AddOnScreenDebugMessage(-1, 50.0f, FColor::Orange, FString::Printf(TEXT("latentCounter: %d"), latentCounter));
    
    if(latentCounter > 5)
    {
        GEngine->AddOnScreenDebugMessage(-1, 50.0f, FColor::Green, FString::Printf(TEXT("latentCounter reached: %d"), latentCounter));
        return true;
    }
    
    return false;
}

IMPLEMENT_SIMPLE_AUTOMATION_TEST(FLatentCounterTest, "tests.LatentCounterTest", EAutomationTestFlags::ApplicationContextMask | EAutomationTestFlags::ProductFilter)

bool FLatentCounterTest::RunTest(const FString& Parameters)
{
  
    int latentCounter = 0;
    ADD_LATENT_AUTOMATION_COMMAND(FLatentCommandCounterCommand(latentCounter));

    return true;
}