Skip to content

Commit

Permalink
Add workflow for performing tests on pushes and PRs (#88)
Browse files Browse the repository at this point in the history
This PR adds a `tox.ini` file to the repository. It exposes a
few different test environments which can be used by the developer
for:

* Linting the code
* Checking Python modules dependencies
* Running unit tests with coverage report
* Running a static type checker

Instructions on how to use `tox` have been added to the `README.md`
file.

These facilities exposed by the implemented `tox.ini` file are used
by the `tests.yml` workflow which is also introduced by this PR. It is
responsible for running all the tests which are exposed by `tox.ini`

As part of this PR we also applied a few fixes which were reported by
the workflow runs:

* Issues reported by `flake8` in `setup.py`
* Issues reported by `pyright` in `server_operation.py`, `utils.py`,
  and `utility_controller.py`
* Issues on unit test `test_main_helper`: it was failing because of a
  difference between the terminal size of runners and our laptops
* Issues on unit test `test_server_operation_post_not_json` when
  ran through GitHub Actions with Python 3.7 + Flask 2.2.5. That
  combination caused Flask to return `400 Bad Request` instead of
  `415 Unsupported Media Type`, which is returned by other
  combinations

References: BAR-133.
  • Loading branch information
barthisrael authored Nov 24, 2023
1 parent c1d3286 commit 455f783
Show file tree
Hide file tree
Showing 11 changed files with 367 additions and 30 deletions.
135 changes: 135 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
name: Tests

on:
pull_request:

push:
branches:
- main

workflow_dispatch:
inputs:
source-ref:
description: Source code branch/ref name
default: main
required: true
type: string

env:
SOURCE_REF: ${{ inputs.source-ref || github.ref }}
WORKING_DIRECTORY: ./pg_backup_api

jobs:
lint:
runs-on: ubuntu-latest

steps:
- name: Checkout the source code
uses: actions/checkout@v3
with:
ref: ${{ env.SOURCE_REF }}

- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: 3.x

- name: Install tox
run:
pip install tox

- name: Run linter
working-directory: ${{ env.WORKING_DIRECTORY }}
run:
tox -e lint

dependency_checking:
runs-on: ubuntu-latest

steps:
- name: Checkout the source code
uses: actions/checkout@v3
with:
ref: ${{ env.SOURCE_REF }}

- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: 3.x

- name: Install tox
run:
pip install tox

- name: Run dependency checker
working-directory: ${{ env.WORKING_DIRECTORY }}
run:
tox -e dep

unit_tests:
runs-on: ubuntu-latest

strategy:
fail-fast: false

matrix:
python-version:
- '3.7'
- '3.8'
- '3.9'
- '3.10'
- '3.11'

steps:
- name: Checkout the source code
uses: actions/checkout@v3
with:
ref: ${{ env.SOURCE_REF }}

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}

- name: Install tox
run:
pip install tox

- name: Run unit tests
working-directory: ${{ env.WORKING_DIRECTORY }}
run:
tox -m test

static_type_checking:
runs-on: ubuntu-latest

strategy:
fail-fast: false

matrix:
python-version:
- '3.7'
- '3.8'
- '3.9'
- '3.10'
- '3.11'

steps:
- name: Checkout the source code
uses: actions/checkout@v3
with:
ref: ${{ env.SOURCE_REF }}

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}

- name: Install tox
run:
pip install tox

- name: Run static type checks
working-directory: ${{ env.WORKING_DIRECTORY }}
run:
tox -m type
57 changes: 53 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,12 +117,61 @@ The command returns `"OK"` if the app is up and running.

## Testing

You can run unit tests through `pytest`:
The repository contains a `tox.ini` file which declares a set of test
environments that are available.

In the following sub-sections you will find more information about how to
manually run each of the tests. All of them assume you are inside the
`pg_backup_api` folder which can be found in the repository root directory.

**Note:** install `tox` Python module if you don't have it yet in your
environment.

### Lint

You can run the `flake8` linter over the code by running this command:

```bash
cd pg-backup-api/pg_backup_api
python3 -m pytest
tox -e lint
```

**Note:** install `pytest` Python module if you don't have it yet in your
It will check the source code, tests, and `setup.py`.

### Dependency checking

You can run the dependency checker `pipdeptree` by running this command:

```bash
tox -e dep
```

It will print the tree of Python modules used by `pg-backup-api`, which can be
helpful in solving conflicts..

### Unit tests

You can run unit tests by running this command:

```bash
tox -m test
```

It will run unit tests using `pytest` module and `pytest-cov` plugin for
coverage report.

**Note:** the command will take care of running the tests using all Python
versions which are supported by `pg-backup-api` and that are available in your
environment.

### Static type checking

You can run the static type checker `pyright` over the source code by running
this command:

```bash
tox -m type
```

**Note:** the command will take care of running the static type checker using
all Python versions which are supported by `pg-backup-api` and that are
available in your environment.
11 changes: 10 additions & 1 deletion pg_backup_api/pg_backup_api/logic/utility_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@

if TYPE_CHECKING: # pragma: no cover
from flask import Request, Response
from barman.config import Config as BarmanConfig
from pg_backup_api.server_operation import Operation


@app.route("/diagnose", methods=["GET"])
Expand All @@ -53,11 +55,15 @@ def diagnose() -> 'Response':
"""
# Reload the barman config so that any changes are picked up
load_barman_config()

if TYPE_CHECKING: # pragma: no cover
assert isinstance(barman.__config__, BarmanConfig)

# Get every server (both inactive and temporarily disabled)
servers = barman.__config__.server_names()

server_dict = {}
for server in servers:
for server in servers: # pyright: ignore
conf = barman.__config__.get_server(server)
if conf is None:
# Unknown server
Expand Down Expand Up @@ -214,6 +220,9 @@ def servers_operations_post(server_name: str,
)
subprocess.Popen(cmd.split())

if TYPE_CHECKING: # pragma: no cover
assert isinstance(operation, Operation)

return {"operation_id": operation.id}


Expand Down
13 changes: 8 additions & 5 deletions pg_backup_api/pg_backup_api/server_operation.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
import os
import subprocess
import sys
from typing import (Any, Callable, Dict, List, Optional, Set, Tuple,
from typing import (Any, Callable, Dict, List, Optional, Set, Tuple, Union,
TYPE_CHECKING)

from datetime import datetime
Expand Down Expand Up @@ -494,7 +494,8 @@ def get_status(self) -> str:
return self.server.get_operation_status(self.id)

@staticmethod
def _run_subprocess(cmd: List[str]) -> Tuple[Optional[str], int]:
def _run_subprocess(cmd: List[str]) -> \
Tuple[Union[str, bytearray, memoryview], Union[int, Any]]:
"""
Run *cmd* as a subprocess.
Expand All @@ -512,7 +513,8 @@ def _run_subprocess(cmd: List[str]) -> Tuple[Optional[str], int]:
return stdout, process.returncode

@abstractmethod
def _run_logic(self) -> Tuple[Optional[str], int]:
def _run_logic(self) -> \
Tuple[Union[str, bytearray, memoryview], Union[int, Any]]:
"""
Logic to be ran when executing the operation.
Expand All @@ -526,7 +528,7 @@ def _run_logic(self) -> Tuple[Optional[str], int]:
"""
pass

def run(self) -> Tuple[Optional[str], int]:
def run(self) -> Tuple[Union[str, bytearray, memoryview], Union[int, Any]]:
"""
Run the operation.
Expand Down Expand Up @@ -621,7 +623,8 @@ def _get_args(self) -> List[str]:
remote_ssh_command,
]

def _run_logic(self) -> Tuple[Optional[str], int]:
def _run_logic(self) -> \
Tuple[Union[str, bytearray, memoryview], Union[int, Any]]:
"""
Logic to be ran when executing the recovery operation.
Expand Down
32 changes: 23 additions & 9 deletions pg_backup_api/pg_backup_api/tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
# along with Postgres Backup API. If not, see <http://www.gnu.org/licenses/>.

"""Unit tests for the CLI."""
import sys
from textwrap import dedent
from unittest.mock import MagicMock, patch

Expand All @@ -32,7 +33,7 @@
positional arguments:
{serve,status,recovery}
options:
optional arguments:
-h, --help show this help message and exit
Postgres Backup API by EnterpriseDB (www.enterprisedb.com)
Expand All @@ -41,9 +42,10 @@
"pg-backup-api serve --help": dedent("""\
usage: pg-backup-api serve [-h] [--port PORT]
Start the REST API server. Listen for requests on '127.0.0.1', on the given port.
Start the REST API server. Listen for requests on '127.0.0.1', on the given
port.
options:
optional arguments:
-h, --help show this help message and exit
--port PORT Port to bind to.
\
Expand All @@ -53,17 +55,19 @@
Check if the REST API server is up and running
options:
optional arguments:
-h, --help show this help message and exit
--port PORT Port to be checked.
\
"""), # noqa: E501
"pg-backup-api recovery --help": dedent("""\
usage: pg-backup-api recovery [-h] --server-name SERVER_NAME --operation-id OPERATION_ID
usage: pg-backup-api recovery [-h] --server-name SERVER_NAME --operation-id
OPERATION_ID
Perform a 'barman recover' through the 'pg-backup-api'. Can only be run if a recover operation has been previously registered.
Perform a 'barman recover' through the 'pg-backup-api'. Can only be run if a
recover operation has been previously registered.
options:
optional arguments:
-h, --help show this help message and exit
--server-name SERVER_NAME
Name of the Barman server to be recovered.
Expand All @@ -81,17 +85,27 @@


@pytest.mark.parametrize("command", _HELP_OUTPUT.keys())
def test_main_helper(command, capsys):
@patch("shutil.get_terminal_size")
def test_main_helper(mock_term_size, command, capsys):
"""Test :func:`main`.
Ensure all the ``--help`` calls print the expected content to the console.
"""
# Get a predictable print size
mock_term_size.return_value.columns = 80

with patch("sys.argv", command.split()), pytest.raises(SystemExit) as exc:
main()

assert str(exc.value) == "0"

assert capsys.readouterr().out == _HELP_OUTPUT[command]
expected = _HELP_OUTPUT[command]
version = sys.version_info

if version.major >= 3 and version.minor >= 10:
expected = expected.replace("optional arguments:", "options:")

assert capsys.readouterr().out == expected


@pytest.mark.parametrize("command", _COMMAND_FUNC.keys())
Expand Down
Loading

0 comments on commit 455f783

Please sign in to comment.