-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #26 from S3-Platform-Inc/feature/25-base-and-paylo…
…ad-tests Resolved #25 Feature/25 base and payload tests
- Loading branch information
Showing
10 changed files
with
407 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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} пока не тестируются" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.