@@ -270,6 +270,92 @@ def test_multixact_size_v19_aurora_excludes_native(metrics_config: dict) -> None
270270 assert "has_native_fn" in sql , "Aurora probe should check native function availability"
271271
272272
273+ # =============================================================================
274+ # Unit Tests: RDS Path (rds_tools.pg_ls_multixactdir)
275+ # =============================================================================
276+
277+
278+ @pytest .mark .unit
279+ def test_multixact_size_v11_rds_probe_structure (metrics_config : dict ) -> None :
280+ """Test that v11 SQL has proper RDS probe structure."""
281+ sql = metrics_config ["metrics" ]["multixact_size" ]["sqls" ][11 ]
282+
283+ # Should check for RDS function
284+ assert "rds_tools" in sql , "v11 SQL should reference rds_tools schema"
285+ assert "pg_ls_multixactdir" in sql , "v11 SQL should reference pg_ls_multixactdir function"
286+ assert "has_rds_fn" in sql , "v11 SQL should have has_rds_fn flag"
287+
288+
289+ @pytest .mark .unit
290+ def test_multixact_size_v19_rds_probe_structure (metrics_config : dict ) -> None :
291+ """Test that v19 SQL has proper RDS probe structure."""
292+ sql = metrics_config ["metrics" ]["multixact_size" ]["sqls" ][19 ]
293+
294+ # Should have RDS fallback
295+ assert "rds_tools" in sql , "v19 SQL should reference rds_tools schema"
296+ assert "pg_ls_multixactdir" in sql , "v19 SQL should reference pg_ls_multixactdir function"
297+ assert "rds_probe_xml" in sql , "v19 SQL should have rds_probe_xml CTE"
298+
299+
300+ @pytest .mark .unit
301+ def test_multixact_size_v19_rds_excludes_native_and_aurora (metrics_config : dict ) -> None :
302+ """Test that RDS fallback only triggers when native and Aurora unavailable."""
303+ sql = metrics_config ["metrics" ]["multixact_size" ]["sqls" ][19 ]
304+
305+ # Find the rds_probe_xml WHERE clause
306+ # It should have: WHERE has_rds_fn AND NOT has_aurora_fn AND NOT has_native_fn
307+ rds_section_start = sql .find ("rds_probe_xml as" )
308+ assert rds_section_start > 0 , "rds_probe_xml CTE should exist"
309+
310+ # Get the section after rds_probe_xml definition
311+ rds_section = sql [rds_section_start :rds_section_start + 2000 ]
312+
313+ # Should exclude both native and aurora
314+ assert "has_rds_fn" in rds_section , "RDS probe should check has_rds_fn"
315+ assert "has_aurora_fn" in rds_section , "RDS probe should check has_aurora_fn"
316+ assert "has_native_fn" in rds_section , "RDS probe should check has_native_fn"
317+
318+
319+ @pytest .mark .unit
320+ def test_multixact_size_rds_probe_parses_members_and_offsets (metrics_config : dict ) -> None :
321+ """Test that RDS probe correctly separates members and offsets files."""
322+ sql = metrics_config ["metrics" ]["multixact_size" ]["sqls" ][11 ]
323+
324+ # RDS probe should filter files by path pattern
325+ assert "pg_multixact/members" in sql , "RDS probe should filter members files"
326+ assert "pg_multixact/offsets" in sql , "RDS probe should filter offsets files"
327+
328+
329+ @pytest .mark .unit
330+ def test_multixact_size_rds_probe_sums_file_sizes (metrics_config : dict ) -> None :
331+ """Test that RDS probe sums file sizes correctly."""
332+ sql = metrics_config ["metrics" ]["multixact_size" ]["sqls" ][11 ]
333+
334+ # Should use SUM to aggregate file sizes
335+ # The RDS function returns 'size' column
336+ assert "sum(size)" in sql .lower () or "sum(used_bytes)" in sql .lower (), \
337+ "RDS probe should sum file sizes"
338+
339+
340+ @pytest .mark .unit
341+ def test_multixact_size_rds_priority_between_aurora_and_local (metrics_config : dict ) -> None :
342+ """Test that RDS probe is checked after Aurora but before local filesystem."""
343+ sql = metrics_config ["metrics" ]["multixact_size" ]["sqls" ][19 ]
344+
345+ # Find positions in the picked CTE
346+ picked_section_start = sql .find ("picked as" )
347+ assert picked_section_start > 0 , "picked CTE should exist"
348+ picked_section = sql [picked_section_start :]
349+
350+ aurora_pos = picked_section .find ("aurora_probe_xml" )
351+ rds_pos = picked_section .find ("rds_probe_xml" )
352+ local_pos = picked_section .find ("local_probe_xml" )
353+
354+ # Order should be: aurora < rds < local (in the UNION ALL)
355+ assert aurora_pos < rds_pos , "Aurora should come before RDS in picked CTE"
356+ assert rds_pos < local_pos , "RDS should come before local in picked CTE"
357+
358+
273359@pytest .mark .unit
274360def test_multixact_size_gauges_defined (metrics_config : dict ) -> None :
275361 """Test that the metric has proper gauge definitions."""
@@ -490,6 +576,199 @@ def test_mock_pg19_fallback_when_function_missing() -> None:
490576 assert "expected_probe" in scenario
491577
492578
579+ # =============================================================================
580+ # Mock Tests: RDS Path (rds_tools.pg_ls_multixactdir)
581+ # =============================================================================
582+
583+
584+ @pytest .mark .unit
585+ def test_mock_rds_function_output_parsing () -> None :
586+ """
587+ Mock test: simulate rds_tools.pg_ls_multixactdir() output parsing.
588+
589+ The RDS function returns rows with columns: name, size
590+ where name is like 'pg_multixact/members/0000' or 'pg_multixact/offsets/0000'
591+ """
592+ # Simulate what rds_tools.pg_ls_multixactdir() returns
593+ mock_rds_output = [
594+ {"name" : "pg_multixact/members/0000" , "size" : 262144 },
595+ {"name" : "pg_multixact/members/0001" , "size" : 262144 },
596+ {"name" : "pg_multixact/members/0002" , "size" : 131072 }, # partial segment
597+ {"name" : "pg_multixact/offsets/0000" , "size" : 262144 },
598+ {"name" : "pg_multixact/offsets/0001" , "size" : 8192 }, # partial segment
599+ ]
600+
601+ # Calculate expected sums (simulating the SQL logic)
602+ members_bytes = sum (
603+ row ["size" ] for row in mock_rds_output
604+ if "pg_multixact/members" in row ["name" ]
605+ )
606+ offsets_bytes = sum (
607+ row ["size" ] for row in mock_rds_output
608+ if "pg_multixact/offsets" in row ["name" ]
609+ )
610+
611+ assert members_bytes == 262144 + 262144 + 131072 # 655360
612+ assert offsets_bytes == 262144 + 8192 # 270336
613+
614+
615+ @pytest .mark .unit
616+ def test_mock_rds_empty_directory () -> None :
617+ """
618+ Mock test: simulate empty pg_multixact directories on RDS.
619+
620+ When directories are empty, the function should return 0 bytes.
621+ """
622+ mock_rds_output = [] # No files
623+
624+ members_bytes = sum (
625+ row ["size" ] for row in mock_rds_output
626+ if "pg_multixact/members" in row ["name" ]
627+ )
628+ offsets_bytes = sum (
629+ row ["size" ] for row in mock_rds_output
630+ if "pg_multixact/offsets" in row ["name" ]
631+ )
632+
633+ assert members_bytes == 0
634+ assert offsets_bytes == 0
635+
636+
637+ @pytest .mark .unit
638+ def test_mock_rds_large_multixact_usage () -> None :
639+ """
640+ Mock test: simulate large multixact usage on RDS.
641+
642+ Verify calculations work correctly with many files and large sizes.
643+ """
644+ # Simulate a busy system with many multixact segments
645+ # 100 member segments + 10 offset segments
646+ mock_rds_output = []
647+
648+ # Add 100 member files (each 256KB = 262144 bytes)
649+ for i in range (100 ):
650+ mock_rds_output .append ({
651+ "name" : f"pg_multixact/members/{ i :04X} " ,
652+ "size" : 262144
653+ })
654+
655+ # Add 10 offset files
656+ for i in range (10 ):
657+ mock_rds_output .append ({
658+ "name" : f"pg_multixact/offsets/{ i :04X} " ,
659+ "size" : 262144
660+ })
661+
662+ members_bytes = sum (
663+ row ["size" ] for row in mock_rds_output
664+ if "pg_multixact/members" in row ["name" ]
665+ )
666+ offsets_bytes = sum (
667+ row ["size" ] for row in mock_rds_output
668+ if "pg_multixact/offsets" in row ["name" ]
669+ )
670+
671+ # 100 * 256KB = 25.6MB
672+ assert members_bytes == 100 * 262144 # 26214400 bytes
673+ # 10 * 256KB = 2.56MB
674+ assert offsets_bytes == 10 * 262144 # 2621440 bytes
675+
676+
677+ @pytest .mark .unit
678+ def test_mock_rds_path_filtering () -> None :
679+ """
680+ Mock test: verify path filtering correctly separates members from offsets.
681+
682+ The SQL uses LIKE patterns to filter files.
683+ """
684+ mock_rds_output = [
685+ # Members files (should match 'pg_multixact/members%')
686+ {"name" : "pg_multixact/members/0000" , "size" : 1000 },
687+ {"name" : "pg_multixact/members/0001" , "size" : 2000 },
688+ # Offsets files (should match 'pg_multixact/offsets%')
689+ {"name" : "pg_multixact/offsets/0000" , "size" : 3000 },
690+ # Other files that should NOT be counted
691+ {"name" : "pg_multixact/other_file" , "size" : 9999 },
692+ {"name" : "base/1234/5678" , "size" : 9999 },
693+ ]
694+
695+ # SQL uses: WHERE name LIKE 'pg_multixact/members%'
696+ members_bytes = sum (
697+ row ["size" ] for row in mock_rds_output
698+ if row ["name" ].startswith ("pg_multixact/members" )
699+ )
700+
701+ # SQL uses: WHERE name LIKE 'pg_multixact/offsets%'
702+ offsets_bytes = sum (
703+ row ["size" ] for row in mock_rds_output
704+ if row ["name" ].startswith ("pg_multixact/offsets" )
705+ )
706+
707+ assert members_bytes == 1000 + 2000 # 3000
708+ assert offsets_bytes == 3000
709+ # Other files should not be included
710+ assert members_bytes + offsets_bytes == 6000 # Not 6000 + 9999 + 9999
711+
712+
713+ @pytest .mark .unit
714+ def test_mock_rds_coalesce_null_handling () -> None :
715+ """
716+ Mock test: verify COALESCE handles NULL when no files exist for a category.
717+
718+ If only members files exist (no offsets), offsets_bytes should be 0, not NULL.
719+ """
720+ mock_rds_output = [
721+ {"name" : "pg_multixact/members/0000" , "size" : 262144 },
722+ # No offsets files
723+ ]
724+
725+ members_bytes = sum (
726+ row ["size" ] for row in mock_rds_output
727+ if row ["name" ].startswith ("pg_multixact/members" )
728+ )
729+
730+ offsets_files = [
731+ row for row in mock_rds_output
732+ if row ["name" ].startswith ("pg_multixact/offsets" )
733+ ]
734+
735+ # In SQL: COALESCE((SELECT sz FROM offsets), 0)
736+ offsets_bytes = sum (row ["size" ] for row in offsets_files ) if offsets_files else 0
737+
738+ assert members_bytes == 262144
739+ assert offsets_bytes == 0 # Should be 0, not None/NULL
740+
741+
742+ @pytest .mark .unit
743+ def test_mock_rds_status_code_logic () -> None :
744+ """
745+ Mock test: verify status_code logic for RDS probe.
746+
747+ status_code should be:
748+ - 0: Data found (files exist)
749+ - 1: No data (empty directories but probe worked)
750+ """
751+ # Scenario 1: Files exist
752+ mock_rds_output_with_files = [
753+ {"name" : "pg_multixact/members/0000" , "size" : 262144 },
754+ ]
755+ has_rows = len ([
756+ r for r in mock_rds_output_with_files
757+ if "pg_multixact/" in r ["name" ]
758+ ]) > 0
759+ status_code = 0 if has_rows else 1
760+ assert status_code == 0
761+
762+ # Scenario 2: No files
763+ mock_rds_output_empty = []
764+ has_rows = len ([
765+ r for r in mock_rds_output_empty
766+ if "pg_multixact/" in r ["name" ]
767+ ]) > 0
768+ status_code = 0 if has_rows else 1
769+ assert status_code == 1
770+
771+
493772# =============================================================================
494773# TODO: PG19 Image Tests (Pending PG19 Release)
495774# =============================================================================
0 commit comments