diff --git a/doorstop/core/tests/test_item_validator.py b/doorstop/core/tests/test_item_validator.py index 28e58da63..853eeee27 100644 --- a/doorstop/core/tests/test_item_validator.py +++ b/doorstop/core/tests/test_item_validator.py @@ -299,3 +299,76 @@ def mock_iter2(self): # pylint: disable=W0613 self.item.tree = mock_tree self.assertTrue(self.item_validator.validate(self.item)) + + @patch("doorstop.settings.CHECK_CHILD_LINKS_STRICT", True) + def test_validate_strict_child_links(self): + """Verify root items are linked to from all child documents""" + root_doc = MockSimpleDocument() + root_doc.prefix = "ROOT" + + child_doc_a = MockSimpleDocument() + child_doc_a.prefix = "CHILD_A" + child_doc_a.parent = root_doc.prefix + + child_doc_b = MockSimpleDocument() + child_doc_b.prefix = "CHILD_B" + child_doc_b.parent = root_doc.prefix + + root_doc.children = [child_doc_a.prefix, child_doc_b.prefix] + + root_item_a = MockItem(root_doc, "ROOT001.yml") + root_item_b = MockItem(root_doc, "ROOT002.yml") + child_item_a = MockItem(child_doc_a, "CHILD_A001.yml") + child_item_b = MockItem(child_doc_b, "CHILD_B001.yml") + + all_items = [root_item_a, root_item_b, child_item_a, child_item_b] + + root_doc.set_items([root_item_a, root_item_b]) + child_doc_a.set_items([child_item_a]) + child_doc_b.set_items([child_item_b]) + + def mock_iter(seq): + """Create a mock __iter__ method.""" + + def _iter(self): # pylint: disable=W0613 + """Mock __iter__method.""" + yield from seq + + return _iter + + mock_tree = Mock() + mock_tree.__iter__ = mock_iter([root_doc, child_doc_a, child_doc_b]) + mock_tree.find_item = lambda uid: next( + filter(lambda item: item.uid == uid, all_items), None + ) + + for item in all_items: + item.text = "text" + item.tree = mock_tree + + # Only create a link from each child document + # to a different item in the root document, such + # that not every item in the root has a child link + # from every document + child_item_a.links = [root_item_a.uid] + child_item_b.links = [root_item_b.uid] + + self.item_validator = MockItemValidator() + issues = list(self.item_validator.get_issues(root_item_a)) + self.assertEqual(len(issues), 1) + self.assertIn("no links from document: CHILD_B", "{}".format(issues)) + + issues = list(self.item_validator.get_issues(root_item_b)) + self.assertEqual(len(issues), 1) + self.assertIn("no links from document: CHILD_A", "{}".format(issues)) + + # Now make sure that every item in the root is + # linked to by an item from every child document + child_item_a.links = [root_item_a.uid, root_item_b.uid] + child_item_b.links = [root_item_b.uid, root_item_a.uid] + + issues = list(self.item_validator.get_issues(root_item_a)) + self.assertEqual(len(issues), 0) + + issues = list(self.item_validator.get_issues(root_item_b)) + self.assertEqual(len(issues), 0) diff --git a/scent.py b/scent.py index 01ed57f9a..898af8494 100644 --- a/scent.py +++ b/scent.py @@ -7,6 +7,7 @@ import subprocess from sniffer.api import select_runnable, file_validator, runnable + try: from pync import Notifier except ImportError: @@ -24,30 +25,30 @@ class Options: rerun_args = None targets = [ - (('make', 'test-unit', 'DISABLE_COVERAGE=true'), "Unit Tests", True), - (('make', 'test-int'), "Integration Tests", False), - (('make', 'check'), "Static Analysis", True), - (('make', 'demo'), "Run Demo", False), - (('make', 'docs'), None, True), + (("make", "test-unit", "DISABLE_COVERAGE=true"), "Unit Tests", True), + (("make", "test-int"), "Integration Tests", False), + (("make", "check"), "Static Analysis", True), + (("make", "demo"), "Run Demo", False), + (("make", "docs"), None, True), ] -@select_runnable('run_targets') +@select_runnable("run_targets") @file_validator def python_files(filename): - return filename.endswith('.py') + return filename.endswith(".py") -@select_runnable('run_targets') +@select_runnable("run_targets") @file_validator def html_files(filename): - return filename.split('.')[-1] in ['html', 'css', 'js'] + return filename.split(".")[-1] in ["html", "css", "js"] @runnable def run_targets(*args): """Run targets for Python.""" - Options.show_coverage = 'coverage' in args + Options.show_coverage = "coverage" in args count = 0 for count, (command, title, retry) in enumerate(Options.targets, start=1): @@ -77,7 +78,7 @@ def call(command, title, retry): return False print("") - print("$ %s" % ' '.join(command)) + print("$ %s" % " ".join(command)) failure = subprocess.call(command) if failure and retry: @@ -95,6 +96,6 @@ def show_notification(message, title): def show_coverage(): """Launch the coverage report.""" if Options.show_coverage: - subprocess.call(['make', 'read-coverage']) + subprocess.call(["make", "read-coverage"]) Options.show_coverage = False