Fit Recording: Simulator fails with "Permission 'Fit' required" - how to do it right?

In my datafield app (simpledatafield) i'm using a debug label (:test) for a unit test function.

This function, for testing purposes, calls a function in the production code to process test data with it. The production function contains createField calls, to store data in fit files. As specified, this does not work in simulator, because there is no open activity session while codes runs in simulator. Thus, in the test function i create a session. This does not work and fails with a permission error. But permission "FitRecording" is given to the app. If i change it to "Fit" (as stated in the error message) the code does not compile, How can i solve this problem?

Permission 'Fit' required for '$.Toybox.ActivityRecording.createSession'.

Top Replies

All Replies

  • actually, what i'm doing here, is unit-testing the WatchUI.compute function. And this involves fit recording. Any idea, how i could split this?

  • actually, what i'm doing here, is unit-testing the WatchUI.compute function.

    Got it, my bad.

    I think you need to provide test shims (mocks) for createField(), setData() -- and any other functions which aren't implemented in unit test mode -- which do nothing or implement the bare minimum functionality.

    e.g. for createField() you'd need to return an instance of a test class which implements setData(), even if that implementation is a no-op.

    That's the standard way to do unit tests in Java, js/ts, etc (those languages have 3rd-party testing libraries which make it easy to separate mocks from the "real" implementation.)

    You might have some issues with the type checker though.

  • Could you point me to an example how to implement test shims with Garmin MonkeyC / IQ?

  • I don't know of any example, it's just a general approach that I think might work.

    For example, in your code, you could try something similar to:

    // new test class
    (:test) class TestFitContributorField extends FitContributor.Field {
        public function setData(input as Lang.Object) as Void {
            // do nothing
        }
    }
    
    class ForumsladerView extends WatchUi.SimpleDataField {
        // new test function
        (:test) public function createField() as TestFitContributorField {
            return new TestFitContributorField();
        }
    
        //...
    }

  • I understand your suggestions and this looks like the straight forward way to solve this for me.
    But i am not sure if it's possible in MonkeyC/IQ to overload the original class functions?


    My first trial with your scheme leads to a set of errors:

    Class 'TestFitContributorField' does not initialize its super class 'Field'.
    Class 'ForumsladerView' does not initialize its super class 'SimpleDataField'.
    Cannot override '$.Toybox.WatchUi.DataField.createField' with a different number of parameters.
    Cannot override '$.Toybox.WatchUi.DataField.createField' with a different return type.
    Cannot find symbol ':TestFitContributorField' on type 'self'.
    Cannot determine type for creation.
    Object of type 'Any' does not match return type '$.DataManager.TestFitContributorField'.

  • Hmm, besides the type-checker warnings/errors, my example has a fatal flaw: any function annotated with :test is assumed to be a unit test, so the test driver tries to run createField(), which causes a crash.

    No worries, there's a way around this. It does point to the fact that all of this is a massive hack. It also means that you'll waste a tiny bit of memory for the non-test versions, due to some of the stub / test exclusions code :/, unless you can figure out a way to exclude code from the unit test build using only annotations.

    This works for me with SDK 4.2.4. I tried both unit tests and running on Fenix 7 in the sim, with strict type checking (-l 3).

    > git diff

    diff --git a/source/ForumsladerSettings.mc b/source/ForumsladerSettings.mc
    index 8d00c98..48f3aa4 100644
    --- a/source/ForumsladerSettings.mc
    +++ b/source/ForumsladerSettings.mc
    @@ -5,6 +5,7 @@ var showValues as Array = [10, 3, 6, 7, false, false];
     
     //! read user settings from GCM properties in showValues array
     function getUserSettings() as Void {
    +    if ($ has :_isTest) { return; }
         showValues[0] = Application.Properties.getValue("ShowValue1") as Number;
         showValues[1] = Application.Properties.getValue("ShowValue2") as Number;
         showValues[2] = Application.Properties.getValue("ShowValue3") as Number;
    diff --git a/source/ForumsladerView.mc b/source/ForumsladerView.mc
    index cf2ec71..6777df4 100644
    --- a/source/ForumsladerView.mc
    +++ b/source/ForumsladerView.mc
    @@ -5,7 +5,36 @@ import Toybox.WatchUi;
     import Toybox.Application.Properties;
     import Toybox.FitContributor;
     
    +class TestFitContributorField {
    +    public function setData(input as Lang.Object) as Void {
    +        // do nothing
    +    }
    +}
    +
    +(:test) const _isTest as Boolean = true;
    +
     class ForumsladerView extends WatchUi.SimpleDataField {
    +    public function createField(
    +        name as Lang.String,
    +        fieldId as Lang.Number,
    +        type as FitContributor.DataType,
    +        options
    +    ) as FitContributor.Field {
    +        if ($ has :_isTest) {
    +            return new TestFitContributorField() as FitContributor.Field;
    +        } else {
    +            return WatchUi.SimpleDataField.createField(
    +                name,
    +                fieldId,
    +                type,
    +                options
    +            );
    +        }
    +    }
     
         public var
             _displayString as String = "";
    diff --git a/source/ProfileManager.mc b/source/ProfileManager.mc
    index 01d8157..aded06a 100644
    --- a/source/ProfileManager.mc
    +++ b/source/ProfileManager.mc
    @@ -45,6 +45,7 @@ class ProfileManager {
     		
         //! Register all BLE profiles
         public function registerProfiles() as Void {
    +        if ($ has :_isTest) { return; }
             BluetoothLowEnergy.registerProfile(_profileV5);
             BluetoothLowEnergy.registerProfile(_profileV6);
         }

  • Thanks for your very elaborated help! This way it works. I will now refactor my test code.
    What does the statement if ($ has ...) do, how is $ resolved?

  • Sure, no worries, glad it works for you now!

    $ is the global scope. In the example above, if ($ has :_isTest) will only be true for the test build since _isTest is annotated with :test. This trick works because we can annotate a variable with :test without unintentionally creating a new unit test. I guess it would also work if _isTest was a function that just returns true, but it wouldn't make much difference, except to make the code slightly bigger.

    You can also use $ to speed up global symbol resolution. If you have a global function foo(), $.foo() will be slighter faster than foo() in most cases. i.e. If you call foo() from an instance of a class, the runtime first has to resolve the symbol on the current class, then the parent class, followed by the grandparent class, ..., all the way up to the global scope.

  • When overloading match the number of function parameters and match the type returned and it’ll all be good.