From 353ca832dd2ab7ee5fcda0e777f2e54ccf8de96e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 12:12:03 +0000 Subject: [PATCH 1/3] Initial plan From 977a9ca5184849dd2b07ad6591cb5068df2728d8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 12:31:28 +0000 Subject: [PATCH 2/3] Add history validation implementation and comprehensive tests Co-authored-by: robertatakenaka <505143+robertatakenaka@users.noreply.github.com> --- packtools/sps/validation/history.py | 408 +++++++++++++ tests/sps/validation/test_history.py | 877 +++++++++++++++++++++++++++ 2 files changed, 1285 insertions(+) create mode 100644 packtools/sps/validation/history.py create mode 100644 tests/sps/validation/test_history.py diff --git a/packtools/sps/validation/history.py b/packtools/sps/validation/history.py new file mode 100644 index 000000000..5fc2effc9 --- /dev/null +++ b/packtools/sps/validation/history.py @@ -0,0 +1,408 @@ +""" +Validations for the element according to SPS 1.10 specification. + +This module implements validations for the element, which groups +historical dates for documents (received, accepted, revised, preprint, corrections, +retractions, etc.). + +Reference: https://docs.google.com/document/d/1GTv4Inc2LS_AXY-ToHT3HmO66UT0VAHWJNOIqzBNSgA/edit?tab=t.0#heading=h.history +""" + +from packtools.sps.validation.utils import build_response + + +# Allowed values for @date-type according to SPS 1.10 +ALLOWED_DATE_TYPES = [ + "received", # Date manuscript was received + "accepted", # Date manuscript was accepted + "corrected", # Date of approval of Errata or Addendum + "expression-of-concern", # Date of approval of Expression of Concern + "pub", # Publication date + "preprint", # Date published as preprint + "resubmitted", # Date manuscript was resubmitted + "retracted", # Date of approval of retraction + "rev-recd", # Date revised manuscript was received + "rev-request", # Date revisions were requested + "reviewer-report-received", # Date reviewer report was received (exclusive for @article-type="reviewer-report") +] + +# Date types that require complete date (day, month, year) +COMPLETE_DATE_REQUIRED_TYPES = [ + "received", + "accepted", + "corrected", + "retracted", + "expression-of-concern", +] + +# Article types that are exempt from received/accepted requirements +EXEMPT_ARTICLE_TYPES = [ + "correction", # errata + "retraction", + "addendum", + "expression-of-concern", + "reviewer-report", +] + + +class HistoryValidation: + """ + Validates the element according to SPS 1.10 rules. + + This class implements validation rules for: + - Uniqueness of element + - Presence of @date-type attribute + - Allowed values for @date-type + - Required dates (received, accepted) with exceptions + - Complete date requirements for critical date types + - Minimum year requirement for all dates + """ + + def __init__(self, xmltree, params=None): + """ + Initialize HistoryValidation. + + Args: + xmltree: XML tree containing the article + params: Optional dictionary of validation parameters + """ + self.xmltree = xmltree + self.params = params or {} + self.params.setdefault("history_uniqueness_error_level", "ERROR") + self.params.setdefault("date_type_presence_error_level", "CRITICAL") + self.params.setdefault("date_type_value_error_level", "ERROR") + self.params.setdefault("required_date_error_level", "CRITICAL") + self.params.setdefault("complete_date_error_level", "CRITICAL") + self.params.setdefault("year_presence_error_level", "CRITICAL") + + # Get article type to determine if exempt from required dates + self.article_type = self._get_article_type() + + def _get_article_type(self): + """Get the article type from the XML.""" + article = self.xmltree.find(".") + if article is not None: + return article.get("article-type") + return None + + def _get_parent_info(self, node=None): + """Build parent information for validation responses.""" + article = self.xmltree.find(".") + return { + "parent": "article-meta" if node is None else node.tag, + "parent_id": None, + "parent_article_type": self.article_type, + "parent_lang": article.get("{http://www.w3.org/XML/1998/namespace}lang") if article is not None else None, + } + + def validate_history_uniqueness(self): + """ + Rule 1: Validate that appears at most once. + + The element must appear at most once in or . + + Yields: + dict: Validation result + """ + # Check in article-meta + article_meta_history = self.xmltree.xpath(".//front/article-meta/history") + + # Check in front-stub + front_stub_history = self.xmltree.xpath(".//front-stub/history") + + # Combine all history elements + all_history = article_meta_history + front_stub_history + history_count = len(all_history) + + is_valid = history_count <= 1 + + parent = self._get_parent_info() + + advice = None + if not is_valid: + advice = f"Remove duplicate elements. Found {history_count} occurrences, expected at most 1." + + yield build_response( + title="history uniqueness", + parent=parent, + item="history", + sub_item=None, + validation_type="uniqueness", + is_valid=is_valid, + expected="at most one element", + obtained=f"{history_count} element(s)", + advice=advice, + data={"history_count": history_count}, + error_level=self.params["history_uniqueness_error_level"], + ) + + def validate_date_type_presence(self): + """ + Rule 2: Validate that all elements within have @date-type. + + The @date-type attribute is required for all elements within . + + Yields: + dict: Validation result for each element + """ + history_dates = self.xmltree.xpath(".//history/date") + + for date_elem in history_dates: + date_type = date_elem.get("date-type") + has_date_type = date_type is not None and date_type.strip() != "" + + parent = self._get_parent_info(date_elem) + + # Get date parts for context + day = date_elem.findtext("day") + month = date_elem.findtext("month") + year = date_elem.findtext("year") + date_parts = {"day": day, "month": month, "year": year} + + advice = None + if not has_date_type: + advice = f"Add @date-type attribute to element. Date parts: {date_parts}" + + yield build_response( + title="date-type presence", + parent=parent, + item="date", + sub_item="@date-type", + validation_type="exist", + is_valid=has_date_type, + expected="@date-type attribute present", + obtained=date_type if has_date_type else "missing", + advice=advice, + data=date_parts, + error_level=self.params["date_type_presence_error_level"], + ) + + def validate_date_type_values(self): + """ + Rule 3: Validate that @date-type has allowed values. + + The @date-type attribute must have one of the allowed values according to SPS 1.10. + + Yields: + dict: Validation result for each element + """ + history_dates = self.xmltree.xpath(".//history/date") + + for date_elem in history_dates: + date_type = date_elem.get("date-type") + + # Skip if date-type is missing (covered by validate_date_type_presence) + if date_type is None or date_type.strip() == "": + continue + + is_valid = date_type in ALLOWED_DATE_TYPES + + parent = self._get_parent_info(date_elem) + + # Get date parts for context + day = date_elem.findtext("day") + month = date_elem.findtext("month") + year = date_elem.findtext("year") + date_parts = {"day": day, "month": month, "year": year, "date-type": date_type} + + advice = None + if not is_valid: + advice = f"Change @date-type='{date_type}' to one of the allowed values: {', '.join(ALLOWED_DATE_TYPES)}" + + yield build_response( + title="date-type value", + parent=parent, + item="date", + sub_item="@date-type", + validation_type="value in list", + is_valid=is_valid, + expected=ALLOWED_DATE_TYPES, + obtained=date_type, + advice=advice, + data=date_parts, + error_level=self.params["date_type_value_error_level"], + ) + + def validate_required_dates(self): + """ + Rules 4 & 5: Validate presence of required dates (received, accepted). + + The dates received and accepted are required, except for: + - correction (errata) + - retraction + - addendum + - expression-of-concern + - reviewer-report + + Yields: + dict: Validation results for required dates + """ + # Check if article type is exempt + is_exempt = self.article_type in EXEMPT_ARTICLE_TYPES + + # Get all date-types from history + history_dates = self.xmltree.xpath(".//history/date") + found_date_types = [d.get("date-type") for d in history_dates if d.get("date-type")] + + parent = self._get_parent_info() + + # Check for "received" date + has_received = "received" in found_date_types + received_required = not is_exempt + received_valid = has_received or not received_required + + advice_received = None + if not received_valid: + advice_received = "Add with complete date (day, month, year) to " + + yield build_response( + title="required date: received", + parent=parent, + item="history", + sub_item="date[@date-type='received']", + validation_type="exist", + is_valid=received_valid, + expected=" present" if received_required else "not required (exempt article type)", + obtained="present" if has_received else "missing", + advice=advice_received, + data={"article_type": self.article_type, "is_exempt": is_exempt, "found_date_types": found_date_types}, + error_level=self.params["required_date_error_level"] if received_required else "OK", + ) + + # Check for "accepted" date + has_accepted = "accepted" in found_date_types + accepted_required = not is_exempt + accepted_valid = has_accepted or not accepted_required + + advice_accepted = None + if not accepted_valid: + advice_accepted = "Add with complete date (day, month, year) to " + + yield build_response( + title="required date: accepted", + parent=parent, + item="history", + sub_item="date[@date-type='accepted']", + validation_type="exist", + is_valid=accepted_valid, + expected=" present" if accepted_required else "not required (exempt article type)", + obtained="present" if has_accepted else "missing", + advice=advice_accepted, + data={"article_type": self.article_type, "is_exempt": is_exempt, "found_date_types": found_date_types}, + error_level=self.params["required_date_error_level"] if accepted_required else "OK", + ) + + def validate_complete_date_for_critical_types(self): + """ + Rule 6: Validate complete dates for critical date types. + + For received, accepted, corrected, retracted, expression-of-concern: + , , and are required. + + Yields: + dict: Validation result for each critical date + """ + history_dates = self.xmltree.xpath(".//history/date") + + for date_elem in history_dates: + date_type = date_elem.get("date-type") + + # Skip if not a critical type + if date_type not in COMPLETE_DATE_REQUIRED_TYPES: + continue + + # Check for day, month, year + day = date_elem.findtext("day") + month = date_elem.findtext("month") + year = date_elem.findtext("year") + + has_day = day is not None and day.strip() != "" + has_month = month is not None and month.strip() != "" + has_year = year is not None and year.strip() != "" + + is_complete = has_day and has_month and has_year + + parent = self._get_parent_info(date_elem) + + date_parts = {"day": day, "month": month, "year": year, "date-type": date_type} + + missing_parts = [] + if not has_day: + missing_parts.append("day") + if not has_month: + missing_parts.append("month") + if not has_year: + missing_parts.append("year") + + advice = None + if not is_complete: + advice = f"Add missing elements to : {', '.join(missing_parts)}" + + yield build_response( + title=f"complete date for {date_type}", + parent=parent, + item="date", + sub_item=f"@date-type='{date_type}'", + validation_type="format", + is_valid=is_complete, + expected="complete date with day, month, and year", + obtained=f"day={day}, month={month}, year={year}", + advice=advice, + data=date_parts, + error_level=self.params["complete_date_error_level"], + ) + + def validate_year_presence(self): + """ + Rule 7: Validate that all dates have at least . + + For all date types, at least must be present. + + Yields: + dict: Validation result for each date + """ + history_dates = self.xmltree.xpath(".//history/date") + + for date_elem in history_dates: + date_type = date_elem.get("date-type") + year = date_elem.findtext("year") + + has_year = year is not None and year.strip() != "" + + parent = self._get_parent_info(date_elem) + + day = date_elem.findtext("day") + month = date_elem.findtext("month") + date_parts = {"day": day, "month": month, "year": year, "date-type": date_type} + + advice = None + if not has_year: + advice = f"Add element to " + + yield build_response( + title=f"year presence for {date_type}", + parent=parent, + item="date", + sub_item="year", + validation_type="exist", + is_valid=has_year, + expected=" element present", + obtained=year if has_year else "missing", + advice=advice, + data=date_parts, + error_level=self.params["year_presence_error_level"], + ) + + def validate(self): + """ + Perform all history validations. + + Yields: + Generator of validation results for all checks + """ + yield from self.validate_history_uniqueness() + yield from self.validate_date_type_presence() + yield from self.validate_date_type_values() + yield from self.validate_required_dates() + yield from self.validate_complete_date_for_critical_types() + yield from self.validate_year_presence() diff --git a/tests/sps/validation/test_history.py b/tests/sps/validation/test_history.py new file mode 100644 index 000000000..68f454d77 --- /dev/null +++ b/tests/sps/validation/test_history.py @@ -0,0 +1,877 @@ +""" +Tests for history element validations according to SPS 1.10. + +This module tests the validation rules for the element, +ensuring compliance with the SPS 1.10 specification. +""" + +from unittest import TestCase +from lxml import etree + +from packtools.sps.validation.history import ( + HistoryValidation, + ALLOWED_DATE_TYPES, + COMPLETE_DATE_REQUIRED_TYPES, + EXEMPT_ARTICLE_TYPES, +) + + +class TestHistoryUniqueness(TestCase): + """Tests for Rule 1: History element uniqueness.""" + + def test_single_history_in_article_meta(self): + """Test that a single in article-meta is valid.""" + xml = """ +
+ + + + + 15 + 03 + 2024 + + + + +
+ """ + tree = etree.fromstring(xml) + validator = HistoryValidation(tree) + results = list(validator.validate_history_uniqueness()) + + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["response"], "OK") + self.assertEqual(results[0]["title"], "history uniqueness") + + def test_single_history_in_front_stub(self): + """Test that a single in front-stub is valid.""" + xml = """ +
+ + + + + 15 + 03 + 2024 + + + + +
+ """ + tree = etree.fromstring(xml) + validator = HistoryValidation(tree) + results = list(validator.validate_history_uniqueness()) + + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["response"], "OK") + self.assertEqual(results[0]["response"], "OK") + + def test_multiple_history_elements(self): + """Test that multiple elements are invalid.""" + xml = """ +
+ + + + + 15 + 03 + 2024 + + + + + 20 + 04 + 2024 + + + + +
+ """ + tree = etree.fromstring(xml) + validator = HistoryValidation(tree) + results = list(validator.validate_history_uniqueness()) + + self.assertEqual(len(results), 1) + self.assertNotEqual(results[0]["response"], "OK") + self.assertEqual(results[0]["response"], "ERROR") + self.assertIn("duplicate", results[0]["advice"].lower()) + self.assertIn("2", results[0]["got_value"]) + + def test_no_history_element(self): + """Test that no element is valid.""" + xml = """ +
+ + + + +
+ """ + tree = etree.fromstring(xml) + validator = HistoryValidation(tree) + results = list(validator.validate_history_uniqueness()) + + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["response"], "OK") + self.assertEqual(results[0]["response"], "OK") + + +class TestDateTypePresence(TestCase): + """Tests for Rule 2: @date-type attribute presence.""" + + def test_date_with_date_type(self): + """Test that with @date-type is valid.""" + xml = """ +
+ + + + + 15 + 03 + 2024 + + + + +
+ """ + tree = etree.fromstring(xml) + validator = HistoryValidation(tree) + results = list(validator.validate_date_type_presence()) + + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["response"], "OK") + self.assertEqual(results[0]["response"], "OK") + self.assertEqual(results[0]["got_value"], "received") + + def test_date_without_date_type(self): + """Test that without @date-type is invalid.""" + xml = """ +
+ + + + + 15 + 03 + 2024 + + + + +
+ """ + tree = etree.fromstring(xml) + validator = HistoryValidation(tree) + results = list(validator.validate_date_type_presence()) + + self.assertEqual(len(results), 1) + self.assertNotEqual(results[0]["response"], "OK") + self.assertEqual(results[0]["response"], "CRITICAL") + self.assertEqual(results[0]["got_value"], "missing") + self.assertIn("Add @date-type", results[0]["advice"]) + + def test_date_with_empty_date_type(self): + """Test that with empty @date-type is invalid.""" + xml = """ +
+ + + + + 15 + 03 + 2024 + + + + +
+ """ + tree = etree.fromstring(xml) + validator = HistoryValidation(tree) + results = list(validator.validate_date_type_presence()) + + self.assertEqual(len(results), 1) + self.assertNotEqual(results[0]["response"], "OK") + self.assertEqual(results[0]["response"], "CRITICAL") + + def test_multiple_dates_mixed_presence(self): + """Test validation of multiple dates with mixed @date-type presence.""" + xml = """ +
+ + + + + 15 + 03 + 2024 + + + 20 + 04 + 2024 + + + 25 + 05 + 2024 + + + + +
+ """ + tree = etree.fromstring(xml) + validator = HistoryValidation(tree) + results = list(validator.validate_date_type_presence()) + + self.assertEqual(len(results), 3) + # First and third should be valid + self.assertEqual(results[0]["response"], "OK") + self.assertNotEqual(results[1]["response"], "OK") + self.assertEqual(results[2]["response"], "OK") + + +class TestDateTypeValues(TestCase): + """Tests for Rule 3: Allowed @date-type values.""" + + def test_valid_date_types(self): + """Test that all allowed date types are valid.""" + for date_type in ALLOWED_DATE_TYPES: + xml = f""" +
+ + + + + 2024 + + + + +
+ """ + tree = etree.fromstring(xml) + validator = HistoryValidation(tree) + results = list(validator.validate_date_type_values()) + + self.assertEqual(len(results), 1, f"Failed for date-type={date_type}") + self.assertEqual(results[0]["response"], "OK", f"Failed for date-type={date_type}") + self.assertEqual(results[0]["got_value"], date_type) + + def test_invalid_date_type(self): + """Test that invalid date types are rejected.""" + xml = """ +
+ + + + + 2024 + + + + +
+ """ + tree = etree.fromstring(xml) + validator = HistoryValidation(tree) + results = list(validator.validate_date_type_values()) + + self.assertEqual(len(results), 1) + self.assertNotEqual(results[0]["response"], "OK") + self.assertEqual(results[0]["response"], "ERROR") + self.assertEqual(results[0]["got_value"], "invalid-type") + self.assertIn("allowed values", results[0]["advice"]) + + def test_multiple_dates_mixed_validity(self): + """Test validation with both valid and invalid date types.""" + xml = """ +
+ + + + + 2024 + + + 2024 + + + 2024 + + + + +
+ """ + tree = etree.fromstring(xml) + validator = HistoryValidation(tree) + results = list(validator.validate_date_type_values()) + + self.assertEqual(len(results), 3) + self.assertEqual(results[0]["response"], "OK") # received + self.assertNotEqual(results[1]["response"], "OK") # bad-type + self.assertEqual(results[2]["response"], "OK") # accepted + + +class TestRequiredDates(TestCase): + """Tests for Rules 4 & 5: Required dates (received, accepted).""" + + def test_regular_article_with_required_dates(self): + """Test that regular articles require received and accepted dates.""" + xml = """ +
+ + + + + 15 + 03 + 2024 + + + 20 + 05 + 2024 + + + + +
+ """ + tree = etree.fromstring(xml) + validator = HistoryValidation(tree) + results = list(validator.validate_required_dates()) + + # Should have 2 results: one for received, one for accepted + self.assertEqual(len(results), 2) + # Both should be valid + self.assertTrue(all(r["response"] == "OK" for r in results)) + self.assertTrue(all(r["response"] == "OK" for r in results)) + + def test_regular_article_missing_received(self): + """Test that regular articles without received date are invalid.""" + xml = """ +
+ + + + + 20 + 05 + 2024 + + + + +
+ """ + tree = etree.fromstring(xml) + validator = HistoryValidation(tree) + results = list(validator.validate_required_dates()) + + self.assertEqual(len(results), 2) + # received should be invalid + received_result = next(r for r in results if "received" in r["title"]) + self.assertNotEqual(received_result["response"], "OK") + self.assertEqual(received_result["response"], "CRITICAL") + self.assertIn("Add ", received_result["advice"]) + # accepted should be valid + accepted_result = next(r for r in results if "accepted" in r["title"]) + self.assertEqual(accepted_result["response"], "OK") + + def test_regular_article_missing_accepted(self): + """Test that regular articles without accepted date are invalid.""" + xml = """ +
+ + + + + 15 + 03 + 2024 + + + + +
+ """ + tree = etree.fromstring(xml) + validator = HistoryValidation(tree) + results = list(validator.validate_required_dates()) + + self.assertEqual(len(results), 2) + # received should be valid + received_result = next(r for r in results if "received" in r["title"]) + self.assertEqual(received_result["response"], "OK") + # accepted should be invalid + accepted_result = next(r for r in results if "accepted" in r["title"]) + self.assertNotEqual(accepted_result["response"], "OK") + self.assertEqual(accepted_result["response"], "CRITICAL") + + def test_exempt_article_types(self): + """Test that exempt article types don't require received/accepted.""" + for article_type in EXEMPT_ARTICLE_TYPES: + xml = f""" +
+ + + + + 15 + 03 + 2024 + + + + +
+ """ + tree = etree.fromstring(xml) + validator = HistoryValidation(tree) + results = list(validator.validate_required_dates()) + + # Should have 2 results but both should be valid (not required) + self.assertEqual(len(results), 2, f"Failed for article-type={article_type}") + self.assertTrue(all(r["response"] == "OK" for r in results), f"Failed for article-type={article_type}") + + def test_retraction_without_required_dates(self): + """Test specific case: retraction article type.""" + xml = """ +
+ + + + + 20 + 06 + 2024 + + + + +
+ """ + tree = etree.fromstring(xml) + validator = HistoryValidation(tree) + results = list(validator.validate_required_dates()) + + self.assertEqual(len(results), 2) + # Both should be valid since retraction is exempt + self.assertTrue(all(r["response"] == "OK" for r in results)) + + +class TestCompleteDateForCriticalTypes(TestCase): + """Tests for Rule 6: Complete date requirements for critical types.""" + + def test_received_with_complete_date(self): + """Test that received date with complete date is valid.""" + xml = """ +
+ + + + + 15 + 03 + 2024 + + + + +
+ """ + tree = etree.fromstring(xml) + validator = HistoryValidation(tree) + results = list(validator.validate_complete_date_for_critical_types()) + + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["response"], "OK") + self.assertEqual(results[0]["response"], "OK") + + def test_received_missing_day(self): + """Test that received date without day is invalid.""" + xml = """ +
+ + + + + 03 + 2024 + + + + +
+ """ + tree = etree.fromstring(xml) + validator = HistoryValidation(tree) + results = list(validator.validate_complete_date_for_critical_types()) + + self.assertEqual(len(results), 1) + self.assertNotEqual(results[0]["response"], "OK") + self.assertEqual(results[0]["response"], "CRITICAL") + self.assertIn("day", results[0]["advice"]) + + def test_accepted_missing_month(self): + """Test that accepted date without month is invalid.""" + xml = """ +
+ + + + + 15 + 2024 + + + + +
+ """ + tree = etree.fromstring(xml) + validator = HistoryValidation(tree) + results = list(validator.validate_complete_date_for_critical_types()) + + self.assertEqual(len(results), 1) + self.assertNotEqual(results[0]["response"], "OK") + self.assertEqual(results[0]["response"], "CRITICAL") + self.assertIn("month", results[0]["advice"]) + + def test_corrected_missing_year(self): + """Test that corrected date without year is invalid.""" + xml = """ +
+ + + + + 15 + 03 + + + + +
+ """ + tree = etree.fromstring(xml) + validator = HistoryValidation(tree) + results = list(validator.validate_complete_date_for_critical_types()) + + self.assertEqual(len(results), 1) + self.assertNotEqual(results[0]["response"], "OK") + self.assertEqual(results[0]["response"], "CRITICAL") + self.assertIn("year", results[0]["advice"]) + + def test_all_critical_types(self): + """Test that all critical types are validated for completeness.""" + for date_type in COMPLETE_DATE_REQUIRED_TYPES: + xml = f""" +
+ + + + + 15 + 03 + 2024 + + + + +
+ """ + tree = etree.fromstring(xml) + validator = HistoryValidation(tree) + results = list(validator.validate_complete_date_for_critical_types()) + + self.assertEqual(len(results), 1, f"Failed for date-type={date_type}") + self.assertEqual(results[0]["response"], "OK", f"Failed for date-type={date_type}") + + def test_non_critical_type_not_validated(self): + """Test that non-critical types are not validated by this rule.""" + xml = """ +
+ + + + + 2023 + + + 2024 + + + + +
+ """ + tree = etree.fromstring(xml) + validator = HistoryValidation(tree) + results = list(validator.validate_complete_date_for_critical_types()) + + # Should not return any results for non-critical types + self.assertEqual(len(results), 0) + + def test_multiple_critical_dates_mixed(self): + """Test validation of multiple critical dates with mixed completeness.""" + xml = """ +
+ + + + + 15 + 03 + 2024 + + + 05 + 2024 + + + 10 + 07 + 2024 + + + + +
+ """ + tree = etree.fromstring(xml) + validator = HistoryValidation(tree) + results = list(validator.validate_complete_date_for_critical_types()) + + self.assertEqual(len(results), 3) + # received should be valid + self.assertEqual(results[0]["response"], "OK") + # accepted should be invalid (missing day) + self.assertNotEqual(results[1]["response"], "OK") + # corrected should be valid + self.assertEqual(results[2]["response"], "OK") + + +class TestYearPresence(TestCase): + """Tests for Rule 7: Year presence for all dates.""" + + def test_date_with_year(self): + """Test that date with year is valid.""" + xml = """ +
+ + + + + 2023 + + + + +
+ """ + tree = etree.fromstring(xml) + validator = HistoryValidation(tree) + results = list(validator.validate_year_presence()) + + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["response"], "OK") + self.assertEqual(results[0]["response"], "OK") + self.assertEqual(results[0]["got_value"], "2023") + + def test_date_without_year(self): + """Test that date without year is invalid.""" + xml = """ +
+ + + + + 09 + 21 + + + + +
+ """ + tree = etree.fromstring(xml) + validator = HistoryValidation(tree) + results = list(validator.validate_year_presence()) + + self.assertEqual(len(results), 1) + self.assertNotEqual(results[0]["response"], "OK") + self.assertEqual(results[0]["response"], "CRITICAL") + self.assertEqual(results[0]["got_value"], "missing") + self.assertIn("Add ", results[0]["advice"]) + + def test_date_with_empty_year(self): + """Test that date with empty year is invalid.""" + xml = """ +
+ + + + + + + + + +
+ """ + tree = etree.fromstring(xml) + validator = HistoryValidation(tree) + results = list(validator.validate_year_presence()) + + self.assertEqual(len(results), 1) + self.assertNotEqual(results[0]["response"], "OK") + self.assertEqual(results[0]["response"], "CRITICAL") + + def test_multiple_dates_mixed_year_presence(self): + """Test validation of multiple dates with mixed year presence.""" + xml = """ +
+ + + + + 15 + 03 + 2024 + + + 20 + 05 + + + 2024 + + + + +
+ """ + tree = etree.fromstring(xml) + validator = HistoryValidation(tree) + results = list(validator.validate_year_presence()) + + self.assertEqual(len(results), 3) + # received should be valid + self.assertEqual(results[0]["response"], "OK") + # accepted should be invalid (missing year) + self.assertNotEqual(results[1]["response"], "OK") + # pub should be valid + self.assertEqual(results[2]["response"], "OK") + + +class TestFullValidation(TestCase): + """Tests for complete validation workflow.""" + + def test_valid_complete_example(self): + """Test validation of a completely valid history.""" + xml = """ +
+ + + + + 15 + 03 + 2024 + + + 12 + 05 + 2024 + + + 21 + 09 + 2023 + + + + +
+ """ + tree = etree.fromstring(xml) + validator = HistoryValidation(tree) + results = list(validator.validate()) + + # All results should be valid + errors = [r for r in results if r["response"] != "OK"] + self.assertEqual(len(errors), 0, f"Found errors: {errors}") + + def test_invalid_multiple_issues(self): + """Test validation with multiple issues.""" + xml = """ +
+ + + + + 15 + 03 + 2024 + + + 05 + 2024 + + + + +
+ """ + tree = etree.fromstring(xml) + validator = HistoryValidation(tree) + results = list(validator.validate()) + + # Should have multiple errors + errors = [r for r in results if r["response"] != "OK"] + self.assertGreater(len(errors), 0) + + # Check for specific error types + error_titles = [e["title"] for e in errors] + self.assertIn("date-type presence", error_titles) + self.assertIn("date-type value", error_titles) + self.assertIn("required date: received", error_titles) + self.assertIn("required date: accepted", error_titles) + + def test_retraction_article_valid(self): + """Test validation of retraction article (exempt from received/accepted).""" + xml = """ +
+ + + + + 20 + 06 + 2024 + + + + +
+ """ + tree = etree.fromstring(xml) + validator = HistoryValidation(tree) + results = list(validator.validate()) + + # All results should be valid (retraction is exempt) + errors = [r for r in results if r["response"] != "OK"] + self.assertEqual(len(errors), 0, f"Found errors: {errors}") From ef1e0f26666e1040a3e108f70c1c28ad770fd8e1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 12:33:32 +0000 Subject: [PATCH 3/3] Fix duplicate assertions in test file Co-authored-by: robertatakenaka <505143+robertatakenaka@users.noreply.github.com> --- tests/sps/validation/test_history.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/sps/validation/test_history.py b/tests/sps/validation/test_history.py index 68f454d77..2ce55bb94 100644 --- a/tests/sps/validation/test_history.py +++ b/tests/sps/validation/test_history.py @@ -67,7 +67,6 @@ def test_single_history_in_front_stub(self): self.assertEqual(len(results), 1) self.assertEqual(results[0]["response"], "OK") - self.assertEqual(results[0]["response"], "OK") def test_multiple_history_elements(self): """Test that multiple elements are invalid.""" @@ -119,7 +118,6 @@ def test_no_history_element(self): self.assertEqual(len(results), 1) self.assertEqual(results[0]["response"], "OK") - self.assertEqual(results[0]["response"], "OK") class TestDateTypePresence(TestCase): @@ -148,7 +146,6 @@ def test_date_with_date_type(self): self.assertEqual(len(results), 1) self.assertEqual(results[0]["response"], "OK") - self.assertEqual(results[0]["response"], "OK") self.assertEqual(results[0]["got_value"], "received") def test_date_without_date_type(self): @@ -498,7 +495,6 @@ def test_received_with_complete_date(self): self.assertEqual(len(results), 1) self.assertEqual(results[0]["response"], "OK") - self.assertEqual(results[0]["response"], "OK") def test_received_missing_day(self): """Test that received date without day is invalid.""" @@ -688,7 +684,6 @@ def test_date_with_year(self): self.assertEqual(len(results), 1) self.assertEqual(results[0]["response"], "OK") - self.assertEqual(results[0]["response"], "OK") self.assertEqual(results[0]["got_value"], "2023") def test_date_without_year(self):