5 Replies Latest reply on Sep 7, 2012 3:30 AM by adinn

    BMRule and multiple binds

    abroszni

      Hi, I am using some BMRules :

       

        @BMRules(rules = {

            @BMRule(name = "create countDown for SomeOtherObject", targetClass = "SomeOtherObject",

                targetMethod = "doThat", targetLocation = "AFTER WRITE $nbMaxElts", binding = "nb:Integer=$nbMaxElts", action = "createCountDown($0, nb/2)"),

            @BMRule(name = "TEST", targetClass = "SomeOtherObject", targetMethod = "doThat", targetLocation = "AFTER READ SomeOtherObject.privateMap",

                condition = "countDown($0)", action = "System.exit(5)") })

       

      which works well bt I would like to add a condition on the second Rule,

      I want to bind an variable of my class in my first rule so I can use it in my second rule,

      something like

       

        @BMRules(rules = {

            @BMRule(name = "create countDown for SomeOtherObject", targetClass = "SomeOtherObject",

                targetMethod = "doThat", targetLocation = "AFTER WRITE $nbMaxElts", binding = "nb:Integer=$nbMaxElts;step:Integer=$recoveryStep", action = "createCountDown($0, nb/2)"),

            @BMRule(name = "TEST", targetClass = "SomeOtherObject", targetMethod = "doThat", targetLocation = "AFTER READ SomeOtherObject.privateMap",

                condition = "step==0 && countDown($0)", action = "System.exit(5)") })

       

      but this doesn;t work

      I see first that the multiple bindings declaration doesn't work, how am I supposed to make multiple bindings?

      And then will my value bound in the first rule can be used in my second rule like I'm trying?

      thanks!

        • 1. Re: BMRule and multiple binds
          adinn

          Hmm, lets just reformat that a bit. Here's the first version

           

            @BMRules(rules = {

                @BMRule(name = "create countDown for SomeOtherObject",

                                targetClass = "SomeOtherObject",

                                targetMethod = "doThat",

                                targetLocation = "AFTER WRITE $nbMaxElts",

                                binding = "nb:Integer=$nbMaxElts",

                                action = "createCountDown($0, nb/2)"),

                @BMRule(name = "TEST",

                        targetClass = "SomeOtherObject",

                        targetMethod = "doThat",

                        targetLocation = "AFTER READ SomeOtherObject.privateMap",

                        condition = "countDown($0)",

                        action = "System.exit(5)") })

           

          Ok, so the first rule creates a countdown for any instance of class SomeOtherObject whenever method doThat writes variable nbMaxElts and sets the counter for the countdown to half the value written to nbMaxElts. The second rule counts down each time that object's privateMap is read in method doThat and exits once nbMaxElts/2 reads occur. That looks ok although I'll just note that you don't have to declare nb with type Integer -- declaring it as an int will also work.

           

          Now here's the second version

           

            @BMRules(rules = {

                @BMRule(name = "create countDown for SomeOtherObject",

                        targetClass = "SomeOtherObject",

                        targetMethod = "doThat",

                        targetLocation = "AFTER WRITE $nbMaxElts",

                        binding = "nb:Integer=$nbMaxElts;step:Integer=$recoveryStep",

                        action = "createCountDown($0, nb/2)"),

                @BMRule(name = "TEST",

                        targetClass = "SomeOtherObject",

                        targetMethod = "doThat",

                        targetLocation = "AFTER READ SomeOtherObject.privateMap",

                        condition = "step==0 && countDown($0)",

                        action = "System.exit(5)") })

           

          So, now the first rule is trying to record the value of local variable recoveryStep and then use it to decide whether to fire the second rule. Well, as you say, the variable declared in the binding clause is local to that rule. So, an attempt to refer to it in the second rule will fail with a type error indicating that you are referring to an undefined variable.

           

          Before explaining how to do what you want I'll just mention that I am surprised when you say there is a problem with the binding in the first rule. it ought to work as written. Maybe you need some white space before the comments but I would not have expected so. I'll do some testing to check this and report back in a folow-up comment. Meanwhile let's get back to the howto.

           

          I can see 2 ways of implementing what you want and can also see an interesting possibility for how Byteman could make it easier to do things like this which I'll also discuss. So, first let's look at the possible ways to do what you want

           

          1) you could use a counter to hold the value of variable recoveryStep and refer to that counter in the second rule

           

            @BMRules(rules = {

                @BMRule(name = "create countDown for SomeOtherObject",

                        targetClass = "SomeOtherObject",

                        targetMethod = "doThat",

                        targetLocation = "AFTER WRITE $nbMaxElts",

                        binding = "nb:int=$nbMaxElts;step:int=$recoveryStep",

                        action = "createCountDown($0, nb/2); createCounter($0, $recoveryStep)"),

                @BMRule(name = "TEST",

                        targetClass = "SomeOtherObject",

                        targetMethod = "doThat",

                        targetLocation = "AFTER READ SomeOtherObject.privateMap",

                        condition = "getCounter($0)==0 && countDown($0)",

                        action = "System.exit(5)") })

           

          2) A better option though would be to create the countDown only when you actually need it

           

            @BMRules(rules = {

                @BMRule(name = "create countDown for SomeOtherObject",

                        targetClass = "SomeOtherObject",

                        targetMethod = "doThat",

                        targetLocation = "AFTER WRITE $nbMaxElts",

                        binding = "nb:int=$nbMaxElts;step:int=$recoveryStep",

                        condition = "$recoveryStep == 0",

                        action = "createCountDown($0, nb/2)"),

                @BMRule(name = "TEST",

                        targetClass = "SomeOtherObject",

                        targetMethod = "doThat",

                        targetLocation = "AFTER READ SomeOtherObject.privateMap",

                        condition = "countDown($0)",

                        action = "System.exit(5)") })

           

          You may not have realised but countDown(x) returns false if there is no active countDown associated with the object x supplied as argument. So, if you don't create a countDown when recoveryStep has value 0 then you will never fire the second rule.

           

          However, your question raises an interesting possibility which might make it easier to implement rule sets. What would it be like if one rule could bind or update a variable for use by another rule. So, imagine we provided a syntax something like this:

           

            @BMRules(rules = {

                @BMRule(name = "create countDown for SomeOtherObject",

                        targetClass = "SomeOtherObject",

                        targetMethod = "doThat",

                        targetLocation = "AFTER WRITE $nbMaxElts",

                        binding = "nb:int=$nbMaxElts; global step:int=$recoveryStep",

                        action = "createCountDown($0, nb/2)"),

                @BMRule(name = "TEST",

                        targetClass = "SomeOtherObject",

                        targetMethod = "doThat",

                        targetLocation = "AFTER READ SomeOtherObject.privateMap",

                        binding = "global step:int",

                        condition = "step == 0 && countDown($0)",

                        action = "System.exit(5)") })

           

          So, with this syntax we can declare a global variable for use in both rules. The first rule expects to initialise (or update) it while the second one expects just to read it. Of course, the problem here is that the same variable is used every time the first and second rule fire even though the value of $0 may be different.

           

          So, imagine the first rule fires twice when doThat runs on objects somOtherObject1 and somOtherObject2 and that the second rule then fires on somOtherObject1. The value of variable step will be somOtherObject2.recoveryStep. I suspect what you really want is for the value to be somOtherObject1.recoveryStep.

           

          Ok, so how could we improve on this? Well, the problem is that the value you want is only labelled using the name step. We could provide an indexing builtin to allow us to use both the name and the instance $0 as indexing values. For example,

           

            @BMRules(rules = {

                @BMRule(name = "create countDown for SomeOtherObject",

                        targetClass = "SomeOtherObject",

                        targetMethod = "doThat",

                        targetLocation = "AFTER WRITE $nbMaxElts",

                        binding = "nb:int=$nbMaxElts; step:int=$recoveryStep",

                        action = "createCountDown($0, nb/2), index($0, "step", step)"),

                @BMRule(name = "TEST",

                        targetClass = "SomeOtherObject",

                        targetMethod = "doThat",

                        targetLocation = "AFTER READ SomeOtherObject.privateMap",

                        binding = "global step:int",

                        condition = "indexed($0, "step") == 0 && countDown($0)",

                        action = "System.exit(5)") })

           

          The action index($0, "step", step) in the first rule inserts an int value into a lookup table keyed by the 2-element key comprising $0 and the string "step". The condition in the second rules uses the test indexed($0, "step") to retrieve an int value from the table and compare it to teh literal constant 0. This looks like it would do what you want but there are a few wrinkles here to do with typing. What are the type signatures of the built-ins index and indexed? Is it

           

            void index(Object, String, int);       int indexed(Object, String);

            void index(Object, String, Object)     Object indexed(Object, String);

            void index(Object, Object, int)        int indexed(Object, Object);

            void index(Object, Object, Object)     Object indexed(Object, Object);

           

          The last option is the most useful because we can compare any values but then this means that indexed returns an Object. Now with your example this is ok because the test indexed($0, "step") == 0 will promote 0 to an Integer object and compare it against the Integer object stored in the table and returned by the builtin call. But imagine we had stored a String value in the table and wanted to pass it to a method expecting a String e.g. we called index($0, "colour", "red") and we wanted our condition to be indexed($0, "colour").startsWith("r"). This would fail because Object does nto implement startsWith.

           

          One of the things whcih might fix this is to do automatic downcasting in rules. For example we could have

           

            RULE rule1

            . . .

            DO index($0, "colour", "red")

            ENDRULE

           

            RULE rule2

            ...

            BIND colour : String = indexed($0, "colour")

            IF colour.startsWith("r")

            . . .

           

          So, with these rules the type checker can work out that a String is expected as the return value from indexed() even though it's type signature says it returns an object i.e. there is an implicit downcast in the assignment performed when the local var is initialised. The type chjecker can accept the rule so long as it forces the rule execution engine to perform a runtime type check. This can be done in other places where assignments occur or where a parameter passed to a method requires a downcast. It would be useful if downcasting was something which was only enabled in for specific operations or else per rule or per rule set (using some sort of declaration in the rule body or inline in the script) because in many cases supplying a type which requires downcasting will be an error.

           

          I have been thinking about providing a downcasting capability for quite a while now (it has been discussed before) and your use case is a very good example of one where it really makes sense. So, I will continue to think about it more and maybe -- if I can find time off rom OpenJDK work -- try implementing a prototype for you or others to test, possibly also with prototype index/indexed builtins similar to the ones I described above.

           

          Of course, you coudl also try hacking index/indexed as a built-in yourself with whatever of the signatures you need or even try implementing downcasting to provide a Rolls Royce solution. That's the nice thing about open source -- you always contribute a missing feature to make it better :-)

           

          regards,

           

           

          Andrew Dinn

          • 2. Re: BMRule and multiple binds
            abroszni

            Thanks a lot Andrew for this detailed answer !!!

             

            To add a few more precision, I found out why my bindings were not working

             

            binding = "nb:int=$nbMaxElts;step:int=$recoveryStep",

             

            Since I couldn't find in the documentation what was the separator, I wasn't sure that the ';' was the correct one, but it is.

            The issue was the recoveryStep. It was not a local variable but a field, so trying to bind it failed, and made the other binding fail as well.

            If I could suggest some feature, it would be to have a way to get some more explicit logging.

            I used

            -Dorg.jboss.byteman.contrib.bmunit.verbose=true

            expecting it would output the issues, but it didn't, maybe there's already a way to do this, but it would really useful to have some error logging when something like this is failing.

             

            in the end I have added a getRecoveryStep() method in my class and managed to make it work with

            binding = "nb:int=$nbMaxElts;step:int=$0.getRecoveryStep()",

            And again, there might be a better way to access a field, but couldn't find it in the doc.

             

            But in the end, your way made things simpler, indeed I didn't know that the countDown(x) returns false when not assigned.

            And once more, some extra logging would be really helpful to find out this, because I must admit that I didn't think about looking in the code directly, which would have helped me understand it.

             

            I vote for the 'global' scope of a binding, I met some difficulty with the countdown(x) because I was binding it in a targetClass, and using it as a condition in another targetClass. So I couldn't use a countdown($0), and eventually I used a dirty trick, I used a String as the identifier, something like "abc".

             

            Right now I'm just starting but plan to use it more extensively in our tests and see how far I can go, if I ever implement something that might be useful, I'll be happy to submit it

             

            Thanks again!

            • 3. Re: BMRule and multiple binds
              adinn

              Aur Brsz wrote:

               

              Thanks a lot Andrew for this detailed answer !!!

               

              To add a few more precision, I found out why my bindings were not working

               

              binding = "nb:int=$nbMaxElts;step:int=$recoveryStep",

               

              Since I couldn't find in the documentation what was the separator, I wasn't sure that the ';' was the correct one, but it is.

              The issue was the recoveryStep. It was not a local variable but a field, so trying to bind it failed, and made the other binding fail as well.

               

              If you use the bmcheck script on your script file you can check rules offline to see fi they have parse or type errors. You ought to get a type error saying that recoveryStep is an undefined local variable when you do so.

               

              Aur Brsz wrote:

               

              in the end I have added a getRecoveryStep() method in my class and managed to make it work with

              binding = "nb:int=$nbMaxElts;step:int=$0.getRecoveryStep()",

              And again, there might be a better way to access a field, but couldn't find it in the doc.

              There is indeed a better way. Byteman is able to access any members, private or public. So you could just have written

               

              step:int = $0.recoveryStep

               

              or indeed directly referenced the field inthe rule body

               

               

              DO . . ., createCounter($0.recoveryStep)

               

               

               

              Aur Brsz wrote:

               

              I vote for the 'global' scope of a binding, I met some difficulty with the countdown(x) because I was binding it in a targetClass, and using it as a condition in another targetClass. So I couldn't use a countdown($0), and eventually I used a dirty trick, I used a String as the identifier, something like "abc".

              That's not really a dirty trick. The Object you supply as argument to countdown is merely a label for a specific countdown object. If you want it to be per invoked instance you use the target of the trigger method ($0) as a label. If you want it to be per thread you use the curretn thread as a label (Therad.current()) and if you want it to be globally labelled you use a globally unique object like a String (n.b. Byteman does Strign compares using the .equals method so all Strings with the same represenation are equal as far as Byteman is concerned).

               

               

              Aur Brsz wrote:

               

              Right now I'm just starting but plan to use it more extensively in our tests and see how far I can go, if I ever implement something that might be useful, I'll be happy to submit it

               

              Great. If you need any more advice please ask here and do share anythign you can publicise so that others can learn from your experience.

               

              regards,

               

               

              Andrew Dinn

              • 4. Re: BMRule and multiple binds
                abroszni

                Andrew Dinn a écrit:

                 

                If you use the bmcheck script on your script file you can check rules offline to see fi they have parse or type errors. You ought to get a type error saying that recoveryStep is an undefined local variable when you do so.

                 

                 

                I'm using TestNG with BMNGRunner, and the @BMRule annotation, correct me if I'm wrong but I see in the BMNGAbstractRunner class, bmngBeforeTest() method:

                  

                164      if (methodSingleRuleAnnotation != null) {

                165            String scriptText = BMRunnerUtil.constructScriptText(new BMRule[] { methodSingleRuleAnnotation });

                166            final String name = method.getName();

                167            BMUnit.loadScriptText(testKlazz, name, scriptText);

                168        } else if (methodMultiRuleAnnotation != null) {

                169            BMRule[] rules = methodMultiRuleAnnotation.rules();

                170            String scriptText = BMRunnerUtil.constructScriptText(rules);

                171            final String name = method.getName();

                172            BMUnit.loadScriptText(testKlazz, name, scriptText);

                173        }

                 

                 

                What I understand is that the script is created from the annotations into a String, not a file, so I don't see how I can use the bmcheck script to verify my annotations rules?

                • 5. Re: BMRule and multiple binds
                  adinn

                  Aur Brsz wrote:

                   

                  I'm using TestNG with BMNGRunner, and the @BMRule annotation . . .

                   

                  What I understand is that the script is created from the annotations into a String, not a file, so I don't see how I can use the bmcheck script to verify my annotations rules?

                   

                  Yes, of course, you are not currently able to check @BMRule annottaion rules offline.

                   

                  It is actually fairly difficult to provide easily visible feedback when using the BMUnit package to run rules in TestNG or JUnit tests. Firstly, the rule may be submitted when the test starts but errors may not happen before the submit operation completes. Parsing may only occur some time later when the target class is loaded. Type checking cannot happen until the trigger method for the rule is executed. If the test goes wrong and a trigger method does not get called then some rules may not be type checked during the test run.

                   

                  If an error does occur then the parse or type check errors and exception trace will be printed to System.out. However, this is not always easy to spot because TestNG and JUnit normally redirect the test output to files rather than displaying to the console.

                   

                  It would be very nice if you were able to test rules for parse/type check errors from maven or ant plugin but unfortunately there is no such plugin at present (building one might make an interesting undergraduate project). As a workaround you could maybe create a rule script which looks like your annotation and try running bmcheck offline. Of course this is harder than using maven because you have to supply paths to jars containing classes mentioned in the rules on the command line (using one or more -cp path/to/jar arguments before the rule script name). Anyway, this might help you find some errors in the code.