@@ -357,6 +357,86 @@ def test_func(x):
357357 test_func .clear_cache ()
358358
359359
360+ @pytest .mark .memory
361+ def test_prometheus_prom_client_available_paths ():
362+ """Cover prometheus_client-available code paths via module-level patching.
363+
364+ Exercises: __init__ branch (L157-160), _setup_collector (L168-169),
365+ _init_prometheus_metrics (L179), CachierCollector.describe (L57), and
366+ CachierCollector.collect() None-metrics skip (L66 False branch).
367+
368+ """
369+ from unittest .mock import MagicMock , patch
370+
371+ from cachier .exporters .prometheus import CachierCollector
372+
373+ mock_registry = MagicMock ()
374+
375+ with (
376+ patch ("cachier.exporters.prometheus.PROMETHEUS_CLIENT_AVAILABLE" , True ),
377+ patch ("cachier.exporters.prometheus.CollectorRegistry" , lambda : mock_registry ),
378+ patch ("cachier.exporters.prometheus.prometheus_client" , MagicMock ()),
379+ ):
380+ exporter = PrometheusExporter (port = 0 , use_prometheus_client = True )
381+ assert exporter ._prom_client is not None
382+ assert exporter ._registry is mock_registry
383+
384+ # L57: CachierCollector.describe() -> []
385+ collector = CachierCollector (exporter )
386+ assert collector .describe () == []
387+
388+ # L66 False branch: register a function whose metrics is None
389+ class _NoMetrics :
390+ __module__ = "test"
391+ __name__ = "no_metrics"
392+ metrics = None
393+
394+ def __call__ (self , * a , ** kw ):
395+ pass
396+
397+ exporter ._registered_functions ["test.no_metrics" ] = _NoMetrics ()
398+
399+ with (
400+ patch ("cachier.exporters.prometheus.CounterMetricFamily" , lambda * a , ** kw : MagicMock ()),
401+ patch ("cachier.exporters.prometheus.GaugeMetricFamily" , lambda * a , ** kw : MagicMock ()),
402+ ):
403+ results = list (collector .collect ())
404+ # Yields 8 families even though snapshots is empty (no non-None metrics)
405+ assert len (results ) == 8
406+
407+
408+ def test_prometheus_module_import_with_prom_client ():
409+ """Cover the try-block import lines (L37-40) via module reload with a mocked prometheus_client."""
410+ import importlib
411+ import sys
412+ from unittest .mock import MagicMock
413+
414+ import cachier .exporters .prometheus as prom_mod
415+
416+ mock_prom = MagicMock ()
417+ mock_prom_core = MagicMock ()
418+
419+ saved_prom = sys .modules .get ("prometheus_client" )
420+ saved_core = sys .modules .get ("prometheus_client.core" )
421+
422+ sys .modules ["prometheus_client" ] = mock_prom
423+ sys .modules ["prometheus_client.core" ] = mock_prom_core
424+ try :
425+ importlib .reload (prom_mod )
426+ assert prom_mod .PROMETHEUS_CLIENT_AVAILABLE is True
427+ assert prom_mod .CollectorRegistry is mock_prom .CollectorRegistry
428+ finally :
429+ if saved_prom is None :
430+ sys .modules .pop ("prometheus_client" , None )
431+ else :
432+ sys .modules ["prometheus_client" ] = saved_prom
433+ if saved_core is None :
434+ sys .modules .pop ("prometheus_client.core" , None )
435+ else :
436+ sys .modules ["prometheus_client.core" ] = saved_core
437+ importlib .reload (prom_mod ) # restore original state
438+
439+
360440@pytest .mark .memory
361441def test_prometheus_stop_when_not_started ():
362442 """Test that stop() is a no-op when the server was never started."""
@@ -464,6 +544,82 @@ def test_func(x):
464544 test_func .clear_cache ()
465545
466546
547+ @pytest .mark .memory
548+ def test_prometheus_collector_collect_mocked ():
549+ """Test CachierCollector.collect() loop using mocked metric family types.
550+
551+ Covers lines 81-99 without requiring prometheus_client to be installed.
552+
553+ """
554+ from unittest .mock import MagicMock , patch
555+
556+ from cachier .exporters .prometheus import CachierCollector
557+
558+ @cachier (backend = "memory" , enable_metrics = True )
559+ def test_func (x ):
560+ return x * 2
561+
562+ test_func .clear_cache ()
563+ test_func (5 )
564+ test_func (5 )
565+
566+ exporter = PrometheusExporter (port = 0 , use_prometheus_client = False )
567+ exporter .register_function (test_func )
568+
569+ with (
570+ patch ("cachier.exporters.prometheus.CounterMetricFamily" , lambda * a , ** kw : MagicMock ()),
571+ patch ("cachier.exporters.prometheus.GaugeMetricFamily" , lambda * a , ** kw : MagicMock ()),
572+ ):
573+ collector = CachierCollector (exporter )
574+ results = list (collector .collect ())
575+ # 5 counter families + 3 gauge families
576+ assert len (results ) == 8
577+
578+ test_func .clear_cache ()
579+
580+
581+ @pytest .mark .memory
582+ def test_prometheus_start_prometheus_server_mocked ():
583+ """Test _start_prometheus_server and its MetricsHandler without prometheus_client.
584+
585+ Covers lines 285-329 (start() prom branch, MetricsHandler.do_GET, log_message).
586+
587+ """
588+ import sys
589+ import urllib .request
590+ from http .client import HTTPConnection
591+ from unittest .mock import MagicMock , patch
592+
593+ mock_exposition = MagicMock ()
594+ mock_exposition .generate_latest .return_value = b"# mocked metrics"
595+ mock_exposition .CONTENT_TYPE_LATEST = "text/plain"
596+
597+ prom_mock = MagicMock ()
598+ prom_mock .exposition = mock_exposition
599+
600+ exporter = PrometheusExporter (port = 0 , use_prometheus_client = False )
601+ # Manually inject prometheus state to trigger _start_prometheus_server path
602+ exporter ._prom_client = prom_mock
603+ exporter ._registry = MagicMock ()
604+
605+ with patch .dict (sys .modules , {"prometheus_client" : prom_mock , "prometheus_client.exposition" : mock_exposition }):
606+ exporter .start ()
607+ actual_port = exporter ._server .server_address [1 ]
608+ assert exporter ._server is not None
609+ try :
610+ response = urllib .request .urlopen (f"http://127.0.0.1:{ actual_port } /metrics" , timeout = 5 )
611+ assert b"# mocked metrics" in response .read ()
612+
613+ conn = HTTPConnection ("127.0.0.1" , actual_port )
614+ conn .request ("GET" , "/notfound" )
615+ resp = conn .getresponse ()
616+ assert resp .status == 404
617+ conn .close ()
618+ finally :
619+ exporter .stop ()
620+ assert exporter ._server is None
621+
622+
467623@pytest .mark .memory
468624def test_prometheus_collector_collect_skips_none_metrics ():
469625 """Test CachierCollector.collect() skips functions where metrics is None."""
0 commit comments