From 8db7b1ca7bbaff3a1361e8f1477d3b9548b13353 Mon Sep 17 00:00:00 2001 From: nicolasbisurgi Date: Fri, 3 Apr 2026 09:26:28 -0300 Subject: [PATCH 1/3] fix: make RestService tests config-aware instead of hardcoding defaults The wait_time_generator and parameter assertion tests were hardcoding expected values (0.1s initial delay, 1.0s max, etc.) that break when config.ini overrides those defaults. Now the generator tests explicitly set and restore polling attributes, and the default-parameter tests read expected values from config.ini. Fixes #1368, #1369, #1370, #1371, #1372, #1373, #1374, #1375, #1376, #1377, #1378, #1379 Co-Authored-By: Claude Opus 4.6 (1M context) --- Tests/RestService_test.py | 110 +++++++++++++++++++++++++++++--------- 1 file changed, 85 insertions(+), 25 deletions(-) diff --git a/Tests/RestService_test.py b/Tests/RestService_test.py index f3bccbfe..1e86f1d8 100644 --- a/Tests/RestService_test.py +++ b/Tests/RestService_test.py @@ -24,32 +24,71 @@ def test_is_connected(self): self.assertTrue(self.tm1._tm1_rest.is_connected()) def test_wait_time_generator_with_float_timeout(self): - # With default params (0.1s initial, 1.0s max, 2x factor): 0.1 -> 0.2 -> 0.4 -> 0.8 -> 1.0 -> 1.0... - expected = [0.1, 0.2, 0.4, 0.8, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0] - self.assertEqual(expected, list(self.tm1._tm1_rest.wait_time_generator(10.0))) - self.assertEqual(10.5, sum(self.tm1._tm1_rest.wait_time_generator(10.0))) + # Use fixed known values to test the generator logic deterministically + original_initial = self.tm1._tm1_rest._async_polling_initial_delay + original_max = self.tm1._tm1_rest._async_polling_max_delay + original_factor = self.tm1._tm1_rest._async_polling_backoff_factor + try: + self.tm1._tm1_rest._async_polling_initial_delay = 0.1 + self.tm1._tm1_rest._async_polling_max_delay = 1.0 + self.tm1._tm1_rest._async_polling_backoff_factor = 2.0 + # With 0.1s initial, 1.0s max, 2x factor: 0.1 -> 0.2 -> 0.4 -> 0.8 -> 1.0 -> 1.0... + expected = [0.1, 0.2, 0.4, 0.8, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0] + self.assertEqual(expected, list(self.tm1._tm1_rest.wait_time_generator(10.0))) + self.assertEqual(10.5, sum(self.tm1._tm1_rest.wait_time_generator(10.0))) + finally: + self.tm1._tm1_rest._async_polling_initial_delay = original_initial + self.tm1._tm1_rest._async_polling_max_delay = original_max + self.tm1._tm1_rest._async_polling_backoff_factor = original_factor def test_wait_time_generator_with_timeout(self): - # With default params (0.1s initial, 1.0s max, 2x factor): 0.1 -> 0.2 -> 0.4 -> 0.8 -> 1.0 -> 1.0... - expected = [0.1, 0.2, 0.4, 0.8, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0] - self.assertEqual(expected, list(self.tm1._tm1_rest.wait_time_generator(10))) - self.assertEqual(10.5, sum(self.tm1._tm1_rest.wait_time_generator(10))) + # Use fixed known values to test the generator logic deterministically + original_initial = self.tm1._tm1_rest._async_polling_initial_delay + original_max = self.tm1._tm1_rest._async_polling_max_delay + original_factor = self.tm1._tm1_rest._async_polling_backoff_factor + try: + self.tm1._tm1_rest._async_polling_initial_delay = 0.1 + self.tm1._tm1_rest._async_polling_max_delay = 1.0 + self.tm1._tm1_rest._async_polling_backoff_factor = 2.0 + # With 0.1s initial, 1.0s max, 2x factor: 0.1 -> 0.2 -> 0.4 -> 0.8 -> 1.0 -> 1.0... + expected = [0.1, 0.2, 0.4, 0.8, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0] + self.assertEqual(expected, list(self.tm1._tm1_rest.wait_time_generator(10))) + self.assertEqual(10.5, sum(self.tm1._tm1_rest.wait_time_generator(10))) + finally: + self.tm1._tm1_rest._async_polling_initial_delay = original_initial + self.tm1._tm1_rest._async_polling_max_delay = original_max + self.tm1._tm1_rest._async_polling_backoff_factor = original_factor def test_wait_time_generator_without_timeout(self): - # With default params (0.1s initial, 1.0s max, 2x factor): 0.1 -> 0.2 -> 0.4 -> 0.8 -> 1.0 -> 1.0... - generator = self.tm1._tm1_rest.wait_time_generator(None) - self.assertEqual(0.1, next(generator)) - self.assertEqual(0.2, next(generator)) - self.assertEqual(0.4, next(generator)) - self.assertEqual(0.8, next(generator)) - self.assertEqual(1.0, next(generator)) - self.assertEqual(1.0, next(generator)) + # Use fixed known values to test the generator logic deterministically + original_initial = self.tm1._tm1_rest._async_polling_initial_delay + original_max = self.tm1._tm1_rest._async_polling_max_delay + original_factor = self.tm1._tm1_rest._async_polling_backoff_factor + try: + self.tm1._tm1_rest._async_polling_initial_delay = 0.1 + self.tm1._tm1_rest._async_polling_max_delay = 1.0 + self.tm1._tm1_rest._async_polling_backoff_factor = 2.0 + generator = self.tm1._tm1_rest.wait_time_generator(None) + self.assertEqual(0.1, next(generator)) + self.assertEqual(0.2, next(generator)) + self.assertEqual(0.4, next(generator)) + self.assertEqual(0.8, next(generator)) + self.assertEqual(1.0, next(generator)) + self.assertEqual(1.0, next(generator)) + finally: + self.tm1._tm1_rest._async_polling_initial_delay = original_initial + self.tm1._tm1_rest._async_polling_max_delay = original_max + self.tm1._tm1_rest._async_polling_backoff_factor = original_factor def test_wait_time_generator_custom_max_delay(self): # Test with custom max_delay for long-running operations + original_initial = self.tm1._tm1_rest._async_polling_initial_delay original_max_delay = self.tm1._tm1_rest._async_polling_max_delay + original_factor = self.tm1._tm1_rest._async_polling_backoff_factor try: + self.tm1._tm1_rest._async_polling_initial_delay = 0.1 self.tm1._tm1_rest._async_polling_max_delay = 30.0 + self.tm1._tm1_rest._async_polling_backoff_factor = 2.0 # With 0.1s initial, 30s max, 2x factor: 0.1 -> 0.2 -> 0.4 -> 0.8 -> 1.6 -> 3.2 -> 6.4 -> 12.8 -> 25.6 -> 30.0... generator = self.tm1._tm1_rest.wait_time_generator(None) self.assertEqual(0.1, next(generator)) @@ -64,12 +103,18 @@ def test_wait_time_generator_custom_max_delay(self): self.assertEqual(30.0, next(generator)) self.assertEqual(30.0, next(generator)) finally: + self.tm1._tm1_rest._async_polling_initial_delay = original_initial self.tm1._tm1_rest._async_polling_max_delay = original_max_delay + self.tm1._tm1_rest._async_polling_backoff_factor = original_factor def test_wait_time_generator_custom_backoff_factor(self): # Test with custom backoff factor (3x instead of 2x) + original_initial = self.tm1._tm1_rest._async_polling_initial_delay + original_max = self.tm1._tm1_rest._async_polling_max_delay original_factor = self.tm1._tm1_rest._async_polling_backoff_factor try: + self.tm1._tm1_rest._async_polling_initial_delay = 0.1 + self.tm1._tm1_rest._async_polling_max_delay = 1.0 self.tm1._tm1_rest._async_polling_backoff_factor = 3.0 # With 0.1s initial, 1.0s max, 3x factor: 0.1 -> 0.3 -> 0.9 -> 1.0 -> 1.0... generator = self.tm1._tm1_rest.wait_time_generator(None) @@ -79,13 +124,19 @@ def test_wait_time_generator_custom_backoff_factor(self): self.assertEqual(1.0, next(generator)) self.assertEqual(1.0, next(generator)) finally: + self.tm1._tm1_rest._async_polling_initial_delay = original_initial + self.tm1._tm1_rest._async_polling_max_delay = original_max self.tm1._tm1_rest._async_polling_backoff_factor = original_factor def test_wait_time_generator_custom_initial_delay(self): # Test with custom initial delay original_initial = self.tm1._tm1_rest._async_polling_initial_delay + original_max = self.tm1._tm1_rest._async_polling_max_delay + original_factor = self.tm1._tm1_rest._async_polling_backoff_factor try: self.tm1._tm1_rest._async_polling_initial_delay = 0.5 + self.tm1._tm1_rest._async_polling_max_delay = 1.0 + self.tm1._tm1_rest._async_polling_backoff_factor = 2.0 # With 0.5s initial, 1.0s max, 2x factor: 0.5 -> 1.0 -> 1.0... generator = self.tm1._tm1_rest.wait_time_generator(None) self.assertEqual(0.5, next(generator)) @@ -93,19 +144,28 @@ def test_wait_time_generator_custom_initial_delay(self): self.assertEqual(1.0, next(generator)) finally: self.tm1._tm1_rest._async_polling_initial_delay = original_initial + self.tm1._tm1_rest._async_polling_max_delay = original_max + self.tm1._tm1_rest._async_polling_backoff_factor = original_factor def test_default_remote_disconnect_parameters(self): - # Verify default values for remote disconnect retry parameters - self.assertEqual(5, self.tm1._tm1_rest._remote_disconnect_max_retries) - self.assertEqual(1.0, self.tm1._tm1_rest._remote_disconnect_retry_delay) - self.assertEqual(30.0, self.tm1._tm1_rest._remote_disconnect_max_delay) - self.assertEqual(2.0, self.tm1._tm1_rest._remote_disconnect_backoff_factor) + # Verify values for remote disconnect retry parameters match config or defaults + expected_max_retries = int(self.config["tm1srv01"].get("remote_disconnect_max_retries", 5)) + expected_retry_delay = float(self.config["tm1srv01"].get("remote_disconnect_retry_delay", 1.0)) + expected_max_delay = float(self.config["tm1srv01"].get("remote_disconnect_max_delay", 30.0)) + expected_backoff_factor = float(self.config["tm1srv01"].get("remote_disconnect_backoff_factor", 2.0)) + self.assertEqual(expected_max_retries, self.tm1._tm1_rest._remote_disconnect_max_retries) + self.assertEqual(expected_retry_delay, self.tm1._tm1_rest._remote_disconnect_retry_delay) + self.assertEqual(expected_max_delay, self.tm1._tm1_rest._remote_disconnect_max_delay) + self.assertEqual(expected_backoff_factor, self.tm1._tm1_rest._remote_disconnect_backoff_factor) def test_default_async_polling_parameters(self): - # Verify default values for async polling parameters - self.assertEqual(0.1, self.tm1._tm1_rest._async_polling_initial_delay) - self.assertEqual(1.0, self.tm1._tm1_rest._async_polling_max_delay) - self.assertEqual(2.0, self.tm1._tm1_rest._async_polling_backoff_factor) + # Verify values for async polling parameters match config or defaults + expected_initial_delay = float(self.config["tm1srv01"].get("async_polling_initial_delay", 0.1)) + expected_max_delay = float(self.config["tm1srv01"].get("async_polling_max_delay", 1.0)) + expected_backoff_factor = float(self.config["tm1srv01"].get("async_polling_backoff_factor", 2.0)) + self.assertEqual(expected_initial_delay, self.tm1._tm1_rest._async_polling_initial_delay) + self.assertEqual(expected_max_delay, self.tm1._tm1_rest._async_polling_max_delay) + self.assertEqual(expected_backoff_factor, self.tm1._tm1_rest._async_polling_backoff_factor) def test_build_response_from_async_response_ok(self): response_content = ( From df3320ac8284f53816067ccab154f08c2b9f993d Mon Sep 17 00:00:00 2001 From: nicolasbisurgi Date: Fri, 3 Apr 2026 09:44:07 -0300 Subject: [PATCH 2/3] fix: rename duplicate test method to pass ruff F811 check The second test_delete_edges_use_ti_skip_invalid_edges_true (with use_ti=True) was shadowing the first one (default path). Renamed to test_delete_edges_use_ti_and_skip_invalid_edges_true. Co-Authored-By: Claude Opus 4.6 (1M context) --- Tests/ElementService_test.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Tests/ElementService_test.py b/Tests/ElementService_test.py index bf2a61fd..7cb708a6 100644 --- a/Tests/ElementService_test.py +++ b/Tests/ElementService_test.py @@ -5,14 +5,18 @@ from mdxpy import MdxBuilder -from TM1py.Exceptions import TM1pyException, TM1pyRestException, TM1pyWritePartialFailureException -from TM1py.Objects import Dimension, Element, ElementAttribute, Hierarchy -from TM1py.Services import TM1Service from Tests.Utils import ( generate_test_uuid, skip_if_no_pandas, skip_if_version_lower_than, ) +from TM1py.Exceptions import ( + TM1pyException, + TM1pyRestException, + TM1pyWritePartialFailureException, +) +from TM1py.Objects import Dimension, Element, ElementAttribute, Hierarchy +from TM1py.Services import TM1Service class TestElementService(unittest.TestCase): @@ -1349,7 +1353,7 @@ def test_delete_edges_use_blob_skip_invalid_edges_false(self): ) @skip_if_version_lower_than(version="11.4") - def test_delete_edges_use_ti_skip_invalid_edges_true(self): + def test_delete_edges_use_ti_and_skip_invalid_edges_true(self): self.tm1.elements.delete_edges( dimension_name=self.dimension_name, hierarchy_name=self.hierarchy_name, From 9956d09be8d7545b5134b0db4aa548c751e3c786 Mon Sep 17 00:00:00 2001 From: nicolasbisurgi Date: Fri, 3 Apr 2026 09:51:25 -0300 Subject: [PATCH 3/3] fix: remove extraneous f-string prefix on ENDIF (ruff F541) Co-Authored-By: Claude Opus 4.6 (1M context) --- TM1py/Services/ElementService.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TM1py/Services/ElementService.py b/TM1py/Services/ElementService.py index 815d36b8..701cc47a 100644 --- a/TM1py/Services/ElementService.py +++ b/TM1py/Services/ElementService.py @@ -196,7 +196,7 @@ def escape_single_quote(text): [ f"IF(ElementIsParent('{dimension_name}','{hierarchy_name}','{parent}','{child}')=1);", f"HierarchyElementComponentDelete('{dimension_name}','{hierarchy_name}','{parent}','{child}');", - f"ENDIF;", + "ENDIF;", ] )