Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add test framework using pytest and playwright #1172

Draft
wants to merge 8 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 58 additions & 54 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,58 +2,62 @@
// https://github.com/microsoft/vscode-dev-containers/tree/v0.203.0/containers/docker-existing-docker-compose
// If you want to run as a non-root user in the container, see .devcontainer/docker-compose.yml.
{
"name": "MyRadio + Postgres, Memcached, Mailhog",

// Update the 'dockerComposeFile' list if you have more compose files or use different names.
// The .devcontainer/docker-compose.yml file contains any overrides you need/want to make.
"dockerComposeFile": [
"../docker-compose.yml",
"docker-compose.yml"
],

"service": "myradio",

// The optional 'workspaceFolder' property is the path VS Code should open by default when
// connected. This is typically a file mount in .devcontainer/docker-compose.yml
"workspaceFolder": "/var/www/myradio",

// Set *default* container specific settings.json values on container create.
"settings": {
"sqltools.connections": [{
"name": "Container database",
"driver": "PostgreSQL",
"server": "postgres",
"previewLimit": 50,
"port": 5432,
"database": "myradio",
"username": "myradio",
"password": "myradio"
}]
},

// Add the IDs of extensions you want installed when the container is created.
"extensions": [
"bmewburn.vscode-intelephense-client",
"felixfbecker.php-debug",
"mtxr.sqltools",
"mtxr.sqltools-driver-pg",
"whatwedo.twig"
],

// Use 'forwardPorts' to make a list of ports inside the container available locally.
"forwardPorts": [7080, 8025],

// "initializeCommand": ""

// Uncomment the next line if you want start specific services in your Docker Compose config.
// "runServices": [],

// Uncomment the next line if you want to keep your containers running after VS Code shuts down.
// "shutdownAction": "none",

// Uncomment the next line to run commands after the container is created - for example installing curl.
"postCreateCommand": "COMPOSER_VENDOR_DIR=/workspaces/MyRadio/src/vendor composer install && apachectl restart",

// Uncomment to connect as a non-root user if you've added one. See https://aka.ms/vscode-remote/containers/non-root.
// "remoteUser": "vscode"
"name": "MyRadio + Postgres, Memcached, Mailhog",

// Update the 'dockerComposeFile' list if you have more compose files or use different names.
// The .devcontainer/docker-compose.yml file contains any overrides you need/want to make.
"dockerComposeFile": ["../docker-compose.yml", "docker-compose.yml"],

"service": "myradio",

// The optional 'workspaceFolder' property is the path VS Code should open by default when
// connected. This is typically a file mount in .devcontainer/docker-compose.yml
"workspaceFolder": "/var/www/myradio",

"customizations": {
"vscode": {
// Set *default* container specific settings.json values on container create.
"settings": {
"sqltools.connections": [
{
"name": "Container database",
"driver": "PostgreSQL",
"server": "postgres",
"previewLimit": 50,
"port": 5432,
"database": "myradio",
"username": "myradio",
"password": "myradio"
}
]
},

// Add the IDs of extensions you want installed when the container is created.
"extensions": [
"bmewburn.vscode-intelephense-client",
"felixfbecker.php-debug",
"mtxr.sqltools",
"mtxr.sqltools-driver-pg",
"whatwedo.twig",
"xdebug.php-debug"
]
}
},

// Use 'forwardPorts' to make a list of ports inside the container available locally.
"forwardPorts": [7080, 8025],

// "initializeCommand": ""

// Uncomment the next line if you want start specific services in your Docker Compose config.
// "runServices": [],

// Uncomment the next line if you want to keep your containers running after VS Code shuts down.
// "shutdownAction": "none",

// Uncomment the next line to run commands after the container is created - for example installing curl.
"postCreateCommand": "COMPOSER_VENDOR_DIR=/workspaces/MyRadio/src/vendor composer install && apachectl restart"

// Uncomment to connect as a non-root user if you've added one. See https://aka.ms/vscode-remote/containers/non-root.
// "remoteUser": "vscode"
}
4 changes: 3 additions & 1 deletion .devcontainer/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ services:
# user: vscode

init: true
environment:
E2E_TEST: 'true'

volumes:
# Update this to wherever you want VS Code to mount the folder of your project
- .:/workspaces/MyRadio:cached
- .:/var/www/myradio:cached
- ./sample_configs/codespaces-server-name.conf:/etc/apache2/conf-enabled/server-name.conf
- ./sample_configs/codespaces-apache.conf:/etc/apache2/sites-available/myradio.conf
21 changes: 21 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: Tests

on:
push:
branches:
- master
pull_request:
branches:
- master

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build MyRadio and set up test environment
run: |
docker compose -f docker-compose.yml -f docker-compose.test.yml up -d
- name: Run tests
run: |
docker compose exec myradio poetry run py.test
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,7 @@ src/vendor
composer.lock
node_modules/
src/Public/img/stats_training_*.svg
.idea/
.idea/
__pycache__/
*.pyc
*.pyo
13 changes: 11 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ FROM php:8.2-apache

RUN apt-get update && apt-get install -y libpq-dev libpng-dev libjpeg-dev libldap-dev unzip \
libcurl4-openssl-dev libxslt-dev git libz-dev libzip-dev libmemcached-dev \
postgresql-client jq msmtp-mta ffmpeg
postgresql-client python3-dev jq msmtp-mta ffmpeg

RUN docker-php-ext-install pgsql pdo_pgsql gd ldap curl xsl zip
RUN docker-php-ext-install pgsql gd ldap curl xsl zip

RUN pecl install memcached && \
echo extension=memcached.so >> /usr/local/etc/php/conf.d/memcached.ini
Expand Down Expand Up @@ -52,4 +52,13 @@ COPY src src
COPY sample_configs/docker-config.php src/MyRadio_Config.local.php
RUN chown www-data:www-data /var/www/myradio/src/MyRadio_Config.local.php && chmod 664 /var/www/myradio/src/MyRadio_Config.local.php

# Testing requirements
COPY pyproject.toml poetry.lock ./
RUN curl -sSL https://install.python-poetry.org | python3 -
ENV PATH="${PATH}:/root/.local/bin"
RUN poetry config virtualenvs.in-project true && \
poetry install

COPY *.py ./

CMD ["apache2-foreground"]
2 changes: 1 addition & 1 deletion Dockerfile.prod
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ RUN apt-get update && apt-get install -y libpq-dev libpng-dev libjpeg-dev liblda
libcurl4-openssl-dev libxslt-dev git libz-dev libzip-dev libmemcached-dev \
postgresql-client jq msmtp-mta ffmpeg

RUN docker-php-ext-install pgsql pdo_pgsql gd ldap curl xsl zip
RUN docker-php-ext-install pgsql gd ldap curl xsl zip

RUN pecl install memcached && \
echo extension=memcached.so >> /usr/local/etc/php/conf.d/memcached.ini
Expand Down
10 changes: 0 additions & 10 deletions codeception.yml

This file was deleted.

4 changes: 1 addition & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,14 @@
"spatie/icalendar-generator": "^2.1",
"simshaun/recurr": "^4.0",
"geoip2/geoip2": "^2.10.0",
"spatie/icalendar-generator": "^2.1",
"ext-pgsql": "*",
"rbdwllr/reallysimplejwt": "4.0.3"
"rbdwllr/reallysimplejwt": "4.0.3"
},
"require-dev": {
"squizlabs/php_codesniffer": "~3.5",
"pdepend/pdepend": "~2.5",
"phpmd/phpmd": "~2.6",
"theseer/phpdox": "~0.11",
"codeception/codeception": "~4.2",
"flow/jsonpath": "~0.4"
},
"config": {
Expand Down
147 changes: 147 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
from pathlib import Path
from typing import Dict
import pytest
import subprocess
import psycopg2
import hashlib
import json
from playwright.sync_api import Playwright, Browser # type: ignore


def _get_myradio_config(field: str):
return json.loads(
subprocess.check_output(
[
"php",
"-r",
f'require "src/Controllers/root_cli.php"; echo json_encode(\\MyRadio\\Config::${field});',
]
).decode("utf-8")
)


# We want each test to run in an isolated environment, otherwise tests may randomly fail
# depending on the order that they ran.
# We do this by creating a new PostgreSQL database for each test.
# To speed it up, we first set up a template database, and then make a copy of it
# for each test.
# Database.php respects the X-MyRadio-Database header in E2E test mode (env var E2E_TEST == "true").

@pytest.fixture(autouse=True, scope="session")
def db_conn():
# Get the config details
host = _get_myradio_config("db_hostname")
user = _get_myradio_config("db_user")
password = _get_myradio_config("db_pass")

db = psycopg2.connect(
f"host={host} dbname=postgres user={user} password={password}"
)
# Create myradio database and seed
db.autocommit = True
cursor = db.cursor()
cursor.execute("DROP DATABASE IF EXISTS myradio_test")
cursor.execute("CREATE DATABASE myradio_test")
cursor.execute("DROP USER IF EXISTS myradio_test")
cursor.execute("CREATE USER myradio_test WITH PASSWORD 'myradio_test'")
cursor.execute("GRANT ALL PRIVILEGES ON DATABASE myradio_test TO myradio_test")
cursor.close()
db.close()
db = psycopg2.connect(
f"host={host} dbname=myradio_test user={user} password={password}"
)
db.autocommit = True

schema_dir = Path(__file__).parent / "schema"
sample_configs_dir = Path(__file__).parent / "sample_configs"
for file in [
schema_dir / "base.sql",
*sorted(filter(lambda f: f.suffix == '.sql', (schema_dir / "patches").iterdir()), key=lambda patch: int(patch.stem)),
sample_configs_dir / "test-auth.sql",
]:
with file.open() as f:
cur = db.cursor()
cur.execute("SET search_path = 'public'")
cur.execute(f.read())
db.commit()
cur.close()

yield db

db.close()
db = psycopg2.connect(
f"host={host} dbname=postgres user={user} password={password}"
)
db.autocommit = True
cursor = db.cursor()
cursor.execute("DROP DATABASE myradio_test")
cursor.execute("DROP USER myradio_test")
cursor.close()
db.close()


def _make_pool_db(conn: psycopg2.extensions.connection, name: str):
cur = conn.cursor()
cur.execute(f"DROP DATABASE IF EXISTS {name}")
cur.execute(f"CREATE DATABASE {name} TEMPLATE myradio_test")
cur.close()
return name


def _release_pool_db(conn: psycopg2.extensions.connection, name: str):
cur = conn.cursor()
cur.execute(f"DROP DATABASE {name}")
cur.close()


phase_report_key = pytest.StashKey[Dict[str, pytest.CollectReport]]()
@pytest.hookimpl(wrapper=True, tryfirst=True)
def pytest_runtest_makereport(item, call):
# execute all other hooks to obtain the report object
rep = yield

# store test results for each phase of a call, which can
# be "setup", "call", "teardown"
item.stash.setdefault(phase_report_key, {})[rep.when] = rep

return rep


@pytest.fixture(autouse=True, scope="function")
def myradio_database(request: pytest.FixtureRequest, db_conn):
name = request.node.name
name_hash = hashlib.sha256(name.encode()).hexdigest()[:8]
db_name = f"myradio_test_{name_hash}"
_make_pool_db(db_conn, db_name)
yield db_name
report = request.node.stash[phase_report_key]
if "call" not in report or report["call"].failed:
print(f"Test {name} failed, keeping database {db_name}")
else:
_release_pool_db(db_conn, db_name)


INSIDE_MYRADIO_CONTAINER = Path("/etc/apache2/sites-available/myradio.conf").is_file()


@pytest.fixture()
def api_v2(myradio_database, playwright: Playwright):
ctx = playwright.request.new_context(
base_url=f"http://localhost:{80 if INSIDE_MYRADIO_CONTAINER else 7080}/api/v2/",
extra_http_headers={
"X-MyRadio-Database": myradio_database,
"APIKey": "test-key", # schema/test-auth.sql
},
)
yield ctx
ctx.dispose()


@pytest.fixture()
def browser(myradio_database, browser: Browser):
ctx = browser.new_context(
base_url=f"http://localhost:{80 if INSIDE_MYRADIO_CONTAINER else 7080}/myradio",
extra_http_headers={"X-MyRadio-Database": myradio_database},
)
yield ctx
ctx.close()
6 changes: 6 additions & 0 deletions docker-compose.test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
version: '3'
services:
myradio:
environment:
- E2E_TEST=true
volumes: []
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ services:
volumes:
- ./schema:/var/www/myradio/schema
- ./src:/var/www/myradio/src:rw
- ./tests:/var/www/myradio/tests:rw
# daemon:
# build: .
# depends_on:
Expand Down
Loading
Loading