Example #1
0
 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
     )
Example #2
0
 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)
Example #3
0
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)
Example #4
0
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_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)[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.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)