From e279ad5ad910634a3d74871db840d644877d3463 Mon Sep 17 00:00:00 2001 From: Mohammad Arman Date: Tue, 16 Jun 2026 05:18:03 +0000 Subject: [PATCH 01/12] cmd/mfp-test: add directory structure, doc.go and Makefiles --- cmd/mfp-test/Makefile | 4 ++++ cmd/mfp-test/main.go | 12 ++++++++++++ cmd/mfp-test/test/Makefile | 1 + cmd/mfp-test/test/doc.go | 12 ++++++++++++ 4 files changed, 29 insertions(+) create mode 100644 cmd/mfp-test/Makefile create mode 100644 cmd/mfp-test/main.go create mode 100644 cmd/mfp-test/test/Makefile create mode 100644 cmd/mfp-test/test/doc.go diff --git a/cmd/mfp-test/Makefile b/cmd/mfp-test/Makefile new file mode 100644 index 00000000..99bc5e89 --- /dev/null +++ b/cmd/mfp-test/Makefile @@ -0,0 +1,4 @@ +SUBDIRS = test +CLEAN = mfp-test + +include ../../Rules.mak diff --git a/cmd/mfp-test/main.go b/cmd/mfp-test/main.go new file mode 100644 index 00000000..ed2d82d7 --- /dev/null +++ b/cmd/mfp-test/main.go @@ -0,0 +1,12 @@ +// MFP - Multi-Function Printers and scanners toolkit +// cmd/mfp-test - Print system testing pipeline +// +// Copyright (C) 2026 Mohammad Arman (officialmdarman@gmail.com) +// See LICENSE for license terms and conditions +// +// The main() function. + +package main + +func main() { +} diff --git a/cmd/mfp-test/test/Makefile b/cmd/mfp-test/test/Makefile new file mode 100644 index 00000000..e7eb5b5e --- /dev/null +++ b/cmd/mfp-test/test/Makefile @@ -0,0 +1 @@ +include ../../../Rules.mak diff --git a/cmd/mfp-test/test/doc.go b/cmd/mfp-test/test/doc.go new file mode 100644 index 00000000..13815b2f --- /dev/null +++ b/cmd/mfp-test/test/doc.go @@ -0,0 +1,12 @@ +// MFP - Multi-Function Printers and scanners toolkit +// The "mfp-test" command +// +// Copyright (C) 2026 Mohammad Arman (officialmdarman@gmail.com) +// See LICENSE for license terms and conditions +// +// Package documentation + +// Package test implements the print system testing pipeline. +// It provides document capture, CUPS queue management, and +// test result reporting for the mfp-test command. +package test From d86cfbe932af55de50e9a0264b7f83193f48e30c Mon Sep 17 00:00:00 2001 From: Mohammad Arman Date: Tue, 16 Jun 2026 05:28:43 +0000 Subject: [PATCH 02/12] cmd/mfp-test: add DocumentCapture for collecting printed documents --- cmd/mfp-test/test/capture.go | 87 ++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 cmd/mfp-test/test/capture.go diff --git a/cmd/mfp-test/test/capture.go b/cmd/mfp-test/test/capture.go new file mode 100644 index 00000000..9d9e883b --- /dev/null +++ b/cmd/mfp-test/test/capture.go @@ -0,0 +1,87 @@ +// MFP - Multi-Function Printers and scanners toolkit +// The "mfp-test" command +// +// Copyright (C) 2026 Mohammad Arman (officialmdarman@gmail.com) +// See LICENSE for license terms and conditions +// +// Document capture + +package test + +import ( + "io" + "sync" + + "github.com/OpenPrinting/go-mfp/abstract" +) + +// CapturedDoc holds a single captured print document +// with its negotiated job parameters and raw bytes. +type CapturedDoc struct { + Params abstract.PrinterRequest + Data []byte +} + +// DocumentCapture implements abstract.Printer and collects +// all incoming print documents for later inspection. +// It is safe for concurrent use. +type DocumentCapture struct { + mu sync.Mutex + docs []CapturedDoc + done chan struct{} +} + +// NewDocumentCapture creates a new DocumentCapture. +func NewDocumentCapture() *DocumentCapture { + return &DocumentCapture{ + done: make(chan struct{}), + } +} + +// PrintDocument implements abstract.Printer. +// It reads the full document body and stores it along with params. +func (dc *DocumentCapture) PrintDocument( + params abstract.PrinterRequest, body io.Reader) error { + + data, err := io.ReadAll(body) + if err != nil { + return err + } + + dc.mu.Lock() + dc.docs = append(dc.docs, CapturedDoc{ + Params: params, + Data: data, + }) + dc.mu.Unlock() + + // Signal that at least one document has arrived. + select { + case <-dc.done: + default: + close(dc.done) + } + + return nil +} + +// OnDocument returns a channel that is closed when the first +// document is received. Useful for waiting without polling. +func (dc *DocumentCapture) OnDocument() <-chan struct{} { + return dc.done +} + +// Wait blocks until at least one document has been captured. +func (dc *DocumentCapture) Wait() { + <-dc.done +} + +// Docs returns a snapshot of all captured documents so far. +func (dc *DocumentCapture) Docs() []CapturedDoc { + dc.mu.Lock() + defer dc.mu.Unlock() + + out := make([]CapturedDoc, len(dc.docs)) + copy(out, dc.docs) + return out +} From 4c5df679549e9c5ec251af9e5ff99e8eae5651bb Mon Sep 17 00:00:00 2001 From: Mohammad Arman Date: Tue, 16 Jun 2026 05:36:06 +0000 Subject: [PATCH 03/12] cmd/mfp-test: add CUPS queue management --- cmd/mfp-test/test/cupsctl.go | 53 ++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 cmd/mfp-test/test/cupsctl.go diff --git a/cmd/mfp-test/test/cupsctl.go b/cmd/mfp-test/test/cupsctl.go new file mode 100644 index 00000000..71f56aba --- /dev/null +++ b/cmd/mfp-test/test/cupsctl.go @@ -0,0 +1,53 @@ +// MFP - Multi-Function Printers and scanners toolkit +// The "mfp-test" command +// +// Copyright (C) 2026 Mohammad Arman (officialmdarman@gmail.com) +// See LICENSE for license terms and conditions +// +// CUPS queue management + +package test + +import ( + "context" + "fmt" + "os/exec" +) + +// CreateCUPSQueue creates a CUPS printer queue named name, +// pointing to the virtual IPP printer at ippURL. +// +// It uses lpadmin to register the queue with the IPP Everywhere +// driver, which works with any standards-compliant IPP printer. +func CreateCUPSQueue(ctx context.Context, name, ippURL string) error { + cmd := exec.CommandContext(ctx, + "lpadmin", + "-p", name, + "-E", + "-v", ippURL, + "-m", "everywhere", + ) + + out, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("lpadmin -p %s: %w: %s", name, err, out) + } + + return nil +} + +// RemoveCUPSQueue removes the CUPS printer queue named name. +// It is safe to call even if the queue does not exist. +func RemoveCUPSQueue(ctx context.Context, name string) error { + cmd := exec.CommandContext(ctx, + "lpadmin", + "-x", name, + ) + + out, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("lpadmin -x %s: %w: %s", name, err, out) + } + + return nil +} From dbc7a348b700073a8d2170afefcad835c1ccd29e Mon Sep 17 00:00:00 2001 From: Mohammad Arman Date: Tue, 16 Jun 2026 06:00:31 +0000 Subject: [PATCH 04/12] cmd/mfp-test: add CLI flags and command skeleton --- cmd/mfp-test/main.go | 3 + cmd/mfp-test/test/command.go | 109 +++++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+) create mode 100644 cmd/mfp-test/test/command.go diff --git a/cmd/mfp-test/main.go b/cmd/mfp-test/main.go index ed2d82d7..2aecab84 100644 --- a/cmd/mfp-test/main.go +++ b/cmd/mfp-test/main.go @@ -8,5 +8,8 @@ package main +import "github.com/OpenPrinting/go-mfp/cmd/mfp-test/test" + func main() { + test.Command.Main(nil) } diff --git a/cmd/mfp-test/test/command.go b/cmd/mfp-test/test/command.go new file mode 100644 index 00000000..91cfa38f --- /dev/null +++ b/cmd/mfp-test/test/command.go @@ -0,0 +1,109 @@ +// MFP - Multi-Function Printers and scanners toolkit +// +// Copyright (C) 2026 Mohammad Arman (officialmdarman@gmail.com) +// See LICENSE for license terms and conditions +// +// mfp-test command definition + +package test + +import ( + "context" + "fmt" + + "github.com/OpenPrinting/go-mfp/argv" + "github.com/OpenPrinting/go-mfp/log" +) + +// DefaultTCPPort is the default IPP server TCP port. +const DefaultTCPPort = 60000 + +// DefaultQueueName is the default CUPS queue name. +const DefaultQueueName = "mfp-test" + +// Command is the mfp-test command description. +var Command = argv.Command{ + Name: "mfp-test", + Help: "Print system testing pipeline", + Options: []argv.Option{ + { + Name: "-m", + Aliases: []string{"--model"}, + Help: "printer model file", + HelpArg: "file", + Singleton: true, + Validate: argv.ValidateAny, + Complete: argv.CompleteOSPath, + }, + { + Name: "-P", + Aliases: []string{"--port"}, + Help: fmt.Sprintf("IPP server TCP port (default %d)", DefaultTCPPort), + HelpArg: "port", + Singleton: true, + Validate: argv.ValidateUint16, + }, + { + Name: "-n", + Aliases: []string{"--name"}, + Help: fmt.Sprintf("CUPS queue name (default %q)", DefaultQueueName), + HelpArg: "name", + Singleton: true, + Validate: argv.ValidateAny, + }, + { + Name: "-o", + Aliases: []string{"--output"}, + Help: "write JSON report to file", + HelpArg: "file", + Singleton: true, + Validate: argv.ValidateAny, + Complete: argv.CompleteOSPath, + }, + { + Name: "--threshold", + Help: "minimum similarity score to pass (0.0-1.0, default 0.95)", + HelpArg: "score", + Singleton: true, + Validate: argv.ValidateAny, + }, + { + Name: "--list", + Help: "list all test configurations and exit", + }, + { + Name: "--batch", + Help: "run all test configurations", + Singleton: true, + }, + { + Name: "--single", + Help: "run a single test configuration by name", + HelpArg: "name", + Singleton: true, + Validate: argv.ValidateAny, + }, + { + Name: "-v", + Aliases: []string{"--verbose"}, + Help: "enable verbose output", + }, + argv.HelpOption, + }, + Handler: cmdTestHandler, +} + +// cmdTestHandler is the top-level handler for the mfp-test command. +func cmdTestHandler(ctx context.Context, inv *argv.Invocation) error { + level := log.LevelInfo + if inv.Flag("-v") { + level = log.LevelTrace + } + logger := log.NewLogger(level, log.Console) + ctx = log.NewContext(ctx, logger) + + // Tasks 11-14 will wire model loading, virtual printer, + // CUPS queue, and test execution here. + <-ctx.Done() + return nil +} From f2a85f042bbde6f8e17e292b62c9976845918bb5 Mon Sep 17 00:00:00 2001 From: Mohammad Arman Date: Wed, 17 Jun 2026 06:59:58 +0000 Subject: [PATCH 05/12] cmd/mfp-test: wire model loading --- cmd/mfp-test/test/command.go | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/cmd/mfp-test/test/command.go b/cmd/mfp-test/test/command.go index 91cfa38f..c618bad2 100644 --- a/cmd/mfp-test/test/command.go +++ b/cmd/mfp-test/test/command.go @@ -13,6 +13,7 @@ import ( "github.com/OpenPrinting/go-mfp/argv" "github.com/OpenPrinting/go-mfp/log" + "github.com/OpenPrinting/go-mfp/modeling" ) // DefaultTCPPort is the default IPP server TCP port. @@ -102,8 +103,25 @@ func cmdTestHandler(ctx context.Context, inv *argv.Invocation) error { logger := log.NewLogger(level, log.Console) ctx = log.NewContext(ctx, logger) - // Tasks 11-14 will wire model loading, virtual printer, - // CUPS queue, and test execution here. + // Model file is required: without it, NewIPPServer() returns nil. + modelfile, ok := inv.Get("-m") + if !ok { + return fmt.Errorf("model file required: use -m ") + } + + model, err := modeling.NewModel() + if err != nil { + return err + } + defer model.Close() + + if err := model.Load(modelfile); err != nil { + return fmt.Errorf("load model %q: %w", modelfile, err) + } + + // Tasks 12-14 will wire virtual printer, CUPS queue, + // and test execution here. + _ = model <-ctx.Done() return nil } From 7409c787f735868d28ff4d123f3d01ec51e0d2e5 Mon Sep 17 00:00:00 2001 From: Mohammad Arman Date: Wed, 17 Jun 2026 07:24:04 +0000 Subject: [PATCH 06/12] cmd/mfp-test: wire virtual IPP printer startup --- cmd/mfp-test/test/command.go | 44 +++++++++++++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/cmd/mfp-test/test/command.go b/cmd/mfp-test/test/command.go index c618bad2..eea65f8a 100644 --- a/cmd/mfp-test/test/command.go +++ b/cmd/mfp-test/test/command.go @@ -10,10 +10,13 @@ package test import ( "context" "fmt" + "net" + "strconv" "github.com/OpenPrinting/go-mfp/argv" "github.com/OpenPrinting/go-mfp/log" "github.com/OpenPrinting/go-mfp/modeling" + "github.com/OpenPrinting/go-mfp/transport" ) // DefaultTCPPort is the default IPP server TCP port. @@ -119,9 +122,44 @@ func cmdTestHandler(ctx context.Context, inv *argv.Invocation) error { return fmt.Errorf("load model %q: %w", modelfile, err) } - // Tasks 12-14 will wire virtual printer, CUPS queue, - // and test execution here. - _ = model + // Parse port number + port := DefaultTCPPort + if portStr, ok := inv.Get("-P"); ok { + p, err := strconv.Atoi(portStr) + if err != nil { + return fmt.Errorf("invalid port %q: %w", portStr, err) + } + port = p + } + + // Create document capture backend + capture := NewDocumentCapture() + + // Create virtual IPP printer from model and hook capture into it + ippPrinter := model.NewIPPServer() + if ippPrinter == nil { + return fmt.Errorf("model has no IPP printer attributes") + } + ippPrinter.SetPrintBackend(capture) + + // Register IPP handler on the URL path /ipp/print + mux := transport.NewPathMux() + mux.Add("/ipp/print", ippPrinter) + + // Open TCP port and start HTTP server (IPP runs over HTTP) + addr := fmt.Sprintf("localhost:%d", port) + ln, err := net.Listen("tcp", addr) + if err != nil { + return err + } + + srvr := transport.NewServer(ctx, nil, mux) + log.Info(ctx, "virtual IPP printer at ipp://%s/ipp/print", addr) + go srvr.Serve(ln) + defer srvr.Close() + + // Task 13 will wire CUPS queue here. + _ = capture <-ctx.Done() return nil } From 0c79a65f84d86dfbf1c780b5245f918ac9d93946 Mon Sep 17 00:00:00 2001 From: Mohammad Arman Date: Wed, 17 Jun 2026 07:27:38 +0000 Subject: [PATCH 07/12] cmd/mfp-test: wire CUPS queue create and remove --- cmd/mfp-test/test/command.go | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/cmd/mfp-test/test/command.go b/cmd/mfp-test/test/command.go index eea65f8a..753d8df7 100644 --- a/cmd/mfp-test/test/command.go +++ b/cmd/mfp-test/test/command.go @@ -158,7 +158,24 @@ func cmdTestHandler(ctx context.Context, inv *argv.Invocation) error { go srvr.Serve(ln) defer srvr.Close() - // Task 13 will wire CUPS queue here. + // Get CUPS queue name + queueName := DefaultQueueName + if name, ok := inv.Get("-n"); ok { + queueName = name + } + + // Register virtual printer with CUPS + ippURL := fmt.Sprintf("ipp://localhost:%d/ipp/print", port) + if err := CreateCUPSQueue(ctx, queueName, ippURL); err != nil { + return err + } + // Use background context for cleanup: main ctx is already + // cancelled by the time deferred calls run. + defer RemoveCUPSQueue(context.Background(), queueName) + + log.Info(ctx, "CUPS queue %q ready at %s", queueName, ippURL) + + // Task 14 will wire test execution here. _ = capture <-ctx.Done() return nil From 6bc43046094eefce49540fdd5ee6d77f2fb1282f Mon Sep 17 00:00:00 2001 From: Mohammad Arman Date: Wed, 17 Jun 2026 07:30:51 +0000 Subject: [PATCH 08/12] cmd/mfp-test: add end-to-end sanity check --- cmd/mfp-test/test/command.go | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/cmd/mfp-test/test/command.go b/cmd/mfp-test/test/command.go index 753d8df7..68786ebc 100644 --- a/cmd/mfp-test/test/command.go +++ b/cmd/mfp-test/test/command.go @@ -11,7 +11,10 @@ import ( "context" "fmt" "net" + "os/exec" "strconv" + "strings" + "time" "github.com/OpenPrinting/go-mfp/argv" "github.com/OpenPrinting/go-mfp/log" @@ -175,8 +178,30 @@ func cmdTestHandler(ctx context.Context, inv *argv.Invocation) error { log.Info(ctx, "CUPS queue %q ready at %s", queueName, ippURL) - // Task 14 will wire test execution here. - _ = capture + // Send a minimal test document through the full pipeline + log.Info(ctx, "sending test document via lp...") + lpCmd := exec.CommandContext(ctx, "lp", "-d", queueName) + lpCmd.Stdin = strings.NewReader("mfp-test sanity check\n") + if out, err := lpCmd.CombinedOutput(); err != nil { + return fmt.Errorf("lp -d %s: %w: %s", queueName, err, out) + } + + // Wait for the document to arrive at the capture backend + select { + case <-capture.OnDocument(): + case <-time.After(30 * time.Second): + return fmt.Errorf("timeout: no document received after 30s") + case <-ctx.Done(): + return nil + } + + // Report what was captured + docs := capture.Docs() + for i, d := range docs { + log.Info(ctx, "captured doc %d: %d bytes, format=%q, job=%q", + i+1, len(d.Data), d.Params.Format, d.Params.JobName) + } + <-ctx.Done() return nil } From d6e60a08968d6b628c7273152344e956f43f4e22 Mon Sep 17 00:00:00 2001 From: Mohammad Arman Date: Sun, 21 Jun 2026 09:05:08 +0000 Subject: [PATCH 09/12] cmd/mfp-test: use context.WithoutCancel for CUPS queue cleanup --- cmd/mfp-test/test/command.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/mfp-test/test/command.go b/cmd/mfp-test/test/command.go index 68786ebc..41161468 100644 --- a/cmd/mfp-test/test/command.go +++ b/cmd/mfp-test/test/command.go @@ -172,9 +172,9 @@ func cmdTestHandler(ctx context.Context, inv *argv.Invocation) error { if err := CreateCUPSQueue(ctx, queueName, ippURL); err != nil { return err } - // Use background context for cleanup: main ctx is already - // cancelled by the time deferred calls run. - defer RemoveCUPSQueue(context.Background(), queueName) + // WithoutCancel preserves logging and values from ctx but + // prevents cancellation from stopping the cleanup operation. + defer RemoveCUPSQueue(context.WithoutCancel(ctx), queueName) log.Info(ctx, "CUPS queue %q ready at %s", queueName, ippURL) From 7dfe950b744da2888da277d19ed584a1d22cee66 Mon Sep 17 00:00:00 2001 From: Mohammad Arman Date: Wed, 24 Jun 2026 05:16:04 +0000 Subject: [PATCH 10/12] cmd/mfp-test: use transport.NewLoopback for in-process IPP testing --- cmd/mfp-test/test/command.go | 88 +++++++++++++++++------------------- 1 file changed, 42 insertions(+), 46 deletions(-) diff --git a/cmd/mfp-test/test/command.go b/cmd/mfp-test/test/command.go index 41161468..c1b1a0df 100644 --- a/cmd/mfp-test/test/command.go +++ b/cmd/mfp-test/test/command.go @@ -8,18 +8,18 @@ package test import ( + "bytes" "context" "fmt" - "net" - "os/exec" - "strconv" - "strings" + "net/url" "time" "github.com/OpenPrinting/go-mfp/argv" "github.com/OpenPrinting/go-mfp/log" "github.com/OpenPrinting/go-mfp/modeling" + "github.com/OpenPrinting/go-mfp/proto/ipp" "github.com/OpenPrinting/go-mfp/transport" + "github.com/OpenPrinting/go-mfp/util/optional" ) // DefaultTCPPort is the default IPP server TCP port. @@ -125,16 +125,6 @@ func cmdTestHandler(ctx context.Context, inv *argv.Invocation) error { return fmt.Errorf("load model %q: %w", modelfile, err) } - // Parse port number - port := DefaultTCPPort - if portStr, ok := inv.Get("-P"); ok { - p, err := strconv.Atoi(portStr) - if err != nil { - return fmt.Errorf("invalid port %q: %w", portStr, err) - } - port = p - } - // Create document capture backend capture := NewDocumentCapture() @@ -145,48 +135,54 @@ func cmdTestHandler(ctx context.Context, inv *argv.Invocation) error { } ippPrinter.SetPrintBackend(capture) - // Register IPP handler on the URL path /ipp/print + // Create in-process loopback — no real TCP socket needed + tr, loopback := transport.NewLoopback() + mux := transport.NewPathMux() mux.Add("/ipp/print", ippPrinter) - // Open TCP port and start HTTP server (IPP runs over HTTP) - addr := fmt.Sprintf("localhost:%d", port) - ln, err := net.Listen("tcp", addr) - if err != nil { - return err - } - srvr := transport.NewServer(ctx, nil, mux) - log.Info(ctx, "virtual IPP printer at ipp://%s/ipp/print", addr) - go srvr.Serve(ln) + log.Info(ctx, "virtual IPP printer started (in-process loopback)") + go srvr.Serve(loopback) defer srvr.Close() - // Get CUPS queue name - queueName := DefaultQueueName - if name, ok := inv.Get("-n"); ok { - queueName = name + // Send test document via in-process IPP client + log.Info(ctx, "sending test document...") + + printerURL, _ := url.Parse("ipp://loopback/ipp/print") + ippURI := "ipp://loopback/ipp/print" + client := ipp.NewClient(printerURL, tr) + + // Step 1: Create-Job + createRq := &ipp.CreateJobRequest{ + RequestHeader: ipp.DefaultRequestHeader, + JobCreateOperation: ipp.JobCreateOperation{ + PrinterURI: ippURI, + }, + Job: &ipp.JobAttributes{}, + } + createRsp := &ipp.CreateJobResponse{} + if err := client.Do(ctx, createRq, createRsp); err != nil { + return fmt.Errorf("Create-Job: %w", err) } - // Register virtual printer with CUPS - ippURL := fmt.Sprintf("ipp://localhost:%d/ipp/print", port) - if err := CreateCUPSQueue(ctx, queueName, ippURL); err != nil { - return err + // Step 2: Send-Document + sendRq := &ipp.SendDocumentRequest{ + RequestHeader: ipp.DefaultRequestHeader, + PrinterURI: optional.New(ippURI), + JobID: optional.New(createRsp.Job.JobID), + DocumentFormat: optional.New("text/plain"), + LastDocument: true, + Job: &ipp.JobAttributes{}, } - // WithoutCancel preserves logging and values from ctx but - // prevents cancellation from stopping the cleanup operation. - defer RemoveCUPSQueue(context.WithoutCancel(ctx), queueName) - - log.Info(ctx, "CUPS queue %q ready at %s", queueName, ippURL) - - // Send a minimal test document through the full pipeline - log.Info(ctx, "sending test document via lp...") - lpCmd := exec.CommandContext(ctx, "lp", "-d", queueName) - lpCmd.Stdin = strings.NewReader("mfp-test sanity check\n") - if out, err := lpCmd.CombinedOutput(); err != nil { - return fmt.Errorf("lp -d %s: %w: %s", queueName, err, out) + sendRq.Body = bytes.NewReader([]byte("mfp-test sanity check\n")) + + sendRsp := &ipp.SendDocumentResponse{} + if err := client.Do(ctx, sendRq, sendRsp); err != nil { + return fmt.Errorf("Send-Document: %w", err) } - // Wait for the document to arrive at the capture backend + // Wait for the document to arrive at capture backend select { case <-capture.OnDocument(): case <-time.After(30 * time.Second): @@ -195,7 +191,7 @@ func cmdTestHandler(ctx context.Context, inv *argv.Invocation) error { return nil } - // Report what was captured + // Report captured result docs := capture.Docs() for i, d := range docs { log.Info(ctx, "captured doc %d: %d bytes, format=%q, job=%q", From 3fa02121015c3f6aa21d6c6a2872be778b27e491 Mon Sep 17 00:00:00 2001 From: Mohammad Arman Date: Thu, 25 Jun 2026 05:08:12 +0000 Subject: [PATCH 11/12] cmd/mfp-test: use PNG test image instead of plain text --- cmd/mfp-test/test/command.go | 126 ++++++++++++++++++++++++----------- 1 file changed, 86 insertions(+), 40 deletions(-) diff --git a/cmd/mfp-test/test/command.go b/cmd/mfp-test/test/command.go index c1b1a0df..87af3c1a 100644 --- a/cmd/mfp-test/test/command.go +++ b/cmd/mfp-test/test/command.go @@ -8,18 +8,21 @@ package test import ( - "bytes" "context" "fmt" - "net/url" + "image" + "image/color" + "image/png" + "net" + "os" + "os/exec" + "strconv" "time" "github.com/OpenPrinting/go-mfp/argv" "github.com/OpenPrinting/go-mfp/log" "github.com/OpenPrinting/go-mfp/modeling" - "github.com/OpenPrinting/go-mfp/proto/ipp" "github.com/OpenPrinting/go-mfp/transport" - "github.com/OpenPrinting/go-mfp/util/optional" ) // DefaultTCPPort is the default IPP server TCP port. @@ -125,6 +128,16 @@ func cmdTestHandler(ctx context.Context, inv *argv.Invocation) error { return fmt.Errorf("load model %q: %w", modelfile, err) } + // Parse port number + port := DefaultTCPPort + if portStr, ok := inv.Get("-P"); ok { + p, err := strconv.Atoi(portStr) + if err != nil { + return fmt.Errorf("invalid port %q: %w", portStr, err) + } + port = p + } + // Create document capture backend capture := NewDocumentCapture() @@ -135,54 +148,53 @@ func cmdTestHandler(ctx context.Context, inv *argv.Invocation) error { } ippPrinter.SetPrintBackend(capture) - // Create in-process loopback — no real TCP socket needed - tr, loopback := transport.NewLoopback() - + // Register IPP handler on the URL path /ipp/print mux := transport.NewPathMux() mux.Add("/ipp/print", ippPrinter) + // Open TCP port and start HTTP server (IPP runs over HTTP) + addr := fmt.Sprintf("localhost:%d", port) + ln, err := net.Listen("tcp", addr) + if err != nil { + return err + } + srvr := transport.NewServer(ctx, nil, mux) - log.Info(ctx, "virtual IPP printer started (in-process loopback)") - go srvr.Serve(loopback) + log.Info(ctx, "virtual IPP printer at ipp://%s/ipp/print", addr) + go srvr.Serve(ln) defer srvr.Close() - // Send test document via in-process IPP client - log.Info(ctx, "sending test document...") - - printerURL, _ := url.Parse("ipp://loopback/ipp/print") - ippURI := "ipp://loopback/ipp/print" - client := ipp.NewClient(printerURL, tr) - - // Step 1: Create-Job - createRq := &ipp.CreateJobRequest{ - RequestHeader: ipp.DefaultRequestHeader, - JobCreateOperation: ipp.JobCreateOperation{ - PrinterURI: ippURI, - }, - Job: &ipp.JobAttributes{}, + // Get CUPS queue name + queueName := DefaultQueueName + if name, ok := inv.Get("-n"); ok { + queueName = name } - createRsp := &ipp.CreateJobResponse{} - if err := client.Do(ctx, createRq, createRsp); err != nil { - return fmt.Errorf("Create-Job: %w", err) + + // Register virtual printer with CUPS + ippURL := fmt.Sprintf("ipp://localhost:%d/ipp/print", port) + if err := CreateCUPSQueue(ctx, queueName, ippURL); err != nil { + return err } + // WithoutCancel preserves logging and values from ctx but + // prevents cancellation from stopping the cleanup operation. + defer RemoveCUPSQueue(context.WithoutCancel(ctx), queueName) - // Step 2: Send-Document - sendRq := &ipp.SendDocumentRequest{ - RequestHeader: ipp.DefaultRequestHeader, - PrinterURI: optional.New(ippURI), - JobID: optional.New(createRsp.Job.JobID), - DocumentFormat: optional.New("text/plain"), - LastDocument: true, - Job: &ipp.JobAttributes{}, + log.Info(ctx, "CUPS queue %q ready at %s", queueName, ippURL) + + // Generate a test PNG image and send it through the full pipeline + imgPath, err := generateTestPNG() + if err != nil { + return fmt.Errorf("generate test image: %w", err) } - sendRq.Body = bytes.NewReader([]byte("mfp-test sanity check\n")) + defer os.Remove(imgPath) - sendRsp := &ipp.SendDocumentResponse{} - if err := client.Do(ctx, sendRq, sendRsp); err != nil { - return fmt.Errorf("Send-Document: %w", err) + log.Info(ctx, "sending test PNG via lp...") + lpCmd := exec.CommandContext(ctx, "lp", "-d", queueName, imgPath) + if out, err := lpCmd.CombinedOutput(); err != nil { + return fmt.Errorf("lp -d %s: %w: %s", queueName, err, out) } - // Wait for the document to arrive at capture backend + // Wait for the document to arrive at the capture backend select { case <-capture.OnDocument(): case <-time.After(30 * time.Second): @@ -191,7 +203,7 @@ func cmdTestHandler(ctx context.Context, inv *argv.Invocation) error { return nil } - // Report captured result + // Report what was captured docs := capture.Docs() for i, d := range docs { log.Info(ctx, "captured doc %d: %d bytes, format=%q, job=%q", @@ -201,3 +213,37 @@ func cmdTestHandler(ctx context.Context, inv *argv.Invocation) error { <-ctx.Done() return nil } + +// generateTestPNG creates a temporary PNG test image with three +// horizontal colour bands (red, green, blue) and returns its path. +// The caller is responsible for removing the file after use. +func generateTestPNG() (string, error) { + const size = 300 + img := image.NewRGBA(image.Rect(0, 0, size, size)) + + for y := 0; y < size; y++ { + for x := 0; x < size; x++ { + switch { + case y < size/3: + img.Set(x, y, color.RGBA{R: 255, A: 255}) // red + case y < 2*size/3: + img.Set(x, y, color.RGBA{G: 255, A: 255}) // green + default: + img.Set(x, y, color.RGBA{B: 255, A: 255}) // blue + } + } + } + + f, err := os.CreateTemp("", "mfp-test-*.png") + if err != nil { + return "", err + } + defer f.Close() + + if err := png.Encode(f, img); err != nil { + os.Remove(f.Name()) + return "", err + } + + return f.Name(), nil +} From 6fe82be68e0aff9b9891d77b2e4fdd1c72fe7ce6 Mon Sep 17 00:00:00 2001 From: Mohammad Arman Date: Fri, 26 Jun 2026 05:06:43 +0000 Subject: [PATCH 12/12] cmd/mfp-test: exit after tests complete, always remove CUPS queue --- cmd/mfp-test/test/command.go | 1 - 1 file changed, 1 deletion(-) diff --git a/cmd/mfp-test/test/command.go b/cmd/mfp-test/test/command.go index 87af3c1a..eea7bb0b 100644 --- a/cmd/mfp-test/test/command.go +++ b/cmd/mfp-test/test/command.go @@ -210,7 +210,6 @@ func cmdTestHandler(ctx context.Context, inv *argv.Invocation) error { i+1, len(d.Data), d.Params.Format, d.Params.JobName) } - <-ctx.Done() return nil }