Authoring a Language Runner for Gauge | Gauge Blog

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

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

2. Execution

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:

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:

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 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 has vowels.") def assert_no_of_vowels_in(word, number): assert str(number) == str(number_of_vowels(word))


see [registry.py](https://github.com/kashishm/gauge-runner/blob/master/getgauge/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](https://github.com/kashishm/gauge-runner/blob/master/getgauge/connection.py) for socket communication code). The detailed documentation for every request/response is explained [here](https://getgauge.github.io/documentation/technical/current/language_plugin_api.html).

Copy [registry.py](https://github.com/kashishm/gauge-runner/blob/master/getgauge/registry.py) and [connection.py](https://github.com/kashishm/gauge-runner/blob/master/getgauge/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

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](https://github.com/getgauge/gauge/wiki/Language-Runners) of language runners for an overall idea of the current state of language runners for Gauge.

_[Gauge](/content/site-root.html) is a free and open source test automation framework that takes the pain out of acceptance testing. [Download](/content/get_started/index.html) it or read [documentation](https://docs.gauge.org/) to get started!_