From 97edea85af797eac0d73064e4274b0a346c48dee Mon Sep 17 00:00:00 2001 From: Moritz Martinius Date: Tue, 21 Oct 2025 18:55:32 +0000 Subject: [PATCH] Create more unit tests for USB Devices and Printer Driver (#21) Reviewed-on: https://git.admiralackbar.de/moritz/ptprnt/pulls/21 --- .clangd | 11 + .vscode/c_cpp_properties.json | 7 +- .vscode/settings.json | 1 - src/core/PrinterService.cpp | 12 +- src/core/PrinterService.hpp | 9 +- src/libusbwrap/LibUsbWrapper.cpp | 108 +++++ src/libusbwrap/LibUsbWrapper.hpp | 125 ++++++ src/libusbwrap/UsbDevice.cpp | 57 +-- src/libusbwrap/UsbDevice.hpp | 30 +- src/libusbwrap/UsbDeviceFactory.cpp | 63 +-- src/libusbwrap/UsbDeviceFactory.hpp | 25 +- src/libusbwrap/interface/ILibUsbWrapper.hpp | 0 .../interface/IUsbDeviceFactory.hpp | 1 + src/meson.build | 2 + src/printers/FakePrinter.cpp | 5 + src/printers/P700Printer.cpp | 26 +- tests/cli_parser_test/cli_parser_test.cpp | 365 +++++++++++++++++ tests/fake_printer_test/fake_printer_test.cpp | 247 ++++++++++++ .../label_builder_test/label_builder_test.cpp | 230 +++++++++++ tests/label_test/label_test.cpp | 2 +- tests/meson.build | 33 +- tests/mocks/MockLibUsbWrapper.hpp | 76 ++++ tests/mocks/MockPrinterDriver.hpp | 50 +++ tests/mocks/MockUsbDevice.hpp | 57 +++ tests/mocks/MockUsbDeviceFactory.hpp | 41 ++ tests/monochrome_test/monochrome_test.cpp | 3 - tests/p700_printer_test/p700_printer_test.cpp | 169 ++++++++ .../printer_service_test.cpp | 128 ++++++ tests/ptouch_print_test/ptouch_print_test.cpp | 374 ++++++++++++++++++ tests/usb_device_test/usb_device_test.cpp | 276 +++++++++++++ 30 files changed, 2441 insertions(+), 92 deletions(-) create mode 100644 .clangd create mode 100644 src/libusbwrap/LibUsbWrapper.cpp create mode 100644 src/libusbwrap/LibUsbWrapper.hpp create mode 100644 src/libusbwrap/interface/ILibUsbWrapper.hpp create mode 100644 tests/cli_parser_test/cli_parser_test.cpp create mode 100644 tests/fake_printer_test/fake_printer_test.cpp create mode 100644 tests/label_builder_test/label_builder_test.cpp create mode 100644 tests/mocks/MockLibUsbWrapper.hpp create mode 100644 tests/mocks/MockPrinterDriver.hpp create mode 100644 tests/mocks/MockUsbDevice.hpp create mode 100644 tests/mocks/MockUsbDeviceFactory.hpp create mode 100644 tests/p700_printer_test/p700_printer_test.cpp create mode 100644 tests/printer_service_test/printer_service_test.cpp 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/.clangd b/.clangd new file mode 100644 index 0000000..fd3cac7 --- /dev/null +++ b/.clangd @@ -0,0 +1,11 @@ +--- +If: + PathMatch: tests/.* +CompileFlags: + CompilationDatabase: builddir-debug/ + +--- +If: + PathMatch: src/.* +CompileFlags: + CompilationDatabase: builddir/ diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json index eb6a4b8..103ccfd 100644 --- a/.vscode/c_cpp_properties.json +++ b/.vscode/c_cpp_properties.json @@ -5,11 +5,12 @@ "compilerPath": "/usr/bin/clang", "cStandard": "c11", "cppStandard": "c++20", - "compileCommands": "${workspaceFolder}/builddir/compile_commands.json", "browse": { - "path": ["${workspaceFolder}"] + "path": [ + "${workspaceFolder}" + ] } } ], "version": 4 -} +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index bf6734c..7781d85 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,7 +1,6 @@ { "clangd.arguments": [ "-background-index", - "-compile-commands-dir=builddir/" ], "editor.formatOnType": false, "editor.formatOnSave": true, 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..9d18be8 --- /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 +void LibUsbDeviceDeleter::operator()(libusb_device* dev) const { + if (dev && wrapper) { + wrapper->unrefDevice(dev); + } +} + +// LibUsbWrapper + +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..dd042ed --- /dev/null +++ b/src/libusbwrap/LibUsbWrapper.hpp @@ -0,0 +1,125 @@ +/* + 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 + +// 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 +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. + */ +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..1492724 100644 --- a/src/libusbwrap/UsbDevice.cpp +++ b/src/libusbwrap/UsbDevice.cpp @@ -24,44 +24,55 @@ #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 +84,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 +93,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 +103,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 +113,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 +133,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..9ffe8d9 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..9383a7b 100644 --- a/src/libusbwrap/UsbDeviceFactory.cpp +++ b/src/libusbwrap/UsbDeviceFactory.cpp @@ -27,16 +27,24 @@ #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 +59,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..ebcc42f 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 @@ -60,12 +67,12 @@ class UsbDeviceFactory : public IUsbDeviceFactory { private: // methods ssize_t refreshDeviceList(); - std::vector> buildMaskedDeviceVector(uint16_t vidMask, - uint16_t pidMask, uint16_t vid, + std::vector> buildMaskedDeviceVector(uint16_t vidMask, 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/ILibUsbWrapper.hpp b/src/libusbwrap/interface/ILibUsbWrapper.hpp new file mode 100644 index 0000000..e69de29 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..f21d1e2 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/src/printers/FakePrinter.cpp b/src/printers/FakePrinter.cpp index 8efe009..2e5c3e1 100644 --- a/src/printers/FakePrinter.cpp +++ b/src/printers/FakePrinter.cpp @@ -113,6 +113,11 @@ bool FakePrinter::printMonochromeData(const graphics::MonochromeData& data) { } bool FakePrinter::printLabel(const std::unique_ptr label) { + if (!label) { + spdlog::error("FakePrinter: Cannot print null label"); + return false; + } + // Convert label directly to MonochromeData // getRaw() returns data in Cairo surface coordinates matching getWidth() × getHeight() auto pixels = label->getRaw(); diff --git a/src/printers/P700Printer.cpp b/src/printers/P700Printer.cpp index 1ebcdeb..cb17f54 100644 --- a/src/printers/P700Printer.cpp +++ b/src/printers/P700Printer.cpp @@ -90,6 +90,11 @@ libusbwrap::usbId P700Printer::getUsbId() { } bool P700Printer::attachUsbDevice(std::shared_ptr usbHndl) { + if (!usbHndl) { + spdlog::error("Cannot attach null USB device"); + return false; + } + if (!usbHndl->open()) { spdlog::error("Unable to open USB device: {}", usbHndl->getLastErrorString()); return false; @@ -133,6 +138,10 @@ bool P700Printer::printBitmap(const graphics::Bitmap& bitmap) } bool P700Printer::printMonochromeData(const graphics::MonochromeData& data) { + if (!mUsbHndl) { + spdlog::error("USB Handle is invalid!"); + return false; + } // Send initialization sequence // The INITIALIZE command needs to be sent as a 128-byte packet with ESC @ at the end std::vector initCmd(128, 0x00); @@ -182,6 +191,11 @@ bool P700Printer::printMonochromeData(const graphics::MonochromeData& data) { } bool P700Printer::printLabel(std::unique_ptr label) { + if (!label) { + spdlog::error("Cannot print null label"); + return false; + } + // Convert label directly to MonochromeData // getRaw() returns data in Cairo surface coordinates matching getWidth() × getHeight() auto pixels = label->getRaw(); @@ -200,9 +214,15 @@ bool P700Printer::printLabel(std::unique_ptr label) { } bool P700Printer::print() { - send(p700::commands::LF); - send(p700::commands::FF); - send(p700::commands::EJECT); + if (!send(p700::commands::LF)) { + return false; + } + if (!send(p700::commands::FF)) { + return false; + } + if (!send(p700::commands::EJECT)) { + return false; + } return true; } 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/fake_printer_test/fake_printer_test.cpp b/tests/fake_printer_test/fake_printer_test.cpp new file mode 100644 index 0000000..258743b --- /dev/null +++ b/tests/fake_printer_test/fake_printer_test.cpp @@ -0,0 +1,247 @@ +/* + 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 "graphics/Bitmap.hpp" +#include "graphics/Label.hpp" +#include "graphics/Monochrome.hpp" +#include "printers/FakePrinter.hpp" + +namespace ptprnt::printer { + +class FakePrinterTest : public ::testing::Test { + protected: + void SetUp() override { + printer = std::make_unique(); + } + + void TearDown() override { printer.reset(); } + + std::unique_ptr printer; +}; + +// Test: Get driver name +TEST_F(FakePrinterTest, GetDriverName) { + auto name = printer->getDriverName(); + EXPECT_FALSE(name.empty()); + EXPECT_EQ(name, "FakePrinter"); +} + +// Test: Get printer name +TEST_F(FakePrinterTest, GetName) { + auto name = printer->getName(); + EXPECT_FALSE(name.empty()); +} + +// Test: Get USB ID +TEST_F(FakePrinterTest, GetUsbId) { + auto usbId = printer->getUsbId(); + EXPECT_EQ(usbId.first, 0x0000); // Virtual printer - no USB ID + EXPECT_EQ(usbId.second, 0x0000); // Virtual printer - no USB ID +} + +// Test: Get printer version +TEST_F(FakePrinterTest, GetVersion) { + auto version = printer->getVersion(); + EXPECT_FALSE(version.empty()); +} + +// Test: Get printer info +TEST_F(FakePrinterTest, GetPrinterInfo) { + auto info = printer->getPrinterInfo(); + + EXPECT_FALSE(info.driverName.empty()); + EXPECT_FALSE(info.name.empty()); + EXPECT_GT(info.pixelLines, 0); +} + +// Test: Get printer status without device +TEST_F(FakePrinterTest, GetPrinterStatusWithoutDevice) { + auto status = printer->getPrinterStatus(); + + // FakePrinter should return empty status when no device attached + EXPECT_EQ(status.tapeWidthMm, 0); +} + +// Test: Get printer status with device +TEST_F(FakePrinterTest, GetPrinterStatusWithDevice) { + printer->attachUsbDevice(nullptr); + auto status = printer->getPrinterStatus(); + + // FakePrinter should return default status when device attached + EXPECT_EQ(status.tapeWidthMm, 12); // Default tape width +} + +// Test: Attach USB device (should always succeed for fake printer) +TEST_F(FakePrinterTest, AttachUsbDevice) { + bool result = printer->attachUsbDevice(nullptr); + + // FakePrinter doesn't need a real device + EXPECT_TRUE(result); +} + +// Test: Detach USB device +TEST_F(FakePrinterTest, DetachUsbDevice) { + printer->attachUsbDevice(nullptr); + bool result = printer->detachUsbDevice(); + + EXPECT_TRUE(result); +} + +// Test: Print without attaching device first +TEST_F(FakePrinterTest, PrintWithoutDevice) { + bool result = printer->print(); + + // FakePrinter should fail if no device attached + EXPECT_FALSE(result); +} + +// Test: Print after attaching device +TEST_F(FakePrinterTest, PrintWithDevice) { + printer->attachUsbDevice(nullptr); + bool result = printer->print(); + + EXPECT_TRUE(result); +} + +// Test: Print bitmap +TEST_F(FakePrinterTest, PrintBitmap) { + graphics::Bitmap bitmap(100, 128); + + // Fill with some pattern + std::vector pixels(bitmap.getWidth() * bitmap.getHeight()); + for (size_t i = 0; i < pixels.size(); ++i) { + pixels[i] = (i % 2) ? 0xFF : 0x00; + } + bitmap.setPixels(pixels); + + printer->attachUsbDevice(nullptr); + bool result = printer->printBitmap(bitmap); + + EXPECT_TRUE(result); + + // Check that last print was saved + const auto& lastPrint = printer->getLastPrint(); + EXPECT_GT(lastPrint.getWidth(), 0); + EXPECT_GT(lastPrint.getHeight(), 0); +} + +// Test: Print monochrome data +TEST_F(FakePrinterTest, PrintMonochromeData) { + graphics::Bitmap bitmap(50, 128); + auto pixels = bitmap.getPixelsCpy(); + auto mono = graphics::Monochrome(pixels, bitmap.getWidth(), bitmap.getHeight()); + auto data = mono.get(); + + printer->attachUsbDevice(nullptr); + bool result = printer->printMonochromeData(data); + + EXPECT_TRUE(result); + + // Verify last print + const auto& lastPrint = printer->getLastPrint(); + EXPECT_GT(lastPrint.getWidth(), 0); +} + +// Test: Print label +TEST_F(FakePrinterTest, PrintLabel) { + auto label = std::make_unique(128); + label->create("Test Label"); + + printer->attachUsbDevice(nullptr); + bool result = printer->printLabel(std::move(label)); + + EXPECT_TRUE(result); +} + +// Test: Print null label +TEST_F(FakePrinterTest, PrintNullLabel) { + printer->attachUsbDevice(nullptr); + bool result = printer->printLabel(nullptr); + + EXPECT_FALSE(result); +} + +// Test: Print empty bitmap +TEST_F(FakePrinterTest, PrintEmptyBitmap) { + graphics::Bitmap bitmap(0, 0); + + printer->attachUsbDevice(nullptr); + bool result = printer->printBitmap(bitmap); + + // Should handle empty bitmap gracefully + EXPECT_TRUE(result); +} + +// Test: Get last print before printing +TEST_F(FakePrinterTest, GetLastPrintBeforePrinting) { + // Should throw when getting last print before any print operation + EXPECT_THROW({ + const auto& lastPrint = printer->getLastPrint(); + (void)lastPrint; // Suppress unused variable warning + }, std::runtime_error); +} + +// Test: Save last print to PNG (may fail without valid data) +TEST_F(FakePrinterTest, SaveLastPrintToPng) { + graphics::Bitmap bitmap(100, 128); + + printer->attachUsbDevice(nullptr); + printer->printBitmap(bitmap); + + // Try to save to /tmp + bool result = printer->saveLastPrintToPng("/tmp/test_fake_printer_output.png"); + + // Should succeed if we have valid print data + EXPECT_TRUE(result); +} + +// Test: Multiple prints +TEST_F(FakePrinterTest, MultiplePrints) { + graphics::Bitmap bitmap1(50, 128); + graphics::Bitmap bitmap2(100, 128); + + printer->attachUsbDevice(nullptr); + + bool result1 = printer->printBitmap(bitmap1); + EXPECT_TRUE(result1); + + bool result2 = printer->printBitmap(bitmap2); + EXPECT_TRUE(result2); + + // Last print should be from second bitmap + const auto& lastPrint = printer->getLastPrint(); + EXPECT_GT(lastPrint.getWidth(), 0); +} + +// Test: Detach and reattach +TEST_F(FakePrinterTest, DetachAndReattach) { + printer->attachUsbDevice(nullptr); + EXPECT_TRUE(printer->detachUsbDevice()); + + // Should be able to reattach + EXPECT_TRUE(printer->attachUsbDevice(nullptr)); + + // Should be able to print after reattach + graphics::Bitmap bitmap(50, 128); + EXPECT_TRUE(printer->printBitmap(bitmap)); +} + +} // namespace ptprnt::printer diff --git a/tests/label_builder_test/label_builder_test.cpp b/tests/label_builder_test/label_builder_test.cpp new file mode 100644 index 0000000..f4a3dc8 --- /dev/null +++ b/tests/label_builder_test/label_builder_test.cpp @@ -0,0 +1,230 @@ +/* + 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 "graphics/LabelBuilder.hpp" +#include "graphics/interface/ILabel.hpp" + +namespace ptprnt::graphics { + +class LabelBuilderTest : public ::testing::Test { + protected: + void SetUp() override { + builder = std::make_unique(128); // P700 printer height + } + + void TearDown() override { builder.reset(); } + + std::unique_ptr builder; +}; + +// Test: Constructor +TEST_F(LabelBuilderTest, Constructor) { + EXPECT_NO_THROW(LabelBuilder(128)); + EXPECT_NO_THROW(LabelBuilder(64)); +} + +// Test: Add single text +TEST_F(LabelBuilderTest, AddSingleText) { + auto& result = builder->addText("Hello"); + + // Should return reference to builder for chaining + EXPECT_EQ(&result, builder.get()); +} + +// Test: Add multiple texts +TEST_F(LabelBuilderTest, AddMultipleTexts) { + builder->addText("Line 1") + .addText("Line 2") + .addText("Line 3"); + + // Build should succeed + auto label = builder->build(); + EXPECT_NE(label, nullptr); +} + +// Test: Add empty text (should be ignored) +TEST_F(LabelBuilderTest, AddEmptyText) { + builder->addText(""); + + auto label = builder->build(); + EXPECT_NE(label, nullptr); +} + +// Test: Set font family +TEST_F(LabelBuilderTest, SetFontFamily) { + auto& result = builder->setFontFamily("monospace"); + + // Should return reference to builder for chaining + EXPECT_EQ(&result, builder.get()); +} + +// Test: Set font size +TEST_F(LabelBuilderTest, SetFontSize) { + auto& result = builder->setFontSize(24.0); + + // Should return reference to builder for chaining + EXPECT_EQ(&result, builder.get()); +} + +// Test: Set horizontal alignment +TEST_F(LabelBuilderTest, SetHAlign) { + auto& result = builder->setHAlign(HAlignPosition::CENTER); + + // Should return reference to builder for chaining + EXPECT_EQ(&result, builder.get()); +} + +// Test: Set vertical alignment +TEST_F(LabelBuilderTest, SetVAlign) { + auto& result = builder->setVAlign(VAlignPosition::TOP); + + // Should return reference to builder for chaining + EXPECT_EQ(&result, builder.get()); +} + +// Test: Build with default settings +TEST_F(LabelBuilderTest, BuildWithDefaults) { + builder->addText("Test"); + + auto label = builder->build(); + + EXPECT_NE(label, nullptr); + EXPECT_GT(label->getWidth(), 0); + EXPECT_GT(label->getHeight(), 0); +} + +// Test: Build with custom settings +TEST_F(LabelBuilderTest, BuildWithCustomSettings) { + builder->setFontFamily("serif") + .setFontSize(48.0) + .setHAlign(HAlignPosition::CENTER) + .setVAlign(VAlignPosition::MIDDLE) + .addText("Custom Text"); + + auto label = builder->build(); + + EXPECT_NE(label, nullptr); +} + +// Test: Method chaining +TEST_F(LabelBuilderTest, MethodChaining) { + auto label = builder->setFontFamily("monospace") + .setFontSize(20.0) + .setHAlign(HAlignPosition::RIGHT) + .setVAlign(VAlignPosition::BOTTOM) + .addText("Chained") + .addText("Methods") + .build(); + + EXPECT_NE(label, nullptr); +} + +// Test: Reset builder +TEST_F(LabelBuilderTest, Reset) { + builder->setFontFamily("monospace") + .setFontSize(48.0) + .setHAlign(HAlignPosition::RIGHT) + .setVAlign(VAlignPosition::BOTTOM) + .addText("Test"); + + auto& result = builder->reset(); + + // Should return reference to builder for chaining + EXPECT_EQ(&result, builder.get()); + + // After reset, should be able to build with defaults + builder->addText("After Reset"); + auto label = builder->build(); + EXPECT_NE(label, nullptr); +} + +// Test: Build empty label +TEST_F(LabelBuilderTest, BuildEmptyLabel) { + // Build without adding any text + auto label = builder->build(); + + EXPECT_NE(label, nullptr); +} + +// Test: Multiple builds from same builder +TEST_F(LabelBuilderTest, MultipleBuilds) { + builder->addText("First Build"); + auto label1 = builder->build(); + EXPECT_NE(label1, nullptr); + + // Build again with same content + auto label2 = builder->build(); + EXPECT_NE(label2, nullptr); + + // Both labels should be valid + EXPECT_GT(label1->getWidth(), 0); + EXPECT_GT(label2->getWidth(), 0); +} + +// Test: All horizontal alignments +TEST_F(LabelBuilderTest, AllHorizontalAlignments) { + builder->addText("Left").setHAlign(HAlignPosition::LEFT); + auto label1 = builder->build(); + EXPECT_NE(label1, nullptr); + + builder->reset().addText("Center").setHAlign(HAlignPosition::CENTER); + auto label2 = builder->build(); + EXPECT_NE(label2, nullptr); + + builder->reset().addText("Right").setHAlign(HAlignPosition::RIGHT); + auto label3 = builder->build(); + EXPECT_NE(label3, nullptr); + + builder->reset().addText("Justify").setHAlign(HAlignPosition::JUSTIFY); + auto label4 = builder->build(); + EXPECT_NE(label4, nullptr); +} + +// Test: All vertical alignments +TEST_F(LabelBuilderTest, AllVerticalAlignments) { + builder->addText("Top").setVAlign(VAlignPosition::TOP); + auto label1 = builder->build(); + EXPECT_NE(label1, nullptr); + + builder->reset().addText("Middle").setVAlign(VAlignPosition::MIDDLE); + auto label2 = builder->build(); + EXPECT_NE(label2, nullptr); + + builder->reset().addText("Bottom").setVAlign(VAlignPosition::BOTTOM); + auto label3 = builder->build(); + EXPECT_NE(label3, nullptr); +} + +// Test: Different font sizes +TEST_F(LabelBuilderTest, DifferentFontSizes) { + builder->setFontSize(12.0).addText("Small"); + auto label1 = builder->build(); + EXPECT_NE(label1, nullptr); + + builder->reset().setFontSize(64.0).addText("Large"); + auto label2 = builder->build(); + EXPECT_NE(label2, nullptr); + + // Larger font should produce wider label (assuming same text length) + // Note: This is a heuristic test and may not always be true depending on font rendering +} + +} // namespace ptprnt::graphics diff --git a/tests/label_test/label_test.cpp b/tests/label_test/label_test.cpp index e0a3ea1..e678a3d 100644 --- a/tests/label_test/label_test.cpp +++ b/tests/label_test/label_test.cpp @@ -25,8 +25,8 @@ #include #include -#include "../../tests/mocks/MockCairoWrapper.hpp" #include "graphics/interface/ILabel.hpp" +#include "mocks/MockCairoWrapper.hpp" using ::testing::_; using ::testing::DoAll; diff --git a/tests/meson.build b/tests/meson.build index 5bffb57..177f41d 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -5,11 +5,38 @@ test_sources = [ 'bitmap_test/bitmap_test.cpp', 'monochrome_test/monochrome_test.cpp', 'label_test/label_test.cpp', + 'label_builder_test/label_builder_test.cpp', + '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 + # Source files under test - graphics '../src/graphics/Bitmap.cpp', '../src/graphics/Monochrome.cpp', '../src/graphics/Label.cpp', + '../src/graphics/LabelBuilder.cpp', + + # Source files under test - core + '../src/core/PrinterService.cpp', + '../src/core/PrinterDriverFactory.cpp', + + # Source files under test - printers + '../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', ] test_exe = executable( @@ -17,7 +44,7 @@ test_exe = executable( sources: test_sources, include_directories: incdir, dependencies: [ - gmock_dep, # GMock includes GTest + gmock_dep, # GMock includes GTest usb_dep, log_dep, pangocairo_dep, @@ -26,4 +53,4 @@ test_exe = executable( ) # Single test that runs all test suites -test('all_tests', test_exe) +test('all_tests', test_exe) \ No newline at end of file diff --git a/tests/mocks/MockLibUsbWrapper.hpp b/tests/mocks/MockLibUsbWrapper.hpp new file mode 100644 index 0000000..9aac781 --- /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/mocks/MockPrinterDriver.hpp b/tests/mocks/MockPrinterDriver.hpp new file mode 100644 index 0000000..47c442d --- /dev/null +++ b/tests/mocks/MockPrinterDriver.hpp @@ -0,0 +1,50 @@ +/* + 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 "printers/interface/IPrinterDriver.hpp" + +namespace ptprnt { + +/** + * @brief GMock implementation of IPrinterDriver for unit testing + * + * This mock allows tests to verify printer interactions without + * requiring actual printer hardware. + */ +class MockPrinterDriver : public IPrinterDriver { + public: + MOCK_METHOD(std::string_view, getDriverName, (), (override)); + MOCK_METHOD(std::string_view, getName, (), (override)); + MOCK_METHOD(std::string_view, getVersion, (), (override)); + MOCK_METHOD(libusbwrap::usbId, getUsbId, (), (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, (const std::unique_ptr label), (override)); + MOCK_METHOD(bool, print, (), (override)); +}; + +} // namespace ptprnt diff --git a/tests/mocks/MockUsbDevice.hpp b/tests/mocks/MockUsbDevice.hpp new file mode 100644 index 0000000..1a38360 --- /dev/null +++ b/tests/mocks/MockUsbDevice.hpp @@ -0,0 +1,57 @@ +/* + 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/interface/IUsbDevice.hpp" + +namespace libusbwrap { + +/** + * @brief GMock implementation of IUsbDevice for unit testing + * + * This mock allows tests to verify USB device interactions without + * requiring actual hardware or libusb context. + */ +class MockUsbDevice : public IUsbDevice { + public: + MOCK_METHOD(bool, open, (), (override)); + MOCK_METHOD(void, close, (), (override)); + + // libusb wrappers + MOCK_METHOD(bool, detachKernelDriver, (int interfaceNo), (override)); + MOCK_METHOD(bool, claimInterface, (int interfaceNo), (override)); + MOCK_METHOD(bool, releaseInterface, (int interfaceNo), (override)); + MOCK_METHOD(bool, bulkTransfer, + (uint8_t endpoint, const std::vector& data, int* tx, unsigned int timeout), (override)); + + // getters + MOCK_METHOD(const usbId, getUsbId, (), (override)); + MOCK_METHOD(const device::Speed, getSpeed, (), (override)); + MOCK_METHOD(const uint8_t, getBusNumber, (), (override)); + MOCK_METHOD(const uint8_t, getPortNumber, (), (override)); + + // errors + MOCK_METHOD(const Error, getLastError, (), (override)); + MOCK_METHOD(const std::string, getLastErrorString, (), (override)); +}; + +} // namespace libusbwrap diff --git a/tests/mocks/MockUsbDeviceFactory.hpp b/tests/mocks/MockUsbDeviceFactory.hpp new file mode 100644 index 0000000..3022379 --- /dev/null +++ b/tests/mocks/MockUsbDeviceFactory.hpp @@ -0,0 +1,41 @@ +/* + 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/interface/IUsbDeviceFactory.hpp" + +namespace libusbwrap { + +/** + * @brief GMock implementation of IUsbDeviceFactory for unit testing + * + * This mock allows tests to control USB device discovery without + * requiring actual libusb context or hardware. + */ +class MockUsbDeviceFactory : public IUsbDeviceFactory { + public: + MOCK_METHOD(bool, init, (), (override)); + MOCK_METHOD(std::vector>, findAllDevices, (), (override)); + MOCK_METHOD(std::vector>, findDevices, (uint16_t vid, uint16_t pid), (override)); +}; + +} // namespace libusbwrap diff --git a/tests/monochrome_test/monochrome_test.cpp b/tests/monochrome_test/monochrome_test.cpp index 8d5ce0b..29ec73c 100644 --- a/tests/monochrome_test/monochrome_test.cpp +++ b/tests/monochrome_test/monochrome_test.cpp @@ -57,9 +57,6 @@ TEST(basic_test, Monochrome_convertWithCustomThreshhold_yieldsMonochromeRespecti } TEST(basic_test, Monochrome_convertNonAlignedPixels_spillsOverIntoNewByte) { - // TODO: We need to find to access the vector without the possiblity of out-of-bounds access - // Ideas: constexpr? compile time check? - GTEST_SKIP() << "Skipping this test, as ASAN will halt as this is an out-of-bounds access"; const std::vector pixels( {0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF}); diff --git a/tests/p700_printer_test/p700_printer_test.cpp b/tests/p700_printer_test/p700_printer_test.cpp new file mode 100644 index 0000000..d310af1 --- /dev/null +++ b/tests/p700_printer_test/p700_printer_test.cpp @@ -0,0 +1,169 @@ +/* + 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 "graphics/Bitmap.hpp" +#include "graphics/Monochrome.hpp" +#include "mocks/MockUsbDevice.hpp" +#include "printers/P700Printer.hpp" + +using ::testing::_; +using ::testing::DoAll; +using ::testing::NiceMock; +using ::testing::Return; +using ::testing::SetArgPointee; + +namespace ptprnt::printer { + +// Test fixture for P700Printer tests +class P700PrinterTest : public ::testing::Test { + protected: + void SetUp() override { + printer = std::make_unique(); + mockUsbDev = std::make_shared>(); + + // Default mock behaviors + ON_CALL(*mockUsbDev, open()).WillByDefault(Return(true)); + ON_CALL(*mockUsbDev, close()).WillByDefault(Return()); + ON_CALL(*mockUsbDev, detachKernelDriver(_)).WillByDefault(Return(true)); + ON_CALL(*mockUsbDev, claimInterface(_)).WillByDefault(Return(true)); + ON_CALL(*mockUsbDev, releaseInterface(_)).WillByDefault(Return(true)); + ON_CALL(*mockUsbDev, bulkTransfer(_, _, _, _)).WillByDefault(Return(true)); + ON_CALL(*mockUsbDev, getUsbId()).WillByDefault(Return(libusbwrap::usbId{0x04f9, 0x2061})); + } + + void TearDown() override { printer.reset(); } + + std::unique_ptr printer; + std::shared_ptr mockUsbDev; +}; + +// Test: Get printer driver name +TEST_F(P700PrinterTest, GetDriverName) { + EXPECT_EQ(printer->getDriverName(), "P700"); +} + +// Test: Get printer name +TEST_F(P700PrinterTest, GetName) { + auto name = printer->getName(); + EXPECT_FALSE(name.empty()); +} + +// Test: Get USB ID +TEST_F(P700PrinterTest, GetUsbId) { + auto usbId = printer->getUsbId(); + EXPECT_EQ(usbId.first, 0x04f9); // Brother VID + EXPECT_EQ(usbId.second, 0x2061); // P700 PID +} + +// Test: Get printer version +TEST_F(P700PrinterTest, GetVersion) { + auto version = printer->getVersion(); + EXPECT_FALSE(version.empty()); +} + +// Test: Get printer info +TEST_F(P700PrinterTest, GetPrinterInfo) { + auto info = printer->getPrinterInfo(); + EXPECT_EQ(info.pixelLines, 128); + EXPECT_FALSE(info.name.empty()); +} + +// Test: Attach USB device +TEST_F(P700PrinterTest, AttachUsbDevice) { + bool result = printer->attachUsbDevice(mockUsbDev); + + EXPECT_TRUE(result); +} + +// Test: Attach USB device with null pointer +TEST_F(P700PrinterTest, AttachNullUsbDevice) { + bool result = printer->attachUsbDevice(nullptr); + + EXPECT_FALSE(result); +} + +// Test: Detach USB device +TEST_F(P700PrinterTest, DetachUsbDevice) { + printer->attachUsbDevice(mockUsbDev); + + bool result = printer->detachUsbDevice(); + + EXPECT_TRUE(result); +} + +// Test: Detach when no device attached +TEST_F(P700PrinterTest, DetachNoDevice) { + bool result = printer->detachUsbDevice(); + + // Production code returns true when no device is attached (just logs warning) + EXPECT_TRUE(result); +} + +// Test: Get printer status without device +TEST_F(P700PrinterTest, GetStatusNoDevice) { + auto status = printer->getPrinterStatus(); + + EXPECT_EQ(status.tapeWidthPixel, 0); // Should be 0 when no device +} + +// Test: Print without attached device +TEST_F(P700PrinterTest, PrintWithoutDevice) { + bool result = printer->print(); + + // Should fail when no device is attached + EXPECT_FALSE(result); +} + +// Test: Print bitmap without device +TEST_F(P700PrinterTest, PrintBitmapWithoutDevice) { + graphics::Bitmap bitmap(10, 10); + + bool result = printer->printBitmap(bitmap); + + EXPECT_FALSE(result); +} + +// Test: Print monochrome data without device +TEST_F(P700PrinterTest, PrintMonochromeDataWithoutDevice) { + graphics::MonochromeData data; + data.width = 10; + data.height = 10; + data.bytes = std::vector(20, 0xFF); + data.stride = 2; + + bool result = printer->printMonochromeData(data); + + EXPECT_FALSE(result); +} + +// Test: Print label with null pointer +TEST_F(P700PrinterTest, PrintNullLabel) { + std::unique_ptr label = nullptr; + + bool result = printer->printLabel(std::move(label)); + + EXPECT_FALSE(result); +} + +} // namespace ptprnt::printer diff --git a/tests/printer_service_test/printer_service_test.cpp b/tests/printer_service_test/printer_service_test.cpp new file mode 100644 index 0000000..2d29c55 --- /dev/null +++ b/tests/printer_service_test/printer_service_test.cpp @@ -0,0 +1,128 @@ +/* + 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 + +#include "core/PrinterDriverFactory.hpp" +#include "core/PrinterService.hpp" +#include "mocks/MockUsbDeviceFactory.hpp" + +using ::testing::_; +using ::testing::Invoke; +using ::testing::NiceMock; +using ::testing::Return; + +namespace ptprnt::core { + +// Helper function to return empty vector of unique_ptrs +std::vector> returnEmptyDeviceList() { + return {}; +} + +// Test fixture for PrinterService tests +class PrinterServiceTest : public ::testing::Test { + protected: + void SetUp() override { + // Create mock USB device factory + auto mockFactory = std::make_unique>(); + mockFactoryPtr = mockFactory.get(); + + // Default behavior: init succeeds + ON_CALL(*mockFactoryPtr, init()).WillByDefault(Return(true)); + + // Default behavior: no devices found + ON_CALL(*mockFactoryPtr, findAllDevices()).WillByDefault(Invoke(returnEmptyDeviceList)); + + // Inject mock factory into service + service = std::make_unique(std::move(mockFactory)); + } + + void TearDown() override { service.reset(); } + + std::unique_ptr service; + libusbwrap::MockUsbDeviceFactory* mockFactoryPtr; // Non-owning pointer for expectations +}; + +// Test: PrinterService initialization +TEST_F(PrinterServiceTest, InitializeSuccess) { + EXPECT_CALL(*mockFactoryPtr, init()).WillOnce(Return(true)); + + EXPECT_TRUE(service->initialize()); +} + +// Test: Detect printers when none are connected +TEST_F(PrinterServiceTest, DetectPrintersNoneFound) { + service->initialize(); + + // Mock returns empty device list - no real USB enumeration happens + EXPECT_CALL(*mockFactoryPtr, findAllDevices()).WillOnce(Invoke(returnEmptyDeviceList)); + + auto printers = service->detectPrinters(); + + // Should get empty list since mock returns no devices + EXPECT_EQ(printers.size(), 0); +} + +// Test: Select printer with auto-detect when none found +TEST_F(PrinterServiceTest, SelectPrinterAutoNoneFound) { + service->initialize(); + + // Mock returns empty device list + EXPECT_CALL(*mockFactoryPtr, findAllDevices()).WillOnce(Invoke(returnEmptyDeviceList)); + + auto printer = service->selectPrinter("auto"); + + // Should be nullptr since no printers detected + EXPECT_EQ(printer, nullptr); + EXPECT_EQ(service->getCurrentPrinter(), nullptr); +} + +// Test: Select non-existent printer by name +TEST_F(PrinterServiceTest, SelectPrinterByNameNotFound) { + service->initialize(); + + // This test doesn't need USB mocking since selectPrinter("name") + // uses PrinterDriverFactory directly, not USB enumeration + auto printer = service->selectPrinter("NonExistentPrinter"); + + EXPECT_EQ(printer, nullptr); + EXPECT_EQ(service->getCurrentPrinter(), nullptr); +} + +// Test: Get current printer when none selected +TEST_F(PrinterServiceTest, GetCurrentPrinterNoneSelected) { + EXPECT_EQ(service->getCurrentPrinter(), nullptr); +} + +// Test: Print label without selecting printer +TEST_F(PrinterServiceTest, PrintLabelNoPrinterSelected) { + // Create a simple label (we need a valid ILabel pointer) + std::unique_ptr label = nullptr; // nullptr label for this test + + bool result = service->printLabel(std::move(label)); + + // Should fail because no printer is selected + EXPECT_FALSE(result); +} + +} // namespace ptprnt::core 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..37576e1 --- /dev/null +++ b/tests/usb_device_test/usb_device_test.cpp @@ -0,0 +1,276 @@ +/* + 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 "libusb.h" +#include "libusbwrap/LibUsbTypes.hpp" +#include "libusbwrap/UsbDevice.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