Skip to content
This repository has been archived by the owner on May 4, 2022. It is now read-only.

Writing Ladon

Astha Patni edited this page Oct 2, 2017 · 1 revision

Writing Ladon

This is a brief overview you can use to get started writing Ladon code.

Note: the Ladon framework is fully documented in YARD style; please read the docs!

Models

You should always start with a solid idea of what you're modeling, why you're modeling it, and what components your model will need to accurately capture its subject. In short, you must iron out five key details:

  • What system are you trying to model?
  • What is a state in the model you will create?
  • How will you define transitions between those states?
  • How will you use the models you create?
  • Where and how will you store your models and automations?

Once you have answered these questions, you can move forward with writing your Ladon model.

Enumerate the states you want to model of your software system

Best practice: start with the state(s) that you'd call starting states

For example, if you're modeling a web application, your states may look something like:

  • LoginPage (starting state)
  • DashboardPage
  • (etc...)

Create Ladon::Modeler::State subclasses to represent each state

Once you have a list of your states, pick one to start working on; the "starting state" of your software is probably a good one to tackle first. Best practice: do many or all of your states share behavior? Subclass Ladon::Modeler::State and define a state type that your states can inherit from!

Example:

# Our model's states are web pages, as the web app consists of various pages you can be using
class WebPageState < Ladon::Modeler::State
  # some common code here
end

# Models a hypothetical login page
class LoginPage < WebPageState
  # implementation code here, modeling the architecture of this state
  # in a web app example such as this, you might follow the "page object" design pattern

  # example:

  def can_try_to_log_in?
    # implementation goes here...
  end

  def try_to_log_in
    # implementation goes here...
  end
end

# Models a hypothetical dashboard page
class DashboardPage < WebPageState
end

Use Ladon::Modeler::Transition to model the transitions for your states

Once you have two or more states drawn up, and you've defined architecture and mapped it to behaviors, you can model the connections between those states. In Ladon, you do this by declaring outbound transitions on the source state.

Example:

# Models a hypothetical login page
class LoginPage < WebPageState
  # implementation code here, modeling the architecture of this state
  # in a web app example such as this, you might follow the "page object" design pattern

  # example:

  # tell your transition how to identify the target state class type
  transition 'DashboardPage' do |t|
    # tell your transition where to find the source for the target state type
    t.target_loader do
      require 'path/to/dashboard/page.rb'
    end

    # tell your transition when the current state can make this transition
    t.when(&:can_attempt_login?)

    # tell your transition what needs to happen to make the software change to the target state
    t.by(&:attempt_login)
  end

  def can_attempt_login?
    # implementation goes here...
  end

  def attempt_login
    # implementation goes here...
  end
end

This is syntactic sugar for encoding transitions via the class-level method self.transitions. Example:

class LoginPage < WebPageState
  # Models the transitions this state can make to other states
  # MUST return a list of Ladon::Modeler::Transition instances.
  def self.transitions
    [
      Transition.new do |transition|

        # tell your transition where to find the source for the target state type
        transition.target_loader do
          require 'path/to/dashboard/page.rb'
        end

        # tell your transition how to identify the target state class type
        transition.target_identifier { DashboardPage }

        # tell your transition when the current state can make this transition
        transition.when(&:can_attempt_login?)

        # tell your transition what needs to happen to make the software change to the target state
        transition.by(&:attempt_login)
      end
    ]
  end

Automations

To begin automating, you need to determine whether or not you want the benefits of automating through a model. Generally, you do want to use a model -- however, your use cases may vary. If you expect the system being automated to be very stable, you can opt not to model it. Beware: this may lead to flakiness, instability, or maintainability problems in your automations if you make the wrong choice.

If not using a model

Bold move! The class you use to define an automation that doesn't leverage a model is: Ladon::Automator::Automation. Create a subclass of this and name it something that appropriately represents the procedure you are going to automate.

If using a model

The model needs to exist. Follow the workflow above to create a model of the system you want to automate; it should model all of the behavior of the software that you will leverage in your automation.

The class you use to define a model-driven automation is Ladon::Automator::ModelAutomation.

Create your automation script skeleton

At its core, any Ladon Automation is simply a series of phases of operation, each phase being either required or optional. Using the appropriate Automation class, build your skeleton:

class LoginAndLogout < Ladon::Automator::Automation # or ModelAutomation

      # Defines ALL of the phases of this automation, in the order they should be executed, as well as other metadata.
      def self.phases
        [
          Ladon::Automator::Phase.new(:setup, required: true),
          Ladon::Automator::Phase.new(:execute, required: true, validator: -> automation { automation.result.success? }),
          Ladon::Automator::Phase.new(:teardown, required: true)
        ]
      end

      def execute
      end
end

The above is, in fact, the default implementation of all Ladon Automations, and so you could omit all of the code in the LoginAndLogout class above except for the definition of the execute method!

If using a model

You will need to define the build_model phase, which will create a model harnessing the states and transitions you've written. Your automation will be able to drive through this model to accomplish its goals.

class LoginAndLogout < Ladon::Automator::ModelAutomation

      # Defines ALL of the phases of this automation, in the order they should be executed, as well as other metadata.
      def self.phases
        [
          Ladon::Automator::Phase.new(:setup, required: true),
          Ladon::Automator::Phase.new(:execute, required: true, validator: -> automation { automation.result.success? }),
          Ladon::Automator::Phase.new(:teardown, required: true)
        ]
      end

      def execute
      end

      # Builds the model that will represent the web application
      def build_model
        self.model = Ladon::Modeler::FiniteStateMachine.new
        # use methods of self.model to load a starting state (at a minimum)
      end
end

Now, in any of your automation phases, you can work with your software via self.model, calling any of the FSM methods on it to do work!

Define automation behaviors

All that's left is to implement your actual automation! This involves defining your phases (e.g., execute in these examples) and scripting interactions with your software (as well as any side behaviors, such as accessing data on the internet.)

If you're using a ModelAutomation, any automation of the software itself should ideally happen through the self.model you've defined -- and in fact, if your build_model doesn't result in self.model being a Ladon::Modeler::FiniteStateMachine instance, your automation will die at the verify_model phase.

In either case, you can leverage basically any Ruby code you want (and any gems you've installed) in your automations. The following example shows how to use some of the built-in features available to every automation:

class LoginAndLogout < Ladon::Automator::Automation # or ModelAutomation

      # Defines ALL of the phases of this automation, in the order they should be executed, as well as other metadata.
      def self.phases
        [
          Ladon::Automator::Phase.new(:setup, required: true),
          Ladon::Automator::Phase.new(:execute, required: true, validator: -> automation { automation.result.success? }),
          Ladon::Automator::Phase.new(:teardown, required: true)
        ]
      end

      def execute
        # every automation has a Result object retaining observations made while the automation runs
        @result

        # you can record arbitrary Ruby objects -- KEY and DATA can both be any Ruby object
        # NOTE: when using the default ladon-run utility, these arbitrary objects should have standard serialization methods
        # (e.g., to_s, to_h, to_json, etc etc etc)
        @result.record_data(KEY, DATA)

        # see the Logger API docs for other supported log levels
        @logger.warn('Some message here') 

        # will record begin/end time observed when running the given code block
        @timer.for('Some label here') { some_code_here }

        # If an automation is able to run to completion, its @result will be marked SUCCESS
        # If an unhandled error occurs, the @result will be marked ERROR
        # To define failure cases, write ASSERTIONS, which will mark @result FAILURE if they are not met
        # This pattern is useful for distinguishing "noise" from what your automation considers a real failure
        assert('Some label explaining the assertion') do
          # an assert PASSES if and ONLY if the block returns true (not truthy)
        end

        # assertions are sandboxed by default; if there's a failure, it will note it and continue trying to execute your automation
        # you can also make a "halting" assert, which will raise an Exception and cause the current phase to short circuit.
        hard_assert('Some label explaining the assertion') do
          # code that you assert should work
        end
      end
end

Next: Running Ladon

Clone this wiki locally