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