Appian Locust Library documentation
What is Appian Locust?
Appian Locust is a wrapper library around Locust for load testing Appian. This library is intended to be used as an alternative to tools such as Jmeter and Load Runner.
Appian Locust capabilities
Logging in and logging out
Form interactions (filling/submitting)
Finding and interacting with basic components on a SAIL interface
Navigating to records/reports/sites
What is Locust?
It’s an open source python library for doing load testing (think JMeter, but in Python). It is by default HTTP-driven, but can be made to work with other types of interactions. Visit Locust for more information.
Locust has the benefit of relying purely on API requests, which makes it lower overhead than frameworks building
on Selenium or browser automation libraries. We have also found python to be common denominator across software and quality engineers,
making it a convenient language for extending the framework and defining tests.
Using Locust’s model of TaskSets
and TaskSequences
, it is easy to compose user operations in a maintainable way.
Appian-Locust builds on these concepts by defining AppianTaskSet
and AppianTaskSequence
, which layer on Appian-specific
functionality such as login and session management.
Quick Installation Guide
This is a quick guide to getting up and running with the appian-locust library. You will need Python 3.10 installed on your machine before proceeding.
Setup
Install appian-locust using pip, for more comprehensive projects we recommend using pipenv.
pip install appian-locust
If using pipenv
, simply start from the following Pipfile
:
[packages]
appian-locust = {version = "*"}
[requires]
python_version = "3.10"
[pipenv]
allow_prereleases = true
Download the sample test example_locustfile.py from the Appian Locust repo and run it.
locust -f example_locustfile.py
If everything is set up correctly, you should see a link to the Locust web interface, which you can use to start test runs and view results.
For more information about how to build the workflow for your locust test, see the How to Write a Locust Test section.
For more information on running locust tests, see the How to Run Locust section.
Build from source
Clone the repository:
git clone -o prod git@gitlab.com:appian-oss/appian-locust.git
Install the library globally:
pip install -e appian-locust
If you’re using a virtualenv or a dependency management tool (e.g. pipenv
), you can do the same type of install, but you will want to be in the context of the virtualenv (i.e. source the virtualenv), and you’ll need to pass the path to the repository you cloned.
Note: It’s highly recommended that you use a virtual environment when installing python artifacts. You can follow the instructions here to install virtualenv and pip.
If you have issues installing, make sure you have the proper prerequisites installed for Locust and its dependencies. If you’re having trouble on Windows, check here
Troubleshooting
Do not have permissions to clone appian-locust
Ensure you have added you ssh key to your profile. See here for how to do this.
“locust is not available”
Verify that you ran
pip install -e appian-locust
“Failed to establish a new connection: [Errno 8] nodename nor servname provided, or not known”
check that
host_address
is specified correctly in your locust test file.
“Login unsuccessful, no multipart cookie found…make sure credentials are correct”
check that auth specifies a valid username and password combination for the site you’re testing on in your locust test file.
“General request and response debugging”
Add
self.client.record_mode = True
to yourHttpUser
subclass. Files will be placed in/record_responses
where the runner is executed.
How to Write a Locust Test
The majority of the work involved in writing Appian Locust tests is around creating Locust Tasks. Each Task represents a workflow for a virtual Locust user to execute.
This section will go over how to get started writing tasks and introduce the two core Appian Locust concepts: the Visitor
class and the SailUiForm
class.
Sample Workflow
For this workflow, we will use the Employee Record Type found in the Appian documentation. We will implement a Locust Task that will create a new Employee record. The specific workflow will be as follows:
Navigate to the Employee Record List
Click the “New Employee” button
Fill in the First Name, Last Name, Department, Title, and Phone Number fields
Click the “Create” button
UI Interactions - SailUiForm
Now that we have navigated to the Employee Record List, we need to execute the workflow steps that will create the new Employee.
As briefly touched on above, all navigations done via the Visitor return a SailUiForm
which is capable of performing interactions with a UI.
SailUiForm supports filling in text fields
,
clicking buttons
and more.
In our specific case, because we navigated to a Record list, our visitor returned a subclass of the SailUiForm: the RecordListUiForm
. Some types of
interfaces in Appian have a specific subclass that will support additional functionality catered to the kind of interface it represents. The RecordListUiForm will enable us to click
on the Record List Action which is unique to Record Lists via click_record_list_action
, like so:
@task
def create_new_employee(self):
# Navigate to Employee Record List
record_list_uiform = self.appian.visitor.visit_record_type(record_type="Employees")
# Click on "New Employee" Record List Action
record_list_uiform.click_record_list_action(label="New Employee")
As shown above, in many cases the only thing required to interact with a UI element is the label associated with that element.
At this point in the workflow, the dialog to create a new employee has been launched. Now we can use the various other interactions supported by the base SailUiForm class which are available on all of its subclasses to fill out the new Employee’s information:
@task
def create_new_employee(self):
# Navigate to Employee Record List
record_list_uiform = self.appian.visitor.visit_record_type(record_type="Employees")
# Click on "New Employee" Record List Action
record_list_uiform.click_record_list_action(label="New Employee")
# Fill in new Employee information
record_list_uiform.fill_text_field(label="First Name", value="Sample")
record_list_uiform.fill_text_field(label="Last Name", value="User")
record_list_uiform.fill_text_field(label="Department", value="Engineering")
record_list_uiform.fill_text_field(label="Title", value="Senior Software Engineer")
record_list_uiform.fill_text_field(label="Phone Number", value="(703) 442-8844")
Now all we need to do is click the “Create” button, and our new Employee will be created!
@task
def create_new_employee(self):
# Navigate to Employee Record List
record_list_uiform = self.appian.visitor.visit_record_type(record_type="Employees")
# Click on "New Employee" Record List Action
record_list_uiform.click_record_list_action(label="New Employee")
# Fill in new Employee information
record_list_uiform.fill_text_field(label="First Name", value="Sample")
record_list_uiform.fill_text_field(label="Last Name", value="User")
record_list_uiform.fill_text_field(label="Department", value="Engineering")
record_list_uiform.fill_text_field(label="Title", value="Senior Software Engineer")
record_list_uiform.fill_text_field(label="Phone Number", value="(703) 442-8844")
# Create Employee!
record_list_uiform.click_button(label="Create")
If you run a locust test with the task above, you should be able to check the Employee record list and see the “Sample User” employees that the virtual Locust user just made! You can see a full version of a locust test including the task we just wrote here.
How to Run Locust
Once you have the library installed, you can simply follow the output from running locust -h
to run your test.
Note that if you don’t specify a locustfile with -f FILE_NAME
, locust will look for a file called locustfile in the current directory by default.
Command Line Flow
If you’re running Locust somewhere where there is no web ui, or you don’t want to bother with the web flow, I tend to run Locust like so:
locust -f examples/example_locustfile.py -u 1 -r 10 -t 3600 --headless
Required Arguments
This is the bare minimum to run a Locust test without the web view. You’ll notice you need to specify:
The hatch rate (-r is for rate)
The number of users (-u is for users)
The time of the test (-t) in seconds
The –headless flag
Once you run this command, the test will start immediately, and start logging output. It should run for the duration you run the test, and hitting ctrl+c may orphan some locusts. Some arguments that we use are
–csv-full-history -> prints out the different percentile changes every 30 seconds
It’s recommended to capture log file output when running Locust as well, i.e. | tee run.log
Web Flow
If you supply no arguments to locust
other than the locustfile, Locust will launch in a web mode. This is not recommended, mainly because you can’t automate running it as it requires manual interaction as well as access to the flask application.
locust -f example_locustfile.py
If you navigate to http://localhost:8089/ you’ll see the following:

These arguments map to the same arguments described in the Required Arguments section.
Once you hit “start swarming”, you’ll see graphs reporting latencies, errors, and other data. These can be useful for visually understanding how a load test is performing.
Debugging
Oftentimes you will encounter errors or not get the appropriate load you expect when running tests.
Things you can use to get more information:
Add print statements to your Locust code or the installed
appian-locust
libraryInspect the output of the latencies that Locust periodically prints out, to see if certain requests are much slower than you expect
Verify using the browser console that the requests you are attempting to simulate match up with what Locust/appian-locust is sending
Setting the “record_mode” attribute to True on your HttpUser’s
client
object will create a “recorded_responses” folder which will contain all requests and responses sent during test execution. You can do this in the__init__
method of your HttpUser, like so:
def __init__(self, environment) -> None:
super().__init__(environment)
self.client.record_mode = True
Limitations
Disclaimer: This library is continuously evolving. Currently the main focus is supporting essential use-cases. We are happy to accept contributions to further extend functionality, address bug fixes and improve usability. Please see the Contributing section and feel free to reach out.
Currently unsupported Appian Interactions
Multiple links with the same name on the same page, use labels to differentiate
Legacy forms
Limitations when running Locust
On Windows, running locust in distributed mode is not supported
Advanced Appian Locust Usage
Loading Test Settings from Config
These three lines look for a config.json
file at the location from which the script is run (not where the locustfile is).
from appian_locust.utilities import loadDriverUtils
utls = loadDriverUtils()
utls.load_config()
This takes the content of the config.json
file and places it into a variable as utls.c.
This allows us to access configurations required for logging in inside the class that extends HttpUser:
config = utls.c
auth = config['auth']
host = "https://" + config['host_address']
A minimal config.json looks like:
{
"host_address": "site-name.appiancloud.com",
"auth": [
"user.name",
"password"
]
}
Advanced Test Examples
Locust Test Example: Records
An example of a Locust Test showing interaction with Appian Records - example_locust_test_records.py.
This test has 2 locust tasks defined and it will execute all three for each spawned locust user for the duration of the test.
Task 1 will visit a random record instance for a random record type and return the SAIL form.
Task 2 will visit a random record type list and get the SAIL form for it, then filter the records in the list.
@task takes an optional weight argument that can be used to specify the task’s execution ratio. For example: If there are two tasks with weights 3 and 6 then second task will have twice the chance of being picked as first task.
# Locust tests executing against Appian with a TaskSet should set AppianTaskSet as their base class to have access to various functionality.
# This class handles creation of basic objects like self.appian (appian client) and actions like `login` and `logout`
class RecordsTaskSet(AppianTaskSet):
def on_start(self):
super().on_start()
# could be either view or list:
if "endpoint_type" not in CONFIG: # could be either view or list:
logger.error("endpoint_type not found in config.json")
sys.exit(1)
self.endpoint_type = CONFIG["endpoint_type"]
# view - to view records from opaqueId
# list - to list all the record types from url_stub
if "view" not in self.endpoint_type and "list" not in self.endpoint_type:
logger.error(
"This behavior is not defined for the provided endpoint type. Supported types are : view and list")
sys.exit(1)
def on_stop(self):
logger.info("logging out")
super().on_stop()
@task(X)
def visit_random_record(self):
if "view" in self.endpoint_type:
self.appian.visitor.visit_record_instance()
@task(X)
# this task visits a random record type list and return the SAIL form.
def visit_random_record_type_list(self):
if "list" in self.endpoint_type:
record_list = self.appian.records.visit_record_type_and_get_form()
record_list.filter_records_using_searchbox("Favorite Record Name")
By calling super().on_start() inside the on_start() function of the locust test you get access to the appian client which allows you to call self.appian.visitor, self.appian.system_operator etc. These properties allow us to navigate to a specific object or access metadata about available objects.
Functions like visit_XYZ() access the actual appian object and return its SAIL form as an instance of
uiform
. This class contains methods that helps you interact with the UI.
Locust Test Example: Grids
An example of a Locust Test showing interaction with Appian Grids - example_locust_test_grids.py.
This test has a locust task defined that will interact with a read-only paging grid layout that contains an Employee directory. Configuration for this grid layout is inspired by Appian’s Grid Tutorial. The goal of this test is to select all Engineering employees and view the details for one of them.
The first step in this workflow is to navigate our user to a report which is backed by an interface containing the grid:
@task
def interact_with_grid_in_interface(self):
# Navigate to the interface backed report that contains a grid
report_uiform = self.appian.visitor.visit_report(report_name="Employee Report with Grid")
The interface will look similar to this:

Now that we have navigated to the report, we will sort the grid by the Department field in the ascending order to have all Engineering department employees at the top:
@task
def interact_with_grid_in_interface(self):
# Navigate to the interface backed report that contains a grid
report_uiform = self.appian.visitor.visit_report(report_name="Employee Report with Grid")
# Sort the grid rows by the "Department" field name
report_uiform.sort_paging_grid(label="Employee Directory", field_name="Department", ascending=True)
The interface with the sorted grid will look similar to this:

Next, we will select the first five rows on the first page of the grid:
@task
def interact_with_grid_in_interface(self):
# Navigate to the interface backed report that contains a grid
report_uiform = self.appian.visitor.visit_report(report_name="Employee Report with Grid")
# Sort the grid rows by the "Department" field name
report_uiform.sort_paging_grid(label="Employee Directory", field_name="Department", ascending=True)
# Select the first five rows on the first page of the grid
report_uiform.select_rows_in_grid(rows=[0,1,2,3,4], label="Employee Directory")
Because the grid is configured to show the selected rows under Selected Employees, the resultant interface will look similar to this:

During development, this would be a good way to test the selection using the JSON response from the above request. Next, we will move to the second page of the grid and select the first row since it also contains an Engineering employee:
@task
def interact_with_grid_in_interface(self):
# Navigate to the interface backed report that contains a grid
report_uiform = self.appian.visitor.visit_report(report_name="Employee Report with Grid")
# Sort the grid rows by the "Department" field name
report_uiform.sort_paging_grid(label="Employee Directory", field_name="Department", ascending=True)
# Select the first five on the first page of the grid
report_uiform.select_rows_in_grid(rows=[0,1,2,3,4], label="Employee Directory")
# Move to the second page of the grid
report_uiform.move_to_right_in_paging_grid(label="Employee Directory")
# Select the first row on the second page of the grid
report_uiform.select_rows_in_grid(rows=[0], label="Employee Directory", append_to_existing_selected=True)
The interface will look similar to this:

The grid contains a First Name column which is a link to the employee record. Finally, we will click on the link for an employee William:
@task
def interact_with_grid_in_interface(self):
# Navigate to the interface backed report that contains a grid
report_uiform = self.appian.visitor.visit_report(report_name="Employee Report with Grid")
# Sort the grid rows by the "Department" field name
report_uiform.sort_paging_grid(label="Employee Directory", field_name="Department", ascending=True)
# Select the first five on the first page of the grid
report_uiform.select_rows_in_grid(rows=[0,1,2,3,4], label="Employee Directory")
# Move to the second page of the grid
report_uiform.move_to_right_in_paging_grid(label="Employee Directory")
# Select the first row on the second page of the grid
report_uiform.select_rows_in_grid(rows=[0], label="Employee Directory", append_to_existing_selected=True)
# Click on the row with a record link with the given label
report_uiform.click_record_link(label="William")
The user will be navigated to the employee’s record which will look similar to this:

You can see a full version of this locust test here. There are other useful functions for interacting with grids that can be found in our documentation.
Locust Test Example: Multiple Users
An example of a Locust Test showing interaction with the frontend and admin pages - example_multi_user_locustfile.py.
This test has 2 locust TaskSets defined and 2 HttpUsers defined that simulate different login information.
TaskSets
The
GetFrontPageTaskSet
simply navigates to a basic user landing pageThe
GetAdminPageTaskSet
navigates to the admin console
HttpUsers
The
FrontendUserActor
uses the login credentials for the regular frontend userThe
AdminUserActor
uses the login credentials for the admin user
Running
When running this locustfile, it is important to note that the users specified when running are spread evenly across the HttpUsers. If you only specify one user to run when running locust, it will only choose one user actor to start:
locust -f examples/example_multi_user_locustfile.py --headless -r 10 -t 3600 --users 1
...
All users hatched: FrontendUserActor: 1, AdminUserActor: 0 (0 already running)
Make sure to run the locustfile with at least as many users as required by how you have the weights configured for each HttpUser.
For the included sample, the weights are 3 and 1 respectively, meaning you’ll have to spawn 4 users to get the AdminUserActor
to start up
locust -f examples/example_multi_user_locustfile.py --headless -r 10 -t 3600 --users 4
...
All users hatched: FrontendUserActor: 3, AdminUserActor: 1 (0 already running)
Advanced Task Execution
Executing a specific number of tasks
One can also write a test that executes a set number of iterations of your TaskSequence Class and all its tasks, instead of executing the test for X number of seconds/mins/hours. Here’s a snippet showing how to run a test for a set number of iterations.
import json
import os
from locust import HttpUser, task, between, events
from appian_locust import AppianTaskSet
@events.init.add_listener
def on_locust_init(environment, **kw):
global ENV
ENV = environment
class OrderedEndToEndTaskSequence(AppianTaskSequence):
@task
def nav_to_random_site(self):
pass
@task
def nav_to_specific_site(self):
pass
@task
def increment_iteration_counter(self):
logger.info(f"Iteration Number: {self.iterations}")
# Stop the test if 40 iterations of the set have been completed.
# This would mean approximately 40K requests in total for the test.
if self.iterations >= CONFIG["num_of_iterations"]:
logger.info(f"Stopping the Locust runner")
ENV.runner.quit()
else:
logger.info(f"Incrementing the iteration set counter")
self.iterations += 1
class UserActor(HttpUser):
tasks = [GetFrontPageTaskSet]
config_file = "./example_config.json"
CONFIG = {}
if os.path.exists(config_file):
with open(config_file, 'r') as config_file:
CONFIG = json.load(config_file)
else:
raise Exception("No config.json found")
host = f'https://{CONFIG["host_address"]}'
auth = CONFIG["auth"]
wait_time = between(0.500, 0.500)
Note: CONFIG["num_of_iterations"]
is retrieved from the test configuration. This should be provided in the example_config.json file.
The way to achieve specific number of tasks in this test is by having a counter in your task, that you increment once in a specific Locust task and then stop the test when you have reached the desired number of iterations.
Waiting until all users are spawned
If you want to wait for your test to spawn all of the Locust users
from gevent.lock import Semaphore
all_locusts_spawned = Semaphore()
all_locusts_spawned.acquire()
@events.spawning_complete.add_listener
def on_spawn_complete(**kw):
print("All news users can start now!")
all_locusts_spawned.release()
class WaitingTaskSet(AppianTaskSet):
def on_start(self):
""" Executes before any tasks begin."""
super().on_start()
all_locusts_spawned.wait()
Latest Release
Version 2.0.0
Appian Locust v2.0 introduces a significant rework of the API to guide a clear and streamlined development experience. Our target was to meet feature parity while simplifying the steps to interact with Appian.
New Paradigm
Visitor
is the new hub for all SailUiForm navigations. From the client, you can call various methods to retrieve the desiredSailUiForm
that matches the type of page that the caller has navigated to, which will enable further interaction with the represented UI.SystemOperator
is for non UI form interactions at the system level (i.e.get_webapi()
).The
info
module extended fromAppianClient
provides metadata for News, Tasks, Records, Reports, Actions, and Sites (i.e.appian_locust.appian_client.AppianClient.actions_info
).
Breaking Changes
Appian Locust now requires Python 3.10. Update your dependencies globally or within your dependency management config file.
Fetching SailUIForms from News, Tasks, Records, Reports, Actions, and Sites have been marked as private. Use
Visitor
to handle all UI navigations.SailUIForms types can be found in the
uiform
module.Design objects and types have been moved to the
objects
module.Various helper methods have been moved to the
utilities
module.loadDriverUtils()
does not provideutls
anymore. Instead, callloadDriverUtils()
to setutls
:from appian_locust.utilities import loadDriverUtils utls = loadDriverUtils()
For a more comprehensive list of changes in Appian Locust 2.0, see the Appian Locust 2.0 Migration Guide document.
Appian Locust 2.0 Migration Guide
_actions.py
The _Actions class is no longer available. You may migrate any functionality using this class as follows:
Method in 1.x |
Method in 2.x |
Example Usage |
---|---|---|
get_actions_interface |
Not available anymore. Handled internally by the framework whenever it is necessary, so any calls to this can be removed without replacement. |
|
get_actions_feed |
Not available anymore. Handled internally by the framework whenever it is necessary, so any calls to this can be removed without replacement. |
|
get_all |
We can do the same operation from the actions_info.py method named “get_all_available_actions”. |
all_available_actions = |
get_action |
We can do the same operation from the actions_info.py method named “get_action_info”. |
specific_action_info = |
visit_and_get_form |
We can perform the same operation by the “visit_action” method from visitor.py. |
uiform = |
visit |
Not available. Call the “visit_action” method from visitor.py instead. |
uiform = |
start_action |
We can perform the same operation by calling “start_action” in system_operator.py |
response = |
_admin.py
The _Admin class is no longer available. You may migrate any functionality using this class as follows:
Method in 1.x |
Method in 2.x |
Example Usage |
---|---|---|
visit |
We can do the same operation by calling “visit_admin” method in visitor.py |
uiform = |
_design.py
The _Design class is no longer available. You may migrate any functionality using this class as follows:
Method in 1.x |
Method in 2.x |
Example Usage |
---|---|---|
visit |
We can do the same operation by calling “visit_design” method in visitor.py |
uiform = |
visit_object |
We can do the same operation by calling “visit_design_object_by_id” method in visitor.py |
design_object_uiform = |
visit_app |
We can do the same operation by calling “visit_application_by_id” method in visitor.py |
application_uiform = |
create_application |
We can do the same operation by calling “create_application” method in design_uiform.py |
design_uiform = self.appian.visit_design()
application_uiform =
design_uiform.create_application(..) |
create_record_type |
We can do the same operation by calling “create_record_type” method in application_uiform.py |
application_uiform = self.appian.visitor.visit_application_by_id(..)
application_uiform =
application_uiform.create_record_type(..) |
create_report |
We can do the same operation by calling “create_recport” method in application_uiform.py |
application_uiform = self.appian.visitor.visit_application_by_id(..)
application_uiform =
application_uiform.create_report(..) |
_news.py
The _News class is no longer available. You may migrate any functionality using this class as follows:
Method in 1.x |
Method in 2.x |
Example Usage |
---|---|---|
get_all |
We can do the same operation by calling the “get_all_available_entries” method in news_info.py. |
news_dict = |
get_news |
We can do the same operation by calling the “get_news_entry” method in news_info.py. |
specific_news_info = |
visit |
Not available. Call “get_news_entry” method in news_info.py instead. |
specific_news_info = |
visit_news_entry |
Not available. Call “get_news_entry” method in news_info.py instead. |
specific_news_info = |
search |
We can do the same operation by calling the “get_all_available_entries” with a search string argument. |
uiform = |
_records.py
The _Records class is no longer available. You may migrate any functionality using this class as follows:
Method in 1.x |
Method in 2.x |
Example Usage |
---|---|---|
visit_record_instance_and_get_feed_form |
Not available. Call “visit_record_instance” method in visitor.py instead. |
record_instance_uiform = |
visit_record_instance_and_get_form |
We can do the same operation by calling the “visit_record_instance” method in visitor.py |
record_instance_uiform = |
visit_record_type_and_get_form |
We can do the same operation by calling the “visit_record_type” method in visitor.py |
record_list_uiform = |
get_all |
We can do the same operation from the record_list_uiform.py method named “get_visible_record_instances”. |
record_list_uiform = self.appian.visitor.visit_record_type(..)
all_records_info =
record_list_uiform.get_visible_record_instances(..) |
get_all_record_types |
“get_all_available_record_types” in records_info.py |
records_dict = |
get_all_records_of_record_type |
“get_visible_record_instances” in record_list_uiform.py |
record_list_uiform = self.appian.visitor.visit_record_type(..)
all_records_info =
record_list_uiform.get_visible_record_instances(..) |
get_records_interface |
Not available. Handled internally by the framework whenever it is necessary, so any calls to this can be removed without replacement. |
|
get_records_nav |
Not available. Handled internally by the framework whenever it is necessary, so any calls to this can be removed without replacement. |
|
get_all_records_of_record_type_mobile |
To interact with an Appian instance as a mobile client, pass in “is_mobile_client=True” in the AppianTaskSet on_start method. |
class SampleTaskSet(AppianTaskSet)
def on_start(self):
super().on_start(is_mobile_client=True)
|
get_all_mobile |
To interact with an Appian instance as a mobile client, pass in “is_mobile_client=True” in the AppianTaskSet on_start method. |
class SampleTaskSet(AppianTaskSet)
def on_start(self):
super().on_start(is_mobile_client=True)
|
fetch_record_instance |
Not available. Instead call “visit_record_instance” method in visitor.py |
record_instance_uiform = |
fetch_record_type |
Not available. Instead call “visit_record_type” method in visitor.py |
record_list_uiform = |
visit_record_instance |
Not available. Instead call “visit_record_instance” method in visitor.py |
record_instance_uiform = |
visit_record_type |
Not available. Instead call “visit_record_type” method in visitor.py |
record_list_uiform = |
_reports.py
The _Reports class is no longer available. You may migrate any functionality using this class as follows:
Method in 1.x |
Method in 2.x |
Example Usage |
---|---|---|
get_reports_interface |
Not available anymore. Handled internally by the framework whenever it is necessary, so any calls to this can be removed without replacement. |
|
get_reports_nav |
Not available anymore. Handled internally by the framework whenever it is necessary, so any calls to this can be removed without replacement. |
|
get_all |
We can do the same operation by calling the “get_all_available_reports” in reports_info.py |
reports_dict = |
get_report |
We can do the same operation by calling the “get_report_info” in reports_info.py |
report_info = |
visit_and_get_form |
We can do the same operation by calling the “visit_report” in visitor.py |
uiform = |
visit |
Not available. Instead call “visit_report” in visitor.py |
uiform = |
_tasks.py
The _Tasks class is no longer available. You may migrate any functionality using this class as follows:
Method in 1.x |
Method in 2.x |
Example Usage |
---|---|---|
get_all |
We can do the same operation by calling “get_all_available_tasks” method in tasks_info.py |
tasks_dict = |
get_task_pages |
Not available. Handled internally by the framework whenever it is necessary, so any calls to this can be removed without replacement. |
|
get_next_task_page_uri |
Not available. Handled internally by the framework whenever it is necessary, so any calls to this can be removed without replacement. |
|
visit_and_get_form |
We can do the same operation by calling “visit_task” method in visitor.py |
uiform = |
visit |
Not available. Instead call “visit_task” in visitor.py |
uiform = |
_sites.py
The _Sites class is no longer available. You may migrate any functionality using this class as follows:
Method in 1.x |
Method in 2.x |
Example Usage |
---|---|---|
navigate_to_tab |
Not available. Instead call “visit_site” in visitor.py |
uiform = |
navigate_to_tab_and_record_if_applicable |
Not available. Instead call “visit_site_recordlist_and_get_random_record_form” in visitor.py |
record_instance_uiform = |
navigate_to_tab_and_record_get_form |
We can do the same operation by calling “visit_site_recordlist_and_get_random_record_form” in visitor.py |
record_instance_uiform = |
get_all |
We can do the same operation by calling “get_all_available_sites” in sites_info.py |
sites_dict = |
get_site_data_by_site_name |
We can do the same operation by calling “get_site_info” in sites_info.py |
specific_site = |
get_page_names_from_ui |
Not available. Instead call “get_site_info” in sites_info.py |
specific_site = |
get_site_page |
Not available. You can get page information from the Site object returned by “get_site_info” in sites_info.py |
specific_site = |
visit_and_get_form |
We can do the same operation by calling “visit_site” in visitor.py |
uiform = |
_app_importer.py
The _app_importer module is no longer available. You may migrate any functionality using this module as follows:
Method in 1.x |
Method in 2.x |
Example Usage |
---|---|---|
import_app |
Available on DesignUiForm.py as “import_application” |
design_uiform = self.appian.visitor.visit_design()
design_uiform.import_application(..) |
uiform.py
The following methods in SailUiForm have removed or modified:
Method in 1.x |
Method in 2.x |
Example Usage |
---|---|---|
get_record_header_form |
Available on RecordInstanceUiForm.py as “get_header_view” |
record_form = self.appian.visitor.visit_record_instance(“record_type”, “record_name”)
record_header_form =
record_form.get_header_view() |
get_record_view_form |
Available on RecordInstanceUiForm.py as “get_summary_view” |
record_form = self.appian.visitor.visit_record_instance(“record_type”, “record_name”)
record_header_form =
record_form.get_summary_view() |
get_response |
Not available. Instead use “get_latest_state” |
|
latest_state |
Not available. Instead use “get_latest_state” |
|
get_latest_form |
Not available. This method basically just returned “this”, so it was unnecessary. |
|
click_record_link |
This method now returns a new type, a RecordInstanceUiForm, so you must make sure to save the return value into a new variable |
record_uiform: |
_records_helper.py
This class and all its methods are not accessible anymore.
_interactor.py
This class and all its methods are not accessible anymore. There are corresponding methods in other new classes/existing classes.
Import Changes
Class/Module |
Import in V1 |
Import in V2 |
---|---|---|
helper |
from appian_locust.helper import * |
from appian_locust.utilities.helper import * |
Site |
from appian_locust._sites import Site |
from appian_locust.objects import Site |
Page |
from appian_locust._sites import Page |
from appian_locust.objects import Page |
PageType |
from appian_locust._sites import PageType |
from appian_locust.objects import PageType |
utls |
from appian_locust.loadDriverUtils import utls |
from appian_locust.utilities import loadDriverUtils
utls = loadDriverUtils()
|
API
- class appian_locust.feature_flag.FeatureFlag(value)
Bases:
Enum
An enumeration.
- ALL_FEATURES = 1
- ALWAYS_ADD_RECORD_TYPE_INFORMATION = 45
- BILLBOARD_LAYOUT = 35
- BODY_URI_TEMPLATES = 14
- BOX_LAYOUT = 20
- CARD_LAYOUT = 39
- CERTIFIED_SAIL_EXTENSION = 47
- COMPACT_URI_TEMPLATES = 10
- DATA_EXPORT = 23
- DOCUMENT_VIEWER_LAYOUT = 44
- EVOLVED_BILLBOARD_LAYOUT = 50
- EVOLVED_GRIDFIELD = 52
- FILTERS_LAYOUT = 48
- FLUSH_RECORD_HEADERS = 57
- GAUGE_FIELD = 55
- GRID_ROW_SELECTION = 40
- ICON_WIDGET = 31
- IMAGES_INTERFACES_2 = 27
- IMAGE_CROPPING = 21
- IMPLICIT_SYSTYPE_NAMESPACE = 5
- INLINE_TASK_CONTROLS = 12
- IN_APP_BROWSER_AUTH = 59
- JUSTIFIED_LABEL_POSITION = 22
- LESS_OPAQUE_BILLBOARD_OVERLAYS = 58
- MEDIUM_LARGE_RICH_TEXT = 19
- MODERN_RECORD_TYPES_LIST = 38
- MULTIPLE_SAVE_USER_FILTERS = 60
- MULTI_SELECT_RECORD_FILTERS = 17
- NESTED_COLUMNS = 16
- NEWS_ENTRY_LAYOUT = 43
- NEWS_SUBSCRIPTION_SETTINGS = 46
- NEW_COLUMN_WIDTHS = 53
- NEW_RICH_TEXT_SIZES = 54
- NO_FEATURES = 0
- OFFLINE = 6
- PARTIAL_RENDERING = 7
- POSITIVE_NEGATIVE_RICH_TEXT = 18
- REACT_CLIENT = 11
- RECORD_ACTION_COMPONENT = 61
- RECORD_CHROME = 34
- RECORD_LIST_ACTION_LINK = 49
- RECORD_LIST_FEED_ITEM_DTO = 36
- RECORD_NEWS = 4
- RECORD_NEWS_FIELD = 37
- RELATIVE_URI_TEMPLATES = 13
- REPORT_LINK = 26
- REST_REDIRECT = 9
- RICH_TEXT_ACCENT_STYLE = 32
- SAIL_FORMS = 2
- SHORT_CIRCUIT_PARTIAL_RENDERING = 8
- SIDE_BY_SIDE_LAYOUT = 28
- SITE_RECORD_NEWS = 24
- START_PROCESS_LINK = 25
- SUBMISSION_LOCATION = 51
- TAG_FIELD = 56
- TASK_FORM_LAYOUT = 33
- TASK_PREVIEW = 3
- TWO_PART_RECORD_TAG_URI = 15
- USE_CLIENT_LOCALE = 41
- USE_MULTIPART_RECORD_UIS = 42
- WCC_READ_ONLY = 29
- WCC_READ_WRITE = 30
Exceptions module
- exception appian_locust.exceptions.exceptions.BadCredentialsException
Bases:
Exception
- exception appian_locust.exceptions.exceptions.ChoiceNotFoundException
Bases:
Exception
- exception appian_locust.exceptions.exceptions.ComponentNotFoundException
Bases:
Exception
- exception appian_locust.exceptions.exceptions.IncorrectDesignAccessException(object_type: str, correct_access_method: str)
Bases:
Exception
- exception appian_locust.exceptions.exceptions.InvalidComponentException
Bases:
Exception
- exception appian_locust.exceptions.exceptions.InvalidDateRangeException(start_date: date, end_date: date)
Bases:
Exception
- exception appian_locust.exceptions.exceptions.InvalidSiteException
Bases:
Exception
- exception appian_locust.exceptions.exceptions.MissingConfigurationException(missing_keys: list)
Bases:
Exception
- exception appian_locust.exceptions.exceptions.MissingCsrfTokenException(found_cookies: dict)
Bases:
Exception
- exception appian_locust.exceptions.exceptions.MissingUrlProviderException
Bases:
Exception
- exception appian_locust.exceptions.exceptions.PageNotFoundException
Bases:
Exception
- exception appian_locust.exceptions.exceptions.SiteNotFoundException
Bases:
Exception
Info module
Objects module
- class appian_locust.objects.ai_skill.AiSkill(host_url: str, object_uuid: str)
Bases:
object
- class appian_locust.objects.ai_skill_type.AISkillObjectType(value)
Bases:
Enum
An enumeration.
- DOCUMENT_CLASSIFICATION = 2
- DOCUMENT_EXTRACTION = 5
- EMAIL_CLASSIFICATION = 8
- class appian_locust.objects.application.Application(name: str, opaque_id: str)
Bases:
object
Class representing an application
- class appian_locust.objects.design_object.DesignObject(name: str, opaque_id: str)
Bases:
object
Class representing an Design Object
- class appian_locust.objects.design_object.DesignObjectType(value)
Bases:
Enum
An enumeration.
- DATA_TYPE = 'Data Type'
- DECISION = 'Decision'
- EXPRESSION_RULE = 'Expression Rule'
- INTEGRATION = 'Integration'
- INTERFACE = 'Interface'
- RECORD_TYPE = 'Record Type'
- SITE = 'Site'
- TRANSLATION_SET = 'Translation Set'
- WEB_API = 'Web API'
- class appian_locust.objects.page.Page(page_name: str, page_type: PageType, site_stub: str, group_name: str | None = None)
Bases:
object
Class representing a single Page within a site
Uiform module
Utilities module
- appian_locust.utilities.credentials.procedurally_generate_credentials(CONFIG: dict) None
Helper method that can be used to procedurally generate a set of Appian user credentials
Note: This class must be called in the UserActor class of your Locust test in order to create the credentials before any Locust users begin to pick them up.
- Parameters:
CONFIG – full locust config dictionary, AKA the utls.c variable in locust tests Make sure the following keys are present.
procedural_credentials_prefix – Base string for each generated username
procedural_credentials_count – Appended to prefix, will create 1 -> Count+1 users
procedural_credentials_password – String which will serve as the password for all users
- Returns:
None
- appian_locust.utilities.credentials.setup_distributed_creds(CONFIG: dict) dict
Helper method to distribute Appian credentials across separate load drivers when running Locust in distributed mode. Credential pairs will be passed out in Round Robin fashion to each load driver.
Note: This class must be called in the UserActor class of your Locust test to ensure that the “credentials” key is prepared before tests begin.
Note: If fewer credential pairs are provided than workers, credentials will be distributed to workers in a Modulo fashion.
- Parameters:
CONFIG – full locust config dictionary, AKA the utls.c variable in locust tests Make sure the following keys are present.
- Returns:
same as input but with credentials key updated to just the subset of credentials required for given load driver.
- Return type:
CONFIG
- appian_locust.utilities.helper.extract_all_by_label(obj: dict | list, label: str) list
Recursively search for all fields with a matching label in JSON tree. :param obj: The json tree to search for fields in :param label: The label used to identify elements we want to return
Returns (list): A list of all elements in obj that match label
- appian_locust.utilities.helper.extract_values(obj: Dict[str, Any], key: str, val: Any) List[Dict[str, Any]]
Pull all values of specified key from nested JSON.
- Parameters:
obj (dict) – Dictionary to be searched
key (str) – tuple of key and value.
val (any) – value, which can be any type
- Returns:
list of matched key-value pairs
- appian_locust.utilities.helper.extract_values_multiple_key_values(obj: Dict[str, Any], key: str, vals: List[Any]) List[Dict[str, Any]]
Pull all values where the key value matches an entry in vals from nested JSON.
- Parameters:
obj (dict) – Dictionary to be searched
key (str) – a key in the dictionary
vals (List[any]) – A list of values corresponding to the key, which can be any type
- Returns:
list of matched key-value pairs
- appian_locust.utilities.helper.find_component_by_attribute_and_index_in_dict(attribute: str, value: str, index: int, component_tree: Dict[str, Any]) Any
Find a UI component by the given attribute (label for example) in a dictionary It returns the index’th match in a depth first search of the json tree It returns the dictionary that contains the given attribute with the given value or throws an error when none is found
- Parameters:
attribute – an attribute to search (‘label’ for example)
value – the value of the attribute (‘Submit’ for example)
index – the index of the component to find if multiple components are found with the same ‘value’ for ‘attribute’ (1 for example)
component_tree – the json response.
- Returns:
The json object of the component
- Raises:
ComponentNotFoundException if the component cannot be found. –
Example
>>> find_component_by_attribute_and_index_in_dict('label', 'Submit', 1, self.json_response)
will search the json response to find the first component that has ‘Submit’ as the label
- appian_locust.utilities.helper.find_component_by_attribute_in_dict(attribute: str, value: str, component_tree: Dict[str, Any], raise_error: bool = True, throw_attribute_exception: bool = False) Any
Find a UI component by the given attribute (label for example) in a dictionary It only returns the first match in a depth first search of the json tree It returns the dictionary that contains the given attribute with the given value or throws an error when none is found.
- Parameters:
attribute – an attribute to search (‘label’ for example)
value – the value of the attribute (‘Submit’ for example)
component_tree – the json response.
raise_error – If set to False, will return None instead of raising an error. (Default: True)
throw_attribute_exception – If set to False then if the component is not found an exception is thrown using the attribute and value in the exception.
- Returns:
The json object of the component
- Raises:
ComponentNotFoundException if the component cannot be found. –
Example
>>> find_component_by_attribute_in_dict('label', 'Submit', self.json_response)
will search the json response to find a component that has ‘Submit’ as the label
- appian_locust.utilities.helper.find_component_by_index_in_dict(component_type: str, index: int, component_tree: Dict[str, Any]) Any
Find a UI component by the index of a given type of component (“RadioButtonField” for example) in a dictionary Performs a depth first search and counts quantity of the component, so the 1st is the first one It returns the dictionary that contains the given attribute with the requested index or throws an error when none is found.
- Parameters:
component_type – type of the component(#t in the JSON response, ‘RadioButtonField’ for example)
index – the index of the component with the component_type (‘1’ for example - Indices start from 1)
component_tree – the json response
- Returns:
The json object of the component
- Raises:
ComponentNotFoundException if the component cannot be found. –
Example
>>> find_component_by_index_in_dict('RadioButtonField', 1, self.json_response)
will search the json response to find the first component that has ‘RadioButtonField’ as the type
- appian_locust.utilities.helper.find_component_by_label_and_type_dict(attribute: str, value: str, type: str, component_tree: Dict[str, Any], raise_error: bool = True) Any
Find a UI component by the given attribute (like label) in a dictionary, and the type of the component as well. (#t should match the type value passed in) It only returns the first match in a depth first search of the json tree. It returns the dictionary that contains the given attribute with the given label and type or throws an error when none is found if the raise_error value is True. Otherwise it will return None if the component cannot be found.
- Parameters:
label – label of the component to search
value – the value of the label
type – Type of the component (TextField, StartProcessLink etc.)
component_tree – the json response.
raise_error – If set to False, will return None instead of raising an error. (Default: True)
- Returns:
The json object of the component or None if the component cannot be found.
- Raises:
ComponentNotFoundException if the component cannot be found. –
Example
>>> find_component_by_label_and_type_dict('label', 'MyLabel', 'StartProcessLink', self.json_response)
- appian_locust.utilities.helper.find_component_by_type_and_attribute_and_index_in_dict(component_tree: Dict[str, Any], type: str = '', attribute: str = '', value: str = '', index: int = 1, raise_error: bool = True) Any
Find a UI component by the given type and/or attribute with ‘value’ in a dictionary Returns the index’th match in a depth first search of the json tree Returns the dictionary that contains the given attribute with the given value or throws an error when none is found Note: Both type and attribute matching are optional, which will cause this function to return the index’th component in the tree
- Parameters:
component_tree – the json response
- Keyword Arguments:
type (str) – the component type to match against (default: ‘’)
attribute (str) – an attribute to search (default: ‘’)
value (str) – the value of the attribute (default: ‘’)
index (int) – the index of the component to find if multiple components match the above criteria, 1-indexed (default: 1)
raise_error (bool) – if this is set to false, it will return None instead of raising an error.
- Returns:
The json object of the component or None if ‘raise_error’ is set to false.
- Raises:
ComponentNotFoundException if the attribute or type checks fail. –
Exception if the component is found but at an incorrect index. –
Example
>>> find_component_by_attribute_and_index_in_dict('label', 'Submit', 1, self.json_response)
will search the json response to find the first component that has ‘Submit’ as the label
- appian_locust.utilities.helper.format_label(label: str, delimiter: str | None = None, index: int = 0) str
Simply formats the string by replacing a space with underscores
- Parameters:
label – string to be formatted
delimiter – If provided, string will be split by it
index – used with delimiter parameter, which item will be used in the “split”ed list.
- Returns:
formatted string
- appian_locust.utilities.helper.get_random_item(list_of_items: List[Any], exclude: List[Any] = []) Any
Gets a random item from the given list excluding the items if any provided
- Parameters:
list_of_items – list of items of any data type
exclude – if any items needs to be excluded in random pick
- Returns:
Randomly picked Item
- Raises:
In case of no item to pick, Exception will be raised –
- appian_locust.utilities.helper.get_username(auth: list) str
Returns the username from an auth list :param auth: Appian Locust authorization list
Returns (str): Username from auth list
- appian_locust.utilities.helper.list_filter(list_var: List[str], filter_string: str, exact_match: bool = False) List[str]
from the given list, return the list with filtered values.
- Parameters:
list_var (list) – list of strings
filter_string (str) – string which will be used to filter
exact_match (bool, optional) – filter should be based on exact match or partial match. default is partial.
- Returns:
List with filtered values
- appian_locust.utilities.helper.remove_type_info(sail_dict: Dict[str, Any]) Dict[str, Any]
Returns a flattened dictionary with SAIL type info removed :param sail_dict: SAIL Dictionary to remove type information from
Returns (dict): Flattened dictionary
- appian_locust.utilities.helper.repeat(num_times: int = 2, wait_time: float = 0.0) Callable
This function allows an arbitrary function to be executed an arbitrary number of times The intended use is as a decorator:
>>> @repeat(2) ... def arbitrary_function(): ... print("Hello World") ... arbitrary_function() Hello World Hello World
- Parameters:
num_times (int) – an integer
- Implicit Args:
arbitrary_function (Callable): a python function
- Returns:
A reference to a function which performs the decorated function num_times times
- class appian_locust.utilities.loadDriverUtils.loadDriverUtils
Bases:
object
- load_config(config_file: str = './config.json') dict
Load a json configuration file into a dictionary :param config_file: Location where config file can be found
- Returns (dict): Dictionary containing configuration. Will also be stored in
loadDriverUtils.c
- appian_locust.utilities.logger.getLogger(name: str | None = None) Logger
- Parameters:
name (str, optional) – Name of the logger. it is common practice to use file name here but it can be anything.
Returns: logger object
Note
By contributing to this Open Source project, you provide Appian Corporation a non-exclusive, perpetual, royalty-free license to use your contribution for any purpose.
Contributing
Fork the appian-locust repository
Make any desired changes
Commit changes and push to your fork
Make a merge request to the upstream fork and project maintainers will review it
New Development
As new development is done to Appian Locust, the core principle of user navigation and resulting interaction should be kept in mind.
Is your feature adding interaction capabilities to an existing type of page? If so, you likely want to add a new method to an
existing SailUiForm
type. Otherwise, you might need to create a new extention of SailUiForm
and ensure that the Visitor
class has the capabilities to visit the new page type. Lastly, functionality that doesn’t involve user interaction should be included in the
SiteHelper
class.
If you think that your development falls outside of the above criteria, you should submit an issue for the maintainers of this project to discuss your use case.
To Test Your Changes
In any test-implementation repo where you use appian-locust, change the following (assuming you’re using a Pipfile
)
appian-locust = {path="../appian-locust", editable=true}
NOTE The path above assumes appian-locust is checked out locally, hence we can use a relative directory path.
And run pipenv install --skip-lock
to allow you to use a local version of appian-locust
without recreating the lock file. However, remember to use a lock file in your test-implementation repo.
Now you can test your changes as you normally would.
Internal Classes
Apart from our exposed API, we provide internal classes for more granular control when developing or testing.
- appian_locust._locust_error_handler._format_http_error(resp: Response, uri: str, username: str) str
- Taken from Response.raise_for_status. Formats the http error message,
additionally adding the username
- Parameters:
resp (Response) – Response to generate an http error message from
uri (str) – URI accessed as part of the request
username (str) – Username of the calling user
- Returns:
the http error message to use
- Return type:
str
- appian_locust._locust_error_handler.test_response_for_error(resp: ResponseContextManager, uri: str = 'No URI Specified', raise_error: bool = True, username: str = '', name: str = '') None
Locust relies on errors to be logged to the global_stats attribute for error handling. This function is used to notify Locust that its instances are failing and that it should fail too.
- Parameters:
resp (Response) – a python response object from a client.get() or client.post() call in Locust tests.
uri (Str) – URI in the request that caused the above response.
username (Str) – identifies the current user when we use multiple different users for locust test)
- Returns:
None
Example (Returns a HTTP 500 error):
username = 'admin' uri = 'https://httpbin.org/status/500' with self.client.get(uri) as resp: test_response_for_error(resp, uri, username)
- class appian_locust._save_request_builder._SaveRequestBuilder
Bases:
object
Builds a save request, that can be used to trigger saves on the UI
- build() Dict[str, Any]
- component(component: Dict[str, Any]) _SaveRequestBuilder
- context(context: Dict[str, Any]) _SaveRequestBuilder
- identifier(identifier: Dict[str, Any] | None) _SaveRequestBuilder
- uuid(uuid: str) _SaveRequestBuilder
- value(value: dict | list) _SaveRequestBuilder
- appian_locust._save_request_builder.save_builder() _SaveRequestBuilder