From 8be268c61c8548164b59f909147a7ac79a1bb577 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Mon, 6 Jul 2026 17:17:23 +0000 Subject: [PATCH] CodeRabbit Generated Unit Tests: Add unit tests for PR changes --- tests/test_conftest_helpers.py | 61 ++++++++ tests/test_index_dtype_resolution.py | 134 ++++++++++++++++ tests/test_io_results.py | 92 ++++++++++- tests/test_non_dates.py | 144 ++++++++++++++++++ tests/test_process_signals.py | 51 +++++++ .../test_plot_pytrendy_index_types.py | 122 +++++++++++++++ 6 files changed, 601 insertions(+), 3 deletions(-) create mode 100644 tests/test_conftest_helpers.py create mode 100644 tests/test_index_dtype_resolution.py create mode 100644 tests/test_non_dates.py create mode 100644 tests/test_process_signals.py create mode 100644 tests/tests_plotting/edgecases/test_plot_pytrendy_index_types.py diff --git a/tests/test_conftest_helpers.py b/tests/test_conftest_helpers.py new file mode 100644 index 00000000..a0183664 --- /dev/null +++ b/tests/test_conftest_helpers.py @@ -0,0 +1,61 @@ +""" +Unit tests for the `assert_segments_match` test helper in conftest.py. + +This helper now tolerates minor floating point noise (rounding to 6 decimal +places) when comparing `start`/`end` values that are floats, to support the +new float-index test cases, while still requiring exact equality for +non-float values (e.g. integers, strings, dates). +""" + +import pytest +from conftest import assert_segments_match + + +class TestAssertSegmentsMatchFloatTolerance: + """Tests for the float-rounding tolerance added to assert_segments_match.""" + + def test_matches_exact_float_values(self): + """Identical float values should match.""" + detected = [{'direction': 'Up', 'start': 0.1, 'end': 0.2}] + expected = [{'direction': 'Up', 'start': 0.1, 'end': 0.2}] + assert_segments_match(detected, expected) # should not raise + + def test_matches_float_within_rounding_tolerance(self): + """Floating point noise beyond the 6th decimal place should still match.""" + detected = [{'direction': 'Up', 'start': 0.1 + 1e-9, 'end': 0.2 - 1e-9}] + expected = [{'direction': 'Up', 'start': 0.1, 'end': 0.2}] + assert_segments_match(detected, expected) # should not raise + + def test_mismatched_float_beyond_tolerance_raises(self): + """Differences within the first 6 decimal places should still fail.""" + detected = [{'direction': 'Up', 'start': 0.100002, 'end': 0.2}] + expected = [{'direction': 'Up', 'start': 0.1, 'end': 0.2}] + with pytest.raises(AssertionError): + assert_segments_match(detected, expected) + + def test_non_float_values_use_exact_equality(self): + """Integer start/end values should still require exact equality.""" + detected = [{'direction': 'Up', 'start': 5, 'end': 10}] + expected = [{'direction': 'Up', 'start': 5, 'end': 10}] + assert_segments_match(detected, expected) # should not raise + + mismatched_expected = [{'direction': 'Up', 'start': 5, 'end': 11}] + with pytest.raises(AssertionError): + assert_segments_match(detected, mismatched_expected) + + def test_string_values_use_exact_equality(self): + """String start/end labels should still require exact equality.""" + detected = [{'direction': 'Up', 'start': 'Step 1', 'end': 'Step 5'}] + expected = [{'direction': 'Up', 'start': 'Step 1', 'end': 'Step 5'}] + assert_segments_match(detected, expected) # should not raise + + mismatched_expected = [{'direction': 'Up', 'start': 'Step 1', 'end': 'Step 6'}] + with pytest.raises(AssertionError): + assert_segments_match(detected, mismatched_expected) + + def test_direction_mismatch_still_raises(self): + """Mismatched direction should raise regardless of float tolerance changes.""" + detected = [{'direction': 'Up', 'start': 0.1, 'end': 0.2}] + expected = [{'direction': 'Down', 'start': 0.1, 'end': 0.2}] + with pytest.raises(AssertionError): + assert_segments_match(detected, expected) \ No newline at end of file diff --git a/tests/test_index_dtype_resolution.py b/tests/test_index_dtype_resolution.py new file mode 100644 index 00000000..c8374255 --- /dev/null +++ b/tests/test_index_dtype_resolution.py @@ -0,0 +1,134 @@ +""" +Tests for detect_trends's date_col dtype resolution logic. + +`detect_trends` now inspects the dtype of `date_col` (or generates an implicit +integer index when `date_col` is None) to decide how segment boundaries should +be represented downstream: as dates, integers, floats, string labels, or +pre-parsed datetime64 values. These tests exercise each of those resolution +branches, including the fallback and error paths. +""" + +import pytest +import pandas as pd +import numpy as np +import pytrendy as pt + + +class TestIndexDtypeResolution: + """Tests for detect_trends's automatic detection of date_col dtype / index_type.""" + + @pytest.mark.core + def test_default_date_col_none_produces_integer_index(self): + """When date_col is not supplied, an implicit integer index should be used.""" + df = pt.load_data('series_synthetic') + results = pt.detect_trends( + df, + value_col='gradual', + plot=False, + ) + + assert results.index_type == 'integer' + assert all(isinstance(seg['start'], (int, np.integer)) for seg in results.segments) + assert all(isinstance(seg['end'], (int, np.integer)) for seg in results.segments) + + @pytest.mark.core + def test_explicit_integer_date_col_matches_implicit_index(self): + """Passing an explicit integer date_col should behave the same as date_col=None.""" + df = pt.load_data('series_synthetic') + df['int_index'] = np.arange(len(df)) + results = pt.detect_trends( + df, + value_col='gradual', + date_col='int_index', + plot=False, + ) + + assert results.index_type == 'integer' + assert results.segments[0]['direction'] == 'Up' + assert results.segments[0]['start'] == 1 + assert results.segments[0]['end'] == 23 + + @pytest.mark.core + def test_datetime64_column_sets_datetime64_index_type(self): + """A date_col already parsed to datetime64 should be classified as 'datetime64', + distinct from the 'date' index_type used for parseable string columns. + """ + df = pt.load_data('series_synthetic') + df['date'] = pd.to_datetime(df['date']) + results = pt.detect_trends( + df, + value_col='gradual', + date_col='date', + plot=False, + ) + + assert results.index_type == 'datetime64' + # Underlying detection is unaffected: boundaries match the date-string variant. + assert results.segments[0]['direction'] == 'Up' + assert pd.Timestamp(results.segments[0]['start']) == pd.Timestamp('2025-01-02') + assert pd.Timestamp(results.segments[0]['end']) == pd.Timestamp('2025-01-24') + + @pytest.mark.core + def test_datetime64_column_with_plot_raises_not_implemented(self): + """plot_pytrendy does not yet accept the 'datetime64' index_type, so requesting a + plot with a pre-parsed datetime64 date_col should surface a clear error rather + than silently mis-rendering. + """ + df = pt.load_data('series_synthetic') + df['date'] = pd.to_datetime(df['date']) + + with pytest.raises(NotImplementedError, match="Index Type datetime64 not yet implemented"): + pt.detect_trends( + df, + value_col='gradual', + date_col='date', + plot=True, + ) + + @pytest.mark.core + def test_partially_parseable_string_falls_back_to_string_index(self, capsys): + """If not every value in a string date_col parses to a date, pytrendy should fall + back to treating the column as a string lookup rather than raising. + """ + df = pt.load_data('series_synthetic') + df.loc[0, 'date'] = 'not-a-date' # breaks full-column date parsing + + results = pt.detect_trends( + df, + value_col='gradual', + date_col='date', + plot=False, + ) + + assert results.index_type == 'string' + captured = capsys.readouterr() + assert 'treating as string lookup' in captured.out + + @pytest.mark.core + def test_unsupported_dtype_raises_not_implemented_error(self): + """An unsupported dtype for date_col (e.g. boolean) should raise NotImplementedError.""" + df = pt.load_data('series_synthetic') + df['flag_col'] = [i % 2 == 0 for i in range(len(df))] + + with pytest.raises(NotImplementedError, match="unimplimented dtype"): + pt.detect_trends( + df, + value_col='gradual', + date_col='flag_col', + plot=False, + ) + + @pytest.mark.core + def test_float_date_col_sets_float_index_type(self): + """A float date_col should be classified as the 'float' index_type.""" + df = pt.load_data('series_synthetic') + df['float_lookup'] = np.linspace(0, 1, len(df)) + results = pt.detect_trends( + df, + value_col='gradual', + date_col='float_lookup', + plot=False, + ) + + assert results.index_type == 'float' + assert all(isinstance(seg['start'], float) for seg in results.segments) \ No newline at end of file diff --git a/tests/test_io_results.py b/tests/test_io_results.py index cce71741..74f60354 100644 --- a/tests/test_io_results.py +++ b/tests/test_io_results.py @@ -62,6 +62,37 @@ def zeros_signal(): return df +@pytest.fixture +def integer_index_results(): + """ + Fixture to create results from gradual synthetic data using the implicit + integer index (no date_col supplied). + """ + df = pt.load_data('series_synthetic') + results = pt.detect_trends( + df, + value_col='gradual', + plot=False + ) + return results + + +@pytest.fixture +def string_index_results(): + """ + Fixture to create results from gradual synthetic data using a string-label index. + """ + df = pt.load_data('series_synthetic') + df['label'] = [f"Step {i}" for i in range(len(df))] + results = pt.detect_trends( + df, + value_col='gradual', + date_col='label', + plot=False + ) + return results + + @pytest.fixture def outlier_signal(): """ @@ -401,6 +432,7 @@ def test_filter_segments_by_direction_flat(self, gradual_results): @pytest.mark.core def test_filter_segments_by_direction_noise(self, outlier_signal): """Test filtering segments by 'Noise' direction.""" + results = pt.detect_trends( outlier_signal, date_col='date', @@ -415,8 +447,11 @@ def test_filter_segments_by_direction_noise(self, outlier_signal): assert len(noise_segments) == 1 # Expected Noise segment from outlier signal - expected_noise = [ - {'direction': 'Noise', 'start': '2025-02-19', 'end': '2025-02-21'}, + expected_noise = [{ + 'direction': 'Noise', + 'start': pd.to_datetime('2025-02-19'), + 'end': pd.to_datetime('2025-02-21') + }, ] assert_segments_match(noise_segments, expected_noise) @@ -712,7 +747,58 @@ class TestResultsDataStructures: def test_segments_have_required_fields(self, gradual_results): """Test that all segments have required fields.""" required_fields = ['direction', 'start', 'end', 'time_index', 'days'] - + for i, segment in enumerate(gradual_results.segments): for field in required_fields: assert field in segment, f"Segment {i} missing field: {field}" + + +class TestResultsIndexTypeAwareness: + """Tests for PyTrendyResults handling of the new `index_type` parameter.""" + + @pytest.mark.core + def test_default_index_type_is_date(self): + """PyTrendyResults defaults to 'date' index_type when not specified.""" + from pytrendy.io.results_pytrendy import PyTrendyResults + results = PyTrendyResults([]) + assert results.index_type == 'date' + + @pytest.mark.core + def test_index_type_stored_from_detect_trends(self, gradual_results, integer_index_results, string_index_results): + """detect_trends should populate index_type based on the resolved date_col dtype.""" + assert gradual_results.index_type == 'date' + assert integer_index_results.index_type == 'integer' + assert string_index_results.index_type == 'string' + + @pytest.mark.core + def test_summary_uses_days_for_date_index(self, gradual_results): + """Date-indexed results keep the 'days' column name in the summary.""" + assert 'days' in gradual_results.df_summary.columns + assert 'index steps' not in gradual_results.df_summary.columns + + @pytest.mark.core + def test_summary_uses_index_steps_for_integer_index(self, integer_index_results): + """Integer-indexed results rename the 'days' column to 'index steps' in the summary.""" + assert 'index steps' in integer_index_results.df_summary.columns + assert 'days' not in integer_index_results.df_summary.columns + + @pytest.mark.core + def test_print_summary_descriptor_for_date_index(self, gradual_results, capsys): + """print_summary describes the best segment using 'dates' for date-indexed results.""" + gradual_results.print_summary() + captured = capsys.readouterr() + assert 'between dates' in captured.out + + @pytest.mark.core + def test_print_summary_descriptor_for_integer_index(self, integer_index_results, capsys): + """print_summary describes the best segment using 'indexes' for integer-indexed results.""" + integer_index_results.print_summary() + captured = capsys.readouterr() + assert 'between indexes' in captured.out + + @pytest.mark.core + def test_print_summary_descriptor_for_string_index(self, string_index_results, capsys): + """print_summary describes the best segment using 'labels' for string-indexed results.""" + string_index_results.print_summary() + captured = capsys.readouterr() + assert 'between labels' in captured.out diff --git a/tests/test_non_dates.py b/tests/test_non_dates.py new file mode 100644 index 00000000..210f73a1 --- /dev/null +++ b/tests/test_non_dates.py @@ -0,0 +1,144 @@ +""" +TODO Add description here +""" +import pytest +import pytrendy as pt +import pandas as pd +import numpy as np +from conftest import assert_segments_match + + +class TestNonDateCases: + """Test cases where non-date indexes are used""" + + @pytest.mark.core + def test_integer_index(self): + """Test standard gradual trend but with no date index.""" + df = pt.load_data('series_synthetic') + results = pt.detect_trends( + df, + value_col='gradual', + plot=False, + method_params=dict(is_abrupt_padded=False) + ) + + # Expected segments based on current behavior + expected_segments = [ + {'direction': 'Up', 'start': 1, 'end': 23}, + {'direction': 'Down', 'start': 24, 'end': 35}, + {'direction': 'Flat', 'start': 36, 'end': 39}, + {'direction': 'Up', 'start': 40, 'end': 72}, + {'direction': 'Flat', 'start': 73, 'end': 75}, + {'direction': 'Down', 'start': 76, 'end': 90}, + {'direction': 'Up', 'start': 91, 'end': 127}, + {'direction': 'Down', 'start': 128, 'end': 167}, + {'direction': 'Flat', 'start': 168, 'end': 180}, + ] + + assert_segments_match(results.segments, expected_segments) + + @pytest.mark.core + def test_float_index(self): + """Test standard gradual trend but with float lookup.""" + df = pt.load_data('series_synthetic') + df['float_lookup'] = np.linspace(0, 1, len(df)) + results = pt.detect_trends( + df, + value_col='gradual', + date_col='float_lookup', + plot=False, + method_params=dict(is_abrupt_padded=False) + ) + + # Expected segments based on current behavior + expected_segments = [ + {'direction': 'Up', 'start': 0.005556, 'end': 0.127778}, + {'direction': 'Down', 'start': 0.133333, 'end': 0.194444}, + {'direction': 'Flat', 'start': 0.200000, 'end': 0.216667}, + {'direction': 'Up', 'start': 0.222222, 'end': 0.400000}, + {'direction': 'Flat', 'start': 0.405556, 'end': 0.416667}, + {'direction': 'Down', 'start': 0.422222, 'end': 0.500000}, + {'direction': 'Up', 'start': 0.505556, 'end': 0.705556}, + {'direction': 'Down', 'start': 0.711111, 'end': 0.927778}, + {'direction': 'Flat', 'start': 0.933333, 'end': 1.000000}, + ] + + assert_segments_match(results.segments, expected_segments) + + @pytest.mark.core + def test_abrupt_trends_no_padding_integer_index(self): + """Test that abrupt detection/shaving/cleanup arithmetic works with an implicit + integer index, matching the equivalent date-indexed result (see + TestCoreCases.test_abrupt_trends_no_padding) but expressed as day-offsets + from 2025-01-01 (the first row of the synthetic dataset). + """ + df = pt.load_data('series_synthetic') + results = pt.detect_trends( + df, + value_col='abrupt', + plot=False + ) + + # Day-offsets from 2025-01-01, mirroring TestCoreCases.test_abrupt_trends_no_padding + expected_segments = [ + {'direction': 'Flat', 'start': 0, 'end': 57}, + {'direction': 'Up', 'start': 58, 'end': 59}, + {'direction': 'Flat', 'start': 60, 'end': 120}, + {'direction': 'Down', 'start': 121, 'end': 124}, + {'direction': 'Flat', 'start': 125, 'end': 180}, + ] + + assert_segments_match(results.segments, expected_segments) + + @pytest.mark.core + def test_abrupt_trends_with_padding_integer_index(self): + """Test that abrupt padding (shave_abrupt_trends + clean_artifacts + update_neighbours) + works correctly using integer index arithmetic, matching the equivalent date-indexed + result (see TestCoreCases.test_abrupt_trends_with_padding). + """ + df = pt.load_data('series_synthetic') + results = pt.detect_trends( + df, + value_col='abrupt', + plot=False, + method_params=dict(abrupt_padding=28) + ) + + # Day-offsets from 2025-01-01, mirroring TestCoreCases.test_abrupt_trends_with_padding + expected_segments = [ + {'direction': 'Flat', 'start': 0, 'end': 57}, + {'direction': 'Up', 'start': 58, 'end': 87}, + {'direction': 'Flat', 'start': 88, 'end': 120}, + {'direction': 'Down', 'start': 121, 'end': 152}, + {'direction': 'Flat', 'start': 153, 'end': 180}, + ] + + assert_segments_match(results.segments, expected_segments) + + @pytest.mark.core + def test_string_index(self): + """Test standard gradual trend but with string lookup.""" + df = pt.load_data('series_synthetic') + df['string_lookup'] = [f"Step {i}" for i in range(len(df))] + results = pt.detect_trends( + df, + value_col='gradual', + date_col='string_lookup', + plot=False, + method_params=dict(is_abrupt_padded=False) + ) + + # Expected segments based on current behavior + expected_segments = [ + {'direction': 'Up', 'start': 'Step 1', 'end': 'Step 23'}, + {'direction': 'Down', 'start': 'Step 24', 'end': 'Step 35'}, + {'direction': 'Flat', 'start': 'Step 36', 'end': 'Step 39'}, + {'direction': 'Up', 'start': 'Step 40', 'end': 'Step 72'}, + {'direction': 'Flat', 'start': 'Step 73', 'end': 'Step 75'}, + {'direction': 'Down', 'start': 'Step 76', 'end': 'Step 90'}, + {'direction': 'Up', 'start': 'Step 91', 'end': 'Step 127'}, + {'direction': 'Down', 'start': 'Step 128', 'end': 'Step 167'}, + {'direction': 'Flat', 'start': 'Step 168', 'end': 'Step 180'}, + ] + + assert_segments_match(results.segments, expected_segments) \ No newline at end of file diff --git a/tests/test_process_signals.py b/tests/test_process_signals.py new file mode 100644 index 00000000..3caf1649 --- /dev/null +++ b/tests/test_process_signals.py @@ -0,0 +1,51 @@ +""" +Tests for process_signals's requirement that the input DataFrame use an integer index. + +pytrendy's internal pipeline always converts the caller-facing index (dates, floats, +strings, etc.) into a positional integer index before calling `process_signals`, so +downstream arithmetic (previously date-based, now purely integer-based) is valid. +`process_signals` asserts this precondition explicitly. +""" + +import pytest +import pandas as pd +import numpy as np +from pytrendy.process_signals import process_signals + + +class TestProcessSignalsIndexAssertion: + """Tests for the integer-index precondition added to process_signals.""" + + def test_datetime_index_raises_assertion_error(self): + """A DatetimeIndex is not an integer index and should be rejected immediately.""" + dates = pd.date_range('2025-01-01', periods=30, freq='D') + df = pd.DataFrame({'value': np.arange(30, dtype=float)}, index=dates) + method_params = {'avoid_noise': True} + + with pytest.raises(AssertionError, match="Supplied Index has type"): + process_signals(df, 'value', method_params) + + def test_float_index_raises_assertion_error(self): + """A float index should also be rejected.""" + df = pd.DataFrame( + {'value': np.arange(30, dtype=float)}, + index=np.linspace(0, 1, 30) + ) + method_params = {'avoid_noise': True} + + with pytest.raises(AssertionError, match="Supplied Index has type"): + process_signals(df, 'value', method_params) + + def test_integer_index_passes_assertion(self): + """A plain integer (RangeIndex) should satisfy the precondition and proceed normally.""" + df = pd.DataFrame( + {'value': np.linspace(0, 30, 30)}, + index=pd.RangeIndex(30) + ) + method_params = {'avoid_noise': True} + + result = process_signals(df, 'value', method_params) + + assert 'trend_flag' in result.columns + assert 'noise_flag' in result.columns + assert 'flat_flag' in result.columns \ No newline at end of file diff --git a/tests/tests_plotting/edgecases/test_plot_pytrendy_index_types.py b/tests/tests_plotting/edgecases/test_plot_pytrendy_index_types.py new file mode 100644 index 00000000..ff94a124 --- /dev/null +++ b/tests/tests_plotting/edgecases/test_plot_pytrendy_index_types.py @@ -0,0 +1,122 @@ +""" +Tests for plot_pytrendy's new `index_type` parameter. + +`plot_pytrendy` now accepts an `index_type` argument ("date", "integer", "float", +or "string") that governs how segment boundaries and neighbour adjacency are +computed. These tests are functional smoke tests (no image comparison baseline) +verifying that each supported index_type renders without error, that invalid +index types are rejected, and that the index type is announced via print. +""" + +import pytest +import pandas as pd +import numpy as np +import matplotlib.pyplot as plt +from pytrendy.io.plot_pytrendy import plot_pytrendy + + +class TestPlotPytrendyIndexTypes: + """Tests for plot_pytrendy support of non-date index types.""" + + def test_invalid_index_type_raises(self): + """An unsupported index_type should raise NotImplementedError before any plotting occurs.""" + dates = pd.date_range('2025-01-01', periods=10, freq='D') + df = pd.DataFrame({'value': np.arange(10, dtype=float)}, index=dates) + segments = [{'direction': 'Up', 'start': dates[0], 'end': dates[-1]}] + + with pytest.raises(NotImplementedError, match="Index Type bogus not yet implemented"): + plot_pytrendy(df=df, value_col='value', segments_enhanced=segments, + index_type='bogus', suppress_show=True) + + def test_prints_index_type(self, capsys): + """plot_pytrendy should print the index_type it was invoked with.""" + df = pd.DataFrame({'value': [0.0, 1.0, 2.0]}, index=pd.RangeIndex(3)) + fig = plot_pytrendy(df=df, value_col='value', segments_enhanced=[], + index_type='integer', suppress_show=True) + plt.close(fig) + + captured = capsys.readouterr() + assert 'internal index type integer' in captured.out + + def test_plot_with_integer_index_single_segment(self): + """A single Up segment on an integer index should render without error.""" + df = pd.DataFrame({'value': np.arange(20, dtype=float)}, index=pd.RangeIndex(20)) + segments = [{'direction': 'Up', 'start': 5, 'end': 15, 'trend_class': 'gradual'}] + + fig = plot_pytrendy(df=df, value_col='value', segments_enhanced=segments, + index_type='integer', suppress_show=True) + + assert isinstance(fig, plt.Figure) + plt.close(fig) + + def test_plot_with_integer_index_neighbouring_segments(self): + """Two touching same-direction segments should exercise the neighbour-adjustment + and vertical-line-drawing logic paths for integer indices.""" + df = pd.DataFrame({'value': np.arange(20, dtype=float)}, index=pd.RangeIndex(20)) + segments = [ + {'direction': 'Up', 'start': 0, 'end': 9, 'trend_class': 'gradual', 'change_rank': 2}, + {'direction': 'Up', 'start': 10, 'end': 19, 'trend_class': 'gradual', 'change_rank': 1}, + ] + + fig = plot_pytrendy(df=df, value_col='value', segments_enhanced=segments, + index_type='integer', suppress_show=True) + + assert isinstance(fig, plt.Figure) + plt.close(fig) + + def test_plot_with_float_index(self): + """A segment addressed by float labels should render without error.""" + index = np.linspace(0, 1, 20) + df = pd.DataFrame({'value': np.arange(20, dtype=float)}, index=index) + segments = [{'direction': 'Down', 'start': float(index[5]), 'end': float(index[15]), + 'trend_class': 'gradual'}] + + fig = plot_pytrendy(df=df, value_col='value', segments_enhanced=segments, + index_type='float', suppress_show=True) + + assert isinstance(fig, plt.Figure) + plt.close(fig) + + def test_plot_with_string_index(self): + """A segment addressed by string labels should render without error, including the + change_rank annotation path (which is intentionally skipped for string indices).""" + labels = [f"Step {i}" for i in range(20)] + df = pd.DataFrame({'value': np.arange(20, dtype=float)}, index=labels) + segments = [{'direction': 'Up', 'start': 'Step 5', 'end': 'Step 15', + 'trend_class': 'gradual', 'change_rank': 1}] + + fig = plot_pytrendy(df=df, value_col='value', segments_enhanced=segments, + index_type='string', suppress_show=True) + + assert isinstance(fig, plt.Figure) + plt.close(fig) + + def test_plot_with_string_index_neighbouring_segments(self): + """Two touching same-direction string-indexed segments should render without error.""" + labels = [f"Step {i}" for i in range(20)] + df = pd.DataFrame({'value': np.arange(20, dtype=float)}, index=labels) + segments = [ + {'direction': 'Down', 'start': 'Step 0', 'end': 'Step 9', 'trend_class': 'gradual'}, + {'direction': 'Down', 'start': 'Step 10', 'end': 'Step 19', 'trend_class': 'gradual'}, + ] + + fig = plot_pytrendy(df=df, value_col='value', segments_enhanced=segments, + index_type='string', suppress_show=True) + + assert isinstance(fig, plt.Figure) + plt.close(fig) + + def test_plot_with_noise_and_abrupt_segment_integer_index(self): + """Noise and abrupt segments follow different start/end adjustment branches; verify + they render without error on an integer index.""" + df = pd.DataFrame({'value': np.arange(20, dtype=float)}, index=pd.RangeIndex(20)) + segments = [ + {'direction': 'Noise', 'start': 3, 'end': 4}, + {'direction': 'Up', 'start': 5, 'end': 6, 'trend_class': 'abrupt'}, + ] + + fig = plot_pytrendy(df=df, value_col='value', segments_enhanced=segments, + index_type='integer', suppress_show=True) + + assert isinstance(fig, plt.Figure) + plt.close(fig) \ No newline at end of file