Dec 07, 2017 | Kashish Munjal
Authoring a language runner for Gauge
Note: This article was first posted on Medium.
In this blog, I will try to explain the purpose of a language plugin/runner and how to write one of your own.
Gauge has a modular architecture with plugin support. There are three types of plugins in Gauge
- Language
- IDE
- Reporting
What is a Language Runner?
A Language Runner is a plugin which lets you write test code implementations in a specific programming language.
There are two phases in the life cycle of a language runner:
1. Initialization
- Initialize the project with the language-specific skeleton file.
2. Execution
- Execute hooks (when requested by Gauge core).
- Execute method corresponding to a step and send execution response back to Gauge.
- Send custom report messages to Gauge.
- Read/write to datastore (currently, datastore is intra-process, hence parallel processes cannot use a shared datastore).
This post will help you write a basic Python language runner which supports initialization and step execution. Let’s call our runner “mypython”, so that our tutorial code does not conflict with Gauge’s Python language runner. By the end of this article, will be able to use it through Gauge to create a new project and run specifications, like this:
gauge --init mypython
gauge specs
How to write a language runner
Requirements
For writing a language runner, you will need:
- Gauge installed and available on the PATH.
- Installation of the programming language that you writing the language runner in (Python in this case).
- A version control system. Preferably, git.
- A text editor or IDE of your choice.
- Familiarity with the programming language that you writing the language runner in.
Create your plugin files/folders
Create a directory called “mypython” and create these files and sub-directories in it:
mypython | |--- mypython.json | |--- README.md | |--- step_impl/ | |--- __init__.py | |--- step_impl.py | |--- getgauge/ | |--- __init__.py | |--- messages/ | |--- __init__.py | |--- start.py
We recommend you use a version control system, preferably git, so that we can track changes and add dependencies from other repositories as we go ahead.
Runner metadata file
Every language runner needs to have a <runner_id>.json
file, which contains the metadata for the language runner. For an example, see the mypython.json
file in the mypython language runner.
<runner_name>.json
file serves the following three primary functions:
- Plugin name, description, version are specified in this file.
- The properties “init” and “run” in this file are used to specify initialize and run commands for different platforms. For example: while initializing a Gauge project, Gauge will execute the command present in init section of the JSON file.
- Minimum and maximum Gauge version support are specified here.
README.md
README.md
file in the root of this project contains the user and technical documentation, although this is not a requirement.
For an example, see README.md from the JavaScript and Python language runner.
Skeleton files
Each language runner should come with a sample implementation file that can be used as a quick-start for creating a new project.
Put the following content to step_impl/step_impl.py
.
from getgauge.python import step
vowels = ["a", "e", "i", "o", "u"]
def number_of_vowels(word):
return len(filter(lambda elem: elem in vowels, list(word)))
@step("The word <word> has <number> vowels.")
def assert_no_of_vowels_in(word, number):
assert str(number) == str(number_of_vowels(word))
@step("Vowels in English language are <vowels>.")
def assert_default_vowels(given_vowels):
assert given_vowels == "".join(vowels)
@step("Almost all words have vowels <table>")
def assert_words_vowel_count(table):
assert 1 == 1
Initializing project
When you run this command, Gauge will ask the language runner to copy implementation specific skeleton files in the project directory. Gauge will run the command which is present in the init section in mypython.json
file
Put the following content in the start.py
and provide execute permission to the file.
chmod +x start.py
This will copy the skeleton files to the user’s project directory which is present in an environment variable GAUGE_PROJECT_ROOT
.
import os
import shutil
import sys
from getgauge import connection, processor
PROJECT_ROOT_ENV = 'GAUGE_PROJECT_ROOT'
STEP_IMPL_DIR = "step_impl"
project_root = os.environ[PROJECT_ROOT_ENV]
impl_dir = os.path.join(project_root, STEP_IMPL_DIR)
def main():
if sys.argv[1] == "--init":
print("Initialising Gauge Python project")
print("create {}".format(impl_dir))
shutil.copytree(STEP_IMPL_DIR, impl_dir)
else:
s = connection.connect()
__import__("step_impl.step_impl")
processor.dispatch_messages(s)
if __name__ == '__main__':
main()
Protocol Buffers
Communication between Gauge and Language Runner happens via TCP using Protocol Buffers.
To communicate with Gauge, there are different request/response messages are configured in the gauge-proto repository. Let’s add the gauge-proto repository as a git submodule to our language runner.
git submodule add https://github.com/getgauge/gauge-proto.git
Create getgauge/messages
getgauge/messages directory. In this directory, we will put the python code which is generated from the
proto ``` files present in the submodule.
cd gauge-proto
protoc --python_out=../getgauge/messages/ spec.proto
protoc --python_out=../getgauge/messages/ messages.proto
cd ../
Execution
Let’s define a step function which will be used as a decorator in the step implementation. Create getgauge/python.py
with the following function definition.
from getgauge.registry import registry
def step(step_text):
def _step(func):
# Storing function in registry, so that it can be called when Gauge requests
registry.add_step_definition(step_text, func)
return func
return _step
The above decorator will be used to define step implementation in the following way.
@step("The word <word> has <number> vowels.")
def assert_no_of_vowels_in(word, number):
assert str(number) == str(number_of_vowels(word))
see registry.py for more information on storing the implementation and step text.
Gauge opens up a port for protobuf communication and runner connects to the socket by reading the environment variable GAUGE_INTERNAL_PORT
.
Now the language runner needs to send responses to the requests sent by Gauge. To read the request and send the response, language runner needs to use socket. (See connection.py for socket communication code). The detailed documentation for every request/response is explained here.
Copy registry.py and connection.py to getgauge directory.
Create getgauge/processor.py
and put the following code in it. This will wait for requests and sends respective responses till kill request is received.
import os
import sys
import traceback
import time
from connection import read_message, send_message
from getgauge.registry import registry
from messages.messages_pb2 import Message, StepValidateResponse
from messages.spec_pb2 import ProtoExecutionResult
PROJECT_ROOT_ENV = 'GAUGE_PROJECT_ROOT'
STEP_IMPL_DIR = "step_impl"
project_root = os.environ[PROJECT_ROOT_ENV]
impl_dir = os.path.join(project_root, STEP_IMPL_DIR)
def _current_time(): return int(round(time.time() * 1000))
processors = {Message.ExecutionStarting: set_response_values,
Message.ExecutionEnding: set_response_values,
Message.SpecExecutionStarting: set_response_values,
Message.SpecExecutionEnding: set_response_values,
Message.ScenarioExecutionStarting: set_response_values,
Message.ScenarioExecutionEnding: set_response_values,
Message.StepExecutionStarting: set_response_values,
Message.StepExecutionEnding: set_response_values,
Message.ExecuteStep: _execute_step,
Message.StepValidateRequest: _validate_step,
Message.StepNamesRequest: set_response_values,
Message.ScenarioDataStoreInit: set_response_values,
Message.SpecDataStoreInit: set_response_values,
Message.SuiteDataStoreInit: set_response_values,
Message.StepNameRequest: set_response_values,
Message.RefactorRequest: set_response_values,
Message.KillProcessRequest: _kill_runner,
}
def dispatch_messages(socket):
sys.path.append(impl_dir)
map(__import__, ['step_impl'])
while True:
request = read_message(socket)
response = Message()
processors[request.messageType](request, response, socket)
send_message(response, request, socket)
Before executing steps, Gauge sends a request to check if the implementation for the given steps are present or not. _validate_step
will check in the registry, it will set is_valid
to True
if present otherwise False
.
def _validate_step(req, res, socket):
res.messageType = Message.StepValidateResponse
res.stepValidateResponse.isValid = registry.is_step_implemented(req.stepValidateRequest.stepText)
if res.stepValidateResponse.isValid is False:
res.stepValidateResponse.errorType = StepValidateResponse.STEP_IMPLEMENTATION_NOT_FOUND
After validating steps, Gauge will send requests to execute steps, a request will contain parameters and implementation function is called with those parameters. It will send the response back with the result(failed, error, stack trace, type, execution time).
def _execute_step(req, res, socket):
params = []
for param in req.executeStepRequest.parameters:
params.append(param.value)
set_response_values(req, res)
execute_method(params, registry.get_info(req.executeStepRequest.parsedStepText).impl, res)
def set_response_values(request, response, s=None):
response.messageType = Message.ExecutionStatusResponse
response.executionStatusResponse.executionResult.failed = False
response.executionStatusResponse.executionResult.executionTime = 0
def execute_method(params, func, response):
start = _current_time()
try:
func(*params)
except Exception as e:
_add_exception(e, response)
response.executionStatusResponse.executionResult.executionTime = _current_time() - start
def _add_exception(e, response):
response.executionStatusResponse.executionResult.failed = True
response.executionStatusResponse.executionResult.errorMessage = e.__str__()
response.executionStatusResponse.executionResult.stackTrace = traceback.format_exc()
response.executionStatusResponse.executionResult.errorType = ProtoExecutionResult.ASSERTION
After execution is complete Gauge will send a kill request to language runner and runner needs to close the socket and end the program.
def _kill_runner(req, res, socket):
socket.close()
sys.exit()
Install & Test
There are two ways to install the plugin
-
Copy the content of your project to Gauge plugins path i.e
%APPDATA%\gauge\plugins\mypython\0.0.1
for Windows and$HOME/.gauge/plugins/mypython/0.0.1
for Mac and Linux. -
Create a zip file of your project and install it using
gauge --install mypython -f <path to zip file>
.
The output of gauge -v
will contain mypython plugin with version 0.0.1
.
After this, you should be able to initialize and execute a Python Gauge project.
gauge --init mypython
gauge specs
Next Steps
In this article, you have seen how to write a basic language runner for Gauge and start executing steps. A language runner can do a lot more. Take a look at the feature matrix of language runners for an overall idea of the current state of language runners for Gauge.
Gauge is a free and open source test automation framework that takes the pain out of acceptance testing. Download it or read documentation to get started!
–