-
Notifications
You must be signed in to change notification settings - Fork 6
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!
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.
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...)
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
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
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.
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.
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
.
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 Automation
s, and so you could omit all of the code in the LoginAndLogout
class above except for the definition of the execute
method!
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!
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