Repository: mondeja/mkdocs-include-markdown-plugin (python)

Issue:
Log warning when failing to find `start` or `end`
Currently, if a `start` or `end` delimiter is not detected, the include tag silently handles this. This really ought to be logged via warning.

Pull request description:
Warn if `start` and `end` delimiters are not detected in content to include
Closes #92

Patch:
diff --git a/.bumpversion.cfg b/.bumpversion.cfg
index eecdd95..1344d60 100644
--- a/.bumpversion.cfg
+++ b/.bumpversion.cfg
@@ -1,5 +1,5 @@
 [bumpversion]
-current_version = 3.3.0
+current_version = 3.4.0
 
 [bumpversion:file:mkdocs_include_markdown_plugin/__init__.py]
 
diff --git a/.gitignore b/.gitignore
index b6e4761..b139e0b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,6 @@
+# Mkdocs files
+site/
+
 # Byte-compiled / optimized / DLL files
 __pycache__/
 *.py[cod]
diff --git a/examples/start-end-not-found-warnings/docs/included.md b/examples/start-end-not-found-warnings/docs/included.md
new file mode 100644
index 0000000..b8a5283
--- /dev/null
+++ b/examples/start-end-not-found-warnings/docs/included.md
@@ -0,0 +1,1 @@
+Delimiters not found, so this inclusion must raise warnings.
diff --git a/examples/start-end-not-found-warnings/docs/index.md b/examples/start-end-not-found-warnings/docs/index.md
new file mode 100644
index 0000000..c427e3c
--- /dev/null
+++ b/examples/start-end-not-found-warnings/docs/index.md
@@ -0,0 +1,14 @@
+# Home
+
+{%
+  include-markdown "./included.md"
+  start="<--start-->"
+  end="<--end-->"
+  comments=false
+%}
+
+{%
+  include "./included.md"
+  start="<--start-->"
+  end="<--end-->"
+%}
diff --git a/examples/start-end-not-found-warnings/mkdocs.yml b/examples/start-end-not-found-warnings/mkdocs.yml
new file mode 100644
index 0000000..65ba68c
--- /dev/null
+++ b/examples/start-end-not-found-warnings/mkdocs.yml
@@ -0,0 +1,3 @@
+plugins:
+  - include-markdown
+site_name: Foo
diff --git a/mkdocs_include_markdown_plugin/__init__.py b/mkdocs_include_markdown_plugin/__init__.py
index d4b5330..a8c2f79 100644
--- a/mkdocs_include_markdown_plugin/__init__.py
+++ b/mkdocs_include_markdown_plugin/__init__.py
@@ -1,2 +1,2 @@
 __title__ = 'mkdocs_include_markdown_plugin'
-__version__ = '3.3.0'
+__version__ = '3.4.0'
diff --git a/mkdocs_include_markdown_plugin/event.py b/mkdocs_include_markdown_plugin/event.py
index 2e10176..8465545 100644
--- a/mkdocs_include_markdown_plugin/event.py
+++ b/mkdocs_include_markdown_plugin/event.py
@@ -1,7 +1,8 @@
-'''Module where the `on_page_markdown` plugin event is defined.'''
+'''Module where the `on_page_markdown` plugin event is processed.'''
 
 import glob
 import html
+import logging
 import os
 import re
 import textwrap
@@ -59,8 +60,16 @@ ARGUMENT_REGEXES = {
     'exclude': re.compile(r'exclude="([^"]+)"'),
 }
 
+logger = logging.getLogger('mkdocs.plugins.mkdocs_include_markdown_plugin')
 
-def get_file_content(markdown, abs_src_path, cumulative_heading_offset=0):
+
+def get_file_content(
+    markdown,
+    abs_src_path,
+    docs_dir,
+    includer_page_path,
+    cumulative_heading_offset=0,
+):
     page_src_path = Path(abs_src_path)
 
     def found_include_tag(match):
@@ -104,7 +113,8 @@ def get_file_content(markdown, abs_src_path, cumulative_heading_offset=0):
 
         if not file_paths_to_include:
             raise FileNotFoundError(
-                f'Any files found using \'{filename}\' at {page_src_path}',
+                f'No files found including with \'{filename}\''
+                f' at {page_src_path}',
             )
 
         # handle options and regex modifiers
@@ -143,24 +153,48 @@ def get_file_content(markdown, abs_src_path, cumulative_heading_offset=0):
         end = None if not end_match else end_match.group(1)
 
         text_to_include = ''
+        expected_but_any_found = [start is not None, end is not None]
         for file_path in file_paths_to_include:
             with open(file_path, encoding='utf-8') as f:
                 new_text_to_include = f.read()
 
-            new_text_to_include, _, _ = process.filter_inclusions(
-                start,
-                end,
-                new_text_to_include,
-            )
+            if start is not None or end is not None:
+                new_text_to_include, *expected_not_found = (
+                    process.filter_inclusions(
+                        start,
+                        end,
+                        new_text_to_include,
+                    )
+                )
+                for i in range(2):
+                    if expected_but_any_found[i] and not expected_not_found[i]:
+                        expected_but_any_found[i] = False
 
             # nested includes
             new_text_to_include = get_file_content(
                 new_text_to_include,
                 file_path,
+                docs_dir,
+                Path(file_path),
             )
 
             text_to_include += new_text_to_include
 
+        # warn if expected start or ends haven't been found in included content
+        for i, argname in enumerate(['start', 'end']):
+            if expected_but_any_found[i]:
+                value = locals()[argname]
+                readable_files_to_include = ', '.join([
+                    str(Path(f).relative_to(docs_dir))
+                    for f in file_paths_to_include
+                ])
+                plural_suffix = 's' if len(file_paths_to_include) > 1 else ''
+                logger.warning(
+                    f"Delimiter {argname} '{value}' defined at"
+                    f' {includer_page_path.relative_to(docs_dir)} not detected'
+                    f' in the file{plural_suffix} {readable_files_to_include}',
+                )
+
         if bool_options['dedent']:
             text_to_include = textwrap.dedent(text_to_include)
 
@@ -217,7 +251,7 @@ def get_file_content(markdown, abs_src_path, cumulative_heading_offset=0):
 
         if not file_paths_to_include:
             raise FileNotFoundError(
-                f'Any files found using \'{filename}\' at {page_src_path}',
+                f'No files found using \'{filename}\' at {page_src_path}',
             )
 
         # handle options and regex modifiers
@@ -273,20 +307,35 @@ def get_file_content(markdown, abs_src_path, cumulative_heading_offset=0):
             offset += int(offset_match.group(1))
 
         text_to_include = ''
+
+        # if any start or end strings are found in the included content
+        # but the arguments are specified, we must raise a warning
+        #
+        # `True` means that no start/end strings have been found in content
+        # but they have been specified, so the warning(s) must be raised
+        expected_but_any_found = [start is not None, end is not None]
         for file_path in file_paths_to_include:
             with open(file_path, encoding='utf-8') as f:
                 new_text_to_include = f.read()
 
-            new_text_to_include, _, _ = process.filter_inclusions(
-                start,
-                end,
-                new_text_to_include,
-            )
+            if start is not None or end is not None:
+                new_text_to_include, *expected_not_found = (
+                    process.filter_inclusions(
+                        start,
+                        end,
+                        new_text_to_include,
+                    )
+                )
+                for i in range(2):
+                    if expected_but_any_found[i] and not expected_not_found[i]:
+                        expected_but_any_found[i] = False
 
             # nested includes
             new_text_to_include = get_file_content(
                 new_text_to_include,
                 file_path,
+                docs_dir,
+                Path(file_path),
             )
 
             # relative URLs rewriting
@@ -314,6 +363,8 @@ def get_file_content(markdown, abs_src_path, cumulative_heading_offset=0):
             new_text_to_include = get_file_content(
                 new_text_to_include,
                 file_path,
+                docs_dir,
+                Path(file_path),
                 cumulative_heading_offset=cumulative_heading_offset,
             )
 
@@ -325,13 +376,30 @@ def get_file_content(markdown, abs_src_path, cumulative_heading_offset=0):
 
             text_to_include += new_text_to_include
 
+        # warn if expected start or ends haven't been found in included content
+        for i, argname in enumerate(['start', 'end']):
+            if expected_but_any_found[i]:
+                value = locals()[argname]
+                readable_files_to_include = ', '.join([
+                    str(Path(f).relative_to(docs_dir))
+                    for f in file_paths_to_include
+                ])
+                plural_suffix = 's' if len(file_paths_to_include) > 1 else ''
+                logger.warning(
+                    f"Delimiter {argname} '{value}' defined at"
+                    f' {includer_page_path.relative_to(docs_dir)} not detected'
+                    f' in the file{plural_suffix} {readable_files_to_include}',
+                )
+
         if not bool_options['comments']['value']:
             return text_to_include
 
         return (
             _includer_indent
             + '<!-- BEGIN INCLUDE {} {} {} -->\n'.format(
-                filename, html.escape(start or ''), html.escape(end or ''),
+                filename,
+                html.escape(start or ''),
+                html.escape(end or ''),
             )
             + text_to_include
             + '\n' + _includer_indent + '<!-- END INCLUDE -->'
@@ -342,13 +410,17 @@ def get_file_content(markdown, abs_src_path, cumulative_heading_offset=0):
         found_include_tag,
         markdown,
     )
-    markdown = re.sub(
+    return re.sub(
         INCLUDE_MARKDOWN_TAG_REGEX,
         found_include_markdown_tag,
         markdown,
     )
-    return markdown
 
 
-def on_page_markdown(markdown, page, **kwargs):
-    return get_file_content(markdown, page.file.abs_src_path)
+def on_page_markdown(markdown, page, docs_dir):
+    return get_file_content(
+        markdown,
+        page.file.abs_src_path,
+        Path(docs_dir),
+        Path(page.file.abs_src_path),
+    )
diff --git a/mkdocs_include_markdown_plugin/plugin.py b/mkdocs_include_markdown_plugin/plugin.py
index 0d0daa0..397699e 100644
--- a/mkdocs_include_markdown_plugin/plugin.py
+++ b/mkdocs_include_markdown_plugin/plugin.py
@@ -7,4 +7,4 @@ from mkdocs_include_markdown_plugin.event import (
 
 class IncludeMarkdownPlugin(mkdocs.plugins.BasePlugin):
     def on_page_markdown(self, markdown, page, **kwargs):
-        return _on_page_markdown(markdown, page, **kwargs)
+        return _on_page_markdown(markdown, page, kwargs['config']['docs_dir'])
diff --git a/mkdocs_include_markdown_plugin/process.py b/mkdocs_include_markdown_plugin/process.py
index 5595354..50feb75 100644
--- a/mkdocs_include_markdown_plugin/process.py
+++ b/mkdocs_include_markdown_plugin/process.py
@@ -2,6 +2,7 @@ import os
 import re
 from functools import partial
 from pathlib import Path
+from typing import Optional, Tuple
 from urllib.parse import urlparse, urlunparse
 
 
@@ -257,35 +258,60 @@ def interpret_escapes(value: str) -> str:
     return value.encode('latin-1', 'backslashreplace').decode('unicode_escape')
 
 
-def filter_inclusions(new_start, new_end, text_to_include):
+def filter_inclusions(
+    new_start: Optional[str],
+    new_end: Optional[str],
+    text_to_include: str,
+) -> Tuple[str, bool, bool]:
     '''Manages inclusions from files using ``start`` and ``end`` directive
     arguments.
     '''
-    start = None
-    end = None
+
+    expected_not_found = [False, False]  # start, end
+
     if new_start is not None:
         start = interpret_escapes(new_start)
-        if new_end is not None:
-            end = interpret_escapes(new_end)
+        end = interpret_escapes(new_end) if new_end is not None else None
 
         new_text_to_include = ''
-        if new_end is not None:
-            for start_text in text_to_include.split(start)[1:]:
-                for i, end_text in enumerate(start_text.split(end)):
-                    if not i % 2:
-                        new_text_to_include += end_text
+
+        if end is not None:
+            end_found = False
+            start_split = text_to_include.split(start)[1:]
+            if not start_split:
+                expected_not_found[0] = True
+            else:
+                for start_text in start_split:
+                    for i, end_text in enumerate(start_text.split(end)):
+                        if not i % 2:
+                            new_text_to_include += end_text
+                            end_found = True
+            if not end_found:
+                expected_not_found[1] = True
         else:
-            new_text_to_include = text_to_include.split(start)[1]
+            if start in text_to_include:
+                new_text_to_include = text_to_include.split(
+                    start,
+                    maxsplit=1,
+                )[1]
+            else:
+                expected_not_found[0] = True
         text_to_include = new_text_to_include
 
     elif new_end is not None:
         end = interpret_escapes(new_end)
-        text_to_include, _, _ = text_to_include.partition(end)
+        if end in text_to_include:
+            text_to_include = text_to_include.split(
+                end,
+                maxsplit=1,
+            )[0]
+        else:
+            expected_not_found[1] = True
 
-    return (text_to_include, start, end)
+    return (text_to_include, *expected_not_found)
 
 
-def increase_headings_offset(markdown, offset=0):
+def increase_headings_offset(markdown: str, offset: int = 0):
     '''Increases the headings depth of a snippet of Makdown content.'''
     if not offset:
         return markdown
@@ -304,7 +330,7 @@ def increase_headings_offset(markdown, offset=0):
     )
 
 
-def filter_paths(filepaths, ignore_paths=[]):
+def filter_paths(filepaths: list, ignore_paths: list = []):
     """Filters a list of paths removing those defined in other list of paths.
 
     The paths to filter can be defined in the list of paths to ignore in
diff --git a/setup.cfg b/setup.cfg
index 53845bf..8480538 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,6 +1,6 @@
 [metadata]
 name = mkdocs_include_markdown_plugin
-version = 3.3.0
+version = 3.4.0
 description = Mkdocs Markdown includer plugin.
 long_description = file: README.md
 long_description_content_type = text/markdown
@@ -47,6 +47,7 @@ dev =
     flake8-print==4.0.0
     isort==5.9.1
     mdpo==0.3.61
+    mkdocs==1.2.3
     pre-commit==2.13.0
     pytest==6.2.5
     pytest-cov==3.0.0


Test patch:
diff --git a/tests/test_exclude.py b/tests/test_exclude.py
index 5530e43..c429aff 100644
--- a/tests/test_exclude.py
+++ b/tests/test_exclude.py
@@ -89,9 +89,13 @@ def test_exclude(
     if hasattr(expected_result, '__traceback__'):
         with pytest.raises(expected_result):
             on_page_markdown(
-                includer_filepath_content, page(includer_file),
+                includer_filepath_content,
+                page(includer_file),
+                includer_folder,
             )
     else:
         assert on_page_markdown(
-            includer_filepath_content, page(includer_file),
+            includer_filepath_content,
+            page(includer_file),
+            includer_folder,
         ) == expected_result
diff --git a/tests/test_glob_include.py b/tests/test_glob_include.py
index 4631c79..591bcd7 100644
--- a/tests/test_glob_include.py
+++ b/tests/test_glob_include.py
@@ -40,24 +40,28 @@ barbaz
 '''
 
     assert on_page_markdown(
-        includer_filepath_content, page(includer_file),
+        includer_filepath_content, page(includer_file), tmp_path,
     ) == expected_result
 
 
 @pytest.mark.parametrize('directive', ('include', 'include-markdown'))
 @pytest.mark.parametrize(
-    ('includer_content', 'expected_result'),
+    (
+        'includer_content',
+        'expected_result',
+        'expected_warnings_schemas',
+    ),
     (
         pytest.param(
             '''{%
-  include "./included*.txt"
+  directive "./included*.txt"
   start="<!-- start-2 -->"
   end="<!-- end-2 -->"
   comments=false
 %}
 
 {%
-  include "./included*.txt"
+  directive "./included*.txt"
   start="<!-- start-1 -->"
   end="<!-- end-1 -->"
   comments=false
@@ -71,11 +75,12 @@ baz
 bar
 
 ''',
+            [],
             id='start-end',
         ),
         pytest.param(
             '''{%
-  include "./included*.txt"
+  directive "./included*.txt"
   end="<!-- end-2 -->"
   comments=false
 %}
@@ -92,16 +97,61 @@ This 02 must appear only without specifying start.
 baz
 
 ''',
+            [],
             id='end',
         ),
+
+        # both start and end specified but not found in files to include
+        pytest.param(
+            '''{%
+  directive "./included*.txt"
+  start="<!-- start-not-found-2 -->"
+  end="<!-- end-not-found-2 -->"
+  comments=false
+%}
+
+{%
+  directive "./included*.txt"
+  start="<!-- start-not-found-1 -->"
+  end="<!-- end-not-found-1 -->"
+  comments=false
+%}
+''',
+            '\n\n\n',
+            [
+                (
+                    "Delimiter end '<!-- end-not-found-1 -->'"
+                    ' defined at {includer_file} not detected in'
+                    ' the files {included_file_01}, {included_file_02}'
+                ),
+                (
+                    "Delimiter end '<!-- end-not-found-2 -->'"
+                    ' defined at {includer_file} not detected in'
+                    ' the files {included_file_01}, {included_file_02}'
+                ),
+                (
+                    "Delimiter start '<!-- start-not-found-1 -->'"
+                    ' defined at {includer_file} not detected in'
+                    ' the files {included_file_01}, {included_file_02}'
+                ),
+                (
+                    "Delimiter start '<!-- start-not-found-2 -->'"
+                    ' defined at {includer_file} not detected in'
+                    ' the files {included_file_01}, {included_file_02}'
+                ),
+            ],
+            id='start-end-not-found',
+        ),
     ),
 )
 def test_glob_include(
-    page,
-    tmp_path,
     includer_content,
     directive,
     expected_result,
+    expected_warnings_schemas,
+    page,
+    caplog,
+    tmp_path,
 ):
     includer_file = tmp_path / 'includer.txt'
     included_01_file = tmp_path / 'included_01.txt'
@@ -109,7 +159,7 @@ def test_glob_include(
 
     includer_filepath_content = f'''foo
 
-{includer_content.replace('include "', directive + ' "')}
+{includer_content.replace('directive "', directive + ' "')}
 '''
 
     included_01_content = '''This 01 must appear only without specifying start.
@@ -129,11 +179,31 @@ This 02 must appear only without specifying end.
     included_01_file.write_text(included_01_content)
     included_02_file.write_text(included_02_content)
 
+    # assert content
     expected_result = f'''foo
 
 {expected_result}
 '''
 
     assert on_page_markdown(
-        includer_filepath_content, page(includer_file),
+        includer_filepath_content, page(includer_file), tmp_path,
     ) == expected_result
+
+    # assert warnings
+    expected_warnings_schemas = expected_warnings_schemas or []
+    expected_warnings = [
+        msg_schema.replace(
+            '{includer_file}',
+            str(includer_file.relative_to(tmp_path)),
+        ).replace(
+            '{included_file_01}',
+            str(included_01_file.relative_to(tmp_path)),
+        ).replace(
+            '{included_file_02}',
+            str(included_02_file.relative_to(tmp_path)),
+        ) for msg_schema in expected_warnings_schemas
+    ]
+
+    for record in caplog.records:
+        assert record.msg in expected_warnings
+    assert len(expected_warnings_schemas) == len(caplog.records)
diff --git a/tests/test_include.py b/tests/test_include.py
index 44ae254..6f3be9c 100644
--- a/tests/test_include.py
+++ b/tests/test_include.py
@@ -1,7 +1,5 @@
 '''``include`` directive tests.'''
 
-import textwrap
-
 import pytest
 
 from mkdocs_include_markdown_plugin.event import on_page_markdown
@@ -12,12 +10,14 @@ from mkdocs_include_markdown_plugin.event import on_page_markdown
         'includer_schema',
         'content_to_include',
         'expected_result',
+        'expected_warnings_schemas',
     ),
     (
         pytest.param(
             '# Header\n\n{% include "{filepath}" %}\n',
             'This must be included.',
             '# Header\n\nThis must be included.\n',
+            [],
             id='simple-case',
         ),
 
@@ -26,6 +26,7 @@ from mkdocs_include_markdown_plugin.event import on_page_markdown
             '# Header\n\n{% include "{filepath}" %}\n',
             'This must be included.\n',
             '# Header\n\nThis must be included.\n\n',
+            [],
             id='newline-at-end-of-included',
         ),
 
@@ -47,6 +48,7 @@ This must be ignored also.
 
 This must be included.
 ''',
+            [],
             id='start/end',
         ),
 
@@ -68,6 +70,7 @@ This must be ignored also.
 
 This must be included.
 ''',
+            [],
             id='start/end (escaped special characters)',
         ),
 
@@ -89,6 +92,7 @@ This must be ignored also.
 
 This must be included.
 ''',
+            [],
             id='start/end (unescaped special characters)',
         ),
 
@@ -129,9 +133,95 @@ This should be included also.
 
 This should be included even if hasn't defined after end tag.
 ''',
+            [],
             id='multiple-start-end-matchs',
         ),
 
+        # Don't specify end and finds start in included content
+        pytest.param(
+            '''Foo
+{%
+    include "{filepath}"
+    start="<!--start-->"
+%}''',
+            '''Some text
+
+<!--start-->
+More text
+''',
+            '''Foo
+
+More text
+''',
+            [],
+            id='start=foo-end=None',
+        ),
+
+        # Don't specify start and finds end in included content
+        pytest.param(
+            '''Foo
+{%
+    include "{filepath}"
+    end="<!--end-->"
+%}''',
+            '''
+Some text
+<!--end-->
+More text
+''',
+            '''Foo
+
+Some text
+''',
+            [],
+            id='start=None-end=foo',
+        ),
+
+        # Don't specify end but not finds start in included content
+        pytest.param(
+            '''Foo
+{%
+    include-markdown "{filepath}"
+    start="<!--start-->"
+    comments=false
+%}''',
+            '''Some text
+''',
+            '''Foo
+''',
+            [
+                (
+                    "Delimiter start '<!--start-->' defined at {filepath}"
+                    ' not detected in the file {included_filepath}'
+                ),
+            ],
+            id='start=foo (not found)-end=None',
+        ),
+
+        # Don't specify start but not finds end in included content
+        pytest.param(
+            '''Foo
+{%
+    include-markdown "{filepath}"
+    end="<!--end-->"
+    comments=false
+%}''',
+            '''
+Some text
+''',
+            '''Foo
+
+Some text
+''',
+            [
+                (
+                    "Delimiter end '<!--end-->' defined at {filepath}"
+                    ' not detected in the file {included_filepath}'
+                ),
+            ],
+            id='start=None-end=foo (not found)',
+        ),
+
         # Preserve included indent
         pytest.param(
             '''1. Ordered list item
@@ -144,6 +234,7 @@ This should be included even if hasn't defined after end tag.
             '''1. Ordered list item
     - Unordered sublist item
     - Other unordered sublist item''',
+            [],
             id='preserve included indent',
         ),
 
@@ -160,6 +251,7 @@ This should be included even if hasn't defined after end tag.
     - First unordered sublist item
     - Second unordered sublist item
     - Third unordered sublist item''',
+            [],
             id='preserve includer indent',
         ),
 
@@ -178,6 +270,7 @@ This should be included even if hasn't defined after end tag.
             '''1. Ordered list item
     - First unordered sublist item
     - Second unordered sublist item''',
+            [],
             id='custom options ordering',
         ),
 
@@ -199,6 +292,7 @@ This should be included even if hasn't defined after end tag.
 - Bar
     - Baz
 ''',
+            [],
             id='dedent=true',
         ),
 
@@ -221,13 +315,19 @@ This should be included even if hasn't defined after end tag.
     - Bar
         - Baz
 ''',
+            [],
             id='dedent=true,preserve-includer-indent=true',
         ),
     ),
 )
 def test_include(
-    includer_schema, content_to_include, expected_result,
-    page, tmp_path,
+    includer_schema,
+    content_to_include,
+    expected_result,
+    expected_warnings_schemas,
+    page,
+    caplog,
+    tmp_path,
 ):
     included_filepath = tmp_path / 'included.md'
     includer_filepath = tmp_path / 'includer.md'
@@ -237,15 +337,32 @@ def test_include(
         content_to_include.replace('{filepath}', included_filepath.as_posix()),
     )
 
+    # assert content
     page_content = includer_schema.replace(
         '{filepath}', included_filepath.as_posix(),
     )
     includer_filepath.write_text(page_content)
 
     assert on_page_markdown(
-        page_content, page(included_filepath),
+        page_content, page(includer_filepath), tmp_path,
     ) == expected_result
 
+    # assert warnings
+    expected_warnings_schemas = expected_warnings_schemas or []
+    expected_warnings = [
+        msg_schema.replace(
+            '{filepath}',
+            str(includer_filepath.relative_to(tmp_path)),
+        ).replace(
+            '{included_filepath}',
+            str(included_filepath.relative_to(tmp_path)),
+        ) for msg_schema in expected_warnings_schemas
+    ]
+
+    for record in caplog.records:
+        assert record.msg in expected_warnings
+    assert len(expected_warnings_schemas) == len(caplog.records)
+
 
 def test_include_filepath_error(page, tmp_path):
     page_content = '''# Header
@@ -257,7 +374,7 @@ def test_include_filepath_error(page, tmp_path):
     page_filepath.write_text(page_content)
 
     with pytest.raises(FileNotFoundError):
-        on_page_markdown(page_content, page(page_filepath))
+        on_page_markdown(page_content, page(page_filepath), tmp_path)
 
 
 @pytest.mark.parametrize(
@@ -269,14 +386,14 @@ def test_include_filepath_error(page, tmp_path):
 )
 def test_include_invalid_bool_option(opt_name, page, tmp_path):
     page_filepath = tmp_path / 'example.md'
-    page_content = textwrap.dedent(f'''{{%
-        include "{page_filepath}"
-        {opt_name}=invalidoption
-    %}}''')
+    page_content = f'''{{%
+    include "{page_filepath}"
+    {opt_name}=invalidoption
+%}}'''
     page_filepath.write_text(page_content)
 
     with pytest.raises(ValueError) as excinfo:
-        on_page_markdown(page_content, page(page_filepath))
+        on_page_markdown(page_content, page(page_filepath), tmp_path)
 
     expected_exc_message = (
         f'Unknown value for \'{opt_name}\'.'
diff --git a/tests/test_include_markdown.py b/tests/test_include_markdown.py
index 932b19c..45029a6 100644
--- a/tests/test_include_markdown.py
+++ b/tests/test_include_markdown.py
@@ -1,7 +1,5 @@
 '''``include-markdown`` directive tests'''
 
-import textwrap
-
 import pytest
 
 from mkdocs_include_markdown_plugin.event import on_page_markdown
@@ -9,7 +7,10 @@ from mkdocs_include_markdown_plugin.event import on_page_markdown
 
 @pytest.mark.parametrize(
     (
-        'includer_schema', 'content_to_include', 'expected_result_schema',
+        'includer_schema',
+        'content_to_include',
+        'expected_result_schema',
+        'expected_warnings_schemas',
     ),
     (
         # Simple case
@@ -22,6 +23,7 @@ from mkdocs_include_markdown_plugin.event import on_page_markdown
 This must be included.
 <!-- END INCLUDE -->
 ''',
+            [],
             id='simple case',
         ),
 
@@ -44,6 +46,7 @@ This must be included.''',
 This must be included.
 <!-- END INCLUDE -->
 ''',
+            [],
             id='start',
         ),
 
@@ -66,6 +69,7 @@ This must be included.
 
 <!-- END INCLUDE -->
 ''',
+            [],
             id='end',
         ),
 
@@ -92,6 +96,7 @@ This must be included.
 
 <!-- END INCLUDE -->
 ''',
+            [],
             id='start/end',
         ),
 
@@ -118,6 +123,7 @@ This must be included.
 
 <!-- END INCLUDE -->
 ''',
+            [],
             id='start/end (escaped special characters)',
         ),
 
@@ -146,6 +152,7 @@ This must be included.
 
 <!-- END INCLUDE -->
 ''',
+            [],
             id='start/end (unescaped special characters)',
         ),
 
@@ -157,6 +164,7 @@ This must be included.
 %}''',
             '''Foo''',
             '''Foo''',
+            [],
             id='comments=false',
         ),
 
@@ -197,6 +205,7 @@ This should be included also.
 
 This should be included even if hasn't defined after end tag.
 ''',
+            [],
             id='multiple-start-end-matchs',
         ),
 
@@ -213,9 +222,97 @@ This should be included even if hasn't defined after end tag.
             '''1. Ordered list item
     - Unordered sublist item
     - Other unordered sublist item''',
+            [],
             id='preserve-includer-indent=false',
         ),
 
+        # Don't specify end and finds start in included content
+        pytest.param(
+            '''# Header
+{%
+    include-markdown "{filepath}"
+    start="<!--start-->"
+    comments=false
+%}''',
+            '''Some text
+
+<!--start-->
+More text
+''',
+            '''# Header
+
+More text
+''',
+            [],
+            id='start=foo-end=None',
+        ),
+
+        # Don't specify start and finds end in included content
+        pytest.param(
+            '''# Header
+{%
+    include-markdown "{filepath}"
+    end="<!--end-->"
+    comments=false
+%}''',
+            '''
+Some text
+<!--end-->
+More text
+''',
+            '''# Header
+
+Some text
+''',
+            [],
+            id='start=None-end=foo',
+        ),
+
+        # Don't specify end but not finds start in included content
+        pytest.param(
+            '''# Header
+{%
+    include-markdown "{filepath}"
+    start="<!--start-->"
+    comments=false
+%}''',
+            '''Some text
+''',
+            '''# Header
+''',
+            [
+                (
+                    "Delimiter start '<!--start-->' defined at {filepath}"
+                    ' not detected in the file {included_filepath}'
+                ),
+            ],
+            id='start=foo (not found)-end=None',
+        ),
+
+        # Don't specify start but not finds end in included content
+        pytest.param(
+            '''# Header
+{%
+    include-markdown "{filepath}"
+    end="<!--end-->"
+    comments=false
+%}''',
+            '''
+Some text
+''',
+            '''# Header
+
+Some text
+''',
+            [
+                (
+                    "Delimiter end '<!--end-->' defined at {filepath}"
+                    ' not detected in the file {included_filepath}'
+                ),
+            ],
+            id='start=None-end=foo (not found)',
+        ),
+
         # Preserve includer indent
         pytest.param(
             '''1. Ordered list item
@@ -230,6 +327,7 @@ This should be included even if hasn't defined after end tag.
     - First unordered sublist item
     - Second unordered sublist item
     - Third unordered sublist item''',
+            [],
             id='preserve-includer-indent=true (default)',
         ),
 
@@ -249,6 +347,7 @@ This should be included even if hasn't defined after end tag.
             '''1. Ordered list item
     - First unordered sublist item
     - Second unordered sublist item''',
+            [],
             id='custom options ordering',
         ),
 
@@ -271,6 +370,7 @@ This should be included even if hasn't defined after end tag.
 - Bar
     - Baz
 ''',
+            [],
             id='dedent=true',
         ),
 
@@ -294,6 +394,7 @@ This should be included even if hasn't defined after end tag.
     - Bar
         - Baz
 ''',
+            [],
             id='dedent=true,preserve-includer-indent=true',
         ),
 
@@ -317,6 +418,7 @@ Example data''',
 Example data
 <!-- END INCLUDE -->
 ''',
+            [],
             id='heading-offset=1',
         ),
         # Markdown heading offset 2
@@ -339,6 +441,7 @@ Example data''',
 Example data
 <!-- END INCLUDE -->
 ''',
+            [],
             id='heading-offset=2',
         ),
 
@@ -361,6 +464,7 @@ Example data''',
 Example data
 <!-- END INCLUDE -->
 ''',
+            [],
             id='no heading-offset (default)',
         ),
 
@@ -384,6 +488,7 @@ Example data''',
 Example data
 <!-- END INCLUDE -->
 ''',
+            [],
             id='heading-offset=0',
         ),
 
@@ -407,6 +512,7 @@ Example data''',
 Example data
 <!-- END INCLUDE -->
 ''',
+            [],
             id='heading-offset=<str>',
         ),
 
@@ -430,6 +536,7 @@ Example data''',
 Example data
 <!-- END INCLUDE -->
 ''',
+            [],
             id='heading-offset=-2',
         ),
 
@@ -453,6 +560,7 @@ Example data''',
 Example data
 <!-- END INCLUDE -->
 ''',
+            [],
             id='heading-offset=90',
         ),
 
@@ -476,6 +584,7 @@ Example data''',
 Example data
 <!-- END INCLUDE -->
 ''',
+            [],
             id='heading-offset=-90',
         ),
 
@@ -521,6 +630,7 @@ vɛвѣди
 
 <!-- END INCLUDE -->
 ''',
+            [],
             id='russian-characters',
         ),
     ),
@@ -529,7 +639,9 @@ def test_include_markdown(
     includer_schema,
     content_to_include,
     expected_result_schema,
+    expected_warnings_schemas,
     page,
+    caplog,
     tmp_path,
 ):
     included_filepath = tmp_path / 'included.md'
@@ -546,13 +658,33 @@ def test_include_markdown(
     )
     includer_filepath.write_text(page_content, encoding='utf-8')
 
+    # assert content
     expected_result = expected_result_schema.replace(
         '{filepath}', included_filepath.as_posix(),
     )
+
     assert on_page_markdown(
-        page_content, page(included_filepath),
+        page_content,
+        page(includer_filepath),
+        tmp_path,
     ) == expected_result
 
+    # assert warnings
+    expected_warnings_schemas = expected_warnings_schemas or []
+    expected_warnings = [
+        msg_schema.replace(
+            '{filepath}',
+            str(includer_filepath.relative_to(tmp_path)),
+        ).replace(
+            '{included_filepath}',
+            str(included_filepath.relative_to(tmp_path)),
+        ) for msg_schema in expected_warnings_schemas
+    ]
+
+    for record in caplog.records:
+        assert record.msg in expected_warnings
+    assert len(expected_warnings_schemas) == len(caplog.records)
+
 
 def test_include_markdown_filepath_error(page, tmp_path):
     page_content = '''{%
@@ -565,81 +697,85 @@ def test_include_markdown_filepath_error(page, tmp_path):
     page_filepath.write_text(page_content)
 
     with pytest.raises(FileNotFoundError):
-        on_page_markdown(page_content, page(page_filepath))
+        on_page_markdown(page_content, page(page_filepath), tmp_path)
 
 
 @pytest.mark.parametrize('rewrite_relative_urls', ['true', 'false', None])
 def test_include_markdown_relative_rewrite(
-    page, tmp_path,
+    page,
+    tmp_path,
     rewrite_relative_urls,
 ):
     option_value = '' if rewrite_relative_urls is None else (
         'rewrite-relative-urls=' + rewrite_relative_urls
     )
 
+    docs_dir = tmp_path / 'docs'
+    docs_dir.mkdir()
+
     includer_path = tmp_path / 'includer.md'
     includer_path.write_text(
-        textwrap.dedent(f'''
-        # Heading
-
-        {{%
-            include-markdown "docs/page.md"
-            start="<!--start-here-->"
-            end="<!--end-here-->"
-            {option_value}
-        %}}
-    '''),
+        f'''
+# Heading
+
+{{%
+    include-markdown "docs/page.md"
+    start="<!--start-->"
+    end="<!--end-->"
+    {option_value}
+%}}
+''',
     )
 
-    (tmp_path / 'docs').mkdir()
-    included_file_path = tmp_path / 'docs' / 'page.md'
+    included_file_path = docs_dir / 'page.md'
     included_file_path.write_text(
-        textwrap.dedent('''
-        # Subpage Heading
-        <!--start-here-->
-        Here's [a link](page2.md) and here's an image: ![](image.png)
+        '''
+# Subpage Heading
+<!--start-->
+Here's [a link](page2.md) and here's an image: ![](image.png)
 
-        Here's a [reference link][ref-link].
+Here's a [reference link][ref-link].
 
-        [ref-link]: page3.md
-        <!--end-here-->
-    '''),
+[ref-link]: page3.md
+<!--end-->
+''',
     )
 
     output = on_page_markdown(
         includer_path.read_text(),
         page(str(includer_path)),
+        docs_dir,
     )
 
     if rewrite_relative_urls in ['true', None]:
-        assert output == textwrap.dedent('''
-            # Heading
+        assert output == '''
+# Heading
 
-            <!-- BEGIN INCLUDE docs/page.md &lt;!--start-here--&gt; &lt;!--end-here--&gt; -->
+<!-- BEGIN INCLUDE docs/page.md &lt;!--start--&gt; &lt;!--end--&gt; -->
 
-            Here's [a link](docs/page2.md) and here's an image: ![](docs/image.png)
+Here's [a link](docs/page2.md) and here's an image: ![](docs/image.png)
 
-            Here's a [reference link][ref-link].
+Here's a [reference link][ref-link].
 
-            [ref-link]: docs/page3.md
+[ref-link]: docs/page3.md
 
-            <!-- END INCLUDE -->
-        ''')  # noqa: E501
+<!-- END INCLUDE -->
+'''
     else:
         # include without rewriting
-        assert output == textwrap.dedent('''
-            # Heading
+        assert output == '''
+# Heading
 
-            <!-- BEGIN INCLUDE docs/page.md &lt;!--start-here--&gt; &lt;!--end-here--&gt; -->
+<!-- BEGIN INCLUDE docs/page.md &lt;!--start--&gt; &lt;!--end--&gt; -->
 
-            Here's [a link](page2.md) and here's an image: ![](image.png)
+Here's [a link](page2.md) and here's an image: ![](image.png)
 
-            Here's a [reference link][ref-link].
+Here's a [reference link][ref-link].
 
-            [ref-link]: page3.md
+[ref-link]: page3.md
 
-            <!-- END INCLUDE -->
-        ''')  # noqa: E501
+<!-- END INCLUDE -->
+'''
 
 
 @pytest.mark.parametrize(
@@ -653,14 +789,14 @@ def test_include_markdown_relative_rewrite(
 )
 def test_include_markdown_invalid_bool_option(opt_name, page, tmp_path):
     page_filepath = tmp_path / 'example.md'
-    page_content = textwrap.dedent(f'''{{%
-        include-markdown "{page_filepath}"
-        {opt_name}=invalidoption
-    %}}''')
+    page_content = f'''{{%
+    include-markdown "{page_filepath}"
+    {opt_name}=invalidoption
+%}}'''
     page_filepath.write_text(page_content)
 
     with pytest.raises(ValueError) as excinfo:
-        on_page_markdown(page_content, page(page_filepath))
+        on_page_markdown(page_content, page(page_filepath), tmp_path)
 
     expected_exc_message = (
         f'Unknown value for \'{opt_name}\'.'
@@ -712,5 +848,5 @@ Another
 Another
 '''
     assert on_page_markdown(
-        includer_content, page(includer_filepath),
+        includer_content, page(includer_filepath), tmp_path,
     ) == expected_result
diff --git a/tests/test_nested_includes.py b/tests/test_nested_includes.py
index 99574fb..4ce6f63 100644
--- a/tests/test_nested_includes.py
+++ b/tests/test_nested_includes.py
@@ -11,6 +11,7 @@ from mkdocs_include_markdown_plugin.event import on_page_markdown
         'second_includer_content',
         'included_content',
         'expected_result',
+        'expected_warnings_schemas',
     ),
     (
         # Includer -> Markdown -> Markdown
@@ -35,6 +36,7 @@ Some text from second includer.
 
 Some test from final included.
 ''',
+            [],
             id='includer -> markdown -> markdown',
         ),
 
@@ -59,6 +61,7 @@ Some text from second includer.
 
 Some test from final included.
 ''',
+            [],
             id='includer -> markdown -> file',
         ),
 
@@ -82,6 +85,7 @@ Some text from second includer.
 
 Some test from final included.
 ''',
+            [],
             id='includer -> file -> file',
         ),
 
@@ -112,6 +116,7 @@ Some text from second includer.
 
 Some test from final included.
 ''',
+            [],
             id='includer -> file -> markdown',
         ),
 
@@ -141,8 +146,82 @@ Some test from final included.
 ### Header 3
 
 ''',
+            [],
             id='cumulative_heading_offset',
         ),
+
+        # start and end defined in second inclusion but not found
+        pytest.param(
+            '''# Header
+
+{%
+  include-markdown "{filepath}"
+  comments=false
+%}''',
+            '''# Header 2
+
+{%
+  include-markdown "{filepath}"
+  comments=false
+  start="<!--start-->"
+  end="<!--end-->"
+%}
+''',
+            '''# Header 3
+''',
+            '''# Header
+
+# Header 2
+
+
+''',
+            [
+                (
+                    "Delimiter start '<!--start-->' defined at"
+                    ' {second_includer_filepath} not detected in the'
+                    ' file {included_filepath}'
+                ),
+                (
+                    "Delimiter end '<!--end-->' defined at"
+                    ' {second_includer_filepath} not detected in the'
+                    ' file {included_filepath}'
+                ),
+            ],
+            id='start-end-not-found (second-level)',
+        ),
+
+        # start and end defined in first inclusion but not found
+        pytest.param(
+            '''# Header
+
+{%
+  include-markdown "{filepath}"
+  comments=false
+  start="<!--start-->"
+  end="<!--end-->"
+%}''',
+            '''# Header 2
+
+''',
+            '''# Header 3
+''',
+            '''# Header
+
+''',
+            [
+                (
+                    "Delimiter start '<!--start-->' defined at"
+                    ' {first_includer_filepath} not detected in the file'
+                    ' {second_includer_filepath}'
+                ),
+                (
+                    "Delimiter end '<!--end-->' defined at"
+                    ' {first_includer_filepath} not detected in the file'
+                    ' {second_includer_filepath}'
+                ),
+            ],
+            id='start-end-not-found (first-level)',
+        ),
     ),
 )
 def test_nested_include(
@@ -150,7 +229,9 @@ def test_nested_include(
     second_includer_content,
     included_content,
     expected_result,
+    expected_warnings_schemas,
     page,
+    caplog,
     tmp_path,
 ):
     first_includer_filepath = tmp_path / 'first-includer.txt'
@@ -168,16 +249,37 @@ def test_nested_include(
     second_includer_filepath.write_text(second_includer_content)
     included_filepath.write_text(included_content)
 
+    # assert content
     assert on_page_markdown(
-        first_includer_content, page(first_includer_filepath),
+        first_includer_content, page(first_includer_filepath), tmp_path,
     ) == expected_result
 
+    # assert warnings
+    expected_warnings_schemas = expected_warnings_schemas or []
+    expected_warnings = [
+        msg_schema.replace(
+            '{first_includer_filepath}',
+            str(first_includer_filepath.relative_to(tmp_path)),
+        ).replace(
+            '{second_includer_filepath}',
+            str(second_includer_filepath.relative_to(tmp_path)),
+        ).replace(
+            '{included_filepath}',
+            str(included_filepath.relative_to(tmp_path)),
+        ) for msg_schema in expected_warnings_schemas
+    ]
+
+    for record in caplog.records:
+        assert record.msg in expected_warnings
+    assert len(expected_warnings_schemas) == len(caplog.records)
+
 
 def test_nested_include_relpath(page, tmp_path):
+    docs_dir = tmp_path / 'docs'
+    docs_dir.mkdir()
+
     first_includer_filepath = tmp_path / 'first-includer.txt'
-    docs_path = tmp_path / 'docs'
-    docs_path.mkdir()
-    second_includer_filepath = docs_path / 'second-includer.txt'
+    second_includer_filepath = docs_dir / 'second-includer.txt'
     included_filepath = tmp_path / 'included.txt'
 
     first_includer_content = '''# Header
@@ -209,5 +311,7 @@ Included content.
 '''
 
     assert on_page_markdown(
-        first_includer_content, page(first_includer_filepath),
+        first_includer_content,
+        page(first_includer_filepath),
+        docs_dir,
     ) == expected_result