From 7f0082ddbe7b9974a8c7aae4a52b35a735ed0cd0 Mon Sep 17 00:00:00 2001 From: Moritz Martinius Date: Tue, 21 Oct 2025 20:12:52 +0200 Subject: [PATCH] Add libusbwrapper for unit tests and UsbDevice unit tests --- src/core/PrinterService.cpp | 12 +- src/core/PrinterService.hpp | 9 +- src/libusbwrap/LibusbWrapper.cpp | 108 +++++ src/libusbwrap/LibusbWrapper.hpp | 129 ++++++ src/libusbwrap/UsbDevice.cpp | 59 +-- src/libusbwrap/UsbDevice.hpp | 30 +- src/libusbwrap/UsbDeviceFactory.cpp | 64 +-- src/libusbwrap/UsbDeviceFactory.hpp | 22 +- .../interface/IUsbDeviceFactory.hpp | 1 + src/meson.build | 2 + tests/cli_parser_test/cli_parser_test.cpp | 365 +++++++++++++++++ tests/meson.build | 10 + tests/mocks/MockLibusbWrapper.hpp | 76 ++++ tests/ptouch_print_test/ptouch_print_test.cpp | 374 ++++++++++++++++++ tests/usb_device_test/usb_device_test.cpp | 296 ++++++++++++++ 15 files changed, 1481 insertions(+), 76 deletions(-) create mode 100644 src/libusbwrap/LibusbWrapper.cpp create mode 100644 src/libusbwrap/LibusbWrapper.hpp create mode 100644 tests/cli_parser_test/cli_parser_test.cpp create mode 100644 tests/mocks/MockLibusbWrapper.hpp create mode 100644 tests/ptouch_print_test/ptouch_print_test.cpp create mode 100644 tests/usb_device_test/usb_device_test.cpp diff --git a/src/core/PrinterService.cpp b/src/core/PrinterService.cpp index b61de91..9ec6f54 100644 --- a/src/core/PrinterService.cpp +++ b/src/core/PrinterService.cpp @@ -22,13 +22,19 @@ #include #include "core/PrinterDriverFactory.hpp" +#include "libusbwrap/UsbDeviceFactory.hpp" namespace ptprnt::core { -PrinterService::PrinterService() = default; +// Default constructor delegates to DI constructor +PrinterService::PrinterService() : PrinterService(std::make_unique()) {} + +// Constructor with dependency injection +PrinterService::PrinterService(std::unique_ptr usbFactory) + : mUsbDeviceFactory(std::move(usbFactory)) {} bool PrinterService::initialize() { - if (!mUsbDeviceFactory.init()) { + if (!mUsbDeviceFactory->init()) { spdlog::error("Could not initialize libusb"); return false; } @@ -38,7 +44,7 @@ bool PrinterService::initialize() { std::vector> PrinterService::detectPrinters() { spdlog::debug("Detecting printers..."); - auto usbDevs = mUsbDeviceFactory.findAllDevices(); + auto usbDevs = mUsbDeviceFactory->findAllDevices(); auto driverFactory = std::make_unique(); mDetectedPrinters.clear(); diff --git a/src/core/PrinterService.hpp b/src/core/PrinterService.hpp index b4ea55f..5c7ce8c 100644 --- a/src/core/PrinterService.hpp +++ b/src/core/PrinterService.hpp @@ -24,7 +24,7 @@ #include #include "interface/IPrinterService.hpp" -#include "libusbwrap/UsbDeviceFactory.hpp" +#include "libusbwrap/interface/IUsbDeviceFactory.hpp" #include "printers/interface/IPrinterDriver.hpp" namespace ptprnt::core { @@ -40,7 +40,12 @@ namespace ptprnt::core { */ class PrinterService : public IPrinterService { public: + // Default constructor (uses real UsbDeviceFactory) PrinterService(); + + // Constructor for testing (inject mock factory) + explicit PrinterService(std::unique_ptr usbFactory); + ~PrinterService() override = default; PrinterService(const PrinterService&) = delete; @@ -81,7 +86,7 @@ class PrinterService : public IPrinterService { bool printLabel(std::unique_ptr label) override; private: - libusbwrap::UsbDeviceFactory mUsbDeviceFactory; + std::unique_ptr mUsbDeviceFactory; std::vector> mDetectedPrinters; std::shared_ptr mCurrentPrinter; }; diff --git a/src/libusbwrap/LibusbWrapper.cpp b/src/libusbwrap/LibusbWrapper.cpp new file mode 100644 index 0000000..c3dc7b7 --- /dev/null +++ b/src/libusbwrap/LibusbWrapper.cpp @@ -0,0 +1,108 @@ +/* + ptrnt - print labels on linux + Copyright (C) 2025 Moritz Martinius + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + + */ + +#include "LibusbWrapper.hpp" + +#include "libusb.h" + +namespace libusbwrap { + +// LibusbDeviceDeleter implementation +void LibusbDeviceDeleter::operator()(libusb_device* dev) const { + if (dev && wrapper) { + wrapper->unrefDevice(dev); + } +} + +// LibusbWrapper implementation - thin forwarding to libusb C API + +int LibusbWrapper::init(libusb_context** ctx) { + return libusb_init(ctx); +} + +void LibusbWrapper::exit(libusb_context* ctx) { + libusb_exit(ctx); +} + +ssize_t LibusbWrapper::getDeviceList(libusb_context* ctx, libusb_device*** list) { + return libusb_get_device_list(ctx, list); +} + +void LibusbWrapper::freeDeviceList(libusb_device** list, int unrefDevices) { + libusb_free_device_list(list, unrefDevices); +} + +void LibusbWrapper::refDevice(libusb_device* dev) { + libusb_ref_device(dev); +} + +void LibusbWrapper::unrefDevice(libusb_device* dev) { + libusb_unref_device(dev); +} + +int LibusbWrapper::getDeviceDescriptor(libusb_device* dev, libusb_device_descriptor* desc) { + return libusb_get_device_descriptor(dev, desc); +} + +int LibusbWrapper::open(libusb_device* dev, libusb_device_handle** handle) { + return libusb_open(dev, handle); +} + +void LibusbWrapper::close(libusb_device_handle* handle) { + libusb_close(handle); +} + +int LibusbWrapper::getSpeed(libusb_device* dev) { + return libusb_get_device_speed(dev); +} + +uint8_t LibusbWrapper::getBusNumber(libusb_device* dev) { + return libusb_get_bus_number(dev); +} + +uint8_t LibusbWrapper::getPortNumber(libusb_device* dev) { + return libusb_get_port_number(dev); +} + +int LibusbWrapper::kernelDriverActive(libusb_device_handle* handle, int interfaceNo) { + return libusb_kernel_driver_active(handle, interfaceNo); +} + +int LibusbWrapper::detachKernelDriver(libusb_device_handle* handle, int interfaceNo) { + return libusb_detach_kernel_driver(handle, interfaceNo); +} + +int LibusbWrapper::claimInterface(libusb_device_handle* handle, int interfaceNo) { + return libusb_claim_interface(handle, interfaceNo); +} + +int LibusbWrapper::releaseInterface(libusb_device_handle* handle, int interfaceNo) { + return libusb_release_interface(handle, interfaceNo); +} + +int LibusbWrapper::bulkTransfer(libusb_device_handle* handle, uint8_t endpoint, unsigned char* data, int length, + int* transferred, unsigned int timeout) { + return libusb_bulk_transfer(handle, endpoint, data, length, transferred, timeout); +} + +const char* LibusbWrapper::errorName(int errorCode) { + return libusb_error_name(errorCode); +} + +} // namespace libusbwrap diff --git a/src/libusbwrap/LibusbWrapper.hpp b/src/libusbwrap/LibusbWrapper.hpp new file mode 100644 index 0000000..191ad11 --- /dev/null +++ b/src/libusbwrap/LibusbWrapper.hpp @@ -0,0 +1,129 @@ +/* + ptrnt - print labels on linux + Copyright (C) 2025 Moritz Martinius + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + + */ + +#pragma once + +#include +#include +#include +#include + +// Forward declarations to avoid pulling in libusb.h in header +struct libusb_context; +struct libusb_device; +struct libusb_device_handle; +struct libusb_device_descriptor; + +namespace libusbwrap { + +// Forward declaration +class ILibusbWrapper; + +// Custom deleter for libusb_device that uses the wrapper +// Implementation in LibusbWrapper.cpp +struct LibusbDeviceDeleter { + std::shared_ptr wrapper; + void operator()(libusb_device* dev) const; +}; +using LibusbDevicePtr = std::unique_ptr; + +/** + * @brief Thin wrapper around libusb C API + * + * This class provides a 1:1 mapping to libusb C functions. + * NO LOGIC - just wraps the C calls directly. + * All logic (RAII, error handling, etc.) belongs in UsbDevice/UsbDeviceFactory. + */ +class ILibusbWrapper { + public: + virtual ~ILibusbWrapper() = default; + + // Context management (raw pointers - caller manages lifecycle) + virtual int init(libusb_context** ctx) = 0; + virtual void exit(libusb_context* ctx) = 0; + + // Device enumeration (raw pointers - caller manages lifecycle) + virtual ssize_t getDeviceList(libusb_context* ctx, libusb_device*** list) = 0; + virtual void freeDeviceList(libusb_device** list, int unrefDevices) = 0; + virtual void refDevice(libusb_device* dev) = 0; + virtual void unrefDevice(libusb_device* dev) = 0; + + // Device descriptor + virtual int getDeviceDescriptor(libusb_device* dev, libusb_device_descriptor* desc) = 0; + + // Device opening/closing (raw pointers - caller manages lifecycle) + virtual int open(libusb_device* dev, libusb_device_handle** handle) = 0; + virtual void close(libusb_device_handle* handle) = 0; + + // Device information + virtual int getSpeed(libusb_device* dev) = 0; + virtual uint8_t getBusNumber(libusb_device* dev) = 0; + virtual uint8_t getPortNumber(libusb_device* dev) = 0; + + // Kernel driver management + virtual int kernelDriverActive(libusb_device_handle* handle, int interfaceNo) = 0; + virtual int detachKernelDriver(libusb_device_handle* handle, int interfaceNo) = 0; + + // Interface management + virtual int claimInterface(libusb_device_handle* handle, int interfaceNo) = 0; + virtual int releaseInterface(libusb_device_handle* handle, int interfaceNo) = 0; + + // Data transfer + virtual int bulkTransfer(libusb_device_handle* handle, uint8_t endpoint, unsigned char* data, int length, + int* transferred, unsigned int timeout) = 0; + + // Error handling + virtual const char* errorName(int errorCode) = 0; +}; + +/** + * @brief Concrete implementation - thin wrapper forwarding to libusb C API + */ +class LibusbWrapper : public ILibusbWrapper { + public: + int init(libusb_context** ctx) override; + void exit(libusb_context* ctx) override; + + ssize_t getDeviceList(libusb_context* ctx, libusb_device*** list) override; + void freeDeviceList(libusb_device** list, int unrefDevices) override; + void refDevice(libusb_device* dev) override; + void unrefDevice(libusb_device* dev) override; + + int getDeviceDescriptor(libusb_device* dev, libusb_device_descriptor* desc) override; + + int open(libusb_device* dev, libusb_device_handle** handle) override; + void close(libusb_device_handle* handle) override; + + int getSpeed(libusb_device* dev) override; + uint8_t getBusNumber(libusb_device* dev) override; + uint8_t getPortNumber(libusb_device* dev) override; + + int kernelDriverActive(libusb_device_handle* handle, int interfaceNo) override; + int detachKernelDriver(libusb_device_handle* handle, int interfaceNo) override; + + int claimInterface(libusb_device_handle* handle, int interfaceNo) override; + int releaseInterface(libusb_device_handle* handle, int interfaceNo) override; + + int bulkTransfer(libusb_device_handle* handle, uint8_t endpoint, unsigned char* data, int length, int* transferred, + unsigned int timeout) override; + + const char* errorName(int errorCode) override; +}; + +} // namespace libusbwrap diff --git a/src/libusbwrap/UsbDevice.cpp b/src/libusbwrap/UsbDevice.cpp index c480947..6c4b390 100644 --- a/src/libusbwrap/UsbDevice.cpp +++ b/src/libusbwrap/UsbDevice.cpp @@ -24,44 +24,57 @@ #include "libusb.h" #include "libusbwrap/LibUsbTypes.hpp" +#include "libusbwrap/LibusbWrapper.hpp" #include "libusbwrap/interface/IUsbDevice.hpp" namespace libusbwrap { -UsbDevice::UsbDevice(libusb_context* ctx, usbDevice_ptr dev) : mLibusbCtx(ctx), mLibusbDev(std::move(dev)) { - if (mLibusbCtx == nullptr || mLibusbDev == nullptr) { - throw std::invalid_argument("ctx or device are nullptr"); - } - libusb_get_device_descriptor(mLibusbDev.get(), &mLibusbDevDesc); +// Default constructor delegates to DI constructor +UsbDevice::UsbDevice(libusb_device* device, const libusb_device_descriptor& desc) + : UsbDevice(device, desc, std::make_shared()) {} + +// Constructor with dependency injection +UsbDevice::UsbDevice(libusb_device* device, const libusb_device_descriptor& desc, + std::shared_ptr libusbWrapper) + : mLibusb(libusbWrapper), + mDevice(device, LibusbDeviceDeleter{libusbWrapper}), + mDeviceDescriptor(desc) { + if (!mDevice) { + throw std::invalid_argument("device is nullptr"); + } } UsbDevice::~UsbDevice() { - // only free the device, not the context, which is shared between every device - // the UsbDeviceFactory will take care of that - if (mIsOpen) { - close(); + if (mIsOpen && mDeviceHandle) { + mLibusb->close(mDeviceHandle); } + // mDevice auto-deleted by unique_ptr (calls LibusbDeviceDeleter) } bool UsbDevice::open() { - int openStatus = libusb_open(mLibusbDev.get(), &mLibusbDevHandle); + int openStatus = mLibusb->open(mDevice.get(), &mDeviceHandle); if (openStatus != 0) { mLastError = static_cast(openStatus); return false; } + mIsOpen = true; return true; } void UsbDevice::close() { - libusb_close(mLibusbDevHandle); + if (mDeviceHandle) { + mLibusb->close(mDeviceHandle); + mDeviceHandle = nullptr; + mIsOpen = false; + } } bool UsbDevice::detachKernelDriver(int interfaceNo) { // TODO: cover the other status codes that can be returned - int kernelDriverStatus = libusb_kernel_driver_active(mLibusbDevHandle, interfaceNo); + int kernelDriverStatus = mLibusb->kernelDriverActive(mDeviceHandle, interfaceNo); if (kernelDriverStatus == 1) { // kernel driver is active, we have to detach to continue... - int detachStatus = libusb_detach_kernel_driver(mLibusbDevHandle, interfaceNo); + int detachStatus = mLibusb->detachKernelDriver(mDeviceHandle, interfaceNo); if (detachStatus != 0) { mLastError = static_cast(detachStatus); return false; @@ -73,7 +86,7 @@ bool UsbDevice::detachKernelDriver(int interfaceNo) { bool UsbDevice::claimInterface(int interfaceNo) { // TODO: cover the other status codes that can be returned - int claimInterfaceStatus = libusb_claim_interface(mLibusbDevHandle, interfaceNo); + int claimInterfaceStatus = mLibusb->claimInterface(mDeviceHandle, interfaceNo); if (claimInterfaceStatus != 0) { mLastError = static_cast(claimInterfaceStatus); return false; @@ -82,7 +95,7 @@ bool UsbDevice::claimInterface(int interfaceNo) { } bool UsbDevice::releaseInterface(int interfaceNo) { - int releaseInterfaceStatus = libusb_release_interface(mLibusbDevHandle, interfaceNo); + int releaseInterfaceStatus = mLibusb->releaseInterface(mDeviceHandle, interfaceNo); if (releaseInterfaceStatus != 0) { mLastError = static_cast(releaseInterfaceStatus); return false; @@ -92,10 +105,8 @@ bool UsbDevice::releaseInterface(int interfaceNo) { bool UsbDevice::bulkTransfer(uint8_t endpoint, const std::vector& data, int* tx, unsigned int timeout) { // TODO: implement error handling for incomplete transactions (tx length != data length) - int bulkTransferStatus = 0; - - bulkTransferStatus = libusb_bulk_transfer(mLibusbDevHandle, endpoint, const_cast(data.data()), - data.size(), tx, timeout); + int bulkTransferStatus = mLibusb->bulkTransfer(mDeviceHandle, endpoint, const_cast(data.data()), + data.size(), tx, timeout); if (bulkTransferStatus != 0) { mLastError = static_cast(bulkTransferStatus); return false; @@ -104,19 +115,19 @@ bool UsbDevice::bulkTransfer(uint8_t endpoint, const std::vector& data, } const usbId UsbDevice::getUsbId() { - return {mLibusbDevDesc.idVendor, mLibusbDevDesc.idProduct}; + return {mDeviceDescriptor.idVendor, mDeviceDescriptor.idProduct}; } const device::Speed UsbDevice::getSpeed() { - return static_cast(libusb_get_device_speed(mLibusbDev.get())); + return static_cast(mLibusb->getSpeed(mDevice.get())); } const uint8_t UsbDevice::getBusNumber() { - return libusb_get_bus_number(mLibusbDev.get()); + return mLibusb->getBusNumber(mDevice.get()); } const uint8_t UsbDevice::getPortNumber() { - return libusb_get_port_number(mLibusbDev.get()); + return mLibusb->getPortNumber(mDevice.get()); } const Error UsbDevice::getLastError() { @@ -124,6 +135,6 @@ const Error UsbDevice::getLastError() { } const std::string UsbDevice::getLastErrorString() { - return std::string(libusb_error_name(static_cast(mLastError))); + return std::string(mLibusb->errorName(static_cast(mLastError))); } } // namespace libusbwrap \ No newline at end of file diff --git a/src/libusbwrap/UsbDevice.hpp b/src/libusbwrap/UsbDevice.hpp index c89aadf..d58c499 100644 --- a/src/libusbwrap/UsbDevice.hpp +++ b/src/libusbwrap/UsbDevice.hpp @@ -24,23 +24,20 @@ #include "libusb.h" #include "libusbwrap/LibUsbTypes.hpp" +#include "libusbwrap/LibusbWrapper.hpp" #include "libusbwrap/interface/IUsbDevice.hpp" namespace libusbwrap { -struct usbDevice_deleter { - void operator()(libusb_device* dev_ptr) const { - if (nullptr != dev_ptr) { - libusb_unref_device(dev_ptr); - } - } -}; - -using usbDevice_ptr = std::unique_ptr; - class UsbDevice : public IUsbDevice { public: - explicit UsbDevice(libusb_context* ctx, usbDevice_ptr dev); + // Default constructor (uses real LibusbWrapper) + UsbDevice(libusb_device* device, const libusb_device_descriptor& desc); + + // Constructor for testing (inject mock wrapper) + UsbDevice(libusb_device* device, const libusb_device_descriptor& desc, + std::shared_ptr libusbWrapper); + ~UsbDevice() override; // delete copy ctor and assignment @@ -69,10 +66,13 @@ class UsbDevice : public IUsbDevice { const std::string getLastErrorString() override; private: - libusb_context* mLibusbCtx{nullptr}; - usbDevice_ptr mLibusbDev{nullptr}; - libusb_device_handle* mLibusbDevHandle{nullptr}; - libusb_device_descriptor mLibusbDevDesc{0}; + std::shared_ptr mLibusb; // Injectable wrapper + + // RAII wrappers (UsbDevice owns the lifecycle logic) + LibusbDevicePtr mDevice; // unique_ptr with custom deleter + libusb_device_handle* mDeviceHandle = nullptr; // Managed by UsbDevice (calls mLibusb->close()) + + libusb_device_descriptor mDeviceDescriptor{0}; std::atomic mIsOpen = false; Error mLastError = Error::SUCCESS; }; diff --git a/src/libusbwrap/UsbDeviceFactory.cpp b/src/libusbwrap/UsbDeviceFactory.cpp index 5ad5349..72c8fd3 100644 --- a/src/libusbwrap/UsbDeviceFactory.cpp +++ b/src/libusbwrap/UsbDeviceFactory.cpp @@ -27,16 +27,25 @@ #include #include "libusb.h" +#include "libusbwrap/LibusbWrapper.hpp" #include "libusbwrap/UsbDevice.hpp" #include "libusbwrap/interface/IUsbDevice.hpp" namespace libusbwrap { +// Default constructor delegates to DI constructor +UsbDeviceFactory::UsbDeviceFactory() : UsbDeviceFactory(std::make_shared()) {} + +// Constructor with dependency injection +UsbDeviceFactory::UsbDeviceFactory(std::shared_ptr libusbWrapper) + : mLibusb(std::move(libusbWrapper)) {} + UsbDeviceFactory::~UsbDeviceFactory() { - if (mLibusbCtx) { - libusb_exit(mLibusbCtx); + mDeviceList.clear(); // Release devices first + if (mContext) { + mLibusb->exit(mContext); } -}; +} std::vector> UsbDeviceFactory::findAllDevices() { refreshDeviceList(); @@ -51,46 +60,51 @@ std::vector> UsbDeviceFactory::findDevices(uint16_t ssize_t UsbDeviceFactory::refreshDeviceList() { libusb_device** list{nullptr}; - ssize_t ret = libusb_get_device_list(mLibusbCtx, &list); - mLibusbDeviceList.clear(); - if (ret < 0) { - spdlog::error("Error enumarating USB devices"); - } else if (ret == 0) { + ssize_t count = mLibusb->getDeviceList(mContext, &list); + mDeviceList.clear(); + + if (count < 0) { + spdlog::error("Error enumerating USB devices"); + } else if (count == 0) { spdlog::warn("No USB devices found"); + } else { + for (ssize_t i = 0; i < count; i++) { + mLibusb->refDevice(list[i]); // Increment refcount + // Create LibusbDevicePtr with deleter that uses the wrapper + mDeviceList.emplace_back(list[i], LibusbDeviceDeleter{mLibusb}); + } } - for (ssize_t i = 0; i < ret; i++) { - mLibusbDeviceList.emplace_back(list[i]); - } - - libusb_free_device_list(list, false); - return ret; + mLibusb->freeDeviceList(list, false); // Don't unref (we did that above) + return count; } std::vector> UsbDeviceFactory::buildMaskedDeviceVector(uint16_t vidMask, uint16_t pidMask, uint16_t vid, uint16_t pid) { std::vector> matchedDevices; // see libusb/examples/listdevs.c - for (auto& currDev : mLibusbDeviceList) { - struct libusb_device_descriptor currDevDesc{}; + for (auto& dev : mDeviceList) { + libusb_device_descriptor desc{}; - int ret = libusb_get_device_descriptor(currDev.get(), &currDevDesc); - spdlog::trace("Detected Device {:04x}:{:04x} ", currDevDesc.idVendor, currDevDesc.idProduct); - if (ret < 0) { - continue; - } - if (((currDevDesc.idVendor & vidMask) == vid) && ((currDevDesc.idProduct & pidMask) == pid)) { - matchedDevices.push_back(std::make_unique(mLibusbCtx, std::move(currDev))); + int ret = mLibusb->getDeviceDescriptor(dev.get(), &desc); + spdlog::trace("Detected Device {:04x}:{:04x} ", desc.idVendor, desc.idProduct); + + if (ret >= 0 && ((desc.idVendor & vidMask) == vid) && ((desc.idProduct & pidMask) == pid)) { + // Transfer ownership of device to UsbDevice + libusb_device* raw_dev = dev.release(); + + // Create UsbDevice with same wrapper instance + matchedDevices.push_back(std::make_unique(raw_dev, desc, mLibusb)); } } return matchedDevices; } bool UsbDeviceFactory::init() { - auto err = libusb_init(&mLibusbCtx); + int err = mLibusb->init(&mContext); if (err != (int)Error::SUCCESS) { - spdlog::error("Could not intialize libusb"); + spdlog::error("Could not initialize libusb"); return false; } diff --git a/src/libusbwrap/UsbDeviceFactory.hpp b/src/libusbwrap/UsbDeviceFactory.hpp index d8f7efa..812b31f 100644 --- a/src/libusbwrap/UsbDeviceFactory.hpp +++ b/src/libusbwrap/UsbDeviceFactory.hpp @@ -20,8 +20,10 @@ #pragma once #include +#include #include "libusb.h" +#include "libusbwrap/LibusbWrapper.hpp" #include "libusbwrap/UsbDevice.hpp" #include "libusbwrap/interface/IUsbDeviceFactory.hpp" @@ -31,7 +33,12 @@ constexpr const uint16_t LIBUSB_BITMASK_ALL = 0xffff; class UsbDeviceFactory : public IUsbDeviceFactory { public: - UsbDeviceFactory() = default; + // Default constructor (uses real LibusbWrapper) + UsbDeviceFactory(); + + // Constructor for testing (inject mock wrapper) + explicit UsbDeviceFactory(std::shared_ptr libusbWrapper); + virtual ~UsbDeviceFactory(); // delete copy ctor and assignment @@ -40,17 +47,17 @@ class UsbDeviceFactory : public IUsbDeviceFactory { UsbDeviceFactory(UsbDeviceFactory&&) = delete; UsbDeviceFactory& operator=(UsbDeviceFactory&&) = delete; - bool init(); + bool init() override; /** * @brief Gets all devices that are detected by libusb. Will allocate a shared_ptr for each Device - * + * * @return std::vector> Vector of all detected USB devices */ std::vector> findAllDevices() override; /** - * @brief Gets all devices with certain vid/pid combination. + * @brief Gets all devices with certain vid/pid combination. * If only one device of certain type is connected, vector is usually only one element - * + * * @param vid VendorId of the devices to find * @param pid ProductId of the devices to find * @return std::vector> Vector of detected USB devices based on vid/pid @@ -64,8 +71,9 @@ class UsbDeviceFactory : public IUsbDeviceFactory { uint16_t pidMask, uint16_t vid, uint16_t pid); // members - libusb_context* mLibusbCtx{nullptr}; - std::vector mLibusbDeviceList{}; + std::shared_ptr mLibusb; + libusb_context* mContext{nullptr}; // Factory manages lifecycle + std::vector mDeviceList{}; bool mDeviceListInitialized = false; }; } // namespace libusbwrap \ No newline at end of file diff --git a/src/libusbwrap/interface/IUsbDeviceFactory.hpp b/src/libusbwrap/interface/IUsbDeviceFactory.hpp index a4d6ea0..b8b12c7 100644 --- a/src/libusbwrap/interface/IUsbDeviceFactory.hpp +++ b/src/libusbwrap/interface/IUsbDeviceFactory.hpp @@ -29,6 +29,7 @@ namespace libusbwrap { class IUsbDeviceFactory { public: virtual ~IUsbDeviceFactory() = default; + virtual bool init() = 0; virtual std::vector> findAllDevices() = 0; virtual std::vector> findDevices(uint16_t vid, uint16_t pid) = 0; }; diff --git a/src/meson.build b/src/meson.build index 596422f..34e32fd 100644 --- a/src/meson.build +++ b/src/meson.build @@ -7,6 +7,7 @@ ptprnt_hpps = files( 'graphics/LabelBuilder.hpp', 'graphics/Monochrome.hpp', 'libusbwrap/LibUsbTypes.hpp', + 'libusbwrap/LibusbWrapper.hpp', 'libusbwrap/UsbDevice.hpp', 'libusbwrap/UsbDeviceFactory.hpp', 'libusbwrap/interface/IUsbDevice.hpp', @@ -26,6 +27,7 @@ ptprnt_srcs = files( 'graphics/Label.cpp', 'graphics/LabelBuilder.cpp', 'graphics/Monochrome.cpp', + 'libusbwrap/LibusbWrapper.cpp', 'libusbwrap/UsbDevice.cpp', 'libusbwrap/UsbDeviceFactory.cpp', 'printers/FakePrinter.cpp', diff --git a/tests/cli_parser_test/cli_parser_test.cpp b/tests/cli_parser_test/cli_parser_test.cpp new file mode 100644 index 0000000..7309c41 --- /dev/null +++ b/tests/cli_parser_test/cli_parser_test.cpp @@ -0,0 +1,365 @@ +/* + ptrnt - print labels on linux + Copyright (C) 2025 Moritz Martinius + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + + */ + +#include + +#include +#include + +#include "cli/CliParser.hpp" + +namespace ptprnt::cli { + +class CliParserTest : public ::testing::Test { + protected: + void SetUp() override { + parser = std::make_unique("Test Application", "v1.0.0"); + } + + void TearDown() override { parser.reset(); } + + // Helper to convert vector of strings to argc/argv + std::vector makeArgv(const std::vector& args) { + argv_storage.clear(); + argv_storage.reserve(args.size()); + + for (const auto& arg : args) { + argv_storage.push_back(const_cast(arg.c_str())); + } + + return argv_storage; + } + + std::unique_ptr parser; + std::vector argv_storage; +}; + +// Test: Constructor +TEST_F(CliParserTest, Constructor) { + EXPECT_NO_THROW(CliParser("App", "v1.0")); +} + +// Test: Parse with no arguments +TEST_F(CliParserTest, ParseNoArguments) { + std::vector args = {"ptprnt"}; + auto argv = makeArgv(args); + + int result = parser->parse(args.size(), argv.data()); + + EXPECT_EQ(result, 0); + const auto& options = parser->getOptions(); + EXPECT_FALSE(options.verbose); + EXPECT_FALSE(options.trace); + EXPECT_FALSE(options.listDrivers); + EXPECT_EQ(options.printerSelection, "auto"); + EXPECT_TRUE(options.commands.empty()); +} + +// Test: Parse verbose flag +TEST_F(CliParserTest, ParseVerboseShort) { + std::vector args = {"ptprnt", "-v"}; + auto argv = makeArgv(args); + + int result = parser->parse(args.size(), argv.data()); + + EXPECT_EQ(result, 0); + EXPECT_TRUE(parser->getOptions().verbose); +} + +TEST_F(CliParserTest, ParseVerboseLong) { + std::vector args = {"ptprnt", "--verbose"}; + auto argv = makeArgv(args); + + int result = parser->parse(args.size(), argv.data()); + + EXPECT_EQ(result, 0); + EXPECT_TRUE(parser->getOptions().verbose); +} + +// Test: Parse trace flag +TEST_F(CliParserTest, ParseTrace) { + std::vector args = {"ptprnt", "--trace"}; + auto argv = makeArgv(args); + + int result = parser->parse(args.size(), argv.data()); + + EXPECT_EQ(result, 0); + EXPECT_TRUE(parser->getOptions().trace); +} + +// Test: Parse list drivers flag +TEST_F(CliParserTest, ParseListDrivers) { + std::vector args = {"ptprnt", "--list-all-drivers"}; + auto argv = makeArgv(args); + + int result = parser->parse(args.size(), argv.data()); + + EXPECT_EQ(result, 0); + EXPECT_TRUE(parser->getOptions().listDrivers); +} + +// Test: Parse printer selection short +TEST_F(CliParserTest, ParsePrinterShort) { + std::vector args = {"ptprnt", "-p", "P700"}; + auto argv = makeArgv(args); + + int result = parser->parse(args.size(), argv.data()); + + EXPECT_EQ(result, 0); + EXPECT_EQ(parser->getOptions().printerSelection, "P700"); +} + +// Test: Parse printer selection long +TEST_F(CliParserTest, ParsePrinterLong) { + std::vector args = {"ptprnt", "--printer", "FakePrinter"}; + auto argv = makeArgv(args); + + int result = parser->parse(args.size(), argv.data()); + + EXPECT_EQ(result, 0); + EXPECT_EQ(parser->getOptions().printerSelection, "FakePrinter"); +} + +// Test: Parse single text +TEST_F(CliParserTest, ParseSingleText) { + std::vector args = {"ptprnt", "-t", "Hello"}; + auto argv = makeArgv(args); + + int result = parser->parse(args.size(), argv.data()); + + EXPECT_EQ(result, 0); + const auto& commands = parser->getOptions().commands; + ASSERT_EQ(commands.size(), 1); + EXPECT_EQ(commands[0].first, CommandType::Text); + EXPECT_EQ(commands[0].second, "Hello"); +} + +// Test: Parse multiple texts +TEST_F(CliParserTest, ParseMultipleTexts) { + std::vector args = {"ptprnt", "-t", "Hello", "-t", "World"}; + auto argv = makeArgv(args); + + int result = parser->parse(args.size(), argv.data()); + + EXPECT_EQ(result, 0); + const auto& commands = parser->getOptions().commands; + ASSERT_EQ(commands.size(), 2); + EXPECT_EQ(commands[0].first, CommandType::Text); + EXPECT_EQ(commands[0].second, "Hello"); + EXPECT_EQ(commands[1].first, CommandType::Text); + EXPECT_EQ(commands[1].second, "World"); +} + +// Test: Parse font +TEST_F(CliParserTest, ParseFont) { + std::vector args = {"ptprnt", "-f", "monospace", "-t", "Test"}; + auto argv = makeArgv(args); + + int result = parser->parse(args.size(), argv.data()); + + EXPECT_EQ(result, 0); + const auto& commands = parser->getOptions().commands; + ASSERT_EQ(commands.size(), 2); + EXPECT_EQ(commands[0].first, CommandType::Font); + EXPECT_EQ(commands[0].second, "monospace"); +} + +// Test: Parse font size +TEST_F(CliParserTest, ParseFontSize) { + std::vector args = {"ptprnt", "-s", "48", "-t", "Large"}; + auto argv = makeArgv(args); + + int result = parser->parse(args.size(), argv.data()); + + EXPECT_EQ(result, 0); + const auto& commands = parser->getOptions().commands; + ASSERT_GE(commands.size(), 1); + EXPECT_EQ(commands[0].first, CommandType::FontSize); + EXPECT_EQ(commands[0].second, "48"); +} + +// Test: Parse horizontal alignment +TEST_F(CliParserTest, ParseHAlign) { + std::vector args = {"ptprnt", "--halign", "center", "-t", "Centered"}; + auto argv = makeArgv(args); + + int result = parser->parse(args.size(), argv.data()); + + EXPECT_EQ(result, 0); + const auto& commands = parser->getOptions().commands; + ASSERT_GE(commands.size(), 1); + EXPECT_EQ(commands[0].first, CommandType::HAlign); + EXPECT_EQ(commands[0].second, "center"); +} + +// Test: Parse vertical alignment +TEST_F(CliParserTest, ParseVAlign) { + std::vector args = {"ptprnt", "--valign", "top", "-t", "Top"}; + auto argv = makeArgv(args); + + int result = parser->parse(args.size(), argv.data()); + + EXPECT_EQ(result, 0); + const auto& commands = parser->getOptions().commands; + ASSERT_GE(commands.size(), 1); + EXPECT_EQ(commands[0].first, CommandType::VAlign); + EXPECT_EQ(commands[0].second, "top"); +} + +// Test: Parse new label flag +TEST_F(CliParserTest, ParseNewLabel) { + std::vector args = {"ptprnt", "-t", "First", "--new", "-t", "Second"}; + auto argv = makeArgv(args); + + int result = parser->parse(args.size(), argv.data()); + + EXPECT_EQ(result, 0); + const auto& commands = parser->getOptions().commands; + ASSERT_EQ(commands.size(), 3); + EXPECT_EQ(commands[0].first, CommandType::Text); + EXPECT_EQ(commands[1].first, CommandType::NewLabel); + EXPECT_EQ(commands[2].first, CommandType::Text); +} + +// Test: Parse complex command sequence +TEST_F(CliParserTest, ParseComplexSequence) { + std::vector args = { + "ptprnt", + "-f", "serif", + "-s", "24", + "--halign", "center", + "--valign", "middle", + "-t", "Title", + "--new", + "-f", "monospace", + "-s", "16", + "-t", "Body" + }; + auto argv = makeArgv(args); + + int result = parser->parse(args.size(), argv.data()); + + EXPECT_EQ(result, 0); + const auto& commands = parser->getOptions().commands; + ASSERT_EQ(commands.size(), 9); + + // Verify order is preserved + EXPECT_EQ(commands[0].first, CommandType::Font); + EXPECT_EQ(commands[1].first, CommandType::FontSize); + EXPECT_EQ(commands[2].first, CommandType::HAlign); + EXPECT_EQ(commands[3].first, CommandType::VAlign); + EXPECT_EQ(commands[4].first, CommandType::Text); + EXPECT_EQ(commands[5].first, CommandType::NewLabel); + EXPECT_EQ(commands[6].first, CommandType::Font); + EXPECT_EQ(commands[7].first, CommandType::FontSize); + EXPECT_EQ(commands[8].first, CommandType::Text); +} + +// Test: Parse with verbose and printer options +TEST_F(CliParserTest, ParseCombinedOptions) { + std::vector args = { + "ptprnt", + "-v", + "--trace", + "-p", "P700", + "-t", "Test" + }; + auto argv = makeArgv(args); + + int result = parser->parse(args.size(), argv.data()); + + EXPECT_EQ(result, 0); + EXPECT_TRUE(parser->getOptions().verbose); + EXPECT_TRUE(parser->getOptions().trace); + EXPECT_EQ(parser->getOptions().printerSelection, "P700"); + EXPECT_FALSE(parser->getOptions().commands.empty()); +} + +// Test: Parse help flag (should return 1) +TEST_F(CliParserTest, ParseHelp) { + std::vector args = {"ptprnt", "-h"}; + auto argv = makeArgv(args); + + int result = parser->parse(args.size(), argv.data()); + + EXPECT_EQ(result, 1); // Signal clean exit +} + +TEST_F(CliParserTest, ParseHelpLong) { + std::vector args = {"ptprnt", "--help"}; + auto argv = makeArgv(args); + + int result = parser->parse(args.size(), argv.data()); + + EXPECT_EQ(result, 1); // Signal clean exit +} + +// Test: Parse version flag (should return 1) +TEST_F(CliParserTest, ParseVersion) { + std::vector args = {"ptprnt", "-V"}; + auto argv = makeArgv(args); + + int result = parser->parse(args.size(), argv.data()); + + EXPECT_EQ(result, 1); // Signal clean exit +} + +TEST_F(CliParserTest, ParseVersionLong) { + std::vector args = {"ptprnt", "--version"}; + auto argv = makeArgv(args); + + int result = parser->parse(args.size(), argv.data()); + + EXPECT_EQ(result, 1); // Signal clean exit +} + +// Test: Parse invalid option (should return -1) +TEST_F(CliParserTest, ParseInvalidOption) { + std::vector args = {"ptprnt", "--invalid-option"}; + auto argv = makeArgv(args); + + int result = parser->parse(args.size(), argv.data()); + + EXPECT_EQ(result, -1); // Signal error +} + +// Test: Default printer selection is "auto" +TEST_F(CliParserTest, DefaultPrinterSelection) { + std::vector args = {"ptprnt", "-t", "Test"}; + auto argv = makeArgv(args); + + parser->parse(args.size(), argv.data()); + + EXPECT_EQ(parser->getOptions().printerSelection, "auto"); +} + +// Test: Long text with spaces +TEST_F(CliParserTest, ParseTextWithSpaces) { + std::vector args = {"ptprnt", "-t", "Hello World"}; + auto argv = makeArgv(args); + + int result = parser->parse(args.size(), argv.data()); + + EXPECT_EQ(result, 0); + const auto& commands = parser->getOptions().commands; + ASSERT_EQ(commands.size(), 1); + EXPECT_EQ(commands[0].second, "Hello World"); +} + +} // namespace ptprnt::cli diff --git a/tests/meson.build b/tests/meson.build index c664d95..c169341 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -9,6 +9,9 @@ test_sources = [ 'printer_service_test/printer_service_test.cpp', 'p700_printer_test/p700_printer_test.cpp', 'fake_printer_test/fake_printer_test.cpp', + 'cli_parser_test/cli_parser_test.cpp', + 'ptouch_print_test/ptouch_print_test.cpp', + 'usb_device_test/usb_device_test.cpp', # Source files under test - graphics '../src/graphics/Bitmap.cpp', @@ -24,7 +27,14 @@ test_sources = [ '../src/printers/P700Printer.cpp', '../src/printers/FakePrinter.cpp', + # Source files under test - CLI + '../src/cli/CliParser.cpp', + + # Source files under test - Main app + '../src/PtouchPrint.cpp', + # Source files under test - USB + '../src/libusbwrap/LibusbWrapper.cpp', '../src/libusbwrap/UsbDevice.cpp', '../src/libusbwrap/UsbDeviceFactory.cpp', ] diff --git a/tests/mocks/MockLibusbWrapper.hpp b/tests/mocks/MockLibusbWrapper.hpp new file mode 100644 index 0000000..bde1cd9 --- /dev/null +++ b/tests/mocks/MockLibusbWrapper.hpp @@ -0,0 +1,76 @@ +/* + ptrnt - print labels on linux + Copyright (C) 2025 Moritz Martinius + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + + */ + +#pragma once + +#include + +#include "libusbwrap/LibusbWrapper.hpp" + +namespace libusbwrap { + +/** + * @brief GMock implementation of ILibusbWrapper for unit testing + * + * This mock allows tests to verify that UsbDevice and UsbDeviceFactory + * correctly interact with the libusb API without requiring actual USB hardware. + */ +class MockLibusbWrapper : public ILibusbWrapper { + public: + // Context management + MOCK_METHOD(int, init, (libusb_context * *ctx), (override)); + MOCK_METHOD(void, exit, (libusb_context * ctx), (override)); + + // Device enumeration + MOCK_METHOD(ssize_t, getDeviceList, (libusb_context * ctx, libusb_device***list), (override)); + MOCK_METHOD(void, freeDeviceList, (libusb_device * *list, int unrefDevices), (override)); + MOCK_METHOD(void, refDevice, (libusb_device * dev), (override)); + MOCK_METHOD(void, unrefDevice, (libusb_device * dev), (override)); + + // Device descriptor + MOCK_METHOD(int, getDeviceDescriptor, (libusb_device * dev, libusb_device_descriptor* desc), (override)); + + // Device opening/closing + MOCK_METHOD(int, open, (libusb_device * dev, libusb_device_handle** handle), (override)); + MOCK_METHOD(void, close, (libusb_device_handle * handle), (override)); + + // Device information + MOCK_METHOD(int, getSpeed, (libusb_device * dev), (override)); + MOCK_METHOD(uint8_t, getBusNumber, (libusb_device * dev), (override)); + MOCK_METHOD(uint8_t, getPortNumber, (libusb_device * dev), (override)); + + // Kernel driver management + MOCK_METHOD(int, kernelDriverActive, (libusb_device_handle * handle, int interfaceNo), (override)); + MOCK_METHOD(int, detachKernelDriver, (libusb_device_handle * handle, int interfaceNo), (override)); + + // Interface management + MOCK_METHOD(int, claimInterface, (libusb_device_handle * handle, int interfaceNo), (override)); + MOCK_METHOD(int, releaseInterface, (libusb_device_handle * handle, int interfaceNo), (override)); + + // Data transfer + MOCK_METHOD(int, bulkTransfer, + (libusb_device_handle * handle, uint8_t endpoint, unsigned char* data, int length, int* transferred, + unsigned int timeout), + (override)); + + // Error handling + MOCK_METHOD(const char*, errorName, (int errorCode), (override)); +}; + +} // namespace libusbwrap diff --git a/tests/ptouch_print_test/ptouch_print_test.cpp b/tests/ptouch_print_test/ptouch_print_test.cpp new file mode 100644 index 0000000..610ce01 --- /dev/null +++ b/tests/ptouch_print_test/ptouch_print_test.cpp @@ -0,0 +1,374 @@ +/* + ptrnt - print labels on linux + Copyright (C) 2025 Moritz Martinius + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + + */ + +#include +#include + +#include "PtouchPrint.hpp" +#include "cli/interface/ICliParser.hpp" +#include "core/interface/IPrinterService.hpp" +#include "printers/interface/IPrinterDriver.hpp" + +using ::testing::_; +using ::testing::NiceMock; +using ::testing::Return; +using ::testing::ReturnRef; + +namespace ptprnt { + +// Mock CLI Parser +class MockCliParser : public cli::ICliParser { + public: + MOCK_METHOD(int, parse, (int argc, char** argv), (override)); + MOCK_METHOD(const cli::CliOptions&, getOptions, (), (const, override)); + + cli::CliOptions options; +}; + +// Mock Printer Service +class MockPrinterService : public core::IPrinterService { + public: + MOCK_METHOD(bool, initialize, (), (override)); + MOCK_METHOD(std::vector>, detectPrinters, (), (override)); + MOCK_METHOD(std::shared_ptr, selectPrinter, (const std::string& selection), (override)); + MOCK_METHOD(std::shared_ptr, getCurrentPrinter, (), (const, override)); + MOCK_METHOD(bool, printLabel, (std::unique_ptr label), (override)); +}; + +// Mock Printer Driver +class MockPrinterDriver : public IPrinterDriver { + public: + MOCK_METHOD(std::string_view, getDriverName, (), (override)); + MOCK_METHOD(std::string_view, getName, (), (override)); + MOCK_METHOD(libusbwrap::usbId, getUsbId, (), (override)); + MOCK_METHOD(std::string_view, getVersion, (), (override)); + MOCK_METHOD(PrinterInfo, getPrinterInfo, (), (override)); + MOCK_METHOD(PrinterStatus, getPrinterStatus, (), (override)); + MOCK_METHOD(bool, attachUsbDevice, (std::shared_ptr usbHndl), (override)); + MOCK_METHOD(bool, detachUsbDevice, (), (override)); + MOCK_METHOD(bool, printBitmap, (const graphics::Bitmap& bitmap), (override)); + MOCK_METHOD(bool, printMonochromeData, (const graphics::MonochromeData& data), (override)); + MOCK_METHOD(bool, printLabel, (std::unique_ptr label), (override)); + MOCK_METHOD(bool, print, (), (override)); +}; + +class PtouchPrintTest : public ::testing::Test { + protected: + void SetUp() override { + mockCliParser = std::make_unique>(); + mockPrinterService = std::make_unique>(); + + // Store raw pointers for setting expectations + cliParserPtr = mockCliParser.get(); + printerServicePtr = mockPrinterService.get(); + + // Default behavior: parse succeeds and returns empty options + ON_CALL(*cliParserPtr, parse(_, _)).WillByDefault(Return(0)); + ON_CALL(*cliParserPtr, getOptions()).WillByDefault(ReturnRef(cliParserPtr->options)); + ON_CALL(*printerServicePtr, initialize()).WillByDefault(Return(true)); + ON_CALL(*printerServicePtr, printLabel(_)).WillByDefault(Return(true)); + } + + void TearDown() override {} + + std::unique_ptr mockCliParser; + std::unique_ptr mockPrinterService; + MockCliParser* cliParserPtr; + MockPrinterService* printerServicePtr; +}; + +// Test: Constructor with default implementations +TEST_F(PtouchPrintTest, ConstructorDefault) { + EXPECT_NO_THROW(PtouchPrint app("v1.0.0")); +} + +// Test: Constructor with custom implementations +TEST_F(PtouchPrintTest, ConstructorCustom) { + EXPECT_NO_THROW({ + PtouchPrint app("v1.0.0", std::move(mockCliParser), std::move(mockPrinterService)); + }); +} + +// Test: init with successful parse +TEST_F(PtouchPrintTest, InitSuccess) { + PtouchPrint app("v1.0.0", std::move(mockCliParser), std::move(mockPrinterService)); + + char* argv[] = {(char*)"ptprnt"}; + int result = app.init(1, argv); + + EXPECT_EQ(result, 0); +} + +// Test: init with parse returning help (should return 1) +TEST_F(PtouchPrintTest, InitHelp) { + ON_CALL(*cliParserPtr, parse(_, _)).WillByDefault(Return(1)); + + PtouchPrint app("v1.0.0", std::move(mockCliParser), std::move(mockPrinterService)); + + char* argv[] = {(char*)"ptprnt", (char*)"-h"}; + int result = app.init(2, argv); + + EXPECT_EQ(result, 1); // Clean exit +} + +// Test: init with parse error (should return -1) +TEST_F(PtouchPrintTest, InitParseError) { + ON_CALL(*cliParserPtr, parse(_, _)).WillByDefault(Return(-1)); + + PtouchPrint app("v1.0.0", std::move(mockCliParser), std::move(mockPrinterService)); + + char* argv[] = {(char*)"ptprnt", (char*)"--invalid"}; + int result = app.init(2, argv); + + EXPECT_EQ(result, -1); // Error +} + +// Test: init with printer service initialization failure +TEST_F(PtouchPrintTest, InitPrinterServiceFailure) { + ON_CALL(*printerServicePtr, initialize()).WillByDefault(Return(false)); + + PtouchPrint app("v1.0.0", std::move(mockCliParser), std::move(mockPrinterService)); + + char* argv[] = {(char*)"ptprnt"}; + int result = app.init(1, argv); + + EXPECT_EQ(result, -1); // Error +} + +// Test: run with list drivers option +TEST_F(PtouchPrintTest, RunListDrivers) { + cliParserPtr->options.listDrivers = true; + + PtouchPrint app("v1.0.0", std::move(mockCliParser), std::move(mockPrinterService)); + + char* argv[] = {(char*)"ptprnt"}; + app.init(1, argv); + + int result = app.run(); + + EXPECT_EQ(result, 0); +} + +// Test: run with no commands (should warn but succeed) +TEST_F(PtouchPrintTest, RunNoCommands) { + auto mockPrinter = std::make_shared>(); + PrinterStatus status{.tapeWidthMm = 12}; + + ON_CALL(*mockPrinter, getPrinterStatus()).WillByDefault(Return(status)); + ON_CALL(*printerServicePtr, selectPrinter(_)).WillByDefault(Return(mockPrinter)); + + cliParserPtr->options.commands.clear(); + + PtouchPrint app("v1.0.0", std::move(mockCliParser), std::move(mockPrinterService)); + + char* argv[] = {(char*)"ptprnt"}; + app.init(1, argv); + + int result = app.run(); + + EXPECT_EQ(result, 0); +} + +// Test: run with printer selection failure +TEST_F(PtouchPrintTest, RunPrinterSelectionFailure) { + ON_CALL(*printerServicePtr, selectPrinter(_)).WillByDefault(Return(nullptr)); + + cliParserPtr->options.commands.push_back({cli::CommandType::Text, "Test"}); + + PtouchPrint app("v1.0.0", std::move(mockCliParser), std::move(mockPrinterService)); + + char* argv[] = {(char*)"ptprnt"}; + app.init(1, argv); + + int result = app.run(); + + EXPECT_EQ(result, -1); +} + +// Test: run with simple text command +TEST_F(PtouchPrintTest, RunSimpleText) { + auto mockPrinter = std::make_shared>(); + PrinterInfo info{.driverName = "Test", .name = "Test", .version = "v1.0", .usbId = {0, 0}, .pixelLines = 128}; + PrinterStatus status{.tapeWidthMm = 12}; + + ON_CALL(*mockPrinter, getPrinterInfo()).WillByDefault(Return(info)); + ON_CALL(*mockPrinter, getPrinterStatus()).WillByDefault(Return(status)); + ON_CALL(*mockPrinter, printLabel(_)).WillByDefault(Return(true)); + ON_CALL(*printerServicePtr, selectPrinter(_)).WillByDefault(Return(mockPrinter)); + + cliParserPtr->options.commands.push_back({cli::CommandType::Text, "Hello"}); + + PtouchPrint app("v1.0.0", std::move(mockCliParser), std::move(mockPrinterService)); + + char* argv[] = {(char*)"ptprnt"}; + app.init(1, argv); + + int result = app.run(); + + EXPECT_EQ(result, 0); +} + +// Test: run with font and text commands +TEST_F(PtouchPrintTest, RunWithFormatting) { + auto mockPrinter = std::make_shared>(); + PrinterInfo info{.driverName = "Test", .name = "Test", .version = "v1.0", .usbId = {0, 0}, .pixelLines = 128}; + PrinterStatus status{.tapeWidthMm = 12}; + + ON_CALL(*mockPrinter, getPrinterInfo()).WillByDefault(Return(info)); + ON_CALL(*mockPrinter, getPrinterStatus()).WillByDefault(Return(status)); + ON_CALL(*mockPrinter, printLabel(_)).WillByDefault(Return(true)); + ON_CALL(*printerServicePtr, selectPrinter(_)).WillByDefault(Return(mockPrinter)); + + cliParserPtr->options.commands.push_back({cli::CommandType::Font, "monospace"}); + cliParserPtr->options.commands.push_back({cli::CommandType::FontSize, "48"}); + cliParserPtr->options.commands.push_back({cli::CommandType::Text, "Formatted"}); + + PtouchPrint app("v1.0.0", std::move(mockCliParser), std::move(mockPrinterService)); + + char* argv[] = {(char*)"ptprnt"}; + app.init(1, argv); + + int result = app.run(); + + EXPECT_EQ(result, 0); +} + +// Test: run with alignment commands +TEST_F(PtouchPrintTest, RunWithAlignment) { + auto mockPrinter = std::make_shared>(); + PrinterInfo info{.driverName = "Test", .name = "Test", .version = "v1.0", .usbId = {0, 0}, .pixelLines = 128}; + PrinterStatus status{.tapeWidthMm = 12}; + + ON_CALL(*mockPrinter, getPrinterInfo()).WillByDefault(Return(info)); + ON_CALL(*mockPrinter, getPrinterStatus()).WillByDefault(Return(status)); + ON_CALL(*mockPrinter, printLabel(_)).WillByDefault(Return(true)); + ON_CALL(*printerServicePtr, selectPrinter(_)).WillByDefault(Return(mockPrinter)); + + cliParserPtr->options.commands.push_back({cli::CommandType::HAlign, "center"}); + cliParserPtr->options.commands.push_back({cli::CommandType::VAlign, "middle"}); + cliParserPtr->options.commands.push_back({cli::CommandType::Text, "Centered"}); + + PtouchPrint app("v1.0.0", std::move(mockCliParser), std::move(mockPrinterService)); + + char* argv[] = {(char*)"ptprnt"}; + app.init(1, argv); + + int result = app.run(); + + EXPECT_EQ(result, 0); +} + +// Test: run with invalid alignment (should handle gracefully) +TEST_F(PtouchPrintTest, RunWithInvalidAlignment) { + auto mockPrinter = std::make_shared>(); + PrinterInfo info{.driverName = "Test", .name = "Test", .version = "v1.0", .usbId = {0, 0}, .pixelLines = 128}; + PrinterStatus status{.tapeWidthMm = 12}; + + ON_CALL(*mockPrinter, getPrinterInfo()).WillByDefault(Return(info)); + ON_CALL(*mockPrinter, getPrinterStatus()).WillByDefault(Return(status)); + ON_CALL(*mockPrinter, printLabel(_)).WillByDefault(Return(true)); + ON_CALL(*printerServicePtr, selectPrinter(_)).WillByDefault(Return(mockPrinter)); + + cliParserPtr->options.commands.push_back({cli::CommandType::HAlign, "invalid"}); + cliParserPtr->options.commands.push_back({cli::CommandType::Text, "Test"}); + + PtouchPrint app("v1.0.0", std::move(mockCliParser), std::move(mockPrinterService)); + + char* argv[] = {(char*)"ptprnt"}; + app.init(1, argv); + + int result = app.run(); + + EXPECT_EQ(result, 0); // Should handle gracefully +} + +// Test: run with new label command +TEST_F(PtouchPrintTest, RunWithNewLabel) { + auto mockPrinter = std::make_shared>(); + PrinterInfo info{.driverName = "Test", .name = "Test", .version = "v1.0", .usbId = {0, 0}, .pixelLines = 128}; + PrinterStatus status{.tapeWidthMm = 12}; + + ON_CALL(*mockPrinter, getPrinterInfo()).WillByDefault(Return(info)); + ON_CALL(*mockPrinter, getPrinterStatus()).WillByDefault(Return(status)); + ON_CALL(*mockPrinter, printLabel(_)).WillByDefault(Return(true)); + ON_CALL(*printerServicePtr, selectPrinter(_)).WillByDefault(Return(mockPrinter)); + + cliParserPtr->options.commands.push_back({cli::CommandType::Text, "First"}); + cliParserPtr->options.commands.push_back({cli::CommandType::NewLabel, ""}); + cliParserPtr->options.commands.push_back({cli::CommandType::Text, "Second"}); + + PtouchPrint app("v1.0.0", std::move(mockCliParser), std::move(mockPrinterService)); + + char* argv[] = {(char*)"ptprnt"}; + app.init(1, argv); + + int result = app.run(); + + EXPECT_EQ(result, 0); +} + +// Test: run with verbose option +TEST_F(PtouchPrintTest, RunWithVerbose) { + auto mockPrinter = std::make_shared>(); + PrinterInfo info{.driverName = "Test", .name = "Test", .version = "v1.0", .usbId = {0, 0}, .pixelLines = 128}; + PrinterStatus status{.tapeWidthMm = 12}; + + ON_CALL(*mockPrinter, getPrinterInfo()).WillByDefault(Return(info)); + ON_CALL(*mockPrinter, getPrinterStatus()).WillByDefault(Return(status)); + ON_CALL(*mockPrinter, printLabel(_)).WillByDefault(Return(true)); + ON_CALL(*printerServicePtr, selectPrinter(_)).WillByDefault(Return(mockPrinter)); + + cliParserPtr->options.verbose = true; + cliParserPtr->options.commands.push_back({cli::CommandType::Text, "Test"}); + + PtouchPrint app("v1.0.0", std::move(mockCliParser), std::move(mockPrinterService)); + + char* argv[] = {(char*)"ptprnt"}; + app.init(1, argv); + + int result = app.run(); + + EXPECT_EQ(result, 0); +} + +// Test: run with trace option +TEST_F(PtouchPrintTest, RunWithTrace) { + auto mockPrinter = std::make_shared>(); + PrinterInfo info{.driverName = "Test", .name = "Test", .version = "v1.0", .usbId = {0, 0}, .pixelLines = 128}; + PrinterStatus status{.tapeWidthMm = 12}; + + ON_CALL(*mockPrinter, getPrinterInfo()).WillByDefault(Return(info)); + ON_CALL(*mockPrinter, getPrinterStatus()).WillByDefault(Return(status)); + ON_CALL(*mockPrinter, printLabel(_)).WillByDefault(Return(true)); + ON_CALL(*printerServicePtr, selectPrinter(_)).WillByDefault(Return(mockPrinter)); + + cliParserPtr->options.trace = true; + cliParserPtr->options.commands.push_back({cli::CommandType::Text, "Test"}); + + PtouchPrint app("v1.0.0", std::move(mockCliParser), std::move(mockPrinterService)); + + char* argv[] = {(char*)"ptprnt"}; + app.init(1, argv); + + int result = app.run(); + + EXPECT_EQ(result, 0); +} + +} // namespace ptprnt diff --git a/tests/usb_device_test/usb_device_test.cpp b/tests/usb_device_test/usb_device_test.cpp new file mode 100644 index 0000000..6d57665 --- /dev/null +++ b/tests/usb_device_test/usb_device_test.cpp @@ -0,0 +1,296 @@ +/* + ptrnt - print labels on linux + Copyright (C) 2025 Moritz Martinius + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + + */ + +#include "libusbwrap/UsbDevice.hpp" + +#include +#include + +#include + +#include "libusb.h" +#include "libusbwrap/LibUsbTypes.hpp" +#include "mocks/MockLibusbWrapper.hpp" + +using ::testing::_; +using ::testing::DoAll; +using ::testing::NiceMock; +using ::testing::Return; +using ::testing::SetArgPointee; + +namespace libusbwrap { + +// Test fixture for UsbDevice tests +class UsbDeviceTest : public ::testing::Test { + protected: + void SetUp() override { + mockLibusb = std::make_shared>(); + + // Create mock device pointer + mockDevice = reinterpret_cast(0x1000); + + // Create mock device handle + mockHandle = reinterpret_cast(0x2000); + + // Setup device descriptor + desc.idVendor = 0x04f9; // Brother vendor ID + desc.idProduct = 0x2042; // P700 product ID + + // Default behaviors + ON_CALL(*mockLibusb, open(_, _)) + .WillByDefault(DoAll(SetArgPointee<1>(mockHandle), Return(0))); + ON_CALL(*mockLibusb, getSpeed(_)).WillByDefault(Return(LIBUSB_SPEED_FULL)); + ON_CALL(*mockLibusb, getBusNumber(_)).WillByDefault(Return(1)); + ON_CALL(*mockLibusb, getPortNumber(_)).WillByDefault(Return(2)); + ON_CALL(*mockLibusb, errorName(_)).WillByDefault(Return("LIBUSB_SUCCESS")); + } + + std::shared_ptr mockLibusb; + libusb_device* mockDevice; + libusb_device_handle* mockHandle; + libusb_device_descriptor desc{}; +}; + +// Constructor tests +TEST_F(UsbDeviceTest, ConstructorWithValidDevice) { + EXPECT_NO_THROW({ + auto device = std::make_unique(mockDevice, desc, mockLibusb); + EXPECT_NE(device, nullptr); + }); +} + +TEST_F(UsbDeviceTest, ConstructorWithNullptrThrows) { + EXPECT_THROW({ auto device = std::make_unique(nullptr, desc, mockLibusb); }, std::invalid_argument); +} + +// Open/Close tests +TEST_F(UsbDeviceTest, OpenSuccess) { + auto device = std::make_unique(mockDevice, desc, mockLibusb); + + EXPECT_CALL(*mockLibusb, open(mockDevice, _)) + .WillOnce(DoAll(SetArgPointee<1>(mockHandle), Return(0))); + + EXPECT_TRUE(device->open()); +} + +TEST_F(UsbDeviceTest, OpenFailure) { + auto device = std::make_unique(mockDevice, desc, mockLibusb); + + EXPECT_CALL(*mockLibusb, open(mockDevice, _)) + .WillOnce(Return(LIBUSB_ERROR_ACCESS)); + + EXPECT_FALSE(device->open()); + EXPECT_EQ(device->getLastError(), Error::ACCESS); +} + +TEST_F(UsbDeviceTest, CloseWithOpenDevice) { + auto device = std::make_unique(mockDevice, desc, mockLibusb); + device->open(); + + EXPECT_CALL(*mockLibusb, close(mockHandle)).Times(1); + device->close(); +} + +TEST_F(UsbDeviceTest, CloseWithoutOpenDevice) { + auto device = std::make_unique(mockDevice, desc, mockLibusb); + + // Should not call close if device was never opened + EXPECT_CALL(*mockLibusb, close(_)).Times(0); + device->close(); +} + +TEST_F(UsbDeviceTest, DestructorClosesOpenDevice) { + EXPECT_CALL(*mockLibusb, close(mockHandle)).Times(1); + + { + auto device = std::make_unique(mockDevice, desc, mockLibusb); + device->open(); + // Device goes out of scope, destructor should call close + } +} + +// Kernel driver tests +TEST_F(UsbDeviceTest, DetachKernelDriverWhenActive) { + auto device = std::make_unique(mockDevice, desc, mockLibusb); + device->open(); + + EXPECT_CALL(*mockLibusb, kernelDriverActive(mockHandle, 0)) + .WillOnce(Return(1)); // Active + EXPECT_CALL(*mockLibusb, detachKernelDriver(mockHandle, 0)) + .WillOnce(Return(0)); // Success + + EXPECT_TRUE(device->detachKernelDriver(0)); +} + +TEST_F(UsbDeviceTest, DetachKernelDriverWhenNotActive) { + auto device = std::make_unique(mockDevice, desc, mockLibusb); + device->open(); + + EXPECT_CALL(*mockLibusb, kernelDriverActive(mockHandle, 0)) + .WillOnce(Return(0)); // Not active + EXPECT_CALL(*mockLibusb, detachKernelDriver(_, _)) + .Times(0); // Should not call detach + + EXPECT_TRUE(device->detachKernelDriver(0)); +} + +TEST_F(UsbDeviceTest, DetachKernelDriverFailure) { + auto device = std::make_unique(mockDevice, desc, mockLibusb); + device->open(); + + EXPECT_CALL(*mockLibusb, kernelDriverActive(mockHandle, 0)) + .WillOnce(Return(1)); // Active + EXPECT_CALL(*mockLibusb, detachKernelDriver(mockHandle, 0)) + .WillOnce(Return(LIBUSB_ERROR_NOT_FOUND)); + + EXPECT_FALSE(device->detachKernelDriver(0)); + EXPECT_EQ(device->getLastError(), Error::NOT_FOUND); +} + +// Interface tests +TEST_F(UsbDeviceTest, ClaimInterfaceSuccess) { + auto device = std::make_unique(mockDevice, desc, mockLibusb); + device->open(); + + EXPECT_CALL(*mockLibusb, claimInterface(mockHandle, 0)) + .WillOnce(Return(0)); + + EXPECT_TRUE(device->claimInterface(0)); +} + +TEST_F(UsbDeviceTest, ClaimInterfaceFailure) { + auto device = std::make_unique(mockDevice, desc, mockLibusb); + device->open(); + + EXPECT_CALL(*mockLibusb, claimInterface(mockHandle, 0)) + .WillOnce(Return(LIBUSB_ERROR_BUSY)); + + EXPECT_FALSE(device->claimInterface(0)); + EXPECT_EQ(device->getLastError(), Error::BUSY); +} + +TEST_F(UsbDeviceTest, ReleaseInterfaceSuccess) { + auto device = std::make_unique(mockDevice, desc, mockLibusb); + device->open(); + + EXPECT_CALL(*mockLibusb, releaseInterface(mockHandle, 0)) + .WillOnce(Return(0)); + + EXPECT_TRUE(device->releaseInterface(0)); +} + +TEST_F(UsbDeviceTest, ReleaseInterfaceFailure) { + auto device = std::make_unique(mockDevice, desc, mockLibusb); + device->open(); + + EXPECT_CALL(*mockLibusb, releaseInterface(mockHandle, 0)) + .WillOnce(Return(LIBUSB_ERROR_NOT_FOUND)); + + EXPECT_FALSE(device->releaseInterface(0)); + EXPECT_EQ(device->getLastError(), Error::NOT_FOUND); +} + +// Bulk transfer tests +TEST_F(UsbDeviceTest, BulkTransferSuccess) { + auto device = std::make_unique(mockDevice, desc, mockLibusb); + device->open(); + + std::vector data = {0x01, 0x02, 0x03}; + int transferred = 0; + + EXPECT_CALL(*mockLibusb, bulkTransfer(mockHandle, 0x02, _, 3, _, 1000)) + .WillOnce(DoAll(SetArgPointee<4>(3), Return(0))); + + EXPECT_TRUE(device->bulkTransfer(0x02, data, &transferred, 1000)); +} + +TEST_F(UsbDeviceTest, BulkTransferFailure) { + auto device = std::make_unique(mockDevice, desc, mockLibusb); + device->open(); + + std::vector data = {0x01, 0x02, 0x03}; + int transferred = 0; + + EXPECT_CALL(*mockLibusb, bulkTransfer(mockHandle, 0x02, _, 3, _, 1000)) + .WillOnce(Return(LIBUSB_ERROR_TIMEOUT)); + + EXPECT_FALSE(device->bulkTransfer(0x02, data, &transferred, 1000)); + EXPECT_EQ(device->getLastError(), Error::TIMEOUT); +} + +// Getter tests +TEST_F(UsbDeviceTest, GetUsbId) { + auto device = std::make_unique(mockDevice, desc, mockLibusb); + + auto usbId = device->getUsbId(); + EXPECT_EQ(usbId.first, 0x04f9); // vid + EXPECT_EQ(usbId.second, 0x2042); // pid +} + +TEST_F(UsbDeviceTest, GetSpeed) { + auto device = std::make_unique(mockDevice, desc, mockLibusb); + + EXPECT_CALL(*mockLibusb, getSpeed(mockDevice)) + .WillOnce(Return(LIBUSB_SPEED_HIGH)); + + EXPECT_EQ(device->getSpeed(), device::Speed::HIGH); +} + +TEST_F(UsbDeviceTest, GetBusNumber) { + auto device = std::make_unique(mockDevice, desc, mockLibusb); + + EXPECT_CALL(*mockLibusb, getBusNumber(mockDevice)) + .WillOnce(Return(5)); + + EXPECT_EQ(device->getBusNumber(), 5); +} + +TEST_F(UsbDeviceTest, GetPortNumber) { + auto device = std::make_unique(mockDevice, desc, mockLibusb); + + EXPECT_CALL(*mockLibusb, getPortNumber(mockDevice)) + .WillOnce(Return(3)); + + EXPECT_EQ(device->getPortNumber(), 3); +} + +TEST_F(UsbDeviceTest, GetLastError) { + auto device = std::make_unique(mockDevice, desc, mockLibusb); + + // Initially no error + EXPECT_EQ(device->getLastError(), Error::SUCCESS); + + // After a failed operation + EXPECT_CALL(*mockLibusb, open(_, _)) + .WillOnce(Return(LIBUSB_ERROR_NO_DEVICE)); + device->open(); + + EXPECT_EQ(device->getLastError(), Error::NO_DEVICE); +} + +TEST_F(UsbDeviceTest, GetLastErrorString) { + auto device = std::make_unique(mockDevice, desc, mockLibusb); + + EXPECT_CALL(*mockLibusb, errorName(static_cast(Error::SUCCESS))) + .WillOnce(Return("LIBUSB_SUCCESS")); + + EXPECT_EQ(device->getLastErrorString(), "LIBUSB_SUCCESS"); +} + +} // namespace libusbwrap