Announcements
Attention for Customers without Multi-Factor Authentication or Single Sign-On - OTP Verification rolls out April 2025. Read all about it here.

Writing Test Cases for Fusion 360 API Add in

Anonymous

Writing Test Cases for Fusion 360 API Add in

Anonymous
Not applicable

I'm writing a large, multiple-file add-in for Fusion 360, and I'm wondering the best way to develop a series of unit tests. My current way to run tests is to have one module with all the test functions in it, and I copy a function call that I would like to test into the run block in the main add-in module. Then I have to manually check whether the function did the correct thing within the drawing. There are two things I don't like about this approach. First and foremost, it involves editing the source code just to run some tests. Second, it requires time-consuming manual checking of the test results. One workaround for the first problem would be to add a testing GUI button that brings up all the various tests to run. But that would be a more cluttered GUI for the user. For the second problem writing 'asserts' the work is profound. To make a comprehensive assertion library would take a long time. 

 

One suggestion for the API team would be to add another "test" function. Much like the "run" function, this could be initiated through the add-in dropdown menu (currently with "run" and "debug"). This way, we wouldn't have to clutter the GUI more, and it would be one step closer to developing a comprehensive testing framework for add-ins. I suppose it would be ideal to only show this to people who are developing the code rather than the consumers.

 

And now for my main questions: how do others handle writing test cases for their add-in? Did the developers of the API have an idea about how to do this? 

0 Likes
Reply
856 Views
4 Replies
Replies (4)

marshaltu
Autodesk
Autodesk

Hello,

 

You can probably make use of text command "Commands.start". You would be able to create separate command for all your test cases and run it on demand by "Command.start". The steps are as below.

 

1. Create a command (e.g. id is "MyCommand") in your addin and hook its CommandExecuted event.

2. Put all your test cases in your "CommandExecuteHandler".

3. Launch Fusion and load your addin.

4. Go to "File" -> "View" -> "Show Text Commands".

5. Run text command "Commands.start "MyCommand"".

 

You can probably write the status which every code line/block are running to a log file and take the file as baseline. You can compare new result to the baseline to find regression out when you run those cases next time. It should be easy to integrate some open-source unit test framework to your addin. It depends on which language (C++ or Python) you choose for your addin. 

 

Thanks,

Marshal



Marshal Tu
Fusion Developer
>
0 Likes

Anonymous
Not applicable

Firstly, decouple yourself from the mess that is the Fusion API "handler" construct. Once you have a solution which lets you write more standard style code and classes then standard unit testing strategies can be employed. Of course the more coupled your code is to the fusion API the less testable it will be. Keep in mind "unit testing frameworks" can be really, really, simple - see this 3 LINE unit test framework for embedded C http://www.jera.com/techinfo/jtns/jtn002.html

 

I've written a framework to do some black-magic to deal with abstracting away the handler constructs - I'd be happy to share it if you'd like... one day I'll get around to putting my framework on GitHub (I hope).

 

 

 

1 Like

Anonymous
Not applicable

Hi Ross,

 

I would very much like to decouple my code from the handling. I'd be interested in seeing your script. Please share!

0 Likes

Anonymous
Not applicable

One day i'll get around to putting my framework on GitHub, but for now here's my black-magic abstraction layer.

class FusionEventHandler(object):
  """Makes wiering up to Fusion events dead simple and friendly!

  Usage:
    Inherit from this class and call self._auto_wire(command) at some point.
    Annotate the methods you wish to handle events using the return
    annotation to indicate which event to wire to.

  Example Annotation:
    def on_execute(self, args) -> 'execute':
      self.ui.messageBox('EXECUTE')

  NOTICE:
    Only one subscribing method per event source, per class instance, is supported.
    If the same event source is subscribed to multiple times only one method will
    receive events.

  Supported Event Names:
    command_created (may not be autowired)
    destroy
    execute
    activate
    deactivate
    preview
    input_changed
    validate_inputs
    key_down
    key_up
    mouse_click
    mouse_double_click
    mouse_down
    mouse_move
    mouse_up
    mouse_wheel
    mouse_drag_begin
    mouse_drag
    mouse_drag_end
    selecting
  """

  def __init__(self):
    self.__event_handlers = dict()

  def __handler_factory(self, event, callback, handler_cls ):
    """Factory method to create handler classes and bind them to Fusion event sources.

    Args:
      event: object - on which to call .add passing the handler instance
      callback: function - which will be called when the event fires
      handler_cls: type - one of the adsk.core.*EventHandler types

    Returns:
      Instance of a handler if subscription to event was successfull, None otherwise.
    """
    class _Handler(handler_cls):
      def __init__(self):
        super().__init__()

      def notify(self, *args):
        try:
           callback(*args)
        except Exception as ex:
          #adsk.core.Application.get().userInterface.messageBox('Failed:\n{}'.format(traceback.format_exc()))
          adsk.core.Application.get().userInterface.messageBox(
              '{}\n\n--------\n{}'.format(ex, traceback.format_exc()),
              'Error')

    h = _Handler()
    return h if event.add(h) else None

  def _wire_event(self, command, event, callback ):
    """Subscribes a listener to an event trigger.

    See core.py near line 2977

    Args:
      command: adsk.core.Command or adsk.core.CommandDefinitions
      event: string
      callback: Function - which will be called when the event fires
    """
    _wire_handler = self.__handler_factory
    events = {
      'command_created': lambda command, callback: _wire_handler(command.commandCreated, callback, adsk.core.CommandCreatedEventHandler),
      'destroy': lambda command, callback: _wire_handler(command.destroy, callback, adsk.core.CommandEventHandler),
      'execute': lambda command, callback: _wire_handler(command.execute, callback, adsk.core.CommandEventHandler),
      'activate': lambda command, callback: _wire_handler(command.activate, callback, adsk.core.CommandEventHandler),
      'deactivate': lambda command, callback: _wire_handler(command.deactivate, callback, adsk.core.CommandEventHandler),
      'preview': lambda command, callback: _wire_handler(command.executePreview, callback, adsk.core.CommandEventHandler),
      'input_changed': lambda command, callback: _wire_handler(command.inputChanged, callback, adsk.core.InputChangedEventHandler),
      'validate_inputs': lambda command, callback: _wire_handler(command.validateInputs, callback, adsk.core.ValidateInputsEventHandler),
      'key_down': lambda command, callback: _wire_handler(command.keyDown, callback, adsk.core.KeyboardEventHandler),
      'key_up': lambda command, callback: _wire_handler(command.keyUp, callback, adsk.core.KeyboardEventHandler),
      'mouse_click': lambda command, callback: _wire_handler(command.mouseClick, callback, adsk.core.MouseEventHandler),
      'mouse_double_click': lambda command, callback: _wire_handler(command.mouseDoubleClick, callback, adsk.core.MouseEventHandler),
      'mouse_down': lambda command, callback: _wire_handler(command.mouseDown, callback, adsk.core.MouseEventHandler),
      'mouse_move': lambda command, callback: _wire_handler(command.mouseMove, callback, adsk.core.MouseEventHandler),
      'mouse_up': lambda command, callback: _wire_handler(command.mouseUp, callback, adsk.core.MouseEventHandler),
      'mouse_wheel': lambda command, callback: _wire_handler(command.mouseWheel, callback, adsk.core.MouseEventHandler),
      'mouse_drag_begin': lambda command, callback: _wire_handler(command.mouseDragBegin, callback, adsk.core.MouseEventHandler),
      'mouse_drag': lambda command, callback: _wire_handler(command.mouseDrag, callback, adsk.core.MouseEventHandler),
      'mouse_drag_end': lambda command, callback: _wire_handler(command.mouseDragEnd, callback, adsk.core.MouseEventHandler),
      'selecting': lambda command, callback: _wire_handler(command.selectionEvent, callback, adsk.core.SelectionEventHandler)}
    h = events[event](command, callback) if event in events else None

    if h: self.__event_handlers[event] = h

  def _auto_wire(self, command):
    for name in dir(self):
      attr = getattr(self, name)
      if callable(attr) and hasattr(attr,'__annotations__') and 'return' in attr.__annotations__:
        self._wire_event(command, attr.__annotations__['return'], attr)

Inherit from this class to make use of it. Here's a mostly complete example...

class MyAddin(FusionEventHandler):
  def __init__(self):
    super().__init__()
    
  @property
  def app(self):
    return adsk.core.Application.get()

  @property
  def ui(self):
    return self.app.userInterface
    
  def run(self, context):
    try:
      self._command_definition = self.ui.commandDefinitions.itemById(self.command_id)
      if not self._command_definition:
        self._command_definition = self.add_button()
      assert self._command_definition, 'Script/Addin failed to produce a \'button\''
      self._wire_event(self._command_definition, 'command_created', self.__on_create)

    except:
      self.ui.messageBox('Failed:\n{}'.format(traceback.format_exc()))
      
  def stop(self, context):
    if self._command_definition:
      self._command_definition.deleteMe()
      self.remove_button()
      
  def add_button(self):
    button = self.ui.commandDefinitions.addButtonDefinition(
      self.command_id,
      self.command_name,
      self.command_description,
      self.resource_dir)
    panel = self.ui.allToolbarPanels.itemById('SketchPanel')
    panel.controls.addCommand(button)
    button.isPromotedByDefault = True
    button.isPromoted = True
    return button
      
  def remove_button(self):
    # cleanup self._command_definition
    pass
  
  def __on_create(self, _args):
    args = adsk.core.CommandCreatedEventArgs.cast(_args)
    self._command = args.command
    self._auto_wire(self._command)
    self.initialize_inputs(self._command)
    
  def initialize_inputs(self, command):
    """Perform all UI initialization here."""
    pass
    
  def on_execute(self, args) -> 'execute':
    pass

  def on_preview(self, args) -> 'preview':
    pass

Then somewhere you'll need the standard run/stop code
def run(context):
  global __addin
  __addin = MyAddin(context)
  __addin.run()

def stop(context):
  global __addin
  if __addin:
    __addin.stop(context)
2 Likes