"""Tests for service_inventory. Every test here pins a parser trap that was found by hand-checking the real sources, and that would otherwise have made this tool produce a *tenth* wrong number in a repo that already had nine. They are written against synthetic sources so they keep failing for the right reason when the real files change. Run with: pytest tools/docs -q (or: make docs-test) """ from __future__ import annotations from datetime import date import pytest import service_inventory as si # --------------------------------------------------------------------------- # # Key normalisation # --------------------------------------------------------------------------- # def test_key_normalises_across_naming_conventions(): # The same service, spelled three ways by three sources. assert si.key("bcmDataExports") == si.key("bcm-data-exports") == si.key("bcmdataexports") def test_key_does_not_collide_across_distinct_services(): assert si.key("rds") != si.key("rds-data") # --------------------------------------------------------------------------- # # Descriptor parsing # --------------------------------------------------------------------------- # SIMPLE = ''' descriptor("ssm", "ssm", config.services().ssm().enabled(), true, "ssm", storageMode(...), 5000L, null, ServiceProtocol.JSON, protocols(ServiceProtocol.JSON), Set.of("AmazonSSM."), Set.of("ssm"), Set.of(), Set.of()), ''' def test_parses_key_config_key_and_status_flag(): [d] = si.parse_descriptors(SIMPLE) assert d["external_key"] == "ssm" assert d["config_key"] == "ssm" assert d["toggles"] == ["ssm"] assert d["in_status"] is True def test_conjunction_toggle_wrapping_onto_a_continuation_line(): # rds-data and redshift-data wrap because their enabled expression is a conjunction. # A line-oriented parser reads `true` off the *first* line, which for these two is # the configKey's line, and gets both the toggles and includeInStatus wrong. src = ''' descriptor("rds-data", "rds-data", config.services().rds().enabled() && config.services().rdsData().enabled(), false, null, null, 5000L, null, ServiceProtocol.REST_JSON, protocols(ServiceProtocol.REST_JSON), Set.of(), Set.of("rds-data"), Set.of(), Set.of(RdsDataController.class)), ''' [d] = si.parse_descriptors(src) assert d["external_key"] == "rds-data" assert d["toggles"] == ["rds", "rdsData"] assert d["in_status"] is False def test_status_flag_is_read_from_the_fourth_argument_not_the_first_true(): # `includeInStatus=false` on a descriptor whose second line contains other literals. src = ''' descriptor("sts", "iam", config.services().iam().enabled(), false, null, null, 5000L, AwsNamespaces.STS, ServiceProtocol.QUERY, protocols(ServiceProtocol.QUERY), Set.of(), Set.of("sts"), Set.of(), Set.of()), ''' assert si.parse_descriptors(src)[0]["in_status"] is False def test_nested_commas_do_not_split_an_argument(): args = si.split_top_level_args('f("a", Set.of("b", "c"), true)', 1) assert args == ['"a"', 'Set.of("b", "c")', "true"] def test_unbalanced_parens_raise_rather_than_returning_a_short_list(): with pytest.raises(ValueError, match="unbalanced"): si.split_top_level_args('f("a", "b"', 1) def test_non_literal_external_key_raises(): # check_service_matrix.py catches this case by counting call sites; here it must # fail loudly rather than silently dropping a service from every count. with pytest.raises(ValueError, match="not a plain string literal"): si.parse_descriptors("descriptor(KEY, \"ssm\", true, true, ...)") # --------------------------------------------------------------------------- # # Config accessors # --------------------------------------------------------------------------- # def test_config_accessors_exclude_docker_network_and_include_non_service_configs(): # DuckConfig does not end in ServiceConfig, so a `*ServiceConfig` pattern misses it # and under-counts by one. dockerNetwork() returns Optional and must not # be counted at all. src = """ interface ServicesConfig { Optional dockerNetwork(); SsmServiceConfig ssm(); SqsServiceConfig sqs(); DuckConfig duck(); } """ assert si.parse_config_accessors(src) == ["duck", "sqs", "ssm"] def test_missing_services_config_interface_raises(): with pytest.raises(ValueError, match="ServicesConfig not found"): si.parse_config_accessors("interface Unrelated { }") # --------------------------------------------------------------------------- # # Matrix rows # --------------------------------------------------------------------------- # MATRIX = f"""{si.csm.MATRIX_HEADING} | Service | Endpoint | Protocol | Supported operations | |---|---|---|---| | [DynamoDB](dynamodb.md) | `POST /` | JSON 1.1 | 28 | | [DynamoDB Streams](dynamodb.md#streams) | `POST /` | JSON 1.1 | 4 | | [IoT Core](iot.md) | `/things` | REST JSON | 40 | | [IoT Data](iot.md) | `/topics` | REST JSON | 5 | | [S3](s3.md) | `/{{bucket}}` | REST XML | 58 | {si.csm.MATRIX_END_HEADING} """ def test_facet_rows_are_detected_by_duplicate_slug_not_by_anchor(): # IoT Core and IoT Data both link plain `iot.md` with no anchor at all. An # anchor-based rule finds only the DynamoDB pair and over-counts pages by one. rows = si.parse_matrix_rows(MATRIX) assert len(rows) == 5 assert {r["slug"] for r in rows} == {"dynamodb", "iot", "s3"} assert rows[3]["anchor"] is None def test_rows_outside_the_matrix_headings_are_ignored(): src = f"| [Elsewhere](elsewhere.md) | x |\n\n{MATRIX}" assert all(r["slug"] != "elsewhere" for r in si.parse_matrix_rows(src)) # --------------------------------------------------------------------------- # # README # --------------------------------------------------------------------------- # def test_category_names_do_not_split_inside_parentheses(): # "IAM Identity Center (SSO Admin, OIDC, Access Portal, SCIM)" is one service name. # Splitting on every comma turns it into four, three of which ("OIDC", "SCIM)") # are not service names and inflate the count. src = ( "| Category | Services |\n|---|---|\n" "| Identity | IAM, IAM Identity Center (SSO Admin, OIDC, Access Portal, SCIM), KMS |\n" "\nFor operation-level compatibility, see the Services Overview.\n" ) assert si.parse_readme_category_names(src) == [ "IAM", "IAM Identity Center (SSO Admin, OIDC, Access Portal, SCIM)", "KMS", ] def test_detail_rows_take_the_first_cell_only(): src = ( "| Service | How it works | Notable features |\n|---|---|---|\n" "| SQS | In-process | Standard and FIFO queues, DLQ |\n" "\n" ) assert si.parse_readme_detail_rows(src) == ["SQS"] # --------------------------------------------------------------------------- # # Reconciliation # --------------------------------------------------------------------------- # def _data(**overrides) -> dict: base = { "descriptors": [ {"external_key": "iam", "config_key": "iam", "toggles": ["iam"], "in_status": True}, {"external_key": "sts", "config_key": "iam", "toggles": ["iam"], "in_status": False}, {"external_key": "s3", "config_key": "s3", "toggles": ["s3"], "in_status": True}, ], "config_accessors": ["iam", "s3", "duck"], "yaml_keys": ["iam", "s3", "duck"], "yaml_enabled": ["iam", "s3"], "packages": ["iam", "s3"], "matrix_rows": [ {"label": "IAM", "slug": "iam", "anchor": None}, {"label": "AWS Sign-In", "slug": "iam", "anchor": "signin"}, {"label": "S3", "slug": "s3", "anchor": None}, ], "doc_pages": ["iam", "s3"], "readme_categories": ["IAM", "S3"], "readme_details": ["S3"], "action_tables": ["s3"], "aliases": {"sts": "iam"}, "facet_pages": [], "deferred": [], } base.update(overrides) return base def test_shared_toggle_is_reported_with_every_descriptor_it_gates(): r = si.reconcile(_data()) assert r["catalog"]["shared_toggles"] == {"iam": ["iam", "sts"]} assert r["counts"]["catalog_toggles"] == 2 def test_facet_descriptor_is_the_non_owner_of_a_shared_page(): r = si.reconcile(_data()) assert r["catalog"]["facet_descriptors"] == ["sts"] assert r["counts"]["catalog_distinct_products"] == 2 def test_non_aws_entries_are_subtracted_from_the_aws_key_count(): r = si.reconcile(_data()) assert r["config"]["non_aws"] == ["duck"] assert r["counts"]["yaml_aws_keys"] == 2 def test_config_and_yaml_agree_after_normalisation(): r = si.reconcile(_data(config_accessors=["rdsData"], yaml_keys=["rds-data"])) assert r["config"]["accessor_only"] == [] assert r["config"]["yaml_only"] == [] def test_unregistered_package_is_reported_when_not_noted(): r = si.reconcile(_data(packages=["iam", "s3", "newthing"])) assert r["packages"]["unregistered"] == ["newthing"] def test_noted_package_is_not_reported_as_unregistered(): si.PACKAGE_NOTES["temporary"] = "test fixture" try: r = si.reconcile(_data(packages=["iam", "s3", "temporary"])) assert r["packages"]["unregistered"] == [] assert "temporary" in r["packages"]["noted"] finally: del si.PACKAGE_NOTES["temporary"] def test_facet_row_missing_from_readme_is_separated_from_a_real_gap(): # "AWS Sign-In" shares iam.md, so its absence from the README is by design. # A non-facet row missing from the README is a genuine finding. r = si.reconcile(_data(readme_categories=["IAM"])) assert r["readme"]["missing_from_categories"] == ["S3"] assert r["readme"]["missing_facet_rows"] == ["AWS Sign-In"] def test_readme_alias_covers_several_matrix_rows_at_once(): si.README_NAME_ALIASES["Identity"] = ["IAM", "AWS Sign-In"] try: r = si.reconcile(_data(readme_categories=["Identity", "S3"])) assert r["readme"]["unmatched_category_names"] == [] assert r["readme"]["missing_facet_rows"] == [] finally: del si.README_NAME_ALIASES["Identity"] def test_readme_name_matching_no_row_is_reported_rather_than_dropped(): r = si.reconcile(_data(readme_categories=["IAM", "S3", "Imaginary Service"])) assert r["readme"]["unmatched_category_names"] == ["Imaginary Service"] def test_deferral_within_the_warning_window_is_flagged(): deferred = [{"key": "x", "reason": "r", "by": "2026-09-15"}] r = si.reconcile(_data(deferred=deferred), today=date(2026, 9, 14)) assert r["docs"]["deferred_expiring"] == deferred def test_deferral_beyond_the_warning_window_is_not_flagged(): deferred = [{"key": "x", "reason": "r", "by": "2027-01-01"}] r = si.reconcile(_data(deferred=deferred), today=date(2026, 9, 14)) assert r["docs"]["deferred_expiring"] == [] def test_row_exempt_facet_pages_are_counted_without_requiring_a_matrix_row(): r = si.reconcile(_data(doc_pages=["iam", "s3", "iam-verification"], facet_pages=["iam-verification"])) assert r["counts"]["matrix_pages"] == 2 assert r["counts"]["doc_pages"] == 3 assert r["docs"]["pages_without_row"] == [] assert r["docs"]["rows_without_page"] == [] def test_facet_exemption_does_not_hide_an_unregistered_page(): r = si.reconcile(_data(doc_pages=["iam", "s3", "iam-verification", "unregistered"], facet_pages=["iam-verification"])) assert r["docs"]["pages_without_row"] == ["unregistered"] # --------------------------------------------------------------------------- # # End to end, against the real repo # --------------------------------------------------------------------------- # def test_real_sources_parse_and_render(): data = si.collect(si._repo_root()) result = si.reconcile(data) counts = result["counts"] # Not pinned to exact values: they move every time a service lands. What must hold # is that every source parsed to something plausible and the two gated equalities # this tool reports as green really are green. assert all(v > 50 for v in counts.values()) assert counts["matrix_pages"] == len(set(data["doc_pages"]) - set(data["facet_pages"])) assert counts["config_accessors"] == counts["yaml_keys"] assert result["docs"]["pages_without_row"] == [] assert result["docs"]["rows_without_page"] == [] assert result["packages"]["unregistered"] == [] assert "THE ANSWER" in si.render(result) def test_report_never_suggests_publishing_a_count(): # The hard rule the tool exists to protect, asserted rather than trusted to prose. text = si.render(si.reconcile(si.collect(si._repo_root()))) assert "Never write any of them into README.md or docs/" in text