diff --git a/benchmarks/bench_uko_persistence.py b/benchmarks/bench_uko_persistence.py index 775b4e14c..b974fdc5c 100644 --- a/benchmarks/bench_uko_persistence.py +++ b/benchmarks/bench_uko_persistence.py @@ -27,10 +27,10 @@ class UKOPersistenceBenchmarks: """Set up benchmark fixtures.""" self.triple_count = triple_count self.temp_dir = tempfile.mkdtemp() - + # Create mock graph backend self.graph_backend = MagicMock(spec=GraphIndexBackend) - + # Create sample triples self.triples = [ { @@ -40,7 +40,7 @@ class UKOPersistenceBenchmarks: } for i in range(triple_count) ] - + # Set up graph backend to return triples self.graph_backend.query.return_value = [ { @@ -54,6 +54,7 @@ class UKOPersistenceBenchmarks: def teardown(self, triple_count: int) -> None: """Clean up after benchmark.""" import shutil + shutil.rmtree(self.temp_dir, ignore_errors=True) def time_save_with_in_memory_backend(self, triple_count: int) -> None: @@ -80,7 +81,7 @@ class UKOPersistenceBenchmarks: """Benchmark restore() with InMemoryPersistenceBackend.""" persistence_backend = InMemoryPersistenceBackend() persistence_backend.save("local/benchmark", self.triples) - + service = UKOGraphPersistence( graph_backend=self.graph_backend, project="local/benchmark", @@ -92,7 +93,7 @@ class UKOPersistenceBenchmarks: """Benchmark restore() with JSONFilePersistenceBackend.""" persistence_backend = JSONFilePersistenceBackend(self.temp_dir) persistence_backend.save("local/benchmark", self.triples) - + service = UKOGraphPersistence( graph_backend=self.graph_backend, project="local/benchmark", diff --git a/features/steps/uko_persistence_steps.py b/features/steps/uko_persistence_steps.py index 116bfe86c..e84e0a759 100644 --- a/features/steps/uko_persistence_steps.py +++ b/features/steps/uko_persistence_steps.py @@ -79,20 +79,15 @@ def _capture_uko_logs() -> Generator[list[dict[str, Any]]]: # ================================================================= -@given("uko a JSON file persistence backend in a temp directory") -def step_json_file_persistence_backend(context: Any) -> None: - """Create a JSON file persistence backend in a temp directory.""" - temp_dir = tempfile.mkdtemp() - context.uko_temp_dir = temp_dir - context.uko_json_backend = JSONFilePersistenceBackend(temp_dir) - - @given("uko a JSON file persistence backend in a non-existent temp directory") def step_json_file_persistence_backend_nonexistent(context: Any) -> None: """Create a JSON file persistence backend in a non-existent temp directory.""" temp_dir = Path(tempfile.gettempdir()) / "uko_test_nonexistent" / "subdir" context.uko_temp_dir = str(temp_dir) context.uko_json_backend = JSONFilePersistenceBackend(temp_dir) + # Set uko_test_triples so parameterized save steps work without a prior + # "uko an in-memory persistence backend" step. + context.uko_test_triples = create_sample_triples() @given("uko the JSON file contains corrupted JSON data") @@ -104,7 +99,7 @@ def step_json_file_corrupted(context: Any) -> None: path.write_text("{invalid json content", encoding="utf-8") -@given("uko the JSON file contains valid JSON but missing \"triples\" key") +@given('uko the JSON file contains valid JSON but missing "triples" key') def step_json_file_missing_triples_key(context: Any) -> None: """Create a JSON file with valid JSON but missing triples key.""" backend: JSONFilePersistenceBackend = context.uko_json_backend @@ -128,7 +123,7 @@ def step_json_file_unreadable(context: Any) -> None: context.uko_unreadable_file = path -@when("uko I load from the JSON file backend for project \"local/test\"") +@when('uko I load from the JSON file backend for project "local/test"') def step_load_from_json_backend(context: Any) -> None: """Load from JSON file backend, capturing any log output.""" backend: JSONFilePersistenceBackend = context.uko_json_backend @@ -140,7 +135,9 @@ def step_load_from_json_backend(context: Any) -> None: @then("uko the result should be an empty list") def step_result_is_empty_list(context: Any) -> None: """Verify result is an empty list.""" - assert context.uko_load_result == [], f"Expected empty list, got {context.uko_load_result}" + assert context.uko_load_result == [], ( + f"Expected empty list, got {context.uko_load_result}" + ) @then("uko a warning should be logged for load failure") @@ -148,7 +145,8 @@ def step_warning_logged_for_load_failure(context: Any) -> None: """Verify a warning was logged for load failure.""" logs: list[dict[str, Any]] = context.uko_captured_logs warning_events = [ - e for e in logs + e + for e in logs if e.get("log_level") == "warning" and "load_failed" in e.get("event", "") ] assert warning_events, ( @@ -190,18 +188,13 @@ def step_call_save_on_persistence_service(context: Any) -> None: context.uko_captured_logs = entries -@then("uko the save result should be 0") -def step_save_result_is_zero(context: Any) -> None: - """Verify save result is 0.""" - assert context.uko_save_result == 0, f"Expected 0, got {context.uko_save_result}" - - @then("uko a warning should be logged for query failure") def step_warning_logged_for_query_failure(context: Any) -> None: """Verify a warning was logged for query failure.""" logs: list[dict[str, Any]] = context.uko_captured_logs warning_events = [ - e for e in logs + e + for e in logs if e.get("log_level") == "warning" and "query_failed" in e.get("event", "") ] assert warning_events, ( @@ -214,7 +207,7 @@ def step_warning_logged_for_query_failure(context: Any) -> None: # ================================================================= -@given("uko a persistence backend with triples missing the \"subject\" key") +@given('uko a persistence backend with triples missing the "subject" key') def step_persistence_backend_missing_subject(context: Any) -> None: """Create a persistence backend with triples missing subject.""" backend = InMemoryPersistenceBackend() @@ -230,7 +223,7 @@ def step_persistence_backend_missing_subject(context: Any) -> None: context.uko_persistence_backend = backend -@given("uko a persistence backend with triples missing the \"predicate\" key") +@given('uko a persistence backend with triples missing the "predicate" key') def step_persistence_backend_missing_predicate(context: Any) -> None: """Create a persistence backend with triples missing predicate.""" backend = InMemoryPersistenceBackend() @@ -246,7 +239,7 @@ def step_persistence_backend_missing_predicate(context: Any) -> None: context.uko_persistence_backend = backend -@given("uko a persistence backend with triples missing the \"object\" key") +@given('uko a persistence backend with triples missing the "object" key') def step_persistence_backend_missing_object(context: Any) -> None: """Create a persistence backend with triples missing object.""" backend = InMemoryPersistenceBackend() @@ -334,12 +327,6 @@ def step_call_restore_on_persistence_service(context: Any) -> None: context.uko_captured_logs = entries -@then("uko the restore result should be 0") -def step_restore_result_is_zero(context: Any) -> None: - """Verify restore result is 0.""" - assert context.uko_restore_result == 0, f"Expected 0, got {context.uko_restore_result}" - - @then("uko no triples should be added to the graph backend") def step_no_triples_added(context: Any) -> None: """Verify no triples were added to the graph backend.""" @@ -373,7 +360,8 @@ def step_warning_logged_for_triple_failure(context: Any) -> None: """Verify a warning was logged for triple failure.""" logs: list[dict[str, Any]] = context.uko_captured_logs warning_events = [ - e for e in logs + e + for e in logs if e.get("log_level") == "warning" and "triple_failed" in e.get("event", "") ] assert warning_events, ( @@ -427,7 +415,9 @@ def step_persistence_service_with_mixed_backend(context: Any) -> None: @then("uko the restore result should be greater than 0") def step_restore_result_greater_than_zero(context: Any) -> None: """Verify restore result is greater than 0.""" - assert context.uko_restore_result > 0, f"Expected > 0, got {context.uko_restore_result}" + assert context.uko_restore_result > 0, ( + f"Expected > 0, got {context.uko_restore_result}" + ) @then("uko warnings should be logged for failed triples") @@ -435,7 +425,8 @@ def step_warnings_logged_for_failed_triples(context: Any) -> None: """Verify warnings were logged for failed triples.""" logs: list[dict[str, Any]] = context.uko_captured_logs warning_events = [ - e for e in logs + e + for e in logs if e.get("log_level") == "warning" and "triple_failed" in e.get("event", "") ] assert warning_events, ( @@ -448,27 +439,16 @@ def step_warnings_logged_for_failed_triples(context: Any) -> None: # ================================================================= -@given("uko an in-memory persistence backend") -def step_in_memory_persistence_backend(context: Any) -> None: - """Create an in-memory persistence backend.""" - context.uko_persistence_backend = InMemoryPersistenceBackend() - - -@given("uko I save triples to the persistence backend for project \"local/test\"") -def step_given_save_triples_test(context: Any) -> None: - """Save triples for project local/test (Given variant).""" - backend: InMemoryPersistenceBackend = context.uko_persistence_backend - backend.save("local/test", create_sample_triples()) - - -@when("uko I clear the persistence backend for project \"local/test\"") +@when('uko I clear the persistence backend for project "local/test"') def step_clear_persistence_backend(context: Any) -> None: """Clear the persistence backend for a project.""" backend: InMemoryPersistenceBackend = context.uko_persistence_backend backend.clear("local/test") -@then("uko loading from the persistence backend for project \"local/test\" should return empty list") +@then( + 'uko loading from the persistence backend for project "local/test" should return empty list' +) def step_load_after_clear_returns_empty(context: Any) -> None: """Verify loading after clear returns empty list.""" backend: InMemoryPersistenceBackend = context.uko_persistence_backend @@ -476,42 +456,16 @@ def step_load_after_clear_returns_empty(context: Any) -> None: assert result == [], f"Expected empty list, got {result}" -@given("uko I save triples to the persistence backend for project \"local/test1\"") -def step_given_save_triples_test1(context: Any) -> None: - """Save triples for project test1 (Given variant).""" - backend: InMemoryPersistenceBackend = context.uko_persistence_backend - backend.save("local/test1", create_sample_triples()) - - -@given("uko I save triples to the persistence backend for project \"local/test2\"") -def step_given_save_triples_test2(context: Any) -> None: - """Save triples for project test2 (Given variant).""" - backend: InMemoryPersistenceBackend = context.uko_persistence_backend - backend.save("local/test2", create_sample_triples()) - - -@when("uko I save triples to the persistence backend for project \"local/test1\"") -def step_save_triples_test1(context: Any) -> None: - """Save triples for project test1.""" - backend: InMemoryPersistenceBackend = context.uko_persistence_backend - backend.save("local/test1", create_sample_triples()) - - -@when("uko I save triples to the persistence backend for project \"local/test2\"") -def step_save_triples_test2(context: Any) -> None: - """Save triples for project test2.""" - backend: InMemoryPersistenceBackend = context.uko_persistence_backend - backend.save("local/test2", create_sample_triples()) - - -@when("uko I clear the persistence backend for project \"local/test1\"") +@when('uko I clear the persistence backend for project "local/test1"') def step_clear_persistence_backend_test1(context: Any) -> None: """Clear the persistence backend for project test1.""" backend: InMemoryPersistenceBackend = context.uko_persistence_backend backend.clear("local/test1") -@then("uko loading from the persistence backend for project \"local/test1\" should return empty list") +@then( + 'uko loading from the persistence backend for project "local/test1" should return empty list' +) def step_load_test1_returns_empty(context: Any) -> None: """Verify loading test1 after clear returns empty list.""" backend: InMemoryPersistenceBackend = context.uko_persistence_backend @@ -519,7 +473,9 @@ def step_load_test1_returns_empty(context: Any) -> None: assert result == [], f"Expected empty list, got {result}" -@then("uko loading from the persistence backend for project \"local/test2\" should return the saved triples") +@then( + 'uko loading from the persistence backend for project "local/test2" should return the saved triples' +) def step_load_test2_returns_saved(context: Any) -> None: """Verify loading test2 returns the saved triples.""" backend: InMemoryPersistenceBackend = context.uko_persistence_backend @@ -527,7 +483,7 @@ def step_load_test2_returns_saved(context: Any) -> None: assert len(result) == 2, f"Expected 2 triples, got {len(result)}" -@when("uko I clear the persistence backend for project \"local/nonexistent\"") +@when('uko I clear the persistence backend for project "local/nonexistent"') def step_clear_nonexistent_project(context: Any) -> None: """Clear a non-existent project.""" backend: InMemoryPersistenceBackend = context.uko_persistence_backend @@ -541,7 +497,9 @@ def step_clear_nonexistent_project(context: Any) -> None: @then("uko no error should be raised") def step_no_error_raised(context: Any) -> None: """Verify no error was raised.""" - assert context.uko_clear_error is None, f"Expected no error, got {context.uko_clear_error}" + assert context.uko_clear_error is None, ( + f"Expected no error, got {context.uko_clear_error}" + ) # ================================================================= @@ -549,15 +507,7 @@ def step_no_error_raised(context: Any) -> None: # ================================================================= -@given("uko a graph backend with no triples") -def step_graph_backend_no_triples(context: Any) -> None: - """Create a mock graph backend with no triples.""" - backend = create_mock_graph_backend() - backend.query.return_value = [] - context.uko_graph_backend = backend - - -@given("uko a UKOGraphPersistence service with project \"local/my-app\"") +@given('uko a UKOGraphPersistence service with project "local/my-app"') def step_persistence_service_with_project(context: Any) -> None: """Create a UKOGraphPersistence service with a specific project.""" graph_backend = context.uko_graph_backend @@ -576,12 +526,12 @@ def step_access_project_property(context: Any) -> None: context.uko_project_value = service.project -@then("uko the project property should return \"local/my-app\"") +@then('uko the project property should return "local/my-app"') def step_project_property_returns_value(context: Any) -> None: """Verify the project property returns the correct value.""" - assert ( - context.uko_project_value == "local/my-app" - ), f"Expected 'local/my-app', got {context.uko_project_value}" + assert context.uko_project_value == "local/my-app", ( + f"Expected 'local/my-app', got {context.uko_project_value}" + ) @then("uko attempting to set the project property should raise AttributeError") @@ -600,9 +550,9 @@ def step_project_property_is_readonly(context: Any) -> None: # ================================================================= -@given("uko a graph backend with triples for resource \"01HQ8ZDRX50000000000000020\"") +@given("uko a mock graph backend with two sample triples") def step_graph_backend_with_triples_020(context: Any) -> None: - """Create a graph backend with triples.""" + """Create a mock graph backend with two sample triples for lifecycle tests.""" backend = create_mock_graph_backend() backend.query.return_value = [ { @@ -631,14 +581,16 @@ def step_persistence_service_with_json_backend(context: Any) -> None: ) -@when("uko I save the graph state") +@when("uko I save the graph state via the persistence service") def step_save_graph_state(context: Any) -> None: - """Save the graph state.""" + """Save the graph state via the pre-configured persistence service.""" service: UKOGraphPersistence = context.uko_persistence_service context.uko_save_result = service.save() -@when("uko I restore the graph state into a fresh backend with the same JSON file backend") +@when( + "uko I restore the graph state into a fresh backend with the same JSON file backend" +) def step_restore_graph_state_with_json_backend(context: Any) -> None: """Restore the graph state into a fresh backend.""" fresh_backend = create_mock_graph_backend() @@ -656,16 +608,9 @@ def step_restore_graph_state_with_json_backend(context: Any) -> None: def step_restored_backend_has_same_triples(context: Any) -> None: """Verify the restored backend has the same triples.""" fresh_backend = context.uko_fresh_backend - assert ( - fresh_backend.add_triple.call_count == 2 - ), f"Expected 2 add_triple calls, got {fresh_backend.add_triple.call_count}" - - -@when("uko I save triples to the JSON file backend for project \"local/test\"") -def step_save_triples_to_json_backend(context: Any) -> None: - """Save triples to the JSON file backend.""" - backend: JSONFilePersistenceBackend = context.uko_json_backend - backend.save("local/test", create_sample_triples()) + assert fresh_backend.add_triple.call_count == 2, ( + f"Expected 2 add_triple calls, got {fresh_backend.add_triple.call_count}" + ) @then("uko the base directory should be created") @@ -688,7 +633,9 @@ def step_json_file_exists(context: Any) -> None: # ================================================================= -@then("uko creating UKOGraphPersistence with whitespace-only project should raise ValueError") +@then( + "uko creating UKOGraphPersistence with whitespace-only project should raise ValueError" +) def step_persistence_service_whitespace_project(context: Any) -> None: """Verify creating service with whitespace-only project raises ValueError.""" graph_backend = context.uko_graph_backend @@ -703,7 +650,9 @@ def step_persistence_service_whitespace_project(context: Any) -> None: pass -@then("uko saving to JSON file backend with whitespace-only project should raise ValueError") +@then( + "uko saving to JSON file backend with whitespace-only project should raise ValueError" +) def step_save_json_whitespace_project(context: Any) -> None: """Verify saving with whitespace-only project raises ValueError.""" backend: JSONFilePersistenceBackend = context.uko_json_backend @@ -714,7 +663,9 @@ def step_save_json_whitespace_project(context: Any) -> None: pass -@then("uko loading from JSON file backend with whitespace-only project should raise ValueError") +@then( + "uko loading from JSON file backend with whitespace-only project should raise ValueError" +) def step_load_json_whitespace_project(context: Any) -> None: """Verify loading with whitespace-only project raises ValueError.""" backend: JSONFilePersistenceBackend = context.uko_json_backend @@ -725,7 +676,9 @@ def step_load_json_whitespace_project(context: Any) -> None: pass -@then("uko saving to persistence backend with whitespace-only project should raise ValueError") +@then( + "uko saving to persistence backend with whitespace-only project should raise ValueError" +) def step_save_memory_whitespace_project(context: Any) -> None: """Verify saving with whitespace-only project raises ValueError.""" backend: InMemoryPersistenceBackend = context.uko_persistence_backend @@ -736,7 +689,9 @@ def step_save_memory_whitespace_project(context: Any) -> None: pass -@then("uko loading from persistence backend with whitespace-only project should raise ValueError") +@then( + "uko loading from persistence backend with whitespace-only project should raise ValueError" +) def step_load_memory_whitespace_project(context: Any) -> None: """Verify loading with whitespace-only project raises ValueError.""" backend: InMemoryPersistenceBackend = context.uko_persistence_backend @@ -804,9 +759,8 @@ def step_info_log_no_data(context: Any) -> None: """Verify an info log was recorded for no data.""" logs: list[dict[str, Any]] = context.uko_captured_logs info_events = [ - e for e in logs + e + for e in logs if e.get("log_level") == "info" and "no_data" in e.get("event", "") ] - assert info_events, ( - f"Expected an info log with 'no_data' event, got: {logs}" - ) + assert info_events, f"Expected an info log with 'no_data' event, got: {logs}" diff --git a/features/uko_persistence.feature b/features/uko_persistence.feature index 61be7eaa0..2906f5c5e 100644 --- a/features/uko_persistence.feature +++ b/features/uko_persistence.feature @@ -148,10 +148,10 @@ Feature: UKO Graph Persistence — Full Coverage for Error Paths and Edge Cases # ================================================================= Scenario: Full save/restore lifecycle with JSONFilePersistenceBackend - Given uko a graph backend with triples for resource "01HQ8ZDRX50000000000000020" + Given uko a mock graph backend with two sample triples And uko a JSON file persistence backend in a temp directory And uko a UKOGraphPersistence service with the JSON file backend - When uko I save the graph state + When uko I save the graph state via the persistence service And uko I restore the graph state into a fresh backend with the same JSON file backend Then uko the restored backend should have the same triples diff --git a/robot/helper_uko_persistence.py b/robot/helper_uko_persistence.py index 1a7d70162..ac00bb465 100644 --- a/robot/helper_uko_persistence.py +++ b/robot/helper_uko_persistence.py @@ -74,9 +74,7 @@ def main() -> int: persistence_backend=persistence_backend, ) saved_count = service.save() - assert saved_count == 2, ( - f"Expected 2 triples saved, got {saved_count}" - ) + assert saved_count == 2, f"Expected 2 triples saved, got {saved_count}" json_path = persistence_backend._path_for("local/test") assert json_path.exists(), f"Expected JSON file at {json_path}" @@ -121,9 +119,7 @@ def main() -> int: ], ) - assert temp_dir.exists(), ( - f"Expected directory to be created: {temp_dir}" - ) + assert temp_dir.exists(), f"Expected directory to be created: {temp_dir}" json_path = persistence_backend._path_for("local/test") assert json_path.exists(), f"Expected JSON file at {json_path}" @@ -145,9 +141,7 @@ def main() -> int: json_path.write_text("{invalid json content", encoding="utf-8") result = persistence_backend.load("local/test") - assert result == [], ( - f"Expected empty list for corrupted file, got {result}" - ) + assert result == [], f"Expected empty list for corrupted file, got {result}" print("uko-persistence-corrupted-file-ok") return 0 @@ -223,9 +217,7 @@ def main() -> int: persistence_backend=backend, ) restored = service.restore() - assert restored == 0, ( - f"Expected 0 triples restored, got {restored}" - ) + assert restored == 0, f"Expected 0 triples restored, got {restored}" graph_backend.add_triple.assert_not_called() print("uko-persistence-restore-skips-incomplete-ok")