From da8f8c708838d963040c0a2086e8c751c3fe0538 Mon Sep 17 00:00:00 2001 From: Ahmed Essam Date: Tue, 22 Sep 2026 11:55:32 +0300 Subject: [PATCH] [hcs] Fix issue with powering off after suspension --- .../hyperv_api/hcs_virtual_machine.cpp | 163 +++++++++--- .../backends/hyperv_api/hcs_virtual_machine.h | 4 + .../hcs_virtual_machine_exceptions.h | 5 + .../test_ut_hyperv_hcs_virtual_machine.cpp | 247 +++++++++++++++++- 4 files changed, 374 insertions(+), 45 deletions(-) diff --git a/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp b/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp index 0dc3ae3e61..f1c3cce054 100644 --- a/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp +++ b/src/platform/backends/hyperv_api/hcs_virtual_machine.cpp @@ -121,16 +121,20 @@ HCSVirtualMachine::HCSVirtualMachine(const std::string& network_guid, monitor(monitor) { const auto created_from_scratch = maybe_create_compute_system(); - const auto state = fetch_state_from_api(); + const auto compute_state = fetch_state_from_api(); mpl::debug(get_name(), "HCSVirtualMachine() > created_from_scratch: {}, state: {}", created_from_scratch, - state); + compute_state); // Reflect compute system's state - set_state(state); - HCSVirtualMachine::handle_state_update(); + auto prev_state = state; + set_state(compute_state); + + // Persist initial state even if unchanged + if (prev_state == this->state) + HCSVirtualMachine::handle_state_update(); } HCSVirtualMachine::~HCSVirtualMachine() @@ -142,8 +146,7 @@ HCSVirtualMachine::~HCSVirtualMachine() // Auto-suspend if running suspend(); // Persist previous VM state - state = VirtualMachine::State::running; - handle_state_update(); + set_state(State::running); }); } @@ -162,8 +165,7 @@ void HCSVirtualMachine::compute_system_event_callback(HCS_EVENT* event, void* co case hcs::HcsEventType::SystemExited: { mpl::info(vm->get_name(), "compute_system_event_callback() > SystemExited event received"); - vm->state = State::off; - vm->handle_state_update(); + vm->set_state(State::off); vm->termination_signal.signal(); } break; @@ -364,31 +366,36 @@ void HCSVirtualMachine::set_state(hcs::ComputeSystemState compute_system_state) return; } - const auto prev_state = state; switch (compute_system_state) { case hcs::ComputeSystemState::created: - state = State::off; + set_state(State::off); break; case hcs::ComputeSystemState::paused: - state = State::suspended; + mpl::debug(vm_name, "VM is paused but not completely suspended"); + set_state(State::suspended); break; case hcs::ComputeSystemState::running: - state = State::running; + set_state(State::running); break; case hcs::ComputeSystemState::saved_as_template: case hcs::ComputeSystemState::stopped: - state = has_saved_state_file() ? State::suspended : State::stopped; + set_state(has_saved_state_file() ? State::suspended : State::stopped); break; case hcs::ComputeSystemState::unknown: - state = State::unknown; + set_state(State::unknown); break; } +} - if (state == prev_state) +void HCSVirtualMachine::set_state(VirtualMachine::State new_state) +{ + if (state == new_state) return; - mpl::info(get_name(), "set_state() -> State changed from {} to {}", prev_state, state); + mpl::info(get_name(), "set_state() -> State changed from {} to {}", state, new_state); + state = new_state; + handle_state_update(); } void HCSVirtualMachine::start() @@ -400,8 +407,7 @@ void HCSVirtualMachine::start() mpl::debug(get_name(), "start() -> VM was not present, created from scratch"); const auto prev_state = state; - state = VirtualMachine::State::starting; - handle_state_update(); + set_state(VirtualMachine::State::starting); // Resume and start are the same thing in Multipass terms // Try to determine whether we need to resume or start here. const auto result = [&] { @@ -426,8 +432,7 @@ void HCSVirtualMachine::start() if (!result) { - state = prev_state; - handle_state_update(); + set_state(prev_state); throw StartComputeSystemException{"Could not start the VM: {}", result}; } else if (has_saved_state_file()) @@ -444,6 +449,7 @@ void HCSVirtualMachine::start() mpl::debug(get_name(), "start() -> result `{}`", result); } + void HCSVirtualMachine::shutdown(ShutdownPolicy shutdown_policy) { mpl::debug(get_name(), "shutdown() -> Shutting down, current state {}", state); @@ -458,6 +464,10 @@ void HCSVirtualMachine::shutdown(ShutdownPolicy shutdown_policy) return; } + // Ensure that the ssh session is dropped at the end of this function, even if shutdown + // fails, since it means that the VM is in some sort of error state. + auto drop_ssh_session_sg = sg::make_scope_guard([this]() noexcept { drop_ssh_session(); }); + switch (shutdown_policy) { case ShutdownPolicy::Powerdown: @@ -468,23 +478,36 @@ void HCSVirtualMachine::shutdown(ShutdownPolicy shutdown_policy) { // Fall back to SSH shutdown. ssh_exec("sudo shutdown -h now"); - drop_ssh_session(); } break; case ShutdownPolicy::Halt: case ShutdownPolicy::Poweroff: mpl::debug(get_name(), "shutdown() -> Requested halt/poweroff, initiating forceful shutdown"); + + // FIXME: There is a rare case where suspend fails, and fails to terminate + // the VM as well. In this case the VM will be "paused". This is not handled + // for now. + if (state == State::suspended) + { + if (const auto ec = remove_saved_state_file_if_exists(); ec) + throw ShutdownComputeSystemException("Could not remove state file '{}': {}", + get_saved_state_file_path(), + ec); + update_current_state(); + return; + } // These are non-graceful variants. Just terminate the system immediately. const auto r = HCS().terminate_compute_system(hcs_system); + if (!r) + throw ShutdownComputeSystemException("Could not terminate VM `{}`: {}", get_name(), r); mpl::debug(get_name(), "shutdown -> terminate_compute_system result: {}", r.code); - drop_ssh_session(); break; } // We need to wait here. if (!termination_signal.wait_for(vm_shutdown_timeout)) - throw std::runtime_error("timed out waiting for VM shutdown to complete"); + throw ShutdownComputeSystemException("timed out waiting for VM shutdown to complete"); switch (auto s = current_state()) { @@ -493,7 +516,7 @@ void HCSVirtualMachine::shutdown(ShutdownPolicy shutdown_policy) case VirtualMachine::State::suspended: break; default: - mpl::warn(get_name(), "shutdown -> VM is not in stopped state after termination: {}", s); + throw ShutdownComputeSystemException("VM is not in stopped state after termination: {}", s); break; } } @@ -502,34 +525,74 @@ void HCSVirtualMachine::suspend() { mpl::debug(get_name(), "suspend() -> Suspending, current state {}", state); - if (const auto pause_result = HCS().pause_compute_system(hcs_system)) + if (const auto pause_result = HCS().pause_compute_system(hcs_system); !pause_result) + throw SaveComputeSystemException{"Could not pause VM for suspend: {}", pause_result}; + + const auto save_result = HCS().save_compute_system(hcs_system, get_saved_state_file_path()); + if (save_result) { - // Pause succeeded. We can suspend to disk now - if (const auto& r = HCS().save_compute_system(hcs_system, get_saved_state_file_path()); r) - { - // Save succeeded. Now, it's safe to terminate the system. - shutdown(ShutdownPolicy::Poweroff); - } - else - throw SaveComputeSystemException{"Could not save the virtual machine state for VM `{}` " - "to the disk for suspend. Error details: {}", - get_name(), - r}; + // Save succeeded. Now, it's safe to terminate the system. + return shutdown(ShutdownPolicy::Poweroff); } - else + + // If saved failed, we try to resume + if (const auto ec = remove_saved_state_file_if_exists(); ec) { - throw SaveComputeSystemException{"Could not pause VM for suspend: {}", pause_result}; + // FIXME: This leaves the class in an undefined state. + // Although the suspend file is corrupt, terminating the VM will still + // transition it to the suspended state. + mpl::warn(get_name(), + "Could not remove state file after partial write '{}': {}", + get_saved_state_file_path(), + ec); } - set_state(fetch_state_from_api()); - handle_state_update(); + const auto resume_result = HCS().resume_compute_system(hcs_system); + if (resume_result) + { + update_current_state(); + throw SaveComputeSystemException{ + "Could not save the virtual machine state for VM `{}` to disk for suspend; the VM " + "was resumed. Error details: {}", + get_name(), + save_result}; + } + + // If resuming failed as well, we try to force shutdown. + try + { + shutdown(ShutdownPolicy::Poweroff); + } + catch (const std::exception& exception) + { + // If we're here, then everything failed. + throw SaveComputeSystemException{ + "VM `{}` is not responding correctly: saving its state, resuming it, and " + "terminating it all failed. Save error: {}; resume error: {}.\n{}", + get_name(), + save_result, + resume_result, + exception.what()}; + } + + // We managed to at least shutdown the VM. + throw SaveComputeSystemException{ + "Could not save the virtual machine state for VM `{}` to disk for suspend or " + "resume it; the VM was terminated. Save error: {}; resume error: {}", + get_name(), + save_result, + resume_result}; } HCSVirtualMachine::State HCSVirtualMachine::current_state() { - set_state(fetch_state_from_api()); + update_current_state(); return state; } +void HCSVirtualMachine::update_current_state() +{ + set_state(fetch_state_from_api()); +} int HCSVirtualMachine::ssh_port() { return default_ssh_port; @@ -687,4 +750,22 @@ std::shared_ptr HCSVirtualMachine::make_specific_snapshot(const QStrin description); } +std::error_code HCSVirtualMachine::remove_saved_state_file_if_exists() +{ + if (has_saved_state_file()) + { + mpl::trace(get_name(), "Saved state file exists, attempting to remove"); + std::error_code ec{}; + if (!MP_FILEOPS.remove(get_saved_state_file_path(), ec)) + { + // FIXME: If the VM is stopped or terminated, it will still be reported as + // suspended because the saved-state file exists. + mpl::warn(get_name(), "Could not remove the saved state file, error: {}", ec); + return ec; + } + } + + return {}; +} + } // namespace multipass::hyperv diff --git a/src/platform/backends/hyperv_api/hcs_virtual_machine.h b/src/platform/backends/hyperv_api/hcs_virtual_machine.h index 2af7c347af..ddf8086e65 100644 --- a/src/platform/backends/hyperv_api/hcs_virtual_machine.h +++ b/src/platform/backends/hyperv_api/hcs_virtual_machine.h @@ -28,6 +28,7 @@ #include #include +#include struct HCS_EVENT; @@ -105,6 +106,8 @@ struct HCSVirtualMachine : public BaseVirtualMachine [[nodiscard]] hcs::ComputeSystemState fetch_state_from_api() const; void set_state(hcs::ComputeSystemState state); + void set_state(State state); + void update_current_state(); /** * Create the compute system if it's not already present. @@ -123,6 +126,7 @@ struct HCSVirtualMachine : public BaseVirtualMachine [[nodiscard]] std::filesystem::path get_runtime_state_file_path() const; [[nodiscard]] std::filesystem::path get_saved_state_file_path() const; [[nodiscard]] bool has_saved_state_file() const; + std::error_code remove_saved_state_file_if_exists(); void grant_access_to_scsi_device(const hcs::HcsScsiDevice& device) const; void grant_access_to_paths(std::list paths) const; diff --git a/src/platform/backends/hyperv_api/hcs_virtual_machine_exceptions.h b/src/platform/backends/hyperv_api/hcs_virtual_machine_exceptions.h index b76744c0ec..fa4ee816fd 100644 --- a/src/platform/backends/hyperv_api/hcs_virtual_machine_exceptions.h +++ b/src/platform/backends/hyperv_api/hcs_virtual_machine_exceptions.h @@ -92,4 +92,9 @@ struct SaveComputeSystemException : public FormattedExceptionBase<> using FormattedExceptionBase::FormattedExceptionBase; }; +struct ShutdownComputeSystemException : public FormattedExceptionBase<> +{ + using FormattedExceptionBase::FormattedExceptionBase; +}; + } // namespace multipass::hyperv diff --git a/tests/unit/hyperv_api/test_ut_hyperv_hcs_virtual_machine.cpp b/tests/unit/hyperv_api/test_ut_hyperv_hcs_virtual_machine.cpp index 8f43526a98..9d678731bf 100644 --- a/tests/unit/hyperv_api/test_ut_hyperv_hcs_virtual_machine.cpp +++ b/tests/unit/hyperv_api/test_ut_hyperv_hcs_virtual_machine.cpp @@ -476,6 +476,116 @@ TEST_F(HyperVHCSVirtualMachine_UnitTests, vm_shutdown_halt) // --------------------------------------------------------- +TEST_F(HyperVHCSVirtualMachine_UnitTests, vm_shutdown_poweroff_suspended_removes_saved_state) +{ + auto [mock_file_ops, guard] = mpt::MockFileOps::inject(); + default_open_success(); + + bool state_file_removed = false; + + auto is_suspend_state_file = [](const std::filesystem::path& p) { + return p.string().find(".SavedState.vmrs") != std::string::npos; + }; + + EXPECT_CALL(mock_hcs, get_compute_system_state(Eq(mock_handle), _)) + .WillRepeatedly([&](const mhv::hcs::HcsSystemHandle&, hcs_system_state_t& state) { + state = hcs_system_state_t::stopped; + return hcs_op_result_t{0, L""}; + }); + + EXPECT_CALL(*mock_file_ops, exists(::testing::A())) + .WillRepeatedly([&](const std::filesystem::path& p) { return !state_file_removed; }); + + EXPECT_CALL(*mock_file_ops, remove(::testing::A(), _)) + .WillOnce([&](const std::filesystem::path& p, std::error_code& err) { + if (is_suspend_state_file(p)) + { + state_file_removed = true; + } + return true; + }); + + std::shared_ptr uut{nullptr}; + ASSERT_NO_THROW(uut = construct_vm()); + + ASSERT_EQ(uut->state, multipass::VirtualMachine::State::suspended); + + uut->shutdown(multipass::VirtualMachine::ShutdownPolicy::Poweroff); + + EXPECT_EQ(uut->state, multipass::VirtualMachine::State::stopped); + EXPECT_EQ(state_file_removed, true); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSVirtualMachine_UnitTests, + vm_shutdown_poweroff_suspended_saved_state_removal_failure_throws) +{ + auto [mock_file_ops, guard] = mpt::MockFileOps::inject(); + default_open_success(); + + bool state_file_removal_attempted = false; + + auto is_suspend_state_file = [](const std::filesystem::path& p) { + return p.string().find(".SavedState.vmrs") != std::string::npos; + }; + + EXPECT_CALL(mock_hcs, get_compute_system_state(Eq(mock_handle), _)) + .WillRepeatedly([&](const mhv::hcs::HcsSystemHandle&, hcs_system_state_t& state) { + state = hcs_system_state_t::stopped; + return hcs_op_result_t{0, L""}; + }); + + EXPECT_CALL(*mock_file_ops, exists(::testing::A())) + .WillRepeatedly(Return(true)); + + EXPECT_CALL(*mock_file_ops, remove(::testing::A(), _)) + .WillOnce([&](const std::filesystem::path& p, std::error_code& err) { + if (is_suspend_state_file(p)) + { + state_file_removal_attempted = true; + err = std::make_error_code(std::errc::permission_denied); + } + return false; + }); + + std::shared_ptr uut{nullptr}; + ASSERT_NO_THROW(uut = construct_vm()); + + ASSERT_EQ(uut->state, multipass::VirtualMachine::State::suspended); + + EXPECT_THROW(uut->shutdown(multipass::VirtualMachine::ShutdownPolicy::Poweroff), + mhv::ShutdownComputeSystemException); + EXPECT_EQ(uut->state, multipass::VirtualMachine::State::suspended); + EXPECT_EQ(state_file_removal_attempted, true); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSVirtualMachine_UnitTests, vm_shutdown_termination_failure_throws) +{ + default_open_success(); + + EXPECT_CALL(mock_hcs, terminate_compute_system(Eq(mock_handle))) + .WillOnce(Return(hcs_op_result_t{1, L"termination failed"})); + + EXPECT_CALL(mock_hcs, get_compute_system_state(Eq(mock_handle), _)) + .WillOnce( + DoAll(SetArgReferee<1>(hcs_system_state_t::running), Return(hcs_op_result_t{0, L""}))); + + std::shared_ptr uut{nullptr}; + ASSERT_NO_THROW(uut = construct_vm()); + EXPECT_THROW(uut->shutdown(multipass::VirtualMachine::ShutdownPolicy::Poweroff), + mhv::ShutdownComputeSystemException); + + // Change the state to stopped to prevent auto suspension + EXPECT_CALL(mock_hcs, get_compute_system_state(Eq(mock_handle), _)) + .WillOnce( + DoAll(SetArgReferee<1>(hcs_system_state_t::stopped), Return(hcs_op_result_t{0, L""}))); +} + +// --------------------------------------------------------- + TEST_F(HyperVHCSVirtualMachine_UnitTests, vm_suspend_success) { auto [mock_file_ops, guard] = mpt::MockFileOps::inject(); @@ -498,6 +608,119 @@ TEST_F(HyperVHCSVirtualMachine_UnitTests, vm_suspend_success) EXPECT_EQ(uut->state, multipass::VirtualMachine::State::suspended); } +TEST_F(HyperVHCSVirtualMachine_UnitTests, vm_suspend_save_failure_resumes_and_throws) +{ + default_open_success(); + + EXPECT_CALL(mock_hcs, get_compute_system_state(Eq(mock_handle), _)) + .Times(2) + .WillRepeatedly( + DoAll(SetArgReferee<1>(hcs_system_state_t::running), Return(hcs_op_result_t{0, L""}))); + EXPECT_CALL(mock_hcs, pause_compute_system(Eq(mock_handle))) + .WillOnce(Return(hcs_op_result_t{0, L""})); + EXPECT_CALL(mock_hcs, save_compute_system(Eq(mock_handle), _)) + .WillOnce(Return(hcs_op_result_t{1, L"save failed"})); + EXPECT_CALL(mock_hcs, resume_compute_system(Eq(mock_handle))) + .WillOnce(Return(hcs_op_result_t{0, L""})); + + std::shared_ptr uut{nullptr}; + ASSERT_NO_THROW(uut = construct_vm()); + + EXPECT_THROW(uut->suspend(), mhv::SaveComputeSystemException); + + // Change the state to stopped to prevent auto suspension + EXPECT_CALL(mock_hcs, get_compute_system_state(Eq(mock_handle), _)) + .WillOnce( + DoAll(SetArgReferee<1>(hcs_system_state_t::stopped), Return(hcs_op_result_t{0, L""}))); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSVirtualMachine_UnitTests, vm_suspend_save_and_resume_failure_terminates_and_throws) +{ + default_open_success(); + + EXPECT_CALL(mock_hcs, get_compute_system_state(Eq(mock_handle), _)) + .WillOnce( + DoAll(SetArgReferee<1>(hcs_system_state_t::running), Return(hcs_op_result_t{0, L""}))) + .WillOnce( + DoAll(SetArgReferee<1>(hcs_system_state_t::stopped), Return(hcs_op_result_t{0, L""}))); + EXPECT_CALL(mock_hcs, pause_compute_system(Eq(mock_handle))) + .WillOnce(Return(hcs_op_result_t{0, L""})); + EXPECT_CALL(mock_hcs, save_compute_system(Eq(mock_handle), _)) + .WillOnce(Return(hcs_op_result_t{1, L"save failed"})); + EXPECT_CALL(mock_hcs, resume_compute_system(Eq(mock_handle))) + .WillOnce(Return(hcs_op_result_t{1, L"resume failed"})); + EXPECT_CALL(mock_hcs, terminate_compute_system(Eq(mock_handle))); + + std::shared_ptr uut{nullptr}; + ASSERT_NO_THROW(uut = construct_vm()); + + EXPECT_THROW(uut->suspend(), mhv::SaveComputeSystemException); + EXPECT_EQ(uut->state, multipass::VirtualMachine::State::stopped); + + // Change the state to stopped to prevent auto suspension + EXPECT_CALL(mock_hcs, get_compute_system_state(Eq(mock_handle), _)) + .WillOnce( + DoAll(SetArgReferee<1>(hcs_system_state_t::stopped), Return(hcs_op_result_t{0, L""}))); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSVirtualMachine_UnitTests, vm_suspend_save_resume_and_terminate_failure_throws) +{ + default_open_success(); + + EXPECT_CALL(mock_hcs, get_compute_system_state(Eq(mock_handle), _)) + .WillOnce( + DoAll(SetArgReferee<1>(hcs_system_state_t::running), Return(hcs_op_result_t{0, L""}))); + EXPECT_CALL(mock_hcs, pause_compute_system(Eq(mock_handle))) + .WillOnce(Return(hcs_op_result_t{0, L""})); + EXPECT_CALL(mock_hcs, save_compute_system(Eq(mock_handle), _)) + .WillOnce(Return(hcs_op_result_t{1, L"save failed"})); + EXPECT_CALL(mock_hcs, resume_compute_system(Eq(mock_handle))) + .WillOnce(Return(hcs_op_result_t{1, L"resume failed"})); + EXPECT_CALL(mock_hcs, terminate_compute_system(Eq(mock_handle))) + .WillOnce(Return(hcs_op_result_t{1, L"termination failed"})); + + std::shared_ptr uut{nullptr}; + ASSERT_NO_THROW(uut = construct_vm()); + + EXPECT_THROW(uut->suspend(), mhv::SaveComputeSystemException); + + // Change the state to stopped to prevent auto suspension + EXPECT_CALL(mock_hcs, get_compute_system_state(Eq(mock_handle), _)) + .WillOnce( + DoAll(SetArgReferee<1>(hcs_system_state_t::stopped), Return(hcs_op_result_t{0, L""}))); +} + +// --------------------------------------------------------- + +TEST_F(HyperVHCSVirtualMachine_UnitTests, vm_suspend_termination_failure_throws) +{ + default_open_success(); + + EXPECT_CALL(mock_hcs, get_compute_system_state(Eq(mock_handle), _)) + .WillOnce( + DoAll(SetArgReferee<1>(hcs_system_state_t::running), Return(hcs_op_result_t{0, L""}))); + EXPECT_CALL(mock_hcs, pause_compute_system(Eq(mock_handle))) + .WillOnce(Return(hcs_op_result_t{0, L""})); + EXPECT_CALL(mock_hcs, save_compute_system(Eq(mock_handle), _)) + .WillOnce(Return(hcs_op_result_t{0, L""})); + EXPECT_CALL(mock_hcs, terminate_compute_system(Eq(mock_handle))) + .WillOnce(Return(hcs_op_result_t{1, L"termination failed"})); + + std::shared_ptr uut{nullptr}; + ASSERT_NO_THROW(uut = construct_vm()); + + EXPECT_THROW(uut->suspend(), mhv::ShutdownComputeSystemException); + + // Change the state to stopped to prevent auto suspension + EXPECT_CALL(mock_hcs, get_compute_system_state(Eq(mock_handle), _)) + .WillOnce( + DoAll(SetArgReferee<1>(hcs_system_state_t::stopped), Return(hcs_op_result_t{0, L""}))); +} + // --------------------------------------------------------- TEST_F(HyperVHCSVirtualMachine_UnitTests, vm_suspend_on_destruction_persists_running_state) @@ -712,21 +935,37 @@ TEST_F(HyperVHCSVirtualMachine_UnitTests, management_ipv4_returns_empty_without_ // --------------------------------------------------------- -TEST_F(HyperVHCSVirtualMachine_UnitTests, update_state) +TEST_F(HyperVHCSVirtualMachine_UnitTests, update_state_calls_persist_state_only_on_change) { default_open_success(); api_state = hcs_system_state_t::paused; mpt::MockVMStatusMonitor mock_monitor{}; + EXPECT_CALL(mock_hcs, get_compute_system_state(Eq(mock_handle), _)) + .WillOnce( + DoAll(SetArgReferee<1>(hcs_system_state_t::running), Return(hcs_op_result_t{0, L""}))) + .WillOnce( + DoAll(SetArgReferee<1>(hcs_system_state_t::running), Return(hcs_op_result_t{0, L""}))) + .WillOnce( + DoAll(SetArgReferee<1>(hcs_system_state_t::running), Return(hcs_op_result_t{0, L""}))) + .WillRepeatedly( + DoAll(SetArgReferee<1>(hcs_system_state_t::stopped), Return(hcs_op_result_t{0, L""}))); + + InSequence sequence; + EXPECT_CALL( + mock_monitor, + persist_state_for(Eq(dummy_vm_name), Eq(multipass::VirtualMachine::State::running))); EXPECT_CALL( mock_monitor, - persist_state_for(Eq(dummy_vm_name), Eq(multipass::VirtualMachine::State::suspended))) - .Times(2); + persist_state_for(Eq(dummy_vm_name), Eq(multipass::VirtualMachine::State::stopped))); std::shared_ptr uut{nullptr}; ASSERT_NO_THROW(uut = construct_vm(&mock_monitor)); - uut->handle_state_update(); + EXPECT_EQ(uut->current_state(), multipass::VirtualMachine::State::running); + EXPECT_EQ(uut->current_state(), multipass::VirtualMachine::State::running); + EXPECT_EQ(uut->current_state(), multipass::VirtualMachine::State::stopped); + EXPECT_EQ(uut->current_state(), multipass::VirtualMachine::State::stopped); } // ---------------------------------------------------------