From 962472c77a6942c1ed293161aabfa6d2bfc71c9c Mon Sep 17 00:00:00 2001 From: Laurent Franceschetti Date: Mon, 30 Sep 2024 08:37:38 +0200 Subject: [PATCH] Split test framework into DocProject (general) and MacrosDocProject - issue #244 --- .github/workflows/greetings.yml | 2 +- setup.py | 2 +- test/fixture.py | 305 +++++++++++++++++++------------- test/module/test_site.py | 6 +- test/null/test_site.py | 6 +- test/opt_in/test_site.py | 4 +- test/opt_out/test_site.py | 4 +- test/simple/test_site.py | 6 +- test/test_fixture.py | 50 +++++- 9 files changed, 243 insertions(+), 142 deletions(-) diff --git a/.github/workflows/greetings.yml b/.github/workflows/greetings.yml index 0f48bc1..fdd6614 100644 --- a/.github/workflows/greetings.yml +++ b/.github/workflows/greetings.yml @@ -10,4 +10,4 @@ jobs: with: repo-token: ${{ secrets.GITHUB_TOKEN }} issue-message: 'Welcome to this project and thank you!' - pr-message: 'Thank you for submitting a PR, this is appreciated. Please do not forget to submit a corresponding issue, and to reference its number in the PR'' + pr-message: 'Thank you for submitting a PR, this is appreciated. If not already done, please do not forget to submit a corresponding issue, and to reference its number in the PR' diff --git a/setup.py b/setup.py index fda81c2..7be12ce 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ # Initialization # -------------------- -VERSION_NUMBER = '1.3.1' +VERSION_NUMBER = '1.3.2' # required if you want to run document/test # pip install 'mkdocs-macros-plugin[test]' diff --git a/test/fixture.py b/test/fixture.py index 495d185..4283283 100644 --- a/test/fixture.py +++ b/test/fixture.py @@ -61,11 +61,7 @@ def list_doc_projects(directory:str): "The default docs directory" DOCS_DEFAULT_DIRNAME = 'docs' -"The directory containing the macros rendered" -RENDERED_MACROS_DIRNAME = '__docs_macros_rendered' -"The error string" -MACRO_ERROR_STRING = '# _Macro Rendering Error_' # --------------------------- # Log parsing @@ -150,7 +146,7 @@ def parse_log(mkdocs_log: str) -> list[LogEntry]: return [SuperDict(item) for item in log_entries] # --------------------------- -# Target file +# An Mkdocs Documentation project # --------------------------- @dataclass class MarkdownPage(object): @@ -229,69 +225,12 @@ def find(self, pattern: str, header=header, header_level=header_level) -@dataclass -class TestMarkdownPage(MarkdownPage): - "A subclass of markdown page, for MkDocs-Macros purposes" - - "The source markdown page (before the rendering of macros)" - source_page: MarkdownPage = field(init=False) - - "The source doc dir (normally the docs dir)" - source_doc_dir: str = DOCS_DEFAULT_DIRNAME - - # "Difference of the source" - # diff_markdown: str = field(init=False) - - - def __post_init__(self): - "Additional actions after the rest" - super().__post_init__() - self.source_page = MarkdownPage(self.filename, - project_dir=self.project_dir, - doc_dir=self.source_doc_dir) - # this should be the case, always, or something is wrong - assert self.filename == self.source_page.filename - assert self.metadata == self.source_page.metadata - - - @property - def has_error(self) -> bool: - "Checks whether there is an error" - return self.markdown.startswith(MACRO_ERROR_STRING) - - @property - def is_rendered(self) -> bool: - """ - "Rendered" means that the target markdown - is different from the source; - more accurately, that the source markdown is not - contained in the target markdown. - - Hence "not rendered" is a "nothing happened". - It covers these cases: - 1. An order to render was given, but there where actually - NO jinja2 directives. - 2. A jinja2 rendering has not taken place at all - (some order to exclude the page). - 3. A header and/or footer were added (in `on_pre_page_macros() - or in `on_post_page_macro()`) but the text itself - was not modified. - """ - # make sure that the source is stripped, to be sure. - return self.source_page.markdown.strip() not in self.markdown - - def __repr__(self): - """ - Important for error printout - """ - return f"Markdown page ({self.filename}):\n{self.text}" - -# --------------------------- -# Main class -# --------------------------- class DocProject(object): - "An object that describes the current MkDocs project being tested." + """ + An object that describes the current MkDocs project being tested + (any plugin). + """ def __init__(self, directory:str=''): "Initialize" @@ -337,17 +276,24 @@ def config(self) -> SuperDict: return self._config - @property - def target_doc_dir(self): + def source_doc_dir(self): "The target directory of markdown files (rendered macros)" return os.path.join(REF_DIR, self.project_dir, - RENDERED_MACROS_DIRNAME) + DOCS_DEFAULT_DIRNAME) + + + + + # ---------------------------------- + # Build + # ---------------------------------- + def build(self, strict:bool=False, verbose:bool=True) -> subprocess.CompletedProcess: @@ -493,6 +439,173 @@ def find_entry(self, title:str='', else: return None + # ---------------------------------- + # Get the Markdown pages + # ---------------------------------- + + + @property + def pages(self) -> List[MarkdownPage]: + "The list of Markdown pages + the HTML produced by the build (TBD)" + try: + return self._pages + except AttributeError: + # Make the list and + full_project_dir = os.path.join(REF_DIR, self.project_dir) + full_doc_dir = os.path.join(REF_DIR, self.source_doc_dir) + self._pages = [MarkdownPage(el, + project_dir = full_project_dir, + doc_dir=DOCS_DEFAULT_DIRNAME + ) + for el in list_markdown_files(full_doc_dir)] + return self._pages + + def get_page(self, name:str): + """ + Find a name in the list of Markdown pages (filenames) + using a name (full or partial, with or without extension). + """ + # get all the filenames of pages: + filenames = [page.filename for page in self.pages] + # get the filename we want, from that list: + filename = find_page(name, filenames) + # return the corresponding page: + for page in self.pages: + if page.filename == filename: + return page + + + def get_plugin(self, name:str) -> SuperDict: + "Get the plugin by its plugin name" + for el in self.config.plugins: + if name in el: + if isinstance(el, str): + return SuperDict() + elif isinstance(el, dict): + plugin = el[name] + return SuperDict(plugin) + else: + raise ValueError(f"Unexpected content of plugin {name}!") + return SuperDict(self.config.plugins.get(name)) + + + +# --------------------------- +# The Macros Doc project +# --------------------------- + +"The directory containing the macros rendered" +RENDERED_MACROS_DIRNAME = '__docs_macros_rendered' + +"The error string" +MACRO_ERROR_STRING = '# _Macro Rendering Error_' + +@dataclass +class TestMarkdownPage(MarkdownPage): + "A subclass of markdown page, for MkDocs-Macros purposes" + + "The source markdown page (before the rendering of macros)" + source_page: MarkdownPage = field(init=False) + + "The source doc dir (normally the docs dir)" + source_doc_dir: str = DOCS_DEFAULT_DIRNAME + + # "Difference of the source" + # diff_markdown: str = field(init=False) + + + def __post_init__(self): + "Additional actions after the rest" + super().__post_init__() + self.source_page = MarkdownPage(self.filename, + project_dir=self.project_dir, + doc_dir=self.source_doc_dir) + # this should be the case, always, or something is wrong + assert self.filename == self.source_page.filename + assert self.metadata == self.source_page.metadata + + + @property + def has_error(self) -> bool: + "Checks whether there is an error" + return self.markdown.startswith(MACRO_ERROR_STRING) + + @property + def is_rendered(self) -> bool: + """ + "Rendered" means that the target markdown + is different from the source; + more accurately, that the source markdown is not + contained in the target markdown. + + Hence "not rendered" is a "nothing happened". + It covers these cases: + 1. An order to render was given, but there where actually + NO jinja2 directives. + 2. A jinja2 rendering has not taken place at all + (some order to exclude the page). + 3. A header and/or footer were added (in `on_pre_page_macros() + or in `on_post_page_macro()`) but the text itself + was not modified. + """ + # make sure that the source is stripped, to be sure. + return self.source_page.markdown.strip() not in self.markdown + + + def __repr__(self): + """ + Important for error printout + """ + return f"Markdown page ({self.filename}):\n{self.text}" + + + + +class MacrosDocProject(DocProject): + """ + An object that describes the current MkDocs-Macros project + being tested. + + The difference is that it relies heavily on the Markdown + pages rendered by Jinja2. These are produced at the end of + the `on_page_markdown()` method of the plugin. + + The pages() property has thus been redefined. + """ + + + @property + def target_doc_dir(self): + "The target directory of markdown files (rendered macros)" + return os.path.join(REF_DIR, + self.project_dir, + RENDERED_MACROS_DIRNAME) + + @property + def pages(self) -> List[TestMarkdownPage]: + """ + The list of Markdown pages produced by the build. + It must be called after the build. + """ + try: + return self._pages + except AttributeError: + # Make the list and + full_project_dir = os.path.join(REF_DIR, self.project_dir) + full_target_dir = os.path.join(REF_DIR, self.target_doc_dir) + self._pages = [TestMarkdownPage(el, + project_dir = full_project_dir, + doc_dir=RENDERED_MACROS_DIRNAME, + source_doc_dir=DOCS_DEFAULT_DIRNAME + ) + for el in list_markdown_files(full_target_dir)] + return self._pages + + @property + def macros_plugin(self) -> SuperDict: + "Return the plugin config" + return self.get_plugin('macros') + # ---------------------------------- # Smart properties (from log, etc.) @@ -545,56 +658,4 @@ def filters(self) -> SuperDict: """ entry = self.find_entry('config filters', severity='debug') if entry and entry.payload: - return SuperDict(json.loads(entry.payload)) - - @property - def pages(self) -> List[TestMarkdownPage]: - "The list of Markdown pages produced by the build" - try: - return self._pages - except AttributeError: - # Make the list and - full_project_dir = os.path.join(REF_DIR, self.project_dir) - full_target_dir = os.path.join(REF_DIR, self.target_doc_dir) - self._pages = [TestMarkdownPage(el, - project_dir = full_project_dir, - doc_dir=RENDERED_MACROS_DIRNAME, - source_doc_dir=DOCS_DEFAULT_DIRNAME - ) - for el in list_markdown_files(full_target_dir)] - return self._pages - - def get_page(self, name:str): - """ - Find a name in the list of Markdown pages (filenames) - using a name (full or partial, with or without extension). - """ - # get all the filenames of pages: - filenames = [page.filename for page in self.pages] - # get the filename we want, from that list: - filename = find_page(name, filenames) - # return the corresponding page: - for page in self.pages: - if page.filename == filename: - return page - - - def get_plugin(self, name:str) -> SuperDict: - "Get the plugin by its plugin name" - for el in self.config.plugins: - if name in el: - if isinstance(el, str): - return SuperDict() - elif isinstance(el, dict): - plugin = el[name] - return SuperDict(plugin) - else: - raise ValueError(f"Unexpected content of plugin {name}!") - return SuperDict(self.config.plugins.get(name)) - - @property - def macros_plugin(self) -> SuperDict: - "Return the plugin config" - return self.get_plugin('macros') - - + return SuperDict(json.loads(entry.payload)) \ No newline at end of file diff --git a/test/module/test_site.py b/test/module/test_site.py index ab15b21..dadffc0 100644 --- a/test/module/test_site.py +++ b/test/module/test_site.py @@ -8,14 +8,14 @@ import pytest import re -from test.fixture import DocProject, find_after +from test.fixture import MacrosDocProject, find_after CURRENT_PROJECT = 'module' def test_pages(): - PROJECT = DocProject(CURRENT_PROJECT) + PROJECT = MacrosDocProject(CURRENT_PROJECT) PROJECT.build() # did not fail assert not PROJECT.build_result.returncode @@ -107,7 +107,7 @@ def test_pages(): def test_strict(): "This project must fail" - PROJECT = DocProject(CURRENT_PROJECT) + PROJECT = MacrosDocProject(CURRENT_PROJECT) # it must fail with the --strict option, # because the second page contains an error diff --git a/test/null/test_site.py b/test/null/test_site.py index afa26c6..3739135 100644 --- a/test/null/test_site.py +++ b/test/null/test_site.py @@ -7,14 +7,14 @@ import pytest -from test.fixture import DocProject +from test.fixture import MacrosDocProject CURRENT_PROJECT = 'null' def test_pages(): - PROJECT = DocProject(CURRENT_PROJECT) + PROJECT = MacrosDocProject(CURRENT_PROJECT) build_result = PROJECT.build(strict=False) # did not fail return_code = PROJECT.build_result.returncode @@ -44,7 +44,7 @@ def test_pages(): def test_strict(): "This project must fail" - PROJECT = DocProject(CURRENT_PROJECT) + PROJECT = MacrosDocProject(CURRENT_PROJECT) # it must not fail with the --strict option, PROJECT.build(strict=True) diff --git a/test/opt_in/test_site.py b/test/opt_in/test_site.py index d74e123..7ada1df 100644 --- a/test/opt_in/test_site.py +++ b/test/opt_in/test_site.py @@ -5,7 +5,7 @@ """ import pytest -from test.fixture import DocProject +from test.fixture import MacrosDocProject CURRENT_PROJECT = 'opt_in' @@ -13,7 +13,7 @@ def test_opt_in(): - PROJECT = DocProject(CURRENT_PROJECT) + PROJECT = MacrosDocProject(CURRENT_PROJECT) PROJECT.build() # did not fail assert not PROJECT.build_result.returncode diff --git a/test/opt_out/test_site.py b/test/opt_out/test_site.py index 70ca469..8fa494c 100644 --- a/test/opt_out/test_site.py +++ b/test/opt_out/test_site.py @@ -5,7 +5,7 @@ """ import pytest -from test.fixture import DocProject +from test.fixture import MacrosDocProject CURRENT_PROJECT = 'opt_out' @@ -13,7 +13,7 @@ def test_opt_in(): - PROJECT = DocProject(CURRENT_PROJECT) + PROJECT = MacrosDocProject(CURRENT_PROJECT) PROJECT.build() # did not fail assert not PROJECT.build_result.returncode diff --git a/test/simple/test_site.py b/test/simple/test_site.py index 33d023b..f1a702d 100644 --- a/test/simple/test_site.py +++ b/test/simple/test_site.py @@ -7,14 +7,14 @@ import pytest -from test.fixture import DocProject +from test.fixture import MacrosDocProject CURRENT_PROJECT = 'simple' def test_pages(): - PROJECT = DocProject(CURRENT_PROJECT) + PROJECT = MacrosDocProject(CURRENT_PROJECT) build_result = PROJECT.build(strict=False) # did not fail return_code = PROJECT.build_result.returncode @@ -47,7 +47,7 @@ def test_pages(): def test_strict(): "This project must fail" - PROJECT = DocProject(CURRENT_PROJECT) + PROJECT = MacrosDocProject(CURRENT_PROJECT) # it must fail with the --strict option, # because the second page contains an error diff --git a/test/test_fixture.py b/test/test_fixture.py index b45ec92..e994b34 100644 --- a/test/test_fixture.py +++ b/test/test_fixture.py @@ -7,7 +7,8 @@ import os -from .fixture import PROJECTS, DocProject, REF_DIR, parse_log +from .fixture import (PROJECTS, DocProject, MacrosDocProject, + REF_DIR, parse_log) from .fixture_util import ( h1, h2, h3, std_print, get_tables, list_markdown_files, find_in_html, find_page) @@ -137,9 +138,48 @@ def test_find_pages(): +def test_doc_project(): + """ + Test a project + """ + MYPROJECT = 'null' + # MYPROJECT = 'simple' + h1(f"TESTING MKDOCS PROJECT ({MYPROJECT})") + + h2("Config") + myproject = DocProject(MYPROJECT) + config = myproject.config + print(config) + + + + h2("Build") + result = myproject.build() + assert result == myproject.build_result + + h2("Log") + assert myproject.trace == result.stderr + std_print(myproject.trace) + + + + + h2("Filtering the log by severity") + infos = myproject.find_entries(severity='INFO') + print(f"There are {len(infos)} info items.") + print('\n'.join(f" {i} - {item.title}" for i, item in enumerate(infos))) -def test_high_level_fixtures(): + h2("Page objects") + for page in myproject.pages: + h3(f"PAGE: {page.filename}") + print("- Main title:", page.h1) + print("- Filename:", page.filename) + print("- Markdown(start):", page.markdown[:50]) + + + +def test_macros_doc_project(): """ Test a project """ @@ -148,7 +188,7 @@ def test_high_level_fixtures(): h1(f"TESTING MKDOCS-MACROS PROJECT ({MYPROJECT})") h2("Config") - myproject = DocProject(MYPROJECT) + myproject = MacrosDocProject(MYPROJECT) config = myproject.config print(config) @@ -236,11 +276,11 @@ def test_high_level_fixtures(): help='Test low-level fixtures only') def command_line(short:bool): LOW_LEVEL = [test_functions, test_find_pages] - HIGH_LEVEL = [test_high_level_fixtures] + HIGH_LEVEL = [test_doc_project, test_macros_doc_project] if short: TESTS = LOW_LEVEL else: - TESTS = HIGH_LEVEL + HIGH_LEVEL + TESTS = LOW_LEVEL + HIGH_LEVEL # run: for test in TESTS: test()