Skip to content

Commit

Permalink
Merge pull request #26 from S3-Platform-Inc/feature/25-base-and-paylo…
Browse files Browse the repository at this point in the history
…ad-tests

Resolved #25 Feature/25 base and payload tests
  • Loading branch information
CuberHuber authored Nov 13, 2024
2 parents c834bef + cc3779f commit 7dd29ca
Show file tree
Hide file tree
Showing 10 changed files with 407 additions and 3 deletions.
37 changes: 37 additions & 0 deletions .github/workflows/plugin_test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: Test Plugin

on:
pull_request:
branches:
- "main"
- "patch*"
- "dev"

jobs:
structure-tests:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Set up Python 3.11
uses: actions/setup-python@v4
with:
python-version: '3.11' # Specify Python version

- name: Install Poetry
run: |
curl -sSL https://install.python-poetry.org | python3 - # Install Poetry
echo "$HOME/.local/bin" >> $GITHUB_PATH # Add Poetry to PATH
- name: Install dependencies
run: |
poetry install # Install dependencies using Poetry
- name: Run base tests
run: |
poetry run pytest -v -m pre_set
- name: Run payload tests
run: |
poetry run pytest -v -m payload_set
4 changes: 4 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[pytest]
markers =
pre_set: mark test as part of the previous set
payload_set: mark test a part of the main payload set (plugin run)
29 changes: 26 additions & 3 deletions readme.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,37 @@
# S3 Platform Plugin Template


> **Note**
>
> [!INFO]
> Нажми на <kbd>Use this template</kbd> кнопку и клонируй его в IDE.
S3 Platform Plugin Template - это репозиторий предоставляет чистый шаблон для простого и быстрого создания проекта плагина (Посмотри статью [Creating a repository from a template](https://docs.github.com/en/repositories/creating-and-managing-repositories/creating-a-repository-from-a-template)).

Основная цель этого шаблона - ускорить этап установки плагина как для новичков, так и для опытных разработчиков, предварительно настроив CI проекта, указав ссылки на нужные страницы документации и сохранив все в порядке.

Если вы все еще не совсем понимаете, о чем идет речь, прочитайте наше введение: [Что такое S3 Platform?](The-S3-Platform-page)
[//]: # (Если вы все еще не совсем понимаете, о чем идет речь, прочитайте наше введение: Что такое S3 Platform?)

# Содержание
- [Требования](#требования-к-разработке-плагина)
- [Структура](#обязательная-структура)


# Требования к разработке плагина

## Обязательная структура
Репозиторий плагина состоит из основных компонентов:

```markdown
my-plugin/ # Репозиторий
├── .github/ #
│ └── workflows/ # GitHub Actions
├── src/ # Основная директория разработки
│ └── <uniq plugin name>/ # Каталог с файлами плагина.
│ ├── config.py # Конфигурация плагина
│ └── <some files>.* # Файлы плагина (его payload)
├── tests/ # Тесты для плагина
└── plugin.xml # Основной декларативный файл плагина
```
Empty file added tests/__init__.py
Empty file.
50 changes: 50 additions & 0 deletions tests/config/fixtures.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import importlib.util
import os
from pathlib import Path

import xml.etree.ElementTree as ET

import pytest
from s3p_sdk.plugin.config import PluginConfig


class Project:
name: str
version: str
root: str
plugin_path: str

def __init__(self, dir: str):
try:
tree = ET.parse(Path(dir) / 'plugin.xml')
root = tree.getroot()
project_name = root.attrib.get('name')
assert project_name is not None
self.name = project_name
version = root.find('version').text
assert version is not None
self.version = version
self.root = dir
self.plugin_path = str(Path(dir) / 'src' / self.name)
except ET.ParseError as e:
print(f"Error parsing XML: {e}")
raise Exception(f"Error parsing XML") from e
except FileNotFoundError:
print(f"File not found: {Path(dir) / 'plugin.xml'}")
raise FileNotFoundError(f"File not found {Path(dir) / 'plugin.xml'}")


@pytest.fixture(scope="module")
def project_config() -> Project:
return Project(str(Path(__file__).parent.parent.parent))


@pytest.fixture(scope="module")
def fix_plugin_config(project_config) -> PluginConfig:
"""Загружает конфигурацию из config.py файла по динамическому пути на основании конфигурации"""
config_path = Path(project_config.root) / 'src' / project_config.name / 'config.py'
assert os.path.exists(config_path)
spec = importlib.util.spec_from_file_location('s3p_plugin_config', config_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module.config
25 changes: 25 additions & 0 deletions tests/config/test_base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import os
from pathlib import Path

import pytest

from tests.config.fixtures import Project, project_config


@pytest.mark.pre_set
class TestBaseConfig:

def test_version(self, project_config: Project):
assert project_config.version.startswith("3.")

def test_plugin_structure(self, project_config: Project):
# Тест проверяет структуру проекта плагина версии 3
if str(project_config.version).startswith("3.0"):
assert os.path.exists(
Path(project_config.root) / 'src'), "проект должен иметь каталог `src` в корне проекта"
assert os.path.exists(Path(
project_config.root) / 'src' / project_config.name), f"проект должен иметь каталог `{project_config.name}` в каталоге `src`"
assert os.path.exists(Path(
project_config.root) / 'src' / project_config.name / 'config.py'), f"проект должен иметь файл `config.py` в каталоге `{project_config.name}`"
else:
assert False, f"Плагины версии {project_config.version} пока не тестируются"
112 changes: 112 additions & 0 deletions tests/config/test_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import os
from pathlib import Path

import pytest
from s3p_sdk.plugin.config.payload import EntryConfig
from s3p_sdk.plugin.config.payload.entry import AbcParamConfig
from s3p_sdk.plugin.types import PIPELINE, SOURCE, ML

from tests.config.fixtures import fix_plugin_config, project_config
from s3p_sdk.plugin.config import (
PluginConfig, CoreConfig, TaskConfig, MiddlewareConfig, PayloadConfig,
)


class PluginStructure:
PLUGIN: str = 'plugin'
TASK: str = 'task'
MIDDLEWARE: str = 'middleware'
PAYLOAD: str = 'payload'


@pytest.mark.pre_set
class TestBaseConfig:

def test_config_exists(self, fix_plugin_config):
"""Проверка на то, что файл существует"""
assert isinstance(fix_plugin_config,
PluginConfig), "`config.py` файл должен содержать переменную `config` с типом `PluginConfig`"

def test_config_structure(self, fix_plugin_config):
"""Проверка базовой структуры плагина"""
assert isinstance(fix_plugin_config.__dict__.get(PluginStructure.PLUGIN),
CoreConfig), "`PluginConfig` должен содержать переменную `plugin` с типом `CorePlugin`"
assert isinstance(fix_plugin_config.__dict__.get(PluginStructure.TASK),
TaskConfig), "`PluginConfig` должен содержать переменную `talk` с типом `TaskConfig`"
assert isinstance(fix_plugin_config.__dict__.get(PluginStructure.MIDDLEWARE),
MiddlewareConfig), "`PluginConfig` должен содержать переменную `middleware` с типом `MiddlewareConfig`"
assert isinstance(fix_plugin_config.__dict__.get(PluginStructure.PAYLOAD),
PayloadConfig), "`PluginConfig` должен содержать переменную `payload` с типом `PayloadConfig`"


@pytest.mark.pre_set
class TestConfigPlugin:

def test_config_plugin_structure(self, fix_plugin_config):
_cplugin = fix_plugin_config.__dict__.get(PluginStructure.PLUGIN)

assert isinstance(_cplugin.__dict__.get('reference'), str)
assert isinstance(_cplugin.__dict__.get('type'), str) and str(_cplugin.__dict__.get('type')) in (SOURCE, ML, PIPELINE)
assert isinstance(_cplugin.__dict__.get('files'), list) and all([isinstance(it, str) for it in _cplugin.__dict__.get('files')])
assert isinstance(_cplugin.__dict__.get('is_localstorage'), bool)

def test_config_plugin_files(self, fix_plugin_config, project_config):
"""Проверка наличия файлов плагина"""
_cplugin = fix_plugin_config.__dict__.get(PluginStructure.PLUGIN)
_files = _cplugin.__dict__.get('files')

for _file in _files:
file_path = Path(project_config.root) / 'src' / project_config.name / _file
assert os.path.exists(file_path)


@pytest.mark.pre_set
class TestConfigPayload:

def test_config_payload_structure(self, fix_plugin_config, project_config):
"""Провека структуры PayliadConfig"""
_cpayload = fix_plugin_config.__dict__.get(PluginStructure.PAYLOAD)

assert isinstance(_cpayload.__dict__.get('file'), str)
assert isinstance(_cpayload.__dict__.get('classname'), str)
assert isinstance(_cpayload.__dict__.get('entry'), EntryConfig)

def test_config_payload_entry_structure(self, fix_plugin_config, project_config):
"""Провека структуры PayliadConfig"""
_pentry = fix_plugin_config.__dict__.get(PluginStructure.PAYLOAD).__dict__.get('entry')

assert isinstance(_pentry.__dict__.get('method'), str)
assert isinstance(_pentry.__dict__.get('params'), list) and all([isinstance(it, AbcParamConfig) for it in _pentry.__dict__.get('params')])

def test_config_plugin_files(self, fix_plugin_config, project_config):
"""Проверка наличия файлов плагина"""
_cplugin = fix_plugin_config.__dict__.get(PluginStructure.PLUGIN)
_files = _cplugin.__dict__.get('files')

for _file in _files:
file_path = Path(project_config.root) / 'src' / project_config.name / _file
assert os.path.exists(file_path)

def test_exists_entry_file(self, fix_plugin_config, project_config):
"""Проверяет наличие файла-точки входа в плагин"""
_cpayload = fix_plugin_config.__dict__.get(PluginStructure.PAYLOAD)
_cpayload.__dict__.get('file')

entry_path = Path(project_config.root) / 'src' / project_config.name / _cpayload.__dict__.get('file')
assert os.path.exists(entry_path)

def test_right_extensions_file(self, fix_plugin_config, project_config):
"""Расширение файла-точки входа в плагин должен быть `.py`"""
_cpayload = fix_plugin_config.__dict__.get(PluginStructure.PAYLOAD)
_cpayload.__dict__.get('file')

entry_path = Path(project_config.root) / 'src' / project_config.name / _cpayload.__dict__.get('file')
assert os.path.exists(entry_path)
assert entry_path.suffix == '.py'

def test_compare_entry_file_and_plugin_files(self, fix_plugin_config):
"""Файл в параметре `payload.file` должен быть описан в `plugin.files`"""
_cpayload = fix_plugin_config.__dict__.get(PluginStructure.PAYLOAD)
_cplugin = fix_plugin_config.__dict__.get(PluginStructure.PLUGIN)

assert _cpayload.__dict__.get('file') in _cplugin.__dict__.get('files')
20 changes: 20 additions & 0 deletions tests/payload/fixtures.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import signal


def handler(signum, frame):
raise TimeoutError("Test took too long to execute")


def execute_timeout(seconds):
def decorator(func):
def wrapper(*args, **kwargs):
signal.signal(signal.SIGALRM, handler)
signal.alarm(seconds)
try:
return func(*args, **kwargs)
finally:
signal.alarm(0) # Disable the alarm

return wrapper

return decorator
90 changes: 90 additions & 0 deletions tests/payload/test_plugin_run.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import datetime
import importlib.util
import os
from typing import Type
import sys

import pytest
from pathlib import Path

from s3p_sdk.plugin.payloads.parsers import S3PParserBase
from selenium.webdriver.chrome import webdriver
from selenium.webdriver import Chrome
from selenium.webdriver.ie.webdriver import WebDriver

from tests.config.fixtures import fix_plugin_config, project_config
from tests.payload.fixtures import execute_timeout
from s3p_sdk.types import S3PRefer, S3PDocument
from s3p_sdk.plugin.types import SOURCE


@pytest.mark.payload_set
class TestPayloadRun:

@pytest.fixture(scope="class", autouse=True)
def chrome_driver(self) -> WebDriver:
options = webdriver.Options()

options.add_argument('--headless')
options.add_argument('--no-sandbox')
options.add_argument('--disable-dev-shm-usage')
options.add_argument('window-size=1920x1080')
options.add_argument("disable-gpu")
driver = Chrome(options=options)
yield driver
driver.quit()

@pytest.fixture(scope="class")
def fix_s3pRefer(self):
return S3PRefer(1, 'test-refer', SOURCE, None)

@pytest.fixture(scope="module", autouse=True)
def fix_payload(self, project_config, fix_plugin_config) -> Type[S3PParserBase]:
MODULE_NAME: str = 's3p_test_plugin_payload'
"""Загружает конфигурацию из config.py файла по динамическому пути на основании конфигурации"""
payload_path = Path(project_config.root) / 'src' / project_config.name / fix_plugin_config.payload.file
assert os.path.exists(payload_path)
spec = importlib.util.spec_from_file_location(MODULE_NAME, payload_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
# Get the class from the module
class_name = fix_plugin_config.payload.classname
assert class_name in module.__dict__, f"Class {class_name} not found in module."

# Create an instance of the class
parser_class = module.__dict__[class_name]
assert issubclass(parser_class, S3PParserBase), f"{class_name} is not a subclass of S3PParserBase."
return parser_class

def run_payload(self, payload: Type[S3PParserBase], driver: WebDriver, refer: S3PRefer, max_document: int,
timeout: int = 2):
from src.s3_platform_plugin_template.template_payload import MyTemplateParser
if isinstance(payload, type(MyTemplateParser)):
_payload = payload(refer=refer, web_driver=driver, max_count_documents=max_document, last_document=None)

@execute_timeout(timeout)
def execute() -> tuple[S3PDocument, ...]:
return _payload.content()

return execute()
else:
assert False, "Тест проверяет payload плагина"

def test_run_with_0_docs_restriction(self, chrome_driver, fix_s3pRefer, fix_payload):
max_docs = 10
docs = self.run_payload(fix_payload, chrome_driver, fix_s3pRefer, max_docs)
assert len(docs) <= max_docs

def test_return_types(self, chrome_driver, fix_s3pRefer, fix_payload):
max_docs = 10
docs = self.run_payload(fix_payload, chrome_driver, fix_s3pRefer, max_docs)
assert isinstance(docs, tuple) and all([isinstance(el, S3PDocument) for el in docs])

def test_returned_parameters_are_sufficient(self, chrome_driver, fix_s3pRefer, fix_payload):
max_docs = 10
docs = self.run_payload(fix_payload, chrome_driver, fix_s3pRefer, max_docs)
for el in docs:
assert el.title is not None and isinstance(el.title, str)
assert el.link is not None and isinstance(el.link, str)
assert el.published is not None and isinstance(el.published, datetime.datetime)
assert el.hash
Loading

0 comments on commit 7dd29ca

Please sign in to comment.