diff --git a/doorstop/core/publishers/tests/test_publisher_html.py b/doorstop/core/publishers/tests/test_publisher_html.py index 58a389f0..5c6406a1 100644 --- a/doorstop/core/publishers/tests/test_publisher_html.py +++ b/doorstop/core/publishers/tests/test_publisher_html.py @@ -325,6 +325,7 @@ def test_toc_no_links_or_heading_levels(self): {"depth": 2, "text": "1.6 Hello, world! (REQ004)", "uid": ""}, {"depth": 2, "text": "2.1 Plantuml (REQ002)", "uid": ""}, {"depth": 2, "text": "2.1 Hello, world! (REQ2-001)", "uid": ""}, + {"depth": 1, "text": "3.0 My Heading", "uid": ""}, ] html_publisher = publisher.check(".html", self.document) toc = html_publisher.table_of_contents(linkify=None, obj=self.document) @@ -345,6 +346,7 @@ def test_toc_no_links(self): {"depth": 2, "text": "Hello, world! (REQ004)", "uid": ""}, {"depth": 2, "text": "Plantuml (REQ002)", "uid": ""}, {"depth": 2, "text": "Hello, world! (REQ2-001)", "uid": ""}, + {"depth": 1, "text": "My Heading", "uid": ""}, ] html_publisher = publisher.check(".html", self.document) @@ -373,6 +375,7 @@ def test_toc(self): "text": "2.1 Hello, world! (REQ2-001)", "uid": UID("REQ2-001"), }, + {"depth": 1, "text": "3.0 My Heading", "uid": UID("REQ007")}, ] html_publisher = publisher.check(".html", self.document) toc = html_publisher.table_of_contents(linkify=True, obj=self.document) diff --git a/doorstop/core/publishers/tests/test_publisher_markdown.py b/doorstop/core/publishers/tests/test_publisher_markdown.py index e10bcead..3d2ab791 100644 --- a/doorstop/core/publishers/tests/test_publisher_markdown.py +++ b/doorstop/core/publishers/tests/test_publisher_markdown.py @@ -230,7 +230,8 @@ def test_toc_no_links_or_heading_levels(self): * 1.5 Hello, world! (REQ006) * 1.6 Hello, world! (REQ004) * 2.1 Plantuml (REQ002) - * 2.1 Hello, world! (REQ2-001)\n""" + * 2.1 Hello, world! (REQ2-001) + * 3.0 My Heading\n""" md_publisher = publisher.check(".md", self.document) toc = md_publisher.table_of_contents(linkify=None, obj=self.document) self.assertEqual(expected, toc) @@ -246,6 +247,7 @@ def test_toc_no_links(self): * Hello, world! (REQ004) * Plantuml (REQ002) * Hello, world! (REQ2-001) + * My Heading """ md_publisher = publisher.check(".md", self.document) toc = md_publisher.table_of_contents(linkify=None, obj=self.document) @@ -260,7 +262,8 @@ def test_toc(self): * [1.5 Hello, world! (REQ006)](#15-req006-req006) * [1.6 Hello, world! (REQ004)](#16-req004-req004) * [2.1 Plantuml (REQ002)](#21-plantuml-req002-req002) - * [2.1 Hello, world! (REQ2-001)](#21-req2-001-req2-001)\n""" + * [2.1 Hello, world! (REQ2-001)](#21-req2-001-req2-001) + * [3.0 My Heading](#30-my-heading-req007)\n""" self.maxDiff = None md_publisher = publisher.check(".md", self.document) toc = md_publisher.table_of_contents(linkify=True, obj=self.document) diff --git a/doorstop/core/tests/__init__.py b/doorstop/core/tests/__init__.py index aca01c4b..22857d8a 100644 --- a/doorstop/core/tests/__init__.py +++ b/doorstop/core/tests/__init__.py @@ -17,6 +17,19 @@ TESTS_ROOT = os.path.dirname(__file__) FILES = os.path.join(os.path.dirname(__file__), "files") +# Files that use the golden master pattern and are intentionally +# updated by tests - these should not be reset after each test run +GOLDEN_MASTER_FILES = { + os.path.join(FILES, "exported.yml"), + os.path.join(FILES, "exported.csv"), + os.path.join(FILES, "exported.tsv"), + os.path.join(FILES, "published.html"), + os.path.join(FILES, "published.md"), + os.path.join(FILES, "published.txt"), + os.path.join(FILES, "published2.html"), + os.path.join(FILES, "published2.md"), + os.path.join(FILES, "published2.txt"), +} FILES_MD = os.path.join(os.path.dirname(__file__), "files_md") SYS = os.path.join(FILES, "parent") TST = os.path.join(FILES, "child") diff --git a/doorstop/core/tests/files/REQ007.yml b/doorstop/core/tests/files/REQ007.yml new file mode 100644 index 00000000..25fea3bf --- /dev/null +++ b/doorstop/core/tests/files/REQ007.yml @@ -0,0 +1,10 @@ +active: true +derived: false +header: | + My Heading +level: 3.0 +links: [] +normative: false +ref: '' +reviewed: null +text: '' diff --git a/doorstop/core/tests/files/exported.csv b/doorstop/core/tests/files/exported.csv index 2254cb6b..ab14fef3 100644 --- a/doorstop/core/tests/files/exported.csv +++ b/doorstop/core/tests/files/exported.csv @@ -38,3 +38,4 @@ Test Math Expressions in Latex Style: Inline Style 1: $a \ne 0$ Inline Style 2: \(ax^2 + bx + c = 0\) Multiline: $$x = {-b \pm \sqrt{b^2-4ac} \over 2a}.$$",,,REQ001,True,False,,True, +REQ007,3.0,,,,,True,False,My Heading,False, diff --git a/doorstop/core/tests/files/exported.tsv b/doorstop/core/tests/files/exported.tsv index 11b64e22..e4b4fea5 100644 --- a/doorstop/core/tests/files/exported.tsv +++ b/doorstop/core/tests/files/exported.tsv @@ -38,3 +38,4 @@ Test Math Expressions in Latex Style: Inline Style 1: $a \ne 0$ Inline Style 2: \(ax^2 + bx + c = 0\) Multiline: $$x = {-b \pm \sqrt{b^2-4ac} \over 2a}.$$" REQ001 True False True +REQ007 3.0 True False My Heading False diff --git a/doorstop/core/tests/files/exported.yml b/doorstop/core/tests/files/exported.yml index 94bb89c3..9a967913 100644 --- a/doorstop/core/tests/files/exported.yml +++ b/doorstop/core/tests/files/exported.yml @@ -113,3 +113,15 @@ REQ2-001: Inline Style 2: \(ax^2 + bx + c = 0\) Multiline: $$x = {-b \pm \sqrt{b^2-4ac} \over 2a}.$$ +REQ007: + active: true + derived: false + header: | + My Heading + level: 3.0 + links: [] + normative: false + ref: '' + reviewed: null + text: '' + diff --git a/doorstop/core/tests/files/published.html b/doorstop/core/tests/files/published.html index 33a8394e..3f533ad9 100644 --- a/doorstop/core/tests/files/published.html +++ b/doorstop/core/tests/files/published.html @@ -90,7 +90,14 @@ data-bs-placement="left" title="REQ2-001">2.1 Hello, world! (REQ2-001) - + +
  • + 3.0 My Heading +
  • @@ -186,6 +193,7 @@

    2.1 REQ2-001

    Multiline: $$x = {-b \pm \sqrt{b^2-4ac} \over 2a}.$$

    Parent links: REQ001

    Child links: TST001

    +

    3.0 My Heading

    diff --git a/doorstop/core/tests/files/published.md b/doorstop/core/tests/files/published.md index d4f595fa..cc556968 100644 --- a/doorstop/core/tests/files/published.md +++ b/doorstop/core/tests/files/published.md @@ -70,3 +70,5 @@ Multiline: $$x = {-b \pm \sqrt{b^2-4ac} \over 2a}.$$ *Child links: TST001* +# 3.0 My Heading {#REQ007} + diff --git a/doorstop/core/tests/files/published.txt b/doorstop/core/tests/files/published.txt index 2a6c6880..f713b9e7 100644 --- a/doorstop/core/tests/files/published.txt +++ b/doorstop/core/tests/files/published.txt @@ -73,3 +73,5 @@ Child links: TST001 +3.0 My Heading + diff --git a/doorstop/core/tests/files/published2.html b/doorstop/core/tests/files/published2.html index cf6e6d39..e54321a9 100644 --- a/doorstop/core/tests/files/published2.html +++ b/doorstop/core/tests/files/published2.html @@ -90,7 +90,14 @@ data-bs-placement="left" title="REQ2-001">2.1 Hello, world! (REQ2-001) - + +
  • + 3.0 My Heading +
  • @@ -184,6 +191,7 @@

    2.1 REQ2-001

    Inline Style 2: (ax^2 + bx + c = 0) Multiline: $$x = {-b \pm \sqrt{b^2-4ac} \over 2a}.$$

    Links: REQ001

    +

    3.0 My Heading

    diff --git a/doorstop/core/tests/files/published2.md b/doorstop/core/tests/files/published2.md index cf6447b6..c397ceb7 100644 --- a/doorstop/core/tests/files/published2.md +++ b/doorstop/core/tests/files/published2.md @@ -66,3 +66,5 @@ Multiline: $$x = {-b \pm \sqrt{b^2-4ac} \over 2a}.$$ *Links: REQ001* +# 3.0 My Heading {#REQ007} + diff --git a/doorstop/core/tests/files/published2.txt b/doorstop/core/tests/files/published2.txt index 1a8ff99a..ff2b7b66 100644 --- a/doorstop/core/tests/files/published2.txt +++ b/doorstop/core/tests/files/published2.txt @@ -69,3 +69,5 @@ Links: REQ001 +3.0 My Heading + diff --git a/doorstop/core/tests/test_all.py b/doorstop/core/tests/test_all.py index add9c84b..6cd1a8e1 100644 --- a/doorstop/core/tests/test_all.py +++ b/doorstop/core/tests/test_all.py @@ -28,6 +28,7 @@ ENV, FILES, FILES_MD, + GOLDEN_MASTER_FILES, REASON, ROOT, SYS, @@ -77,6 +78,17 @@ def cleanup_test_yml_files(): if result.returncode == 0 and result.stdout.strip(): yaml_files = result.stdout.strip().split("\n") + # Exclude golden master files from reset + yaml_files = [ + f + for f in yaml_files + if os.path.abspath(os.path.join(repo_root, f)) + not in GOLDEN_MASTER_FILES + ] + + if not yaml_files: + continue + # Reset only these YAML files subprocess.run( ["git", "-c", "core.autocrlf=false", "checkout", "--"] + yaml_files, @@ -215,7 +227,7 @@ def test_load(self): self.assertEqual("REQ", doc.prefix) self.assertEqual("yaml", doc.itemformat) self.assertEqual(2, doc.digits) - self.assertEqual(6, len(doc.items)) + self.assertEqual(7, len(doc.items)) def test_new(self): """Verify a new document can be created.""" @@ -238,7 +250,7 @@ def test_issues_count(self): issues = self.document.issues for issue in self.document.issues: logging.info(repr(issue)) - self.assertEqual(13, len(issues)) + self.assertEqual(15, len(issues)) @patch("doorstop.settings.REORDER", False) @patch("doorstop.settings.REVIEW_NEW_ITEMS", False) @@ -394,7 +406,7 @@ def test_issues_count(self): issues = self.tree.issues for issue in self.tree.issues: logging.info(repr(issue)) - self.assertEqual(15, len(issues)) + self.assertEqual(17, len(issues)) @patch("doorstop.settings.REORDER", False) @patch("doorstop.settings.REVIEW_NEW_ITEMS", False) @@ -600,8 +612,13 @@ def test_export_yml(self): # Assert self.assertIs(temp, path2) actual = read_yml(temp) + # Assert + if actual != expected: + common.log.error(f"Published content changed: {path}") + move_file(temp, path) + else: + common.delete(temp) # clean up in case the file didn't change self.assertEqual(expected, actual) - move_file(temp, path) def test_export_csv(self): """Verify a document can be exported as a CSV file.""" @@ -613,8 +630,13 @@ def test_export_csv(self): # Assert self.assertIs(temp, path2) actual = read_csv(temp) + # Assert + if actual != expected: + common.log.error(f"Published content changed: {path}") + move_file(temp, path) + else: + common.delete(temp) # clean up in case the file didn't change self.assertEqual(expected, actual) - move_file(temp, path) @patch("doorstop.settings.REVIEW_NEW_ITEMS", False) def test_export_tsv(self): @@ -627,8 +649,13 @@ def test_export_tsv(self): # Assert self.assertIs(temp, path2) actual = read_csv(temp, delimiter="\t") + # Assert + if actual != expected: + common.log.error(f"Published content changed: {path}") + move_file(temp, path) + else: + common.delete(temp) # clean up in case the file didn't change self.assertEqual(expected, actual) - move_file(temp, path) class TestPublisher(unittest.TestCase): @@ -684,8 +711,10 @@ def test_lines_text_document(self): lines = core.publisher.publish_lines(self.document, ".txt") text = "".join(line + "\n" for line in lines) # Assert + if text != expected: + common.log.error(f"Published content changed: {path}") + common.write_text(text, path) self.assertEqual(expected, text) - common.write_text(text, path) @patch("doorstop.settings.PUBLISH_CHILD_LINKS", False) def test_lines_text_document_without_child_links(self): @@ -696,8 +725,10 @@ def test_lines_text_document_without_child_links(self): lines = core.publisher.publish_lines(self.document, ".txt") text = "".join(line + "\n" for line in lines) # Assert + if text != expected: + common.log.error(f"Published content changed: {path}") + common.write_text(text, path) self.assertEqual(expected, text) - common.write_text(text, path) def test_lines_markdown_document(self): """Verify Markdown can be published from a document.""" @@ -709,7 +740,7 @@ def test_lines_markdown_document(self): # Assert if text != expected: common.log.error(f"Published content changed: {path}") - common.write_text(text, path) + common.write_text(text, path) self.assertEqual(expected, text) @patch("doorstop.settings.PUBLISH_CHILD_LINKS", False) @@ -723,7 +754,7 @@ def test_lines_markdown_document_without_child_links(self): # Assert if text != expected: common.log.error(f"Published content changed: {path}") - common.write_text(text, path) + common.write_text(text, path) self.assertEqual(expected, text) @patch("plantuml_markdown.PlantUMLPreprocessor.run") @@ -751,7 +782,7 @@ def run(lines: List[str]) -> List[str]: # Assert if actual != expected: common.log.error(f"Published content changed: {path}") - common.write_text(actual, path) + common.write_text(actual, path) self.assertEqual(expected, actual) @patch("plantuml_markdown.PlantUMLPreprocessor.run") @@ -776,7 +807,7 @@ def run(lines: List[str]) -> List[str]: # Assert if actual != expected: common.log.error(f"Published content changed: {path}") - common.write_text(actual, path) + common.write_text(actual, path) self.assertEqual(expected, actual) diff --git a/doorstop/core/tests/test_document.py b/doorstop/core/tests/test_document.py index ec321537..253f2f77 100644 --- a/doorstop/core/tests/test_document.py +++ b/doorstop/core/tests/test_document.py @@ -297,12 +297,12 @@ def test_hash(self): def test_len(self): """Verify a document has a length.""" - self.assertEqual(6, len(self.document)) + self.assertEqual(7, len(self.document)) def test_items(self): """Verify the items in a document can be accessed.""" items = self.document.items - self.assertEqual(6, len(items)) + self.assertEqual(7, len(items)) for item in self.document: logging.debug("item: {}".format(item)) self.assertIs(self.document, item.document) @@ -313,7 +313,7 @@ def test_items_cache(self): self.document.tree = Mock() self.document.tree._item_cache = {} print(self.document.items) - self.assertEqual(7, len(self.document.tree._item_cache)) + self.assertEqual(8, len(self.document.tree._item_cache)) @patch("doorstop.core.document.Document", MockDocument) def test_new(self): @@ -381,7 +381,7 @@ def test_depth(self): def test_next_number(self): """Verify the next item number can be determined.""" - self.assertEqual(7, self.document.next_number) + self.assertEqual(8, self.document.next_number) def test_next_number_server(self): """Verify the next item number can be determined with a server.""" @@ -396,28 +396,6 @@ def test_index_get(self): 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%", - " - REQ006: # Hello, world!", - " - 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): @@ -450,6 +428,29 @@ def test_read_index(self, mock_write_lines): # Assert self.assertEqual(expected, actual) + @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%", + " - REQ006: # Hello, world!", + " - REQ004: # Hello, world!", + " - REQ002: # Hello, world!", + " - REQ2-001: # Hello, world!", + " - REQ007: # ", + ] + # 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.delete") def test_index_del(self, mock_delete): """Verify a document's index can be deleted.""" @@ -463,7 +464,7 @@ def test_add_item(self, mock_new, mock_reorder): with patch("doorstop.settings.REORDER", True): self.document.add_item() mock_new.assert_called_once_with( - None, self.document, FILES, ROOT, "REQ007", level=Level("2.2") + None, self.document, FILES, ROOT, "REQ008", level=Level("3.1") ) self.assertEqual(0, mock_reorder.call_count) @@ -474,7 +475,7 @@ def test_add_item_with_level(self, mock_new, mock_reorder): 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, "REQ007", level="4.2" + None, self.document, FILES, ROOT, "REQ008", level="4.2" ) mock_reorder.assert_called_once_with(keep=item) @@ -483,7 +484,7 @@ 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") + None, self.document, FILES, ROOT, "REQ999", level=Level("3.1") ) def test_add_item_with_no_sep(self): @@ -515,7 +516,7 @@ def test_add_item_with_name(self, mock_new): self.document.sep = "-" self.document.add_item(name="ABC") mock_new.assert_called_once_with( - None, self.document, FILES, ROOT, "REQ-ABC", level=Level("2.2") + None, self.document, FILES, ROOT, "REQ-ABC", level=Level("3.1") ) @patch("doorstop.core.item.Item.new") @@ -524,7 +525,7 @@ def test_add_item_with_number_name(self, mock_new): self.document.sep = "-" self.document.add_item(name="99") mock_new.assert_called_once_with( - None, self.document, FILES, ROOT, "REQ-099", level=Level("2.2") + None, self.document, FILES, ROOT, "REQ-099", level=Level("3.1") ) @patch("doorstop.core.item.Item.set_attributes") @@ -729,7 +730,7 @@ def test_validate(self, mock_reorder, mock_get_issues): with patch("doorstop.settings.REORDER", True): self.assertTrue(self.document.validate()) mock_reorder.assert_called_once_with(_items=self.document.items) - self.assertEqual(6, mock_get_issues.call_count) + self.assertEqual(7, mock_get_issues.call_count) @patch( "doorstop.core.validators.item_validator.ItemValidator.get_issues", @@ -753,14 +754,14 @@ def test_validate_hook(self): """Verify an item hook can be called.""" mock_hook = MagicMock() self.document.validate(item_hook=mock_hook) - self.assertEqual(6, mock_hook.call_count) + self.assertEqual(7, 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(7, mock_item_delete.call_count) + self.assertEqual(8, mock_item_delete.call_count) self.assertEqual(1, mock_delete.call_count) self.document.delete() # ensure a second delete is ignored @@ -770,7 +771,7 @@ 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(7, mock_item_delete.call_count) + self.assertEqual(8, mock_item_delete.call_count) self.assertEqual(1, mock_delete.call_count) self.document.delete() # ensure a second delete is ignored diff --git a/doorstop/core/tests/test_importer.py b/doorstop/core/tests/test_importer.py index 1df09bf8..63015f64 100644 --- a/doorstop/core/tests/test_importer.py +++ b/doorstop/core/tests/test_importer.py @@ -90,7 +90,7 @@ def test_file_yml(self, mock_add_item): # Act importer._file_yml(path, mock_document) # Assert - self.assertEqual(6, mock_add_item.call_count) + self.assertEqual(7, mock_add_item.call_count) @patch("doorstop.core.importer.add_item") def test_file_yml_duplicates(self, mock_add_item): @@ -100,7 +100,7 @@ def test_file_yml_duplicates(self, mock_add_item): # Act importer._file_yml(path, mock_document) # Assert - self.assertEqual(6, mock_add_item.call_count) + self.assertEqual(7, mock_add_item.call_count) def test_file_yml_bad_format(self): """Verify YAML file import can handle bad data.""" @@ -202,6 +202,19 @@ def test_file_csv(self, mock_itemize): True, "", ], + [ + "REQ007", + "3.0", + "", + "", + "", + "", + True, + False, + "My Heading", + False, + "", + ], ] self.assertEqual(expected_data, data) self.assertIs(mock_document, document) diff --git a/doorstop/core/tests/test_tree.py b/doorstop/core/tests/test_tree.py index 4f21c0d6..27745ab1 100644 --- a/doorstop/core/tests/test_tree.py +++ b/doorstop/core/tests/test_tree.py @@ -17,7 +17,7 @@ from doorstop.common import DoorstopError, DoorstopInfo, DoorstopWarning from doorstop.core.builder import build from doorstop.core.document import Document -from doorstop.core.tests import EMPTY, FILES, SYS, MockDocumentSkip +from doorstop.core.tests import EMPTY, FILES, GOLDEN_MASTER_FILES, SYS, MockDocumentSkip from doorstop.core.tree import Tree @@ -30,7 +30,7 @@ def reset_fixture_files(): try: test_dir = os.path.dirname(__file__) - # get all git-tracked YAML-files in files/ + # Get all git-tracked YAML files in files/ result = subprocess.run( ["git", "ls-files", "files/*.yml", "files/*.yaml"], capture_output=True, @@ -43,7 +43,18 @@ def reset_fixture_files(): if result.returncode == 0 and result.stdout.strip(): yaml_files = result.stdout.strip().split("\n") - # reset those files + # Exclude golden master files from reset as they are + # intentionally updated by tests using the golden master pattern + yaml_files = [ + f + for f in yaml_files + if os.path.abspath(os.path.join(test_dir, f)) not in GOLDEN_MASTER_FILES + ] + + if not yaml_files: + return + + # Reset those files subprocess.run( ["git", "checkout", "--"] + yaml_files, capture_output=True,