Skip to content

Commit

Permalink
replace pylint+flake8 with ruff (#282)
Browse files Browse the repository at this point in the history
* replace flake8 + pylint with ruff (WIP)

* fix coding according to ruff rules (WIP)

* more files updated according to ruff rules

* more files updated according to ruff rules

can be some rewrites to make it easier to follow the rules.

* ruff'ed up grizzly.testdata.variables

* ruff'ed up grizzly.tasks

* ruff'ed up grizzly.steps

* fixed servicebus client bug

ruff'ed up some additional files related to troubleshooting this.

* ruff'ed up grizzly* packages, and example

* removed all pylint disable comments

* ruff'ed up tests.* (WIP)

* ruff'ed up remaining tests

* handle exception message directly in pytest.raises

* replace pylint+flake8 with ruff

* fixed code quality errors

* fixed documentation

added anchors to all grizzly.steps modules.

updated readme, was abit out-of-date. new badges for editor support.

fixed grizzly_extras.novella postprocessing bug, where newly generated "code signature" was added above the step header.
  • Loading branch information
mgor authored Nov 21, 2023
1 parent b2101ed commit 8a44416
Show file tree
Hide file tree
Showing 242 changed files with 9,352 additions and 9,092 deletions.
8 changes: 3 additions & 5 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@
"settings": {
"python.defaultInterpreterPath": "/usr/local/bin/python",
"python.pythonPath": "/usr/local/bin/python",
"pylint.path": ["pylint"],
"flake8.path": ["flake8"],
"mypy-type-checker.path": ["mypy"],
"python.languageServer": "Pylance",
"pythonTestExplorer.testFramework": "pytest",
Expand All @@ -48,8 +46,6 @@
"extensions": [
"ms-python.python",
"ms-python.vscode-pylance",
"ms-python.pylint",
"ms-python.flake8",
"ms-python.mypy-type-checker",
"editorconfig.editorconfig",
"eamodio.gitlens",
Expand All @@ -60,7 +56,9 @@
"silverbulleters.gherkin-autocomplete",
"matangover.mypy",
"redhat.vscode-yaml",
"ms-vscode.live-server"
"ms-vscode.live-server",
"charliermarsh.ruff",
"oderwat.indent-rainbow"
]
}
},
Expand Down
3 changes: 3 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,6 @@ indent_size = 2

[*.feature]
indent_size = 2

[*.md]
trim_trailing_whitespace = false
10 changes: 3 additions & 7 deletions .github/workflows/code-quality.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,14 @@ jobs:
id: pip
run: python -m pip install -e .[dev,docs]

- name: run pylint
id: pylint
run: python -m pylint --jobs=0 --fail-under=10 grizzly/ grizzly_extras/ tests/ example/
- name: run ruff
id: ruff
run: python -m ruff grizzly/ grizzly_extras/ tests/ example/

- name: run mypy
id: mypy
run: python -m mypy grizzly/ grizzly_extras/ tests/ example/

- name: run flake8
id: flake8
run: python -m flake8

- name: gevent poison check
id: gevent-poison
run: |
Expand Down
74 changes: 31 additions & 43 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,27 +1,38 @@
# Grizzly - `/ˈɡɹɪzli/`

Framework:
<img align="right" src="https://raw.githubusercontent.com/Biometria-se/grizzly/main/docs/content/assets/logo/grizzly_grasshopper_brown_256px.png" alt="grizzly logo">
<span>
###### Framework

![PyPI - License](https://img.shields.io/pypi/l/grizzly-loadtester?style=for-the-badge)
![PyPI](https://img.shields.io/pypi/v/grizzly-loadtester?style=for-the-badge)
![PyPI - Python Version](https://img.shields.io/pypi/pyversions/grizzly-loadtester?style=for-the-badge)
![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/biometria-se/grizzly/code-quality.yaml?branch=main&style=for-the-badge)

Command Line Interface:
###### Command Line Interface

![PyPI - License](https://img.shields.io/pypi/l/grizzly-loadtester-cli?style=for-the-badge)
![PyPI](https://img.shields.io/pypi/v/grizzly-loadtester-cli?style=for-the-badge)
![PyPI - Python Version](https://img.shields.io/pypi/pyversions/grizzly-loadtester-cli?style=for-the-badge)
![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/biometria-se/grizzly-cli/code-quality.yaml?branch=main&style=for-the-badge)

![grizzly logo](https://raw.githubusercontent.com/Biometria-se/grizzly/main/docs/content/assets/logo/grizzly_grasshopper_brown_256px.png)
###### Editor Support / Language Server
![PyPI - License](https://img.shields.io/pypi/l/grizzly-loadtester-ls?style=for-the-badge)
![PyPI](https://img.shields.io/pypi/v/grizzly-loadtester-ls?style=for-the-badge)
![PyPI - Python Version](https://img.shields.io/pypi/pyversions/grizzly-loadtester-ls?style=for-the-badge)

Grizzly is a framework to be able to easily define load scenarios, and is mainly built on-top of two other frameworks:
###### Editor Support / Visual Studio Code Extension
![GitHub License](https://img.shields.io/github/license/Biometria-se/grizzly-lsp?style=for-the-badge)
![Visual Studio Marketplace Version (including pre-releases)](https://img.shields.io/visual-studio-marketplace/v/biometria-se.grizzly-loadtester-vscode?style=for-the-badge)
![Visual Studio Marketplace Release Date](https://img.shields.io/visual-studio-marketplace/release-date/biometria-se.grizzly-loadtester-vscode?style=for-the-badge)
</span>

**Grizzly is a framework to be able to easily define load scenarios, and is primarily built on-top of two other frameworks.**

> [Locust](https://locust.io): Define user behaviour with Python code, and swarm your system with millions of simultaneous users.
> [Behave](https://behave.readthedocs.io/): Uses tests written in a natural language style, backed up by Python code.
**`behave` is <del>ab</del>used for being able to define `locust` load test scenarios using [gherkin](https://cucumber.io/docs/gherkin). A feature can contain more than one scenario and all scenarios will run in parallell. This makes it possible to implement load test scenarios without knowing python or how to use `locust`.**

[Locust](https://en.wikipedia.org/wiki/Locust) are a group of certain species of short-horned grasshoppers in the family Arcididae that have a swarming phase.

The name grizzly was chosen based on the grasshopper [Melanoplus punctulatus](https://en.wikipedia.org/wiki/Melanoplus_punctulatus), also known as __grizzly__ spur-throat grasshopper. This species [prefers living in trees](https://www.sciencedaily.com/releases/2005/07/050718234418.htm) over grass, which is a hint to [Biometria](https://www.biometria.se/)<sup>1</sup>, where `grizzly` originally was created.
Expand All @@ -32,38 +43,6 @@ The name grizzly was chosen based on the grasshopper [Melanoplus punctulatus](ht

More detailed documentation can be found [here](https://biometria-se.github.io/grizzly) and the easiest way to get started is to check out the [example](https://biometria-se.github.io/grizzly/example/).

## Description

`behave` is <del>ab</del>used for being able to define `locust` load test scenarios using [gherkin](https://cucumber.io/docs/gherkin). A feature can contain more than one scenario and all scenarios will run in parallell.

```gherkin
Feature: Rest API endpoint testing
Background: Common properties for all scenarios
Given "2" users
And spawn rate is "2" user per second
And stop on first failure
Scenario: Authorize
Given a user of type "RestApi" sending requests to "https://api.example.com"
And repeat for "2" iterations
And wait time inbetween requests is random between "0.1" and "0.3" seconds
And value for variable "AtomicDate.called" is "now | format='%Y-%m-%dT%H:%M:%S.00Z', timezone=UTC"
And value for variable "callback_endpoint" is "none"
Then post request with name "authorize" from endpoint "/api/v1/authorize?called={{ AtomicDate.called }} | content_type=json"
"""
{
"username": "test",
"password": "password123",
"callback": "/api/v1/user/test"
}
"""
Then save response payload "$.callback" in variable "callback_endpoint"
Then get request with name "user info" from endpoint "{{ callback_endpoint }} | content_type=json"
When response payload "$.user.name" is not "Test User" stop user
```

This makes it possible to implement load test scenarios without knowing python or how to use `locust`.

## Features

Expand All @@ -73,7 +52,7 @@ A number of features that we thought `locust` was missing out-of-the-box has bee

Support for synchronous handling of test data (variables). This is extra important when running `locust` distributed and there is a need for each worker and user to have unique test data, that cannot be re-used.

The solution is heavily inspired by [Karol Brejnas locust experiments - feeding the locust](https://medium.com/locust-io-experiments/locust-experiments-feeding-the-locusts-cf09e0f65897). A producer is running on the master (or local) node and keeps track of what has been sent to the consumer running on a worker (or local) node. The two communicates over a seperate [ZeroMQ](https://zeromq.org) session.
The solution is heavily inspired by [Karol Brejnas locust experiments - feeding the locust](https://medium.com/locust-io-experiments/locust-experiments-feeding-the-locusts-cf09e0f65897). A producer is running on the master (or local) node and keeps track of what has been sent to the consumer running on a worker (or local) node. The two communicates over a dedicated [ZeroMQ](https://zeromq.org) connection.

When the consumer wants new test data, it sends a message to the server that it is available and for which scenario it is going to run. The producer then responds with unique test data that can be used.

Expand All @@ -91,11 +70,9 @@ They are useful when history of test runs is needed, or when wanting to correlat
* `ServiceBusUser`: send to and receive from Azure Service Bus queues and topics
* `MessageQueueUser`: send and receive from IBM MQ queues
* `SftpUser`: send and receive files from an SFTP-server
* `BlobStorageUser`: send files to Azure Blob Storage<sup>2</sup>
* `BlobStorageUser`: send and receive files to Azure Blob Storage
* `IotHubUser`: send/put files to Azure IoT Hub

<sup>2</sup> A pull request for functionality in the other direction is appreciated!

### Request log

All failed requests are logged to a file which includes both header and body, both for request and response.
Expand All @@ -117,4 +94,15 @@ grizzly-cli init my-grizzly-project

The easiest way to start contributing to this project is to have [Visual Studio Code](https://code.visualstudio.com/) (with "Remote - Containers" extension) and [docker](https://www.docker.com/) installed. The project comes with a `devcontainer`, which encapsulates everything needed for a development environment.

It is also possible to use a python virtual environment where `requirements-dev.txt` is installed.
It is also possible to use a python virtual environment, but then you would have to manually download and install IBM MQ libraries, and install `grizzly` dependencies.

```bash
sudo mkdir /opt/mqm && cd /opt/mqm && wget https://ibm.biz/IBM-MQC-Redist-LinuxX64targz -O - | tar xzf -
export LD_LIBRARY_PATH="/opt/mqm/lib64:${LD_LIBRARY_PATH}"
cd ~/
git clone https://github.com/Biometria-se/grizzly.git
cd grizzly/
python -m venv .venv
source .venv/bin/activate
python -m pip install -e .[dev,ci,mq,docs]
```
4 changes: 2 additions & 2 deletions docs/content/framework/usage/steps/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
title: Steps
---
@anchor pydoc:grizzly.steps Steps
This package contains all step implementations needed to write a feature file that describes a `locust` load test scenario for `grizzly`.
This module contains all step implementations needed to write a feature file that describes a `locust` load test scenario for `grizzly`.

A feature is described by using [Gherkin](https://cucumber.io/docs/gherkin/reference/). These expressions is then used by `grizzly` to configure and
start `locust`, which takes care of generating the load.
Expand All @@ -21,7 +21,7 @@ Feature: description of the test

In this package there are modules with step implementations that can be used in both `Background` and `Scenario` sections in a feature file.

@anchor pydoc:grizzly.steps.custom
@anchor pydoc:grizzly.steps.custom Custom steps
## Custom

Custom steps are implemented in your `grizzly` project `features/steps/steps.py` file. This is also the file that imports all `grizzly`-defined step implementations.
Expand Down
8 changes: 0 additions & 8 deletions docs/diff.py

This file was deleted.

21 changes: 11 additions & 10 deletions example/features/environment.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
# noqa: INP001, D100
# pyright: reportMissingImports=false
from typing import Any, Dict, Tuple, cast
from typing import Any, cast

from behave.runner import Context
from behave.model import Scenario
from behave.runner import Context

# pylint: disable=unused-import
from grizzly.behave import ( # noqa: F401
before_feature,
after_feature,
before_scenario as grizzly_before_scenario,
after_scenario,
before_step,
after_step,
before_feature,
before_step,
)
from grizzly.behave import (
before_scenario as grizzly_before_scenario,
)
from grizzly.context import GrizzlyContext
from grizzly.exceptions import StopUser
Expand All @@ -24,11 +26,10 @@ def touppercase(value: str) -> str:


def before_scenario(
context: Context, scenario: Scenario, *args: Tuple[Any, ...], **kwargs: Dict[str, Any]
context: Context, scenario: Scenario, *args: Any, **kwargs: Any,
) -> None:
"""
Overloads before_scenario from grizzly, to set "stop_on_failure" for all scenarios.
This would be the same as having the following step for all Scenario in a Feature:
"""Overload before_scenario from grizzly.
To set "stop_on_failure" for all scenarios. This would be the same as having the following step for all Scenario in a Feature.
```gherkin
And stop on first on first failure
Expand Down
38 changes: 21 additions & 17 deletions example/features/steps/custom.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
import logging
# noqa: D100, INP001
from __future__ import annotations

import logging
from json import loads as jsonloads
from typing import Any, Dict
from typing import TYPE_CHECKING, Any, Dict

from grizzly.scenarios import GrizzlyScenario
from grizzly.users import RestApiUser
from grizzly.tasks import RequestTask, GrizzlyTask, grizzlytask
from grizzly.types import GrizzlyResponse
from grizzly.types.locust import Message, Environment, WorkerRunner, LocalRunner
from grizzly.tasks import GrizzlyTask, RequestTask, grizzlytask
from grizzly.testdata.variables import AtomicVariable
from grizzly.types.locust import Environment, LocalRunner, Message, WorkerRunner
from grizzly.users import RestApiUser

if TYPE_CHECKING: # pragma: no cover
from grizzly.scenarios import GrizzlyScenario
from grizzly.types import GrizzlyResponse


class User(RestApiUser):
def request_impl(self, request: RequestTask) -> GrizzlyResponse:
self.logger.info(f'executing custom.User.request for {request.name} and {request.endpoint}')
self.logger.info('executing custom.User.request for %s and %s', request.name, request.endpoint)

return super().request_impl(request)

Expand All @@ -32,29 +36,29 @@ def implementation(parent: GrizzlyScenario) -> Any:
self.logger.info('sending "server_client" from SERVER')
parent.grizzly.state.locust.send_message('server_client', self.data)

if isinstance(parent.grizzly.state.locust, (WorkerRunner, LocalRunner,)) and self.data.get('client', None) is not None:
if isinstance(parent.grizzly.state.locust, (WorkerRunner, LocalRunner)) and self.data.get('client', None) is not None:
self.logger.info('sending "client_server" from CLIENT')
parent.grizzly.state.locust.send_message('client_server', self.data)

@implementation.on_start
def on_start(parent: GrizzlyScenario) -> None:
self.logger.info(f'{self.__class__.__name__} on_start called before test')
def on_start(_parent: GrizzlyScenario) -> None:
self.logger.info('%s on_start called before test', self.__class__.__name__)

@implementation.on_stop
def on_stop(parent: GrizzlyScenario) -> None:
self.logger.info(f'{self.__class__.__name__} on_stop called after test')
def on_stop(_parent: GrizzlyScenario) -> None:
self.logger.info('%s on_stop called after test', self.__class__.__name__)

return implementation


def callback_server_client(environment: Environment, msg: Message, **kwargs: Dict[str, Any]) -> None:
def callback_server_client(environment: Environment, msg: Message, **_kwargs: Any) -> None: # noqa: ARG001
import logging
logging.info(f'received from SERVER: {msg.node_id=}, {msg.data=}')
logging.info('received from SERVER: msg.node_id=%r, msg.data=%r', msg.node_id, msg.data)


def callback_client_server(environment: Environment, msg: Message) -> None:
def callback_client_server(environment: Environment, msg: Message) -> None: # noqa: ARG001
import logging
logging.info(f'received from CLIENT: {msg.node_id=}, {msg.data=}')
logging.info('received from CLIENT: msg.node_id=%r, msg.data=%r', msg.node_id, msg.data)


class AtomicCustomVariable(AtomicVariable[str]):
Expand Down
28 changes: 18 additions & 10 deletions example/features/steps/steps.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
from typing import cast
# noqa: D100, INP001
from __future__ import annotations

from behave.runner import Context
from behave import given, then # pylint: disable=no-name-in-module
from typing import TYPE_CHECKING, cast

from behave import given, then
from custom import Task

from grizzly.steps import * # pylint: disable=unused-wildcard-import # noqa: F401
from grizzly.context import GrizzlyContext
from grizzly.utils import merge_dicts
from grizzly.steps import *
from grizzly.testdata.utils import create_context_variable
from grizzly.utils import merge_dicts

from custom import Task # pylint: disable=import-error
if TYPE_CHECKING: # pragma: no cover
from behave.runner import Context


@given(u'also log successful requests')
@given('also log successful requests')
def step_log_all_requests(context: Context) -> None:
"""This step does the same as:
"""Step to explicit enable request logging.
This step does the same as:
```gherkin
And set context variable "log_all_requests" to "True"
Expand All @@ -30,9 +36,11 @@ def step_log_all_requests(context: Context) -> None:
grizzly.scenario.context = merge_dicts(grizzly.scenario.context, context_variable)


@then(u'send message "{data}"')
@then('send message "{data}"')
def step_send_message(context: Context, data: str) -> None:
"""This step adds task steps.custom.Task to the scenario, which sends a message of type
"""Step to send a message.
This step adds task steps.custom.Task to the scenario, which sends a message of type
"example_message" from the server to the client, which will trigger the callback registered
in `before_feature` in the projects `environment.py`.
"""
Expand Down
4 changes: 3 additions & 1 deletion grizzly/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
"""Any import from a grizzly module should intialize version (grizzly and locust) variables."""
from gevent import monkey

monkey.patch_all()

from importlib.metadata import version, PackageNotFoundError
from importlib.metadata import PackageNotFoundError, version

from .__version__ import __version__

try:
Expand Down
Loading

0 comments on commit 8a44416

Please sign in to comment.