Completed
Push — develop ( 00311a...62764c )
by Jace
10s
created

TestTableOfContents.test_toc()   A

Complexity

Conditions 1

Size

Total Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 1
c 1
b 0
f 1
dl 0
loc 11
rs 9.4285
1
"""Unit tests for the doorstop.core.publisher module."""
2
3
import unittest
4
from unittest.mock import patch, Mock, MagicMock, call
5
from unittest import mock
6
7
import os
8
9
from doorstop.common import DoorstopError
10
from doorstop.core import publisher
11
from doorstop.core.document import Document
12
from doorstop.core.test import (FILES, EMPTY, ROOT, MockDataMixIn,
13
                                MockItemAndVCS, MockItem, MockDocument)
14
15
16
class TestModule(MockDataMixIn, unittest.TestCase):
17
    """Unit tests for the doorstop.core.publisher module."""
18
19
    @patch('os.path.isdir', Mock(return_value=False))
20
    @patch('os.makedirs')
21
    @patch('builtins.open')
22
    def test_publish_document(self, mock_open, mock_makedirs):
23
        """Verify a document can be published."""
24
        dirpath = os.path.join('mock', 'directory')
25
        path = os.path.join(dirpath, 'published.html')
26
        self.document.items = []
27
        # Act
28
        path2 = publisher.publish(self.document, path)
29
        # Assert
30
        self.assertIs(path, path2)
31
        mock_makedirs.assert_called_once_with(os.path.join(dirpath, Document.ASSETS))
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (85/80).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
32
        mock_open.assert_called_once_with(path, 'wb')
33
34
    @patch('os.path.isdir', Mock(return_value=False))
35
    @patch('os.makedirs')
36
    @patch('builtins.open')
37
    @patch('doorstop.core.publisher.publish_lines')
38
    def test_publish_document_html(self, mock_lines, mock_open, mock_makedirs):
39
        """Verify a (mock) HTML file can be created."""
40
        dirpath = os.path.join('mock', 'directory')
41
        path = os.path.join(dirpath, 'published.custom')
42
        # Act
43
        path2 = publisher.publish(self.document, path, '.html')
44
        # Assert
45
        self.assertIs(path, path2)
46
        mock_makedirs.assert_called_once_with(os.path.join(dirpath, Document.ASSETS))
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (85/80).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
47
        mock_open.assert_called_once_with(path, 'wb')
48
        mock_lines.assert_called_once_with(self.document, '.html',
49
                                           template=publisher.HTMLTEMPLATE, toc=True,
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (85/80).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
50
                                           linkify=False)
51
52
    @patch('os.path.isdir', Mock(side_effect=[True, False, False, False]))
53
    @patch('os.remove')
54
    @patch('glob.glob')
55
    @patch('builtins.open')
56
    @patch('doorstop.core.publisher.publish_lines')
57
    def test_publish_document_deletes_the_contents_of_assets_folder(self, mock_lines, mock_open, mock_glob, mock_rm):
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (117/80).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
58
        """Verify that the contents of an assets directory next to the published file is deleted"""
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (99/80).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
59
        dirpath = os.path.abspath(os.path.join('mock', 'directory'))
60
        path = os.path.join(dirpath, 'published.custom')
61
        assets = [os.path.join(dirpath, Document.ASSETS, dir) for dir in ['css', 'logo.png']]
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (93/80).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
62
        mock_glob.return_value = assets
63
        # Act
64
        path2 = publisher.publish(self.document, path, '.html')
65
        # Assert
66
        self.assertIs(path, path2)
67
        mock_open.assert_called_once_with(path, 'wb')
68
        mock_lines.assert_called_once_with(self.document, '.html',
69
                                           template=publisher.HTMLTEMPLATE,
70
                                           toc=True,
71
                                           linkify=False)
72
        calls = [call(assets[0]), call(assets[1])]
73
        self.assertEqual(calls, mock_rm.call_args_list)
74
75 View Code Duplication
    @patch('os.path.isdir', Mock(return_value=False))
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
76
    @patch('doorstop.core.document.Document.copy_assets')
77
    @patch('os.makedirs')
78
    @patch('builtins.open')
79
    def test_publish_document_copies_assets(self, mock_open, mock_makedirs, mock_copyassets):
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (93/80).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
80
        """Verify that assets are published"""
81
        dirpath = os.path.join('mock', 'directory')
82
        assets_path = os.path.join(dirpath, 'assets')
83
        path = os.path.join(dirpath, 'published.custom')
84
        document = MockDocument('/some/path')
85
        mock_open.side_effect = lambda *args, **kw: mock.mock_open(read_data="$body").return_value
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (98/80).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
86
        # Act
87
        path2 = publisher.publish(document, path, '.html')
88
        # Assert
89
        self.assertIs(path, path2)
90
        mock_makedirs.assert_called_once_with(os.path.join(dirpath, Document.ASSETS))
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (85/80).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
91
        mock_copyassets.assert_called_once_with(assets_path)
92
93
    def test_publish_document_unknown(self):
94
        """Verify an exception is raised when publishing unknown formats."""
95
        self.assertRaises(DoorstopError,
96
                          publisher.publish, self.document, 'a.a')
97
        self.assertRaises(DoorstopError,
98
                          publisher.publish, self.document, 'a.txt', '.a')
99
100 View Code Duplication
    @patch('os.path.isdir', Mock(return_value=False))
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
101
    @patch('os.makedirs')
102
    @patch('doorstop.core.publisher._index')
103
    @patch('builtins.open')
104
    def test_publish_tree(self, mock_open, mock_index, mock_makedirs):
0 ignored issues
show
Unused Code introduced by
The argument mock_makedirs seems to be unused.
Loading history...
105
        """Verify a tree can be published."""
106
        dirpath = os.path.join('mock', 'directory')
107
        mock_open.side_effect = lambda *args, **kw: mock.mock_open(read_data="$body").return_value
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (98/80).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
108
        expected_calls = [call(os.path.join('mock', 'directory', 'MOCK.html'), 'wb'), call(publisher.HTMLTEMPLATE, 'r')]
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (120/80).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
109
        # Act
110
        dirpath2 = publisher.publish(self.mock_tree, dirpath)
111
        # Assert
112
        self.assertIs(dirpath, dirpath2)
113
        self.assertEqual(expected_calls, mock_open.call_args_list)
114
        mock_index.assert_called_once_with(dirpath, tree=self.mock_tree)
115
116 View Code Duplication
    @patch('os.path.isdir', Mock(return_value=False))
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
117
    @patch('os.makedirs')
118
    @patch('doorstop.core.publisher._index')
119
    @patch('builtins.open')
120
    def test_publish_tree_no_index(self, mock_open, mock_index, mock_makedirs):
0 ignored issues
show
Unused Code introduced by
The argument mock_makedirs seems to be unused.
Loading history...
121
        """Verify a tree can be published."""
122
        dirpath = os.path.join('mock', 'directory')
123
        mock_open.side_effect = lambda *args, **kw: mock.mock_open(read_data="$body").return_value
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (98/80).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
124
        expected_calls = [call(os.path.join('mock', 'directory', 'MOCK.html'), 'wb'), call(publisher.HTMLTEMPLATE, 'r')]
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (120/80).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
125
        # Act
126
        dirpath2 = publisher.publish(self.mock_tree, dirpath, index=False)
127
        # Assert
128
        self.assertIs(dirpath, dirpath2)
129
        self.assertEqual(0, mock_index.call_count)
130
        print(mock_open.call_args_list)
131
        self.assertEqual(expected_calls, mock_open.call_args_list)
132
133
    def test_index(self):
134
        """Verify an HTML index can be created."""
135
        # Arrange
136
        path = os.path.join(FILES, 'index.html')
137
        # Act
138
        publisher._index(FILES)  # pylint: disable=W0212
139
        # Assert
140
        self.assertTrue(os.path.isfile(path))
141
142
    def test_index_no_files(self):
143
        """Verify an HTML index is only created when files exist."""
144
        path = os.path.join(EMPTY, 'index.html')
145
        # Act
146
        publisher._index(EMPTY)  # pylint: disable=W0212
147
        # Assert
148
        self.assertFalse(os.path.isfile(path))
149
150
    def test_index_tree(self):
151
        """Verify an HTML index can be created with a tree."""
152
        path = os.path.join(FILES, 'index2.html')
153
        mock_tree = MagicMock()
154
        mock_tree.documents = []
155
        for prefix in ('SYS', 'HLR', 'LLR', 'HLT', 'LLT'):
156
            mock_document = MagicMock()
157
            mock_document.prefix = prefix
158
            mock_tree.documents.append(mock_document)
159
        mock_tree.draw = lambda: "(mock tree structure)"
160
        mock_item = Mock()
161
        mock_item.uid = 'KNOWN-001'
162
        mock_item.document = Mock()
163
        mock_item.document.prefix = 'KNOWN'
164
        mock_item_unknown = Mock(spec=['uid'])
165
        mock_item_unknown.uid = 'UNKNOWN-002'
166
        mock_trace = [
167
            (None, mock_item, None, None, None),
168
            (None, None, None, mock_item_unknown, None),
169
            (None, None, None, None, None),
170
        ]
171
        mock_tree.get_traceability = lambda: mock_trace
172
        # Act
173
        publisher._index(FILES, index="index2.html", tree=mock_tree)  # pylint: disable=W0212
174
        # Assert
175
        self.assertTrue(os.path.isfile(path))
176
177
    def test_lines_text_item(self):
178
        """Verify text can be published from an item."""
179
        with patch.object(self.item5, 'find_ref',
180
                          Mock(return_value=('path/to/mock/file', 42))):
181
            lines = publisher.publish_lines(self.item5, '.txt')
182
            text = ''.join(line + '\n' for line in lines)
183
        self.assertIn("Reference: path/to/mock/file (line 42)", text)
184
185
    def test_lines_text_item_heading(self):
186
        """Verify text can be published from an item (heading)."""
187
        expected = "1.1     Heading\n\n"
188
        lines = publisher.publish_lines(self.item, '.txt')
189
        # Act
190
        text = ''.join(line + '\n' for line in lines)
191
        # Assert
192
        self.assertEqual(expected, text)
193
194
    @patch('doorstop.settings.PUBLISH_HEADING_LEVELS', False)
195
    def test_lines_text_item_heading_no_heading_levels(self):
196
        """Verify an item heading level can be ommitted."""
197
        expected = "Heading\n\n"
198
        lines = publisher.publish_lines(self.item, '.txt')
199
        # Act
200
        text = ''.join(line + '\n' for line in lines)
201
        # Assert
202
        self.assertEqual(expected, text)
203
204
    def test_single_line_heading_to_markdown(self):
205
        """Verify a single line heading is published as a heading with an attribute equal to the item id"""
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (107/80).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
206
        expected = "## 1.1 Heading {#req3 }\n\n"
207
        lines = publisher.publish_lines(self.item, '.md', linkify=True)
208
        # Act
209
        text = ''.join(line + '\n' for line in lines)
210
        # Assert
211
        self.assertEqual(expected, text)
212
213
    def test_multi_line_heading_to_markdown(self):
214
        """Verify a multi line heading is published as a heading with an attribute equal to the item id"""
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (106/80).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
215
        item = MockItemAndVCS('path/to/req3.yml',
216
                              _file=("links: [sys3]" + '\n'
217
                                     "text: 'Heading\n\nThis section describes publishing.'" + '\n'
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (99/80).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
218
                                     "level: 1.1.0" + '\n'
219
                                     "normative: false"))
220
        expected = "## 1.1 Heading {#req3 }\nThis section describes publishing.\n\n"
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (84/80).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
221
        lines = publisher.publish_lines(item, '.md', linkify=True)
222
        # Act
223
        text = ''.join(line + '\n' for line in lines)
224
        # Assert
225
        self.assertEqual(expected, text)
226
227
    @patch('doorstop.settings.PUBLISH_CHILD_LINKS', False)
228
    def test_lines_text_item_normative(self):
229
        """Verify text can be published from an item (normative)."""
230
        expected = ("1.2     req4" + '\n\n'
231
                    "        This shall..." + '\n\n'
232
                    "        Reference: Doorstop.sublime-project" + '\n\n'
233
                    "        Links: sys4" + '\n\n')
234
        lines = publisher.publish_lines(self.item3, '.txt')
235
        # Act
236
        text = ''.join(line + '\n' for line in lines)
237
        # Assert
238
        self.assertEqual(expected, text)
239
240
    @patch('doorstop.settings.CHECK_REF', False)
241
    def test_lines_text_item_no_ref(self):
242
        """Verify text can be published without checking references."""
243
        lines = publisher.publish_lines(self.item5, '.txt')
244
        text = ''.join(line + '\n' for line in lines)
245
        self.assertIn("Reference: 'abc123'", text)
246
247
    @patch('doorstop.settings.PUBLISH_CHILD_LINKS', True)
248
    def test_lines_text_item_with_child_links(self):
249
        """Verify text can be published with child links."""
250
        # Act
251
        lines = publisher.publish_lines(self.item2, '.txt')
252
        text = ''.join(line + '\n' for line in lines)
253
        # Assert
254
        self.assertIn("Child links: tst1", text)
255
256
    def test_lines_markdown_item(self):
257
        """Verify Markdown can be published from an item."""
258
        with patch.object(self.item5, 'find_ref',
259
                          Mock(return_value=('path/to/mock/file', 42))):
260
            lines = publisher.publish_lines(self.item5, '.md')
261
            text = ''.join(line + '\n' for line in lines)
262
        self.assertIn("> `path/to/mock/file` (line 42)", text)
263
264
    def test_lines_markdown_item_heading(self):
265
        """Verify Markdown can be published from an item (heading)."""
266
        expected = "## 1.1 Heading {#req3 }\n\n"
267
        # Act
268
        lines = publisher.publish_lines(self.item, '.md', linkify=True)
269
        text = ''.join(line + '\n' for line in lines)
270
        # Assert
271
        self.assertEqual(expected, text)
272
273
    @patch('doorstop.settings.PUBLISH_HEADING_LEVELS', False)
274
    def test_lines_markdown_item_heading_no_heading_levels(self):
275
        """Verify an item heading level can be ommitted."""
276
        expected = "## Heading {#req3 }\n\n"
277
        # Act
278
        lines = publisher.publish_lines(self.item, '.md', linkify=True)
279
        text = ''.join(line + '\n' for line in lines)
280
        # Assert
281
        self.assertEqual(expected, text)
282
283
    @patch('doorstop.settings.PUBLISH_CHILD_LINKS', False)
284
    def test_lines_markdown_item_normative(self):
285
        """Verify Markdown can be published from an item (normative)."""
286
        expected = ("## 1.2 req4" + '\n\n'
287
                    "This shall..." + '\n\n'
288
                    "> `Doorstop.sublime-project`" + '\n\n'
289
                    "*Links: sys4*" + '\n\n')
290
        # Act
291
        lines = publisher.publish_lines(self.item3, '.md', linkify=False)
292
        text = ''.join(line + '\n' for line in lines)
293
        # Assert
294
        self.assertEqual(expected, text)
295
296
    @patch('doorstop.settings.PUBLISH_CHILD_LINKS', True)
297
    def test_lines_markdown_item_with_child_links(self):
298
        """Verify Markdown can be published from an item w/ child links."""
299
        # Act
300
        lines = publisher.publish_lines(self.item2, '.md')
301
        text = ''.join(line + '\n' for line in lines)
302
        # Assert
303
        self.assertIn("Child links: tst1", text)
304
305
    @patch('doorstop.settings.PUBLISH_CHILD_LINKS', False)
306
    def test_lines_markdown_item_without_child_links(self):
307
        """Verify Markdown can be published from an item w/o child links."""
308
        # Act
309
        lines = publisher.publish_lines(self.item2, '.md')
310
        text = ''.join(line + '\n' for line in lines)
311
        # Assert
312
        self.assertNotIn("Child links", text)
313
314
    @patch('doorstop.settings.PUBLISH_BODY_LEVELS', False)
315
    @patch('doorstop.settings.PUBLISH_CHILD_LINKS', False)
316
    def test_lines_markdown_item_without_body_levels(self):
317
        """Verify Markdown can be published from an item (no body levels)."""
318
        expected = ("## req4" + '\n\n'
319
                    "This shall..." + '\n\n'
320
                    "> `Doorstop.sublime-project`" + '\n\n'
321
                    "*Links: sys4*" + '\n\n')
322
        # Act
323
        lines = publisher.publish_lines(self.item3, '.md', linkify=False)
324
        text = ''.join(line + '\n' for line in lines)
325
        # Assert
326
        self.assertEqual(expected, text)
327
328
    @patch('doorstop.settings.CHECK_REF', False)
329
    def test_lines_markdown_item_no_ref(self):
330
        """Verify Markdown can be published without checking references."""
331
        lines = publisher.publish_lines(self.item5, '.md')
332
        text = ''.join(line + '\n' for line in lines)
333
        self.assertIn("> 'abc123'", text)
334
335
    def test_lines_html_item(self):
336
        """Verify HTML can be published from an item."""
337
        expected = '<h2>1.1 Heading</h2>\n'
338
        # Act
339
        lines = publisher.publish_lines(self.item, '.html')
340
        text = ''.join(line + '\n' for line in lines)
341
        # Assert
342
        self.assertEqual(expected, text)
343
344
    @patch('doorstop.settings.PUBLISH_HEADING_LEVELS', False)
345
    def test_lines_html_item_no_heading_levels(self):
346
        """Verify an item heading level can be ommitted."""
347
        expected = '<h2>Heading</h2>\n'
348
        # Act
349
        lines = publisher.publish_lines(self.item, '.html')
350
        text = ''.join(line + '\n' for line in lines)
351
        # Assert
352
        self.assertEqual(expected, text)
353
354
    def test_lines_html_item_linkify(self):
355
        """Verify HTML (hyper) can be published from an item."""
356
        expected = '<h2 id="req3">1.1 Heading</h2>\n'
357
        # Act
358
        lines = publisher.publish_lines(self.item, '.html', linkify=True)
359
        text = ''.join(line + '\n' for line in lines)
360
        # Assert
361
        self.assertEqual(expected, text)
362
363
    @patch('doorstop.settings.PUBLISH_CHILD_LINKS', True)
364
    def test_lines_html_item_with_child_links(self):
365
        """Verify HTML can be published from an item w/ child links."""
366
        # Act
367
        lines = publisher.publish_lines(self.item2, '.html')
368
        text = ''.join(line + '\n' for line in lines)
369
        # Assert
370
        self.assertIn("Child links: tst1", text)
371
372
    @patch('doorstop.settings.PUBLISH_CHILD_LINKS', False)
373
    def test_lines_html_item_without_child_links(self):
374
        """Verify HTML can be published from an item w/o child links."""
375
        # Act
376
        lines = publisher.publish_lines(self.item2, '.html')
377
        text = ''.join(line + '\n' for line in lines)
378
        # Assert
379
        self.assertNotIn("Child links", text)
380
381
    @patch('doorstop.settings.PUBLISH_CHILD_LINKS', True)
382
    def test_lines_html_item_with_child_links_linkify(self):
383
        """Verify HTML (hyper) can be published from an item w/ child links."""
384
        # Act
385
        lines = publisher.publish_lines(self.item2, '.html', linkify=True)
386
        text = ''.join(line + '\n' for line in lines)
387
        # Assert
388
        self.assertIn("Child links:", text)
389
        self.assertIn("tst.html#tst1", text)
390
391
    def test_lines_unknown(self):
392
        """Verify an exception is raised when iterating an unknown format."""
393
        # Act
394
        gen = publisher.publish_lines(self.document, '.a')
395
        # Assert
396
        self.assertRaises(DoorstopError, list, gen)
397
398
399
@patch('doorstop.core.item.Item', MockItem)
400
class TestTableOfContents(unittest.TestCase):
401
    """Unit tests for the Document class."""  # pylint: disable=W0212
402
403
    def setUp(self):
404
        self.document = MockDocument(FILES, root=ROOT)
405
406
    def test_toc_no_links_or_heading_levels(self):
407
        """Verify the table of contents is generated with heading levels"""
408
        expected = '''### Table of Contents
409
410
        * 1.2.3 REQ001
411
    * 1.4 REQ003
412
    * 1.6 REQ004
413
    * 2.1 REQ002
414
    * 2.1 REQ2-001\n'''
415
        toc = publisher._table_of_contents_md(self.document, linkify=None)
416
        print(toc)
417
        self.assertEqual(expected, toc)
418
419
    @patch('doorstop.settings.PUBLISH_HEADING_LEVELS', False)
420
    def test_toc_no_links(self):
421
        """Verify the table of contents is generated without heading levels"""
422
        expected = '''### Table of Contents
423
424
        * REQ001
425
    * REQ003
426
    * REQ004
427
    * REQ002
428
    * REQ2-001\n'''
429
        toc = publisher._table_of_contents_md(self.document, linkify=None)
430
        print(toc)
431
        self.assertEqual(expected, toc)
432
433
    def test_toc(self):
434
        """Verify the table of contents is generated with an ID for the heading"""
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (82/80).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
435
        expected = '''### Table of Contents
436
437
        * [1.2.3 REQ001](#REQ001)
438
    * [1.4 REQ003](#REQ003)
439
    * [1.6 REQ004](#REQ004)
440
    * [2.1 REQ002](#REQ002)
441
    * [2.1 REQ2-001](#REQ2-001)\n'''
442
        toc = publisher._table_of_contents_md(self.document, linkify=True)
443
        self.assertEqual(expected, toc)
444