diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6ff183d --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ + + +# IDE +.idea/ +.vs/ diff --git a/solution/cpp/.gitignore b/solution/cpp/.gitignore new file mode 100644 index 0000000..ce5fc10 --- /dev/null +++ b/solution/cpp/.gitignore @@ -0,0 +1,12 @@ +# CMake build directories +build/ +cmake-build-*/ +out/ + +# CMake cache and generated files +CMakeCache.txt +CMakeFiles/ +cmake_install.cmake +Makefile +*.cmake +!CMakeLists.txt \ No newline at end of file diff --git a/solution/cpp/CMakeLists.txt b/solution/cpp/CMakeLists.txt new file mode 100644 index 0000000..4ebcab2 --- /dev/null +++ b/solution/cpp/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.20) +project(AirlockProblem LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 23) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +# Add Debug build by default if not specified +if(NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE Debug) +endif() + +# Fetch GTest +include(FetchContent) +FetchContent_Declare( + googletest + URL https://github.com/google/googletest/archive/refs/tags/v1.15.2.tar.gz +) +# For Windows: Prevent overriding the parent project's compiler/linker settings +set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) +FetchContent_MakeAvailable(googletest) + +# Source files +set(SOURCES + src/CAirLock.cpp + src/CAstronaut.cpp +) + +# Main executable +add_executable(airlock_main src/main.cpp ${SOURCES}) +target_include_directories(airlock_main PRIVATE src) + +# Tests +enable_testing() +add_executable(airlock_tests tests/tests.cpp ${SOURCES}) +target_include_directories(airlock_tests PRIVATE src) +target_link_libraries(airlock_tests GTest::gtest_main) + +include(GoogleTest) +gtest_discover_tests(airlock_tests) diff --git a/solution/cpp/README.md b/solution/cpp/README.md new file mode 100644 index 0000000..9e7c401 --- /dev/null +++ b/solution/cpp/README.md @@ -0,0 +1,5 @@ +# C++ Solution + +This solution explores the way of using std::mutex and lock_guards. +It is a simple implementation of the airlock problem. +It uses the C++23 standard and follows [my coding style](https://gist.github.com/AbsintheScripting/4f2be73c91fc49fc6bc2cefbb2a52895#file-code_style-md). \ No newline at end of file diff --git a/solution/cpp/src/CAirLock.cpp b/solution/cpp/src/CAirLock.cpp new file mode 100644 index 0000000..6cd1325 --- /dev/null +++ b/solution/cpp/src/CAirLock.cpp @@ -0,0 +1,161 @@ +#include "CAirLock.h" +// STL headers +#include +#include +#include +#include + +std::string CAirLock::GetChamberStateName(const EChamberState chamber_state) +{ + switch (chamber_state) + { + case EChamberState::EMPTY: + return "empty"; + case EChamberState::PRESSURIZED: + return "pressurized"; + case EChamberState::CHANGING_STATE: + return "cycling"; + default: + return "unknown"; + } +} + +std::string CAirLock::GetDoorName(const EDoor door) +{ + switch (door) + { + case EDoor::OUTSIDE: + return "outside"; + case EDoor::INSIDE: + return "inside"; + default: + return "unknown"; + } +} + +CAirLock::CAirLock() + : bIsOutsideDoorOpen(false), + bIsInsideDoorOpen(false), + chamberState(EChamberState::EMPTY), + bInsideChamberRequest(false) +{} + +bool CAirLock::TryOpenDoor(const EDoor door_location) +{ + bool bResult = false; + { // CRITICAL SECTION + std::lock_guard lock(mutexLock); + + switch (door_location) + { + case EDoor::OUTSIDE: + if (chamberState == EChamberState::EMPTY && !bIsInsideDoorOpen) + { + bIsOutsideDoorOpen = true; + bInsideChamberRequest = false; + bResult = true; + } + break; + case EDoor::INSIDE: + if (chamberState == EChamberState::PRESSURIZED && !bIsOutsideDoorOpen) + { + bIsInsideDoorOpen = true; + bInsideChamberRequest = false; + bResult = true; + } + break; + } + } // CRITICAL SECTION END + + return bResult; +} + +void CAirLock::CloseDoor(const EDoor door_location) +{ // CRITICAL SECTION + std::lock_guard lock(mutexLock); + + switch (door_location) + { + case EDoor::OUTSIDE: + bIsOutsideDoorOpen = false; + break; + case EDoor::INSIDE: + bIsInsideDoorOpen = false; + break; + } +} // CRITICAL SECTION END + +bool CAirLock::TryCycleChamber(const ECycleLocation cycle_location) +{ + auto target_state = EChamberState::EMPTY; + + { // CRITICAL SECTION + std::lock_guard lock(mutexLock); + + if (bIsOutsideDoorOpen || bIsInsideDoorOpen) + { + return false; + } + + // Logic: + // - If inside (CHAMBER), always allowed. + // - If outside (OUTSIDE/INSIDE in space/station), only allowed if no one requested cycle from inside the chamber. + if (cycle_location == ECycleLocation::OUTSIDE && bInsideChamberRequest) + { + return false; + } + + if (chamberState == EChamberState::EMPTY) + { + target_state = EChamberState::PRESSURIZED; + } + else if (chamberState == EChamberState::PRESSURIZED) + { + target_state = EChamberState::EMPTY; + } + else + { + return false; + } + + // Set inside request flag if cycle is from inside + if (cycle_location == ECycleLocation::INSIDE) + { + bInsideChamberRequest = true; + } + + // chamber is cycling + std::cout << std::format("[AirLock] Chamber is cycling from {} to {} state.\n", + GetChamberStateName(chamberState), GetChamberStateName(target_state)); + chamberState = EChamberState::CHANGING_STATE; + } // CRITICAL SECTION END + + // Simulate pressure change + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + { // CRITICAL SECTION + std::lock_guard lock(mutexLock); + // chamber is ready + chamberState = target_state; + std::cout << std::format("[AirLock] Chamber cycled to {} state.\n", + GetChamberStateName(chamberState)); + } // CRITICAL SECTION END + + return true; +} + +bool CAirLock::IsDoorOpen(const EDoor door_location) const +{ // CRITICAL SECTION + std::lock_guard lock(mutexLock); + if (door_location == EDoor::OUTSIDE) + { + return bIsOutsideDoorOpen; + } + return bIsInsideDoorOpen; +} // CRITICAL SECTION END + +CAirLock::EChamberState CAirLock::GetChamberState() const +{ // CRITICAL SECTION + std::lock_guard lock(mutexLock); + return chamberState; +} // CRITICAL SECTION END diff --git a/solution/cpp/src/CAirLock.h b/solution/cpp/src/CAirLock.h new file mode 100644 index 0000000..1bccbd4 --- /dev/null +++ b/solution/cpp/src/CAirLock.h @@ -0,0 +1,59 @@ +#pragma once +// STL headers +#include +#include +#include + +/** + * CAirLock + * Handles the airlock state and enforces safety rules. + */ +class CAirLock +{ +public: + enum class EChamberState + { + EMPTY, + PRESSURIZED, + CHANGING_STATE + }; + + enum class EDoor + { + OUTSIDE, + INSIDE + }; + + enum class ECycleLocation + { + OUTSIDE, // Outside of the airlock (e.g., from space or station) + INSIDE // Inside the airlock chamber + }; + + static std::string GetChamberStateName(EChamberState chamber_state); + static std::string GetDoorName(EDoor door); + + CAirLock(); + + [[nodiscard]] + bool TryOpenDoor(EDoor door_location); + + void CloseDoor(EDoor door_location); + + [[nodiscard]] + bool TryCycleChamber(ECycleLocation cycle_location); + + [[nodiscard]] + bool IsDoorOpen(EDoor door_location) const; + + [[nodiscard]] + EChamberState GetChamberState() const; + +private: + mutable std::mutex mutexLock; + + bool bIsOutsideDoorOpen; + bool bIsInsideDoorOpen; + EChamberState chamberState; + bool bInsideChamberRequest; +}; diff --git a/solution/cpp/src/CAstronaut.cpp b/solution/cpp/src/CAstronaut.cpp new file mode 100644 index 0000000..18e8ee9 --- /dev/null +++ b/solution/cpp/src/CAstronaut.cpp @@ -0,0 +1,120 @@ +#include "CAstronaut.h" +// STL headers +#include +#include +#include + +std::string CAstronaut::GetLocationName(const ELocation location) +{ + std::string result = "error"; + switch (location) + { + case ELocation::OUTSIDE: + result = "outside"; + break; + case ELocation::INSIDE: + result = "inside"; + break; + case ELocation::CHAMBER: + result = "chamber"; + break; + } + return result; +} + +CAstronaut::CAstronaut(std::string name, const ELocation starting_location, CAirLock& airlock_ref) + : name(std::move(name)), + location(starting_location), + airlock(airlock_ref) +{} + +bool CAstronaut::TryOpenDoor(const CAirLock::EDoor door_location) const +{ + if (IsDoorOpen(door_location)) + { + std::cout << std::format("[{}] The {} door was already open.\n", name, CAirLock::GetDoorName(door_location)); + return true; + } + std::cout << std::format("[{}] Trying to open the {} door...\n", name, CAirLock::GetDoorName(door_location)); + // Simulate opening door + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + if (IsDoorOpen(door_location)) + { + std::cout << std::format("[{}] Someone else opened the door.\n", name, CAirLock::GetDoorName(door_location)); + return true; + } + const bool bResult = airlock.TryOpenDoor(door_location); + if (bResult) + std::cout << std::format("[{}] Opened {} door.\n", name, CAirLock::GetDoorName(door_location)); + else + std::cout << std::format("[{}] Could not open {} door.\n", name, CAirLock::GetDoorName(door_location)); + return bResult; +} + +void CAstronaut::CloseDoor(const CAirLock::EDoor door_location) const +{ + if (!IsDoorOpen(door_location)) + { + std::cout << std::format("[{}] The {} door was already closed.\n", name, CAirLock::GetDoorName(door_location)); + return; + } + std::cout << std::format("[{}] Closing {} door...\n", name, CAirLock::GetDoorName(door_location)); + // Simulate closing door + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + if (!IsDoorOpen(door_location)) + { + std::cout << std::format("[{}] Someone else closed the door.\n", name, CAirLock::GetDoorName(door_location)); + return; + } + std::cout << std::format("[{}] Closed the {} door.\n", name, CAirLock::GetDoorName(door_location)); + airlock.CloseDoor(door_location); +} + +bool CAstronaut::TryCycleAirlock() const +{ + std::cout << std::format("[{}] Trying to cycle airlock from {}...\n", name, GetLocationName(location)); + const CAirLock::ECycleLocation cycle_location = (location == ELocation::CHAMBER) + ? CAirLock::ECycleLocation::INSIDE // in chamber + : CAirLock::ECycleLocation::OUTSIDE; // not in chamber + const bool bResult = airlock.TryCycleChamber(cycle_location); + if (bResult && cycle_location == CAirLock::ECycleLocation::OUTSIDE) + std::cout << std::format("[{}] Cycled airlock to match entry door requirement.\n", name); + else if (bResult && cycle_location == CAirLock::ECycleLocation::INSIDE) + std::cout << std::format("[{}] Cycled airlock. Safe to exit now.\n", name); + else + std::cout << std::format("[{}] Could not cycle airlock.\n", name); + + return bResult; +} + +bool CAstronaut::IsDoorOpen(const CAirLock::EDoor door_location) const +{ + return airlock.IsDoorOpen(door_location); +} + +CAstronaut::ELocation CAstronaut::GetLocation() const +{ + return location; +} + +std::string CAstronaut::GetName() const +{ + return name; +} + +bool CAstronaut::IsChamberSafeToExit(const ELocation target_location) const +{ + const CAirLock::EChamberState chamberState = airlock.GetChamberState(); + return + chamberState == CAirLock::EChamberState::PRESSURIZED && target_location == ELocation::INSIDE + || chamberState == CAirLock::EChamberState::EMPTY && target_location == ELocation::OUTSIDE; +} + +void CAstronaut::MoveTo(const ELocation new_location) +{ + std::cout << std::format("[{}] Moving to {}...\n", name, GetLocationName(new_location)); + // Simulate moving to new location + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + location = new_location; + std::cout << std::format("[{}] Reached {}.\n", name, GetLocationName(new_location)); +} diff --git a/solution/cpp/src/CAstronaut.h b/solution/cpp/src/CAstronaut.h new file mode 100644 index 0000000..08ed94f --- /dev/null +++ b/solution/cpp/src/CAstronaut.h @@ -0,0 +1,52 @@ +#pragma once +// STL headers +#include + +// Project headers +#include "CAirLock.h" + +/** + * CAstronaut + * Represents an astronaut that can interact with the airlock. + */ +class CAstronaut +{ +public: + enum class ELocation + { + OUTSIDE, + INSIDE, + CHAMBER + }; + + static std::string GetLocationName(ELocation location); + + CAstronaut(std::string name, ELocation starting_location, CAirLock& airlock_ref); + + [[nodiscard]] + bool TryOpenDoor(CAirLock::EDoor door_location) const; + + void CloseDoor(CAirLock::EDoor door_location) const; + + [[nodiscard]] + bool TryCycleAirlock() const; + + [[nodiscard]] + bool IsDoorOpen(CAirLock::EDoor door_location) const; + + [[nodiscard]] + ELocation GetLocation() const; + + [[nodiscard]] + std::string GetName() const; + + [[nodiscard]] + bool IsChamberSafeToExit(ELocation target_location) const; + + void MoveTo(ELocation new_location); + +private: + std::string name; + ELocation location; + CAirLock& airlock; +}; diff --git a/solution/cpp/src/main.cpp b/solution/cpp/src/main.cpp new file mode 100644 index 0000000..744e0e7 --- /dev/null +++ b/solution/cpp/src/main.cpp @@ -0,0 +1,79 @@ +// STL headers +#include +#include +#include +#include +#include + +// Project headers +#include "CAirLock.h" +#include "CAstronaut.h" + +/** + * Main function simulating the airlock problem with two astronauts. + */ +int main() +{ + CAirLock airlock; + + CAstronaut astroOutside("Astro-Outside", CAstronaut::ELocation::OUTSIDE, airlock); + CAstronaut astroInside("Astro-Inside", CAstronaut::ELocation::INSIDE, airlock); + + auto runAstro = [](CAstronaut& astro, + const CAirLock::EDoor entry_door, + const CAirLock::EDoor exit_door, + const CAstronaut::ELocation target_location) + { + std::cout << std::format("[{}] Starting mission to move from {} to {}.\n", + astro.GetName(), + CAstronaut::GetLocationName(astro.GetLocation()), + CAstronaut::GetLocationName(target_location)); + + bool bReachedDestination = false; + while (!bReachedDestination) + { + // Try to open entry door + if (astro.TryOpenDoor(entry_door)) + { + astro.MoveTo(CAstronaut::ELocation::CHAMBER); + astro.CloseDoor(entry_door); + // First check if the exit door is already open (maybe someone else opened it for us) + // Check if airlock is in correct state, if not try to cycle it. + while (!astro.IsChamberSafeToExit(target_location) && !astro.TryCycleAirlock()) + { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + } + // Open exit door + while (!astro.IsDoorOpen(exit_door) && !astro.TryOpenDoor(exit_door)) + { + // Wait for the exit door to be able to open + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + } + astro.MoveTo(target_location); + if (astro.IsDoorOpen(exit_door)) + astro.CloseDoor(exit_door); + bReachedDestination = true; + + } + // Chamber might be in wrong state, try to cycle it + else if (!astro.TryCycleAirlock()) + // Someone else might have a door open or someone requested a cycle from inside the chamber + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + } + std::cout << std::format("[{}] Reached destination {}.\n", + astro.GetName(), + CAstronaut::GetLocationName(target_location)); + }; + + std::thread astroOutsideThread(runAstro, std::ref(astroOutside), CAirLock::EDoor::OUTSIDE, CAirLock::EDoor::INSIDE, + CAstronaut::ELocation::INSIDE); + std::thread astroInsideThread(runAstro, std::ref(astroInside), CAirLock::EDoor::INSIDE, CAirLock::EDoor::OUTSIDE, + CAstronaut::ELocation::OUTSIDE); + + astroOutsideThread.join(); + astroInsideThread.join(); + + std::cout << "All astronauts have finished their maneuvers.\n"; + + return 0; +} diff --git a/solution/cpp/tests/tests.cpp b/solution/cpp/tests/tests.cpp new file mode 100644 index 0000000..9104849 --- /dev/null +++ b/solution/cpp/tests/tests.cpp @@ -0,0 +1,355 @@ +// External headers +#include +#include +#include + +// Project headers +#include "CAirLock.h" +#include "CAstronaut.h" + +/** + * CAirLockTest + * Unit tests for the airlock logic and safety rules. + */ +TEST(CAirLockTest, StaticHelpers) +{ + EXPECT_EQ(CAirLock::GetChamberStateName(CAirLock::EChamberState::EMPTY), "empty"); + EXPECT_EQ(CAirLock::GetChamberStateName(CAirLock::EChamberState::PRESSURIZED), "pressurized"); + EXPECT_EQ(CAirLock::GetChamberStateName(CAirLock::EChamberState::CHANGING_STATE), "cycling"); + EXPECT_EQ(CAirLock::GetChamberStateName(static_cast(999)), "unknown"); + + EXPECT_EQ(CAirLock::GetDoorName(CAirLock::EDoor::OUTSIDE), "outside"); + EXPECT_EQ(CAirLock::GetDoorName(CAirLock::EDoor::INSIDE), "inside"); + EXPECT_EQ(CAirLock::GetDoorName(static_cast(999)), "unknown"); +} + +TEST(CAirLockTest, InitialState) +{ + const CAirLock airlock; + EXPECT_FALSE(airlock.IsDoorOpen(CAirLock::EDoor::OUTSIDE)); + EXPECT_FALSE(airlock.IsDoorOpen(CAirLock::EDoor::INSIDE)); + EXPECT_EQ(airlock.GetChamberState(), CAirLock::EChamberState::EMPTY); +} + +TEST(CAirLockTest, DoorSafetyRules) +{ + CAirLock airlock; + + // Rule: Outside door should only open when the chamber is empty. + EXPECT_EQ(airlock.GetChamberState(), CAirLock::EChamberState::EMPTY); + EXPECT_TRUE(airlock.TryOpenDoor(CAirLock::EDoor::OUTSIDE)); + EXPECT_TRUE(airlock.IsDoorOpen(CAirLock::EDoor::OUTSIDE)); + + // Cannot open inside door if outside is open (even if it was pressurized, which it isn't) + EXPECT_FALSE(airlock.TryOpenDoor(CAirLock::EDoor::INSIDE)); + + airlock.CloseDoor(CAirLock::EDoor::OUTSIDE); + EXPECT_FALSE(airlock.IsDoorOpen(CAirLock::EDoor::OUTSIDE)); + + // Cycle to pressurized from outside + EXPECT_TRUE(airlock.TryCycleChamber(CAirLock::ECycleLocation::OUTSIDE)); + EXPECT_EQ(airlock.GetChamberState(), CAirLock::EChamberState::PRESSURIZED); + + // Rule: Inside door should only open when the chamber is pressurized. + EXPECT_FALSE(airlock.TryOpenDoor(CAirLock::EDoor::OUTSIDE)); + EXPECT_TRUE(airlock.TryOpenDoor(CAirLock::EDoor::INSIDE)); + EXPECT_TRUE(airlock.IsDoorOpen(CAirLock::EDoor::INSIDE)); + + airlock.CloseDoor(CAirLock::EDoor::INSIDE); +} + +TEST(CAirLockTest, ChamberCycleSafety) +{ + CAirLock airlock; + + // Rule: The chamber should only be able to (de-)pressurize when both doors are closed. + EXPECT_TRUE(airlock.TryOpenDoor(CAirLock::EDoor::OUTSIDE)); + EXPECT_FALSE(airlock.TryCycleChamber(CAirLock::ECycleLocation::OUTSIDE)); + + airlock.CloseDoor(CAirLock::EDoor::OUTSIDE); + EXPECT_TRUE(airlock.TryCycleChamber(CAirLock::ECycleLocation::OUTSIDE)); + EXPECT_EQ(airlock.GetChamberState(), CAirLock::EChamberState::PRESSURIZED); + + EXPECT_TRUE(airlock.TryOpenDoor(CAirLock::EDoor::INSIDE)); + EXPECT_FALSE(airlock.TryCycleChamber(CAirLock::ECycleLocation::INSIDE)); + + airlock.CloseDoor(CAirLock::EDoor::INSIDE); + EXPECT_TRUE(airlock.TryCycleChamber(CAirLock::ECycleLocation::INSIDE)); + EXPECT_EQ(airlock.GetChamberState(), CAirLock::EChamberState::EMPTY); +} + +TEST(CAirLockTest, HijackPrevention) +{ + CAirLock airlock; + + // Initially EMPTY + // Someone cycles it from INSIDE the chamber + EXPECT_TRUE(airlock.TryCycleChamber(CAirLock::ECycleLocation::INSIDE)); + EXPECT_EQ(airlock.GetChamberState(), CAirLock::EChamberState::PRESSURIZED); + + // Now it should be "locked" for anyone trying to cycle it from OUTSIDE. + EXPECT_FALSE(airlock.TryCycleChamber(CAirLock::ECycleLocation::OUTSIDE)); + EXPECT_EQ(airlock.GetChamberState(), CAirLock::EChamberState::PRESSURIZED); + + // But the person INSIDE can cycle it as much as they want. + EXPECT_TRUE(airlock.TryCycleChamber(CAirLock::ECycleLocation::INSIDE)); + EXPECT_EQ(airlock.GetChamberState(), CAirLock::EChamberState::EMPTY); + + // Still locked from OUTSIDE because it was last cycled from INSIDE. + EXPECT_FALSE(airlock.TryCycleChamber(CAirLock::ECycleLocation::OUTSIDE)); + + // Once a door is opened, the "lock" is released. + EXPECT_TRUE(airlock.TryOpenDoor(CAirLock::EDoor::OUTSIDE)); + airlock.CloseDoor(CAirLock::EDoor::OUTSIDE); + + // Now someone from OUTSIDE can cycle it again. + EXPECT_TRUE(airlock.TryCycleChamber(CAirLock::ECycleLocation::OUTSIDE)); + EXPECT_EQ(airlock.GetChamberState(), CAirLock::EChamberState::PRESSURIZED); +} + +TEST(CAirLockTest, DoorRedundancy) +{ + CAirLock airlock; + EXPECT_TRUE(airlock.TryOpenDoor(CAirLock::EDoor::OUTSIDE)); + EXPECT_TRUE(airlock.IsDoorOpen(CAirLock::EDoor::OUTSIDE)); + // Opening again should succeed (or at least not fail/crash and keep it open) + EXPECT_TRUE(airlock.TryOpenDoor(CAirLock::EDoor::OUTSIDE)); + EXPECT_TRUE(airlock.IsDoorOpen(CAirLock::EDoor::OUTSIDE)); + + airlock.CloseDoor(CAirLock::EDoor::OUTSIDE); + EXPECT_FALSE(airlock.IsDoorOpen(CAirLock::EDoor::OUTSIDE)); + // Closing again + airlock.CloseDoor(CAirLock::EDoor::OUTSIDE); + EXPECT_FALSE(airlock.IsDoorOpen(CAirLock::EDoor::OUTSIDE)); +} + +TEST(CAirLockTest, DoorPressureSafety) +{ + CAirLock airlock; + // Initially EMPTY, cannot open INSIDE door + EXPECT_FALSE(airlock.TryOpenDoor(CAirLock::EDoor::INSIDE)); + EXPECT_FALSE(airlock.IsDoorOpen(CAirLock::EDoor::INSIDE)); + + // Cycle to PRESSURIZED + EXPECT_TRUE(airlock.TryCycleChamber(CAirLock::ECycleLocation::OUTSIDE)); + + // Now cannot open OUTSIDE door + EXPECT_FALSE(airlock.TryOpenDoor(CAirLock::EDoor::OUTSIDE)); + EXPECT_FALSE(airlock.IsDoorOpen(CAirLock::EDoor::OUTSIDE)); +} + +TEST(CAirLockTest, ConcurrentCycling) +{ + CAirLock airlock; + std::atomic successCount{0}; + + // Check initial state + EXPECT_EQ(airlock.GetChamberState(), CAirLock::EChamberState::EMPTY); + + auto cycleFunc = [&]() + { + if (airlock.TryCycleChamber(CAirLock::ECycleLocation::OUTSIDE)) + { + ++successCount; + } + }; + + std::thread t1(cycleFunc); + std::thread t2(cycleFunc); + + t1.join(); + t2.join(); + + // Only one should succeed because the first one sets state to CHANGING_STATE + EXPECT_EQ(successCount.load(), 1); + EXPECT_EQ(airlock.GetChamberState(), CAirLock::EChamberState::PRESSURIZED); +} + +TEST(CAirLockTest, CyclingBlocksActions) +{ + CAirLock airlock; + + std::thread cyclingThread([&]() + { + (void)airlock.TryCycleChamber(CAirLock::ECycleLocation::OUTSIDE); + }); + + // Wait a bit for it to start cycling (it sleeps for 100ms) + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + + // While cycling: + EXPECT_EQ(airlock.GetChamberState(), CAirLock::EChamberState::CHANGING_STATE); + + // Cannot open doors + EXPECT_FALSE(airlock.TryOpenDoor(CAirLock::EDoor::OUTSIDE)); + EXPECT_FALSE(airlock.TryOpenDoor(CAirLock::EDoor::INSIDE)); + + // Cannot cycle again + EXPECT_FALSE(airlock.TryCycleChamber(CAirLock::ECycleLocation::INSIDE)); + + cyclingThread.join(); + EXPECT_EQ(airlock.GetChamberState(), CAirLock::EChamberState::PRESSURIZED); +} + +TEST(CAstronautTest, StaticHelpers) +{ + EXPECT_EQ(CAstronaut::GetLocationName(CAstronaut::ELocation::OUTSIDE), "outside"); + EXPECT_EQ(CAstronaut::GetLocationName(CAstronaut::ELocation::INSIDE), "inside"); + EXPECT_EQ(CAstronaut::GetLocationName(CAstronaut::ELocation::CHAMBER), "chamber"); + EXPECT_EQ(CAstronaut::GetLocationName(static_cast(999)), "error"); +} + +TEST(CAstronautTest, SafetyChecks) +{ + CAirLock airlock; + const CAstronaut astro("Test", CAstronaut::ELocation::OUTSIDE, airlock); + + // Chamber is EMPTY + EXPECT_TRUE(astro.IsChamberSafeToExit(CAstronaut::ELocation::OUTSIDE)); + EXPECT_FALSE(astro.IsChamberSafeToExit(CAstronaut::ELocation::INSIDE)); + + // set airlock into cycling state + std::thread cyclingThread([&]() + { + (void)airlock.TryCycleChamber(CAirLock::ECycleLocation::OUTSIDE); + }); + // Wait a bit for it to start cycling (it sleeps for 100ms) + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + // Chamber is CYCLING + EXPECT_FALSE(astro.IsChamberSafeToExit(CAstronaut::ELocation::OUTSIDE)); + EXPECT_FALSE(astro.IsChamberSafeToExit(CAstronaut::ELocation::INSIDE)); + + // wait until cycling is done + cyclingThread.join(); + + // Chamber is PRESSURIZED + EXPECT_FALSE(astro.IsChamberSafeToExit(CAstronaut::ELocation::OUTSIDE)); + EXPECT_TRUE(astro.IsChamberSafeToExit(CAstronaut::ELocation::INSIDE)); +} + +TEST(CAstronautTest, ConcurrentDoorActions) +{ + CAirLock airlock; + const CAstronaut astro("Astro", CAstronaut::ELocation::OUTSIDE, airlock); + + // Test TryOpenDoor when someone else opens it during the sleep + std::thread externalOpener([&]() + { + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + (void)airlock.TryOpenDoor(CAirLock::EDoor::OUTSIDE); + }); + + // astro.TryOpenDoor sleeps for 50ms before calling airlock.TryOpenDoor + // but it checks IsDoorOpen twice. + EXPECT_TRUE(astro.TryOpenDoor(CAirLock::EDoor::OUTSIDE)); + externalOpener.join(); + + // Test CloseDoor when someone else closes it during the sleep + std::thread externalCloser([&]() + { + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + airlock.CloseDoor(CAirLock::EDoor::OUTSIDE); + }); + EXPECT_TRUE(airlock.IsDoorOpen(CAirLock::EDoor::OUTSIDE)); + astro.CloseDoor(CAirLock::EDoor::OUTSIDE); + externalCloser.join(); + EXPECT_FALSE(airlock.IsDoorOpen(CAirLock::EDoor::OUTSIDE)); +} + +TEST(CAstronautTest, BasicMovement) +{ + CAirLock airlock; + CAstronaut astro("Astro", CAstronaut::ELocation::OUTSIDE, airlock); + + astro.MoveTo(CAstronaut::ELocation::CHAMBER); + EXPECT_EQ(astro.GetLocation(), CAstronaut::ELocation::CHAMBER); +} + +TEST(CAstronautTest, CycleAirlock) +{ + CAirLock airlock; + const CAstronaut astroOutside("AstroOut", CAstronaut::ELocation::OUTSIDE, airlock); + const CAstronaut astroChamber("AstroCham", CAstronaut::ELocation::CHAMBER, airlock); + + // Initially EMPTY + EXPECT_TRUE(astroOutside.TryCycleAirlock()); + EXPECT_EQ(airlock.GetChamberState(), CAirLock::EChamberState::PRESSURIZED); + + // Cycle back from chamber + EXPECT_TRUE(astroChamber.TryCycleAirlock()); + EXPECT_EQ(airlock.GetChamberState(), CAirLock::EChamberState::EMPTY); +} + +TEST(CAstronautTest, Interaction) +{ + CAirLock airlock; + CAstronaut astro("Major Tom", CAstronaut::ELocation::OUTSIDE, airlock); + + EXPECT_EQ(astro.GetName(), "Major Tom"); + EXPECT_EQ(astro.GetLocation(), CAstronaut::ELocation::OUTSIDE); + + // Try to get in + EXPECT_TRUE(astro.TryOpenDoor(CAirLock::EDoor::OUTSIDE)); + astro.MoveTo(CAstronaut::ELocation::CHAMBER); + astro.CloseDoor(CAirLock::EDoor::OUTSIDE); + + EXPECT_TRUE(astro.TryCycleAirlock()); + EXPECT_EQ(airlock.GetChamberState(), CAirLock::EChamberState::PRESSURIZED); + + EXPECT_TRUE(astro.TryOpenDoor(CAirLock::EDoor::INSIDE)); + astro.MoveTo(CAstronaut::ELocation::INSIDE); + astro.CloseDoor(CAirLock::EDoor::INSIDE); + + EXPECT_EQ(astro.GetLocation(), CAstronaut::ELocation::INSIDE); +} + +TEST(CAstronautTest, TwoAstronautsConflict) +{ + CAirLock airlock; + CAstronaut astroOutside("Astro-Outside", CAstronaut::ELocation::OUTSIDE, airlock); + CAstronaut astroInside("Astro-Inside", CAstronaut::ELocation::INSIDE, airlock); + + auto runAstro = [](CAstronaut& astro, + const CAirLock::EDoor entry_door, + const CAirLock::EDoor exit_door, + const CAstronaut::ELocation target_location) + { + bool bReachedDestination = false; + int attempts = 0; + while (!bReachedDestination && attempts < 100) + { + attempts++; + if (astro.TryOpenDoor(entry_door)) + { + astro.MoveTo(CAstronaut::ELocation::CHAMBER); + astro.CloseDoor(entry_door); + while (!astro.IsChamberSafeToExit(target_location) && !astro.TryCycleAirlock()) + { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + while (!astro.IsDoorOpen(exit_door) && !astro.TryOpenDoor(exit_door)) + { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + astro.MoveTo(target_location); + if (astro.IsDoorOpen(exit_door)) + astro.CloseDoor(exit_door); + bReachedDestination = true; + } + else if (!astro.TryCycleAirlock()) + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + EXPECT_TRUE(bReachedDestination); + }; + + std::thread t1(runAstro, std::ref(astroOutside), CAirLock::EDoor::OUTSIDE, CAirLock::EDoor::INSIDE, + CAstronaut::ELocation::INSIDE); + std::thread t2(runAstro, std::ref(astroInside), CAirLock::EDoor::INSIDE, CAirLock::EDoor::OUTSIDE, + CAstronaut::ELocation::OUTSIDE); + + t1.join(); + t2.join(); + + EXPECT_EQ(astroOutside.GetLocation(), CAstronaut::ELocation::INSIDE); + EXPECT_EQ(astroInside.GetLocation(), CAstronaut::ELocation::OUTSIDE); +}