From 0443637fe16d55bb2c8549004325da56399e9c67 Mon Sep 17 00:00:00 2001 From: Sai Sanjay <97831658+sanjay7178@users.noreply.github.com> Date: Tue, 13 Jan 2026 14:47:12 +0000 Subject: [PATCH] Add unit tests to `pvc_scenario_plugin.py` (#1014) * Add PVC outage scenario plugin to manage PVC annotations during outages Signed-off-by: sanjay7178 * Remove PvcOutageScenarioPlugin as it is no longer needed; refactor PvcScenarioPlugin to include rollback functionality for temporary file cleanup during PVC scenarios. Signed-off-by: sanjay7178 * Refactor rollback_data handling in PvcScenarioPlugin to use str() instead of json.dumps() for resource_identifier. Signed-off-by: sanjay7178 * Import json module in PvcScenarioPlugin for decoding rollback data from resource_identifier. Signed-off-by: sanjay7178 * feat: Encode rollback data in base64 format for resource_identifier in PvcScenarioPlugin to enhance data handling and security. Signed-off-by: Sai Sanjay * feat: refactor: Update logging level from debug to info for temp file operations in PvcScenarioPlugin to improve visibility of command execution. Signed-off-by: Sai Sanjay * Add unit tests for PvcScenarioPlugin methods and enhance test coverage Signed-off-by: Sai Sanjay * Add missed lines test cov Signed-off-by: Sai Sanjay * Refactor tests in test_pvc_scenario_plugin.py to use unittest framework and enhance test coverage for to_kbytes method Signed-off-by: Sai Sanjay * Enhance rollback_temp_file test to verify logging of errors for invalid data Signed-off-by: Sai Sanjay * Refactor tests in TestPvcScenarioPluginRun to clarify pod_name behavior and enhance logging verification in rollback_temp_file tests Signed-off-by: Sai Sanjay * Refactored imports Signed-off-by: Sai Sanjay * Refactor assertions in test cases to use assertEqual for consistency Signed-off-by: Sai Sanjay --------- Signed-off-by: sanjay7178 Signed-off-by: Sai Sanjay Co-authored-by: Paige Patton <64206430+paigerube14@users.noreply.github.com> --- tests/test_pvc_scenario_plugin.py | 795 +++++++++++++++++++++++++++++- 1 file changed, 793 insertions(+), 2 deletions(-) diff --git a/tests/test_pvc_scenario_plugin.py b/tests/test_pvc_scenario_plugin.py index 053cc4b5..b030feb1 100644 --- a/tests/test_pvc_scenario_plugin.py +++ b/tests/test_pvc_scenario_plugin.py @@ -9,16 +9,22 @@ Usage: Assisted By: Claude Code """ +import base64 +import json +import os +import tempfile import unittest -from unittest.mock import MagicMock - +from unittest.mock import MagicMock, patch +import yaml from krkn_lib.k8s import KrknKubernetes from krkn_lib.telemetry.ocp import KrknTelemetryOpenshift from krkn.scenario_plugins.pvc.pvc_scenario_plugin import PvcScenarioPlugin +from krkn.rollback.config import RollbackContent class TestPvcScenarioPlugin(unittest.TestCase): + """Unit tests for PvcScenarioPlugin class""" def setUp(self): """ @@ -36,5 +42,790 @@ class TestPvcScenarioPlugin(unittest.TestCase): self.assertEqual(len(result), 1) +class TestToKbytes(unittest.TestCase): + """Tests for the to_kbytes method""" + + def setUp(self): + """Set up test fixtures""" + self.plugin = PvcScenarioPlugin() + + def test_to_kbytes_1ki(self): + """Test to_kbytes with 1Ki""" + self.assertEqual(self.plugin.to_kbytes("1Ki"), 1) + + def test_to_kbytes_2ki(self): + """Test to_kbytes with 2Ki""" + self.assertEqual(self.plugin.to_kbytes("2Ki"), 2) + + def test_to_kbytes_1mi(self): + """Test to_kbytes with 1Mi""" + self.assertEqual(self.plugin.to_kbytes("1Mi"), 1024) + + def test_to_kbytes_2mi(self): + """Test to_kbytes with 2Mi""" + self.assertEqual(self.plugin.to_kbytes("2Mi"), 2 * 1024) + + def test_to_kbytes_1gi(self): + """Test to_kbytes with 1Gi""" + self.assertEqual(self.plugin.to_kbytes("1Gi"), 1024 * 1024) + + def test_to_kbytes_5gi(self): + """Test to_kbytes with 5Gi""" + self.assertEqual(self.plugin.to_kbytes("5Gi"), 5 * 1024 * 1024) + + def test_to_kbytes_1ti(self): + """Test to_kbytes with 1Ti""" + self.assertEqual(self.plugin.to_kbytes("1Ti"), 1024 * 1024 * 1024) + + def test_to_kbytes_invalid_missing_i(self): + """Test to_kbytes raises RuntimeError for 1K (missing i)""" + with self.assertRaises(RuntimeError): + self.plugin.to_kbytes("1K") + + def test_to_kbytes_invalid_format_mb(self): + """Test to_kbytes raises RuntimeError for 1MB""" + with self.assertRaises(RuntimeError): + self.plugin.to_kbytes("1MB") + + def test_to_kbytes_invalid_extra_char(self): + """Test to_kbytes raises RuntimeError for 1Gib""" + with self.assertRaises(RuntimeError): + self.plugin.to_kbytes("1Gib") + + def test_to_kbytes_invalid_non_numeric(self): + """Test to_kbytes raises RuntimeError for abc""" + with self.assertRaises(RuntimeError): + self.plugin.to_kbytes("abc") + + def test_to_kbytes_invalid_missing_unit(self): + """Test to_kbytes raises RuntimeError for 1024""" + with self.assertRaises(RuntimeError): + self.plugin.to_kbytes("1024") + + def test_to_kbytes_invalid_empty(self): + """Test to_kbytes raises RuntimeError for empty string""" + with self.assertRaises(RuntimeError): + self.plugin.to_kbytes("") + + def test_to_kbytes_invalid_unsupported_unit(self): + """Test to_kbytes raises RuntimeError for 1Pi (unsupported)""" + with self.assertRaises(RuntimeError): + self.plugin.to_kbytes("1Pi") + + +class TestRemoveTempFile(unittest.TestCase): + """Tests for the remove_temp_file method""" + + def setUp(self): + """Set up test fixtures""" + self.plugin = PvcScenarioPlugin() + + def test_remove_temp_file_success(self): + """Test successful removal of temp file""" + mock_kubecli = MagicMock(spec=KrknKubernetes) + # Simulate file not present in ls output after removal + mock_kubecli.exec_cmd_in_pod.side_effect = [ + "", # rm -f command output + "total 0\ndrwxr-xr-x 2 root root 40 Jan 1 00:00 .", # ls -lh output without kraken.tmp + ] + + # Should not raise any exception + self.plugin.remove_temp_file( + file_name="kraken.tmp", + full_path="/mnt/data/kraken.tmp", + pod_name="test-pod", + namespace="test-ns", + container_name="test-container", + mount_path="/mnt/data", + file_size_kb=1024, + kubecli=mock_kubecli, + ) + + # Verify exec_cmd_in_pod was called twice (rm and ls) + self.assertEqual(mock_kubecli.exec_cmd_in_pod.call_count, 2) + + def test_remove_temp_file_failure(self): + """Test removal failure when file still exists""" + mock_kubecli = MagicMock(spec=KrknKubernetes) + # Simulate file still present in ls output after removal attempt + mock_kubecli.exec_cmd_in_pod.side_effect = [ + "", # rm -f command output + "total 1024\n-rw-r--r-- 1 root root 1M Jan 1 00:00 kraken.tmp", # ls -lh output with kraken.tmp + ] + + with self.assertRaises(RuntimeError): + self.plugin.remove_temp_file( + file_name="kraken.tmp", + full_path="/mnt/data/kraken.tmp", + pod_name="test-pod", + namespace="test-ns", + container_name="test-container", + mount_path="/mnt/data", + file_size_kb=1024, + kubecli=mock_kubecli, + ) + + +class TestRollbackTempFile(unittest.TestCase): + """Tests for the rollback_temp_file static method""" + + def test_rollback_temp_file_success(self): + """Test successful rollback removes temp file""" + # Create mock telemetry + mock_telemetry = MagicMock(spec=KrknTelemetryOpenshift) + mock_kubecli = MagicMock() + mock_telemetry.get_lib_kubernetes.return_value = mock_kubecli + + # Simulate successful file removal + mock_kubecli.exec_cmd_in_pod.side_effect = [ + "", # rm -f command output + "total 0\ndrwxr-xr-x 2 root root 40 Jan 1 00:00 .", # ls -lh output without file + ] + + # Create rollback data + rollback_data = { + "pod_name": "test-pod", + "container_name": "test-container", + "full_path": "/mnt/data/kraken.tmp", + "file_name": "kraken.tmp", + "mount_path": "/mnt/data", + } + encoded_data = base64.b64encode( + json.dumps(rollback_data).encode("utf-8") + ).decode("utf-8") + + rollback_content = RollbackContent( + namespace="test-ns", + resource_identifier=encoded_data, + ) + + # Should not raise any exception + PvcScenarioPlugin.rollback_temp_file(rollback_content, mock_telemetry) + + # Verify exec_cmd_in_pod was called + self.assertEqual(mock_kubecli.exec_cmd_in_pod.call_count, 2) + + @patch("krkn.scenario_plugins.pvc.pvc_scenario_plugin.logging") + def test_rollback_temp_file_invalid_data(self, mock_logging): + """Test rollback handles invalid encoded data gracefully and logs error""" + mock_telemetry = MagicMock(spec=KrknTelemetryOpenshift) + + rollback_content = RollbackContent( + namespace="test-ns", + resource_identifier="invalid-base64-data!!!", + ) + + # Should not raise exception, just log the error + PvcScenarioPlugin.rollback_temp_file(rollback_content, mock_telemetry) + + # Verify error was logged to inform users of rollback failure + mock_logging.error.assert_called_once() + error_message = mock_logging.error.call_args[0][0] + self.assertIn("Failed to rollback PVC scenario temp file", error_message) + + +class TestPvcScenarioPluginRun(unittest.TestCase): + """Tests for the run method of PvcScenarioPlugin""" + + def setUp(self): + """Set up test fixtures""" + self.plugin = PvcScenarioPlugin() + + def create_scenario_file(self, config: dict, temp_dir: str) -> str: + """Helper to create a temporary scenario YAML file in the given directory""" + path = os.path.join(temp_dir, "scenario.yaml") + with open(path, "w") as f: + yaml.dump(config, f) + return path + + def test_run_missing_namespace(self): + """Test run returns 1 when namespace is missing""" + with tempfile.TemporaryDirectory() as temp_dir: + scenario_config = { + "pvc_scenario": { + "pvc_name": "test-pvc", + # namespace is missing + } + } + scenario_path = self.create_scenario_file(scenario_config, temp_dir) + + mock_telemetry = MagicMock(spec=KrknTelemetryOpenshift) + mock_scenario_telemetry = MagicMock() + + result = self.plugin.run( + run_uuid="test-uuid", + scenario=scenario_path, + krkn_config={}, + lib_telemetry=mock_telemetry, + scenario_telemetry=mock_scenario_telemetry, + ) + + assert result == 1 + + def test_run_missing_pvc_and_pod_name(self): + """Test run returns 1 when both pvc_name and pod_name are missing""" + with tempfile.TemporaryDirectory() as temp_dir: + scenario_config = { + "pvc_scenario": { + "namespace": "test-ns", + # pvc_name and pod_name are missing + } + } + scenario_path = self.create_scenario_file(scenario_config, temp_dir) + + mock_telemetry = MagicMock(spec=KrknTelemetryOpenshift) + mock_scenario_telemetry = MagicMock() + + result = self.plugin.run( + run_uuid="test-uuid", + scenario=scenario_path, + krkn_config={}, + lib_telemetry=mock_telemetry, + scenario_telemetry=mock_scenario_telemetry, + ) + + self.assertEqual(result, 1) + + def test_run_pod_not_found(self): + """Test run returns 1 when pod doesn't exist""" + with tempfile.TemporaryDirectory() as temp_dir: + scenario_config = { + "pvc_scenario": { + "namespace": "test-ns", + "pod_name": "non-existent-pod", + } + } + scenario_path = self.create_scenario_file(scenario_config, temp_dir) + + mock_telemetry = MagicMock(spec=KrknTelemetryOpenshift) + mock_kubecli = MagicMock() + mock_telemetry.get_lib_kubernetes.return_value = mock_kubecli + mock_kubecli.get_pod_info.return_value = None + mock_scenario_telemetry = MagicMock() + + result = self.plugin.run( + run_uuid="test-uuid", + scenario=scenario_path, + krkn_config={}, + lib_telemetry=mock_telemetry, + scenario_telemetry=mock_scenario_telemetry, + ) + + self.assertEqual(result, 1) + + def test_run_pvc_not_found_for_pod(self): + """Test run returns 1 when pod has no PVC""" + with tempfile.TemporaryDirectory() as temp_dir: + scenario_config = { + "pvc_scenario": { + "namespace": "test-ns", + "pod_name": "test-pod", + } + } + scenario_path = self.create_scenario_file(scenario_config, temp_dir) + + mock_telemetry = MagicMock(spec=KrknTelemetryOpenshift) + mock_kubecli = MagicMock() + mock_telemetry.get_lib_kubernetes.return_value = mock_kubecli + + # Create mock pod with no PVC volumes + mock_pod = MagicMock() + mock_volume = MagicMock() + mock_volume.pvcName = None # No PVC attached + mock_pod.volumes = [mock_volume] + mock_kubecli.get_pod_info.return_value = mock_pod + + mock_scenario_telemetry = MagicMock() + + result = self.plugin.run( + run_uuid="test-uuid", + scenario=scenario_path, + krkn_config={}, + lib_telemetry=mock_telemetry, + scenario_telemetry=mock_scenario_telemetry, + ) + + self.assertEqual(result, 1) + + def test_run_invalid_fill_percentage(self): + """Test run returns 1 when target fill percentage is invalid""" + with tempfile.TemporaryDirectory() as temp_dir: + scenario_config = { + "pvc_scenario": { + "namespace": "test-ns", + "pod_name": "test-pod", + "fill_percentage": 10, # Lower than current usage + } + } + scenario_path = self.create_scenario_file(scenario_config, temp_dir) + + mock_telemetry = MagicMock(spec=KrknTelemetryOpenshift) + mock_kubecli = MagicMock() + mock_telemetry.get_lib_kubernetes.return_value = mock_kubecli + + # Create mock pod with PVC volume + mock_pod = MagicMock() + mock_volume = MagicMock() + mock_volume.pvcName = "test-pvc" + mock_volume.name = "test-volume" + mock_pod.volumes = [mock_volume] + + # Create mock container with volume mount + mock_container = MagicMock() + mock_container.name = "test-container" + mock_vol_mount = MagicMock() + mock_vol_mount.name = "test-volume" + mock_vol_mount.mountPath = "/mnt/data" + mock_container.volumeMounts = [mock_vol_mount] + mock_pod.containers = [mock_container] + + mock_kubecli.get_pod_info.return_value = mock_pod + + # Mock PVC info + mock_pvc = MagicMock() + mock_kubecli.get_pvc_info.return_value = mock_pvc + + # Mock df command output: 50% used (50000 used, 50000 available, 100000 total) + mock_kubecli.exec_cmd_in_pod.return_value = ( + "/dev/sda1 100000 50000 50000 50% /mnt/data" + ) + + mock_scenario_telemetry = MagicMock() + + result = self.plugin.run( + run_uuid="test-uuid", + scenario=scenario_path, + krkn_config={}, + lib_telemetry=mock_telemetry, + scenario_telemetry=mock_scenario_telemetry, + ) + + # Should return 1 because target fill (10%) < current fill (50%) + self.assertEqual(result, 1) + + @patch("krkn.scenario_plugins.pvc.pvc_scenario_plugin.time.sleep") + @patch("krkn.scenario_plugins.pvc.pvc_scenario_plugin.cerberus.publish_kraken_status") + def test_run_success_with_fallocate(self, mock_publish, mock_sleep): + """Test successful run using fallocate""" + with tempfile.TemporaryDirectory() as temp_dir: + scenario_config = { + "pvc_scenario": { + "namespace": "test-ns", + "pod_name": "test-pod", + "fill_percentage": 80, + "duration": 1, + } + } + scenario_path = self.create_scenario_file(scenario_config, temp_dir) + + mock_telemetry = MagicMock(spec=KrknTelemetryOpenshift) + mock_kubecli = MagicMock() + mock_telemetry.get_lib_kubernetes.return_value = mock_kubecli + + # Create mock pod with PVC volume + mock_pod = MagicMock() + mock_volume = MagicMock() + mock_volume.pvcName = "test-pvc" + mock_volume.name = "test-volume" + mock_pod.volumes = [mock_volume] + + # Create mock container with volume mount + mock_container = MagicMock() + mock_container.name = "test-container" + mock_vol_mount = MagicMock() + mock_vol_mount.name = "test-volume" + mock_vol_mount.mountPath = "/mnt/data" + mock_container.volumeMounts = [mock_vol_mount] + mock_pod.containers = [mock_container] + + mock_kubecli.get_pod_info.return_value = mock_pod + + # Mock PVC info + mock_pvc = MagicMock() + mock_kubecli.get_pvc_info.return_value = mock_pvc + + # Set up exec_cmd_in_pod responses + mock_kubecli.exec_cmd_in_pod.side_effect = [ + "/dev/sda1 100000 10000 90000 10% /mnt/data", # df command (10% used) + "/usr/bin/fallocate", # command -v fallocate + "/usr/bin/dd", # command -v dd + "", # fallocate command + "-rw-r--r-- 1 root root 70M Jan 1 00:00 kraken.tmp", # ls -lh (file created) + "", # rm -f (cleanup) + "total 0", # ls -lh (file removed) + ] + + mock_scenario_telemetry = MagicMock() + + result = self.plugin.run( + run_uuid="test-uuid", + scenario=scenario_path, + krkn_config={}, + lib_telemetry=mock_telemetry, + scenario_telemetry=mock_scenario_telemetry, + ) + + self.assertEqual(result, 0) + mock_sleep.assert_called_once_with(1) + + @patch("krkn.scenario_plugins.pvc.pvc_scenario_plugin.time.sleep") + @patch("krkn.scenario_plugins.pvc.pvc_scenario_plugin.cerberus.publish_kraken_status") + def test_run_success_with_dd(self, mock_publish, mock_sleep): + """Test successful run using dd when fallocate is not available""" + with tempfile.TemporaryDirectory() as temp_dir: + scenario_config = { + "pvc_scenario": { + "namespace": "test-ns", + "pod_name": "test-pod", + "fill_percentage": 80, + "duration": 1, + } + } + scenario_path = self.create_scenario_file(scenario_config, temp_dir) + + mock_telemetry = MagicMock(spec=KrknTelemetryOpenshift) + mock_kubecli = MagicMock() + mock_telemetry.get_lib_kubernetes.return_value = mock_kubecli + + # Create mock pod with PVC volume + mock_pod = MagicMock() + mock_volume = MagicMock() + mock_volume.pvcName = "test-pvc" + mock_volume.name = "test-volume" + mock_pod.volumes = [mock_volume] + + # Create mock container with volume mount + mock_container = MagicMock() + mock_container.name = "test-container" + mock_vol_mount = MagicMock() + mock_vol_mount.name = "test-volume" + mock_vol_mount.mountPath = "/mnt/data" + mock_container.volumeMounts = [mock_vol_mount] + mock_pod.containers = [mock_container] + + mock_kubecli.get_pod_info.return_value = mock_pod + + # Mock PVC info + mock_pvc = MagicMock() + mock_kubecli.get_pvc_info.return_value = mock_pvc + + # Set up exec_cmd_in_pod responses (fallocate not available) + mock_kubecli.exec_cmd_in_pod.side_effect = [ + "/dev/sda1 100000 10000 90000 10% /mnt/data", # df command + "", # command -v fallocate (not found) + "/usr/bin/dd", # command -v dd + "", # dd command + "-rw-r--r-- 1 root root 70M Jan 1 00:00 kraken.tmp", # ls -lh + "", # rm -f + "total 0", # ls -lh (file removed) + ] + + mock_scenario_telemetry = MagicMock() + + result = self.plugin.run( + run_uuid="test-uuid", + scenario=scenario_path, + krkn_config={}, + lib_telemetry=mock_telemetry, + scenario_telemetry=mock_scenario_telemetry, + ) + + self.assertEqual(result, 0) + + def test_run_no_binary_available(self): + """Test run returns 1 when neither fallocate nor dd is available""" + with tempfile.TemporaryDirectory() as temp_dir: + scenario_config = { + "pvc_scenario": { + "namespace": "test-ns", + "pod_name": "test-pod", + "fill_percentage": 80, + } + } + scenario_path = self.create_scenario_file(scenario_config, temp_dir) + + mock_telemetry = MagicMock(spec=KrknTelemetryOpenshift) + mock_kubecli = MagicMock() + mock_telemetry.get_lib_kubernetes.return_value = mock_kubecli + + # Create mock pod with PVC volume + mock_pod = MagicMock() + mock_volume = MagicMock() + mock_volume.pvcName = "test-pvc" + mock_volume.name = "test-volume" + mock_pod.volumes = [mock_volume] + + # Create mock container with volume mount + mock_container = MagicMock() + mock_container.name = "test-container" + mock_vol_mount = MagicMock() + mock_vol_mount.name = "test-volume" + mock_vol_mount.mountPath = "/mnt/data" + mock_container.volumeMounts = [mock_vol_mount] + mock_pod.containers = [mock_container] + + mock_kubecli.get_pod_info.return_value = mock_pod + + # Mock PVC info + mock_pvc = MagicMock() + mock_kubecli.get_pvc_info.return_value = mock_pvc + + # Neither fallocate nor dd available + mock_kubecli.exec_cmd_in_pod.side_effect = [ + "/dev/sda1 100000 10000 90000 10% /mnt/data", # df command + "", # command -v fallocate (not found) + "", # command -v dd (not found) + ] + + mock_scenario_telemetry = MagicMock() + + result = self.plugin.run( + run_uuid="test-uuid", + scenario=scenario_path, + krkn_config={}, + lib_telemetry=mock_telemetry, + scenario_telemetry=mock_scenario_telemetry, + ) + + self.assertEqual(result, 1) + + def test_run_file_not_found(self): + """Test run returns 1 when scenario file doesn't exist""" + mock_telemetry = MagicMock(spec=KrknTelemetryOpenshift) + mock_scenario_telemetry = MagicMock() + + result = self.plugin.run( + run_uuid="test-uuid", + scenario="/non/existent/path.yaml", + krkn_config={}, + lib_telemetry=mock_telemetry, + scenario_telemetry=mock_scenario_telemetry, + ) + + self.assertEqual(result, 1) + + def test_run_both_pvc_and_pod_name_provided(self): + """Test run when both pvc_name and pod_name are provided (pod_name is overridden)""" + with tempfile.TemporaryDirectory() as temp_dir: + scenario_config = { + "pvc_scenario": { + "namespace": "test-ns", + "pvc_name": "test-pvc", + "pod_name": "ignored-pod", # This will be overridden + "fill_percentage": 80, + "duration": 1, + } + } + scenario_path = self.create_scenario_file(scenario_config, temp_dir) + + mock_telemetry = MagicMock(spec=KrknTelemetryOpenshift) + mock_kubecli = MagicMock() + mock_telemetry.get_lib_kubernetes.return_value = mock_kubecli + + # Mock PVC info with pod names + mock_pvc = MagicMock() + mock_pvc.podNames = ["actual-pod-from-pvc"] + mock_kubecli.get_pvc_info.return_value = mock_pvc + + # Create mock pod with PVC volume + mock_pod = MagicMock() + mock_volume = MagicMock() + mock_volume.pvcName = "test-pvc" + mock_volume.name = "test-volume" + mock_pod.volumes = [mock_volume] + + # Create mock container with volume mount + mock_container = MagicMock() + mock_container.name = "test-container" + mock_vol_mount = MagicMock() + mock_vol_mount.name = "test-volume" + mock_vol_mount.mountPath = "/mnt/data" + mock_container.volumeMounts = [mock_vol_mount] + mock_pod.containers = [mock_container] + + mock_kubecli.get_pod_info.return_value = mock_pod + + # Mock df command output: 10% used + mock_kubecli.exec_cmd_in_pod.side_effect = [ + "/dev/sda1 100000 10000 90000 10% /mnt/data", # df command + "/usr/bin/fallocate", # command -v fallocate + "/usr/bin/dd", # command -v dd + "", # fallocate command + "-rw-r--r-- 1 root root 70M Jan 1 00:00 kraken.tmp", # ls -lh (file created) + "", # rm -f (cleanup) + "total 0", # ls -lh (file removed) + ] + + mock_scenario_telemetry = MagicMock() + + with patch("krkn.scenario_plugins.pvc.pvc_scenario_plugin.time.sleep"): + with patch("krkn.scenario_plugins.pvc.pvc_scenario_plugin.cerberus.publish_kraken_status"): + result = self.plugin.run( + run_uuid="test-uuid", + scenario=scenario_path, + krkn_config={}, + lib_telemetry=mock_telemetry, + scenario_telemetry=mock_scenario_telemetry, + ) + + self.assertEqual(result, 0) + # get_pod_info should be called with "actual-pod-from-pvc", not "ignored-pod" + mock_kubecli.get_pod_info.assert_called_with( + name="actual-pod-from-pvc", + namespace="test-ns" + ) + + # Verify exec_cmd_in_pod uses the overridden pod name + for call in mock_kubecli.exec_cmd_in_pod.call_args_list: + kwargs = call[1] + if 'pod_name' in kwargs: + self.assertEqual(kwargs['pod_name'], "actual-pod-from-pvc") + + def test_run_pvc_name_only_no_pods_associated(self): + """Test run returns 1 when pvc_name is provided but no pods are associated""" + with tempfile.TemporaryDirectory() as temp_dir: + scenario_config = { + "pvc_scenario": { + "namespace": "test-ns", + "pvc_name": "test-pvc", + "fill_percentage": 80, + } + } + scenario_path = self.create_scenario_file(scenario_config, temp_dir) + + mock_telemetry = MagicMock(spec=KrknTelemetryOpenshift) + mock_kubecli = MagicMock() + mock_telemetry.get_lib_kubernetes.return_value = mock_kubecli + + # Mock PVC info with empty pod names (no pods using this PVC) + mock_pvc = MagicMock() + mock_pvc.podNames = [] # No pods associated + mock_kubecli.get_pvc_info.return_value = mock_pvc + + mock_scenario_telemetry = MagicMock() + + result = self.plugin.run( + run_uuid="test-uuid", + scenario=scenario_path, + krkn_config={}, + lib_telemetry=mock_telemetry, + scenario_telemetry=mock_scenario_telemetry, + ) + + # Should return 1 because random.choice on empty list raises IndexError + self.assertEqual(result, 1) + + def test_run_file_creation_failed(self): + """Test run returns 1 when file creation fails and verifies cleanup is attempted""" + with tempfile.TemporaryDirectory() as temp_dir: + scenario_config = { + "pvc_scenario": { + "namespace": "test-ns", + "pod_name": "test-pod", + "fill_percentage": 80, + "duration": 1, + } + } + scenario_path = self.create_scenario_file(scenario_config, temp_dir) + + mock_telemetry = MagicMock(spec=KrknTelemetryOpenshift) + mock_kubecli = MagicMock() + mock_telemetry.get_lib_kubernetes.return_value = mock_kubecli + + # Create mock pod with PVC volume + mock_pod = MagicMock() + mock_volume = MagicMock() + mock_volume.pvcName = "test-pvc" + mock_volume.name = "test-volume" + mock_pod.volumes = [mock_volume] + + # Create mock container with volume mount + mock_container = MagicMock() + mock_container.name = "test-container" + mock_vol_mount = MagicMock() + mock_vol_mount.name = "test-volume" + mock_vol_mount.mountPath = "/mnt/data" + mock_container.volumeMounts = [mock_vol_mount] + mock_pod.containers = [mock_container] + + mock_kubecli.get_pod_info.return_value = mock_pod + + # Mock PVC info + mock_pvc = MagicMock() + mock_kubecli.get_pvc_info.return_value = mock_pvc + + # Set up exec_cmd_in_pod responses - file creation fails + mock_kubecli.exec_cmd_in_pod.side_effect = [ + "/dev/sda1 100000 10000 90000 10% /mnt/data", # df command + "/usr/bin/fallocate", # command -v fallocate + "/usr/bin/dd", # command -v dd + "", # fallocate command + "total 0", # ls -lh shows NO kraken.tmp (file creation failed) + "", # rm -f (cleanup attempt) + "total 0", # ls -lh (cleanup verification) + ] + + mock_scenario_telemetry = MagicMock() + + result = self.plugin.run( + run_uuid="test-uuid", + scenario=scenario_path, + krkn_config={}, + lib_telemetry=mock_telemetry, + scenario_telemetry=mock_scenario_telemetry, + ) + + # Should return 1 because file creation failed + self.assertEqual(result, 1) + + # Verify cleanup was attempted (7 calls total: df, 2x command -v, fallocate, ls, rm, ls) + self.assertEqual(mock_kubecli.exec_cmd_in_pod.call_count, 7) + +class TestRollbackTempFileEdgeCases(unittest.TestCase): + """Additional tests for rollback_temp_file edge cases""" + + @patch("krkn.scenario_plugins.pvc.pvc_scenario_plugin.logging") + def test_rollback_temp_file_still_exists(self, mock_logging): + """Test rollback when file still exists after removal attempt and logs warning""" + mock_telemetry = MagicMock(spec=KrknTelemetryOpenshift) + mock_kubecli = MagicMock() + mock_telemetry.get_lib_kubernetes.return_value = mock_kubecli + + # Simulate file still exists after rm command + mock_kubecli.exec_cmd_in_pod.side_effect = [ + "", # rm -f command output + "-rw-r--r-- 1 root root 70M Jan 1 00:00 kraken.tmp", # ls -lh shows file still exists + ] + + # Create rollback data + rollback_data = { + "pod_name": "test-pod", + "container_name": "test-container", + "full_path": "/mnt/data/kraken.tmp", + "file_name": "kraken.tmp", + "mount_path": "/mnt/data", + } + encoded_data = base64.b64encode( + json.dumps(rollback_data).encode("utf-8") + ).decode("utf-8") + + rollback_content = RollbackContent( + namespace="test-ns", + resource_identifier=encoded_data, + ) + + # Should not raise exception, just log warning + PvcScenarioPlugin.rollback_temp_file(rollback_content, mock_telemetry) + + # Verify exec_cmd_in_pod was called twice + assert mock_kubecli.exec_cmd_in_pod.call_count == 2 + + # Verify warning was logged to inform operators of incomplete rollback + mock_logging.warning.assert_called_once() + warning_message = mock_logging.warning.call_args[0][0] + self.assertIn("may still exist after rollback attempt", warning_message) + self.assertIn("kraken.tmp", warning_message) + + if __name__ == "__main__": unittest.main()