1 Commits

Author SHA1 Message Date
69810e1c5c Refactor cli setup and printer selection logic into separate classes
All checks were successful
Build ptprnt / build (push) Successful in 3m49s
2025-10-13 20:29:14 +02:00
70 changed files with 520 additions and 4639 deletions

11
.clangd
View File

@@ -1,11 +0,0 @@
---
If:
PathMatch: tests/.*
CompileFlags:
CompilationDatabase: builddir-debug/
---
If:
PathMatch: src/.*
CompileFlags:
CompilationDatabase: builddir/

View File

@@ -14,7 +14,7 @@ jobs:
- name: install meson - name: install meson
run: apt-get -yq install meson run: apt-get -yq install meson
- name: Install build dependencies - name: Install build dependencies
run: apt-get -yq install libusb-1.0-0-dev libpango1.0-dev libcairo2-dev gcovr run: apt-get -yq install libusb-1.0-0-dev libspdlog-dev libfmt-dev libpango1.0-dev libcairo2-dev gcovr
- name: get build environment versions - name: get build environment versions
run: | run: |
echo "=== Start meson version ===" echo "=== Start meson version ==="
@@ -32,25 +32,23 @@ jobs:
echo "=== Start dependency package version ===" echo "=== Start dependency package version ==="
apt-cache policy libpango1.0-dev apt-cache policy libpango1.0-dev
apt-cache policy libcairo2-dev apt-cache policy libcairo2-dev
apt-cache policy libfmt-dev
apt-cache policy libspdlog-dev
apt-cache policy libusb-1.0-0-dev apt-cache policy libusb-1.0-0-dev
echo "=== End dependency package version ===" echo "=== End dependency package version ==="
- name: Build ptprnt debug - name: setup builddir
run: scripts/build.sh debug --coverage --test run: meson setup builddir -Db_coverage=true
- name: Generate coverage - name: build and test dist package
run: scripts/generate_coverage.sh --text run: ninja -C builddir dist
- name: run unit tests
run: ninja -C builddir test
- name: calculate coverage
run: ninja -C builddir coverage-text
- name: Coverage report - name: Coverage report
run: cat ./coverageReport/coverage.txt run: cat ./builddir/meson-logs/coverage.txt
- name: build release - name: upload dist package
run: scripts/build.sh release
- name: upload release binary
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
name: ptprnt name: ptprnt-dist
path: ./builddir/ptprnt path: ./builddir/meson-dist/*
if-no-files-found: error
- name: upload coverage report
uses: actions/upload-artifact@v3
with:
name: coverage.txt
path: ./coverageReport/coverage.txt
if-no-files-found: error if-no-files-found: error

16
.gitignore vendored
View File

@@ -1,5 +1,6 @@
# Folder # Folder
builddir*/ builddir/
ptouch-print/
subprojects/* subprojects/*
.cache/ .cache/
coverageReport/ coverageReport/
@@ -11,16 +12,3 @@ ptprnt.log
!.vscode/c_cpp_properties.json !.vscode/c_cpp_properties.json
!.vscode/settings.json !.vscode/settings.json
!.vscode/launch.json !.vscode/launch.json
# ignore generated testlabels
fakelabel_*.png
# ignore package capture files
*.pcapng
*.pcapng.gz
*.pcap
*.pcap.gz
# ignore coverage temporaries
*.gcov.json
*.gcov.json.gz

View File

@@ -5,12 +5,11 @@
"compilerPath": "/usr/bin/clang", "compilerPath": "/usr/bin/clang",
"cStandard": "c11", "cStandard": "c11",
"cppStandard": "c++20", "cppStandard": "c++20",
"compileCommands": "${workspaceFolder}/builddir/compile_commands.json",
"browse": { "browse": {
"path": [ "path": ["${workspaceFolder}"]
"${workspaceFolder}"
]
} }
} }
], ],
"version": 4 "version": 4
} }

View File

@@ -1,6 +1,7 @@
{ {
"clangd.arguments": [ "clangd.arguments": [
"-background-index", "-background-index",
"-compile-commands-dir=builddir/"
], ],
"editor.formatOnType": false, "editor.formatOnType": false,
"editor.formatOnSave": true, "editor.formatOnSave": true,
@@ -87,14 +88,9 @@
}, },
"clangd.onConfigChanged": "restart", "clangd.onConfigChanged": "restart",
"cSpell.words": [ "cSpell.words": [
"fakelabel",
"fontsize", "fontsize",
"gboolean",
"gint",
"gobject",
"halign", "halign",
"libusb", "libusb",
"ptrnt", "ptrnt"
"strv"
], ],
} }

View File

@@ -1,70 +0,0 @@
# Changelog
All notable changes to ptprnt will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.2.0] - v0.2.0
### Added
- Multi-label support - print multiple labels in sequence
- LabelBuilder API with fluent interface for constructing labels
- FakePrinter driver for testing without hardware (outputs PNG files)
- PrinterService core service for printer operations
- CliParser component with ICliParser interface
- ICairoWrapper interface for testable graphics rendering
- MockCairoWrapper for unit testing
- Pre-commit hook for automatic copyright updates
- USB trace mode for debugging (`-Dusb_trace_only=true`)
### Changed
- **Major refactoring**: Reorganized codebase into layered architecture
- Application layer: PtouchPrint, CliParser, PrinterService
- Printer drivers: Moved to `src/printers/` with factory pattern
- Graphics system: Added builder pattern and Cairo abstraction
- Core services: Separated into `src/core/` directory
- Label class now uses dependency injection for Cairo/Pango
- CLI parsing separated from main application logic
- Updated dependencies to latest versions
- Improved project documentation (README.md, CLAUDE.md)
### Fixed
- Label corruption issues resolved
- Printer info retrieval bugs
- USB attachment logic
- Multiple lines handling in labels
- Printer selection logic
## [0.1.0] - 2024
### Added
- Initial release of ptprnt
- Basic label printing functionality for Brother P-touch P700 series
- Pango/Cairo-based text rendering
- USB device communication via libusb-1.0
- Template-based Bitmap class supporting multiple pixel formats (ALPHA8, RGBX8, RGBA8, ARGB8)
- Monochrome bitmap conversion for printer output
- PrinterDriverFactory for creating printer instances
- USB device abstraction layer (IUsbDevice, UsbDevice, UsbDeviceFactory)
- Command-line interface using CLI11
- Logging with spdlog and file output
- Unit tests using GoogleTest
- Code coverage reporting with gcovr
- Meson build system with C++20 support
- CI/CD pipeline with automated testing
### Core Components (v0.1.0)
- PtouchPrint: Main application orchestrator
- P700Printer: Brother P-touch P700 driver implementation
- Bitmap: Template-based image storage
- Label: Text rendering with Pango/Cairo
- Monochrome: Bitmap to monochrome conversion
- UsbDevice: libusb wrapper for device communication
---
## Project Origins
ptprnt is a modern C++20 rewrite of [ptouch-print](https://git.familie-radermacher.ch/linux/ptouch-print.git).
All credits for reverse engineering the Brother P-touch USB protocol go to Dominic Rademacher.

265
README.md
View File

@@ -1,252 +1,57 @@
# ptprnt # ptprnt
A command-line label printer driver for Brother P-touch printers on Linux. Prints text labels directly from your terminal. This is a rewrite of [ptouch-print](https://git.familie-radermacher.ch/linux/ptouch-print.git) as a toy project for my personal amusement. The currently available solutions are good enough for generating labels, but i wanted to explore libusb and maybe improve the functionality of my label printer. All credits for reverse engineering the USB commands to Dominic Rademacher.
## Example ## Dependencies
This project requires:
- spdlog
- libusb
- pango
- cairo
- meson
- gtest (optional, for testing, will be installed by meson)
- gcov (optional, for coverage reports)
Too print a label, provide your text and optionally a font and a font size. This command will print the label below on a Brother P-Touch P700: Install dependencies on Arch Linux
``` bash
```bash pacman -S libusb spdlog pango cairo meson gcovr
ptprnt --font "NotoMono Nerd Font" --fontsize 32 --text "🖶 ptprnt v0.2.0 🥰"
``` ```
![Printed label that proudly reads 🖶 ptprnt v0.2.0 🥰](docs/assets/label_print_v0_2_0.jpg) Install dependencies on Debian/Ubuntu
``` bash
## Quick Start apt-get install libusb-1.0-0-dev libspdlog-dev libfmt-dev libpango1.0-dev libcairo2-dev meson gcovr
## Quick Start
**Binary dependencies**
Arch Linux:
```bash
pacman -S pango cairo libusb
``` ```
Debian/Ubuntu: ## Build
Clone the repository and simply let meson do the heavy lifting.
```bash ```bash
apt install libpangocairo-1.0-0 libusb-1.0-0 meson setup builddir
```
If you want to generate coverage reports, enable them via the command line switch
```bash
meson setup builddir -Db_coverage=true
``` ```
**Build dependencies:** Rebuild by simply invoking ninja
Arch Linux:
```bash ```bash
pacman -S libusb spdlog pango cairo meson
```
Debian/Ubuntu:
```bash
apt install libusb-1.0-0-dev libspdlog-dev libfmt-dev libpango1.0-dev libcairo2-dev meson
```
Note: spdlog is built as a subproject and statically linked, so it's not required as a system dependency.
**Build and run:**
Clone this repository first and enter the directory. Then build:
```bash
# Using the build script (recommended)
./scripts/build.sh release
builddir/ptprnt --help
# Or manually with meson
meson setup builddir
ninja -C builddir ninja -C builddir
builddir/ptprnt --help
``` ```
## Usage ## Run
Run the binary from your builddir
```bash
builddir/ptprnt
```
### Basic Text Printing ## Test
Testing is done via gtest. To run your test simply invoke ninja with the "test" target.
Print a simple label with text:
```bash ```bash
ptprnt --text "Hello World" ninja -C builddir test
``` ```
### Formatting Options Coverage reports can be generated via gcov if you enabled them (see Build section) by building the `coverage-text` target.
Control the appearance of your labels with these options:
- `--font FONT_NAME` - Set the font (e.g., "NotoMono Nerd Font", "DejaVu Sans")
- `--fontsize SIZE` - Set font size in points (default: 24)
- `--halign ALIGNMENT` - Horizontal alignment: `left`, `center`, `right` (default: center)
- `--valign ALIGNMENT` - Vertical alignment: `top`, `center`, `bottom` (default: center)
**Example with formatting:**
```bash
ptprnt --font "DejaVu Sans Mono" --fontsize 28 --halign left --text "Left aligned text"
```
### Multiple Text Elements
You can add multiple text elements to a single label. Formatting options apply to all subsequent `--text` arguments until changed:
```bash
ptprnt \
--font "DejaVu Sans" --fontsize 32 --text "Large Title" \
--fontsize 18 --text "Smaller subtitle"
```
### Multiple Labels (Stitching)
Create multiple labels that will be stitched together horizontally using the `--new` flag:
```bash
ptprnt \
--text "Label 1" \
--new \
--text "Label 2" \
--new \
--text "Label 3"
```
Each `--new` starts a fresh label. The labels are automatically stitched together with 60 pixels of spacing.
**Example - Creating a series of address labels:**
```bash
ptprnt \
--font "DejaVu Sans" --fontsize 20 \
--text "Peter Lustig" --text "Am Bauwagen 1" \
--new \
--text "Donald Duck" --text "Blumenstraße 13" \
--new \
--text "Homer Simpson" --text "742 Evergreen Terrace"
```
**Example - Mixed formatting across labels:**
```bash
ptprnt \
--fontsize 32 --text "BIG" \
--new \
--fontsize 16 --text "small" \
--new \
--fontsize 24 --text "medium"
```
### Printer Selection
By default, ptprnt auto-detects your printer. You can explicitly select a printer:
```bash
ptprnt --printer P700 --text "Hello"
```
List all available printer drivers:
```bash
ptprnt --list-all-drivers
```
### Testing with Fake Printer
Before printing to your real printer, you can test your label output using the built-in fake printer. This generates a PNG image file instead of printing:
```bash
ptprnt --printer FakePrinter --text "Test Label"
```
This will create a file named `fakelabel_YYYYMMDD_HHMMSS.png` in your current directory with a preview of your label. Use this to:
- Verify text formatting and layout
- Test multi-label stitching
- Preview before wasting label tape
**Example workflow:**
```bash
# First, test your label design
ptprnt --printer FakePrinter \
--font "DejaVu Sans" --fontsize 28 \
--text "Test Design" --text "Check Layout"
# View the generated PNG file to verify
# If satisfied, print to real printer
ptprnt --printer P700 \
--font "DejaVu Sans" --fontsize 28 \
--text "Test Design" --text "Check Layout"
```
### Verbose Output
Enable detailed logging for debugging:
```bash
ptprnt --verbose --text "Debug mode"
```
Enable USB trace to see raw USB communication:
```bash
ptprnt --trace --text "USB trace mode"
```
### Complete Example
An example using a mixed bag of features:
```bash
ptprnt \
--verbose \
--printer P700 \
--font "NotoMono Nerd Font" \
--fontsize 28 --halign center --text "Product Label" \
--fontsize 20 --text "SKU: 12345" \
--new \
--fontsize 24 --text "Backup Label" \
--fontsize 18 --text "Date: 2025-10-16"
```
## Supported Printers
(I need more printers for verification 😉)
- Brother P-touch P700 series
## Developer info
This is a modern C++20 rewrite of [ptouch-print](https://git.familie-radermacher.ch/linux/ptouch-print.git). Credits to Dominic Rademacher for reverse engineering the USB protocol.
**Build script:**
```bash
# Release build (tests disabled for faster builds)
./scripts/build.sh release
# Debug build (tests enabled)
./scripts/build.sh debug
# Debug with tests
./scripts/build.sh debug --test
# Debug with coverage
./scripts/build.sh debug --coverage
# Clean all build directories
./scripts/build.sh clean
# Show all options
./scripts/build.sh --help
```
**Note:** Tests are only built in debug mode to keep release builds fast and small. Release builds do not include test binaries or link against gtest/gmock.
**Running tests:**
```bash
# Using build script
./scripts/build.sh --test
# Or manually
ninja -C builddir test
```
**Coverage reports:**
```bash
# 1. Build with coverage enabled and run tests
./scripts/build.sh debug --coverage --test
# 2. Generate coverage reports
./scripts/generate_coverage.sh # All formats (html, xml, text)
./scripts/generate_coverage.sh --html # HTML only
./scripts/generate_coverage.sh --text # Text only
./scripts/generate_coverage.sh --xml # XML only (for CI/CD)
./scripts/generate_coverage.sh --html --xml # HTML and XML
```
## License ## License

Binary file not shown.

Before

Width:  |  Height:  |  Size: 642 KiB

27
generate_coverage.sh Executable file
View File

@@ -0,0 +1,27 @@
#!/bin/bash
SCRIPT_PATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )"
HTML_COV_PATH="coverageReport/html"
XML_COV_PATH="coverageReport/xml"
HTML_START_FILE="index.html"
echo "Generating Coverage report for ptouch-prnt"
ninja -C builddir
ninja -C builddir test
mkdir -p ${HTML_COV_PATH}
gcovr --html --html-details --html-syntax-highlighting --filter src --output ${HTML_COV_PATH}/${HTML_START_FILE}
mkdir -p ${XML_COV_PATH}
gcovr --xml-pretty --filter src --output ${XML_COV_PATH}/cov.xml
if [ $? ]
then
echo "Coverage report successful generated!"
echo "Open: file://${SCRIPT_PATH}/${HTML_COV_PATH}/${HTML_START_FILE}"
else
echo "Error generating coverage report!"
fi
rm *.gcov

View File

@@ -1,43 +0,0 @@
# Git Hooks
This directory contains git hooks for the ptprnt repository.
## Installation
To install the hooks, run:
```bash
./hooks/install_hooks.sh
```
This will copy all hooks from this directory to `.git/hooks/` and make them executable.
## Available Hooks
### pre-commit
The pre-commit hook automatically updates copyright headers in source files before each commit.
**What it does:**
- Runs `scripts/update_copyright.sh` to update copyright years in source files
- Automatically re-stages any modified files
- Ensures copyright headers are always up-to-date
**Requirements:**
- `scripts/update_copyright.sh` must exist and be executable
## Skipping Hooks
If you need to skip the pre-commit hook for a specific commit (not recommended), use:
```bash
git commit --no-verify
```
## Uninstalling
To remove a hook, simply delete it from `.git/hooks/`:
```bash
rm .git/hooks/pre-commit
```

View File

@@ -1,96 +0,0 @@
#!/bin/bash
# Install git hooks for ptprnt repository
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Get the root directory of the git repository
ROOT_DIR=$(git rev-parse --show-toplevel 2>/dev/null)
if [ -z "$ROOT_DIR" ]; then
echo -e "${RED}Error: Not in a git repository${NC}"
exit 1
fi
HOOKS_SOURCE_DIR="$ROOT_DIR/hooks"
HOOKS_TARGET_DIR="$ROOT_DIR/.git/hooks"
echo "Installing git hooks..."
echo " Source: $HOOKS_SOURCE_DIR"
echo " Target: $HOOKS_TARGET_DIR"
echo ""
# Check if hooks directory exists
if [ ! -d "$HOOKS_SOURCE_DIR" ]; then
echo -e "${RED}Error: Hooks source directory not found: $HOOKS_SOURCE_DIR${NC}"
exit 1
fi
# Check if .git/hooks directory exists
if [ ! -d "$HOOKS_TARGET_DIR" ]; then
echo -e "${RED}Error: Git hooks directory not found: $HOOKS_TARGET_DIR${NC}"
exit 1
fi
# Install each hook
installed_count=0
for hook_file in "$HOOKS_SOURCE_DIR"/*; do
# Skip the install script itself
if [[ "$(basename "$hook_file")" == "install_hooks.sh" ]]; then
continue
fi
# Skip if not a file
if [ ! -f "$hook_file" ]; then
continue
fi
hook_name=$(basename "$hook_file")
target_file="$HOOKS_TARGET_DIR/$hook_name"
# Check if hook already exists
if [ -f "$target_file" ]; then
echo -e "${YELLOW}Warning: Hook already exists: $hook_name${NC}"
read -p " Overwrite? (y/N) " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo " Skipped: $hook_name"
continue
fi
fi
# Copy and make executable
cp "$hook_file" "$target_file"
chmod +x "$target_file"
echo -e "${GREEN}${NC} Installed: $hook_name"
((installed_count++))
done
echo ""
if [ $installed_count -eq 0 ]; then
echo -e "${YELLOW}No hooks were installed${NC}"
else
echo -e "${GREEN}Successfully installed $installed_count hook(s)${NC}"
fi
# Verify update_copyright.sh exists and is executable
if [ -f "$ROOT_DIR/scripts/update_copyright.sh" ]; then
if [ ! -x "$ROOT_DIR/scripts/update_copyright.sh" ]; then
echo ""
echo -e "${YELLOW}Making update_copyright.sh executable...${NC}"
chmod +x "$ROOT_DIR/scripts/update_copyright.sh"
echo -e "${GREEN}${NC} update_copyright.sh is now executable"
fi
else
echo ""
echo -e "${YELLOW}Warning: scripts/update_copyright.sh not found${NC}"
echo " The pre-commit hook requires this script to function properly"
fi
echo ""
echo "Hook installation complete!"

View File

@@ -1,43 +0,0 @@
#!/bin/bash
# Pre-commit hook to update copyright headers
# Get the root directory of the git repository
ROOT_DIR=$(git rev-parse --show-toplevel)
# Check if update_copyright.sh exists and is executable
if [ ! -x "$ROOT_DIR/scripts/update_copyright.sh" ]; then
echo "Warning: scripts/update_copyright.sh not found or not executable"
exit 1
fi
# Get list of staged C++ source files
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(cpp|hpp|h|c|cc)$' || true)
if [ -z "$STAGED_FILES" ]; then
# No C++ files staged, nothing to do
exit 0
fi
echo "Updating copyright headers for staged files..."
# Update copyright for each staged file
updated=0
for file in $STAGED_FILES; do
if [ -f "$ROOT_DIR/$file" ]; then
# Run update_copyright.sh on the file
if "$ROOT_DIR/scripts/update_copyright.sh" "$ROOT_DIR/$file" > /dev/null 2>&1; then
# Re-stage the file if it was modified
git add "$ROOT_DIR/$file"
echo " ✓ Updated: $file"
((updated++))
fi
fi
done
if [ $updated -gt 0 ]; then
echo "Updated copyright headers in $updated file(s)"
else
echo "No copyright headers needed updating"
fi
exit 0

View File

@@ -1,11 +1,11 @@
project( project(
'ptprnt', 'ptprnt',
'cpp', 'cpp',
version: 'v0.2.0-' version: 'v0.1.0-' + run_command(
+ run_command(
'git', 'git',
'rev-parse', 'rev-parse',
'--short', 'HEAD', '--short',
'HEAD',
check: true, check: true,
).stdout().strip(), ).stdout().strip(),
license: 'GPLv3', license: 'GPLv3',
@@ -20,17 +20,11 @@ project(
) )
usb_dep = dependency('libusb-1.0') usb_dep = dependency('libusb-1.0')
log_dep = dependency('spdlog')
fmt_dep = dependency('fmt')
pangocairo_dep = dependency('pangocairo') pangocairo_dep = dependency('pangocairo')
# spdlog with std::format (C++20) - static library # CLI11
spdlog_proj = subproject('spdlog', default_options: ['std_format=enabled', 'default_library=static', 'compile_library=true'])
log_dep = spdlog_proj.get_variable('spdlog_dep')
if not log_dep.found()
error('spdlog not found, can not proceed')
endif
# CLI11
cli11_proj = subproject('cli11') cli11_proj = subproject('cli11')
cli11_dep = cli11_proj.get_variable('CLI11_dep') cli11_dep = cli11_proj.get_variable('CLI11_dep')
if not cli11_dep.found() if not cli11_dep.found()
@@ -47,36 +41,27 @@ cpp_args = ['-DPROJ_VERSION="' + meson.project_version() + '"']
# USB trace mode option (for debugging without sending to hardware) # USB trace mode option (for debugging without sending to hardware)
if get_option('usb_trace_only') if get_option('usb_trace_only')
cpp_args += ['-DUSB_TRACE_ONLY'] cpp_args += ['-DUSB_TRACE_ONLY']
message( message('USB_TRACE_ONLY enabled: USB data will be logged but not sent to device')
'USB_TRACE_ONLY enabled: USB data will be logged but not sent to device',
)
endif endif
ptprnt_exe = executable( ptprnt_exe = executable(
'ptprnt', 'ptprnt',
'src/main.cpp', 'src/main.cpp',
install: true, install: true,
dependencies: [usb_dep, log_dep, pangocairo_dep, cli11_dep], dependencies: [usb_dep, log_dep, fmt_dep, pangocairo_dep, cli11_dep],
include_directories: incdir, include_directories: incdir,
sources: [ptprnt_srcs], sources: [ptprnt_srcs],
cpp_args: cpp_args, cpp_args: cpp_args,
) )
### Unit tests ### Unit tests
# Only build tests for debug builds or when explicitly enabled
build_tests = get_option('buildtype') == 'debug' or get_option('build_tests')
if build_tests # GTest
# GTest and GMock gtest_proj = subproject('gtest')
gtest_proj = subproject('gtest') gtest_dep = gtest_proj.get_variable('gtest_main_dep')
gtest_dep = gtest_proj.get_variable('gtest_main_dep') if not gtest_dep.found()
gmock_dep = gtest_proj.get_variable('gmock_main_dep') error('MESON_SKIP_TEST: gtest not installed.')
if not gtest_dep.found() endif
error('MESON_SKIP_TEST: gtest not installed.')
endif
subdir('tests') subdir('tests')
message('Tests enabled (buildtype=' + get_option('buildtype') + ')')
else
message('Tests disabled (use debug build or -Dbuild_tests=true to enable)')
endif

View File

@@ -2,8 +2,3 @@ option('usb_trace_only',
type: 'boolean', type: 'boolean',
value: false, value: false,
description: 'Enable USB trace mode: log USB data without sending to device (saves label tape during debugging)') description: 'Enable USB trace mode: log USB data without sending to device (saves label tape during debugging)')
option('build_tests',
type: 'boolean',
value: false,
description: 'Build unit tests (automatically enabled for debug builds)')

View File

@@ -1,162 +0,0 @@
#!/bin/bash
# Build script for ptprnt - simplifies common build configurations
# Usage: ./scripts/build.sh [release|debug|clean] [options]
set -e
SCRIPT_PATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )"
PROJECT_ROOT="${SCRIPT_PATH}/.."
# Color output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
print_usage() {
echo "Usage: $0 [build-type] [options]"
echo ""
echo "Build Types:"
echo " release Build optimized release version (default)"
echo " debug Build debug version with symbols"
echo " clean Clean build directories"
echo ""
echo "Options:"
echo " --coverage Enable coverage reporting (debug builds only)"
echo " --reconfigure Force reconfiguration"
echo " --test Run tests after building"
echo " -j N Use N parallel jobs (default: auto)"
echo ""
echo "Examples:"
echo " $0 # Build release"
echo " $0 debug --test # Build debug and run tests"
echo " $0 debug --coverage # Build debug with coverage"
echo " $0 clean # Clean all build directories"
}
# Default values
BUILD_TYPE="release"
BUILDDIR="builddir"
COVERAGE=false
RECONFIGURE=false
RUN_TESTS=false
JOBS=""
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
release|debug|clean)
BUILD_TYPE="$1"
shift
;;
--coverage)
COVERAGE=true
shift
;;
--reconfigure)
RECONFIGURE=true
shift
;;
--test)
RUN_TESTS=true
shift
;;
-j)
JOBS="-j $2"
shift 2
;;
-h|--help)
print_usage
exit 0
;;
*)
echo -e "${RED}Error: Unknown option: $1${NC}"
print_usage
exit 1
;;
esac
done
cd "${PROJECT_ROOT}"
# Handle clean
if [[ "${BUILD_TYPE}" == "clean" ]]; then
echo -e "${YELLOW}Cleaning build directories...${NC}"
rm -rf builddir builddir-debug
echo -e "${GREEN}Clean complete!${NC}"
exit 0
fi
# Set build directory and options based on build type
if [[ "${BUILD_TYPE}" == "debug" ]]; then
BUILDDIR="builddir-debug"
MESON_OPTS="--buildtype=debug"
if [[ "${COVERAGE}" == true ]]; then
MESON_OPTS="${MESON_OPTS} -Db_coverage=true"
echo -e "${BLUE}Building debug with coverage enabled${NC}"
else
echo -e "${BLUE}Building debug version${NC}"
fi
else
BUILDDIR="builddir"
MESON_OPTS="--buildtype=release"
if [[ "${COVERAGE}" == true ]]; then
echo -e "${YELLOW}Warning: Coverage is only supported for debug builds, ignoring --coverage${NC}"
fi
if [[ "${RUN_TESTS}" == true ]]; then
echo -e "${YELLOW}Warning: Tests are not built for release builds (use debug build for testing)${NC}"
RUN_TESTS=false
fi
echo -e "${BLUE}Building release version (tests disabled)${NC}"
fi
# Setup or reconfigure build directory
if [[ ! -d "${BUILDDIR}" ]] || [[ "${RECONFIGURE}" == true ]]; then
if [[ "${RECONFIGURE}" == true ]]; then
echo -e "${YELLOW}Reconfiguring build...${NC}"
meson setup "${BUILDDIR}" ${MESON_OPTS} --wipe --reconfigure
else
echo -e "${YELLOW}Setting up build directory...${NC}"
meson setup "${BUILDDIR}" ${MESON_OPTS}
fi
fi
# Build
echo -e "${YELLOW}Building...${NC}"
ninja -C "${BUILDDIR}" ${JOBS}
if [[ $? -eq 0 ]]; then
echo -e "${GREEN}Build successful!${NC}"
echo -e "Binary: ${BUILDDIR}/ptprnt"
else
echo -e "${RED}Build failed!${NC}"
exit 1
fi
# Run tests if requested
if [[ "${RUN_TESTS}" == true ]]; then
echo -e "${YELLOW}Running tests...${NC}"
ninja -C "${BUILDDIR}" test
if [[ $? -eq 0 ]]; then
echo -e "${GREEN}All tests passed!${NC}"
else
echo -e "${RED}Tests failed!${NC}"
exit 1
fi
fi
# Show binary info
echo ""
echo -e "${BLUE}Build Information:${NC}"
echo " Build type: ${BUILD_TYPE}"
echo " Build dir: ${BUILDDIR}"
echo " Binary: $(ls -lh ${BUILDDIR}/ptprnt | awk '{print $5, $9}')"
echo ""
echo -e "${GREEN}Done!${NC}"

View File

@@ -1,195 +0,0 @@
#!/bin/bash
# Coverage report generator for ptprnt
# Usage: ./scripts/generate_coverage.sh [options]
set -e
SCRIPT_PATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )"
PROJECT_ROOT="${SCRIPT_PATH}/.."
# Output paths
HTML_COV_PATH="coverageReport/html"
XML_COV_PATH="coverageReport/xml"
TEXT_COV_PATH="coverageReport"
HTML_START_FILE="index.html"
# Color output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Default values - all formats enabled by default
GENERATE_HTML=false
GENERATE_XML=false
GENERATE_TEXT=false
BUILDDIR="builddir-debug"
# Common gcovr options
GCOVR_OPTS="--filter src --root ."
print_usage() {
echo "Usage: $0 [options]"
echo ""
echo "Coverage Report Generator - generates coverage reports from existing coverage data"
echo ""
echo "Prerequisites:"
echo " Build with coverage enabled first:"
echo " ./scripts/build.sh debug --coverage --test"
echo ""
echo "Format Options (if none specified, all formats are generated):"
echo " --html Generate HTML coverage report"
echo " --xml Generate XML coverage report (for CI/CD)"
echo " --text Generate text coverage report (terminal output)"
echo ""
echo "Build Options:"
echo " --builddir DIR Use custom build directory (default: builddir-debug)"
echo ""
echo "Other Options:"
echo " -h, --help Show this help message"
echo ""
echo "Examples:"
echo " $0 # Generate all formats (html, xml, text)"
echo " $0 --html # Generate only HTML report"
echo " $0 --html --text # Generate HTML and text reports"
echo " $0 --xml # Generate XML for CI/CD"
echo " $0 --builddir builddir # Use release build directory"
}
# Parse arguments
# If no format arguments provided, enable all
if [[ $# -eq 0 ]]; then
GENERATE_HTML=true
GENERATE_XML=true
GENERATE_TEXT=true
fi
while [[ $# -gt 0 ]]; do
case $1 in
--html)
GENERATE_HTML=true
shift
;;
--xml)
GENERATE_XML=true
shift
;;
--text)
GENERATE_TEXT=true
shift
;;
--builddir)
BUILDDIR="$2"
shift 2
;;
-h|--help)
print_usage
exit 0
;;
*)
echo -e "${RED}Error: Unknown option: $1${NC}"
print_usage
exit 1
;;
esac
done
# Check if any format was selected when arguments were provided
if [[ $GENERATE_HTML == false && $GENERATE_XML == false && $GENERATE_TEXT == false ]]; then
echo -e "${RED}Error: No output format specified. Use --html, --xml, and/or --text${NC}"
print_usage
exit 1
fi
cd "${PROJECT_ROOT}"
# Check if build directory exists
if [[ ! -d "${BUILDDIR}" ]]; then
echo -e "${RED}Error: Build directory '${BUILDDIR}' does not exist${NC}"
echo ""
echo "Build with coverage enabled first:"
echo " ./scripts/build.sh debug --coverage --test"
exit 1
fi
# Check if coverage data exists by looking for .gcda files
if ! find "${BUILDDIR}" -name "*.gcda" -print -quit | grep -q .; then
echo -e "${RED}Error: No coverage data found in '${BUILDDIR}'${NC}"
echo ""
echo "Make sure you built with coverage enabled and ran the tests:"
echo " ./scripts/build.sh debug --coverage --test"
exit 1
fi
echo -e "${BLUE}Generating Coverage report for ptprnt${NC}"
echo "Build directory: ${BUILDDIR}"
echo "Formats: $(${GENERATE_HTML} && echo -n "html ")$(${GENERATE_XML} && echo -n "xml ")$(${GENERATE_TEXT} && echo -n "text")"
echo ""
# Check if gcovr is available
if ! command -v gcovr &> /dev/null; then
echo -e "${RED}Error: gcovr is not installed${NC}"
exit 1
fi
if [[ "${GENERATE_HTML}" == true ]]; then
echo -e "${YELLOW}Generating HTML coverage report...${NC}"
mkdir -p "${HTML_COV_PATH}"
gcovr ${GCOVR_OPTS} \
--html --html-details --html-syntax-highlighting \
--output "${HTML_COV_PATH}/${HTML_START_FILE}"
if [[ $? -eq 0 ]]; then
echo -e "${GREEN}✓ HTML report generated${NC}"
echo ""
echo -e "${BLUE}To view HTML report, open:${NC}"
echo " file://${SCRIPT_PATH}/../${HTML_COV_PATH}/${HTML_START_FILE}"
else
echo -e "${RED}✗ HTML report failed${NC}"
fi
fi
if [[ "${GENERATE_XML}" == true ]]; then
echo -e "${YELLOW}Generating XML coverage report...${NC}"
mkdir -p "${XML_COV_PATH}"
gcovr ${GCOVR_OPTS} \
--xml-pretty \
--output "${XML_COV_PATH}/cov.xml"
if [[ $? -eq 0 ]]; then
echo -e "${GREEN}✓ XML report generated${NC}"
echo ""
echo -e "${BLUE}To view XML report, open:${NC}"
echo " file://${SCRIPT_PATH}/../${XML_COV_PATH}/cov.xml"
else
echo -e "${RED}✗ XML report failed${NC}"
fi
fi
if [[ "${GENERATE_TEXT}" == true ]]; then
echo -e "${YELLOW}Generating text coverage report...${NC}"
mkdir -p "${TEXT_COV_PATH}"
# Save to file
gcovr ${GCOVR_OPTS} --output "${TEXT_COV_PATH}/coverage.txt"
if [[ $? -eq 0 ]]; then
echo -e "${GREEN}✓ Text report generated${NC}"
echo ""
echo -e "${BLUE}To view TXT report, open:${NC}"
echo "file://${SCRIPT_PATH}/../${TEXT_COV_PATH}/coverage.txt"
else
echo -e "${RED}✗ Text report failed${NC}"
fi
fi
# Clean up gcov files
rm -f *.gcov 2>/dev/null
# Summary
echo ""
echo -e "${GREEN}Coverage reports generated successfully!${NC}"
echo ""

View File

@@ -1,6 +1,6 @@
/* /*
ptrnt - print labels on linux ptrnt - print labels on linux
Copyright (C) 2025 Moritz Martinius Copyright (C) 2024-2025 Moritz Martinius
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU General Public License as published by

View File

@@ -1,6 +1,6 @@
/* /*
ptrnt - print labels on linux ptrnt - print labels on linux
Copyright (C) 2025 Moritz Martinius Copyright (C) 2024-2025 Moritz Martinius
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU General Public License as published by
@@ -21,7 +21,7 @@
#include <string> #include <string>
#include <vector> #include <vector>
#include "printers/interface/IPrinterDriver.hpp" #include "interface/IPrinterDriver.hpp"
#include "libusbwrap/LibUsbTypes.hpp" #include "libusbwrap/LibUsbTypes.hpp"
namespace ptprnt { namespace ptprnt {

View File

@@ -18,29 +18,23 @@
*/ */
#include "PtouchPrint.hpp" #include "PtouchPrint.hpp"
#include <format> #include <fmt/core.h>
#include <iostream>
#include <spdlog/sinks/basic_file_sink.h> #include <spdlog/sinks/basic_file_sink.h>
#include <spdlog/sinks/stdout_color_sinks.h> #include <spdlog/sinks/stdout_color_sinks.h>
#include <spdlog/spdlog.h> #include <spdlog/spdlog.h>
#include "PrinterDriverFactory.hpp"
#include "cli/CliParser.hpp" #include "cli/CliParser.hpp"
#include "cli/interface/ICliParser.hpp"
#include "constants.hpp" #include "constants.hpp"
#include "core/PrinterDriverFactory.hpp"
#include "core/PrinterService.hpp" #include "core/PrinterService.hpp"
#include "core/interface/IPrinterService.hpp"
#include "graphics/LabelBuilder.hpp" #include "graphics/LabelBuilder.hpp"
namespace ptprnt { namespace ptprnt {
PtouchPrint::PtouchPrint(const char* versionString) PtouchPrint::PtouchPrint(const char* versionString)
: PtouchPrint(versionString, std::make_unique<cli::CliParser>(ptprnt::APP_DESC, versionString), : mVersionString(versionString),
std::make_unique<core::PrinterService>()) {} mCliParser(std::make_unique<cli::CliParser>(ptprnt::APP_DESC, versionString)),
mPrinterService(std::make_unique<core::PrinterService>()) {}
PtouchPrint::PtouchPrint(const char* versionString, std::unique_ptr<cli::ICliParser> cliParser,
std::unique_ptr<core::IPrinterService> printerService)
: mVersionString(versionString), mCliParser(std::move(cliParser)), mPrinterService(std::move(printerService)) {}
PtouchPrint::~PtouchPrint() = default; PtouchPrint::~PtouchPrint() = default;
@@ -103,13 +97,13 @@ void PtouchPrint::setupLogger() {
bool PtouchPrint::handleListDrivers() { bool PtouchPrint::handleListDrivers() {
auto driverFactory = std::make_unique<PrinterDriverFactory>(); auto driverFactory = std::make_unique<PrinterDriverFactory>();
auto drivers = driverFactory->listAllDrivers(); auto drivers = driverFactory->listAllDrivers();
std::cout << "Available printer drivers:\n"; fmt::print("Available printer drivers:\n");
for (const auto& driver : drivers) { for (const auto& driver : drivers) {
std::cout << std::format(" - {}\n", driver); fmt::print(" - {}\n", driver);
} }
std::cout << "\nUse with: -p <driver_name> or --printer <driver_name>\n"; fmt::print("\nUse with: -p <driver_name> or --printer <driver_name>\n");
return true; return true;
} }
@@ -133,48 +127,13 @@ bool PtouchPrint::handlePrinting() {
return true; return true;
} }
// Build label incrementally, appending when --new is encountered // Build label using LabelBuilder
graphics::LabelBuilder labelBuilder(printer->getPrinterInfo().pixelLines); graphics::LabelBuilder labelBuilder(printer->getPrinterInfo().pixelLines);
std::unique_ptr<graphics::ILabel> finalLabel = nullptr;
// Debug: print command sequence
spdlog::debug("Processing {} commands:", options.commands.size());
for (size_t i = 0; i < options.commands.size(); ++i) {
const auto& [cmdType, value] = options.commands[i];
spdlog::debug(" Command {}: type={}, value='{}'", i, static_cast<int>(cmdType), value);
}
for (const auto& [cmdType, value] : options.commands) { for (const auto& [cmdType, value] : options.commands) {
switch (cmdType) { switch (cmdType) {
case cli::CommandType::NewLabel: {
// Finish current label and append to final label
spdlog::debug("Encountered --new, finishing current label segment");
auto currentLabel = labelBuilder.build();
if (!finalLabel) {
// First label becomes the base
finalLabel = std::move(currentLabel);
} else {
// If finalLabel is empty (width=0), replace it instead of appending
if (finalLabel->getWidth() == 0) {
spdlog::debug("Final label is empty, replacing instead of appending");
finalLabel = std::move(currentLabel);
} else if (currentLabel->getWidth() == 0) {
// Current label is empty, skip appending
spdlog::debug("Current label is empty, skipping append");
} else {
// Both labels have content, append
if (!finalLabel->append(*currentLabel)) {
spdlog::error("Failed to append label");
return false;
}
}
}
// Reset builder for next label
labelBuilder = graphics::LabelBuilder(printer->getPrinterInfo().pixelLines);
break;
}
case cli::CommandType::Text: case cli::CommandType::Text:
labelBuilder.addText(value); labelBuilder.addText(value + "\n");
break; break;
case cli::CommandType::Font: case cli::CommandType::Font:
spdlog::debug("Setting font to {}", value); spdlog::debug("Setting font to {}", value);
@@ -214,31 +173,9 @@ bool PtouchPrint::handlePrinting() {
} }
} }
// Build and append final label segment // Build and print the label
auto lastLabel = labelBuilder.build(); auto label = labelBuilder.build();
if (!finalLabel) { if (!mPrinterService->printLabel(std::move(label))) {
// Only one label, no --new was used
finalLabel = std::move(lastLabel);
} else {
// Handle empty labels
if (finalLabel->getWidth() == 0) {
// Final label is empty, replace it
spdlog::debug("Final label is empty, replacing with last segment");
finalLabel = std::move(lastLabel);
} else if (lastLabel->getWidth() == 0) {
// Last segment is empty, skip appending
spdlog::debug("Last label segment is empty, skipping append");
} else {
// Both have content, append
if (!finalLabel->append(*lastLabel)) {
spdlog::error("Failed to append final label segment");
return false;
}
}
}
// Print the final label
if (!mPrinterService->printLabel(std::move(finalLabel))) {
spdlog::error("An error occurred while printing"); spdlog::error("An error occurred while printing");
return false; return false;
} }

View File

@@ -21,18 +21,13 @@
#include <memory> #include <memory>
#include <string> #include <string>
#include <vector>
namespace ptprnt::cli { namespace ptprnt::cli {
class ICliParser; class CliParser;
} }
namespace ptprnt::core { namespace ptprnt::core {
class IPrinterService; class PrinterService;
}
namespace ptprnt::graphics {
class ILabel;
} }
namespace ptprnt { namespace ptprnt {
@@ -42,27 +37,14 @@ namespace ptprnt {
* *
* Acts as a thin glue layer coordinating CLI parsing and core printer functionality. * Acts as a thin glue layer coordinating CLI parsing and core printer functionality.
* Separates CLI frontend concerns from the core library. * Separates CLI frontend concerns from the core library.
*
* Uses interfaces (ICliParser, IPrinterService) to enable dependency injection
* and facilitate unit testing with mocks.
*/ */
class PtouchPrint { class PtouchPrint {
public: public:
/** /**
* @brief Construct the application with default implementations * @brief Construct the application
* @param versionString Version string to display * @param versionString Version string to display
*/ */
PtouchPrint(const char* versionString); PtouchPrint(const char* versionString);
/**
* @brief Construct with custom implementations (for testing)
* @param versionString Version string to display
* @param cliParser Custom CLI parser implementation
* @param printerService Custom printer service implementation
*/
PtouchPrint(const char* versionString, std::unique_ptr<cli::ICliParser> cliParser,
std::unique_ptr<core::IPrinterService> printerService);
~PtouchPrint(); // Must be defined in .cpp where complete types are visible ~PtouchPrint(); // Must be defined in .cpp where complete types are visible
// This is basically a singleton application class, no need to copy or move // This is basically a singleton application class, no need to copy or move
@@ -91,8 +73,8 @@ class PtouchPrint {
bool handlePrinting(); bool handlePrinting();
std::string mVersionString; std::string mVersionString;
std::unique_ptr<cli::ICliParser> mCliParser; std::unique_ptr<cli::CliParser> mCliParser;
std::unique_ptr<core::IPrinterService> mPrinterService; std::unique_ptr<core::PrinterService> mPrinterService;
}; };
} // namespace ptprnt } // namespace ptprnt

View File

@@ -19,13 +19,12 @@
#include "CliParser.hpp" #include "CliParser.hpp"
#include <format> #include <fmt/core.h>
#include <iostream>
namespace ptprnt::cli { namespace ptprnt::cli {
CliParser::CliParser(std::string appDescription, std::string versionString) CliParser::CliParser(const std::string& appDescription, std::string versionString)
: mApp(std::move(appDescription)), mVersionString(std::move(versionString)) { : mApp(appDescription), mVersionString(std::move(versionString)) {
setupParser(); setupParser();
} }
@@ -44,56 +43,13 @@ int CliParser::parse(int argc, char** argv) {
mApp.exit(e); mApp.exit(e);
return -1; // Signal: error return -1; // Signal: error
} }
// Post-process: Re-order commands based on actual command line order
// This is needed because CLI11 groups options by type
reorderCommandsByArgv(argc, argv);
return 0; return 0;
} }
void CliParser::reorderCommandsByArgv(int argc, char** argv) {
std::vector<Command> reorderedCommands;
// Parse argv to determine the actual order
for (int i = 1; i < argc; ++i) {
std::string arg = argv[i];
if (arg == "--new") {
reorderedCommands.emplace_back(CommandType::NewLabel, "");
} else if (arg == "-t" || arg == "--text") {
if (i + 1 < argc) {
reorderedCommands.emplace_back(CommandType::Text, argv[++i]);
}
} else if (arg == "-f" || arg == "--font") {
if (i + 1 < argc) {
reorderedCommands.emplace_back(CommandType::Font, argv[++i]);
}
} else if (arg == "-s" || arg == "--fontsize") {
if (i + 1 < argc) {
reorderedCommands.emplace_back(CommandType::FontSize, argv[++i]);
}
} else if (arg == "--valign") {
if (i + 1 < argc) {
reorderedCommands.emplace_back(CommandType::VAlign, argv[++i]);
}
} else if (arg == "--halign") {
if (i + 1 < argc) {
reorderedCommands.emplace_back(CommandType::HAlign, argv[++i]);
}
}
}
// Only replace if we found relevant commands
if (!reorderedCommands.empty()) {
mOptions.commands = std::move(reorderedCommands);
}
}
void CliParser::setupParser() { void CliParser::setupParser() {
// Version callback // Version callback
auto printVersion = [this](std::size_t) { auto printVersion = [this](std::size_t) {
std::cout << std::format("ptprnt version: {}\n", mVersionString); fmt::print("ptprnt version: {}\n", mVersionString);
throw CLI::CallForVersion(); throw CLI::CallForVersion();
}; };
@@ -111,9 +67,8 @@ void CliParser::setupParser() {
// Text printing options // Text printing options
// Note: CLI11 options are processed in order when using ->each() with callbacks // Note: CLI11 options are processed in order when using ->each() with callbacks
mApp.add_option( mApp.add_option("-t,--text",
"-t,--text", "Text to print (can be used multiple times, use formatting options before to influence text layout)")
"Text to print (can be used multiple times, use formatting options before to influence text layout)")
->multi_option_policy(CLI::MultiOptionPolicy::TakeAll) ->multi_option_policy(CLI::MultiOptionPolicy::TakeAll)
->each([this](const std::string& text) { mOptions.commands.emplace_back(CommandType::Text, text); }); ->each([this](const std::string& text) { mOptions.commands.emplace_back(CommandType::Text, text); });
@@ -133,12 +88,6 @@ void CliParser::setupParser() {
mApp.add_option("--halign", "Horizontal alignment of the following text occurrences") mApp.add_option("--halign", "Horizontal alignment of the following text occurrences")
->multi_option_policy(CLI::MultiOptionPolicy::TakeAll) ->multi_option_policy(CLI::MultiOptionPolicy::TakeAll)
->each([this](const std::string& align) { mOptions.commands.emplace_back(CommandType::HAlign, align); }); ->each([this](const std::string& align) { mOptions.commands.emplace_back(CommandType::HAlign, align); });
// Label separator - use an option with multi_option_policy to maintain parse order
// We need to use a dummy string parameter since .each() expects a string callback
mApp.add_flag("--new", "Start a new label (multiple labels will be stitched together)")
->multi_option_policy(CLI::MultiOptionPolicy::TakeAll)
->each([this](const std::string&) { mOptions.commands.emplace_back(CommandType::NewLabel, ""); });
} }
} // namespace ptprnt::cli } // namespace ptprnt::cli

View File

@@ -22,28 +22,47 @@
#include <CLI/CLI.hpp> #include <CLI/CLI.hpp>
#include <string> #include <string>
#include <vector>
#include "interface/ICliParser.hpp"
namespace ptprnt::cli { namespace ptprnt::cli {
/**
* @brief Types of CLI commands that can be issued
*/
enum class CommandType { None = 0, Text = 1, FontSize = 2, Font = 3, VAlign = 4, HAlign = 5 };
/**
* @brief A command with its type and value
*/
using Command = std::pair<CommandType, std::string>;
/**
* @brief Parsed CLI options and commands
*/
struct CliOptions {
bool verbose{false};
bool trace{false};
bool listDrivers{false};
std::string printerSelection{"auto"};
std::vector<Command> commands{};
};
/** /**
* @brief CLI argument parser for ptprnt * @brief CLI argument parser for ptprnt
* *
* Concrete implementation of ICliParser using CLI11. * Handles all command-line argument parsing using CLI11.
* Handles all command-line argument parsing.
* Separates CLI concerns from core library functionality. * Separates CLI concerns from core library functionality.
*/ */
class CliParser : public ICliParser { class CliParser {
public: public:
/** /**
* @brief Construct a CLI parser * @brief Construct a CLI parser
* @param appDescription Application description for help text * @param appDescription Application description for help text
* @param versionString Version string to display * @param versionString Version string to display
*/ */
CliParser(std::string appDescription, std::string versionString); CliParser(const std::string& appDescription, std::string versionString);
~CliParser() override = default; ~CliParser() = default;
CliParser(const CliParser&) = delete; CliParser(const CliParser&) = delete;
CliParser& operator=(const CliParser&) = delete; CliParser& operator=(const CliParser&) = delete;
@@ -56,17 +75,16 @@ class CliParser : public ICliParser {
* @param argv Argument values * @param argv Argument values
* @return 0 on success, positive value if should exit immediately (help/version), negative on error * @return 0 on success, positive value if should exit immediately (help/version), negative on error
*/ */
int parse(int argc, char** argv) override; int parse(int argc, char** argv);
/** /**
* @brief Get the parsed options * @brief Get the parsed options
* @return Reference to parsed options * @return Reference to parsed options
*/ */
[[nodiscard]] const CliOptions& getOptions() const override { return mOptions; } [[nodiscard]] const CliOptions& getOptions() const { return mOptions; }
private: private:
void setupParser(); void setupParser();
void reorderCommandsByArgv(int argc, char** argv);
CLI::App mApp; CLI::App mApp;
std::string mVersionString; std::string mVersionString;

View File

@@ -1,79 +0,0 @@
/*
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 <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <string>
#include <vector>
namespace ptprnt::cli {
/**
* @brief Types of CLI commands that can be issued
*/
enum class CommandType { None = 0, Text = 1, FontSize = 2, Font = 3, VAlign = 4, HAlign = 5, NewLabel = 6 };
/**
* @brief A command with its type and value
*/
using Command = std::pair<CommandType, std::string>;
/**
* @brief Parsed CLI options and commands
*/
struct CliOptions {
bool verbose{false};
bool trace{false};
bool listDrivers{false};
std::string printerSelection{"auto"};
std::vector<Command> commands{};
};
/**
* @brief Interface for CLI argument parsing
*
* This interface allows for mocking CLI parsing in unit tests
* and provides a clear contract for CLI parser implementations.
*/
class ICliParser {
public:
virtual ~ICliParser() = default;
ICliParser() = default;
ICliParser(const ICliParser&) = default;
ICliParser& operator=(const ICliParser&) = default;
ICliParser(ICliParser&&) noexcept = default;
ICliParser& operator=(ICliParser&&) noexcept = default;
/**
* @brief Parse command line arguments
* @param argc Argument count
* @param argv Argument values
* @return 0 on success, positive value if should exit immediately (help/version), negative on error
*/
virtual int parse(int argc, char** argv) = 0;
/**
* @brief Get the parsed options
* @return Reference to parsed options
*/
[[nodiscard]] virtual const CliOptions& getOptions() const = 0;
};
} // namespace ptprnt::cli

View File

@@ -21,20 +21,14 @@
#include <spdlog/spdlog.h> #include <spdlog/spdlog.h>
#include "core/PrinterDriverFactory.hpp" #include "PrinterDriverFactory.hpp"
#include "libusbwrap/UsbDeviceFactory.hpp"
namespace ptprnt::core { namespace ptprnt::core {
// Default constructor delegates to DI constructor PrinterService::PrinterService() = default;
PrinterService::PrinterService() : PrinterService(std::make_unique<libusbwrap::UsbDeviceFactory>()) {}
// Constructor with dependency injection
PrinterService::PrinterService(std::unique_ptr<libusbwrap::IUsbDeviceFactory> usbFactory)
: mUsbDeviceFactory(std::move(usbFactory)) {}
bool PrinterService::initialize() { bool PrinterService::initialize() {
if (!mUsbDeviceFactory->init()) { if (!mUsbDeviceFactory.init()) {
spdlog::error("Could not initialize libusb"); spdlog::error("Could not initialize libusb");
return false; return false;
} }
@@ -44,22 +38,14 @@ bool PrinterService::initialize() {
std::vector<std::shared_ptr<IPrinterDriver>> PrinterService::detectPrinters() { std::vector<std::shared_ptr<IPrinterDriver>> PrinterService::detectPrinters() {
spdlog::debug("Detecting printers..."); spdlog::debug("Detecting printers...");
auto usbDevs = mUsbDeviceFactory->findAllDevices(); auto usbDevs = mUsbDeviceFactory.findAllDevices();
auto driverFactory = std::make_unique<PrinterDriverFactory>(); auto driverFactory = std::make_unique<PrinterDriverFactory>();
mDetectedPrinters.clear(); mDetectedPrinters.clear();
for (auto& usbDev : usbDevs) { for (auto& usbDev : usbDevs) {
auto driver = driverFactory->create(usbDev->getUsbId()); auto driver = driverFactory->create(usbDev->getUsbId());
if (driver != nullptr) { if (driver != nullptr) {
// Attach the USB device to the printer driver mDetectedPrinters.push_back(driver);
// Convert unique_ptr to shared_ptr for attachment
std::shared_ptr<libusbwrap::IUsbDevice> sharedUsbDev = std::move(usbDev);
if (driver->attachUsbDevice(sharedUsbDev)) {
mDetectedPrinters.push_back(driver);
spdlog::debug("Successfully attached USB device to printer driver: {}", driver->getName());
} else {
spdlog::warn("Failed to attach USB device to printer driver: {}", driver->getName());
}
} }
} }
@@ -68,23 +54,6 @@ std::vector<std::shared_ptr<IPrinterDriver>> PrinterService::detectPrinters() {
} }
std::shared_ptr<IPrinterDriver> PrinterService::selectPrinter(const std::string& printerName) { std::shared_ptr<IPrinterDriver> PrinterService::selectPrinter(const std::string& printerName) {
// If a specific printer is requested by name (not "auto"), try to create it directly
if (printerName != "auto") {
auto driverFactory = std::make_unique<PrinterDriverFactory>();
auto printer = driverFactory->createByName(printerName);
if (printer) {
// For virtual/fake printers, call attachUsbDevice with nullptr to initialize
// For real printers selected explicitly, they would need actual USB device
printer->attachUsbDevice(nullptr);
mCurrentPrinter = printer;
spdlog::info("Using explicitly selected printer: {}", printerName);
return mCurrentPrinter;
}
spdlog::error("Printer driver '{}' not found", printerName);
return nullptr;
}
// Auto mode: detect USB printers
if (mDetectedPrinters.empty()) { if (mDetectedPrinters.empty()) {
detectPrinters(); detectPrinters();
} }
@@ -94,10 +63,24 @@ std::shared_ptr<IPrinterDriver> PrinterService::selectPrinter(const std::string&
return nullptr; return nullptr;
} }
// Auto-select first detected printer // Auto-select first printer
mCurrentPrinter = mDetectedPrinters.front(); if (printerName == "auto") {
spdlog::info("Auto-selected printer: {}", mCurrentPrinter->getName()); mCurrentPrinter = mDetectedPrinters.front();
return mCurrentPrinter; spdlog::info("Auto-selected printer: {}", mCurrentPrinter->getName());
return mCurrentPrinter;
}
// Select printer by name
for (auto& printer : mDetectedPrinters) {
if (printer->getDriverName() == printerName) {
mCurrentPrinter = printer;
spdlog::info("Using explicitly selected printer: {}", printerName);
return mCurrentPrinter;
}
}
spdlog::error("Printer '{}' not found", printerName);
return nullptr;
} }
bool PrinterService::printLabel(std::unique_ptr<graphics::ILabel> label) { bool PrinterService::printLabel(std::unique_ptr<graphics::ILabel> label) {

View File

@@ -23,30 +23,23 @@
#include <string> #include <string>
#include <vector> #include <vector>
#include "interface/IPrinterService.hpp" #include "interface/IPrinterDriver.hpp"
#include "libusbwrap/interface/IUsbDeviceFactory.hpp" #include "libusbwrap/UsbDeviceFactory.hpp"
#include "printers/interface/IPrinterDriver.hpp"
namespace ptprnt::core { namespace ptprnt::core {
/** /**
* @brief Core service for printer operations * @brief Core service for printer operations
* *
* Concrete implementation of IPrinterService.
* Provides the core library functionality for: * Provides the core library functionality for:
* - Detecting printers * - Detecting printers
* - Selecting printers * - Selecting printers
* - Building and printing labels * - Building and printing labels
*/ */
class PrinterService : public IPrinterService { class PrinterService {
public: public:
// Default constructor (uses real UsbDeviceFactory)
PrinterService(); PrinterService();
~PrinterService() = default;
// Constructor for testing (inject mock factory)
explicit PrinterService(std::unique_ptr<libusbwrap::IUsbDeviceFactory> usbFactory);
~PrinterService() override = default;
PrinterService(const PrinterService&) = delete; PrinterService(const PrinterService&) = delete;
PrinterService& operator=(const PrinterService&) = delete; PrinterService& operator=(const PrinterService&) = delete;
@@ -57,36 +50,36 @@ class PrinterService : public IPrinterService {
* @brief Initialize USB device factory * @brief Initialize USB device factory
* @return true on success, false on failure * @return true on success, false on failure
*/ */
bool initialize() override; bool initialize();
/** /**
* @brief Detect all compatible printers * @brief Detect all compatible printers
* @return Vector of detected printers * @return Vector of detected printers
*/ */
std::vector<std::shared_ptr<IPrinterDriver>> detectPrinters() override; std::vector<std::shared_ptr<IPrinterDriver>> detectPrinters();
/** /**
* @brief Select a printer by name or auto-detect * @brief Select a printer by name or auto-detect
* @param printerName Printer driver name, or "auto" for first detected * @param printerName Printer driver name, or "auto" for first detected
* @return Printer driver, or nullptr if not found * @return Printer driver, or nullptr if not found
*/ */
std::shared_ptr<IPrinterDriver> selectPrinter(const std::string& printerName) override; std::shared_ptr<IPrinterDriver> selectPrinter(const std::string& printerName);
/** /**
* @brief Get the currently selected printer * @brief Get the currently selected printer
* @return Current printer, or nullptr if none selected * @return Current printer, or nullptr if none selected
*/ */
[[nodiscard]] std::shared_ptr<IPrinterDriver> getCurrentPrinter() const override { return mCurrentPrinter; } [[nodiscard]] std::shared_ptr<IPrinterDriver> getCurrentPrinter() const { return mCurrentPrinter; }
/** /**
* @brief Print a label * @brief Print a label
* @param label The label to print * @param label The label to print
* @return true on success, false on failure * @return true on success, false on failure
*/ */
bool printLabel(std::unique_ptr<graphics::ILabel> label) override; bool printLabel(std::unique_ptr<graphics::ILabel> label);
private: private:
std::unique_ptr<libusbwrap::IUsbDeviceFactory> mUsbDeviceFactory; libusbwrap::UsbDeviceFactory mUsbDeviceFactory;
std::vector<std::shared_ptr<IPrinterDriver>> mDetectedPrinters; std::vector<std::shared_ptr<IPrinterDriver>> mDetectedPrinters;
std::shared_ptr<IPrinterDriver> mCurrentPrinter; std::shared_ptr<IPrinterDriver> mCurrentPrinter;
}; };

View File

@@ -1,74 +0,0 @@
/*
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 <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <memory>
#include <string>
#include <vector>
#include "graphics/interface/ILabel.hpp"
#include "printers/interface/IPrinterDriver.hpp"
namespace ptprnt::core {
/**
* @brief Interface for core printer service operations
*
* This interface allows for mocking printer operations in unit tests
* and provides a clear contract for printer service implementations.
*/
class IPrinterService {
public:
virtual ~IPrinterService() = default;
/**
* @brief Initialize the printer service
* @return true on success, false on failure
*/
virtual bool initialize() = 0;
/**
* @brief Detect all compatible printers
* @return Vector of detected printers
*/
virtual std::vector<std::shared_ptr<IPrinterDriver>> detectPrinters() = 0;
/**
* @brief Select a printer by name or auto-detect
* @param printerName Printer driver name, or "auto" for first detected
* @return Printer driver, or nullptr if not found
*/
virtual std::shared_ptr<IPrinterDriver> selectPrinter(const std::string& printerName) = 0;
/**
* @brief Get the currently selected printer
* @return Current printer, or nullptr if none selected
*/
[[nodiscard]] virtual std::shared_ptr<IPrinterDriver> getCurrentPrinter() const = 0;
/**
* @brief Print a label
* @param label The label to print
* @return true on success, false on failure
*/
virtual bool printLabel(std::unique_ptr<graphics::ILabel> label) = 0;
};
} // namespace ptprnt::core

View File

@@ -20,6 +20,8 @@
#pragma once #pragma once
#include <cstdint> #include <cstdint>
#include <memory>
#include <span>
#include <vector> #include <vector>
namespace ptprnt::graphics { namespace ptprnt::graphics {

View File

@@ -1,139 +0,0 @@
/*
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 <https://www.gnu.org/licenses/>.
*/
#pragma once
#include "graphics/interface/ICairoWrapper.hpp"
namespace ptprnt::graphics {
/**
* @brief Real implementation of ICairoWrapper that forwards to actual Cairo/Pango C API
*
* This class simply forwards all calls to the real Cairo and Pango library functions.
* It's used as the default implementation in production code.
*/
class CairoWrapper : public ICairoWrapper {
public:
~CairoWrapper() override = default;
// Cairo image surface functions
cairo_surface_t* cairo_image_surface_create(cairo_format_t format, int width, int height) override {
return ::cairo_image_surface_create(format, width, height);
}
void cairo_surface_destroy(cairo_surface_t* surface) override { ::cairo_surface_destroy(surface); }
void cairo_surface_flush(cairo_surface_t* surface) override { ::cairo_surface_flush(surface); }
void cairo_surface_mark_dirty(cairo_surface_t* surface) override { ::cairo_surface_mark_dirty(surface); }
cairo_status_t cairo_surface_status(cairo_surface_t* surface) override { return ::cairo_surface_status(surface); }
cairo_format_t cairo_image_surface_get_format(cairo_surface_t* surface) override {
return ::cairo_image_surface_get_format(surface);
}
int cairo_image_surface_get_width(cairo_surface_t* surface) override {
return ::cairo_image_surface_get_width(surface);
}
int cairo_image_surface_get_height(cairo_surface_t* surface) override {
return ::cairo_image_surface_get_height(surface);
}
int cairo_image_surface_get_stride(cairo_surface_t* surface) override {
return ::cairo_image_surface_get_stride(surface);
}
unsigned char* cairo_image_surface_get_data(cairo_surface_t* surface) override {
return ::cairo_image_surface_get_data(surface);
}
cairo_status_t cairo_surface_write_to_png(cairo_surface_t* surface, const char* filename) override {
return ::cairo_surface_write_to_png(surface, filename);
}
// Cairo context functions
cairo_t* cairo_create(cairo_surface_t* surface) override { return ::cairo_create(surface); }
void cairo_destroy(cairo_t* cr) override { ::cairo_destroy(cr); }
void cairo_move_to(cairo_t* cr, double x, double y) override { ::cairo_move_to(cr, x, y); }
void cairo_set_source_rgb(cairo_t* cr, double red, double green, double blue) override {
::cairo_set_source_rgb(cr, red, green, blue);
}
// Pango-Cairo functions
PangoFontMap* pango_cairo_font_map_new() override { return ::pango_cairo_font_map_new(); }
PangoContext* pango_cairo_create_context(cairo_t* cr) override { return ::pango_cairo_create_context(cr); }
void pango_cairo_show_layout(cairo_t* cr, PangoLayout* layout) override { ::pango_cairo_show_layout(cr, layout); }
// Pango layout functions
PangoLayout* pango_layout_new(PangoContext* context) override { return ::pango_layout_new(context); }
void pango_layout_set_font_description(PangoLayout* layout, const PangoFontDescription* desc) override {
::pango_layout_set_font_description(layout, desc);
}
void pango_layout_set_text(PangoLayout* layout, const char* text, int length) override {
::pango_layout_set_text(layout, text, length);
}
void pango_layout_set_height(PangoLayout* layout, int height) override {
::pango_layout_set_height(layout, height);
}
void pango_layout_set_alignment(PangoLayout* layout, PangoAlignment alignment) override {
::pango_layout_set_alignment(layout, alignment);
}
void pango_layout_set_justify(PangoLayout* layout, gboolean justify) override {
::pango_layout_set_justify(layout, justify);
}
#if PANGO_VERSION_MAJOR >= 1 && PANGO_VERSION_MINOR >= 50
void pango_layout_set_justify_last_line(PangoLayout* layout, gboolean justify) override {
::pango_layout_set_justify_last_line(layout, justify);
}
#endif
void pango_layout_get_size(PangoLayout* layout, int* width, int* height) override {
::pango_layout_get_size(layout, width, height);
}
// Pango font description functions
PangoFontDescription* pango_font_description_new() override { return ::pango_font_description_new(); }
void pango_font_description_set_size(PangoFontDescription* desc, gint size) override {
::pango_font_description_set_size(desc, size);
}
void pango_font_description_set_family(PangoFontDescription* desc, const char* family) override {
::pango_font_description_set_family(desc, family);
}
// GObject reference counting
void g_object_unref(gpointer object) override { ::g_object_unref(object); }
};
} // namespace ptprnt::graphics

View File

@@ -1,6 +1,6 @@
/* /*
ptrnt - print labels on linux ptrnt - print labels on linux
Copyright (C) 2025 Moritz Martinius Copyright (C) 2023-2025 Moritz Martinius
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU General Public License as published by
@@ -25,64 +25,37 @@
#include <cassert> #include <cassert>
#include <cstddef> #include <cstddef>
#include <cstdint> #include <cstdint>
#include <cstring>
#include <memory>
#include <string> #include <string>
#include <vector> #include <vector>
#include "cairo.h" #include "cairo.h"
#include "graphics/CairoWrapper.hpp"
#include "graphics/interface/ICairoWrapper.hpp"
#include "graphics/interface/ILabel.hpp" #include "graphics/interface/ILabel.hpp"
#include "pango/pango-font.h" #include "pango/pango-font.h"
#include "pango/pango-layout.h" #include "pango/pango-layout.h"
#include "pango/pango-types.h" #include "pango/pango-types.h"
#include "pango/pangocairo.h"
namespace ptprnt::graphics { namespace ptprnt::graphics {
Label::Label(const uint16_t heightPixel)
// Deleter implementations : mPrinterHeight(heightPixel) {
void CairoSurfaceDeleter::operator()(cairo_surface_t* surface) const {
if (surface && wrapper)
wrapper->cairo_surface_destroy(surface);
}
void CairoDeleter::operator()(cairo_t* cr) const {
if (cr && wrapper)
wrapper->cairo_destroy(cr);
}
void GObjectDeleter::operator()(gpointer obj) const {
if (obj && wrapper)
wrapper->g_object_unref(obj);
}
// Default constructor - creates real Cairo/Pango wrapper
Label::Label(const uint16_t heightPixel) : Label(heightPixel, std::make_shared<CairoWrapper>()) {}
// Constructor with dependency injection
Label::Label(const uint16_t heightPixel, std::shared_ptr<ICairoWrapper> cairoWrapper)
: mCairoWrapper(std::move(cairoWrapper)), mPrinterHeight(heightPixel) {
// Initialize resources in correct order with RAII // Initialize resources in correct order with RAII
// Pass wrapper to deleter so cleanup uses the wrapper mFontMap.reset(pango_cairo_font_map_new());
GObjectDeleter deleter;
deleter.wrapper = mCairoWrapper;
mFontMap = std::unique_ptr<PangoFontMap, GObjectDeleter>(mCairoWrapper->pango_cairo_font_map_new(), deleter);
} }
std::vector<uint8_t> Label::getRaw() const { std::vector<uint8_t> Label::getRaw() {
assert(mSurface != nullptr); assert(mSurface != nullptr);
auto* surface = mSurface.get(); auto* surface = mSurface.get();
mCairoWrapper->cairo_surface_flush(surface); cairo_surface_flush(surface);
assert(mCairoWrapper->cairo_image_surface_get_format(surface) == CAIRO_FORMAT_A8); assert(cairo_image_surface_get_format(surface) == CAIRO_FORMAT_A8);
int width = mCairoWrapper->cairo_image_surface_get_width(surface); int width = cairo_image_surface_get_width(surface);
int height = mCairoWrapper->cairo_image_surface_get_height(surface); int height = cairo_image_surface_get_height(surface);
int stride = mCairoWrapper->cairo_image_surface_get_stride(surface); int stride = cairo_image_surface_get_stride(surface);
spdlog::debug("Cairo Surface data: W: {}; H: {}; S:{}", width, height, stride); spdlog::debug("Cairo Surface data: W: {}; H: {}; S:{}", width, height, stride);
auto data = mCairoWrapper->cairo_image_surface_get_data(surface); auto data = cairo_image_surface_get_data(surface);
// If stride equals width, we can return data directly // If stride equals width, we can return data directly
if (stride == width) { if (stride == width) {
@@ -107,41 +80,41 @@ uint8_t Label::getNumLines(std::string_view strv) {
return std::count(strv.begin(), strv.end(), '\n'); return std::count(strv.begin(), strv.end(), '\n');
} }
int Label::getWidth() const { int Label::getWidth() {
// Return the actual Cairo surface width (which is the layout width) // Return the actual Cairo surface width (which is the layout width)
return mLayoutWidth; return mLayoutWidth;
} }
int Label::getHeight() const { int Label::getHeight() {
// Return the actual Cairo surface height (which is the printer height) // Return the actual Cairo surface height (which is the printer height)
return mPrinterHeight; return mPrinterHeight;
} }
void Label::configureLayout(PangoLayout* layout, const std::string& text, PangoFontDescription* fontDesc) { void Label::configureLayout(PangoLayout* layout, const std::string& text, PangoFontDescription* fontDesc) {
mCairoWrapper->pango_layout_set_font_description(layout, fontDesc); pango_layout_set_font_description(layout, fontDesc);
mCairoWrapper->pango_layout_set_text(layout, text.c_str(), static_cast<int>(text.length())); pango_layout_set_text(layout, text.c_str(), static_cast<int>(text.length()));
mCairoWrapper->pango_layout_set_height(layout, getNumLines(text) * -1); pango_layout_set_height(layout, getNumLines(text) * -1);
} }
void Label::applyHorizontalAlignment(PangoLayout* layout) { void Label::applyHorizontalAlignment(PangoLayout* layout) {
switch (mHAlign) { switch (mHAlign) {
case HAlignPosition::LEFT: case HAlignPosition::LEFT:
mCairoWrapper->pango_layout_set_alignment(layout, PANGO_ALIGN_LEFT); pango_layout_set_alignment(layout, PANGO_ALIGN_LEFT);
break; break;
case HAlignPosition::RIGHT: case HAlignPosition::RIGHT:
mCairoWrapper->pango_layout_set_alignment(layout, PANGO_ALIGN_RIGHT); pango_layout_set_alignment(layout, PANGO_ALIGN_RIGHT);
break; break;
case HAlignPosition::JUSTIFY: case HAlignPosition::JUSTIFY:
mCairoWrapper->pango_layout_set_alignment(layout, PANGO_ALIGN_LEFT); pango_layout_set_alignment(layout, PANGO_ALIGN_LEFT);
mCairoWrapper->pango_layout_set_justify(layout, true); pango_layout_set_justify(layout, true);
#if PANGO_VERSION_MAJOR >= 1 && PANGO_VERSION_MINOR >= 50 #if PANGO_VERSION_MAJOR >= 1 && PANGO_VERSION_MINOR >= 50
mCairoWrapper->pango_layout_set_justify_last_line(layout, true); pango_layout_set_justify_last_line(layout, true);
#endif #endif
break; break;
case HAlignPosition::CENTER: case HAlignPosition::CENTER:
[[fallthrough]]; [[fallthrough]];
default: default:
mCairoWrapper->pango_layout_set_alignment(layout, PANGO_ALIGN_CENTER); pango_layout_set_alignment(layout, PANGO_ALIGN_CENTER);
break; break;
} }
} }
@@ -160,49 +133,40 @@ bool Label::create(const std::string& labelText) {
// see: https://gist.github.com/CallumDev/7c66b3f9cf7a876ef75f // see: https://gist.github.com/CallumDev/7c66b3f9cf7a876ef75f
// Create a temporary surface for layout size calculations // Create a temporary surface for layout size calculations
auto* tempSurface = mCairoWrapper->cairo_image_surface_create(CAIRO_FORMAT_A8, 1, 1); auto* tempSurface = cairo_image_surface_create(CAIRO_FORMAT_A8, 1, 1);
auto* tempCr = mCairoWrapper->cairo_create(tempSurface); auto* tempCr = cairo_create(tempSurface);
auto* tempPangoCtx = mCairoWrapper->pango_cairo_create_context(tempCr); auto* tempPangoCtx = pango_cairo_create_context(tempCr);
auto* tempPangoLyt = mCairoWrapper->pango_layout_new(tempPangoCtx); auto* tempPangoLyt = pango_layout_new(tempPangoCtx);
PangoFontDescription* regularFont = mCairoWrapper->pango_font_description_new(); PangoFontDescription* regularFont = pango_font_description_new();
mCairoWrapper->pango_font_description_set_size(regularFont, static_cast<int>(mFontSize * PANGO_SCALE)); pango_font_description_set_size(regularFont, static_cast<int>(mFontSize * PANGO_SCALE));
mCairoWrapper->pango_font_description_set_family(regularFont, mFontFamily.c_str()); pango_font_description_set_family(regularFont, mFontFamily.c_str());
// Configure temporary layout for size calculation // Configure temporary layout for size calculation
configureLayout(tempPangoLyt, labelText, regularFont); configureLayout(tempPangoLyt, labelText, regularFont);
applyHorizontalAlignment(tempPangoLyt); applyHorizontalAlignment(tempPangoLyt);
// Calculate label size from temporary layout // Calculate label size from temporary layout
mCairoWrapper->pango_layout_get_size(tempPangoLyt, &mLayoutWidth, &mLayoutHeight); pango_layout_get_size(tempPangoLyt, &mLayoutWidth, &mLayoutHeight);
mLayoutWidth /= PANGO_SCALE; mLayoutWidth /= PANGO_SCALE;
mLayoutHeight /= PANGO_SCALE; mLayoutHeight /= PANGO_SCALE;
spdlog::debug("Layout width: {}, height: {}", mLayoutWidth, mLayoutHeight); spdlog::debug("Layout width: {}, height: {}", mLayoutWidth, mLayoutHeight);
//auto alignedWidth = mLayoutWidth + (8 - (mLayoutWidth % 8));
//spdlog::debug("Aligned Layout width: {}, height: {}", alignedWidth, mLayoutHeight);
// Clean up temporary resources // Clean up temporary resources
mCairoWrapper->g_object_unref(tempPangoLyt); g_object_unref(tempPangoLyt);
mCairoWrapper->g_object_unref(tempPangoCtx); g_object_unref(tempPangoCtx);
mCairoWrapper->cairo_destroy(tempCr); cairo_destroy(tempCr);
mCairoWrapper->cairo_surface_destroy(tempSurface); cairo_surface_destroy(tempSurface);
// Now create the final surface and Pango context for actual rendering // Now create the final surface and Pango context for actual rendering
// Create deleters with wrapper reference mSurface.reset(cairo_image_surface_create(CAIRO_FORMAT_A8, mLayoutWidth, mPrinterHeight));
CairoSurfaceDeleter surfaceDeleter; cairo_t* cr = cairo_create(mSurface.get());
surfaceDeleter.wrapper = mCairoWrapper; mCairoCtx.reset(cr);
CairoDeleter cairoDeleter; mPangoCtx.reset(pango_cairo_create_context(cr));
cairoDeleter.wrapper = mCairoWrapper; mPangoLyt.reset(pango_layout_new(mPangoCtx.get()));
GObjectDeleter gobjectDeleter;
gobjectDeleter.wrapper = mCairoWrapper;
mSurface = std::unique_ptr<cairo_surface_t, CairoSurfaceDeleter>(
mCairoWrapper->cairo_image_surface_create(CAIRO_FORMAT_A8, mLayoutWidth, mPrinterHeight), surfaceDeleter);
cairo_t* cr = mCairoWrapper->cairo_create(mSurface.get());
mCairoCtx = std::unique_ptr<cairo_t, CairoDeleter>(cr, cairoDeleter);
mPangoCtx =
std::unique_ptr<PangoContext, GObjectDeleter>(mCairoWrapper->pango_cairo_create_context(cr), gobjectDeleter);
mPangoLyt =
std::unique_ptr<PangoLayout, GObjectDeleter>(mCairoWrapper->pango_layout_new(mPangoCtx.get()), gobjectDeleter);
// Configure final layout with same settings // Configure final layout with same settings
configureLayout(mPangoLyt.get(), labelText, regularFont); configureLayout(mPangoLyt.get(), labelText, regularFont);
@@ -213,104 +177,31 @@ bool Label::create(const std::string& labelText) {
case VAlignPosition::TOP: case VAlignPosition::TOP:
break; break;
case VAlignPosition::BOTTOM: case VAlignPosition::BOTTOM:
mCairoWrapper->cairo_move_to(mCairoCtx.get(), 0.0, mPrinterHeight - mLayoutHeight); cairo_move_to(mCairoCtx.get(), 0.0, mPrinterHeight - mLayoutHeight);
break; break;
case VAlignPosition::MIDDLE: case VAlignPosition::MIDDLE:
mCairoWrapper->cairo_move_to(mCairoCtx.get(), 0.0, (mPrinterHeight - mLayoutHeight) / 2); cairo_move_to(mCairoCtx.get(), 0.0, (mPrinterHeight - mLayoutHeight) / 2);
break; break;
default: default:
break; break;
} }
// Finally show the layout on the Cairo surface // Finally show the layout on the Cairo surface
mCairoWrapper->pango_cairo_show_layout(mCairoCtx.get(), mPangoLyt.get()); pango_cairo_show_layout(mCairoCtx.get(), mPangoLyt.get());
mCairoWrapper->cairo_set_source_rgb(mCairoCtx.get(), 0.0, 0.0, 0.0); cairo_set_source_rgb(mCairoCtx.get(), 0.0, 0.0, 0.0);
mCairoWrapper->cairo_surface_flush(mSurface.get()); cairo_surface_flush(mSurface.get());
// mCairoCtx smart pointer will handle cleanup // mCairoCtx smart pointer will handle cleanup
return true; return true;
} }
void Label::writeToPng(const std::string& file) { void Label::writeToPng(const std::string& file) {
if (mSurface) { if (mSurface) {
mCairoWrapper->cairo_surface_flush(mSurface.get()); cairo_surface_flush(mSurface.get());
mCairoWrapper->cairo_surface_write_to_png(mSurface.get(), file.c_str()); cairo_surface_write_to_png(mSurface.get(), file.c_str());
} }
} }
bool Label::append(const ILabel& other, uint32_t spacingPx) {
// Check that heights match
if (getHeight() != other.getHeight()) {
spdlog::error("Cannot append labels with different heights: {} vs {}", getHeight(), other.getHeight());
return false;
}
int currentWidth = getWidth();
int otherWidth = other.getWidth();
int height = getHeight();
int spacing = static_cast<int>(spacingPx);
int newWidth = currentWidth + spacing + otherWidth;
spdlog::debug("Appending label: current={}x{}, other={}x{}, spacing={}, new={}x{}", currentWidth, height,
otherWidth, height, spacing, newWidth, height);
// Get current and other label data
auto currentData = getRaw();
auto otherData = other.getRaw();
// Create new surface with extended width
CairoSurfaceDeleter surfaceDeleter;
surfaceDeleter.wrapper = mCairoWrapper;
auto newSurface = std::unique_ptr<cairo_surface_t, CairoSurfaceDeleter>(
mCairoWrapper->cairo_image_surface_create(CAIRO_FORMAT_A8, newWidth, height), surfaceDeleter);
if (mCairoWrapper->cairo_surface_status(newSurface.get()) != CAIRO_STATUS_SUCCESS) {
spdlog::error("Failed to create new surface for appended label");
return false;
}
// Get data pointer and stride
mCairoWrapper->cairo_surface_flush(newSurface.get());
unsigned char* newData = mCairoWrapper->cairo_image_surface_get_data(newSurface.get());
int newStride = mCairoWrapper->cairo_image_surface_get_stride(newSurface.get());
// Clear the new surface (set to transparent/white)
memset(newData, 0x00, newStride * height);
// Copy current label data
for (int y = 0; y < height; ++y) {
for (int x = 0; x < currentWidth; ++x) {
size_t srcIdx = y * currentWidth + x;
size_t dstIdx = y * newStride + x;
if (srcIdx < currentData.size()) {
newData[dstIdx] = currentData[srcIdx];
}
}
}
// Copy other label data (with spacing offset)
int xOffset = currentWidth + spacing;
for (int y = 0; y < height; ++y) {
for (int x = 0; x < otherWidth; ++x) {
size_t srcIdx = y * otherWidth + x;
size_t dstIdx = y * newStride + (xOffset + x);
if (srcIdx < otherData.size()) {
newData[dstIdx] = otherData[srcIdx];
}
}
}
mCairoWrapper->cairo_surface_mark_dirty(newSurface.get());
// Replace current surface with new one
mSurface = std::move(newSurface);
// Update layout dimensions
mLayoutWidth = newWidth;
return true;
}
void Label::setFontSize(const double fontSize) { void Label::setFontSize(const double fontSize) {
mFontSize = fontSize; mFontSize = fontSize;
} }

View File

@@ -1,6 +1,6 @@
/* /*
ptrnt - print labels on linux ptrnt - print labels on linux
Copyright (C) 2025 Moritz Martinius Copyright (C) 2023-2025 Moritz Martinius
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU General Public License as published by
@@ -32,34 +32,31 @@
namespace ptprnt::graphics { namespace ptprnt::graphics {
// Forward declaration // Custom deleters for Cairo/Pango resources
class ICairoWrapper;
// Custom deleters for Cairo/Pango resources that use the wrapper
// Implementation in Label.cpp to avoid incomplete type issues
struct CairoSurfaceDeleter { struct CairoSurfaceDeleter {
std::shared_ptr<ICairoWrapper> wrapper; void operator()(cairo_surface_t* surface) const {
void operator()(cairo_surface_t* surface) const; if (surface)
cairo_surface_destroy(surface);
}
}; };
struct CairoDeleter { struct CairoDeleter {
std::shared_ptr<ICairoWrapper> wrapper; void operator()(cairo_t* cr) const {
void operator()(cairo_t* cr) const; if (cr)
cairo_destroy(cr);
}
}; };
struct GObjectDeleter { struct GObjectDeleter {
std::shared_ptr<ICairoWrapper> wrapper; void operator()(gpointer obj) const {
void operator()(gpointer obj) const; if (obj)
g_object_unref(obj);
}
}; };
class Label : public ILabel { class Label : public ILabel {
public: public:
// Default constructor using real Cairo/Pango implementation Label(const uint16_t heightPixel);
explicit Label(uint16_t heightPixel);
// Constructor for dependency injection (testing)
Label(uint16_t heightPixel, std::shared_ptr<ICairoWrapper> cairoWrapper);
~Label() override; ~Label() override;
Label(const Label&) = delete; Label(const Label&) = delete;
@@ -70,9 +67,9 @@ class Label : public ILabel {
bool create(PrintableText printableText) override; bool create(PrintableText printableText) override;
bool create(const std::string& labelText) override; bool create(const std::string& labelText) override;
void writeToPng(const std::string& file); void writeToPng(const std::string& file);
[[nodiscard]] int getWidth() const override; [[nodiscard]] int getWidth() override;
[[nodiscard]] int getHeight() const override; [[nodiscard]] int getHeight() override;
[[nodiscard]] std::vector<uint8_t> getRaw() const override; [[nodiscard]] std::vector<uint8_t> getRaw() override;
void setFontSize(const double fontSize) override; void setFontSize(const double fontSize) override;
void setFontFamily(const std::string& fontFamily) override; void setFontFamily(const std::string& fontFamily) override;
@@ -80,8 +77,6 @@ class Label : public ILabel {
void setHAlign(HAlignPosition hpos) override; void setHAlign(HAlignPosition hpos) override;
void setVAlign(VAlignPosition vpos) override; void setVAlign(VAlignPosition vpos) override;
bool append(const ILabel& other, uint32_t spacingPx = 60) override;
private: private:
// methods // methods
[[nodiscard]] uint8_t getNumLines(std::string_view str); [[nodiscard]] uint8_t getNumLines(std::string_view str);
@@ -89,9 +84,6 @@ class Label : public ILabel {
void configureLayout(PangoLayout* layout, const std::string& text, PangoFontDescription* fontDesc); void configureLayout(PangoLayout* layout, const std::string& text, PangoFontDescription* fontDesc);
void applyHorizontalAlignment(PangoLayout* layout); void applyHorizontalAlignment(PangoLayout* layout);
// Cairo/Pango wrapper for dependency injection
std::shared_ptr<ICairoWrapper> mCairoWrapper;
std::unique_ptr<cairo_surface_t, CairoSurfaceDeleter> mSurface{nullptr}; std::unique_ptr<cairo_surface_t, CairoSurfaceDeleter> mSurface{nullptr};
std::unique_ptr<cairo_t, CairoDeleter> mCairoCtx{nullptr}; std::unique_ptr<cairo_t, CairoDeleter> mCairoCtx{nullptr};
std::unique_ptr<PangoContext, GObjectDeleter> mPangoCtx{nullptr}; std::unique_ptr<PangoContext, GObjectDeleter> mPangoCtx{nullptr};

View File

@@ -1,83 +0,0 @@
/*
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 <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <cairo.h>
#include <pango/pango.h>
#include <pango/pangocairo.h>
namespace ptprnt::graphics {
/**
* @brief Interface wrapper for Cairo and Pango C API functions
*
* This interface allows for dependency injection and mocking of Cairo/Pango
* functionality in unit tests, making the Label class fully testable.
*/
class ICairoWrapper {
public:
virtual ~ICairoWrapper() = default;
// Cairo image surface functions
virtual cairo_surface_t* cairo_image_surface_create(cairo_format_t format, int width, int height) = 0;
virtual void cairo_surface_destroy(cairo_surface_t* surface) = 0;
virtual void cairo_surface_flush(cairo_surface_t* surface) = 0;
virtual void cairo_surface_mark_dirty(cairo_surface_t* surface) = 0;
virtual cairo_status_t cairo_surface_status(cairo_surface_t* surface) = 0;
virtual cairo_format_t cairo_image_surface_get_format(cairo_surface_t* surface) = 0;
virtual int cairo_image_surface_get_width(cairo_surface_t* surface) = 0;
virtual int cairo_image_surface_get_height(cairo_surface_t* surface) = 0;
virtual int cairo_image_surface_get_stride(cairo_surface_t* surface) = 0;
virtual unsigned char* cairo_image_surface_get_data(cairo_surface_t* surface) = 0;
virtual cairo_status_t cairo_surface_write_to_png(cairo_surface_t* surface, const char* filename) = 0;
// Cairo context functions
virtual cairo_t* cairo_create(cairo_surface_t* surface) = 0;
virtual void cairo_destroy(cairo_t* cr) = 0;
virtual void cairo_move_to(cairo_t* cr, double x, double y) = 0;
virtual void cairo_set_source_rgb(cairo_t* cr, double red, double green, double blue) = 0;
// Pango-Cairo functions
virtual PangoFontMap* pango_cairo_font_map_new() = 0;
virtual PangoContext* pango_cairo_create_context(cairo_t* cr) = 0;
virtual void pango_cairo_show_layout(cairo_t* cr, PangoLayout* layout) = 0;
// Pango layout functions
virtual PangoLayout* pango_layout_new(PangoContext* context) = 0;
virtual void pango_layout_set_font_description(PangoLayout* layout, const PangoFontDescription* desc) = 0;
virtual void pango_layout_set_text(PangoLayout* layout, const char* text, int length) = 0;
virtual void pango_layout_set_height(PangoLayout* layout, int height) = 0;
virtual void pango_layout_set_alignment(PangoLayout* layout, PangoAlignment alignment) = 0;
virtual void pango_layout_set_justify(PangoLayout* layout, gboolean justify) = 0;
#if PANGO_VERSION_MAJOR >= 1 && PANGO_VERSION_MINOR >= 50
virtual void pango_layout_set_justify_last_line(PangoLayout* layout, gboolean justify) = 0;
#endif
virtual void pango_layout_get_size(PangoLayout* layout, int* width, int* height) = 0;
// Pango font description functions
virtual PangoFontDescription* pango_font_description_new() = 0;
virtual void pango_font_description_set_size(PangoFontDescription* desc, gint size) = 0;
virtual void pango_font_description_set_family(PangoFontDescription* desc, const char* family) = 0;
// GObject reference counting
virtual void g_object_unref(gpointer object) = 0;
};
} // namespace ptprnt::graphics

View File

@@ -1,6 +1,6 @@
/* /*
ptrnt - print labels on linux ptrnt - print labels on linux
Copyright (C) 2025 Moritz Martinius Copyright (C) 2024-2025 Moritz Martinius
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU General Public License as published by
@@ -65,24 +65,16 @@ class ILabel {
public: public:
virtual ~ILabel() = default; virtual ~ILabel() = default;
virtual bool create(PrintableText printableText) = 0; virtual bool create(PrintableText printableText) = 0;
virtual bool create(const std::string& labelText) = 0; virtual bool create(const std::string& labelText) = 0;
virtual std::vector<uint8_t> getRaw() const = 0; virtual std::vector<uint8_t> getRaw() = 0;
virtual int getWidth() const = 0; virtual int getWidth() = 0;
virtual int getHeight() const = 0; virtual int getHeight() = 0;
virtual void setText(const std::string& text) = 0; virtual void setText(const std::string& text) = 0;
virtual void setFontSize(const double fontSize) = 0; virtual void setFontSize(const double fontSize) = 0;
virtual void setFontFamily(const std::string& fontFamily) = 0; virtual void setFontFamily(const std::string& fontFamily) = 0;
virtual void setHAlign(HAlignPosition hpos) = 0; virtual void setHAlign(HAlignPosition hpos) = 0;
virtual void setVAlign(VAlignPosition vpos) = 0; virtual void setVAlign(VAlignPosition vpos) = 0;
/**
* @brief Append another label horizontally with spacing
* @param other The label to append
* @param spacingPx Spacing between labels in pixels (default: 60px ~5mm at 300dpi)
* @return true on success, false if heights don't match
*/
virtual bool append(const ILabel& other, uint32_t spacingPx = 60) = 0;
}; };
} // namespace ptprnt::graphics } // namespace ptprnt::graphics

View File

@@ -1,6 +1,6 @@
/* /*
ptrnt - print labels on linux ptrnt - print labels on linux
Copyright (C) 2025 Moritz Martinius Copyright (C) 2023-2025 Moritz Martinius
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU General Public License as published by
@@ -22,22 +22,22 @@
#include <memory> #include <memory>
#include <string_view> #include <string_view>
#include "IPrinterTypes.hpp"
#include "graphics/Bitmap.hpp" #include "graphics/Bitmap.hpp"
#include "graphics/Monochrome.hpp" #include "graphics/Monochrome.hpp"
#include "graphics/interface/ILabel.hpp" #include "graphics/interface/ILabel.hpp"
#include "interface/IPrinterTypes.hpp"
#include "libusbwrap/interface/IUsbDevice.hpp" #include "libusbwrap/interface/IUsbDevice.hpp"
namespace ptprnt { namespace ptprnt {
class IPrinterDriver { class IPrinterDriver {
public: public:
virtual ~IPrinterDriver() = default; virtual ~IPrinterDriver() = default;
[[nodiscard]] virtual std::string_view getDriverName() = 0; [[nodiscard]] virtual const std::string_view getDriverName() = 0;
[[nodiscard]] virtual std::string_view getName() = 0; [[nodiscard]] virtual const std::string_view getName() = 0;
[[nodiscard]] virtual std::string_view getVersion() = 0; [[nodiscard]] virtual const std::string_view getVersion() = 0;
[[nodiscard]] virtual libusbwrap::usbId getUsbId() = 0; [[nodiscard]] virtual const libusbwrap::usbId getUsbId() = 0;
[[nodiscard]] virtual PrinterInfo getPrinterInfo() = 0; [[nodiscard]] virtual const PrinterInfo getPrinterInfo() = 0;
[[nodiscard]] virtual PrinterStatus getPrinterStatus() = 0; [[nodiscard]] virtual const PrinterStatus getPrinterStatus() = 0;
virtual bool attachUsbDevice(std::shared_ptr<libusbwrap::IUsbDevice> usbHndl) = 0; virtual bool attachUsbDevice(std::shared_ptr<libusbwrap::IUsbDevice> usbHndl) = 0;
virtual bool detachUsbDevice() = 0; virtual bool detachUsbDevice() = 0;
virtual bool printBitmap(const graphics::Bitmap<graphics::ALPHA8>& bitmap) = 0; virtual bool printBitmap(const graphics::Bitmap<graphics::ALPHA8>& bitmap) = 0;

View File

@@ -1,6 +1,6 @@
/* /*
ptrnt - print labels on linux ptrnt - print labels on linux
Copyright (C) 2025 Moritz Martinius Copyright (C) 2023-2024 Moritz Martinius
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU General Public License as published by
@@ -20,6 +20,7 @@
#pragma once #pragma once
#include <cstdint> #include <cstdint>
#include <string>
#include <string_view> #include <string_view>
#include <vector> #include <vector>

View File

@@ -1,6 +1,6 @@
/* /*
ptrnt - print labels on linux ptrnt - print labels on linux
Copyright (C) 2023-2025 Moritz Martinius Copyright (C) 2023-2024 Moritz Martinius
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU General Public License as published by

View File

@@ -1,108 +0,0 @@
/*
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 <https://www.gnu.org/licenses/>.
*/
#include "LibUsbWrapper.hpp"
#include "libusb.h"
namespace libusbwrap {
// LibUsbDeviceDeleter
void LibUsbDeviceDeleter::operator()(libusb_device* dev) const {
if (dev && wrapper) {
wrapper->unrefDevice(dev);
}
}
// LibUsbWrapper
int LibUsbWrapper::init(libusb_context** ctx) {
return libusb_init(ctx);
}
void LibUsbWrapper::exit(libusb_context* ctx) {
libusb_exit(ctx);
}
ssize_t LibUsbWrapper::getDeviceList(libusb_context* ctx, libusb_device*** list) {
return libusb_get_device_list(ctx, list);
}
void LibUsbWrapper::freeDeviceList(libusb_device** list, int unrefDevices) {
libusb_free_device_list(list, unrefDevices);
}
void LibUsbWrapper::refDevice(libusb_device* dev) {
libusb_ref_device(dev);
}
void LibUsbWrapper::unrefDevice(libusb_device* dev) {
libusb_unref_device(dev);
}
int LibUsbWrapper::getDeviceDescriptor(libusb_device* dev, libusb_device_descriptor* desc) {
return libusb_get_device_descriptor(dev, desc);
}
int LibUsbWrapper::open(libusb_device* dev, libusb_device_handle** handle) {
return libusb_open(dev, handle);
}
void LibUsbWrapper::close(libusb_device_handle* handle) {
libusb_close(handle);
}
int LibUsbWrapper::getSpeed(libusb_device* dev) {
return libusb_get_device_speed(dev);
}
uint8_t LibUsbWrapper::getBusNumber(libusb_device* dev) {
return libusb_get_bus_number(dev);
}
uint8_t LibUsbWrapper::getPortNumber(libusb_device* dev) {
return libusb_get_port_number(dev);
}
int LibUsbWrapper::kernelDriverActive(libusb_device_handle* handle, int interfaceNo) {
return libusb_kernel_driver_active(handle, interfaceNo);
}
int LibUsbWrapper::detachKernelDriver(libusb_device_handle* handle, int interfaceNo) {
return libusb_detach_kernel_driver(handle, interfaceNo);
}
int LibUsbWrapper::claimInterface(libusb_device_handle* handle, int interfaceNo) {
return libusb_claim_interface(handle, interfaceNo);
}
int LibUsbWrapper::releaseInterface(libusb_device_handle* handle, int interfaceNo) {
return libusb_release_interface(handle, interfaceNo);
}
int LibUsbWrapper::bulkTransfer(libusb_device_handle* handle, uint8_t endpoint, unsigned char* data, int length,
int* transferred, unsigned int timeout) {
return libusb_bulk_transfer(handle, endpoint, data, length, transferred, timeout);
}
const char* LibUsbWrapper::errorName(int errorCode) {
return libusb_error_name(errorCode);
}
} // namespace libusbwrap

View File

@@ -1,125 +0,0 @@
/*
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 <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <cstdint>
#include <memory>
// Forward declarations to avoid pulling in libusb.h in header
struct libusb_context;
struct libusb_device;
struct libusb_device_handle;
struct libusb_device_descriptor;
namespace libusbwrap {
// Forward declaration
class ILibUsbWrapper;
// Custom deleter for libusb_device that uses the wrapper
struct LibUsbDeviceDeleter {
std::shared_ptr<ILibUsbWrapper> wrapper;
void operator()(libusb_device* dev) const;
};
using LibUsbDevicePtr = std::unique_ptr<libusb_device, LibUsbDeviceDeleter>;
/**
* @brief Thin wrapper around libusb C API
*
* This class provides a 1:1 mapping to libusb C functions.
*/
class ILibUsbWrapper {
public:
virtual ~ILibUsbWrapper() = default;
// Context management (raw pointers - caller manages lifecycle)
virtual int init(libusb_context** ctx) = 0;
virtual void exit(libusb_context* ctx) = 0;
// Device enumeration (raw pointers - caller manages lifecycle)
virtual ssize_t getDeviceList(libusb_context* ctx, libusb_device*** list) = 0;
virtual void freeDeviceList(libusb_device** list, int unrefDevices) = 0;
virtual void refDevice(libusb_device* dev) = 0;
virtual void unrefDevice(libusb_device* dev) = 0;
// Device descriptor
virtual int getDeviceDescriptor(libusb_device* dev, libusb_device_descriptor* desc) = 0;
// Device opening/closing (raw pointers - caller manages lifecycle)
virtual int open(libusb_device* dev, libusb_device_handle** handle) = 0;
virtual void close(libusb_device_handle* handle) = 0;
// Device information
virtual int getSpeed(libusb_device* dev) = 0;
virtual uint8_t getBusNumber(libusb_device* dev) = 0;
virtual uint8_t getPortNumber(libusb_device* dev) = 0;
// Kernel driver management
virtual int kernelDriverActive(libusb_device_handle* handle, int interfaceNo) = 0;
virtual int detachKernelDriver(libusb_device_handle* handle, int interfaceNo) = 0;
// Interface management
virtual int claimInterface(libusb_device_handle* handle, int interfaceNo) = 0;
virtual int releaseInterface(libusb_device_handle* handle, int interfaceNo) = 0;
// Data transfer
virtual int bulkTransfer(libusb_device_handle* handle, uint8_t endpoint, unsigned char* data, int length,
int* transferred, unsigned int timeout) = 0;
// Error handling
virtual const char* errorName(int errorCode) = 0;
};
/**
* @brief Concrete implementation - thin wrapper forwarding to libusb C API
*/
class LibUsbWrapper : public ILibUsbWrapper {
public:
int init(libusb_context** ctx) override;
void exit(libusb_context* ctx) override;
ssize_t getDeviceList(libusb_context* ctx, libusb_device*** list) override;
void freeDeviceList(libusb_device** list, int unrefDevices) override;
void refDevice(libusb_device* dev) override;
void unrefDevice(libusb_device* dev) override;
int getDeviceDescriptor(libusb_device* dev, libusb_device_descriptor* desc) override;
int open(libusb_device* dev, libusb_device_handle** handle) override;
void close(libusb_device_handle* handle) override;
int getSpeed(libusb_device* dev) override;
uint8_t getBusNumber(libusb_device* dev) override;
uint8_t getPortNumber(libusb_device* dev) override;
int kernelDriverActive(libusb_device_handle* handle, int interfaceNo) override;
int detachKernelDriver(libusb_device_handle* handle, int interfaceNo) override;
int claimInterface(libusb_device_handle* handle, int interfaceNo) override;
int releaseInterface(libusb_device_handle* handle, int interfaceNo) override;
int bulkTransfer(libusb_device_handle* handle, uint8_t endpoint, unsigned char* data, int length, int* transferred,
unsigned int timeout) override;
const char* errorName(int errorCode) override;
};
} // namespace libusbwrap

View File

@@ -1,6 +1,6 @@
/* /*
ptrnt - print labels on linux ptrnt - print labels on linux
Copyright (C) 2023-2025 Moritz Martinius Copyright (C) 2023-2024 Moritz Martinius
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU General Public License as published by
@@ -24,55 +24,44 @@
#include "libusb.h" #include "libusb.h"
#include "libusbwrap/LibUsbTypes.hpp" #include "libusbwrap/LibUsbTypes.hpp"
#include "libusbwrap/LibUsbWrapper.hpp"
#include "libusbwrap/interface/IUsbDevice.hpp" #include "libusbwrap/interface/IUsbDevice.hpp"
namespace libusbwrap { namespace libusbwrap {
UsbDevice::UsbDevice(libusb_context* ctx, usbDevice_ptr dev) : mLibusbCtx(ctx), mLibusbDev(std::move(dev)) {
// Default constructor delegates to DI constructor if (mLibusbCtx == nullptr || mLibusbDev == nullptr) {
UsbDevice::UsbDevice(libusb_device* device, const libusb_device_descriptor& desc) throw std::invalid_argument("ctx or device are nullptr");
: UsbDevice(device, desc, std::make_shared<LibUsbWrapper>()) {}
// Constructor with dependency injection
UsbDevice::UsbDevice(libusb_device* device, const libusb_device_descriptor& desc,
std::shared_ptr<ILibUsbWrapper> libusbWrapper)
: mLibUsb(libusbWrapper), mDevice(device, LibUsbDeviceDeleter{libusbWrapper}), mDeviceDescriptor(desc) {
if (!mDevice) {
throw std::invalid_argument("device is nullptr");
} }
libusb_get_device_descriptor(mLibusbDev.get(), &mLibusbDevDesc);
} }
UsbDevice::~UsbDevice() { UsbDevice::~UsbDevice() {
if (mIsOpen && mDeviceHandle) { // only free the device, not the context, which is shared between every device
mLibUsb->close(mDeviceHandle); // the UsbDeviceFactory will take care of that
if (mIsOpen) {
close();
} }
// mDevice auto-deleted by unique_ptr (calls LibUsbDeviceDeleter)
} }
bool UsbDevice::open() { bool UsbDevice::open() {
int openStatus = mLibUsb->open(mDevice.get(), &mDeviceHandle); int openStatus = libusb_open(mLibusbDev.get(), &mLibusbDevHandle);
if (openStatus != 0) { if (openStatus != 0) {
mLastError = static_cast<Error>(openStatus); mLastError = static_cast<Error>(openStatus);
return false; return false;
} }
mIsOpen = true;
return true; return true;
} }
void UsbDevice::close() { void UsbDevice::close() {
if (mDeviceHandle) { libusb_close(mLibusbDevHandle);
mLibUsb->close(mDeviceHandle);
mDeviceHandle = nullptr;
mIsOpen = false;
}
} }
bool UsbDevice::detachKernelDriver(int interfaceNo) { bool UsbDevice::detachKernelDriver(int interfaceNo) {
// TODO: cover the other status codes that can be returned // TODO: cover the other status codes that can be returned
int kernelDriverStatus = mLibUsb->kernelDriverActive(mDeviceHandle, interfaceNo); int kernelDriverStatus = libusb_kernel_driver_active(mLibusbDevHandle, interfaceNo);
if (kernelDriverStatus == 1) { // kernel driver is active, we have to detach to continue... if (kernelDriverStatus == 1) { // kernel driver is active, we have to detach to continue...
int detachStatus = mLibUsb->detachKernelDriver(mDeviceHandle, interfaceNo); int detachStatus = libusb_detach_kernel_driver(mLibusbDevHandle, interfaceNo);
if (detachStatus != 0) { if (detachStatus != 0) {
mLastError = static_cast<Error>(detachStatus); mLastError = static_cast<Error>(detachStatus);
return false; return false;
@@ -84,7 +73,7 @@ bool UsbDevice::detachKernelDriver(int interfaceNo) {
bool UsbDevice::claimInterface(int interfaceNo) { bool UsbDevice::claimInterface(int interfaceNo) {
// TODO: cover the other status codes that can be returned // TODO: cover the other status codes that can be returned
int claimInterfaceStatus = mLibUsb->claimInterface(mDeviceHandle, interfaceNo); int claimInterfaceStatus = libusb_claim_interface(mLibusbDevHandle, interfaceNo);
if (claimInterfaceStatus != 0) { if (claimInterfaceStatus != 0) {
mLastError = static_cast<Error>(claimInterfaceStatus); mLastError = static_cast<Error>(claimInterfaceStatus);
return false; return false;
@@ -93,7 +82,7 @@ bool UsbDevice::claimInterface(int interfaceNo) {
} }
bool UsbDevice::releaseInterface(int interfaceNo) { bool UsbDevice::releaseInterface(int interfaceNo) {
int releaseInterfaceStatus = mLibUsb->releaseInterface(mDeviceHandle, interfaceNo); int releaseInterfaceStatus = libusb_release_interface(mLibusbDevHandle, interfaceNo);
if (releaseInterfaceStatus != 0) { if (releaseInterfaceStatus != 0) {
mLastError = static_cast<Error>(releaseInterfaceStatus); mLastError = static_cast<Error>(releaseInterfaceStatus);
return false; return false;
@@ -103,8 +92,10 @@ bool UsbDevice::releaseInterface(int interfaceNo) {
bool UsbDevice::bulkTransfer(uint8_t endpoint, const std::vector<uint8_t>& data, int* tx, unsigned int timeout) { bool UsbDevice::bulkTransfer(uint8_t endpoint, const std::vector<uint8_t>& data, int* tx, unsigned int timeout) {
// TODO: implement error handling for incomplete transactions (tx length != data length) // TODO: implement error handling for incomplete transactions (tx length != data length)
int bulkTransferStatus = mLibUsb->bulkTransfer(mDeviceHandle, endpoint, const_cast<unsigned char*>(data.data()), int bulkTransferStatus = 0;
data.size(), tx, timeout);
bulkTransferStatus = libusb_bulk_transfer(mLibusbDevHandle, endpoint, const_cast<unsigned char*>(data.data()),
data.size(), tx, timeout);
if (bulkTransferStatus != 0) { if (bulkTransferStatus != 0) {
mLastError = static_cast<Error>(bulkTransferStatus); mLastError = static_cast<Error>(bulkTransferStatus);
return false; return false;
@@ -113,19 +104,19 @@ bool UsbDevice::bulkTransfer(uint8_t endpoint, const std::vector<uint8_t>& data,
} }
const usbId UsbDevice::getUsbId() { const usbId UsbDevice::getUsbId() {
return {mDeviceDescriptor.idVendor, mDeviceDescriptor.idProduct}; return {mLibusbDevDesc.idVendor, mLibusbDevDesc.idProduct};
} }
const device::Speed UsbDevice::getSpeed() { const device::Speed UsbDevice::getSpeed() {
return static_cast<device::Speed>(mLibUsb->getSpeed(mDevice.get())); return static_cast<device::Speed>(libusb_get_device_speed(mLibusbDev.get()));
} }
const uint8_t UsbDevice::getBusNumber() { const uint8_t UsbDevice::getBusNumber() {
return mLibUsb->getBusNumber(mDevice.get()); return libusb_get_bus_number(mLibusbDev.get());
} }
const uint8_t UsbDevice::getPortNumber() { const uint8_t UsbDevice::getPortNumber() {
return mLibUsb->getPortNumber(mDevice.get()); return libusb_get_port_number(mLibusbDev.get());
} }
const Error UsbDevice::getLastError() { const Error UsbDevice::getLastError() {
@@ -133,6 +124,6 @@ const Error UsbDevice::getLastError() {
} }
const std::string UsbDevice::getLastErrorString() { const std::string UsbDevice::getLastErrorString() {
return std::string(mLibUsb->errorName(static_cast<int>(mLastError))); return std::string(libusb_error_name(static_cast<int>(mLastError)));
} }
} // namespace libusbwrap } // namespace libusbwrap

View File

@@ -1,6 +1,6 @@
/* /*
ptrnt - print labels on linux ptrnt - print labels on linux
Copyright (C) 2023-2025 Moritz Martinius Copyright (C) 2023-2024 Moritz Martinius
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU General Public License as published by
@@ -24,20 +24,23 @@
#include "libusb.h" #include "libusb.h"
#include "libusbwrap/LibUsbTypes.hpp" #include "libusbwrap/LibUsbTypes.hpp"
#include "libusbwrap/LibUsbWrapper.hpp"
#include "libusbwrap/interface/IUsbDevice.hpp" #include "libusbwrap/interface/IUsbDevice.hpp"
namespace libusbwrap { 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<libusb_device, usbDevice_deleter>;
class UsbDevice : public IUsbDevice { class UsbDevice : public IUsbDevice {
public: public:
// Default constructor (uses real LibUsbWrapper) explicit UsbDevice(libusb_context* ctx, usbDevice_ptr dev);
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<ILibUsbWrapper> libusbWrapper);
~UsbDevice() override; ~UsbDevice() override;
// delete copy ctor and assignment // delete copy ctor and assignment
@@ -66,13 +69,10 @@ class UsbDevice : public IUsbDevice {
const std::string getLastErrorString() override; const std::string getLastErrorString() override;
private: private:
std::shared_ptr<ILibUsbWrapper> mLibUsb; // Injectable wrapper libusb_context* mLibusbCtx{nullptr};
usbDevice_ptr mLibusbDev{nullptr};
// RAII wrappers (UsbDevice owns the lifecycle logic) libusb_device_handle* mLibusbDevHandle{nullptr};
LibUsbDevicePtr mDevice; // unique_ptr with custom deleter libusb_device_descriptor mLibusbDevDesc{0};
libusb_device_handle* mDeviceHandle = nullptr; // Managed by UsbDevice (calls mLibUsb->close())
libusb_device_descriptor mDeviceDescriptor{0};
std::atomic<bool> mIsOpen = false; std::atomic<bool> mIsOpen = false;
Error mLastError = Error::SUCCESS; Error mLastError = Error::SUCCESS;
}; };

View File

@@ -27,24 +27,16 @@
#include <vector> #include <vector>
#include "libusb.h" #include "libusb.h"
#include "libusbwrap/LibUsbWrapper.hpp"
#include "libusbwrap/UsbDevice.hpp" #include "libusbwrap/UsbDevice.hpp"
#include "libusbwrap/interface/IUsbDevice.hpp" #include "libusbwrap/interface/IUsbDevice.hpp"
namespace libusbwrap { namespace libusbwrap {
// Default constructor delegates to DI constructor
UsbDeviceFactory::UsbDeviceFactory() : UsbDeviceFactory(std::make_shared<LibUsbWrapper>()) {}
// Constructor with dependency injection
UsbDeviceFactory::UsbDeviceFactory(std::shared_ptr<ILibUsbWrapper> libusbWrapper) : mLibUsb(std::move(libusbWrapper)) {}
UsbDeviceFactory::~UsbDeviceFactory() { UsbDeviceFactory::~UsbDeviceFactory() {
mDeviceList.clear(); // Release devices first if (mLibusbCtx) {
if (mContext) { libusb_exit(mLibusbCtx);
mLibUsb->exit(mContext);
} }
} };
std::vector<std::unique_ptr<IUsbDevice>> UsbDeviceFactory::findAllDevices() { std::vector<std::unique_ptr<IUsbDevice>> UsbDeviceFactory::findAllDevices() {
refreshDeviceList(); refreshDeviceList();
@@ -59,51 +51,46 @@ std::vector<std::unique_ptr<IUsbDevice>> UsbDeviceFactory::findDevices(uint16_t
ssize_t UsbDeviceFactory::refreshDeviceList() { ssize_t UsbDeviceFactory::refreshDeviceList() {
libusb_device** list{nullptr}; libusb_device** list{nullptr};
ssize_t count = mLibUsb->getDeviceList(mContext, &list); ssize_t ret = libusb_get_device_list(mLibusbCtx, &list);
mDeviceList.clear(); mLibusbDeviceList.clear();
if (ret < 0) {
if (count < 0) { spdlog::error("Error enumarating USB devices");
spdlog::error("Error enumerating USB devices"); } else if (ret == 0) {
} else if (count == 0) {
spdlog::warn("No USB devices found"); 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->freeDeviceList(list, false); // Don't unref (we did that above) for (ssize_t i = 0; i < ret; i++) {
return count; mLibusbDeviceList.emplace_back(list[i]);
}
libusb_free_device_list(list, false);
return ret;
} }
std::vector<std::unique_ptr<IUsbDevice>> UsbDeviceFactory::buildMaskedDeviceVector(uint16_t vidMask, uint16_t pidMask, std::vector<std::unique_ptr<IUsbDevice>> UsbDeviceFactory::buildMaskedDeviceVector(uint16_t vidMask, uint16_t pidMask,
uint16_t vid, uint16_t pid) { uint16_t vid, uint16_t pid) {
std::vector<std::unique_ptr<IUsbDevice>> matchedDevices; std::vector<std::unique_ptr<IUsbDevice>> matchedDevices;
// see libusb/examples/listdevs.c // see libusb/examples/listdevs.c
for (auto& dev : mDeviceList) { for (auto& currDev : mLibusbDeviceList) {
libusb_device_descriptor desc{}; struct libusb_device_descriptor currDevDesc{};
int ret = mLibUsb->getDeviceDescriptor(dev.get(), &desc); int ret = libusb_get_device_descriptor(currDev.get(), &currDevDesc);
spdlog::trace("Detected Device {:04x}:{:04x} ", desc.idVendor, desc.idProduct); spdlog::trace("Detected Device {:04x}:{:04x} ", currDevDesc.idVendor, currDevDesc.idProduct);
if (ret < 0) {
if (ret >= 0 && ((desc.idVendor & vidMask) == vid) && ((desc.idProduct & pidMask) == pid)) { continue;
// Transfer ownership of device to UsbDevice }
libusb_device* raw_dev = dev.release(); if (((currDevDesc.idVendor & vidMask) == vid) && ((currDevDesc.idProduct & pidMask) == pid)) {
matchedDevices.push_back(std::make_unique<UsbDevice>(mLibusbCtx, std::move(currDev)));
// Create UsbDevice with same wrapper instance
matchedDevices.push_back(std::make_unique<UsbDevice>(raw_dev, desc, mLibUsb));
} }
} }
return matchedDevices; return matchedDevices;
} }
bool UsbDeviceFactory::init() { bool UsbDeviceFactory::init() {
int err = mLibUsb->init(&mContext); auto err = libusb_init(&mLibusbCtx);
if (err != (int)Error::SUCCESS) { if (err != (int)Error::SUCCESS) {
spdlog::error("Could not initialize libusb"); spdlog::error("Could not intialize libusb");
return false; return false;
} }

View File

@@ -1,6 +1,6 @@
/* /*
ptrnt - print labels on linux ptrnt - print labels on linux
Copyright (C) 2023-2025 Moritz Martinius Copyright (C) 2023 Moritz Martinius
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU General Public License as published by
@@ -20,10 +20,8 @@
#pragma once #pragma once
#include <cstdint> #include <cstdint>
#include <memory>
#include "libusb.h" #include "libusb.h"
#include "libusbwrap/LibUsbWrapper.hpp"
#include "libusbwrap/UsbDevice.hpp" #include "libusbwrap/UsbDevice.hpp"
#include "libusbwrap/interface/IUsbDeviceFactory.hpp" #include "libusbwrap/interface/IUsbDeviceFactory.hpp"
@@ -33,12 +31,7 @@ constexpr const uint16_t LIBUSB_BITMASK_ALL = 0xffff;
class UsbDeviceFactory : public IUsbDeviceFactory { class UsbDeviceFactory : public IUsbDeviceFactory {
public: public:
// Default constructor (uses real LibUsbWrapper) UsbDeviceFactory() = default;
UsbDeviceFactory();
// Constructor for testing (inject mock wrapper)
explicit UsbDeviceFactory(std::shared_ptr<ILibUsbWrapper> libusbWrapper);
virtual ~UsbDeviceFactory(); virtual ~UsbDeviceFactory();
// delete copy ctor and assignment // delete copy ctor and assignment
@@ -47,17 +40,17 @@ class UsbDeviceFactory : public IUsbDeviceFactory {
UsbDeviceFactory(UsbDeviceFactory&&) = delete; UsbDeviceFactory(UsbDeviceFactory&&) = delete;
UsbDeviceFactory& operator=(UsbDeviceFactory&&) = delete; UsbDeviceFactory& operator=(UsbDeviceFactory&&) = delete;
bool init() override; bool init();
/** /**
* @brief Gets all devices that are detected by libusb. Will allocate a shared_ptr for each Device * @brief Gets all devices that are detected by libusb. Will allocate a shared_ptr for each Device
* *
* @return std::vector<std::shared_ptr<IUsbDevice>> Vector of all detected USB devices * @return std::vector<std::shared_ptr<IUsbDevice>> Vector of all detected USB devices
*/ */
std::vector<std::unique_ptr<IUsbDevice>> findAllDevices() override; std::vector<std::unique_ptr<IUsbDevice>> 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 * If only one device of certain type is connected, vector is usually only one element
* *
* @param vid VendorId of the devices to find * @param vid VendorId of the devices to find
* @param pid ProductId of the devices to find * @param pid ProductId of the devices to find
* @return std::vector<std::shared_ptr<IUsbDevice>> Vector of detected USB devices based on vid/pid * @return std::vector<std::shared_ptr<IUsbDevice>> Vector of detected USB devices based on vid/pid
@@ -67,12 +60,12 @@ class UsbDeviceFactory : public IUsbDeviceFactory {
private: private:
// methods // methods
ssize_t refreshDeviceList(); ssize_t refreshDeviceList();
std::vector<std::unique_ptr<IUsbDevice>> buildMaskedDeviceVector(uint16_t vidMask, uint16_t pidMask, uint16_t vid, std::vector<std::unique_ptr<IUsbDevice>> buildMaskedDeviceVector(uint16_t vidMask,
uint16_t pidMask, uint16_t vid,
uint16_t pid); uint16_t pid);
// members // members
std::shared_ptr<ILibUsbWrapper> mLibUsb; libusb_context* mLibusbCtx{nullptr};
libusb_context* mContext{nullptr}; // Factory manages lifecycle std::vector<usbDevice_ptr> mLibusbDeviceList{};
std::vector<LibUsbDevicePtr> mDeviceList{};
bool mDeviceListInitialized = false; bool mDeviceListInitialized = false;
}; };
} // namespace libusbwrap } // namespace libusbwrap

View File

@@ -1,6 +1,6 @@
/* /*
ptrnt - print labels on linux ptrnt - print labels on linux
Copyright (C) 2023-2025 Moritz Martinius Copyright (C) 2023-2024 Moritz Martinius
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU General Public License as published by
@@ -22,6 +22,7 @@
#include <sys/types.h> #include <sys/types.h>
#include <cstdint> #include <cstdint>
#include <optional>
#include <string> #include <string>
#include <vector> #include <vector>
@@ -43,9 +44,8 @@ enum class Speed {
class IUsbDevice { class IUsbDevice {
public: public:
virtual ~IUsbDevice() = default; virtual ~IUsbDevice() = default;
virtual bool open() = 0;
virtual bool open() = 0; virtual void close() = 0;
virtual void close() = 0;
// libusb wrappers // libusb wrappers
virtual bool detachKernelDriver(int interfaceNo) = 0; virtual bool detachKernelDriver(int interfaceNo) = 0;

View File

@@ -1,6 +1,6 @@
/* /*
ptrnt - print labels on linux ptrnt - print labels on linux
Copyright (C) 2023-2025 Moritz Martinius Copyright (C) 2023 Moritz Martinius
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU General Public License as published by
@@ -29,7 +29,6 @@ namespace libusbwrap {
class IUsbDeviceFactory { class IUsbDeviceFactory {
public: public:
virtual ~IUsbDeviceFactory() = default; virtual ~IUsbDeviceFactory() = default;
virtual bool init() = 0;
virtual std::vector<std::unique_ptr<IUsbDevice>> findAllDevices() = 0; virtual std::vector<std::unique_ptr<IUsbDevice>> findAllDevices() = 0;
virtual std::vector<std::unique_ptr<IUsbDevice>> findDevices(uint16_t vid, uint16_t pid) = 0; virtual std::vector<std::unique_ptr<IUsbDevice>> findDevices(uint16_t vid, uint16_t pid) = 0;
}; };

View File

@@ -1,6 +1,6 @@
/* /*
ptrnt - print labels on linux ptrnt - print labels on linux
Copyright (C) 2022-2025 Moritz Martinius Copyright (C) 2022-2023 Moritz Martinius
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU General Public License as published by

View File

@@ -1,36 +1,34 @@
ptprnt_hpps = files( ptprnt_hpps = files (
'cli/CliParser.hpp', 'libusbwrap/interface/IUsbDeviceFactory.hpp',
'core/PrinterDriverFactory.hpp', 'libusbwrap/interface/IUsbDevice.hpp',
'core/PrinterService.hpp', 'libusbwrap/UsbDeviceFactory.hpp',
'libusbwrap/LibUsbTypes.hpp',
'libusbwrap/UsbDevice.hpp',
'interface/IPrinterDriver.hpp',
'interface/IPrinterTypes.hpp',
'printers/P700Printer.hpp',
'printers/FakePrinter.hpp',
'PtouchPrint.hpp',
'PrinterDriverFactory.hpp',
'graphics/Bitmap.hpp', 'graphics/Bitmap.hpp',
'graphics/Label.hpp', 'graphics/Label.hpp',
'graphics/LabelBuilder.hpp', 'graphics/LabelBuilder.hpp',
'graphics/Monochrome.hpp', 'graphics/Monochrome.hpp',
'libusbwrap/LibUsbTypes.hpp', 'cli/CliParser.hpp',
'libusbwrap/LibUsbWrapper.hpp', 'core/PrinterService.hpp'
'libusbwrap/UsbDevice.hpp',
'libusbwrap/UsbDeviceFactory.hpp',
'libusbwrap/interface/IUsbDevice.hpp',
'libusbwrap/interface/IUsbDeviceFactory.hpp',
'printers/FakePrinter.hpp',
'printers/P700Printer.hpp',
'printers/interface/IPrinterDriver.hpp',
'printers/interface/IPrinterTypes.hpp',
'PtouchPrint.hpp',
) )
ptprnt_srcs = files( ptprnt_srcs = files (
'cli/CliParser.cpp', 'PtouchPrint.cpp',
'core/PrinterDriverFactory.cpp', 'PrinterDriverFactory.cpp',
'core/PrinterService.cpp', 'printers/P700Printer.cpp',
'graphics/Bitmap.cpp', 'printers/FakePrinter.cpp',
'graphics/Label.cpp', 'graphics/Label.cpp',
'graphics/LabelBuilder.cpp', 'graphics/LabelBuilder.cpp',
'graphics/Bitmap.cpp',
'graphics/Monochrome.cpp', 'graphics/Monochrome.cpp',
'libusbwrap/LibUsbWrapper.cpp',
'libusbwrap/UsbDevice.cpp',
'libusbwrap/UsbDeviceFactory.cpp', 'libusbwrap/UsbDeviceFactory.cpp',
'printers/FakePrinter.cpp', 'libusbwrap/UsbDevice.cpp',
'printers/P700Printer.cpp', 'cli/CliParser.cpp',
'PtouchPrint.cpp', 'core/PrinterService.cpp'
) )

View File

@@ -19,50 +19,49 @@
#include "FakePrinter.hpp" #include "FakePrinter.hpp"
#include <cairo.h>
#include <spdlog/spdlog.h> #include <spdlog/spdlog.h>
#include <cairo.h>
#include <chrono>
#include <cstdint> #include <cstdint>
#include <iomanip>
#include <sstream>
#include <stdexcept> #include <stdexcept>
#include <vector> #include <vector>
#include <chrono>
#include <iomanip>
#include <sstream>
#include "graphics/Monochrome.hpp" #include "../graphics/Monochrome.hpp"
namespace ptprnt::printer { namespace ptprnt::printer {
const PrinterInfo FakePrinter::mInfo = {.driverName = "FakePrinter", const PrinterInfo FakePrinter::mInfo = {
.name = "Virtual Test Printer", .driverName = "FakePrinter",
.version = "v1.0", .name = "Virtual Test Printer",
.usbId{0x0000, 0x0000}, // No USB ID - virtual printer created explicitly .version = "v1.0",
.pixelLines = 128}; .usbId{0x0000, 0x0000}, // No USB ID - virtual printer created explicitly
.pixelLines = 128
};
std::string_view FakePrinter::getDriverName() { const std::string_view FakePrinter::getDriverName() {
return mInfo.driverName; return mInfo.driverName;
} }
std::string_view FakePrinter::getName() { const std::string_view FakePrinter::getName() {
return mInfo.name; return mInfo.name;
} }
std::string_view FakePrinter::getVersion() { const std::string_view FakePrinter::getVersion() {
return mInfo.version; return mInfo.version;
} }
PrinterInfo FakePrinter::getPrinterInfo() { const PrinterInfo FakePrinter::getPrinterInfo() {
return mInfo; return mInfo;
} }
PrinterStatus FakePrinter::getPrinterStatus() { const PrinterStatus FakePrinter::getPrinterStatus() {
if (!mHasAttachedDevice) {
return {};
}
return mStatus; return mStatus;
} }
libusbwrap::usbId FakePrinter::getUsbId() { const libusbwrap::usbId FakePrinter::getUsbId() {
return mInfo.usbId; return mInfo.usbId;
} }
@@ -81,25 +80,22 @@ bool FakePrinter::detachUsbDevice() {
bool FakePrinter::printBitmap(const graphics::Bitmap<graphics::ALPHA8>& bitmap) { bool FakePrinter::printBitmap(const graphics::Bitmap<graphics::ALPHA8>& bitmap) {
// Convert bitmap to MonochromeData and delegate // Convert bitmap to MonochromeData and delegate
auto pixels = bitmap.getPixelsCpy(); auto pixels = bitmap.getPixelsCpy();
auto mono = graphics::Monochrome(pixels, bitmap.getWidth(), bitmap.getHeight()); auto mono = graphics::Monochrome(pixels, bitmap.getWidth(), bitmap.getHeight());
auto monoData = mono.get(); auto monoData = mono.get();
return printMonochromeData(monoData); return printMonochromeData(monoData);
} }
bool FakePrinter::printMonochromeData(const graphics::MonochromeData& data) { bool FakePrinter::printMonochromeData(const graphics::MonochromeData& data) {
if (!mHasAttachedDevice) {
return false;
}
spdlog::debug("FakePrinter: Simulating printing of {}x{} bitmap", data.width, data.height); spdlog::debug("FakePrinter: Simulating printing of {}x{} bitmap", data.width, data.height);
// Simulate the printing process by reconstructing the bitmap // Simulate the printing process by reconstructing the bitmap
auto printed = simulatePrinting(data); auto printed = simulatePrinting(data);
mLastPrint = std::make_unique<graphics::Bitmap<graphics::ALPHA8>>(std::move(printed)); mLastPrint = std::make_unique<graphics::Bitmap<graphics::ALPHA8>>(std::move(printed));
spdlog::info("FakePrinter: Successfully 'printed' label ({}x{} pixels)", mLastPrint->getWidth(), spdlog::info("FakePrinter: Successfully 'printed' label ({}x{} pixels)",
mLastPrint->getHeight()); mLastPrint->getWidth(), mLastPrint->getHeight());
// Save to timestamped PNG file // Save to timestamped PNG file
std::string filename = generateTimestampedFilename(); std::string filename = generateTimestampedFilename();
@@ -113,11 +109,6 @@ bool FakePrinter::printMonochromeData(const graphics::MonochromeData& data) {
} }
bool FakePrinter::printLabel(const std::unique_ptr<graphics::ILabel> label) { bool FakePrinter::printLabel(const std::unique_ptr<graphics::ILabel> label) {
if (!label) {
spdlog::error("FakePrinter: Cannot print null label");
return false;
}
// Convert label directly to MonochromeData // Convert label directly to MonochromeData
// getRaw() returns data in Cairo surface coordinates matching getWidth() × getHeight() // getRaw() returns data in Cairo surface coordinates matching getWidth() × getHeight()
auto pixels = label->getRaw(); auto pixels = label->getRaw();
@@ -129,16 +120,12 @@ bool FakePrinter::printLabel(const std::unique_ptr<graphics::ILabel> label) {
// Transform to portrait orientation for printing // Transform to portrait orientation for printing
monoData.transformTo(graphics::Orientation::PORTRAIT); monoData.transformTo(graphics::Orientation::PORTRAIT);
spdlog::debug("FakePrinter: Label surface is {}x{}, transformed to portrait", label->getWidth(), spdlog::debug("FakePrinter: Label surface is {}x{}, transformed to portrait", label->getWidth(), label->getHeight());
label->getHeight());
return printMonochromeData(monoData); return printMonochromeData(monoData);
} }
bool FakePrinter::print() { bool FakePrinter::print() {
if (!mHasAttachedDevice) {
return false;
}
spdlog::debug("FakePrinter: Print command (no-op for virtual printer)"); spdlog::debug("FakePrinter: Print command (no-op for virtual printer)");
return true; return true;
} }
@@ -173,7 +160,7 @@ graphics::Bitmap<graphics::ALPHA8> FakePrinter::simulatePrinting(const graphics:
// Now "print" this column by unpacking the bytes back to pixels // Now "print" this column by unpacking the bytes back to pixels
for (size_t byteIdx = 0; byteIdx < columnBytes.size(); byteIdx++) { for (size_t byteIdx = 0; byteIdx < columnBytes.size(); byteIdx++) {
uint8_t byte = columnBytes[byteIdx]; uint8_t byte = columnBytes[byteIdx];
uint32_t baseRow = byteIdx * 8; uint32_t baseRow = byteIdx * 8;
for (int bit = 0; bit < 8 && (baseRow + bit) < data.height; bit++) { for (int bit = 0; bit < 8 && (baseRow + bit) < data.height; bit++) {
@@ -181,7 +168,7 @@ graphics::Bitmap<graphics::ALPHA8> FakePrinter::simulatePrinting(const graphics:
uint32_t row = baseRow + bit; uint32_t row = baseRow + bit;
// Write to output bitmap // Write to output bitmap
size_t pixelIdx = row * data.width + col; size_t pixelIdx = row * data.width + col;
pixels[pixelIdx] = pixelOn ? 255 : 0; // 255 = black, 0 = white pixels[pixelIdx] = pixelOn ? 255 : 0; // 255 = black, 0 = white
} }
} }
@@ -190,8 +177,8 @@ graphics::Bitmap<graphics::ALPHA8> FakePrinter::simulatePrinting(const graphics:
// Set the pixels in the result bitmap // Set the pixels in the result bitmap
result.setPixels(pixels); result.setPixels(pixels);
spdlog::debug("FakePrinter: Simulation complete, reconstructed {}x{} bitmap", result.getWidth(), spdlog::debug("FakePrinter: Simulation complete, reconstructed {}x{} bitmap",
result.getHeight()); result.getWidth(), result.getHeight());
return result; return result;
} }
@@ -214,8 +201,8 @@ bool FakePrinter::saveLastPrintToPng(const std::string& filename) const {
bool FakePrinter::saveBitmapToPng(const graphics::Bitmap<graphics::ALPHA8>& bitmap, const std::string& filename) const { bool FakePrinter::saveBitmapToPng(const graphics::Bitmap<graphics::ALPHA8>& bitmap, const std::string& filename) const {
// Create Cairo surface from bitmap data // Create Cairo surface from bitmap data
auto pixels = bitmap.getPixelsCpy(); auto pixels = bitmap.getPixelsCpy();
uint16_t width = bitmap.getWidth(); uint16_t width = bitmap.getWidth();
uint16_t height = bitmap.getHeight(); uint16_t height = bitmap.getHeight();
// Cairo expects ARGB32 format, but we have ALPHA8 // Cairo expects ARGB32 format, but we have ALPHA8
@@ -230,9 +217,14 @@ bool FakePrinter::saveBitmapToPng(const graphics::Bitmap<graphics::ALPHA8>& bitm
} }
// Create Cairo surface // Create Cairo surface
int stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, width); int stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, width);
cairo_surface_t* surface = cairo_image_surface_create_for_data(reinterpret_cast<unsigned char*>(argbPixels.data()), cairo_surface_t* surface = cairo_image_surface_create_for_data(
CAIRO_FORMAT_ARGB32, width, height, stride); reinterpret_cast<unsigned char*>(argbPixels.data()),
CAIRO_FORMAT_ARGB32,
width,
height,
stride
);
if (cairo_surface_status(surface) != CAIRO_STATUS_SUCCESS) { if (cairo_surface_status(surface) != CAIRO_STATUS_SUCCESS) {
spdlog::error("FakePrinter: Failed to create Cairo surface: {}", spdlog::error("FakePrinter: Failed to create Cairo surface: {}",
@@ -256,12 +248,14 @@ bool FakePrinter::saveBitmapToPng(const graphics::Bitmap<graphics::ALPHA8>& bitm
std::string FakePrinter::generateTimestampedFilename() const { std::string FakePrinter::generateTimestampedFilename() const {
// Get current time // Get current time
auto now = std::chrono::system_clock::now(); auto now = std::chrono::system_clock::now();
auto time = std::chrono::system_clock::to_time_t(now); auto time = std::chrono::system_clock::to_time_t(now);
// Format: fakelabel_YYYYMMDD_HHMMSS.png // Format: fakelabel_YYYYMMDD_HHMMSS.png
std::stringstream ss; std::stringstream ss;
ss << "fakelabel_" << std::put_time(std::localtime(&time), "%Y%m%d_%H%M%S") << ".png"; ss << "fakelabel_"
<< std::put_time(std::localtime(&time), "%Y%m%d_%H%M%S")
<< ".png";
return ss.str(); return ss.str();
} }

View File

@@ -19,15 +19,15 @@
#pragma once #pragma once
#include <cstdint>
#include <memory> #include <memory>
#include <vector> #include <vector>
#include <cstdint>
#include "graphics/Bitmap.hpp" #include "../interface/IPrinterDriver.hpp"
#include "interface/IPrinterDriver.hpp" #include "../interface/IPrinterTypes.hpp"
#include "interface/IPrinterTypes.hpp" #include "../libusbwrap/LibUsbTypes.hpp"
#include "libusbwrap/LibUsbTypes.hpp" #include "../libusbwrap/interface/IUsbDevice.hpp"
#include "libusbwrap/interface/IUsbDevice.hpp" #include "../graphics/Bitmap.hpp"
namespace ptprnt::printer { namespace ptprnt::printer {
@@ -40,7 +40,7 @@ namespace ptprnt::printer {
*/ */
class FakePrinter : public ::ptprnt::IPrinterDriver { class FakePrinter : public ::ptprnt::IPrinterDriver {
public: public:
FakePrinter() = default; FakePrinter() = default;
~FakePrinter() override = default; ~FakePrinter() override = default;
FakePrinter(const FakePrinter&) = delete; FakePrinter(const FakePrinter&) = delete;
@@ -52,12 +52,12 @@ class FakePrinter : public ::ptprnt::IPrinterDriver {
static const PrinterInfo mInfo; static const PrinterInfo mInfo;
// IPrinterDriver interface // IPrinterDriver interface
[[nodiscard]] std::string_view getDriverName() override; [[nodiscard]] const std::string_view getDriverName() override;
[[nodiscard]] std::string_view getName() override; [[nodiscard]] const std::string_view getName() override;
[[nodiscard]] libusbwrap::usbId getUsbId() override; [[nodiscard]] const libusbwrap::usbId getUsbId() override;
[[nodiscard]] std::string_view getVersion() override; [[nodiscard]] const std::string_view getVersion() override;
[[nodiscard]] PrinterInfo getPrinterInfo() override; [[nodiscard]] const PrinterInfo getPrinterInfo() override;
[[nodiscard]] PrinterStatus getPrinterStatus() override; [[nodiscard]] const PrinterStatus getPrinterStatus() override;
bool attachUsbDevice(std::shared_ptr<libusbwrap::IUsbDevice> usbHndl) override; bool attachUsbDevice(std::shared_ptr<libusbwrap::IUsbDevice> usbHndl) override;
bool detachUsbDevice() override; bool detachUsbDevice() override;
bool printBitmap(const graphics::Bitmap<graphics::ALPHA8>& bitmap) override; bool printBitmap(const graphics::Bitmap<graphics::ALPHA8>& bitmap) override;

View File

@@ -28,9 +28,9 @@
#include <thread> #include <thread>
#include <vector> #include <vector>
#include "graphics/Bitmap.hpp" #include "../graphics/Bitmap.hpp"
#include "graphics/Monochrome.hpp" #include "../graphics/Monochrome.hpp"
#include "libusbwrap/LibUsbTypes.hpp" #include "../libusbwrap/LibUsbTypes.hpp"
#include "spdlog/fmt/bin_to_hex.h" #include "spdlog/fmt/bin_to_hex.h"
namespace ptprnt::printer { namespace ptprnt::printer {
@@ -48,30 +48,24 @@ P700Printer::~P700Printer() {
} }
} }
std::string_view P700Printer::getDriverName() { const std::string_view P700Printer::getDriverName() {
return mInfo.driverName; return mInfo.driverName;
} }
std::string_view P700Printer::getName() { const std::string_view P700Printer::getName() {
return mInfo.name; return mInfo.name;
} }
std::string_view P700Printer::getVersion() { const std::string_view P700Printer::getVersion() {
return mInfo.version; return mInfo.version;
} }
PrinterInfo P700Printer::getPrinterInfo() { const PrinterInfo P700Printer::getPrinterInfo() {
return mInfo; return mInfo;
} }
PrinterStatus P700Printer::getPrinterStatus() { const PrinterStatus P700Printer::getPrinterStatus() {
using namespace std::chrono_literals; using namespace std::chrono_literals;
if (!mUsbHndl) {
spdlog::error("USB Handle is invalid!");
return {};
}
send(p700::commands::GET_STATUS); send(p700::commands::GET_STATUS);
int tx = 0; int tx = 0;
@@ -85,16 +79,11 @@ PrinterStatus P700Printer::getPrinterStatus() {
return PrinterStatus{.tapeWidthMm = recvBuf[10]}; return PrinterStatus{.tapeWidthMm = recvBuf[10]};
} }
libusbwrap::usbId P700Printer::getUsbId() { const libusbwrap::usbId P700Printer::getUsbId() {
return mInfo.usbId; return mInfo.usbId;
} }
bool P700Printer::attachUsbDevice(std::shared_ptr<libusbwrap::IUsbDevice> usbHndl) { bool P700Printer::attachUsbDevice(std::shared_ptr<libusbwrap::IUsbDevice> usbHndl) {
if (!usbHndl) {
spdlog::error("Cannot attach null USB device");
return false;
}
if (!usbHndl->open()) { if (!usbHndl->open()) {
spdlog::error("Unable to open USB device: {}", usbHndl->getLastErrorString()); spdlog::error("Unable to open USB device: {}", usbHndl->getLastErrorString());
return false; return false;
@@ -138,10 +127,6 @@ bool P700Printer::printBitmap(const graphics::Bitmap<graphics::ALPHA8>& bitmap)
} }
bool P700Printer::printMonochromeData(const graphics::MonochromeData& data) { bool P700Printer::printMonochromeData(const graphics::MonochromeData& data) {
if (!mUsbHndl) {
spdlog::error("USB Handle is invalid!");
return false;
}
// Send initialization sequence // Send initialization sequence
// The INITIALIZE command needs to be sent as a 128-byte packet with ESC @ at the end // The INITIALIZE command needs to be sent as a 128-byte packet with ESC @ at the end
std::vector<uint8_t> initCmd(128, 0x00); std::vector<uint8_t> initCmd(128, 0x00);
@@ -191,11 +176,6 @@ bool P700Printer::printMonochromeData(const graphics::MonochromeData& data) {
} }
bool P700Printer::printLabel(std::unique_ptr<graphics::ILabel> label) { bool P700Printer::printLabel(std::unique_ptr<graphics::ILabel> label) {
if (!label) {
spdlog::error("Cannot print null label");
return false;
}
// Convert label directly to MonochromeData // Convert label directly to MonochromeData
// getRaw() returns data in Cairo surface coordinates matching getWidth() × getHeight() // getRaw() returns data in Cairo surface coordinates matching getWidth() × getHeight()
auto pixels = label->getRaw(); auto pixels = label->getRaw();
@@ -214,15 +194,9 @@ bool P700Printer::printLabel(std::unique_ptr<graphics::ILabel> label) {
} }
bool P700Printer::print() { bool P700Printer::print() {
if (!send(p700::commands::LF)) { send(p700::commands::LF);
return false; send(p700::commands::FF);
} send(p700::commands::EJECT);
if (!send(p700::commands::FF)) {
return false;
}
if (!send(p700::commands::EJECT)) {
return false;
}
return true; return true;
} }

View File

@@ -17,8 +17,6 @@
*/ */
#pragma once
#include <spdlog/spdlog.h> #include <spdlog/spdlog.h>
#include <sys/types.h> #include <sys/types.h>
@@ -30,13 +28,15 @@
#include "libusbwrap/LibUsbTypes.hpp" #include "libusbwrap/LibUsbTypes.hpp"
#include "libusbwrap/interface/IUsbDevice.hpp" #include "libusbwrap/interface/IUsbDevice.hpp"
#pragma once
namespace ptprnt::printer { namespace ptprnt::printer {
namespace p700::commands { namespace p700::commands {
const cmd_T INITIALIZE{0x1b, 0x40}; // ESC @ - Initialize const cmd_T INITIALIZE{0x1b, 0x40}; // ESC @ - Initialize
const cmd_T GET_STATUS{0x1b, 0x69, 0x53}; // ESC i S - Status query const cmd_T GET_STATUS{0x1b, 0x69, 0x53}; // ESC i S - Status query
const cmd_T PRINT_MODE{0x4d, 0x02}; // M 0x02 - Print mode const cmd_T PRINT_MODE{0x4d, 0x02}; // M 0x02 - Print mode
const cmd_T AUTO_STATUS{0x1b, 0x69, 0x61, 0x01}; // ESC i a - Auto status const cmd_T AUTO_STATUS{0x1b, 0x69, 0x61, 0x01}; // ESC i a - Auto status
const cmd_T MODE_SETTING{0x1b, 0x69, 0x4d, 0x40}; // ESC i M @ - Advanced mode const cmd_T MODE_SETTING{0x1b, 0x69, 0x4d, 0x40}; // ESC i M @ - Advanced mode
const cmd_T RASTER_START{0x1b, 0x69, 0x7a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; const cmd_T RASTER_START{0x1b, 0x69, 0x7a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
const cmd_T INFO{0x1b, 0x69, 0x7a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; const cmd_T INFO{0x1b, 0x69, 0x7a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
const cmd_T PACKBITSON{0x02}; const cmd_T PACKBITSON{0x02};
@@ -66,12 +66,12 @@ class P700Printer : public ::ptprnt::IPrinterDriver {
static const PrinterInfo mInfo; static const PrinterInfo mInfo;
// IPrinterDriver // IPrinterDriver
[[nodiscard]] std::string_view getDriverName() override; [[nodiscard]] const std::string_view getDriverName() override;
[[nodiscard]] std::string_view getName() override; [[nodiscard]] const std::string_view getName() override;
[[nodiscard]] libusbwrap::usbId getUsbId() override; [[nodiscard]] const libusbwrap::usbId getUsbId() override;
[[nodiscard]] std::string_view getVersion() override; [[nodiscard]] const std::string_view getVersion() override;
[[nodiscard]] PrinterInfo getPrinterInfo() override; [[nodiscard]] const PrinterInfo getPrinterInfo() override;
[[nodiscard]] PrinterStatus getPrinterStatus() override; [[nodiscard]] const PrinterStatus getPrinterStatus() override;
bool attachUsbDevice(std::shared_ptr<libusbwrap::IUsbDevice> usbHndl) override; bool attachUsbDevice(std::shared_ptr<libusbwrap::IUsbDevice> usbHndl) override;
bool detachUsbDevice() override; bool detachUsbDevice() override;
bool printBitmap(const graphics::Bitmap<graphics::ALPHA8>& bitmap) override; bool printBitmap(const graphics::Bitmap<graphics::ALPHA8>& bitmap) override;

View File

@@ -1,10 +1,9 @@
[wrap-file] [wrap-file]
directory = CLI11-2.5.0 directory = CLI11-2.3.2
source_url = https://github.com/CLIUtils/CLI11/archive/refs/tags/v2.5.0.tar.gz source_url = https://github.com/CLIUtils/CLI11/archive/refs/tags/v2.3.2.tar.gz
source_filename = CLI11-2.5.0.tar.gz source_filename = CLI11-2.3.2.tar.gz
source_hash = 17e02b4cddc2fa348e5dbdbb582c59a3486fa2b2433e70a0c3bacb871334fd55 source_hash = aac0ab42108131ac5d3344a9db0fdf25c4db652296641955720a4fbe52334e22
source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/cli11_2.5.0-2/CLI11-2.5.0.tar.gz wrapdb_version = 2.3.2-1
wrapdb_version = 2.5.0-2
[provide] [provide]
dependency_names = CLI11 cli11 = CLI11_dep

View File

@@ -1,13 +1,13 @@
[wrap-file] [wrap-file]
directory = googletest-1.17.0 directory = googletest-1.14.0
source_url = https://github.com/google/googletest/archive/refs/tags/v1.17.0.tar.gz source_url = https://github.com/google/googletest/archive/refs/tags/v1.14.0.tar.gz
source_filename = googletest-1.17.0.tar.gz source_filename = gtest-1.14.0.tar.gz
source_hash = 65fab701d9829d38cb77c14acdc431d2108bfdbf8979e40eb8ae567edf10b27c source_hash = 8ad598c73ad796e0d8280b082cebd82a630d73e73cd3c70057938a6501bba5d7
patch_filename = gtest_1.17.0-4_patch.zip patch_filename = gtest_1.14.0-1_patch.zip
patch_url = https://wrapdb.mesonbuild.com/v2/gtest_1.17.0-4/get_patch patch_url = https://wrapdb.mesonbuild.com/v2/gtest_1.14.0-1/get_patch
patch_hash = 3abf7662d09db706453a5b064a1e914678c74b9d9b0b19382747ca561d0d8750 patch_hash = 2e693c7d3f9370a7aa6dac802bada0874d3198ad4cfdf75647b818f691182b50
source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/gtest_1.17.0-4/googletest-1.17.0.tar.gz source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/gtest_1.14.0-1/gtest-1.14.0.tar.gz
wrapdb_version = 1.17.0-4 wrapdb_version = 1.14.0-1
[provide] [provide]
gtest = gtest_dep gtest = gtest_dep

View File

@@ -1,13 +0,0 @@
[wrap-file]
directory = spdlog-1.15.3
source_url = https://github.com/gabime/spdlog/archive/refs/tags/v1.15.3.tar.gz
source_filename = spdlog-1.15.3.tar.gz
source_hash = 15a04e69c222eb6c01094b5c7ff8a249b36bb22788d72519646fb85feb267e67
source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/spdlog_1.15.3-5/spdlog-1.15.3.tar.gz
patch_filename = spdlog_1.15.3-5_patch.zip
patch_url = https://wrapdb.mesonbuild.com/v2/spdlog_1.15.3-5/get_patch
patch_hash = 5e0eaf0002ff589cd8dac58e1b38c297422e7a0404d7d47ff0d2e285ed18169c
wrapdb_version = 1.15.3-5
[provide]
dependency_names = spdlog

View File

@@ -1,6 +1,6 @@
/* /*
ptrnt - print labels on linux ptrnt - print labels on linux
Copyright (C) 2023-2025 Moritz Martinius Copyright (C) 2023 Moritz Martinius
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU General Public License as published by

View File

@@ -1,365 +0,0 @@
/*
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 <https://www.gnu.org/licenses/>.
*/
#include <gtest/gtest.h>
#include <cstring>
#include <vector>
#include "cli/CliParser.hpp"
namespace ptprnt::cli {
class CliParserTest : public ::testing::Test {
protected:
void SetUp() override {
parser = std::make_unique<CliParser>("Test Application", "v1.0.0");
}
void TearDown() override { parser.reset(); }
// Helper to convert vector of strings to argc/argv
std::vector<char*> makeArgv(const std::vector<std::string>& args) {
argv_storage.clear();
argv_storage.reserve(args.size());
for (const auto& arg : args) {
argv_storage.push_back(const_cast<char*>(arg.c_str()));
}
return argv_storage;
}
std::unique_ptr<CliParser> parser;
std::vector<char*> 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<std::string> 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<std::string> 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<std::string> 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<std::string> 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<std::string> 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<std::string> 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<std::string> 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<std::string> 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<std::string> 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<std::string> 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<std::string> 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<std::string> 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<std::string> 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<std::string> 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<std::string> 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<std::string> 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<std::string> 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<std::string> 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<std::string> 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<std::string> 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<std::string> 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<std::string> 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<std::string> 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

View File

@@ -1,247 +0,0 @@
/*
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 <https://www.gnu.org/licenses/>.
*/
#include <gtest/gtest.h>
#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<FakePrinter>();
}
void TearDown() override { printer.reset(); }
std::unique_ptr<FakePrinter> 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<graphics::ALPHA8> bitmap(100, 128);
// Fill with some pattern
std::vector<graphics::ALPHA8> 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<graphics::ALPHA8> 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<graphics::Label>(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<graphics::ALPHA8> 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<graphics::ALPHA8> 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<graphics::ALPHA8> bitmap1(50, 128);
graphics::Bitmap<graphics::ALPHA8> 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<graphics::ALPHA8> bitmap(50, 128);
EXPECT_TRUE(printer->printBitmap(bitmap));
}
} // namespace ptprnt::printer

View File

@@ -1,230 +0,0 @@
/*
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 <https://www.gnu.org/licenses/>.
*/
#include <gtest/gtest.h>
#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<LabelBuilder>(128); // P700 printer height
}
void TearDown() override { builder.reset(); }
std::unique_ptr<LabelBuilder> 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

View File

@@ -1,6 +1,6 @@
/* /*
ptrnt - print labels on linux ptrnt - print labels on linux
Copyright (C) 2025 Moritz Martinius Copyright (C) 2023 Moritz Martinius
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU General Public License as published by
@@ -19,244 +19,8 @@
#include "graphics/Label.hpp" #include "graphics/Label.hpp"
#include <gmock/gmock.h>
#include <gtest/gtest.h> #include <gtest/gtest.h>
#include <memory>
#include <vector>
#include "graphics/interface/ILabel.hpp"
#include "mocks/MockCairoWrapper.hpp"
using ::testing::_;
using ::testing::DoAll;
using ::testing::NiceMock;
using ::testing::Return;
using ::testing::SetArgPointee;
namespace ptprnt::graphics {
// Test fixture for Label tests with comprehensive mock setup
class LabelTest : public ::testing::Test {
protected:
void SetUp() override {
mockWrapper = std::make_shared<NiceMock<MockCairoWrapper>>();
// Mock pointers for temporary surface (used in size calculation)
mockTempSurface = reinterpret_cast<cairo_surface_t*>(0x2000);
mockTempCr = reinterpret_cast<cairo_t*>(0x2001);
mockTempCtx = reinterpret_cast<PangoContext*>(0x2002);
mockTempLayout = reinterpret_cast<PangoLayout*>(0x2003);
// Mock pointers for final surface (used in rendering)
mockFinalSurface = reinterpret_cast<cairo_surface_t*>(0x3000);
mockFinalCr = reinterpret_cast<cairo_t*>(0x3001);
mockFinalCtx = reinterpret_cast<PangoContext*>(0x3002);
mockFinalLayout = reinterpret_cast<PangoLayout*>(0x3003);
// Mock font description
mockFontDesc = reinterpret_cast<PangoFontDescription*>(0x2004);
// Default layout size: 100x30 pixels (in PANGO_SCALE units)
defaultLayoutWidth = 100;
defaultLayoutHeight = 30;
SetupDefaultBehaviors();
}
void SetupDefaultBehaviors() {
// Font map
ON_CALL(*mockWrapper, pango_cairo_font_map_new())
.WillByDefault(Return(reinterpret_cast<PangoFontMap*>(0x1000)));
// Temporary surface creation (for size calculation)
ON_CALL(*mockWrapper, cairo_image_surface_create(CAIRO_FORMAT_A8, 1, 1)).WillByDefault(Return(mockTempSurface));
ON_CALL(*mockWrapper, cairo_create(mockTempSurface)).WillByDefault(Return(mockTempCr));
ON_CALL(*mockWrapper, pango_cairo_create_context(mockTempCr)).WillByDefault(Return(mockTempCtx));
ON_CALL(*mockWrapper, pango_layout_new(mockTempCtx)).WillByDefault(Return(mockTempLayout));
// Final surface creation (for rendering) - use _ for width since it varies
ON_CALL(*mockWrapper, cairo_image_surface_create(CAIRO_FORMAT_A8, _, _))
.WillByDefault(Return(mockFinalSurface));
ON_CALL(*mockWrapper, cairo_create(mockFinalSurface)).WillByDefault(Return(mockFinalCr));
ON_CALL(*mockWrapper, pango_cairo_create_context(mockFinalCr)).WillByDefault(Return(mockFinalCtx));
ON_CALL(*mockWrapper, pango_layout_new(mockFinalCtx)).WillByDefault(Return(mockFinalLayout));
// Font description
ON_CALL(*mockWrapper, pango_font_description_new()).WillByDefault(Return(mockFontDesc));
// Layout size - return default dimensions
ON_CALL(*mockWrapper, pango_layout_get_size(_, _, _))
.WillByDefault(DoAll(SetArgPointee<1>(defaultLayoutWidth * PANGO_SCALE),
SetArgPointee<2>(defaultLayoutHeight * PANGO_SCALE)));
// Surface status - always success
ON_CALL(*mockWrapper, cairo_surface_status(_)).WillByDefault(Return(CAIRO_STATUS_SUCCESS));
// Surface properties for getRaw()
ON_CALL(*mockWrapper, cairo_image_surface_get_format(_)).WillByDefault(Return(CAIRO_FORMAT_A8));
ON_CALL(*mockWrapper, cairo_image_surface_get_width(_)).WillByDefault(Return(defaultLayoutWidth));
ON_CALL(*mockWrapper, cairo_image_surface_get_height(_)).WillByDefault(Return(128));
ON_CALL(*mockWrapper, cairo_image_surface_get_stride(_)).WillByDefault(Return(defaultLayoutWidth));
// Mock data pointer
mockSurfaceData.resize(defaultLayoutWidth * 128, 0xFF);
ON_CALL(*mockWrapper, cairo_image_surface_get_data(_)).WillByDefault(Return(mockSurfaceData.data()));
}
// Helper method to set custom layout dimensions
void SetLayoutSize(int width, int height) {
defaultLayoutWidth = width;
defaultLayoutHeight = height;
// Update the mock to return new dimensions
ON_CALL(*mockWrapper, pango_layout_get_size(_, _, _))
.WillByDefault(DoAll(SetArgPointee<1>(width * PANGO_SCALE), SetArgPointee<2>(height * PANGO_SCALE)));
ON_CALL(*mockWrapper, cairo_image_surface_get_width(_)).WillByDefault(Return(width));
ON_CALL(*mockWrapper, cairo_image_surface_get_stride(_)).WillByDefault(Return(width));
// Resize mock data
mockSurfaceData.resize(width * 128, 0xFF);
ON_CALL(*mockWrapper, cairo_image_surface_get_data(_)).WillByDefault(Return(mockSurfaceData.data()));
}
std::shared_ptr<NiceMock<MockCairoWrapper>> mockWrapper;
// Mock pointers
cairo_surface_t* mockTempSurface;
cairo_t* mockTempCr;
PangoContext* mockTempCtx;
PangoLayout* mockTempLayout;
cairo_surface_t* mockFinalSurface;
cairo_t* mockFinalCr;
PangoContext* mockFinalCtx;
PangoLayout* mockFinalLayout;
PangoFontDescription* mockFontDesc;
// Default layout dimensions
int defaultLayoutWidth;
int defaultLayoutHeight;
// Mock surface data
std::vector<unsigned char> mockSurfaceData;
};
// Smoke test with real Cairo/Pango
TEST(basic_test, Label_smokeTest_succeeds) { TEST(basic_test, Label_smokeTest_succeeds) {
auto label = Label(128); auto im = ptprnt::graphics::Label(4711);
EXPECT_EQ(label.getHeight(), 128);
EXPECT_EQ(label.getWidth(), 0); // No label created yet
} }
// Constructor test with mock
TEST_F(LabelTest, Constructor_InitializesFontMap) {
EXPECT_CALL(*mockWrapper, pango_cairo_font_map_new()).Times(1);
auto label = Label(128, mockWrapper);
EXPECT_EQ(label.getHeight(), 128);
EXPECT_EQ(label.getWidth(), 0);
}
// Test getters before label creation
TEST_F(LabelTest, Getters_BeforeCreate_ReturnDefaults) {
auto label = Label(256, mockWrapper);
EXPECT_EQ(label.getHeight(), 256);
EXPECT_EQ(label.getWidth(), 0);
}
// Test setters
TEST_F(LabelTest, Setters_ModifyProperties) {
auto label = Label(128, mockWrapper);
label.setFontSize(24.0);
label.setFontFamily("Arial");
label.setText("Test");
label.setHAlign(HAlignPosition::CENTER);
label.setVAlign(VAlignPosition::BOTTOM);
// Properties are set (no way to verify without create, but no crash is good)
SUCCEED();
}
// Test create() - basic functionality with simplified setup
TEST_F(LabelTest, Create_WithText_Succeeds) {
auto label = Label(128, mockWrapper);
label.setFontSize(12.0);
label.setFontFamily("Sans");
bool result = label.create("Hello");
EXPECT_TRUE(result);
EXPECT_EQ(label.getWidth(), defaultLayoutWidth);
EXPECT_EQ(label.getHeight(), 128);
}
// Test horizontal alignment - RIGHT
TEST_F(LabelTest, Create_WithRightAlignment_SetsCorrectPangoAlignment) {
auto label = Label(128, mockWrapper);
label.setHAlign(HAlignPosition::RIGHT);
// Verify RIGHT alignment is set (temp + final layout)
EXPECT_CALL(*mockWrapper, pango_layout_set_alignment(_, PANGO_ALIGN_RIGHT)).Times(2);
label.create("Right");
}
// Test horizontal alignment - JUSTIFY
TEST_F(LabelTest, Create_WithJustifyAlignment_SetsJustifyAndAlignment) {
auto label = Label(128, mockWrapper);
label.setHAlign(HAlignPosition::JUSTIFY);
// Verify JUSTIFY requires LEFT alignment + justify flag
EXPECT_CALL(*mockWrapper, pango_layout_set_alignment(_, PANGO_ALIGN_LEFT)).Times(2);
EXPECT_CALL(*mockWrapper, pango_layout_set_justify(_, true)).Times(2);
#if PANGO_VERSION_MAJOR >= 1 && PANGO_VERSION_MINOR >= 50
EXPECT_CALL(*mockWrapper, pango_layout_set_justify_last_line(_, true)).Times(2);
#endif
label.create("Justify");
}
// Test vertical alignment - TOP (no cairo_move_to)
TEST_F(LabelTest, Create_WithTopAlignment_NoMoveToCall) {
auto label = Label(128, mockWrapper);
label.setVAlign(VAlignPosition::TOP);
// TOP alignment should NOT call cairo_move_to
EXPECT_CALL(*mockWrapper, cairo_move_to(_, _, _)).Times(0);
label.create("Top");
}
// Test vertical alignment - BOTTOM
TEST_F(LabelTest, Create_WithBottomAlignment_CallsMoveToWithCorrectOffset) {
auto label = Label(128, mockWrapper);
label.setVAlign(VAlignPosition::BOTTOM);
SetLayoutSize(50, 20); // Use helper to set custom size
// BOTTOM alignment: offset = printerHeight - layoutHeight = 128 - 20 = 108
EXPECT_CALL(*mockWrapper, cairo_move_to(mockFinalCr, 0.0, 108.0)).Times(1);
label.create("Bottom");
}
// Test vertical alignment - MIDDLE
TEST_F(LabelTest, Create_WithMiddleAlignment_CallsMoveToWithCenteredOffset) {
auto label = Label(128, mockWrapper);
label.setVAlign(VAlignPosition::MIDDLE);
SetLayoutSize(50, 20); // Use helper to set custom size
// MIDDLE alignment: offset = (printerHeight - layoutHeight) / 2 = (128 - 20) / 2 = 54
EXPECT_CALL(*mockWrapper, cairo_move_to(mockFinalCr, 0.0, 54.0)).Times(1);
label.create("Middle");
}
} // namespace ptprnt::graphics

View File

@@ -1,56 +1,40 @@
# Consolidated test binary - all tests in one executable for faster linking tests = [
[
test_sources = [ 'bitmap_test',
# Test files 'bitmap_test_exe',
'bitmap_test/bitmap_test.cpp', ['../src/graphics/Bitmap.cpp', 'bitmap_test/bitmap_test.cpp'],
'monochrome_test/monochrome_test.cpp', ],
'label_test/label_test.cpp', [
'label_builder_test/label_builder_test.cpp', 'label_test',
'printer_service_test/printer_service_test.cpp', 'label_test_exe',
'p700_printer_test/p700_printer_test.cpp', ['../src/graphics/Label.cpp', 'label_test/label_test.cpp'],
'fake_printer_test/fake_printer_test.cpp', ],
'cli_parser_test/cli_parser_test.cpp', [
'ptouch_print_test/ptouch_print_test.cpp', 'monochrome_test',
'usb_device_test/usb_device_test.cpp', 'monochrome_test_exe',
[
# Source files under test - graphics '../src/graphics/Monochrome.cpp',
'../src/graphics/Bitmap.cpp', 'monochrome_test/monochrome_test.cpp',
'../src/graphics/Monochrome.cpp', ],
'../src/graphics/Label.cpp', ],
'../src/graphics/LabelBuilder.cpp',
# Source files under test - core
'../src/core/PrinterService.cpp',
'../src/core/PrinterDriverFactory.cpp',
# Source files under test - printers
'../src/printers/P700Printer.cpp',
'../src/printers/FakePrinter.cpp',
# Source files under test - CLI
'../src/cli/CliParser.cpp',
# Source files under test - Main app
'../src/PtouchPrint.cpp',
# Source files under test - USB
'../src/libusbwrap/LibUsbWrapper.cpp',
'../src/libusbwrap/UsbDevice.cpp',
'../src/libusbwrap/UsbDeviceFactory.cpp',
] ]
test_exe = executable( foreach test : tests
'ptprnt_tests', test(
sources: test_sources, test.get(0),
include_directories: incdir, executable(
dependencies: [ test.get(1),
gmock_dep, # GMock includes GTest sources: test.get(2),
usb_dep, include_directories: incdir,
log_dep, dependencies: [
pangocairo_dep, gtest_dep,
cli11_dep, usb_dep,
], log_dep,
) pangocairo_dep,
cli11_dep,
],
),
)
endforeach
# Single test that runs all test suites
test('all_tests', test_exe)

View File

@@ -1,84 +0,0 @@
/*
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 <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <gmock/gmock.h>
#include "graphics/interface/ICairoWrapper.hpp"
namespace ptprnt::graphics {
/**
* @brief GMock implementation of ICairoWrapper for unit testing
*
* This mock allows tests to verify that the Label class correctly interacts
* with the Cairo/Pango API without requiring actual graphics rendering.
*/
class MockCairoWrapper : public ICairoWrapper {
public:
// Cairo image surface functions
MOCK_METHOD(cairo_surface_t*, cairo_image_surface_create, (cairo_format_t format, int width, int height),
(override));
MOCK_METHOD(void, cairo_surface_destroy, (cairo_surface_t * surface), (override));
MOCK_METHOD(void, cairo_surface_flush, (cairo_surface_t * surface), (override));
MOCK_METHOD(void, cairo_surface_mark_dirty, (cairo_surface_t * surface), (override));
MOCK_METHOD(cairo_status_t, cairo_surface_status, (cairo_surface_t * surface), (override));
MOCK_METHOD(cairo_format_t, cairo_image_surface_get_format, (cairo_surface_t * surface), (override));
MOCK_METHOD(int, cairo_image_surface_get_width, (cairo_surface_t * surface), (override));
MOCK_METHOD(int, cairo_image_surface_get_height, (cairo_surface_t * surface), (override));
MOCK_METHOD(int, cairo_image_surface_get_stride, (cairo_surface_t * surface), (override));
MOCK_METHOD(unsigned char*, cairo_image_surface_get_data, (cairo_surface_t * surface), (override));
MOCK_METHOD(cairo_status_t, cairo_surface_write_to_png, (cairo_surface_t * surface, const char* filename),
(override));
// Cairo context functions
MOCK_METHOD(cairo_t*, cairo_create, (cairo_surface_t * surface), (override));
MOCK_METHOD(void, cairo_destroy, (cairo_t * cr), (override));
MOCK_METHOD(void, cairo_move_to, (cairo_t * cr, double x, double y), (override));
MOCK_METHOD(void, cairo_set_source_rgb, (cairo_t * cr, double red, double green, double blue), (override));
// Pango-Cairo functions
MOCK_METHOD(PangoFontMap*, pango_cairo_font_map_new, (), (override));
MOCK_METHOD(PangoContext*, pango_cairo_create_context, (cairo_t * cr), (override));
MOCK_METHOD(void, pango_cairo_show_layout, (cairo_t * cr, PangoLayout* layout), (override));
// Pango layout functions
MOCK_METHOD(PangoLayout*, pango_layout_new, (PangoContext * context), (override));
MOCK_METHOD(void, pango_layout_set_font_description, (PangoLayout * layout, const PangoFontDescription* desc),
(override));
MOCK_METHOD(void, pango_layout_set_text, (PangoLayout * layout, const char* text, int length), (override));
MOCK_METHOD(void, pango_layout_set_height, (PangoLayout * layout, int height), (override));
MOCK_METHOD(void, pango_layout_set_alignment, (PangoLayout * layout, PangoAlignment alignment), (override));
MOCK_METHOD(void, pango_layout_set_justify, (PangoLayout * layout, gboolean justify), (override));
#if PANGO_VERSION_MAJOR >= 1 && PANGO_VERSION_MINOR >= 50
MOCK_METHOD(void, pango_layout_set_justify_last_line, (PangoLayout * layout, gboolean justify), (override));
#endif
MOCK_METHOD(void, pango_layout_get_size, (PangoLayout * layout, int* width, int* height), (override));
// Pango font description functions
MOCK_METHOD(PangoFontDescription*, pango_font_description_new, (), (override));
MOCK_METHOD(void, pango_font_description_set_size, (PangoFontDescription * desc, gint size), (override));
MOCK_METHOD(void, pango_font_description_set_family, (PangoFontDescription * desc, const char* family),
(override));
// GObject reference counting
MOCK_METHOD(void, g_object_unref, (gpointer object), (override));
};
} // namespace ptprnt::graphics

View File

@@ -1,76 +0,0 @@
/*
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 <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <gmock/gmock.h>
#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

View File

@@ -1,50 +0,0 @@
/*
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 <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <gmock/gmock.h>
#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<libusbwrap::IUsbDevice> usbHndl), (override));
MOCK_METHOD(bool, detachUsbDevice, (), (override));
MOCK_METHOD(bool, printBitmap, (const graphics::Bitmap<graphics::ALPHA8>& bitmap), (override));
MOCK_METHOD(bool, printMonochromeData, (const graphics::MonochromeData& data), (override));
MOCK_METHOD(bool, printLabel, (const std::unique_ptr<graphics::ILabel> label), (override));
MOCK_METHOD(bool, print, (), (override));
};
} // namespace ptprnt

View File

@@ -1,57 +0,0 @@
/*
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 <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <gmock/gmock.h>
#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<uint8_t>& 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

View File

@@ -1,41 +0,0 @@
/*
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 <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <gmock/gmock.h>
#include "libusbwrap/interface/IUsbDeviceFactory.hpp"
namespace libusbwrap {
/**
* @brief GMock implementation of IUsbDeviceFactory for unit testing
*
* This mock allows tests to control USB device discovery without
* requiring actual libusb context or hardware.
*/
class MockUsbDeviceFactory : public IUsbDeviceFactory {
public:
MOCK_METHOD(bool, init, (), (override));
MOCK_METHOD(std::vector<std::unique_ptr<IUsbDevice>>, findAllDevices, (), (override));
MOCK_METHOD(std::vector<std::unique_ptr<IUsbDevice>>, findDevices, (uint16_t vid, uint16_t pid), (override));
};
} // namespace libusbwrap

View File

@@ -1,6 +1,6 @@
/* /*
ptrnt - print labels on linux ptrnt - print labels on linux
Copyright (C) 2023-2025 Moritz Martinius Copyright (C) 2023 Moritz Martinius
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU General Public License as published by
@@ -57,6 +57,9 @@ TEST(basic_test, Monochrome_convertWithCustomThreshhold_yieldsMonochromeRespecti
} }
TEST(basic_test, Monochrome_convertNonAlignedPixels_spillsOverIntoNewByte) { 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<uint8_t> pixels( const std::vector<uint8_t> pixels(
{0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF}); {0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF});

View File

@@ -1,169 +0,0 @@
/*
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 <https://www.gnu.org/licenses/>.
*/
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <memory>
#include "graphics/Bitmap.hpp"
#include "graphics/Monochrome.hpp"
#include "mocks/MockUsbDevice.hpp"
#include "printers/P700Printer.hpp"
using ::testing::_;
using ::testing::DoAll;
using ::testing::NiceMock;
using ::testing::Return;
using ::testing::SetArgPointee;
namespace ptprnt::printer {
// Test fixture for P700Printer tests
class P700PrinterTest : public ::testing::Test {
protected:
void SetUp() override {
printer = std::make_unique<P700Printer>();
mockUsbDev = std::make_shared<NiceMock<libusbwrap::MockUsbDevice>>();
// 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<P700Printer> printer;
std::shared_ptr<libusbwrap::MockUsbDevice> 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<graphics::ALPHA8> 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<uint8_t>(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<graphics::ILabel> label = nullptr;
bool result = printer->printLabel(std::move(label));
EXPECT_FALSE(result);
}
} // namespace ptprnt::printer

View File

@@ -1,128 +0,0 @@
/*
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 <https://www.gnu.org/licenses/>.
*/
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <memory>
#include <vector>
#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<std::unique_ptr<libusbwrap::IUsbDevice>> returnEmptyDeviceList() {
return {};
}
// Test fixture for PrinterService tests
class PrinterServiceTest : public ::testing::Test {
protected:
void SetUp() override {
// Create mock USB device factory
auto mockFactory = std::make_unique<NiceMock<libusbwrap::MockUsbDeviceFactory>>();
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<PrinterService>(std::move(mockFactory));
}
void TearDown() override { service.reset(); }
std::unique_ptr<PrinterService> service;
libusbwrap::MockUsbDeviceFactory* mockFactoryPtr; // Non-owning pointer for expectations
};
// Test: PrinterService initialization
TEST_F(PrinterServiceTest, InitializeSuccess) {
EXPECT_CALL(*mockFactoryPtr, init()).WillOnce(Return(true));
EXPECT_TRUE(service->initialize());
}
// Test: Detect printers when none are connected
TEST_F(PrinterServiceTest, DetectPrintersNoneFound) {
service->initialize();
// Mock returns empty device list - no real USB enumeration happens
EXPECT_CALL(*mockFactoryPtr, findAllDevices()).WillOnce(Invoke(returnEmptyDeviceList));
auto printers = service->detectPrinters();
// Should get empty list since mock returns no devices
EXPECT_EQ(printers.size(), 0);
}
// Test: Select printer with auto-detect when none found
TEST_F(PrinterServiceTest, SelectPrinterAutoNoneFound) {
service->initialize();
// Mock returns empty device list
EXPECT_CALL(*mockFactoryPtr, findAllDevices()).WillOnce(Invoke(returnEmptyDeviceList));
auto printer = service->selectPrinter("auto");
// Should be nullptr since no printers detected
EXPECT_EQ(printer, nullptr);
EXPECT_EQ(service->getCurrentPrinter(), nullptr);
}
// Test: Select non-existent printer by name
TEST_F(PrinterServiceTest, SelectPrinterByNameNotFound) {
service->initialize();
// This test doesn't need USB mocking since selectPrinter("name")
// uses PrinterDriverFactory directly, not USB enumeration
auto printer = service->selectPrinter("NonExistentPrinter");
EXPECT_EQ(printer, nullptr);
EXPECT_EQ(service->getCurrentPrinter(), nullptr);
}
// Test: Get current printer when none selected
TEST_F(PrinterServiceTest, GetCurrentPrinterNoneSelected) {
EXPECT_EQ(service->getCurrentPrinter(), nullptr);
}
// Test: Print label without selecting printer
TEST_F(PrinterServiceTest, PrintLabelNoPrinterSelected) {
// Create a simple label (we need a valid ILabel pointer)
std::unique_ptr<graphics::ILabel> 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

View File

@@ -1,374 +0,0 @@
/*
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 <https://www.gnu.org/licenses/>.
*/
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#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<std::shared_ptr<IPrinterDriver>>, detectPrinters, (), (override));
MOCK_METHOD(std::shared_ptr<IPrinterDriver>, selectPrinter, (const std::string& selection), (override));
MOCK_METHOD(std::shared_ptr<IPrinterDriver>, getCurrentPrinter, (), (const, override));
MOCK_METHOD(bool, printLabel, (std::unique_ptr<graphics::ILabel> 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<libusbwrap::IUsbDevice> usbHndl), (override));
MOCK_METHOD(bool, detachUsbDevice, (), (override));
MOCK_METHOD(bool, printBitmap, (const graphics::Bitmap<graphics::ALPHA8>& bitmap), (override));
MOCK_METHOD(bool, printMonochromeData, (const graphics::MonochromeData& data), (override));
MOCK_METHOD(bool, printLabel, (std::unique_ptr<graphics::ILabel> label), (override));
MOCK_METHOD(bool, print, (), (override));
};
class PtouchPrintTest : public ::testing::Test {
protected:
void SetUp() override {
mockCliParser = std::make_unique<NiceMock<MockCliParser>>();
mockPrinterService = std::make_unique<NiceMock<MockPrinterService>>();
// 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> mockCliParser;
std::unique_ptr<MockPrinterService> 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<NiceMock<MockPrinterDriver>>();
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<NiceMock<MockPrinterDriver>>();
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<NiceMock<MockPrinterDriver>>();
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<NiceMock<MockPrinterDriver>>();
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<NiceMock<MockPrinterDriver>>();
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<NiceMock<MockPrinterDriver>>();
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<NiceMock<MockPrinterDriver>>();
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<NiceMock<MockPrinterDriver>>();
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

View File

@@ -1,276 +0,0 @@
/*
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 <https://www.gnu.org/licenses/>.
*/
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <memory>
#include "libusb.h"
#include "libusbwrap/LibUsbTypes.hpp"
#include "libusbwrap/UsbDevice.hpp"
#include "mocks/MockLibUsbWrapper.hpp"
using ::testing::_;
using ::testing::DoAll;
using ::testing::NiceMock;
using ::testing::Return;
using ::testing::SetArgPointee;
namespace libusbwrap {
// Test fixture for UsbDevice tests
class UsbDeviceTest : public ::testing::Test {
protected:
void SetUp() override {
mockLibUsb = std::make_shared<NiceMock<MockLibUsbWrapper>>();
// Create mock device pointer
mockDevice = reinterpret_cast<libusb_device*>(0x1000);
// Create mock device handle
mockHandle = reinterpret_cast<libusb_device_handle*>(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<MockLibUsbWrapper> 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<UsbDevice>(mockDevice, desc, mockLibUsb);
EXPECT_NE(device, nullptr);
});
}
TEST_F(UsbDeviceTest, ConstructorWithNullptrThrows) {
EXPECT_THROW({ auto device = std::make_unique<UsbDevice>(nullptr, desc, mockLibUsb); }, std::invalid_argument);
}
// Open/Close tests
TEST_F(UsbDeviceTest, OpenSuccess) {
auto device = std::make_unique<UsbDevice>(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<UsbDevice>(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<UsbDevice>(mockDevice, desc, mockLibUsb);
device->open();
EXPECT_CALL(*mockLibUsb, close(mockHandle)).Times(1);
device->close();
}
TEST_F(UsbDeviceTest, CloseWithoutOpenDevice) {
auto device = std::make_unique<UsbDevice>(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<UsbDevice>(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<UsbDevice>(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<UsbDevice>(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<UsbDevice>(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<UsbDevice>(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<UsbDevice>(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<UsbDevice>(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<UsbDevice>(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<UsbDevice>(mockDevice, desc, mockLibUsb);
device->open();
std::vector<uint8_t> 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<UsbDevice>(mockDevice, desc, mockLibUsb);
device->open();
std::vector<uint8_t> 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<UsbDevice>(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<UsbDevice>(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<UsbDevice>(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<UsbDevice>(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<UsbDevice>(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<UsbDevice>(mockDevice, desc, mockLibUsb);
EXPECT_CALL(*mockLibUsb, errorName(static_cast<int>(Error::SUCCESS))).WillOnce(Return("LIBUSB_SUCCESS"));
EXPECT_EQ(device->getLastErrorString(), "LIBUSB_SUCCESS");
}
} // namespace libusbwrap