listVMs() throws HypervisorException;
/**
* Retourne le statut courant d'une machine virtuelle.
diff --git a/src/main/java/tg/cyberlabmanager/hypervisor/VBoxException.java b/src/main/java/tg/cyberlabmanager/hypervisor/VBoxException.java
index 9851744..3ae1587 100644
--- a/src/main/java/tg/cyberlabmanager/hypervisor/VBoxException.java
+++ b/src/main/java/tg/cyberlabmanager/hypervisor/VBoxException.java
@@ -1,16 +1,16 @@
package tg.cyberlabmanager.hypervisor;
/**
- * Exception specifique aux commandes VirtualBox executees via VBoxManage.
+ * Exception specifique aux commandes VirtualBox exécutées via VBoxManage.
*/
public class VBoxException extends HypervisorException {
- /** Code de sortie retourne par VBoxManage. */
+ /** Code de sortie retourné par VBoxManage. */
private final int exitCode;
- /** Sortie standard ou erreur retournee par VBoxManage. */
+ /** Sortie standard ou erreur retournée par VBoxManage. */
private final String output;
/**
- * Cree une exception VirtualBox avec le code de sortie et la sortie texte.
+ * Crée une exception VirtualBox avec le code de sortie et la sortie texte.
*
* @param message message decrivant l'erreur
* @param exitCode code de sortie de VBoxManage
diff --git a/src/main/java/tg/cyberlabmanager/hypervisor/VBoxOrchestrator.java b/src/main/java/tg/cyberlabmanager/hypervisor/VBoxOrchestrator.java
index b8f432d..e1ca5db 100644
--- a/src/main/java/tg/cyberlabmanager/hypervisor/VBoxOrchestrator.java
+++ b/src/main/java/tg/cyberlabmanager/hypervisor/VBoxOrchestrator.java
@@ -1,58 +1,436 @@
package tg.cyberlabmanager.hypervisor;
+import tg.cyberlabmanager.model.AppConfig;
+import tg.cyberlabmanager.model.VMDescriptor;
import tg.cyberlabmanager.model.VMStatus;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
/**
- * Implementation de {@link IHypervisor} pour VirtualBox via VBoxManage.
+ * Implémentation de {@link IHypervisor} pour VirtualBox via VBoxManage.
+ *
+ * À la construction, l'orchestrateur résout le chemin de VBoxManage en
+ * trois étapes :
+ *
+ * - Utilisation du chemin présent dans {@link AppConfig} s'il est valide.
+ * - Auto-détection dans les emplacements standards de l'OS.
+ * - Si introuvable, {@link #vboxManagePath} reste {@code null} ; toute
+ * opération lève une {@link HypervisorException} que l'UI intercepte
+ * pour inviter l'utilisateur à saisir le chemin manuellement.
+ *
*/
public class VBoxOrchestrator implements IHypervisor {
+
+ /** Regex pour analyser une ligne de {@code VBoxManage list vms} : {@code "Nom" {uuid}}. */
+ private static final Pattern LIST_VMS_PATTERN =
+ Pattern.compile(
+ "^\"([^\"]+)\"\\s+\\{([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-"
+ + "[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})\\}\\s*$");
+
+ /** Chemin résolu vers VBoxManage, ou {@code null} si introuvable. */
+ private String vboxManagePath;
+
+ /**
+ * Crée un orchestrateur VirtualBox.
+ *
+ * Tente de résoudre VBoxManage depuis la configuration fournie,
+ * puis par auto-détection si nécessaire.
+ *
+ * @param config configuration de l'application (peut être {@code null})
+ */
+ public VBoxOrchestrator(AppConfig config) {
+ this.vboxManagePath = resolveVboxManagePath(config);
+ }
+
/**
- * Cree un orchestrateur VirtualBox.
+ * Indique si VBoxManage a été résolu et est prêt à l'emploi.
+ *
+ * L'UI appelle cette méthode au démarrage pour décider si elle doit
+ * afficher une boîte de dialogue de configuration.
+ *
+ * @return {@code true} si VBoxManage est disponible
*/
- public VBoxOrchestrator() {
+ public boolean isReady() {
+ return vboxManagePath != null;
}
- /** {@inheritDoc} */
+ /**
+ * Applique une nouvelle configuration à l'orchestrateur déjà instancié.
+ *
+ * Cette méthode permet au contrôleur de synchroniser l'orchestrateur
+ * avec les paramètres courants sans exposer les détails VirtualBox à l'UI.
+ *
+ * @param config nouvelle configuration applicative
+ * @throws HypervisorException si le chemin configuré est invalide
+ */
@Override
- public List listVMs() throws HypervisorException {
- throw new UnsupportedOperationException("Not implemented yet");
+ public void applyConfig(AppConfig config) throws HypervisorException {
+ String configured = (config != null) ? config.getVboxManagePath() : null;
+
+ if (configured == null || configured.isBlank()) {
+ this.vboxManagePath = detectVBoxManage();
+ return;
+ }
+
+ setVboxManagePath(configured);
+ }
+
+ /**
+ * Met à jour le chemin de VBoxManage après saisie par l'utilisateur.
+ *
+ * Utilisée par {@link #applyConfig(AppConfig)} pour appliquer un chemin
+ * saisi dans les paramètres.
+ *
+ * @param path chemin vers l'exécutable VBoxManage
+ * @throws HypervisorException si le chemin est vide ou non exécutable
+ */
+ public void setVboxManagePath(String path) throws HypervisorException {
+ if (path == null || path.isBlank()) {
+ throw new HypervisorException("Le chemin de VBoxManage ne peut pas être vide.");
+ }
+ if (!isVBoxManageExecutable(path)) {
+ throw new HypervisorException(
+ "Impossible d'exécuter VBoxManage au chemin indiqué : " + path);
+ }
+ this.vboxManagePath = path;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * Exécute {@code VBoxManage list vms} et retourne les paires
+ * {@code [nom, uuid]} pour chaque VM trouvée.
+ */
+ @Override
+ public List listVMs() throws HypervisorException {
+ return parseListVMsOutput(runCommand("list", "vms"));
+ }
+
+ static List parseListVMsOutput(String output) {
+ List vms = new ArrayList<>();
+
+ if (output != null) {
+ for (String line : output.split("\\R")) {
+ Matcher matcher = LIST_VMS_PATTERN.matcher(line.trim());
+ if (matcher.matches()) {
+ vms.add(new VMDescriptor(matcher.group(1), matcher.group(2)));
+ }
+ }
+ }
+
+ return vms;
}
- /** {@inheritDoc} */
+ /**
+ * {@inheritDoc}
+ *
+ * Exécute {@code VBoxManage showvminfo --machinereadable} et
+ * extrait le champ {@code VMState}.
+ */
@Override
public VMStatus getStatus(String uuid) throws HypervisorException {
- throw new UnsupportedOperationException("Not implemented yet");
+ String output = runCommand("showvminfo", uuid, "--machinereadable");
+
+ return parseShowVmInfoOutput(output);
+ }
+
+ /**
+ * Extrait le statut d'une VM depuis la sortie de {@code VBoxManage showvminfo --machinereadable}.
+ *
+ * @param output sortie brute de la commande, peut être {@code null}
+ * @return statut extrait, ou {@link VMStatus#UNKNOWN} si absent ou non reconnu
+ */
+ static VMStatus parseShowVmInfoOutput(String output) {
+ if (output == null) return VMStatus.UNKNOWN;
+ for (String line : output.split("\\R")) {
+ if (line.startsWith("VMState=")) {
+ String state = line.substring("VMState=".length())
+ .replace("\"", "").trim();
+ return parseVMStatus(state);
+ }
+ }
+ return VMStatus.UNKNOWN;
}
- /** {@inheritDoc} */
+ /**
+ * {@inheritDoc}
+ *
+ * Exécute {@code VBoxManage startvm --type headless}.
+ */
@Override
public void startVM(String uuid) throws HypervisorException {
- throw new UnsupportedOperationException("Not implemented yet");
+ runCommand("startvm", uuid, "--type", "headless");
}
- /** {@inheritDoc} */
+ /**
+ * {@inheritDoc}
+ *
+ * Exécute {@code VBoxManage controlvm poweroff}.
+ */
@Override
public void stopVM(String uuid) throws HypervisorException {
- throw new UnsupportedOperationException("Not implemented yet");
+ runCommand("controlvm", uuid, "poweroff");
}
- /** {@inheritDoc} */
+ /**
+ * {@inheritDoc}
+ *
+ * Exécute {@code VBoxManage controlvm savestate}.
+ */
@Override
public void saveState(String uuid) throws HypervisorException {
- throw new UnsupportedOperationException("Not implemented yet");
+ runCommand("controlvm", uuid, "savestate");
}
- /** {@inheritDoc} */
+ /**
+ * {@inheritDoc}
+ *
+ * Exécute {@code VBoxManage snapshot take }
+ * avec {@code --description} si une description est fournie.
+ */
@Override
- public void takeSnapshot(String uuid, String name, String description) throws HypervisorException {
- throw new UnsupportedOperationException("Not implemented yet");
+ public void takeSnapshot(String uuid, String name, String description)
+ throws HypervisorException {
+ if (description != null && !description.isBlank()) {
+ runCommand("snapshot", uuid, "take", name, "--description", description);
+ } else {
+ runCommand("snapshot", uuid, "take", name);
+ }
}
- /** {@inheritDoc} */
+ /**
+ * {@inheritDoc}
+ *
+ * Exécute {@code VBoxManage snapshot restore }.
+ */
@Override
public void restoreSnapshot(String uuid, String snapshotName) throws HypervisorException {
- throw new UnsupportedOperationException("Not implemented yet");
+ runCommand("snapshot", uuid, "restore", snapshotName);
+ }
+
+ /**
+ * Exécute une commande VBoxManage et retourne la sortie standard.
+ *
+ * @param args arguments passés à VBoxManage (sans le chemin du binaire)
+ * @return sortie texte produite par VBoxManage
+ * @throws HypervisorException si VBoxManage n'est pas configuré, si la
+ * commande échoue ou si une erreur I/O survient
+ */
+ private String runCommand(String... args) throws HypervisorException {
+ String executable = requireConfigured();
+
+ String[] command = new String[args.length + 1];
+ command[0] = executable;
+ System.arraycopy(args, 0, command, 1, args.length);
+
+ try {
+ ProcessBuilder pb = new ProcessBuilder(command);
+ pb.redirectErrorStream(true);
+ Process process = pb.start();
+
+ // Thread de lecture pour ne pas bloquer
+ CompletableFuture outputFuture = CompletableFuture.supplyAsync(() -> {
+ try {
+ return new String(process.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
+ } catch (IOException e) {
+ return "";
+ }
+ });
+
+ // waitFor() avec timeout
+ boolean completed = process.waitFor(60, TimeUnit.SECONDS);
+
+ // Renvoie exception après les 60 secondes accordées
+ if (!completed) {
+ process.destroyForcibly();
+ throw new HypervisorException("La commande a expiré (Timeout).");
+ }
+
+ String output = outputFuture.get(1, TimeUnit.SECONDS); // déjà terminé, récupération immédiate
+
+ // Commande échouée
+ int exitCode = process.exitValue();
+ if (exitCode != 0) {
+ throw new VBoxException(
+ "VBoxManage a retourné une erreur (code " + exitCode + ")",
+ exitCode,
+ output
+ );
+ }
+
+ return output;
+
+ } catch (IOException e) {
+ throw new HypervisorException("Erreur I/O lors de l'exécution de VBoxManage.", e);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new HypervisorException("Exécution de VBoxManage interrompue.", e);
+ } catch (ExecutionException e) {
+ throw new HypervisorException(
+ "Erreur lors de la lecture de la sortie de VBoxManage.", e.getCause());
+ } catch (TimeoutException e) {
+ // Ne dois pas arriver (process déjà terminé), mais on couvre le cas qd même
+ throw new HypervisorException(
+ "Délai dépassé lors de la récupération de la sortie de VBoxManage.", e);
+ }
+ }
+
+ /**
+ * Vérifie que VBoxManage est configuré avant toute commande.
+ *
+ * @return chemin VBoxManage prêt à être utilisé
+ * @throws HypervisorException si le chemin n'a pas été résolu
+ */
+ private String requireConfigured() throws HypervisorException {
+ if (vboxManagePath == null) {
+ throw new HypervisorException(
+ "VBoxManage est introuvable. "
+ + "Veuillez configurer son chemin dans les paramètres de l'application.");
+ }
+ return vboxManagePath;
+ }
+
+ /**
+ * Parcourt les emplacements standards de l'OS pour trouver VBoxManage.
+ *
+ * @return premier chemin valide trouvé, ou {@code null}
+ */
+ private static String detectVBoxManage() {
+ String os = System.getProperty("os.name", "").toLowerCase();
+
+ for (String candidate : buildCandidatePaths(os)) {
+ if (isVBoxManageExecutable(candidate)) {
+ return candidate;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Résout le chemin d'accès à VBoxManage depuis la configuration fournie,
+ * ou par auto-détection si le chemin configuré est absent ou invalide.
+ *
+ * @param config configuration applicative, peut être {@code null}
+ * @return chemin résolu vers VBoxManage, ou {@code null} si introuvable
+ */
+ private static String resolveVboxManagePath(AppConfig config) {
+ String configured = (config != null) ? config.getVboxManagePath() : null;
+
+ if (configured != null && !configured.isBlank() && isVBoxManageExecutable(configured)) {
+ return configured;
+ }
+
+ return detectVBoxManage();
+ }
+
+ /**
+ * Construit la liste des chemins candidats selon l'OS.
+ *
+ * @param osName nom de l'OS en minuscules
+ * @return chemins à tester, du plus probable au moins probable
+ */
+ private static List buildCandidatePaths(String osName) {
+ List candidates = new ArrayList<>();
+
+ if (osName.contains("win")) {
+ // Commande nue d'abord (si VirtualBox est dans le PATH Windows)
+ candidates.add("VBoxManage.exe");
+ candidates.add("C:\\Program Files\\Oracle\\VirtualBox\\VBoxManage.exe");
+ candidates.add("C:\\Program Files (x86)\\Oracle\\VirtualBox\\VBoxManage.exe");
+ } else {
+ // Linux / macOS (PATH d'abord, puis chemins typiques)
+ candidates.add("VBoxManage");
+ candidates.add("vboxmanage");
+ candidates.add("/usr/bin/VBoxManage");
+ candidates.add("/usr/local/bin/VBoxManage");
+ candidates.add("/opt/VirtualBox/VBoxManage");
+ }
+
+ return candidates;
+ }
+
+ /**
+ * Vérifie qu'un chemin pointe vers un VBoxManage fonctionnel en exécutant
+ * {@code --version}.
+ *
+ * La sortie est drainée pour éviter tout blocage du sous-processus.
+ *
+ * @param path chemin à tester
+ * @return {@code true} si VBoxManage répond avec un code de sortie 0
+ */
+ private static boolean isVBoxManageExecutable(String path) {
+ if (path == null || path.isBlank()) {
+ return false;
+ }
+ try {
+ ProcessBuilder pb = new ProcessBuilder(path, "--version");
+ pb.redirectErrorStream(true);
+ Process process = pb.start();
+ // Drainer la sortie pour ne pas bloquer le processus
+ process.getInputStream().transferTo(OutputStream.nullOutputStream());
+ // Pour éviter les freezes , on attend 5 secondes ou on tue le process
+ boolean complete = process.waitFor(5, TimeUnit.SECONDS);
+ if (!complete) {
+ process.destroyForcibly();
+ return false;
+ }
+ return process.exitValue() == 0;
+
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ /**
+ * Convertit un état VirtualBox brut en {@link VMStatus}.
+ *
+ * @param state valeur du champ {@code VMState} dans la sortie machinereadable
+ * @return statut correspondant, ou {@link VMStatus#UNKNOWN} si non reconnu
+ */
+ static VMStatus parseVMStatus(String state) {
+ if (state == null) {
+ return VMStatus.UNKNOWN;
+ }
+
+ // Normalisation pour éviter les problèmes de casse
+ String s = state.toLowerCase();
+
+ return switch (s) {
+ // ÉTATS ACTIFS
+ case "running",
+ "resuming" -> VMStatus.RUNNING;
+
+ // En pause
+ case "paused" -> VMStatus.PAUSED;
+
+ // ÉTATS ÉTEINTS (ou en transition vers éteint)
+ case "poweroff",
+ "poweredoff",
+ "aborted",
+ "stopping" -> VMStatus.POWERED_OFF;
+
+ // ÉTATS SAUVEGARDÉS (Hibernation)
+ case "saved",
+ "saving",
+ "suspended" -> VMStatus.SAVED;
+
+ // "starting" : La VM n'est pas encore utilisable.
+ case "starting" -> VMStatus.UNKNOWN;
+
+ // Tout le reste
+ // Inclut: restoring, snapshotting, teleporting, etc.
+ default -> VMStatus.UNKNOWN;
+ };
}
}
diff --git a/src/main/java/tg/cyberlabmanager/model/VMDescriptor.java b/src/main/java/tg/cyberlabmanager/model/VMDescriptor.java
new file mode 100644
index 0000000..e6232c1
--- /dev/null
+++ b/src/main/java/tg/cyberlabmanager/model/VMDescriptor.java
@@ -0,0 +1,13 @@
+package tg.cyberlabmanager.model;
+
+/**
+ * Descripteur minimal d'une machine virtuelle découverte auprès de VirtualBox.
+ *
+ * Ce record sert à transporter l'identité affichable d'une VM sans charger
+ * le modèle métier complet.
+ *
+ * @param name nom de la machine virtuelle
+ * @param uuid UUID VirtualBox de la machine virtuelle
+ */
+public record VMDescriptor(String name, String uuid) {
+}
diff --git a/src/test/java/tg/cyberlabmanager/hypervisor/VBoxExceptionTest.java b/src/test/java/tg/cyberlabmanager/hypervisor/VBoxExceptionTest.java
new file mode 100644
index 0000000..8f3227a
--- /dev/null
+++ b/src/test/java/tg/cyberlabmanager/hypervisor/VBoxExceptionTest.java
@@ -0,0 +1,22 @@
+package tg.cyberlabmanager.hypervisor;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class VBoxExceptionTest {
+ @Test
+ void shouldStoreExitCodeAndOutput() {
+ VBoxException exception = new VBoxException("Erreur VBoxManage", 1, "VBOX_E_INVALID_OBJECT_STATE");
+
+ assertEquals(1, exception.getExitCode());
+ assertEquals("VBOX_E_INVALID_OBJECT_STATE", exception.getOutput());
+ }
+
+ @Test
+ void shouldBeInstanceOfHypervisorException() {
+ VBoxException exception = new VBoxException("message", 2, "output");
+
+ assertEquals(HypervisorException.class, exception.getClass().getSuperclass());
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/tg/cyberlabmanager/hypervisor/VBoxOrchestratorTest.java b/src/test/java/tg/cyberlabmanager/hypervisor/VBoxOrchestratorTest.java
new file mode 100644
index 0000000..b2dcb07
--- /dev/null
+++ b/src/test/java/tg/cyberlabmanager/hypervisor/VBoxOrchestratorTest.java
@@ -0,0 +1,143 @@
+package tg.cyberlabmanager.hypervisor;
+
+import org.junit.jupiter.api.Test;
+import tg.cyberlabmanager.model.AppConfig;
+import tg.cyberlabmanager.model.VMDescriptor;
+import tg.cyberlabmanager.model.VMStatus;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+class VBoxOrchestratorTest {
+
+ @Test
+ void shouldParseVBoxManageListVMsOutputLine() {
+ List vms = VBoxOrchestrator.parseListVMsOutput(
+ "\"Cyberlab\" {b9c987d2-cc84-40c5-b523-748d401e7d7e}");
+
+ assertEquals(List.of(
+ new VMDescriptor("Cyberlab", "b9c987d2-cc84-40c5-b523-748d401e7d7e")
+ ), vms);
+ }
+
+ @Test
+ void shouldParseSeveralVBoxManageListVMsOutputLines() {
+ List vms = VBoxOrchestrator.parseListVMsOutput("""
+ "Cyberlab" {b9c987d2-cc84-40c5-b523-748d401e7d7e}
+ "Windows 11 Lab" {1A2B3C4D-1234-4567-89AB-ABCDEF123456}
+ """);
+
+ assertEquals(List.of(
+ new VMDescriptor("Cyberlab", "b9c987d2-cc84-40c5-b523-748d401e7d7e"),
+ new VMDescriptor("Windows 11 Lab", "1A2B3C4D-1234-4567-89AB-ABCDEF123456")
+ ), vms);
+ }
+
+ @Test
+ void shouldReturnEmptyListForBlankVBoxManageListVMsOutput() {
+ assertEquals(List.of(), VBoxOrchestrator.parseListVMsOutput(""));
+ }
+
+ @Test
+ void shouldReturnEmptyListForNullVBoxManageListVMsOutput() {
+ assertEquals(List.of(), VBoxOrchestrator.parseListVMsOutput(null));
+ }
+
+ @Test
+ void shouldIgnoreInvalidVBoxManageListVMsOutputLines() {
+ List vms = VBoxOrchestrator.parseListVMsOutput("""
+ invalid line
+ "Missing uuid"
+ "Bad uuid" {abc---test}
+ "Valid VM" {b9c987d2-cc84-40c5-b523-748d401e7d7e}
+ """);
+
+ assertEquals(List.of(
+ new VMDescriptor("Valid VM", "b9c987d2-cc84-40c5-b523-748d401e7d7e")
+ ), vms);
+ }
+
+ @Test
+ void shouldParseVmNamesContainingSpaces() {
+ List vms = VBoxOrchestrator.parseListVMsOutput(
+ "\"Windows 11 Analysis Lab\" {b9c987d2-cc84-40c5-b523-748d401e7d7e}");
+
+ assertEquals(List.of(
+ new VMDescriptor("Windows 11 Analysis Lab", "b9c987d2-cc84-40c5-b523-748d401e7d7e")
+ ), vms);
+ }
+
+ @Test
+ void shouldMapRunningVBoxStatesToRunningStatus() {
+ assertEquals(VMStatus.RUNNING, VBoxOrchestrator.parseVMStatus("running"));
+ assertEquals(VMStatus.RUNNING, VBoxOrchestrator.parseVMStatus("resuming"));
+ }
+
+ @Test
+ void shouldMapPoweredOffVBoxStatesToPoweredOffStatus() {
+ assertEquals(VMStatus.POWERED_OFF, VBoxOrchestrator.parseVMStatus("poweroff"));
+ assertEquals(VMStatus.POWERED_OFF, VBoxOrchestrator.parseVMStatus("poweredoff"));
+ assertEquals(VMStatus.POWERED_OFF, VBoxOrchestrator.parseVMStatus("aborted"));
+ assertEquals(VMStatus.POWERED_OFF, VBoxOrchestrator.parseVMStatus("stopping"));
+ }
+
+ @Test
+ void shouldMapSavedVBoxStatesToSavedStatus() {
+ assertEquals(VMStatus.SAVED, VBoxOrchestrator.parseVMStatus("saved"));
+ assertEquals(VMStatus.SAVED, VBoxOrchestrator.parseVMStatus("saving"));
+ assertEquals(VMStatus.SAVED, VBoxOrchestrator.parseVMStatus("suspended"));
+ }
+
+ @Test
+ void shouldMapPausedVBoxStatesToPausedStatus() {
+ assertEquals(VMStatus.PAUSED, VBoxOrchestrator.parseVMStatus("paused"));
+ }
+
+ @Test
+ void shouldMapUnknownVBoxStatesToUnknownStatus() {
+ assertEquals(VMStatus.UNKNOWN, VBoxOrchestrator.parseVMStatus(null));
+ assertEquals(VMStatus.UNKNOWN, VBoxOrchestrator.parseVMStatus("starting"));
+ assertEquals(VMStatus.UNKNOWN, VBoxOrchestrator.parseVMStatus("teleporting"));
+ assertEquals(VMStatus.UNKNOWN, VBoxOrchestrator.parseVMStatus("unexpected"));
+ }
+
+ @Test
+ void shouldIgnoreVBoxStatusCase() {
+ assertEquals(VMStatus.RUNNING, VBoxOrchestrator.parseVMStatus("RUNNING"));
+ assertEquals(VMStatus.POWERED_OFF, VBoxOrchestrator.parseVMStatus("PowerOff"));
+ assertEquals(VMStatus.SAVED, VBoxOrchestrator.parseVMStatus("Saved"));
+ }
+
+ @Test
+ void shouldNotBeReadyWhenVBoxManagePathIsNotConfigured() {
+ VBoxOrchestrator orchestrator = new VBoxOrchestrator(new AppConfig());
+ // AppConfig vide → vboxManagePath null après résolution
+ assertFalse(orchestrator.isReady());
+ }
+
+ @Test
+ void shouldParseRunningStatusFromShowVmInfoOutput() {
+ String output = """
+ name="Cyberlab"
+ VMState="running"
+ memory=2048
+ """;
+ assertEquals(VMStatus.RUNNING, VBoxOrchestrator.parseShowVmInfoOutput(output));
+ }
+
+ @Test
+ void shouldReturnUnknownStatusWhenVMStateLineIsAbsent() {
+ String output = """
+ name="Cyberlab"
+ memory=2048
+ """;
+ assertEquals(VMStatus.UNKNOWN, VBoxOrchestrator.parseShowVmInfoOutput(output));
+ }
+
+ @Test
+ void shouldReturnUnknownStatusForNullShowVmInfoOutput() {
+ assertEquals(VMStatus.UNKNOWN, VBoxOrchestrator.parseShowVmInfoOutput(null));
+ }
+}