tcunit / TcUnit

An unit testing framework for Beckhoff's TwinCAT 3
Other
273 stars 75 forks source link

God mode functions for overwriting protected variables #45

Closed claytonketner closed 5 years ago

claytonketner commented 5 years ago

After writing many complicated tests for my company's codebase (phew), I think TcUnit could use a couple of these helper functions. In a number of scenarios, twincat won't let you write directly to certain variables:

Writing to these variables wouldn't make sense and should be prevented in the normal PLC code, so having special privileges during testing is a must. If you try to write to these protected variables like:

TEST('foo');
myFB.someProtectedVariable := 5;
...

You'll get a compiler error saying 'someProtectedVariable' is no input of 'MY_FB'.

In my experience, this is most important for writing to I/O variables and for disabling timers (by setting PT to zero). This issue can be worked around by using pointers. In my PLC projects, I've added helper functions like WRITE_ANY_INT or WRITE_ANY_TIME, which all do the same general thing:

VAR_INPUT
    ptr: POINTER TO INT;  // Pointer to the protected variable
    val: INT;
END_VAR
===================================
ptr^ := val;

Using this function allows you to bypass the compiler warning (and the runtime has no issue executing it) so that you can make your code more "testable". Ex:

WRITE_ANY_BOOL(ADR(myFB.rawInput), TRUE);
or
WRITE_ANY_TIME(ADR(myFB.setpointDelayTime), T#0MS);

Having these helpers in TcUnit would also be nice because then I (read: others) wouldn't be able to access them in my projects that use the libraries being tested. Speaking of which...

Hide Reference

I noticed that your website and this repo don't mention the Hide reference option for referenced libraries (or at least I couldn't find it). This option lets you hide TcUnit from your other projects. For example:

If you use Hide reference on TcUnit in MyLibrary, then TcUnit won't show up in the imports list of MyProject. You can find it in the Properties tab: image

Thanks again for the continued work and upkeep on this library!

Aliazzzz commented 5 years ago

Dear Clayton,

After reading this interesting post, I'd like to ask you if you can upload your 'godmode' helpers so that we can benefit feom them. Personally I had the same kind of issue's with I/O based vars so I am definitly interested in the twchnique you used. Anyway, thank you in advance!

claytonketner commented 5 years ago

@Aliazzzz I threw up https://github.com/tcunit/TcUnit/pull/46 - pretty simple, just cookie cutters of the same function. Oh how I wish TC was a little more flexible with dynamic typing...

sagatowski commented 5 years ago

This is great! I've too usually created the helper functions in the project itself to do "test-writes" on local variables in function blocks. Good initiative! I'll merge the pull request but I noticed it's done on an older version of the commit - so some unecessary files are changed and it's not possible from a pull request to do selection of files to be changed. I don't want to mess up the git commit history either, so could I ask you to do:

  1. A brand fresh branch/repo, apply the changes to it and use that for pull request?
  2. Even though the functionality of these helper functions is quite straightforward, I would still like to have tests in the TcUnit-Verifier for them - if for nothing else then at least so all code is documented. These tests should be fairly straightforward. Just add a FB in the Verifier-TwinCAT that does a write for every type of variable, and the accompanying class that verifies the output in the Verifier-DotNet. I've updated the contributors and readme for the verifier (which is working quite differently now, but everything is documented here:

https://github.com/tcunit/TcUnit-Verifier/blob/master/README.md https://github.com/tcunit/TcUnit/blob/master/CONTRIBUTING.md

I've also added clarifications about which version of VS should be used.

Now regarding the library reference. I was not aware of that option. If I understand it correctly, it means that if we have a library A that uses TcUnit (for unit tests of function blocks in A), and then we have a program B that uses library A - if we in the project A set TcUnit to "Hide reference = True" then program B will not see that library A has a dependency to TcUnit - correct?

If that is so, you are correct that in it should be mentioned in the documentation of TcUnit as seeing the reference to TcUnit is only interesting in the context of opening the library A - not program B. Anyway, thanks for pointing this out to me!

Aliazzzz commented 5 years ago

Hi Clayton,

Having seen the pull request and intended purpose this sure has some validity as helpers. On the other hand I personally think the naming schem of these helpers is very ambiguous(!) My suggestion is to refrain from using 'ANY' in the function and substitute it for e.g. 'PROTECTED'. So 'WRITE_ANY_BOOL' becomes 'WRITE_PROTECTED_BOOL' .

sagatowski commented 5 years ago

Clayton, my plan is to release version 1.0 of TcUnit this weekend. Do you think you'll have time to finish the PR before that? If no, it's not a biggie!

I'll make sure to update the documentation anyways! :)

claytonketner commented 5 years ago

So 'WRITE_ANY_BOOL' becomes 'WRITE_PROTECTED_BOOL' .

Sure I can change that. ~I'll also add *Not* assertion methods while I'm at it, which is another thing that I wanted to add (e.g. AssertNotEquals_UINT)~ Actually I guess that wouldn't give you any more info than AssertFalse(var1 = var2)

I'll merge the pull request but I noticed it's done on an older version of the commit

I'm not sure what you mean. I updated my fork to your master before I did anything, and I'm not seeing any weird files being changed. Github says "This branch has no conflicts with the base branch"

I would still like to have tests in the TcUnit-Verifier for them

I can give that a shot.

...if we in the project A set TcUnit to "Hide reference = True" then program B will not see that library A has a dependency to TcUnit - correct?

Yep!

Aliazzzz commented 5 years ago

Nice!

claytonketner commented 5 years ago

https://github.com/tcunit/TcUnit-Verifier/pull/9 I'm not 100% sure if what I did was fully correct.

sagatowski commented 5 years ago

I've merged these commits into the main branch. Renamed all INPUT-variables in all WRITEPROTECTED* functions to use same naming standard as rest of TcUnit.

Removed return value of BOOL for all WRITEPROTECTED* functions as they were not used and were always set to FALSE.

Thanks for your contribution!

sagatowski commented 5 years ago

For whatever it's worth, I just wanted to mention that I'm constantly using this function now, and it's really great :-) It comes really in handy when you want to write unit tests for function blocks doing the handling of I/O terminals, when you use a lot of %Q and %I-variables that you want to write to.

claytonketner commented 5 years ago

Glad it could be of good use to someone else too! I also use it tons in my tests as well.

sagatowski commented 4 years ago

@claytonketner Coming back to this again... I just wanted to let you know I've created a FAQ for TcUnit (as the amount of users grow, and same questions are coming back again and again). Both of the two suggestions you've added in this issue have been added to the FAQ: https://tcunit.org/frequently-asked-questions/

Hope you don't mind I copy&pasted most of your text :-)

claytonketner commented 4 years ago

@sagatowski no problem at all - glad I could have helped! 😄 And thank you for your continued support of this project!

Roald87 commented 4 years ago

Just leaving this comment here for a suggestion to an alternative solution.

I recently tried to use WRITE_PROTECTED for the first time, but I think this is way to verbose. Especially if you need to set a variable multiple times. Instead of using the WRITE_PROTECTED I usually just extend my original function block FB_ToTest to make a FB_ToTest_Tester which has properties which can set the VAR/%I/%Q variables.

Then you can simply do

fbToTest.InternalVariable := 5;

instead of:


TcUnit.WRITE_PROTECTED_INT(
    Ptr := ADR(fbToTest.InternalVariable),
    Value := 5
);