Completed
Pull Request — develop (#227)
by
unknown
03:08
created

TestTableOfContents.test_toc_no_links()   A

Complexity

Conditions 1

Size

Total Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
c 1
b 0
f 0
dl 0
loc 13
rs 9.4285
1
"""Unit tests for the doorstop.core.publisher module."""
2
3
import unittest
4
from unittest.mock import patch, Mock, MagicMock, mock_open, call
5
6
import os
7
from doorstop.common import DoorstopError
8
from doorstop.core import publisher
9
10
from doorstop.core.test import (FILES, EMPTY, ROOT, MockDataMixIn,
11
                                MockItemAndVCS, MockItem, MockDocument)
12
13
14
class TestModule(MockDataMixIn, unittest.TestCase):
15
    """Unit tests for the doorstop.core.publisher module."""
16
17
    @patch('os.makedirs')
18
    @patch('builtins.open')
19
    def test_publish_document(self, mock_open, mock_makedirs):
0 ignored issues
show
Comprehensibility Bug introduced by
mock_open is re-defining a name which is already available in the outer-scope (previously defined on line 4).

It is generally a bad practice to shadow variables from the outer-scope. In most cases, this is done unintentionally and might lead to unexpected behavior:

param = 5

class Foo:
    def __init__(self, param):   # "param" would be flagged here
        self.param = param
Loading history...
20
        """Verify a document can be published."""
21
        dirpath = os.path.join('mock', 'directory')
22
        path = os.path.join(dirpath, 'published.html')
23
        self.document.items = []
24
        # Act
25
        path2 = publisher.publish(self.document, path)
26
        # Assert
27
        self.assertIs(path, path2)
28
        mock_makedirs.assert_called_once_with(dirpath)
29
        mock_open.assert_called_once_with(path, 'wb')
30
31
    @patch('os.makedirs')
32
    @patch('builtins.open')
33
    @patch('doorstop.core.publisher.publish_lines')
34
    def test_publish_document_html(self, mock_lines, mock_open, mock_makedirs):
0 ignored issues
show
Comprehensibility Bug introduced by
mock_open is re-defining a name which is already available in the outer-scope (previously defined on line 4).

It is generally a bad practice to shadow variables from the outer-scope. In most cases, this is done unintentionally and might lead to unexpected behavior:

param = 5

class Foo:
    def __init__(self, param):   # "param" would be flagged here
        self.param = param
Loading history...
35
        """Verify a (mock) HTML file can be created."""
36
        dirpath = os.path.join('mock', 'directory')
37
        path = os.path.join(dirpath, 'published.custom')
38
        # Act
39
        path2 = publisher.publish(self.document, path, '.html')
40
        # Assert
41
        self.assertIs(path, path2)
42
        mock_makedirs.assert_called_once_with(dirpath)
43
        mock_open.assert_called_once_with(path, 'wb')
44
        mock_lines.assert_called_once_with(self.document, '.html',
45
                                           template=None,
46
                                           toc=None,
47
                                           linkify=False)
48
49
    def test_publish_document_unknown(self):
50
        """Verify an exception is raised when publishing unknown formats."""
51
        self.assertRaises(DoorstopError,
52
                          publisher.publish, self.document, 'a.a')
53
        self.assertRaises(DoorstopError,
54
                          publisher.publish, self.document, 'a.txt', '.a')
55
56 View Code Duplication
    @patch('doorstop.core.publisher._index')
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
57
    @patch('os.makedirs')
58
    @patch('builtins.open')
59
    def test_publish_tree(self, mockopen, mock_makedirs, mock_index):
60
        """Verify a tree can be published."""
61
        dirpath = os.path.join('mock', 'directory')
62
        mockopen.side_effect = lambda *args, **kw: 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 (92/80).

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

Loading history...
63
        expected_calls = [call('mock/directory/MOCK.html', 'wb'), call('some_path', 'r')]
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (89/80).

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

Loading history...
64
        # Act
65
        dirpath2 = publisher.publish(self.mock_tree, dirpath, template='some_path')
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (83/80).

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

Loading history...
66
        # Assert
67
        self.assertIs(dirpath, dirpath2)
68
        self.assertEqual(1, mock_makedirs.call_count)
69
        self.assertEqual(2, mockopen.call_count)
70
        mock_index.assert_called_once_with(dirpath, tree=self.mock_tree)
71
        self.assertEqual(expected_calls, mockopen.call_args_list)
72
73 View Code Duplication
    @patch('doorstop.core.publisher._index')
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
74
    @patch('os.makedirs')
75
    @patch('builtins.open')
76
    def test_publish_tree_no_index(self, mockopen, mock_makedirs, mock_index):
77
        """Verify a tree can be published."""
78
        dirpath = os.path.join('mock', 'directory')
79
        mockopen.side_effect = lambda *args, **kw: 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 (92/80).

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

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

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

Loading history...
81
        # Act
82
        dirpath2 = publisher.publish(self.mock_tree, dirpath, index=False,
83
                                     template='path_to_some_template')
84
        # Assert
85
        self.assertIs(dirpath, dirpath2)
86
        self.assertEqual(1, mock_makedirs.call_count)
87
        self.assertEqual(2, mockopen.call_count)
88
        self.assertEqual(0, mock_index.call_count)
89
        self.assertEqual(expected_calls, mockopen.call_args_list)
90
91
    @patch('doorstop.core.publisher._index')
92
    def test_publish_tree_no_documents(self, mock_index):
93
        """Verify a tree can be published with no documents."""
94
        dirpath = os.path.join('mock', 'directory')
95
        mock_tree = MagicMock()
96
        mock_tree.documents = []
97
        # Act
98
        path2 = publisher.publish(mock_tree, dirpath, index=False)
99
        # Assert
100
        self.assertIs(None, path2)
101
        self.assertEqual(0, mock_index.call_count)
102
103
    def test_index(self):
104
        """Verify an HTML index can be created."""
105
        # Arrange
106
        path = os.path.join(FILES, 'index.html')
107
        # Act
108
        publisher._index(FILES)  # pylint: disable=W0212
109
        # Assert
110
        self.assertTrue(os.path.isfile(path))
111
112
    def test_index_no_files(self):
113
        """Verify an HTML index is only created when files exist."""
114
        path = os.path.join(EMPTY, 'index.html')
115
        # Act
116
        publisher._index(EMPTY)  # pylint: disable=W0212
117
        # Assert
118
        self.assertFalse(os.path.isfile(path))
119
120
    def test_index_tree(self):
121
        """Verify an HTML index can be created with a tree."""
122
        path = os.path.join(FILES, 'index2.html')
123
        mock_tree = MagicMock()
124
        mock_tree.documents = []
125
        for prefix in ('SYS', 'HLR', 'LLR', 'HLT', 'LLT'):
126
            mock_document = MagicMock()
127
            mock_document.prefix = prefix
128
            mock_tree.documents.append(mock_document)
129
        mock_tree.draw = lambda: "(mock tree structure)"
130
        mock_item = Mock()
131
        mock_item.uid = 'KNOWN-001'
132
        mock_item.document = Mock()
133
        mock_item.document.prefix = 'KNOWN'
134
        mock_item_unknown = Mock(spec=['uid'])
135
        mock_item_unknown.uid = 'UNKNOWN-002'
136
        mock_trace = [
137
            (None, mock_item, None, None, None),
138
            (None, None, None, mock_item_unknown, None),
139
            (None, None, None, None, None),
140
        ]
141
        mock_tree.get_traceability = lambda: mock_trace
142
        # Act
143
        publisher._index(FILES, index="index2.html", tree=mock_tree)  # pylint: disable=W0212
144
        # Assert
145
        self.assertTrue(os.path.isfile(path))
146
147
    def test_lines_text_item(self):
148
        """Verify text can be published from an item."""
149
        with patch.object(self.item5, 'find_ref',
150
                          Mock(return_value=('path/to/mock/file', 42))):
151
            lines = publisher.publish_lines(self.item5, '.txt')
152
            text = ''.join(line + '\n' for line in lines)
153
        self.assertIn("Reference: path/to/mock/file (line 42)", text)
154
155
    def test_lines_text_item_heading(self):
156
        """Verify text can be published from an item (heading)."""
157
        expected = "1.1     Heading\n\n"
158
        lines = publisher.publish_lines(self.item, '.txt')
159
        # Act
160
        text = ''.join(line + '\n' for line in lines)
161
        # Assert
162
        self.assertEqual(expected, text)
163
164
    @patch('doorstop.settings.PUBLISH_HEADING_LEVELS', False)
165
    def test_lines_text_item_heading_no_heading_levels(self):
166
        """Verify an item heading level can be ommitted."""
167
        expected = "Heading\n\n"
168
        lines = publisher.publish_lines(self.item, '.txt')
169
        # Act
170
        text = ''.join(line + '\n' for line in lines)
171
        # Assert
172
        self.assertEqual(expected, text)
173
174
    def test_single_line_heading_to_markdown(self):
175
        """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 (100/80).

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

Loading history...
176
        expected = "## 1.1 Heading {#req3 }\n\n"
177
        lines = publisher.publish_lines(self.item, '.md', linkify=True)
178
        # Act
179
        text = ''.join(line + '\n' for line in lines)
180
        # Assert
181
        self.assertEqual(expected, text)
182
183
    def test_multi_line_heading_to_markdown(self):
184
        """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 (99/80).

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

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