Skip to content

Commit 5ce4411

Browse files
committed
Add unit tests for snapshot overlay fix
1 parent 2b120bd commit 5ce4411

3 files changed

Lines changed: 285 additions & 0 deletions

File tree

‎agent/src/test/java/com/cloud/agent/AgentTest.java‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,12 @@
2222
import static org.junit.Assert.assertSame;
2323
import static org.junit.Assert.assertTrue;
2424
import static org.mockito.Mockito.any;
25+
import static org.mockito.Mockito.doNothing;
2526
import static org.mockito.Mockito.doReturn;
2627
import static org.mockito.Mockito.doThrow;
2728
import static org.mockito.Mockito.mock;
2829
import static org.mockito.Mockito.eq;
30+
import static org.mockito.Mockito.spy;
2931
import static org.mockito.Mockito.times;
3032
import static org.mockito.Mockito.verify;
3133
import static org.mockito.Mockito.when;
@@ -254,4 +256,43 @@ public void testStopAndCleanupConnectionValidConnectionWaitForStopWaitsForStartu
254256
verify(mockConnection).cleanUp();
255257
verify(mockBackoff, times(3)).waitBeforeRetry();
256258
}
259+
260+
@Test
261+
public void testWaitForCommandsAndReconnectNoCommandsInProgress() throws ConfigurationException {
262+
Agent spyAgent = spy(new Agent(shell, 1, serverResource));
263+
ReflectionTestUtils.setField(spyAgent, "logger", logger);
264+
doNothing().when(spyAgent).reconnect(any(Link.class));
265+
266+
Link mockLink = mock(Link.class);
267+
spyAgent.commandsInProgress.set(0);
268+
spyAgent.waitForCommandsAndReconnect(mockLink);
269+
270+
verify(spyAgent).reconnect(mockLink);
271+
verify(logger).info(eq("Reconnecting instead of exiting to avoid losing in-flight work (e.g. snapshot overlays)."));
272+
}
273+
274+
@Test
275+
public void testWaitForCommandsAndReconnectCommandsCompleteBeforeTimeout() throws ConfigurationException {
276+
Agent spyAgent = spy(new Agent(shell, 1, serverResource));
277+
ReflectionTestUtils.setField(spyAgent, "logger", logger);
278+
doNothing().when(spyAgent).reconnect(any(Link.class));
279+
280+
Link mockLink = mock(Link.class);
281+
spyAgent.commandsInProgress.set(1);
282+
283+
Thread decrementThread = new Thread(() -> {
284+
try {
285+
Thread.sleep(500);
286+
} catch (InterruptedException e) {
287+
Thread.currentThread().interrupt();
288+
}
289+
spyAgent.commandsInProgress.set(0);
290+
});
291+
decrementThread.start();
292+
293+
spyAgent.waitForCommandsAndReconnect(mockLink);
294+
295+
verify(spyAgent).reconnect(mockLink);
296+
verify(logger).info(eq("All in-progress commands completed. Proceeding with reconnect."));
297+
}
257298
}

‎plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessorTest.java‎

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959

6060
import javax.naming.ConfigurationException;
6161
import java.io.File;
62+
import java.io.FileOutputStream;
6263
import java.io.IOException;
6364
import java.lang.reflect.Method;
6465
import java.nio.file.Files;
@@ -68,6 +69,7 @@
6869
import java.util.List;
6970
import java.util.Map;
7071
import java.util.Set;
72+
import java.util.concurrent.locks.ReentrantLock;
7173

7274
@RunWith(MockitoJUnitRunner.class)
7375
public class KVMStorageProcessorTest {
@@ -765,4 +767,223 @@ public void ensureLibvirtSupportsEncryptedRbdHotplugIgnoresNonRbd() {
765767
// version is not even consulted for them.
766768
storageProcessor.ensureLibvirtSupportsEncryptedRbdHotplug(StoragePoolType.NetworkFilesystem);
767769
}
770+
771+
private File tempMarkerDir;
772+
773+
private void setUpMarkerDir() throws Exception {
774+
tempMarkerDir = Files.createTempDirectory("snapshot_markers_test").toFile();
775+
java.lang.reflect.Field field = KVMStorageProcessor.class.getDeclaredField("SNAPSHOT_MARKER_DIR");
776+
field.setAccessible(true);
777+
778+
java.lang.reflect.Field modifiers = java.lang.reflect.Field.class.getDeclaredField("modifiers");
779+
modifiers.setAccessible(true);
780+
modifiers.setInt(field, field.getModifiers() & ~java.lang.reflect.Modifier.FINAL);
781+
field.set(null, tempMarkerDir.getAbsolutePath());
782+
}
783+
784+
private void cleanUpMarkerDir() {
785+
if (tempMarkerDir != null && tempMarkerDir.exists()) {
786+
File[] files = tempMarkerDir.listFiles();
787+
if (files != null) {
788+
for (File f : files) {
789+
f.delete();
790+
}
791+
}
792+
tempMarkerDir.delete();
793+
}
794+
}
795+
796+
@Test
797+
public void testWriteSnapshotMarkerCreatesFile() throws Exception {
798+
File markerDir = Files.createTempDirectory("marker_test").toFile();
799+
try {
800+
Mockito.when(kvmStoragePoolMock.getUuid()).thenReturn("pool-uuid-1");
801+
storageProcessorSpy.writeSnapshotMarker(kvmStoragePoolMock, "/mnt/pool/vol1", "snap1");
802+
803+
File[] files = new File("/var/cache/cloudstack/snapshot_markers").listFiles(
804+
(dir, name) -> name.contains("pool-uuid-1") && name.contains("snap1"));
805+
if (files != null && files.length > 0) {
806+
String content = new String(Files.readAllBytes(files[0].toPath()));
807+
Assert.assertTrue(content.contains("pool-uuid-1"));
808+
Assert.assertTrue(content.contains("snap1"));
809+
files[0].delete();
810+
}
811+
} finally {
812+
markerDir.delete();
813+
}
814+
}
815+
816+
@Test
817+
public void testWriteAndRemoveSnapshotMarker() throws Exception {
818+
File markerDir = new File("/tmp/test_snapshot_markers_" + System.currentTimeMillis());
819+
markerDir.mkdirs();
820+
try {
821+
java.lang.reflect.Field field = KVMStorageProcessor.class.getDeclaredField("SNAPSHOT_MARKER_DIR");
822+
field.setAccessible(true);
823+
String originalDir = (String) field.get(null);
824+
825+
Mockito.when(kvmStoragePoolMock.getUuid()).thenReturn("test-pool");
826+
827+
storageProcessorSpy.writeSnapshotMarker(kvmStoragePoolMock, "/mnt/pool/vol", "snap");
828+
829+
File[] written = markerDir.listFiles();
830+
831+
storageProcessorSpy.removeSnapshotMarker(kvmStoragePoolMock, "/mnt/pool/vol", "snap");
832+
} finally {
833+
File[] remaining = markerDir.listFiles();
834+
if (remaining != null) {
835+
for (File f : remaining) f.delete();
836+
}
837+
markerDir.delete();
838+
}
839+
}
840+
841+
@Test
842+
public void testReconcileSnapshotMarkersNoMarkerDir() {
843+
storageProcessorSpy.reconcileSnapshotMarkers();
844+
}
845+
846+
@Test
847+
public void testReconcileSingleMarkerOverlayGone() throws Exception {
848+
File tempDir = Files.createTempDirectory("reconcile_test").toFile();
849+
try {
850+
File marker = new File(tempDir, ".snapshot_marker_test");
851+
String content = "pool-uuid|/mnt/vol|snap|||| " + System.currentTimeMillis();
852+
try (FileOutputStream fos = new FileOutputStream(marker)) {
853+
fos.write(content.getBytes());
854+
}
855+
856+
Mockito.when(resource.getSnapshotTemporaryPath(Mockito.anyString(), Mockito.anyString()))
857+
.thenReturn("/nonexistent/overlay/path");
858+
859+
storageProcessorSpy.reconcileSingleMarker(marker);
860+
861+
Assert.assertFalse("Marker should be removed when overlay is gone", marker.exists());
862+
} finally {
863+
File[] files = tempDir.listFiles();
864+
if (files != null) for (File f : files) f.delete();
865+
tempDir.delete();
866+
}
867+
}
868+
869+
@Test
870+
public void testReconcileSingleMarkerInvalidFormat() throws Exception {
871+
File tempDir = Files.createTempDirectory("reconcile_invalid_test").toFile();
872+
try {
873+
File marker = new File(tempDir, ".snapshot_marker_invalid");
874+
try (FileOutputStream fos = new FileOutputStream(marker)) {
875+
fos.write("invalid".getBytes());
876+
}
877+
878+
storageProcessorSpy.reconcileSingleMarker(marker);
879+
880+
Assert.assertTrue("Marker with invalid format should not be deleted", marker.exists());
881+
} finally {
882+
File[] files = tempDir.listFiles();
883+
if (files != null) for (File f : files) f.delete();
884+
tempDir.delete();
885+
}
886+
}
887+
888+
@Test
889+
public void testGetVmBlockJobLockReturnsSameLockForSameVm() throws Exception {
890+
java.lang.reflect.Method method = KVMStorageProcessor.class.getDeclaredMethod("getVmBlockJobLock", String.class);
891+
method.setAccessible(true);
892+
893+
ReentrantLock lock1 = (ReentrantLock) method.invoke(storageProcessorSpy, "vm-1");
894+
ReentrantLock lock2 = (ReentrantLock) method.invoke(storageProcessorSpy, "vm-1");
895+
ReentrantLock lock3 = (ReentrantLock) method.invoke(storageProcessorSpy, "vm-2");
896+
897+
Assert.assertSame("Same VM should return same lock", lock1, lock2);
898+
Assert.assertNotSame("Different VMs should return different locks", lock1, lock3);
899+
}
900+
901+
@Test
902+
public void testReconcileOfflineBaseModifiedSinceMarker() throws Exception {
903+
File tempDir = Files.createTempDirectory("reconcile_offline_test").toFile();
904+
try {
905+
File baseFile = new File(tempDir, "base.qcow2");
906+
File overlayFile = new File(tempDir, "overlay.qcow2");
907+
908+
try (FileOutputStream fos = new FileOutputStream(baseFile)) { fos.write("base".getBytes()); }
909+
try (FileOutputStream fos = new FileOutputStream(overlayFile)) { fos.write("overlay".getBytes()); }
910+
911+
long markerTimestamp = System.currentTimeMillis() - 60000;
912+
913+
baseFile.setLastModified(System.currentTimeMillis());
914+
915+
File marker = new File(tempDir, ".snapshot_marker_test");
916+
String content = String.join("|",
917+
"pool-uuid", baseFile.getAbsolutePath(), "snap", "vm1", "vda",
918+
overlayFile.getAbsolutePath(), String.valueOf(markerTimestamp));
919+
try (FileOutputStream fos = new FileOutputStream(marker)) {
920+
fos.write(content.getBytes());
921+
}
922+
923+
java.lang.reflect.Method method = KVMStorageProcessor.class.getDeclaredMethod(
924+
"reconcileOffline", String.class, String.class, File.class);
925+
method.setAccessible(true);
926+
method.invoke(storageProcessorSpy, baseFile.getAbsolutePath(), overlayFile.getAbsolutePath(), marker);
927+
928+
Assert.assertTrue("Marker should be kept when base was modified after marker", marker.exists());
929+
} finally {
930+
File[] files = tempDir.listFiles();
931+
if (files != null) for (File f : files) f.delete();
932+
tempDir.delete();
933+
}
934+
}
935+
936+
@Test
937+
public void testReconcileOfflineBaseDoesNotExist() throws Exception {
938+
File tempDir = Files.createTempDirectory("reconcile_nobase_test").toFile();
939+
try {
940+
File overlayFile = new File(tempDir, "overlay.qcow2");
941+
try (FileOutputStream fos = new FileOutputStream(overlayFile)) { fos.write("overlay".getBytes()); }
942+
943+
File marker = new File(tempDir, ".snapshot_marker_test");
944+
try (FileOutputStream fos = new FileOutputStream(marker)) { fos.write("marker".getBytes()); }
945+
946+
java.lang.reflect.Method method = KVMStorageProcessor.class.getDeclaredMethod(
947+
"reconcileOffline", String.class, String.class, File.class);
948+
method.setAccessible(true);
949+
method.invoke(storageProcessorSpy, "/nonexistent/base.qcow2", overlayFile.getAbsolutePath(), marker);
950+
951+
Assert.assertTrue("Marker should be kept when base does not exist", marker.exists());
952+
} finally {
953+
File[] files = tempDir.listFiles();
954+
if (files != null) for (File f : files) f.delete();
955+
tempDir.delete();
956+
}
957+
}
958+
959+
@Test
960+
public void testReconcileOfflineOverlayDoesNotExist() throws Exception {
961+
File tempDir = Files.createTempDirectory("reconcile_nooverlay_test").toFile();
962+
try {
963+
File marker = new File(tempDir, ".snapshot_marker_test");
964+
try (FileOutputStream fos = new FileOutputStream(marker)) { fos.write("marker".getBytes()); }
965+
966+
java.lang.reflect.Method method = KVMStorageProcessor.class.getDeclaredMethod(
967+
"reconcileOffline", String.class, String.class, File.class);
968+
method.setAccessible(true);
969+
method.invoke(storageProcessorSpy, "/some/base.qcow2", "/nonexistent/overlay.qcow2", marker);
970+
971+
Assert.assertFalse("Marker should be removed when overlay does not exist", marker.exists());
972+
} finally {
973+
File[] files = tempDir.listFiles();
974+
if (files != null) for (File f : files) f.delete();
975+
tempDir.delete();
976+
}
977+
}
978+
979+
@Test
980+
public void testUpdateSnapshotMarkerDelegatesToWrite() throws Exception {
981+
Mockito.doNothing().when(storageProcessorSpy).writeSnapshotMarker(
982+
Mockito.any(), Mockito.anyString(), Mockito.anyString(),
983+
Mockito.anyString(), Mockito.anyString(), Mockito.anyString());
984+
985+
storageProcessorSpy.updateSnapshotMarker(kvmStoragePoolMock, "/vol", "snap", "vm", "vda", "/overlay");
986+
987+
Mockito.verify(storageProcessorSpy).writeSnapshotMarker(kvmStoragePoolMock, "/vol", "snap", "vm", "vda", "/overlay");
988+
}
768989
}

‎utils/src/test/java/com/cloud/utils/script/ScriptTest.java‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.mockito.junit.MockitoJUnitRunner;
2929

3030
import com.cloud.utils.Pair;
31+
import com.cloud.utils.exception.CloudRuntimeException;
3132

3233
@RunWith(MockitoJUnitRunner.class)
3334
public class ScriptTest {
@@ -108,4 +109,26 @@ public void testBuildCommandLineWithLegacyPasswordOption() {
108109
Assert.assertEquals("test.sh -y ****** ", commandLine);
109110
}
110111

112+
@Test
113+
public void testRunSimpleBashScriptWithFullResultSuccess() {
114+
String result = Script.runSimpleBashScriptWithFullResult("echo hello", 10);
115+
Assert.assertNotNull(result);
116+
Assert.assertEquals("hello", result.trim());
117+
}
118+
119+
@Test(expected = CloudRuntimeException.class)
120+
public void testRunSimpleBashScriptWithFullResultFailureIncludesOutput() {
121+
try {
122+
Script.runSimpleBashScriptWithFullResult("echo 'some error detail' >&2; exit 1", 10);
123+
} catch (CloudRuntimeException e) {
124+
Assert.assertTrue(e.getMessage().contains("Error executing command"));
125+
throw e;
126+
}
127+
}
128+
129+
@Test(expected = CloudRuntimeException.class)
130+
public void testRunSimpleBashScriptWithFullResultFailureOnInvalidCommand() {
131+
Script.runSimpleBashScriptWithFullResult("nonexistent_command_xyz_12345", 10);
132+
}
133+
111134
}

0 commit comments

Comments
 (0)