tcunit / TcUnit

An unit testing framework for Beckhoff's TwinCAT 3
Other
258 stars 72 forks source link

Debugger switches between FB instances if they instantiated as local variable in test method #114

Closed Roald87 closed 4 years ago

Roald87 commented 4 years ago

I don't think my problem is specific for the TcUnit library, but the behavior I noticed i quite strange.

I have a function block FB_FunctionBlock for which I wrote some tests in FB_FunctionBlock_Tests. FB_FunctionBlock_Tests has several methods where the tests are defined as follows:

{attribute 'call_after_init'}
FUNCTION_BLOCK FB_FunctionBlock_Tests EXTENDS TcUnit.FB_TestSuite
========
TestNumberOne();
AnotherTest();

I then instantiate FB_FunctionBlock as a local variable in each test method like so

METHOD TestNumberOne
VAR
    fbFunctionBlock : FB_FunctionBlock;
END_VAR

TEST('TestNumberOne');
....

METHOD AnotherTest
VAR
    fbFunctionBlock : FB_FunctionBlock;
END_VAR

TEST('AnotherTest');
fbFunctionBlock();
....

I then stepped through the code, by setting a break point in one of the tests. Lets say somewhere in AnotherTest. I then stepped into the fbFunctionBlock(); in this method. However, here I noticed that sometimes it would not go into the instance belonging to this method, but it would step into TestNumberOne.fbFunctionBlock. I could see this on the top of the window, where it shows the complete path of the instance.

Furthermore it would show ??? for all variables like so:

image

In the end I solved it by converting the VAR into VAR_INST for each test method.

Has anyone else experienced issues with instantiating function blocks as local variables in methods?

RogerChristopher commented 4 years ago

VAR variables are stored on the stack during execution of the method, so they are not suitable for holding FB instances (which must retain state between calls) - you will get unpredictable behaviour! There is a check for this in the Static Analysis checks, I believe (Report temporary Function Block instances).

The VAR_INST mechanism is a bit of magic that actually createds the variables as local instances of the function block (with a prefix to the name), then hides them from all but the method.

Aliazzzz commented 4 years ago

Instances like timers etc, should be not be instantiated in the head of a method. It's actually better not to instance any FB's in a method if you ask me because the FB instance will not execute correct, just like @RogerChristopher says. Using VAR INST is imho 'an ugly workaround'. Just use the FB's main declaration part instead of the Method and call the FB from the method. Be sure to also call the FB from the main body cyclicly and you'll be fine (composition)

sagatowski commented 4 years ago

There is nothing wrong with instantiating a class/fb in a method, unless you need it to hold state for more than one method-call (in the TcUnit case, one cycle). As everyone has said, the FB will re-initialize (I assume FB_Init is called every cycle?) after every call to the method, which is fine if the test only needs one cycle.

I think VAR_INST is quite neat "magic" and is a feature I haven't seen in many other languages. It's giving me the possibility to keep stuff on the stack of the fb/class, and at the same time hiding it from the other methods and keeping the main-FB much tidier. I guess these behave different than static variables, and can have different values for every instance of that FB. Just noticed that there are indeed static variables in TwinCAT (this must be recent addition, can't recall it was there before): https://infosys.beckhoff.com/english.php?content=../content/1033/tc3_plc_intro/2528787339.html&id=

I haven't tested whether these variables behave like static method variables in C++ though. Anyone here that have elaborated with these?

@Roald87 it's not necessary to add {attribute 'call_after_init'} in the header of the TestSuite anymore :-) I think this was just an old thing laying around in the documentation prior to the 1.0 release of TcUnit. The reason you get ??? shown at the instance of the function block is because of the constant re-initialization at every cycle. You need to tell TwinCAT exactly which instance to show if you don't want to see the ???. This can be done by creating a breakpoint in the method, at which it will show the values for that particular PLC-cycle at which the breakpoint stopped.

Roald87 commented 4 years ago

Thanks for all the responses!

I like to keep my tests organized by putting all the variables inside a method. The alternative is to put many function block instances in the main test function block, which gets messy quite fast if there are ten or more tests. I guess in the end there is no right or wrong, only a style preference.

I get that fb instances in the VAR of a method are not predictable between cycles, but I assumed that for a single cycle it shouldn't matter.

@sagatowski I haven't used VAR_STAT so far. Also thanks for the tip about the pragma. I guess that one stuck around ever since I started using it x).