Repository: sphinx-doc/sphinx

Issue:
Run epubcheck as a testing
### Problem
- Sphinx-1.7.0 generates invalid epub file (see #4608)
- We don't test that generated epub file is correct

#### Expected results
Run epubcheck as a part of testing.


Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/sphinx-doc/sphinx/pull/6010?src=pr&el=h1) Report
> Merging [#6010](https://codecov.io/gh/sphinx-doc/sphinx/pull/6010?src=pr&el=desc) into [master](https://codecov.io/gh/sphinx-doc/sphinx/commit/c70dfcd39007dd6d53d5dc3b686b4c6dcef623dd?src=pr&el=desc) will **decrease** coverage by `<.01%`.
> The diff coverage is `71.42%`.

[![Impacted file tree graph](https://codecov.io/gh/sphinx-doc/sphinx/pull/6010/graphs/tree.svg?width=650&token=VFlFSELbkw&height=150&src=pr)](https://codecov.io/gh/sphinx-doc/sphinx/pull/6010?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master    #6010      +/-   ##
==========================================
- Coverage    82.9%   82.89%   -0.01%     
==========================================
  Files         272      272              
  Lines       39206    39213       +7     
  Branches     5868     5870       +2     
==========================================
+ Hits        32502    32505       +3     
- Misses       5349     5350       +1     
- Partials     1355     1358       +3
```


| [Impacted Files](https://codecov.io/gh/sphinx-doc/sphinx/pull/6010?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [sphinx/builders/\_epub\_base.py](https://codecov.io/gh/sphinx-doc/sphinx/pull/6010/diff?src=pr&el=tree#diff-c3BoaW54L2J1aWxkZXJzL19lcHViX2Jhc2UucHk=) | `66% <71.42%> (+0.08%)` | :arrow_up: |
| [sphinx/environment/collectors/toctree.py](https://codecov.io/gh/sphinx-doc/sphinx/pull/6010/diff?src=pr&el=tree#diff-c3BoaW54L2Vudmlyb25tZW50L2NvbGxlY3RvcnMvdG9jdHJlZS5weQ==) | `90.14% <0%> (-0.5%)` | :arrow_down: |
| [sphinx/util/nodes.py](https://codecov.io/gh/sphinx-doc/sphinx/pull/6010/diff?src=pr&el=tree#diff-c3BoaW54L3V0aWwvbm9kZXMucHk=) | `90.69% <0%> (-0.39%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/sphinx-doc/sphinx/pull/6010?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/sphinx-doc/sphinx/pull/6010?src=pr&el=footer). Last update [c70dfcd...5a4eb7a](https://codecov.io/gh/sphinx-doc/sphinx/pull/6010?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/CHANGES b/CHANGES
index 23a4d0f3b..cd2acfd0c 100644
--- a/CHANGES
+++ b/CHANGES
@@ -166,6 +166,7 @@ Features added
 * C++: add ``cpp:struct`` to complement ``cpp:class``.
 * #1341 the HTML search considers words that contain a search term of length
   three or longer a match.
+* #4611: epub: Show warning for duplicated ToC entries
 
 Bugs fixed
 ----------
diff --git a/sphinx/builders/_epub_base.py b/sphinx/builders/_epub_base.py
index 7038f45f6..5516fdaf8 100644
--- a/sphinx/builders/_epub_base.py
+++ b/sphinx/builders/_epub_base.py
@@ -35,7 +35,7 @@
 
 if False:
     # For type annotation
-    from typing import Any, Dict, List, Tuple  # NOQA
+    from typing import Any, Dict, List, Set, Tuple  # NOQA
     from sphinx.application import Sphinx  # NOQA
 
 
@@ -213,6 +213,15 @@ def get_refnodes(self, doctree, result):
                 result = self.get_refnodes(elem, result)
         return result
 
+    def check_refnodes(self, nodes):
+        # type: (List[Dict[str, Any]]) -> None
+        appeared = set()  # type: Set[str]
+        for node in nodes:
+            if node['refuri'] in appeared:
+                logger.warning(__('duplicated ToC entry found: %s'), node['refuri'])
+            else:
+                appeared.add(node['refuri'])
+
     def get_toc(self):
         # type: () -> None
         """Get the total table of contents, containing the master_doc
@@ -726,6 +735,7 @@ def build_toc(self, outdir=None, outname='toc.ncx'):
         else:
             # 'includehidden'
             refnodes = self.refnodes
+        self.check_refnodes(refnodes)
         navpoints = self.build_navpoints(refnodes)
         level = max(item['level'] for item in self.refnodes)
         level = min(level, self.config.epub_tocdepth)


Test patch:
diff --git a/tests/roots/test-root/index.txt b/tests/roots/test-root/index.txt
index ce0338cf7..27f90f357 100644
--- a/tests/roots/test-root/index.txt
+++ b/tests/roots/test-root/index.txt
@@ -24,7 +24,6 @@ Contents:
    math
    autodoc
    extensions
-   extensions
    footnote
    lists
    otherext
diff --git a/tests/roots/test-toctree-duplicated/conf.py b/tests/roots/test-toctree-duplicated/conf.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/roots/test-toctree-duplicated/foo.rst b/tests/roots/test-toctree-duplicated/foo.rst
new file mode 100644
index 000000000..f23d4ce05
--- /dev/null
+++ b/tests/roots/test-toctree-duplicated/foo.rst
@@ -0,0 +1,2 @@
+foo
+===
diff --git a/tests/roots/test-toctree-duplicated/index.rst b/tests/roots/test-toctree-duplicated/index.rst
new file mode 100644
index 000000000..38a8c44c1
--- /dev/null
+++ b/tests/roots/test-toctree-duplicated/index.rst
@@ -0,0 +1,7 @@
+test-toctree-duplicated
+=======================
+
+.. toctree::
+
+   foo
+   foo
diff --git a/tests/test_build_epub.py b/tests/test_build_epub.py
index 86e40b65d..337363832 100644
--- a/tests/test_build_epub.py
+++ b/tests/test_build_epub.py
@@ -367,6 +367,12 @@ def test_html_download_role(app, status, warning):
             '/_static/sphinxheader.png]</span></p></li>' in content)
 
 
+@pytest.mark.sphinx('epub', testroot='toctree-duplicated')
+def test_duplicated_toctree_entry(app, status, warning):
+    app.build()
+    assert 'WARNING: duplicated ToC entry found: foo.xhtml' in warning.getvalue()
+
+
 @pytest.mark.skipif('DO_EPUBCHECK' not in os.environ,
                     reason='Skipped because DO_EPUBCHECK is not set')
 @pytest.mark.sphinx('epub')