1
|
|
|
"""Tests for the doorstop.core package.""" |
2
|
|
|
|
3
|
|
|
import unittest |
4
|
|
|
from unittest.mock import patch, Mock |
5
|
|
|
|
6
|
|
|
import os |
7
|
|
|
import csv |
8
|
|
|
import tempfile |
9
|
|
|
import shutil |
10
|
|
|
import pprint |
11
|
|
|
import logging |
12
|
|
|
import warnings |
13
|
|
|
|
14
|
|
|
import yaml |
15
|
|
|
import openpyxl |
16
|
|
|
|
17
|
|
|
from doorstop import common |
18
|
|
|
from doorstop.common import DoorstopError, DoorstopWarning, DoorstopInfo |
19
|
|
|
from doorstop import core |
20
|
|
|
from doorstop.core.builder import _get_tree, _clear_tree |
21
|
|
|
from doorstop.core.vcs import mockvcs |
22
|
|
|
|
23
|
|
|
from doorstop.core.test import ENV, REASON, ROOT, FILES, EMPTY, SYS |
24
|
|
|
from doorstop.core.test import DocumentNoSkip |
25
|
|
|
|
26
|
|
|
# Whenever the export format is changed: |
27
|
|
|
# 1. set CHECK_EXPORTED_CONTENT to False |
28
|
|
|
# 2. re-run all tests |
29
|
|
|
# 3. manually verify the newly exported content is correct |
30
|
|
|
# 4. set CHECK_EXPORTED_CONTENT to True |
31
|
|
|
CHECK_EXPORTED_CONTENT = True |
32
|
|
|
|
33
|
|
|
# Whenever the publish format is changed: |
34
|
|
|
# 1. set CHECK_PUBLISHED_CONTENT to False |
35
|
|
|
# 2. re-run all tests |
36
|
|
|
# 3. manually verify the newly published content is correct |
37
|
|
|
# 4. set CHECK_PUBLISHED_CONTENT to True |
38
|
|
|
CHECK_PUBLISHED_CONTENT = True |
39
|
|
|
|
40
|
|
|
|
41
|
|
|
class TestItem(unittest.TestCase): |
42
|
|
|
"""Integration tests for the Item class.""" |
43
|
|
|
|
44
|
|
|
def setUp(self): |
45
|
|
|
self.path = os.path.join(FILES, 'REQ001.yml') |
46
|
|
|
self.backup = common.read_text(self.path) |
47
|
|
|
self.item = core.Item(self.path) |
48
|
|
|
self.item.tree = Mock() |
49
|
|
|
self.item.tree.vcs = mockvcs.WorkingCopy(EMPTY) |
50
|
|
|
|
51
|
|
|
def tearDown(self): |
52
|
|
|
common.write_text(self.backup, self.path) |
53
|
|
|
|
54
|
|
|
def test_save_load(self): |
55
|
|
|
"""Verify an item can be saved and loaded from a file.""" |
56
|
|
|
self.item.level = '1.2.3' |
57
|
|
|
self.item.text = "Hello, world!" |
58
|
|
|
self.item.links = ['SYS001', 'SYS002'] |
59
|
|
|
item2 = core.Item(os.path.join(FILES, 'REQ001.yml')) |
60
|
|
|
self.assertEqual((1, 2, 3), item2.level) |
61
|
|
|
self.assertEqual("Hello, world!", item2.text) |
62
|
|
|
self.assertEqual(['SYS001', 'SYS002'], item2.links) |
63
|
|
|
|
64
|
|
|
@unittest.skipUnless(os.getenv(ENV), REASON) |
65
|
|
|
def test_find_ref(self): |
66
|
|
|
"""Verify an item's external reference can be found.""" |
67
|
|
|
item = core.Item(os.path.join(FILES, 'REQ003.yml')) |
68
|
|
|
item.tree = Mock() |
69
|
|
|
item.tree.vcs = mockvcs.WorkingCopy(ROOT) |
70
|
|
|
path, line = item.find_ref() |
71
|
|
|
relpath = os.path.relpath(os.path.join(FILES, 'external', 'text.txt'), |
72
|
|
|
ROOT) |
73
|
|
|
self.assertEqual(relpath, path) |
74
|
|
|
self.assertEqual(3, line) |
75
|
|
|
|
76
|
|
|
def test_find_ref_error(self): |
77
|
|
|
"""Verify an error occurs when no external reference found.""" |
78
|
|
|
self.item.ref = "not" "found" # space avoids self match |
79
|
|
|
self.assertRaises(DoorstopError, self.item.find_ref) |
80
|
|
|
|
81
|
|
|
|
82
|
|
|
class TestDocument(unittest.TestCase): |
83
|
|
|
"""Integration tests for the Document class.""" |
84
|
|
|
|
85
|
|
|
def setUp(self): |
86
|
|
|
self.document = core.Document(FILES, root=ROOT) |
87
|
|
|
|
88
|
|
|
def tearDown(self): |
89
|
|
|
"""Clean up temporary files.""" |
90
|
|
|
for filename in os.listdir(EMPTY): |
91
|
|
|
path = os.path.join(EMPTY, filename) |
92
|
|
|
common.delete(path) |
93
|
|
|
|
94
|
|
|
def test_load(self): |
95
|
|
|
"""Verify a document can be loaded from a directory.""" |
96
|
|
|
doc = core.Document(FILES) |
97
|
|
|
self.assertEqual('REQ', doc.prefix) |
98
|
|
|
self.assertEqual(2, doc.digits) |
99
|
|
|
self.assertEqual(5, len(doc.items)) |
100
|
|
|
|
101
|
|
|
def test_new(self): |
102
|
|
|
"""Verify a new document can be created.""" |
103
|
|
|
document = core.Document.new(None, |
104
|
|
|
EMPTY, FILES, |
105
|
|
|
prefix='SYS', digits=4) |
106
|
|
|
self.assertEqual('SYS', document.prefix) |
107
|
|
|
self.assertEqual(4, document.digits) |
108
|
|
|
self.assertEqual(0, len(document.items)) |
109
|
|
|
|
110
|
|
|
@patch('doorstop.settings.REORDER', False) |
111
|
|
|
@patch('doorstop.settings.REVIEW_NEW_ITEMS', False) |
112
|
|
|
def test_validate(self): |
113
|
|
|
"""Verify a document can be validated.""" |
114
|
|
|
self.assertTrue(self.document.validate()) |
115
|
|
|
|
116
|
|
|
@patch('doorstop.settings.REORDER', False) |
117
|
|
|
@patch('doorstop.settings.REVIEW_NEW_ITEMS', False) |
118
|
|
|
def test_issues_count(self): |
119
|
|
|
"""Verify a number of issues are found in a document.""" |
120
|
|
|
issues = self.document.issues |
121
|
|
|
for issue in self.document.issues: |
122
|
|
|
logging.info(repr(issue)) |
123
|
|
|
self.assertEqual(12, len(issues)) |
124
|
|
|
|
125
|
|
|
@patch('doorstop.settings.REORDER', False) |
126
|
|
|
@patch('doorstop.settings.REVIEW_NEW_ITEMS', False) |
127
|
|
|
def test_issues_duplicate_level(self): |
128
|
|
|
"""Verify duplicate item levels are detected.""" |
129
|
|
|
expect = DoorstopWarning("duplicate level: 2.1 (REQ002, REQ2-001)") |
130
|
|
|
for issue in self.document.issues: |
131
|
|
|
logging.info(repr(issue)) |
132
|
|
|
if type(issue) == type(expect) and issue.args == expect.args: |
133
|
|
|
break |
134
|
|
|
else: |
135
|
|
|
self.fail("issue not found: {}".format(expect)) |
136
|
|
|
|
137
|
|
|
@patch('doorstop.settings.REORDER', False) |
138
|
|
|
@patch('doorstop.settings.REVIEW_NEW_ITEMS', False) |
139
|
|
|
def test_issues_skipped_level(self): |
140
|
|
|
"""Verify skipped item levels are detected.""" |
141
|
|
|
expect = DoorstopInfo("skipped level: 1.4 (REQ003), 1.6 (REQ004)") |
142
|
|
|
for issue in self.document.issues: |
143
|
|
|
logging.info(repr(issue)) |
144
|
|
|
if type(issue) == type(expect) and issue.args == expect.args: |
145
|
|
|
break |
146
|
|
|
else: |
147
|
|
|
self.fail("issue not found: {}".format(expect)) |
148
|
|
|
|
149
|
|
|
def test_add_item_with_reordering(self): |
150
|
|
|
"""Verify an item can be inserted into a document.""" |
151
|
|
|
document = core.Document.new(None, |
152
|
|
|
EMPTY, FILES, |
153
|
|
|
prefix='TMP') |
154
|
|
|
item_1_0 = document.add_item() |
155
|
|
|
item_1_2 = document.add_item() # will get displaced |
156
|
|
|
item_1_1 = document.add_item(level='1.1') |
157
|
|
|
self.assertEqual((1, 0), item_1_0.level) |
158
|
|
|
self.assertEqual((1, 1), item_1_1.level) |
159
|
|
|
self.assertEqual((1, 2), item_1_2.level) |
160
|
|
|
|
161
|
|
|
def test_remove_item_with_reordering(self): |
162
|
|
|
"""Verify an item can be removed from a document.""" |
163
|
|
|
document = core.Document.new(None, |
164
|
|
|
EMPTY, FILES, |
165
|
|
|
prefix='TMP') |
166
|
|
|
item_1_0 = document.add_item() |
167
|
|
|
item_1_2 = document.add_item() # to be removed |
168
|
|
|
item_1_1 = document.add_item(level='1.1') # will get relocated |
169
|
|
|
document.remove_item(item_1_2) |
170
|
|
|
self.assertEqual((1, 0), item_1_0.level) |
171
|
|
|
self.assertEqual((1, 1), item_1_1.level) |
172
|
|
|
|
173
|
|
View Code Duplication |
def test_reorder(self): |
|
|
|
|
174
|
|
|
"""Verify a document's order can be corrected.""" |
175
|
|
|
document = core.Document.new(None, |
176
|
|
|
EMPTY, FILES, |
177
|
|
|
prefix='TMP') |
178
|
|
|
document.add_item(level='2.0', reorder=False) |
179
|
|
|
document.add_item(level='2.1', reorder=False) |
180
|
|
|
document.add_item(level='2.1', reorder=False) |
181
|
|
|
document.add_item(level='2.5', reorder=False) |
182
|
|
|
document.add_item(level='4.5', reorder=False) |
183
|
|
|
document.add_item(level='4.7', reorder=False) |
184
|
|
|
document.reorder() |
185
|
|
|
expected = [(2, 0), (2, 1), (2, 2), (2, 3), (3, 1), (3, 2)] |
186
|
|
|
actual = [item.level for item in document.items] |
187
|
|
|
self.assertListEqual(expected, actual) |
188
|
|
|
|
189
|
|
|
def test_reorder_with_keep(self): |
190
|
|
|
"""Verify a document's order can be corrected with a kept level.""" |
191
|
|
|
document = core.Document.new(None, |
192
|
|
|
EMPTY, FILES, |
193
|
|
|
prefix='TMP') |
194
|
|
|
document.add_item(level='1.0', reorder=False) |
195
|
|
|
item = document.add_item(level='1.0', reorder=False) |
196
|
|
|
document.add_item(level='1.0', reorder=False) |
197
|
|
|
document.reorder(keep=item) |
198
|
|
|
expected = [(1, 0), (2, 0), (3, 0)] |
199
|
|
|
actual = [i.level for i in document.items] |
200
|
|
|
self.assertListEqual(expected, actual) |
201
|
|
|
self.assertEqual((1, 0), item.level) |
202
|
|
|
|
203
|
|
View Code Duplication |
def test_reorder_with_start(self): |
|
|
|
|
204
|
|
|
"""Verify a document's order can be corrected with a given start.""" |
205
|
|
|
document = core.Document.new(None, |
206
|
|
|
EMPTY, FILES, |
207
|
|
|
prefix='TMP') |
208
|
|
|
document.add_item(level='2.0', reorder=False) |
209
|
|
|
document.add_item(level='2.1', reorder=False) |
210
|
|
|
document.add_item(level='2.1', reorder=False) |
211
|
|
|
document.add_item(level='2.5', reorder=False) |
212
|
|
|
document.add_item(level='4.0', reorder=False) |
213
|
|
|
document.add_item(level='4.7', reorder=False) |
214
|
|
|
document.reorder(start=(1, 0)) |
215
|
|
|
expected = [(1, 0), (1, 1), (1, 2), (1, 3), (2, 0), (2, 1)] |
216
|
|
|
actual = [item.level for item in document.items] |
217
|
|
|
self.assertListEqual(expected, actual) |
218
|
|
|
|
219
|
|
View Code Duplication |
@patch('doorstop.settings.REORDER', True) |
|
|
|
|
220
|
|
|
@patch('doorstop.settings.REVIEW_NEW_ITEMS', False) |
221
|
|
|
def test_validate_with_reordering(self): |
222
|
|
|
"""Verify a document's order is corrected during validation.""" |
223
|
|
|
document = core.Document.new(None, |
224
|
|
|
EMPTY, FILES, |
225
|
|
|
prefix='TMP') |
226
|
|
|
document.add_item(level='1.0', reorder=False) |
227
|
|
|
document.add_item(level='1.1', reorder=False) |
228
|
|
|
document.add_item(level='1.2.0', reorder=False) |
229
|
|
|
document.add_item(level='1.2.5', reorder=False) |
230
|
|
|
document.add_item(level='3.2.1', reorder=False) |
231
|
|
|
document.add_item(level='3.3', reorder=False) |
232
|
|
|
self.assertTrue(document.validate()) |
233
|
|
|
expected = [(1, 0), (1, 1), (1, 2, 0), (1, 2, 1), (2, 1, 1), (2, 2)] |
234
|
|
|
actual = [item.level for item in document.items] |
235
|
|
|
self.assertListEqual(expected, actual) |
236
|
|
|
|
237
|
|
|
|
238
|
|
|
class TestTree(unittest.TestCase): |
239
|
|
|
"""Integration tests for the core.Tree class.""" |
240
|
|
|
|
241
|
|
|
def setUp(self): |
242
|
|
|
self.path = os.path.join(FILES, 'REQ001.yml') |
243
|
|
|
self.backup = common.read_text(self.path) |
244
|
|
|
self.item = core.Item(self.path) |
245
|
|
|
self.tree = core.Tree(core.Document(SYS)) |
246
|
|
|
self.tree._place(core.Document(FILES)) # pylint: disable=W0212 |
247
|
|
|
|
248
|
|
|
def tearDown(self): |
249
|
|
|
common.write_text(self.backup, self.path) |
250
|
|
|
|
251
|
|
|
@patch('doorstop.settings.REORDER', False) |
252
|
|
|
@patch('doorstop.settings.REVIEW_NEW_ITEMS', False) |
253
|
|
|
@patch('doorstop.settings.STAMP_NEW_LINKS', False) |
254
|
|
|
@patch('doorstop.settings.CHECK_REF', False) |
255
|
|
|
def test_issues_count(self): |
256
|
|
|
"""Verify a number of issues are found in a tree.""" |
257
|
|
|
issues = self.tree.issues |
258
|
|
|
for issue in self.tree.issues: |
259
|
|
|
logging.info(repr(issue)) |
260
|
|
|
self.assertEqual(14, len(issues)) |
261
|
|
|
|
262
|
|
|
@patch('doorstop.settings.REORDER', False) |
263
|
|
|
@patch('doorstop.settings.REVIEW_NEW_ITEMS', False) |
264
|
|
|
@patch('doorstop.settings.STAMP_NEW_LINKS', False) |
265
|
|
|
@patch('doorstop.settings.CHECK_REF', False) |
266
|
|
|
def test_issues_count_with_skips(self): |
267
|
|
|
"""Verify a document can be skipped during validation.""" |
268
|
|
|
issues = list(self.tree.get_issues(skip=['req'])) |
269
|
|
|
for issue in self.tree.issues: |
270
|
|
|
logging.info(repr(issue)) |
271
|
|
|
self.assertEqual(2, len(issues)) |
272
|
|
|
|
273
|
|
|
@patch('doorstop.settings.REORDER', False) |
274
|
|
|
@patch('doorstop.settings.STAMP_NEW_LINKS', False) |
275
|
|
|
@patch('doorstop.settings.REVIEW_NEW_ITEMS', False) |
276
|
|
|
@patch('doorstop.core.document.Document', DocumentNoSkip) |
277
|
|
|
def test_validate_invalid_link(self): |
278
|
|
|
"""Verify a tree is invalid with a bad link.""" |
279
|
|
|
self.item.link('SYS003') |
280
|
|
|
tree = core.build(FILES, root=FILES) |
281
|
|
|
self.assertIsInstance(tree, core.Tree) |
282
|
|
|
self.assertFalse(tree.validate()) |
283
|
|
|
|
284
|
|
|
@patch('doorstop.settings.REORDER', False) |
285
|
|
|
@patch('doorstop.settings.REVIEW_NEW_ITEMS', False) |
286
|
|
|
def test_validate_long(self): |
287
|
|
|
"""Verify trees can be checked.""" |
288
|
|
|
logging.info("tree: {}".format(self.tree)) |
|
|
|
|
289
|
|
|
self.assertTrue(self.tree.validate()) |
290
|
|
|
|
291
|
|
|
|
292
|
|
|
@unittest.skipUnless(os.getenv(ENV), REASON) |
293
|
|
|
class TestEditor(unittest.TestCase): |
294
|
|
|
"""Integrations tests for the editor module.""" |
295
|
|
|
|
296
|
|
|
|
297
|
|
|
class TestImporter(unittest.TestCase): |
|
|
|
|
298
|
|
|
"""Integrations tests for the importer module.""" |
299
|
|
|
|
300
|
|
|
def setUp(self): |
301
|
|
|
# Create a temporary mock working copy |
302
|
|
|
self.cwd = os.getcwd() |
303
|
|
|
self.temp = tempfile.mkdtemp() |
304
|
|
|
os.chdir(self.temp) |
305
|
|
|
common.touch('.mockvcs') |
306
|
|
|
# Create default document attributes |
307
|
|
|
self.prefix = 'PREFIX' |
308
|
|
|
self.root = self.temp |
309
|
|
|
self.path = os.path.join(self.root, 'DIRECTORY') |
310
|
|
|
self.parent = 'PARENT_PREFIX' |
311
|
|
|
# Create default item attributes |
312
|
|
|
self.uid = 'PREFIX-00042' |
313
|
|
|
# Load an actual document |
314
|
|
|
self.document = core.Document(FILES, root=ROOT) |
315
|
|
|
# Ensure the tree is reloaded |
316
|
|
|
_clear_tree() |
317
|
|
|
|
318
|
|
|
def tearDown(self): |
319
|
|
|
os.chdir(self.cwd) |
320
|
|
|
shutil.rmtree(self.temp) |
321
|
|
|
|
322
|
|
|
def test_import_yml(self): |
323
|
|
|
"""Verify items can be imported from a YAML file.""" |
324
|
|
|
path = os.path.join(self.temp, 'exported.yml') |
325
|
|
|
core.exporter.export(self.document, path) |
326
|
|
|
_path = os.path.join(self.temp, 'imports', 'req') |
327
|
|
|
_tree = _get_tree() |
328
|
|
|
document = _tree.create_document(_path, 'REQ') |
329
|
|
|
# Act |
330
|
|
|
core.importer.import_file(path, document) |
331
|
|
|
# Assert |
332
|
|
|
expected = [item.data for item in self.document.items] |
333
|
|
|
actual = [item.data for item in document.items] |
334
|
|
|
log_data(expected, actual) |
335
|
|
|
self.assertListEqual(expected, actual) |
336
|
|
|
|
337
|
|
View Code Duplication |
def test_import_csv(self): |
|
|
|
|
338
|
|
|
"""Verify items can be imported from a CSV file.""" |
339
|
|
|
path = os.path.join(self.temp, 'exported.csv') |
340
|
|
|
core.exporter.export(self.document, path) |
341
|
|
|
_path = os.path.join(self.temp, 'imports', 'req') |
342
|
|
|
_tree = _get_tree() |
343
|
|
|
document = _tree.create_document(_path, 'REQ') |
344
|
|
|
# Act |
345
|
|
|
core.importer.import_file(path, document) |
346
|
|
|
# Assert |
347
|
|
|
expected = [item.data for item in self.document.items] |
348
|
|
|
actual = [item.data for item in document.items] |
349
|
|
|
log_data(expected, actual) |
350
|
|
|
self.assertListEqual(expected, actual) |
351
|
|
|
|
352
|
|
|
def test_import_tsv(self): |
353
|
|
|
"""Verify items can be imported from a TSV file.""" |
354
|
|
|
path = os.path.join(self.temp, 'exported.tsv') |
355
|
|
|
core.exporter.export(self.document, path) |
356
|
|
|
_path = os.path.join(self.temp, 'imports', 'req') |
357
|
|
|
_tree = _get_tree() |
358
|
|
|
document = _tree.create_document(_path, 'REQ') |
359
|
|
|
# Act |
360
|
|
|
core.importer.import_file(path, document) |
361
|
|
|
# Assert |
362
|
|
|
expected = [item.data for item in self.document.items] |
363
|
|
|
actual = [item.data for item in document.items] |
364
|
|
|
log_data(expected, actual) |
365
|
|
|
self.assertListEqual(expected, actual) |
366
|
|
|
|
367
|
|
View Code Duplication |
@unittest.skipUnless(os.getenv(ENV), REASON) |
|
|
|
|
368
|
|
|
def test_import_xlsx(self): |
369
|
|
|
"""Verify items can be imported from an XLSX file.""" |
370
|
|
|
path = os.path.join(self.temp, 'exported.xlsx') |
371
|
|
|
core.exporter.export(self.document, path) |
372
|
|
|
_path = os.path.join(self.temp, 'imports', 'req') |
373
|
|
|
_tree = _get_tree() |
374
|
|
|
document = _tree.create_document(_path, 'REQ') |
375
|
|
|
# Act |
376
|
|
|
core.importer.import_file(path, document) |
377
|
|
|
# Assert |
378
|
|
|
expected = [item.data for item in self.document.items] |
379
|
|
|
actual = [item.data for item in document.items] |
380
|
|
|
log_data(expected, actual) |
381
|
|
|
self.assertListEqual(expected, actual) |
382
|
|
|
|
383
|
|
|
# TODO: determine when this test should be run (if at all) |
384
|
|
|
# currently, 'TEST_LONG' isn't set under any condition |
385
|
|
|
@unittest.skipUnless(os.getenv(ENV), REASON) |
386
|
|
|
@unittest.skipUnless(os.getenv('TEST_LONG'), "this test takes too long") |
387
|
|
|
@unittest.skipIf(os.getenv('TRAVIS'), "this test takes too long") |
388
|
|
|
def test_import_xlsx_huge(self): |
389
|
|
|
"""Verify huge XLSX files are handled.""" |
390
|
|
|
path = os.path.join(FILES, 'exported-huge.xlsx') |
391
|
|
|
_path = os.path.join(self.temp, 'imports', 'req') |
392
|
|
|
_tree = _get_tree() |
393
|
|
|
document = _tree.create_document(_path, 'REQ') |
394
|
|
|
# Act |
395
|
|
|
with warnings.catch_warnings(record=True) as warns: |
396
|
|
|
core.importer.import_file(path, document) |
397
|
|
|
# Assert |
398
|
|
|
self.assertEqual(1, len(warns)) |
399
|
|
|
self.assertIn("maximum number of rows", str(warns[-1].message)) |
400
|
|
|
expected = [] |
401
|
|
|
actual = [item.data for item in document.items] |
402
|
|
|
log_data(expected, actual) |
403
|
|
|
self.assertListEqual(expected, actual) |
404
|
|
|
|
405
|
|
|
def test_create_document(self): |
406
|
|
|
"""Verify a new document can be created to import items.""" |
407
|
|
|
document = core.importer.create_document(self.prefix, self.path) |
408
|
|
|
self.assertEqual(self.prefix, document.prefix) |
409
|
|
|
self.assertEqual(self.path, document.path) |
410
|
|
|
|
411
|
|
|
def test_create_document_with_unknown_parent(self): |
412
|
|
|
"""Verify a new document can be created with an unknown parent.""" |
413
|
|
|
# Verify the document does not already exist |
414
|
|
|
self.assertRaises(DoorstopError, core.find_document, self.prefix) |
415
|
|
|
# Import a document |
416
|
|
|
document = core.importer.create_document(self.prefix, self.path, |
417
|
|
|
parent=self.parent) |
418
|
|
|
# Verify the imported document's attributes are correct |
419
|
|
|
self.assertEqual(self.prefix, document.prefix) |
420
|
|
|
self.assertEqual(self.path, document.path) |
421
|
|
|
self.assertEqual(self.parent, document.parent) |
422
|
|
|
# Verify the imported document can be found |
423
|
|
|
document2 = core.find_document(self.prefix) |
424
|
|
|
self.assertIs(document, document2) |
425
|
|
|
|
426
|
|
|
def test_create_document_already_exists(self): |
427
|
|
|
"""Verify non-parent exceptions are re-raised.""" |
428
|
|
|
# Create a document |
429
|
|
|
core.importer.create_document(self.prefix, self.path) |
430
|
|
|
# Attempt to create the same document |
431
|
|
|
self.assertRaises(DoorstopError, core.importer.create_document, |
432
|
|
|
self.prefix, self.path) |
433
|
|
|
|
434
|
|
|
def test_add_item(self): |
435
|
|
|
"""Verify an item can be imported into a document.""" |
436
|
|
|
# Create a document |
437
|
|
|
core.importer.create_document(self.prefix, self.path) |
438
|
|
|
# Verify the item does not already exist |
439
|
|
|
self.assertRaises(DoorstopError, core.find_item, self.uid) |
440
|
|
|
# Import an item |
441
|
|
|
item = core.importer.add_item(self.prefix, self.uid) |
442
|
|
|
# Verify the item's attributes are correct |
443
|
|
|
self.assertEqual(self.uid, item.uid) |
444
|
|
|
# Verify the item can be found |
445
|
|
|
item2 = core.find_item(self.uid) |
446
|
|
|
self.assertIs(item, item2) |
447
|
|
|
# Verify the item is contained in the document |
448
|
|
|
document = core.find_document(self.prefix) |
449
|
|
|
self.assertIn(item, document.items) |
450
|
|
|
|
451
|
|
|
def test_add_item_with_attrs(self): |
452
|
|
|
"""Verify an item with attributes can be imported into a document.""" |
453
|
|
|
# Create a document |
454
|
|
|
core.importer.create_document(self.prefix, self.path) |
455
|
|
|
# Import an item |
456
|
|
|
attrs = {'text': "Item text", 'ext1': "Extended 1"} |
457
|
|
|
item = core.importer.add_item(self.prefix, self.uid, |
458
|
|
|
attrs=attrs) |
459
|
|
|
# Verify the item is correct |
460
|
|
|
self.assertEqual(self.uid, item.uid) |
461
|
|
|
self.assertEqual(attrs['text'], item.text) |
462
|
|
|
self.assertEqual(attrs['ext1'], item.get('ext1')) |
463
|
|
|
|
464
|
|
|
|
465
|
|
|
class TestExporter(unittest.TestCase): |
466
|
|
|
"""Integration tests for the doorstop.core.exporter module.""" |
467
|
|
|
|
468
|
|
|
maxDiff = None |
469
|
|
|
|
470
|
|
|
def setUp(self): |
471
|
|
|
self.document = core.Document(FILES, root=ROOT) |
472
|
|
|
self.temp = tempfile.mkdtemp() |
473
|
|
|
|
474
|
|
|
def tearDown(self): |
475
|
|
|
shutil.rmtree(self.temp) |
476
|
|
|
|
477
|
|
|
def test_export_yml(self): |
478
|
|
|
"""Verify a document can be exported as a YAML file.""" |
479
|
|
|
path = os.path.join(FILES, 'exported.yml') |
480
|
|
|
temp = os.path.join(self.temp, 'exported.yml') |
481
|
|
|
expected = read_yml(path) |
482
|
|
|
# Act |
483
|
|
|
path2 = core.exporter.export(self.document, temp) |
484
|
|
|
# Assert |
485
|
|
|
self.assertIs(temp, path2) |
486
|
|
|
if CHECK_EXPORTED_CONTENT: |
487
|
|
|
actual = read_yml(temp) |
488
|
|
|
self.assertEqual(expected, actual) |
489
|
|
|
move_file(temp, path) |
490
|
|
|
|
491
|
|
|
def test_export_csv(self): |
492
|
|
|
"""Verify a document can be exported as a CSV file.""" |
493
|
|
|
path = os.path.join(FILES, 'exported.csv') |
494
|
|
|
temp = os.path.join(self.temp, 'exported.csv') |
495
|
|
|
expected = read_csv(path) |
496
|
|
|
# Act |
497
|
|
|
path2 = core.exporter.export(self.document, temp) |
498
|
|
|
# Assert |
499
|
|
|
self.assertIs(temp, path2) |
500
|
|
|
if CHECK_EXPORTED_CONTENT: |
501
|
|
|
actual = read_csv(temp) |
502
|
|
|
self.assertEqual(expected, actual) |
503
|
|
|
move_file(temp, path) |
504
|
|
|
|
505
|
|
|
@patch('doorstop.settings.REVIEW_NEW_ITEMS', False) |
506
|
|
|
def test_export_tsv(self): |
507
|
|
|
"""Verify a document can be exported as a TSV file.""" |
508
|
|
|
path = os.path.join(FILES, 'exported.tsv') |
509
|
|
|
temp = os.path.join(self.temp, 'exported.tsv') |
510
|
|
|
expected = read_csv(path, delimiter='\t') |
511
|
|
|
# Act |
512
|
|
|
path2 = core.exporter.export(self.document, temp) |
513
|
|
|
# Assert |
514
|
|
|
self.assertIs(temp, path2) |
515
|
|
|
if CHECK_EXPORTED_CONTENT: |
516
|
|
|
actual = read_csv(temp, delimiter='\t') |
517
|
|
|
self.assertEqual(expected, actual) |
518
|
|
|
move_file(temp, path) |
519
|
|
|
|
520
|
|
|
@unittest.skipUnless(os.getenv(ENV) or not CHECK_EXPORTED_CONTENT, REASON) |
521
|
|
|
def test_export_xlsx(self): |
522
|
|
|
"""Verify a document can be exported as an XLSX file.""" |
523
|
|
|
path = os.path.join(FILES, 'exported.xlsx') |
524
|
|
|
temp = os.path.join(self.temp, 'exported.xlsx') |
525
|
|
|
expected = read_xlsx(path) |
526
|
|
|
# Act |
527
|
|
|
path2 = core.exporter.export(self.document, temp) |
528
|
|
|
# Assert |
529
|
|
|
self.assertIs(temp, path2) |
530
|
|
|
if CHECK_EXPORTED_CONTENT: |
531
|
|
|
actual = read_xlsx(temp) |
532
|
|
|
self.assertEqual(expected, actual) |
533
|
|
|
else: # binary file always changes, only copy when not checking |
534
|
|
|
move_file(temp, path) |
535
|
|
|
|
536
|
|
|
|
537
|
|
|
class TestPublisher(unittest.TestCase): |
538
|
|
|
"""Integration tests for the doorstop.core.publisher module.""" |
539
|
|
|
|
540
|
|
|
maxDiff = None |
541
|
|
|
|
542
|
|
|
@patch('doorstop.core.document.Document', DocumentNoSkip) |
543
|
|
|
def setUp(self): |
544
|
|
|
self.tree = core.build(cwd=FILES, root=FILES) |
545
|
|
|
# self.document = core.Document(FILES, root=ROOT) |
546
|
|
|
self.document = self.tree.find_document('REQ') |
547
|
|
|
self.temp = tempfile.mkdtemp() |
548
|
|
|
|
549
|
|
|
def tearDown(self): |
550
|
|
|
if os.path.exists(self.temp): |
551
|
|
|
shutil.rmtree(self.temp) |
552
|
|
|
|
553
|
|
|
def test_publish_html(self): |
554
|
|
|
"""Verify an HTML file can be created.""" |
555
|
|
|
path = os.path.join(self.temp, 'published.html') |
556
|
|
|
# Act |
557
|
|
|
path2 = core.publisher.publish(self.document, path, '.html') |
558
|
|
|
# Assert |
559
|
|
|
self.assertIs(path, path2) |
560
|
|
|
self.assertTrue(os.path.isfile(path)) |
561
|
|
|
|
562
|
|
|
def test_publish_bad_link(self): |
563
|
|
|
"""Verify a tree can be published with bad links.""" |
564
|
|
|
item = self.document.add_item() |
565
|
|
|
try: |
566
|
|
|
item.link('badlink') |
567
|
|
|
dirpath = os.path.join(self.temp, 'html') |
568
|
|
|
# Act |
569
|
|
|
dirpath2 = core.publisher.publish(self.tree, dirpath) |
570
|
|
|
# Assert |
571
|
|
|
self.assertIs(dirpath, dirpath2) |
572
|
|
|
finally: |
573
|
|
|
item.delete() |
574
|
|
|
|
575
|
|
|
def test_lines_text_document(self): |
576
|
|
|
"""Verify text can be published from a document.""" |
577
|
|
|
path = os.path.join(FILES, 'published.txt') |
578
|
|
|
expected = common.read_text(path) |
579
|
|
|
# Act |
580
|
|
|
lines = core.publisher.publish_lines(self.document, '.txt') |
581
|
|
|
text = ''.join(line + '\n' for line in lines) |
582
|
|
|
# Assert |
583
|
|
|
if CHECK_PUBLISHED_CONTENT: |
584
|
|
|
self.assertEqual(expected, text) |
585
|
|
|
common.write_text(text, path) |
586
|
|
|
|
587
|
|
|
@patch('doorstop.settings.PUBLISH_CHILD_LINKS', False) |
588
|
|
|
def test_lines_text_document_without_child_links(self): |
589
|
|
|
"""Verify text can be published from a document w/o child links.""" |
590
|
|
|
path = os.path.join(FILES, 'published2.txt') |
591
|
|
|
expected = common.read_text(path) |
592
|
|
|
# Act |
593
|
|
|
lines = core.publisher.publish_lines(self.document, '.txt') |
594
|
|
|
text = ''.join(line + '\n' for line in lines) |
595
|
|
|
# Assert |
596
|
|
|
if CHECK_PUBLISHED_CONTENT: |
597
|
|
|
self.assertEqual(expected, text) |
598
|
|
|
common.write_text(text, path) |
599
|
|
|
|
600
|
|
|
def test_lines_markdown_document(self): |
601
|
|
|
"""Verify Markdown can be published from a document.""" |
602
|
|
|
path = os.path.join(FILES, 'published.md') |
603
|
|
|
expected = common.read_text(path) |
604
|
|
|
# Act |
605
|
|
|
lines = core.publisher.publish_lines(self.document, '.md') |
606
|
|
|
text = ''.join(line + '\n' for line in lines) |
607
|
|
|
# Assert |
608
|
|
|
if CHECK_PUBLISHED_CONTENT: |
609
|
|
|
self.assertEqual(expected, text) |
610
|
|
|
common.write_text(text, path) |
611
|
|
|
|
612
|
|
|
@patch('doorstop.settings.PUBLISH_CHILD_LINKS', False) |
613
|
|
|
def test_lines_markdown_document_without_child_links(self): |
614
|
|
|
"""Verify Markdown can be published from a document w/o child links.""" |
615
|
|
|
path = os.path.join(FILES, 'published2.md') |
616
|
|
|
expected = common.read_text(path) |
617
|
|
|
# Act |
618
|
|
|
lines = core.publisher.publish_lines(self.document, '.md') |
619
|
|
|
text = ''.join(line + '\n' for line in lines) |
620
|
|
|
# Assert |
621
|
|
|
if CHECK_PUBLISHED_CONTENT: |
622
|
|
|
self.assertEqual(expected, text) |
623
|
|
|
common.write_text(text, path) |
624
|
|
|
|
625
|
|
|
def test_lines_html_document_linkify(self): |
626
|
|
|
"""Verify HTML can be published from a document.""" |
627
|
|
|
path = os.path.join(FILES, 'published.html') |
628
|
|
|
expected = common.read_text(path) |
629
|
|
|
# Act |
630
|
|
|
lines = core.publisher.publish_lines(self.document, '.html', |
631
|
|
|
linkify=True) |
632
|
|
|
text = ''.join(line + '\n' for line in lines) |
633
|
|
|
# Assert |
634
|
|
|
if CHECK_PUBLISHED_CONTENT: |
635
|
|
|
self.assertEqual(expected, text) |
636
|
|
|
common.write_text(text, path) |
637
|
|
|
|
638
|
|
|
@patch('doorstop.settings.PUBLISH_CHILD_LINKS', False) |
639
|
|
|
def test_lines_html_document_without_child_links(self): |
640
|
|
|
"""Verify HTML can be published from a document w/o child links.""" |
641
|
|
|
path = os.path.join(FILES, 'published2.html') |
642
|
|
|
expected = common.read_text(path) |
643
|
|
|
# Act |
644
|
|
|
lines = core.publisher.publish_lines(self.document, '.html') |
645
|
|
|
text = ''.join(line + '\n' for line in lines) |
646
|
|
|
# Assert |
647
|
|
|
if CHECK_PUBLISHED_CONTENT: |
648
|
|
|
self.assertEqual(expected, text) |
649
|
|
|
common.write_text(text, path) |
650
|
|
|
|
651
|
|
|
|
652
|
|
|
class TestModule(unittest.TestCase): |
653
|
|
|
"""Integration tests for the doorstop.core module.""" |
654
|
|
|
|
655
|
|
|
def setUp(self): |
656
|
|
|
"""Reset the internal tree.""" |
657
|
|
|
_clear_tree() |
658
|
|
|
|
659
|
|
|
def test_find_document(self): |
660
|
|
|
"""Verify documents can be found using a convenience function.""" |
661
|
|
|
# Cache miss |
662
|
|
|
document = core.find_document('req') |
663
|
|
|
self.assertIsNot(None, document) |
664
|
|
|
# Cache hit |
665
|
|
|
document2 = core.find_document('req') |
666
|
|
|
self.assertIs(document2, document) |
667
|
|
|
|
668
|
|
|
def test_find_item(self): |
669
|
|
|
"""Verify items can be found using a convenience function.""" |
670
|
|
|
# Cache miss |
671
|
|
|
item = core.find_item('req1') |
672
|
|
|
self.assertIsNot(None, item) |
673
|
|
|
# Cache hit |
674
|
|
|
item2 = core.find_item('req1') |
675
|
|
|
self.assertIs(item2, item) |
676
|
|
|
|
677
|
|
|
|
678
|
|
|
# helper functions ########################################################### |
679
|
|
|
|
680
|
|
|
|
681
|
|
|
def log_data(expected, actual): |
682
|
|
|
"""Log list values.""" |
683
|
|
|
for index, (evalue, avalue) in enumerate(zip(expected, actual)): |
684
|
|
|
logging.debug("\n{i} expected:\n{e}\n{i} actual:\n{a}".format( |
|
|
|
|
685
|
|
|
i=index, |
686
|
|
|
e=pprint.pformat(evalue), |
687
|
|
|
a=pprint.pformat(avalue))) |
688
|
|
|
|
689
|
|
|
|
690
|
|
|
def read_yml(path): |
691
|
|
|
"""Return a dictionary of items from a YAML file.""" |
692
|
|
|
text = common.read_text(path) |
693
|
|
|
data = yaml.load(text) |
694
|
|
|
return data |
695
|
|
|
|
696
|
|
|
|
697
|
|
|
def read_csv(path, delimiter=','): |
698
|
|
|
"""Return a list of rows from a CSV file.""" |
699
|
|
|
rows = [] |
700
|
|
|
try: |
701
|
|
|
with open(path, 'r', newline='', encoding='utf-8') as stream: |
702
|
|
|
reader = csv.reader(stream, delimiter=delimiter) |
703
|
|
|
for row in reader: |
704
|
|
|
rows.append(row) |
705
|
|
|
except FileNotFoundError: |
706
|
|
|
logging.warning("file not found: {}".format(path)) |
|
|
|
|
707
|
|
|
return rows |
708
|
|
|
|
709
|
|
|
|
710
|
|
|
def read_xlsx(path): |
711
|
|
|
"""Return a list of workbook data from an XLSX file.""" |
712
|
|
|
data = [] |
713
|
|
|
|
714
|
|
|
try: |
715
|
|
|
workbook = openpyxl.load_workbook(path) |
716
|
|
|
except openpyxl.exceptions.InvalidFileException: |
717
|
|
|
logging.warning("file not found: {}".format(path)) |
|
|
|
|
718
|
|
|
else: |
719
|
|
|
worksheet = workbook.active |
720
|
|
|
for row in worksheet.rows: |
721
|
|
|
for cell in row: |
722
|
|
|
values = (cell.value, |
723
|
|
|
cell.style, |
724
|
|
|
worksheet.column_dimensions[cell.column].width) |
725
|
|
|
data.append(values) |
726
|
|
|
data.append(worksheet.auto_filter.ref) |
727
|
|
|
|
728
|
|
|
return data |
729
|
|
|
|
730
|
|
|
|
731
|
|
|
def move_file(src, dst): |
732
|
|
|
"""Move a file from one path to another.""" |
733
|
|
|
common.delete(dst) |
734
|
|
|
shutil.move(src, dst) |
735
|
|
|
|