def _get_issues_document(self, document, skip): """Yield all the item's issues against its document.""" log.debug("getting issues against document...") if document in skip: log.debug("skipping issues against document %s...", document) return # Verify an item's UID matches its document's prefix if self.prefix != document.prefix: msg = "prefix differs from document ({})".format(document.prefix) yield DoorstopInfo(msg) # Verify an item has upward links if all((document.parent, self.normative, not self.derived)) and not self.links: msg = "no links to parent document: {}".format(document.parent) yield DoorstopWarning(msg) # Verify an item's links are to the correct parent for uid in self.links: try: prefix = uid.prefix except DoorstopError: msg = "invalid UID in links: {}".format(uid) yield DoorstopError(msg) else: if document.parent and prefix != document.parent: # this is only 'info' because a document is allowed # to contain items with a different prefix, but # Doorstop will not create items like this msg = "parent is '{}', but linked to: {}".format( document.parent, uid) yield DoorstopInfo(msg)
def _get_issues_level(items): """Yield all the document's issues related to item level.""" prev = items[0] if items else None for item in items[1:]: puid = prev.uid plev = prev.level nuid = item.uid nlev = item.level log.debug("checking level {} to {}...".format(plev, nlev)) # Duplicate level if plev == nlev: uids = sorted((puid, nuid)) msg = "duplicate level: {} ({}, {})".format(plev, *uids) yield DoorstopWarning(msg) # Skipped level length = min(len(plev.value), len(nlev.value)) for index in range(length): # Types of skipped levels: # 1. over: 1.0 --> 1.2 # 2. out: 1.1 --> 3.0 if (nlev.value[index] - plev.value[index] > 1 or # 3. over and out: 1.1 --> 2.2 (plev.value[index] != nlev.value[index] and index + 1 < length and nlev.value[index + 1] not in (0, 1))): msg = "skipped level: {} ({}), {} ({})".format(plev, puid, nlev, nuid) yield DoorstopInfo(msg) break prev = item
def test_validate(self, mock_reorder, mock_get_issues): """Verify a document can be validated.""" mock_get_issues.return_value = [DoorstopInfo('i')] with patch('doorstop.settings.REORDER', True): self.assertTrue(self.document.validate()) mock_reorder.assert_called_once_with(_items=self.document.items) self.assertEqual(5, mock_get_issues.call_count)
def _get_issues_tree(self, tree): """Yield all the item's issues against its tree.""" log.debug("getting issues against tree...") # Verify an item's links are valid identifiers = set() for uid in self.links: try: item = tree.find_item(uid) except DoorstopError: identifiers.add(uid) # keep the invalid UID msg = "linked to unknown item: {}".format(uid) yield DoorstopError(msg) else: # check the linked item if not item.active: msg = "linked to inactive item: {}".format(item) yield DoorstopInfo(msg) if not item.normative: msg = "linked to non-normative item: {}".format(item) yield DoorstopWarning(msg) # check the link status if uid.stamp == Stamp(True): uid.stamp = item.stamp() # convert True to a stamp elif uid.stamp != item.stamp(): if settings.CHECK_SUSPECT_LINKS: msg = "suspect link: {}".format(item) yield DoorstopWarning(msg) # reformat the item's UID identifier2 = UID(item.uid, stamp=uid.stamp) identifiers.add(identifier2) # Apply the reformatted item UIDs if settings.REFORMAT: self._data['links'] = identifiers
def test_issues_skipped_level(self): """Verify skipped item levels are detected.""" expect = DoorstopInfo("skipped level: 1.4 (REQ003), 1.6 (REQ004)") for issue in self.document.issues: logging.info(repr(issue)) if type(issue) == type(expect) and issue.args == expect.args: break else: self.fail("issue not found: {}".format(expect))
def _get_issues_document(item, document, skip): """Yield all the item's issues against its document.""" log.debug("getting issues against document...") if document in skip: log.debug("skipping issues against document %s...", document) return # Verify an item's UID matches its document's prefix if item.uid.prefix != document.prefix: msg = "prefix differs from document ({})".format(document.prefix) yield DoorstopInfo(msg) # Verify that normative, non-derived items in a child document have at # least one link. It is recommended that these items have an upward # link to an item in the parent document, however, this is not # enforced. An info message is generated if this is not the case. if all((document.parent, item.normative, not item.derived)) and not item.links: msg = "no links to parent document: {}".format(document.parent) yield DoorstopWarning(msg) # Verify an item's links are to the correct parent for uid in item.links: try: prefix = uid.prefix except DoorstopError: msg = "invalid UID in links: {}".format(uid) yield DoorstopError(msg) else: if document.parent and prefix != document.parent: # this is only 'info' because a document is allowed # to contain items with a different prefix, but # Doorstop will not create items like this msg = "parent is '{}', but linked to: {}".format( document.parent, uid) yield DoorstopInfo(msg)
def _get_issues_tree(self, item, tree): """Yield all the item's issues against its tree.""" log.debug("getting issues against tree...") # Verify an item's links are valid identifiers = set() for uid in item.links: try: parent = tree.find_item(uid) except DoorstopError: identifiers.add(uid) # keep the invalid UID msg = "linked to unknown item: {}".format(uid) yield DoorstopError(msg) else: # check the parent item if not parent.active: msg = "linked to inactive item: {}".format(parent) yield DoorstopInfo(msg) if not parent.normative: msg = "linked to non-normative item: {}".format(parent) yield DoorstopWarning(msg) # check the link status if uid.stamp == Stamp(True): uid.stamp = parent.stamp() elif not str(uid.stamp) and settings.STAMP_NEW_LINKS: uid.stamp = parent.stamp() elif uid.stamp != parent.stamp(): if settings.CHECK_SUSPECT_LINKS: msg = "suspect link: {}".format(parent) yield DoorstopWarning(msg) # reformat the item's UID identifiers.add(UID(parent.uid, stamp=uid.stamp)) # Apply the reformatted item UIDs if settings.REFORMAT: item.links = identifiers
class TestTree(unittest.TestCase): """Unit tests for the Tree class.""" def setUp(self): document = Document(SYS) self.tree = Tree(document) document.tree = self.tree document = Document(FILES) self.tree._place(document) # pylint: disable=W0212 document.tree = self.tree self.tree._vcs = Mock() # pylint: disable=W0212 @patch('doorstop.core.vcs.find_root', Mock(return_value=EMPTY)) def test_palce_empty(self): """Verify a document can be placed in an empty tree.""" tree = build(EMPTY) doc = MockDocumentSkip.new(tree, os.path.join(EMPTY, 'temp'), EMPTY, 'TEMP') tree._place(doc) # pylint: disable=W0212 self.assertEqual(1, len(tree)) @patch('doorstop.core.vcs.find_root', Mock(return_value=EMPTY)) def test_palce_empty_no_parent(self): """Verify a document with parent cannot be placed in an empty tree.""" tree = build(EMPTY) doc = MockDocumentSkip.new(tree, os.path.join(EMPTY, 'temp'), EMPTY, 'TEMP', parent='REQ') self.assertRaises(DoorstopError, tree._place, doc) # pylint: disable=W0212 def test_documents(self): """Verify the documents in a tree can be accessed.""" documents = self.tree.documents self.assertEqual(2, len(documents)) for document in self.tree: logging.debug("document: {}".format(document)) self.assertIs(self.tree, document.tree) @patch('doorstop.core.document.Document.get_issues') def test_validate(self, mock_get_issues): """Verify trees can be checked.""" logging.info("tree: {}".format(self.tree)) self.assertTrue(self.tree.validate()) self.assertEqual(2, mock_get_issues.call_count) def test_validate_no_documents(self): """Verify an empty tree can be checked.""" tree = Tree(None, root='.') self.assertTrue(tree.validate()) @patch('doorstop.settings.REORDER', False) @patch('doorstop.core.document.Document.get_issues', Mock(return_value=[ DoorstopError('error'), DoorstopWarning('warning'), DoorstopInfo('info') ])) def test_validate_document(self): """Verify a document error fails the tree validation.""" self.assertFalse(self.tree.validate()) @patch('doorstop.core.document.Document.get_issues', Mock(return_value=[])) def test_validate_hook(self): """Verify a document hook can be called.""" mock_hook = MagicMock() self.tree.validate(document_hook=mock_hook) self.assertEqual(2, mock_hook.call_count) @patch('doorstop.core.tree.Tree.get_issues', Mock(return_value=[])) def test_issues(self): """Verify an tree's issues convenience property can be accessed.""" self.assertEqual(0, len(self.tree.issues)) def test_get_traceability(self): """Verify traceability rows are correct.""" rows = [ (self.tree.find_item('SYS001'), self.tree.find_item('REQ001')), (self.tree.find_item('SYS002'), self.tree.find_item('REQ001')), (None, self.tree.find_item('REQ002')), (None, self.tree.find_item('REQ004')), ] # Act rows2 = self.tree.get_traceability() # Assert self.maxDiff = None self.assertListEqual(rows, rows2) def test_new_document(self): """Verify a new document can be created on a tree.""" self.tree.create_document(EMPTY, '_TEST', parent='REQ') def test_new_document_unknown_parent(self): """Verify an exception is raised for an unknown parent.""" temp = tempfile.mkdtemp() self.assertRaises(DoorstopError, self.tree.create_document, temp, '_TEST', parent='UNKNOWN') self.assertFalse(os.path.exists(temp)) @patch('doorstop.core.document.Document.add_item') def test_add_item(self, mock_add_item): """Verify an item can be added to a document.""" self.tree.add_item('REQ') mock_add_item.assert_called_once_with(number=None, level=None, reorder=True) @patch('doorstop.core.document.Document.add_item') def test_add_item_level(self, mock_add): """Verify an item can be added to a document with a level.""" self.tree.add_item('REQ', level='1.2.3') mock_add.assert_called_once_with(number=None, level='1.2.3', reorder=True) def test_add_item_unknown_prefix(self): """Verify an exception is raised for an unknown prefix (item).""" # Cache miss self.assertRaises(DoorstopError, self.tree.add_item, 'UNKNOWN') # Cache hit self.assertRaises(DoorstopError, self.tree.add_item, 'UNKNOWN') @patch('doorstop.settings.REORDER', False) @patch('doorstop.core.item.Item.delete') def test_remove_item(self, mock_delete): """Verify an item can be removed from a document.""" self.tree.remove_item('req1', reorder=False) mock_delete.assert_called_once_with() def test_remove_item_unknown_item(self): """Verify an exception is raised removing an unknown item.""" self.assertRaises(DoorstopError, self.tree.remove_item, 'req9999') @patch('doorstop.core.item.Item.link') def test_link_items(self, mock_link): """Verify two items can be linked.""" self.tree.link_items('req1', 'req2') mock_link.assert_called_once_with('REQ002') def test_link_items_unknown_child_prefix(self): """Verify an exception is raised with an unknown child prefix.""" self.assertRaises(DoorstopError, self.tree.link_items, 'unknown1', 'req2') def test_link_items_unknown_child_number(self): """Verify an exception is raised with an unknown child number.""" self.assertRaises(DoorstopError, self.tree.link_items, 'req9999', 'req2') def test_link_items_unknown_parent_prefix(self): """Verify an exception is raised with an unknown parent prefix.""" self.assertRaises(DoorstopError, self.tree.link_items, 'req1', 'unknown1') def test_link_items_unknown_parent_number(self): """Verify an exception is raised with an unknown parent prefix.""" self.assertRaises(DoorstopError, self.tree.link_items, 'req1', 'req9999') @patch('doorstop.core.item.Item.unlink') def test_unlink_items(self, mock_unlink): """Verify two items can be unlinked.""" self.tree.unlink_items('req3', 'req1') mock_unlink.assert_called_once_with('REQ001') def test_unlink_items_unknown_child_prefix(self): """Verify an exception is raised with an unknown child prefix.""" self.assertRaises(DoorstopError, self.tree.unlink_items, 'unknown1', 'req1') def test_unlink_items_unknown_child_number(self): """Verify an exception is raised with an unknown child number.""" self.assertRaises(DoorstopError, self.tree.unlink_items, 'req9999', 'req1') def test_unlink_items_unknown_parent_prefix(self): """Verify an exception is raised with an unknown parent prefix.""" # Cache miss self.assertRaises(DoorstopError, self.tree.unlink_items, 'req3', 'unknown1') # Cache hit self.assertRaises(DoorstopError, self.tree.unlink_items, 'req3', 'unknown1') def test_unlink_items_unknown_parent_number(self): """Verify an exception is raised with an unknown parent prefix.""" self.assertRaises(DoorstopError, self.tree.unlink_items, 'req3', 'req9999') @patch('doorstop.core.editor.launch') def test_edit_item(self, mock_launch): """Verify an item can be edited in a tree.""" self.tree.edit_item('req2', launch=True) path = os.path.join(FILES, 'REQ002.yml') mock_launch.assert_called_once_with(path, tool=None) def test_edit_item_unknown_prefix(self): """Verify an exception is raised for an unknown prefix (document).""" self.assertRaises(DoorstopError, self.tree.edit_item, 'unknown1') def test_edit_item_unknown_number(self): """Verify an exception is raised for an unknown number.""" self.assertRaises(DoorstopError, self.tree.edit_item, 'req9999') def test_find_item(self): """Verify an item can be found by exact UID.""" # Cache miss item = self.tree.find_item('req2-001') self.assertIsNot(None, item) # Cache hit item2 = self.tree.find_item('req2-001') self.assertIs(item2, item) def test_find_document(self): """Verify an document can be found by prefix""" # Cache miss document = self.tree.find_document('req') self.assertIsNot(None, document) # Cache hit document2 = self.tree.find_document('req') self.assertIs(document2, document) def test_load(self): """Verify an a tree can be reloaded.""" self.tree.load() self.tree.load() # should return immediately @patch('doorstop.core.document.Document.delete') def test_delete(self, mock_delete): """Verify a tree can be deleted.""" self.tree.delete() self.assertEqual(0, len(self.tree)) self.assertEqual(2, mock_delete.call_count) self.tree.delete() # ensure a second delete is ignored
def get_issues(self, skip=None, document_hook=None, item_hook=None): # pylint: disable=unused-argument """Yield all the item's issues. :param skip: list of document prefixes to skip :return: generator of :class:`~doorstop.common.DoorstopError`, :class:`~doorstop.common.DoorstopWarning`, :class:`~doorstop.common.DoorstopInfo` """ assert document_hook is None assert item_hook is None skip = [] if skip is None else skip log.info("checking item %s...", self) # Verify the file can be parsed self.load() # Skip inactive items if not self.active: log.info("skipped inactive item: %s", self) return # Delay item save if reformatting if settings.REFORMAT: self.auto = False # Check text if not self.text: yield DoorstopWarning("no text") # Check external references if settings.CHECK_REF: try: self.find_ref() except DoorstopError as exc: yield exc # Check links if not self.normative and self.links: yield DoorstopWarning("non-normative, but has links") # Check links against the document if self.document: yield from self._get_issues_document(self.document, skip) # Check links against the tree if self.tree: yield from self._get_issues_tree(self.tree) # Check links against both document and tree if self.document and self.tree: yield from self._get_issues_both(self.document, self.tree, skip) # Check review status if not self.reviewed: if settings.CHECK_REVIEW_STATUS: if not self._data['reviewed']: if settings.REVIEW_NEW_ITEMS: self.review() else: yield DoorstopInfo("needs initial review") else: yield DoorstopWarning("unreviewed changes") # Reformat the file if settings.REFORMAT: log.debug("reformatting item %s...", self) self.save()
class TestDocument(unittest.TestCase): """Unit tests for the Document class.""" # pylint: disable=W0212 def setUp(self): self.document = MockDocument(FILES, root=ROOT) def test_init_invalid(self): """Verify a document cannot be initialized from an invalid path.""" self.assertRaises(DoorstopError, Document, 'not/a/path') def test_object_references(self): """Verify a standalone document does not have object references.""" self.assertIs(None, self.document.tree) def test_load_empty(self): """Verify loading calls read.""" self.document.load() self.document._read.assert_called_once_with(self.document.config) def test_load_error(self): """Verify an exception is raised with invalid YAML.""" self.document._file = "invalid: -" self.assertRaises(DoorstopError, self.document.load) def test_load_unexpected(self): """Verify an exception is raised for unexpected file contents.""" self.document._file = "unexpected" self.assertRaises(DoorstopError, self.document.load) def test_load(self): """Verify the document config can be loaded from file.""" self.document._file = YAML_CUSTOM self.document.load(reload=True) self.assertEqual('CUSTOM', self.document.prefix) self.assertEqual('-', self.document.sep) self.assertEqual(4, self.document.digits) def test_load_parent(self): """Verify the document config can be loaded from file with a parent.""" self.document._file = YAML_CUSTOM_PARENT self.document.load() self.assertEqual('PARENT', self.document.parent) def test_save_empty(self): """Verify saving calls write.""" self.document.tree = Mock() self.document.save() self.document._write.assert_called_once_with(YAML_DEFAULT, self.document.config) self.document.tree.vcs.edit.assert_called_once_with( self.document.config) def test_save_parent(self): """Verify a document can be saved with a parent.""" self.document.parent = 'SYS' self.document.save() self.assertIn("parent: SYS", self.document._file) def test_save_custom(self): """Verify a document can be saved with a custom attribute.""" self.document._data['custom'] = 'this' self.document.save() self.assertIn("custom: this", self.document._file) @patch('doorstop.common.verbosity', 2) def test_str(self): """Verify a document can be converted to a string.""" self.assertEqual("REQ", str(self.document)) @patch('doorstop.common.verbosity', 3) def test_str_verbose(self): """Verify a document can be converted to a string (verbose).""" relpath = os.path.relpath(self.document.path, self.document.root) text = "REQ (@{}{})".format(os.sep, relpath) self.assertEqual(text, str(self.document)) def test_ne(self): """Verify document non-equality is correct.""" self.assertNotEqual(self.document, None) def test_hash(self): """Verify documents can be hashed.""" document1 = MockDocument('path/to/fake1') document2 = MockDocument('path/to/fake2') document3 = MockDocument('path/to/fake2') my_set = set() # Act my_set.add(document1) my_set.add(document2) my_set.add(document3) # Assert self.assertEqual(2, len(my_set)) def test_len(self): """Verify a document has a length.""" self.assertEqual(5, len(self.document)) def test_items(self): """Verify the items in a document can be accessed.""" items = self.document.items self.assertEqual(5, len(items)) for item in self.document: logging.debug("item: {}".format(item)) self.assertIs(self.document, item.document) self.assertIs(self.document.tree, item.tree) def test_items_cache(self): """Verify the items in a document get cached.""" self.document.tree = Mock() self.document.tree._item_cache = {} print(self.document.items) self.assertEqual(6, len(self.document.tree._item_cache)) @patch('doorstop.core.document.Document', MockDocument) def test_new(self): """Verify a new document can be created with defaults.""" MockDocument._create.reset_mock() path = os.path.join(EMPTY, '.doorstop.yml') document = MockDocument.new(None, EMPTY, root=FILES, prefix='NEW', digits=2) self.assertEqual('NEW', document.prefix) self.assertEqual(2, document.digits) MockDocument._create.assert_called_once_with(path, name='document') def test_new_existing(self): """Verify an exception is raised if the document already exists.""" self.assertRaises(DoorstopError, Document.new, None, FILES, ROOT, prefix='DUPL') @patch('doorstop.core.document.Document', MockDocument) def test_new_cache(self): """Verify a new documents are cached.""" mock_tree = Mock() mock_tree._document_cache = {} document = MockDocument.new(mock_tree, EMPTY, root=FILES, prefix='NEW', digits=2) mock_tree.vcs.add.assert_called_once_with(document.config) self.assertEqual(document, mock_tree._document_cache[document.prefix]) def test_invalid(self): """Verify an exception is raised on an invalid document.""" self.assertRaises(DoorstopError, Document, EMPTY) def test_relpath(self): """Verify the document's relative path string can be determined.""" relpath = os.path.relpath(self.document.path, self.document.root) text = "@{}{}".format(os.sep, relpath) self.assertEqual(text, self.document.relpath) def test_sep(self): """Verify an documents's separator can be set and read.""" self.document.sep = '_' self.assertIn("sep: _\n", self.document._write.call_args[0][0]) self.assertEqual('_', self.document.sep) def test_sep_invalid(self): """Verify an invalid separator is rejected.""" self.assertRaises(AssertionError, setattr, self.document, 'sep', '?') def test_digits(self): """Verify an documents's digits can be set and read.""" self.document.digits = 42 self.assertIn("digits: 42\n", self.document._write.call_args[0][0]) self.assertEqual(42, self.document.digits) def test_depth(self): """Verify the maximum item level depth can be determined.""" self.assertEqual(3, self.document.depth) def test_next_number(self): """Verify the next item number can be determined.""" self.assertEqual(6, self.document.next_number) def test_next_number_server(self): """Verify the next item number can be determined with a server.""" self.document.tree = MagicMock() self.document.tree.request_next_number = Mock(side_effect=[1, 42]) self.assertEqual(42, self.document.next_number) def test_index_get(self): """Verify a document's index can be retrieved.""" self.assertIs(None, self.document.index) with patch('os.path.isfile', Mock(return_value=True)): path = os.path.join(self.document.path, self.document.INDEX) self.assertEqual(path, self.document.index) @patch('doorstop.common.write_lines') @patch('doorstop.settings.MAX_LINE_LENGTH', 40) def test_index_set(self, mock_write_lines): """Verify an document's index can be created.""" lines = [ 'initial: 1.2.3', 'outline:', ' - REQ001: # Lorem ipsum d...', ' - REQ003: # Unicode: -40° ±1%', ' - REQ004: # Hello, world!', ' - REQ002: # Hello, world!', ' - REQ2-001: # Hello, world!' ] # Act self.document.index = True # create index # Assert gen, path = mock_write_lines.call_args[0] lines2 = list(gen)[6:] # skip lines of info comments self.assertListEqual(lines, lines2) self.assertEqual(os.path.join(FILES, 'index.yml'), path) @patch('doorstop.common.delete') def test_index_del(self, mock_delete): """Verify a document's index can be deleted.""" del self.document.index mock_delete.assert_called_once_with(None) @patch('doorstop.core.document.Document._reorder_automatic') @patch('doorstop.core.item.Item.new') def test_add_item(self, mock_new, mock_reorder): """Verify an item can be added to a document.""" with patch('doorstop.settings.REORDER', True): self.document.add_item() mock_new.assert_called_once_with(None, self.document, FILES, ROOT, 'REQ006', level=Level('2.2')) self.assertEqual(0, mock_reorder.call_count) @patch('doorstop.core.document.Document.reorder') @patch('doorstop.core.item.Item.new') def test_add_item_with_level(self, mock_new, mock_reorder): """Verify an item can be added to a document with a level.""" with patch('doorstop.settings.REORDER', True): item = self.document.add_item(level='4.2') mock_new.assert_called_once_with(None, self.document, FILES, ROOT, 'REQ006', level='4.2') mock_reorder.assert_called_once_with(keep=item) @patch('doorstop.core.item.Item.new') def test_add_item_with_number(self, mock_new): """Verify an item can be added to a document with a number.""" self.document.add_item(number=999) mock_new.assert_called_once_with(None, self.document, FILES, ROOT, 'REQ999', level=Level('2.2')) @patch('doorstop.core.item.Item.new') def test_add_item_empty(self, mock_new): """Verify an item can be added to an new document.""" document = MockDocument(NEW, ROOT) document.prefix = 'NEW' self.assertIsNot(None, document.add_item(reorder=False)) mock_new.assert_called_once_with(None, document, NEW, ROOT, 'NEW001', level=None) @patch('doorstop.core.item.Item.new') def test_add_item_after_header(self, mock_new): """Verify the next item after a header is indented.""" mock_item = Mock() mock_item.number = 1 mock_item.level = Level('1.0') self.document._iter = Mock(return_value=[mock_item]) self.document.add_item() mock_new.assert_called_once_with(None, self.document, FILES, ROOT, 'REQ002', level=Level('1.1')) def test_add_item_contains(self): """Verify an added item is contained in the document.""" item = self.document.items[0] self.assertIn(item, self.document) item2 = self.document.add_item(reorder=False) self.assertIn(item2, self.document) def test_add_item_cache(self): """Verify an added item is cached.""" self.document.tree = Mock() self.document.tree._item_cache = {} self.document.tree.request_next_number = None item = self.document.add_item(reorder=False) self.assertEqual(item, self.document.tree._item_cache[item.uid]) @patch('doorstop.core.document.Document._reorder_automatic') @patch('os.remove') def test_remove_item(self, mock_remove, mock_reorder): """Verify an item can be removed.""" with patch('doorstop.settings.REORDER', True): item = self.document.remove_item('REQ001') mock_reorder.assert_called_once_with(self.document.items, keep=None, start=None) mock_remove.assert_called_once_with(item.path) @patch('os.remove') def test_remove_item_contains(self, mock_remove): """Verify a removed item is not contained in the document.""" item = self.document.items[0] self.assertIn(item, self.document) removed_item = self.document.remove_item(item.uid, reorder=False) self.assertEqual(item, removed_item) self.assertNotIn(item, self.document) mock_remove.assert_called_once_with(item.path) @patch('os.remove') def test_remove_item_by_item(self, mock_remove): """Verify an item can be removed (by item).""" item = self.document.items[0] self.assertIn(item, self.document) removed_item = self.document.remove_item(item, reorder=False) self.assertEqual(item, removed_item) mock_remove.assert_called_once_with(item.path) @patch('os.remove', Mock()) def test_remove_item_cache(self): """Verify a removed item is expunged.""" self.document.tree = Mock() self.document.tree._item_cache = {} item = self.document.items[0] removed_item = self.document.remove_item(item.uid, reorder=False) self.assertIs(None, self.document.tree._item_cache[removed_item.uid]) @patch('doorstop.core.document.Document._reorder_automatic') @patch('doorstop.core.document.Document._reorder_from_index') def test_reorder(self, mock_index, mock_auto): """Verify items can be reordered.""" path = os.path.join(self.document.path, 'index.yml') common.touch(path) # Act self.document.reorder() # Assert mock_index.assert_called_once_with(self.document, path) mock_auto.assert_called_once_with(self.document.items, start=None, keep=None) def test_reorder_automatic(self): """Verify items can be reordered automatically.""" mock_items = [ Mock(level=Level('2.3')), Mock(level=Level('2.3')), Mock(level=Level('2.7')), Mock(level=Level('3.2.2')), Mock(level=Level('3.4.2')), Mock(level=Level('3.5.0')), Mock(level=Level('3.5.0')), Mock(level=Level('3.6')), Mock(level=Level('5.0')), Mock(level=Level('5.9')) ] expected = [ Level('2.3'), Level('2.4'), Level('2.5'), Level('3.1.1'), Level('3.2.1'), Level('3.3.0'), Level('3.4.0'), Level('3.5'), Level('4.0'), Level('4.1') ] Document._reorder_automatic(mock_items) actual = [item.level for item in mock_items] self.assertListEqual(expected, actual) def test_reorder_automatic_no_change(self): """Verify already ordered items can be reordered.""" mock_items = [ Mock(level=Level('1.1')), Mock(level=Level('1.1.1.1')), Mock(level=Level('2')), Mock(level=Level('3')), Mock(level=Level('4.1.1')) ] expected = [ Level('1.1'), Level('1.1.1.1'), Level('2'), Level('3'), Level('4.1.1') ] Document._reorder_automatic(mock_items) actual = [item.level for item in mock_items] self.assertListEqual(expected, actual) def test_reorder_automatic_with_start(self): """Verify items can be reordered with a given start.""" mock_item = Mock(level=Level('2.3')) mock_items = [ Mock(level=Level('2.2')), mock_item, Mock(level=Level('2.3')), Mock(level=Level('2.7')), Mock(level=Level('3.2.2')), Mock(level=Level('3.4.2')), Mock(level=Level('3.5.0')), Mock(level=Level('3.5.0')), Mock(level=Level('3.6')), Mock(level=Level('5.0')), Mock(level=Level('5.9')) ] expected = [ Level('1.2'), Level('1.3'), Level('1.4'), Level('1.5'), Level('2.1.1'), Level('2.2.1'), Level('2.3.0'), Level('2.4.0'), Level('2.5'), Level('3.0'), Level('3.1') ] Document._reorder_automatic(mock_items, start=(1, 2), keep=mock_item) actual = [item.level for item in mock_items] self.assertListEqual(expected, actual) def test_reorder_from_index(self): """Verify items can be reordered from an index.""" mock_items = [] def mock_find_item(uid): """Return a mock item and store it.""" mock_item = MagicMock() if uid == 'bb': mock_item.level = Level('3.2') elif uid == 'bab': raise DoorstopError("unknown UID: bab") mock_item.uid = uid mock_items.append(mock_item) return mock_item mock_document = Mock() mock_document.find_item = mock_find_item data = { 'initial': 2.0, 'outline': [{ 'a': None }, { 'b': [{ 'ba': [{ 'baa': None }, { 'bab': None }, { 'bac': None }] }, { 'bb': None }] }, { 'c': None }] } expected = [ Level('2'), Level('3.0'), Level('3.1.0'), Level('3.1.1'), Level('3.1.3'), Level('3.2'), Level('4') ] # Act with patch('doorstop.common.read_text') as mock_read_text: with patch('doorstop.common.load_yaml', Mock(return_value=data)): Document._reorder_from_index(mock_document, 'mock_path') # Assert mock_read_text.assert_called_once_with('mock_path') actual = [item.level for item in mock_items] self.assertListEqual(expected, actual) def test_find_item(self): """Verify an item can be found by UID.""" item = self.document.find_item('req2') self.assertIsNot(None, item) def test_find_item_exact(self): """Verify an item can be found by its exact UID.""" item = self.document.find_item('req2-001') self.assertIsNot(None, item) def test_find_item_unknown_number(self): """Verify an exception is raised on an unknown number.""" self.assertRaises(DoorstopError, self.document.find_item, 'req99') def test_find_item_unknown_uid(self): """Verify an exception is raised on an unknown UID.""" self.assertRaises(DoorstopError, self.document.find_item, 'unknown99') @patch('doorstop.core.item.Item.get_issues') @patch('doorstop.core.document.Document.reorder') def test_validate(self, mock_reorder, mock_get_issues): """Verify a document can be validated.""" mock_get_issues.return_value = [DoorstopInfo('i')] with patch('doorstop.settings.REORDER', True): self.assertTrue(self.document.validate()) mock_reorder.assert_called_once_with(_items=self.document.items) self.assertEqual(5, mock_get_issues.call_count) @patch('doorstop.core.item.Item.get_issues', Mock(return_value=[ DoorstopError('error'), DoorstopWarning('warning'), DoorstopInfo('info') ])) def test_validate_item(self): """Verify an item error fails the document check.""" self.assertFalse(self.document.validate()) @patch('doorstop.core.item.Item.get_issues', Mock(return_value=[])) def test_validate_hook(self): """Verify an item hook can be called.""" mock_hook = MagicMock() self.document.validate(item_hook=mock_hook) self.assertEqual(5, mock_hook.call_count) @patch('doorstop.core.item.Item.delete') @patch('os.rmdir') def test_delete(self, mock_delete, mock_item_delete): """Verify a document can be deleted.""" self.document.delete() self.assertEqual(6, mock_item_delete.call_count) self.assertEqual(1, mock_delete.call_count) self.document.delete() # ensure a second delete is ignored @patch('doorstop.core.item.Item.delete') @patch('os.rmdir') def test_delete_with_assets(self, mock_delete, mock_item_delete): """Verify a document's assets aren't deleted.""" mock_delete.side_effect = OSError self.document.delete() self.assertEqual(6, mock_item_delete.call_count) self.assertEqual(1, mock_delete.call_count) self.document.delete() # ensure a second delete is ignored @patch('doorstop.core.item.Item.delete', Mock()) @patch('doorstop.common.delete', Mock()) def test_delete_cache(self): """Verify a deleted document is expunged.""" self.document.tree = Mock() self.document.tree._item_cache = {} self.document.tree._document_cache = {} self.document.delete() self.document.tree.vcs.delete.assert_called_once_with( self.document.config) self.assertIs(None, self.document.tree._document_cache[self.document.prefix]) @patch('doorstop.core.document.Document.get_issues', Mock(return_value=[])) def test_issues(self): """Verify an document's issues convenience property can be accessed.""" self.assertEqual(0, len(self.document.issues)) def test_issues_duplicate_level(self): """Verify duplicate item levels are detected.""" mock_item1 = Mock() mock_item1.uid = 'HLT001' mock_item1.level = Level('4.2') mock_item2 = Mock() mock_item2.uid = 'HLT002' mock_item2.level = Level('4.2') mock_items = [mock_item1, mock_item2] expected = DoorstopWarning("duplicate level: 4.2 (HLT001, HLT002)") issue = list(self.document._get_issues_level(mock_items))[0] self.assertIsInstance(issue, type(expected)) self.assertEqual(expected.args, issue.args) def test_issues_skipped_level_over(self): """Verify skipped (over) item levels are detected.""" mock_item1 = Mock() mock_item1.uid = 'HLT001' mock_item1.level = Level('1.1') mock_item2 = Mock() mock_item2.uid = 'HLT002' mock_item2.level = Level('1.3') mock_items = [mock_item1, mock_item2] expected = DoorstopWarning("skipped level: 1.1 (HLT001), 1.3 (HLT002)") issues = list(self.document._get_issues_level(mock_items)) self.assertEqual(1, len(issues)) self.assertIsInstance(issues[0], type(expected)) self.assertEqual(expected.args, issues[0].args) def test_issues_skipped_level_out(self): """Verify skipped (out) item levels are detected.""" mock_item1 = Mock() mock_item1.uid = 'HLT001' mock_item1.level = Level('1.1') mock_item2 = Mock() mock_item2.uid = 'HLT002' mock_item2.level = Level('3.0') mock_items = [mock_item1, mock_item2] expected = DoorstopWarning("skipped level: 1.1 (HLT001), 3.0 (HLT002)") issues = list(self.document._get_issues_level(mock_items)) self.assertEqual(1, len(issues)) self.assertIsInstance(issues[0], type(expected)) self.assertEqual(expected.args, issues[0].args) def test_issues_skipped_level_out_over(self): """Verify skipped (out and over) item levels are detected.""" mock_item1 = Mock() mock_item1.uid = 'HLT001' mock_item1.level = Level('1.1') mock_item2 = Mock() mock_item2.uid = 'HLT002' mock_item2.level = Level('2.2') mock_items = [mock_item1, mock_item2] expected = DoorstopWarning("skipped level: 1.1 (HLT001), 2.2 (HLT002)") issues = list(self.document._get_issues_level(mock_items)) self.assertEqual(1, len(issues)) self.assertIsInstance(issues[0], type(expected)) self.assertEqual(expected.args, issues[0].args)
class TestDocument(unittest.TestCase): """Unit tests for the Document class.""" # pylint: disable=no-value-for-parameter def setUp(self): self.document = MockDocument(FILES, root=ROOT) def test_init_invalid(self): """Verify a document cannot be initialized from an invalid path.""" self.assertRaises(DoorstopError, Document, 'not/a/path') def test_object_references(self): """Verify a standalone document does not have object references.""" self.assertIs(None, self.document.tree) def test_load_empty(self): """Verify loading calls read.""" self.document.load() self.document._read.assert_called_once_with(self.document.config) def test_load_error(self): """Verify an exception is raised with invalid YAML.""" self.document._file = "invalid: -" self.assertRaises(DoorstopError, self.document.load) def test_load_unexpected(self): """Verify an exception is raised for unexpected file contents.""" self.document._file = "unexpected" self.assertRaises(DoorstopError, self.document.load) def test_load(self): """Verify the document config can be loaded from file.""" self.document._file = YAML_CUSTOM self.document.load(reload=True) self.assertEqual('CUSTOM', self.document.prefix) self.assertEqual('-', self.document.sep) self.assertEqual(4, self.document.digits) def test_load_parent(self): """Verify the document config can be loaded from file with a parent.""" self.document._file = YAML_CUSTOM_PARENT self.document.load() self.assertEqual('PARENT', self.document.parent) def test_load_invalid(self): """Verify that an invalid document config raises an exception.""" self.document._file = YAML_INVALID msg = "^invalid value for 'digits' in: .*\\.doorstop.yml$" self.assertRaisesRegex(DoorstopError, msg, self.document.load) def test_load_unknown(self): """Verify loading a document config with an unknown key fails.""" self.document._file = YAML_UNKNOWN msg = "^unexpected document setting 'John' in: .*\\.doorstop.yml$" self.assertRaisesRegex(DoorstopError, msg, self.document.load) def test_load_unknown_attributes(self): """Verify loading a document config with unknown attributes fails.""" self.document._file = YAML_UNKNOWN_ATTRIBUTES msg = "^unexpected attributes configuration 'unknown' in: .*\\.doorstop.yml$" self.assertRaisesRegex(DoorstopError, msg, self.document.load) def test_load_with_non_existing_include(self): """Verify include of non-existing file fails.""" self.document._file = YAML_INCLUDE_NO_SUCH_FILE msg = "^include in '.*\\.doorstop.yml' failed: .*$" self.assertRaisesRegex(DoorstopError, msg, self.document.load) def test_load_extended_reviewed(self): """Verify loaded extended reviewed attribute keys of a document.""" self.document._file = YAML_EXTENDED_REVIEWED self.document.load() self.assertEqual( self.document.extended_reviewed, ['type', 'verification-method'] ) def test_load_custom_defaults(self): """Verify loaded custom defaults for attributes of a document.""" self.document._file = YAML_CUSTOM_DEFAULTS self.document.load() self.assertEqual( self.document._attribute_defaults, {'a': ['b', 'c'], 'd': {'e': 'f', 'g': 'h'}, 'i': 'j', 'k': None}, ) def test_load_defaults_via_include(self): """Verify loaded defaults for attributes via includes.""" self.document._file = YAML_INCLUDE_DEFAULTS self.document.load() self.assertEqual(self.document._attribute_defaults, {'text': 'Some text'}) def test_save_empty(self): """Verify saving calls write.""" self.document.tree = Mock() self.document.save() self.document._write.assert_called_once_with(YAML_DEFAULT, self.document.config) self.document.tree.vcs.edit.assert_called_once_with(self.document.config) def test_save_parent(self): """Verify a document can be saved with a parent.""" self.document.parent = 'SYS' self.document.save() self.assertIn("parent: SYS", self.document._file) def test_save_custom(self): """Verify a document can be saved with a custom attribute.""" self.document._data['custom'] = 'this' self.document.save() self.assertIn("custom: this", self.document._file) def test_save_extended_reviewed(self): """Verify saving of extended reviewed attribute keys.""" self.document._extended_reviewed = ['type', 'verification-method'] self.document.save() self.assertIn("attributes:", self.document._file) self.assertIn(" reviewed:", self.document._file) self.assertIn(" - type", self.document._file) self.assertIn(" - verification-method", self.document._file) def test_no_save_empty_extended_reviewed(self): """Verify not saving of empty extended reviewed attribute keys.""" self.document._extended_reviewed = [] self.document.save() self.assertNotIn("attributes:", self.document._file) self.assertNotIn(" reviewed:", self.document._file) def test_save_custom_defaults(self): """Verify saving of custom default attributes.""" self.document._attribute_defaults = {'key': 'value'} self.document.save() self.assertIn("attributes:", self.document._file) self.assertIn(" defaults:", self.document._file) self.assertIn(" key: value", self.document._file) def test_no_save_missing_custom_defaults(self): """Verify not saving of missing custom default attributes.""" self.document.save() self.assertNotIn("attributes:", self.document._file) self.assertNotIn(" defaults:", self.document._file) @patch('doorstop.common.verbosity', 2) def test_str(self): """Verify a document can be converted to a string.""" self.assertEqual("REQ", str(self.document)) @patch('doorstop.common.verbosity', 3) def test_str_verbose(self): """Verify a document can be converted to a string (verbose).""" relpath = os.path.relpath(self.document.path, self.document.root) text = "REQ (@{}{})".format(os.sep, relpath) self.assertEqual(text, str(self.document)) def test_ne(self): """Verify document non-equality is correct.""" self.assertNotEqual(self.document, None) def test_hash(self): """Verify documents can be hashed.""" document1 = MockDocument('path/to/fake1') document2 = MockDocument('path/to/fake2') document3 = MockDocument('path/to/fake2') my_set = set() # Act my_set.add(document1) my_set.add(document2) my_set.add(document3) # Assert self.assertEqual(2, len(my_set)) def test_len(self): """Verify a document has a length.""" self.assertEqual(5, len(self.document)) def test_items(self): """Verify the items in a document can be accessed.""" items = self.document.items self.assertEqual(5, len(items)) for item in self.document: logging.debug("item: {}".format(item)) self.assertIs(self.document, item.document) self.assertIs(self.document.tree, item.tree) def test_items_cache(self): """Verify the items in a document get cached.""" self.document.tree = Mock() self.document.tree._item_cache = {} print(self.document.items) self.assertEqual(6, len(self.document.tree._item_cache)) @patch('doorstop.core.document.Document', MockDocument) def test_new(self): """Verify a new document can be created with defaults.""" MockDocument._create.reset_mock() path = os.path.join(EMPTY, '.doorstop.yml') document = MockDocument.new(None, EMPTY, root=FILES, prefix='NEW', digits=2) self.assertEqual('NEW', document.prefix) self.assertEqual(2, document.digits) self.assertEqual(None, document._attribute_defaults) self.assertEqual([], document.extended_reviewed) MockDocument._create.assert_called_once_with(path, name='document') def test_new_existing(self): """Verify an exception is raised if the document already exists.""" self.assertRaises(DoorstopError, Document.new, None, FILES, ROOT, prefix='DUPL') @patch('doorstop.core.document.Document', MockDocument) def test_new_cache(self): """Verify a new documents are cached.""" mock_tree = Mock() mock_tree._document_cache = {} document = MockDocument.new( mock_tree, EMPTY, root=FILES, prefix='NEW', digits=2 ) mock_tree.vcs.add.assert_called_once_with(document.config) self.assertEqual(document, mock_tree._document_cache[document.prefix]) def test_invalid(self): """Verify an exception is raised on an invalid document.""" self.assertRaises(DoorstopError, Document, EMPTY) def test_relpath(self): """Verify the document's relative path string can be determined.""" relpath = os.path.relpath(self.document.path, self.document.root) text = "@{}{}".format(os.sep, relpath) self.assertEqual(text, self.document.relpath) def test_sep(self): """Verify an documents's separator can be set and read.""" self.document.sep = '_' self.assertIn("sep: _\n", self.document._write.call_args[0][0]) self.assertEqual('_', self.document.sep) def test_sep_invalid(self): """Verify an invalid separator is rejected.""" self.assertRaises(AssertionError, setattr, self.document, 'sep', '?') def test_digits(self): """Verify an documents's digits can be set and read.""" self.document.digits = 42 self.assertIn("digits: 42\n", self.document._write.call_args[0][0]) self.assertEqual(42, self.document.digits) def test_depth(self): """Verify the maximum item level depth can be determined.""" self.assertEqual(3, self.document.depth) def test_next_number(self): """Verify the next item number can be determined.""" self.assertEqual(6, self.document.next_number) def test_next_number_server(self): """Verify the next item number can be determined with a server.""" self.document.tree = MagicMock() self.document.tree.request_next_number = Mock(side_effect=[1, 42]) self.assertEqual(42, self.document.next_number) def test_index_get(self): """Verify a document's index can be retrieved.""" self.assertIs(None, self.document.index) with patch('os.path.isfile', Mock(return_value=True)): path = os.path.join(self.document.path, self.document.INDEX) self.assertEqual(path, self.document.index) @patch('doorstop.common.write_lines') @patch('doorstop.settings.MAX_LINE_LENGTH', 40) def test_index_set(self, mock_write_lines): """Verify an document's index can be created.""" lines = [ 'initial: 1.2.3', 'outline:', ' - REQ001: # Lorem ipsum d...', ' - REQ003: # Unicode: -40° ±1%', ' - REQ004: # Hello, world!', ' - REQ002: # Hello, world!', ' - REQ2-001: # Hello, world!', ] # Act self.document.index = True # create index # Assert gen, path = mock_write_lines.call_args[0] lines2 = list(gen)[8:] # skip lines of info comments self.assertListEqual(lines, lines2) self.assertEqual(os.path.join(FILES, 'index.yml'), path) @patch('doorstop.common.write_lines') @patch('doorstop.settings.MAX_LINE_LENGTH', 40) def test_read_index(self, mock_write_lines): """Verify a document index can be read.""" lines = '''initial: 1.2.3 outline: - REQ001: # Lorem ipsum d... - REQ003: # Unicode: -40° ±1% - REQ004: # Hello, world! !['.. - REQ002: # Hello, world! !["... - REQ2-001: # Hello, world!''' expected = { 'initial': '1.2.3', 'outline': [ {'REQ001': [{'text': 'Lorem ipsum d...'}]}, {'REQ003': [{'text': 'Unicode: -40° ±1%'}]}, {'REQ004': [{'text': "Hello, world! !['.."}]}, {'REQ002': [{'text': 'Hello, world! !["...'}]}, {'REQ2-001': [{'text': 'Hello, world!'}]}, ], } # Act with patch('builtins.open') as mock_open: mock_open.side_effect = lambda *args, **kw: mock.mock_open( read_data=lines ).return_value actual = self.document._read_index('mock_path') # Check string can be parsed as yaml # Assert self.assertEqual(expected, actual) @patch('doorstop.common.delete') def test_index_del(self, mock_delete): """Verify a document's index can be deleted.""" del self.document.index mock_delete.assert_called_once_with(None) @patch('doorstop.core.document.Document._reorder_automatic') @patch('doorstop.core.item.Item.new') def test_add_item(self, mock_new, mock_reorder): """Verify an item can be added to a document.""" with patch('doorstop.settings.REORDER', True): self.document.add_item() mock_new.assert_called_once_with( None, self.document, FILES, ROOT, 'REQ006', level=Level('2.2') ) self.assertEqual(0, mock_reorder.call_count) @patch('doorstop.core.document.Document.reorder') @patch('doorstop.core.item.Item.new') def test_add_item_with_level(self, mock_new, mock_reorder): """Verify an item can be added to a document with a level.""" with patch('doorstop.settings.REORDER', True): item = self.document.add_item(level='4.2') mock_new.assert_called_once_with( None, self.document, FILES, ROOT, 'REQ006', level='4.2' ) mock_reorder.assert_called_once_with(keep=item) @patch('doorstop.core.item.Item.new') def test_add_item_with_number(self, mock_new): """Verify an item can be added to a document with a number.""" self.document.add_item(number=999) mock_new.assert_called_once_with( None, self.document, FILES, ROOT, 'REQ999', level=Level('2.2') ) @patch('doorstop.core.item.Item.set_attributes') def test_add_item_with_defaults(self, mock_set_attributes): """Verify an item can be added to a document with defaults.""" self.document._file = "text: 'abc'" self.document.add_item(defaults='mock.yml') mock_set_attributes.assert_called_once_with({'text': 'abc'}) @patch('doorstop.core.item.Item.new') def test_add_item_empty(self, mock_new): """Verify an item can be added to an new document.""" document = MockDocument(NEW, ROOT) document.prefix = 'NEW' self.assertIsNot(None, document.add_item(reorder=False)) mock_new.assert_called_once_with( None, document, NEW, ROOT, 'NEW001', level=None ) @patch('doorstop.core.item.Item.new') def test_add_item_after_header(self, mock_new): """Verify the next item after a header is indented.""" mock_item = Mock() mock_item.number = 1 mock_item.level = Level('1.0') self.document._iter = Mock(return_value=[mock_item]) self.document.add_item() mock_new.assert_called_once_with( None, self.document, FILES, ROOT, 'REQ002', level=Level('1.1') ) def test_add_item_contains(self): """Verify an added item is contained in the document.""" item = self.document.items[0] self.assertIn(item, self.document) item2 = self.document.add_item(reorder=False) self.assertIn(item2, self.document) def test_add_item_cache(self): """Verify an added item is cached.""" self.document.tree = Mock() self.document.tree._item_cache = {} self.document.tree.request_next_number = None item = self.document.add_item(reorder=False) self.assertEqual(item, self.document.tree._item_cache[item.uid]) @patch('doorstop.core.document.Document._reorder_automatic') @patch('os.remove') def test_remove_item(self, mock_remove, mock_reorder): """Verify an item can be removed.""" with patch('doorstop.settings.REORDER', True): item = self.document.remove_item('REQ001') mock_reorder.assert_called_once_with(self.document.items, keep=None, start=None) mock_remove.assert_called_once_with(item.path) @patch('os.remove') def test_remove_item_contains(self, mock_remove): """Verify a removed item is not contained in the document.""" item = self.document.items[0] self.assertIn(item, self.document) removed_item = self.document.remove_item(item.uid, reorder=False) self.assertEqual(item, removed_item) self.assertNotIn(item, self.document) mock_remove.assert_called_once_with(item.path) @patch('os.remove') def test_remove_item_by_item(self, mock_remove): """Verify an item can be removed (by item).""" item = self.document.items[0] self.assertIn(item, self.document) removed_item = self.document.remove_item(item, reorder=False) self.assertEqual(item, removed_item) mock_remove.assert_called_once_with(item.path) @patch('os.remove', Mock()) def test_remove_item_cache(self): """Verify a removed item is expunged.""" self.document.tree = Mock() self.document.tree._item_cache = {} item = self.document.items[0] removed_item = self.document.remove_item(item.uid, reorder=False) self.assertIs(None, self.document.tree._item_cache[removed_item.uid]) @patch('doorstop.core.document.Document._reorder_automatic') @patch('doorstop.core.document.Document._reorder_from_index') def test_reorder(self, mock_index, mock_auto): """Verify items can be reordered.""" path = os.path.join(self.document.path, 'index.yml') common.touch(path) # Act self.document.reorder() # Assert mock_index.assert_called_once_with(self.document, path) mock_auto.assert_called_once_with(self.document.items, start=None, keep=None) def test_reorder_automatic(self): """Verify items can be reordered automatically.""" mock_items = [ Mock(level=Level('2.3')), Mock(level=Level('2.3')), Mock(level=Level('2.7')), Mock(level=Level('3.2.2')), Mock(level=Level('3.4.2')), Mock(level=Level('3.5.0')), Mock(level=Level('3.5.0')), Mock(level=Level('3.6')), Mock(level=Level('5.0')), Mock(level=Level('5.9')), ] expected = [ Level('2.3'), Level('2.4'), Level('2.5'), Level('3.1.1'), Level('3.2.1'), Level('3.3.0'), Level('3.4.0'), Level('3.5'), Level('4.0'), Level('4.1'), ] Document._reorder_automatic(mock_items) actual = [item.level for item in mock_items] self.assertListEqual(expected, actual) def test_reorder_automatic_no_change(self): """Verify already ordered items can be reordered.""" mock_items = [ Mock(level=Level('1.1')), Mock(level=Level('1.1.1.1')), Mock(level=Level('2')), Mock(level=Level('3')), Mock(level=Level('4.1.1')), ] expected = [ Level('1.1'), Level('1.1.1.1'), Level('2'), Level('3'), Level('4.1.1'), ] Document._reorder_automatic(mock_items) actual = [item.level for item in mock_items] self.assertListEqual(expected, actual) def test_reorder_automatic_with_start(self): """Verify items can be reordered with a given start.""" mock_item = Mock(level=Level('2.3')) mock_items = [ Mock(level=Level('2.2')), mock_item, Mock(level=Level('2.3')), Mock(level=Level('2.7')), Mock(level=Level('3.2.2')), Mock(level=Level('3.4.2')), Mock(level=Level('3.5.0')), Mock(level=Level('3.5.0')), Mock(level=Level('3.6')), Mock(level=Level('5.0')), Mock(level=Level('5.9')), ] expected = [ Level('1.2'), Level('1.3'), Level('1.4'), Level('1.5'), Level('2.1.1'), Level('2.2.1'), Level('2.3.0'), Level('2.4.0'), Level('2.5'), Level('3.0'), Level('3.1'), ] Document._reorder_automatic(mock_items, start=(1, 2), keep=mock_item) actual = [item.level for item in mock_items] self.assertListEqual(expected, actual) def test_find_item(self): """Verify an item can be found by UID.""" item = self.document.find_item('req2') self.assertIsNot(None, item) def test_find_item_exact(self): """Verify an item can be found by its exact UID.""" item = self.document.find_item('req2-001') self.assertIsNot(None, item) def test_find_item_unknown_number(self): """Verify an exception is raised on an unknown number.""" self.assertRaises(DoorstopError, self.document.find_item, 'req99') def test_find_item_unknown_uid(self): """Verify an exception is raised on an unknown UID.""" self.assertRaises(DoorstopError, self.document.find_item, 'unknown99') @patch('doorstop.core.item.Item.get_issues') @patch('doorstop.core.document.Document.reorder') def test_validate(self, mock_reorder, mock_get_issues): """Verify a document can be validated.""" mock_get_issues.return_value = [DoorstopInfo('i')] with patch('doorstop.settings.REORDER', True): self.assertTrue(self.document.validate()) mock_reorder.assert_called_once_with(_items=self.document.items) self.assertEqual(5, mock_get_issues.call_count) @patch( 'doorstop.core.item.Item.get_issues', Mock( return_value=[ DoorstopError('error'), DoorstopWarning('warning'), DoorstopInfo('info'), ] ), ) def test_validate_item(self): """Verify an item error fails the document check.""" self.assertFalse(self.document.validate()) @patch('doorstop.core.item.Item.get_issues', Mock(return_value=[])) def test_validate_hook(self): """Verify an item hook can be called.""" mock_hook = MagicMock() self.document.validate(item_hook=mock_hook) self.assertEqual(5, mock_hook.call_count) @patch('doorstop.core.item.Item.delete') @patch('os.rmdir') def test_delete(self, mock_delete, mock_item_delete): """Verify a document can be deleted.""" self.document.delete() self.assertEqual(6, mock_item_delete.call_count) self.assertEqual(1, mock_delete.call_count) self.document.delete() # ensure a second delete is ignored @patch('doorstop.core.item.Item.delete') @patch('os.rmdir') def test_delete_with_assets(self, mock_delete, mock_item_delete): """Verify a document's assets aren't deleted.""" mock_delete.side_effect = OSError self.document.delete() self.assertEqual(6, mock_item_delete.call_count) self.assertEqual(1, mock_delete.call_count) self.document.delete() # ensure a second delete is ignored @patch('doorstop.core.item.Item.delete', Mock()) @patch('doorstop.common.delete', Mock()) def test_delete_cache(self): """Verify a deleted document is expunged.""" self.document.tree = Mock() self.document.tree._item_cache = {} self.document.tree._document_cache = {} self.document.delete() self.document.tree.vcs.delete.assert_called_once_with(self.document.config) self.assertIs(None, self.document.tree._document_cache[self.document.prefix]) @patch('doorstop.core.document.Document.get_issues', Mock(return_value=[])) def test_issues(self): """Verify an document's issues convenience property can be accessed.""" self.assertEqual(0, len(self.document.issues)) def test_issues_duplicate_level(self): """Verify duplicate item levels are detected.""" mock_item1 = Mock() mock_item1.uid = 'HLT001' mock_item1.level = Level('4.2') mock_item2 = Mock() mock_item2.uid = 'HLT002' mock_item2.level = Level('4.2') mock_items = [mock_item1, mock_item2] expected = DoorstopWarning("duplicate level: 4.2 (HLT001, HLT002)") issue = list(self.document._get_issues_level(mock_items))[0] self.assertIsInstance(issue, type(expected)) self.assertEqual(expected.args, issue.args) def test_issues_skipped_level_over(self): """Verify skipped (over) item levels are detected.""" mock_item1 = Mock() mock_item1.uid = 'HLT001' mock_item1.level = Level('1.1') mock_item2 = Mock() mock_item2.uid = 'HLT002' mock_item2.level = Level('1.3') mock_items = [mock_item1, mock_item2] expected = DoorstopWarning("skipped level: 1.1 (HLT001), 1.3 (HLT002)") issues = list(self.document._get_issues_level(mock_items)) self.assertEqual(1, len(issues)) self.assertIsInstance(issues[0], type(expected)) self.assertEqual(expected.args, issues[0].args) def test_issues_skipped_level_out(self): """Verify skipped (out) item levels are detected.""" mock_item1 = Mock() mock_item1.uid = 'HLT001' mock_item1.level = Level('1.1') mock_item2 = Mock() mock_item2.uid = 'HLT002' mock_item2.level = Level('3.0') mock_items = [mock_item1, mock_item2] expected = DoorstopWarning("skipped level: 1.1 (HLT001), 3.0 (HLT002)") issues = list(self.document._get_issues_level(mock_items)) self.assertEqual(1, len(issues)) self.assertIsInstance(issues[0], type(expected)) self.assertEqual(expected.args, issues[0].args) def test_issues_skipped_level_out_over(self): """Verify skipped (out and over) item levels are detected.""" mock_item1 = Mock() mock_item1.uid = 'HLT001' mock_item1.level = Level('1.1') mock_item2 = Mock() mock_item2.uid = 'HLT002' mock_item2.level = Level('2.2') mock_items = [mock_item1, mock_item2] expected = DoorstopWarning("skipped level: 1.1 (HLT001), 2.2 (HLT002)") issues = list(self.document._get_issues_level(mock_items)) self.assertEqual(1, len(issues)) self.assertIsInstance(issues[0], type(expected)) self.assertEqual(expected.args, issues[0].args) @patch('os.path.isdir', Mock(return_value=True)) def test_assets_exist(self): """Verify a document can report the existence of the assets folder.""" path = os.path.join(self.document.path, self.document.ASSETS) self.assertEqual(path, self.document.assets) @patch('os.path.isdir', Mock(return_value=False)) def test_assets_missing(self): """Verify a document can report the existence of the assets folder.""" self.assertEqual(None, self.document.assets) @patch('os.path.isdir', Mock(return_value=True)) @patch('glob.glob') @patch('shutil.copytree') def test_copy_assets(self, mock_copytree, mock_glob): """Verify a document can copy its assets""" assets = ['css', 'logo.png'] assets_full_path = [ os.path.join(self.document.path, self.document.ASSETS, dir) for dir in assets ] mock_glob.return_value = assets_full_path dst = os.path.join('publishdir', 'assets') expected_calls = [ call(assets_full_path[0], os.path.join(dst, assets[0])), call(assets_full_path[1], os.path.join(dst, assets[1])), ] # Act] self.document.copy_assets(dst) # Assert self.assertEqual(expected_calls, mock_copytree.call_args_list) @patch('os.path.exists', Mock(return_value=True)) @patch('os.path.isdir', Mock(return_value=True)) @patch('glob.glob') @patch('shutil.copytree') def test_copy_assets_skipping(self, mock_copytree, mock_glob): """Verify duplicate file or directory names are skipped """ assets = ['doorstop'] mock_glob.return_value = assets mock_copytree.side_effect = FileExistsError dst = os.path.join('publishdir', 'assets') # Act] self.document.copy_assets(dst) # Assert self.assertEqual([], mock_copytree.call_args_list)