Research 027 asked for cups with the printer's driver. Measured: both Brother queues already print through IPP Everywhere, so cups and cups-filters are the whole driver, and the AUR vendor packages beside them serve no queue. The desktop's Canon is the exception: its 2012 driver is AUR-only and waits for the mesh's package repository; it stays as found. Seven Go tools: printers (state, device, driverless or not, supply levels), queue, cancel (the account first, sudo -n when CUPS refuses it), print, default, resume, and drivers (which packages bring drivers, which are foreign, which no queue uses).
235 lines
9.5 KiB
Go
235 lines
9.5 KiB
Go
package main
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestTheManifestIsTheSchedulerAndDriverlessPrintingAndNoVendorDriver(t *testing.T) {
|
|
m := readManifest(t)
|
|
holdsTheBundle(t, m, "cups")
|
|
if got := strings.Join(m.packages(), ","); got != "cups,cups-filters" {
|
|
t.Errorf("packages %s: a vendor driver is from outside the official repositories, and no queue measured needs one but the desktop's Canon", got)
|
|
}
|
|
s := m.services()
|
|
for _, u := range []string{"cups.socket", "cups.service"} {
|
|
if s[u] == nil || s[u]["state"] != "running" || s[u]["boot"] != "enabled" {
|
|
t.Errorf("%s: %v", u, s[u])
|
|
}
|
|
}
|
|
for _, r := range m.Resources {
|
|
if r["type"] == "file" {
|
|
t.Errorf("the queues and cupsd's files are CUPS's own: %v", r["id"])
|
|
}
|
|
}
|
|
}
|
|
|
|
// The desktop's two queues on 2026-10-04.
|
|
func theDesktop(t *testing.T, more func(line string, c Cmd) (Result, bool)) *fake {
|
|
return using(t, func(line string, c Cmd) Result {
|
|
if more != nil {
|
|
if r, handled := more(line, c); handled {
|
|
return r
|
|
}
|
|
}
|
|
switch line {
|
|
case "lpstat -r":
|
|
return ok("scheduler is running\n")
|
|
case "lpstat -d":
|
|
return ok("system default destination: Brother_MFC_Novox\n")
|
|
case "lpstat -p":
|
|
return ok("printer Brother_MFC_Novox is idle. enabled since Mon Jun 29 21:34:30 2026\n" +
|
|
"printer Kanjuro disabled since Sun Jun 9 11:16:03 2024 -\n\tPaused\n")
|
|
case "lpstat -a":
|
|
return ok("Brother_MFC_Novox accepting requests since Mon Jun 29 21:34:30 2026\nKanjuro not accepting requests since Sun Jun 9 11:16:03 2024 -\n\tRejecting Jobs\n")
|
|
case "lpoptions -p Brother_MFC_Novox":
|
|
return ok(`copies=1 device-uri=ipp://192.0.2.171/ipp/port1 finishings=3 marker-levels=70,100,8,100 marker-low-levels=10,10,10,10 marker-names='Black\ Toner\ Cartridge,Cyan\ Toner\ Cartridge,Magenta\ Toner\ Cartridge,Yellow\ Toner\ Cartridge' marker-types=toner,toner,toner,toner printer-is-shared=false printer-make-and-model='Printer - IPP Everywhere' printer-state-reasons=none`)
|
|
case "lpoptions -p Kanjuro":
|
|
return ok(`device-uri=cnijnet:/18-0C-AC-B0-62-83 printer-is-shared=false printer-make-and-model='Canon MG4200 series Ver.3.80' printer-state-reasons=paused`)
|
|
}
|
|
return Result{Status: 9, Stderr: "unexpected " + line}
|
|
})
|
|
}
|
|
|
|
func TestPrintersReadsStateDeviceModelAndSupplies(t *testing.T) {
|
|
theDesktop(t, nil)
|
|
got, err := Printers()
|
|
if err != nil || len(got.Printers) != 2 || got.Default != "Brother_MFC_Novox" || got.Scheduler != "scheduler is running" {
|
|
t.Fatalf("%+v %v", got, err)
|
|
}
|
|
b, k := got.Printers[0], got.Printers[1]
|
|
if b.State != "idle" || !b.Enabled || !b.Accepting || !b.Default || !b.Driverless || b.URI != "ipp://192.0.2.171/ipp/port1" || len(b.Reasons) != 0 {
|
|
t.Errorf("%+v", b)
|
|
}
|
|
if len(b.Markers) != 4 || b.Markers[0].Name != "Black Toner Cartridge" || b.Markers[2].Level != 8 || !b.Markers[2].Low || b.Markers[0].Low {
|
|
t.Errorf("markers %+v", b.Markers)
|
|
}
|
|
if k.State != "stopped" || k.Enabled || k.Accepting || k.Driverless || strings.Join(k.Reasons, ",") != "paused" || k.Message != "Paused" {
|
|
t.Errorf("%+v", k)
|
|
}
|
|
}
|
|
|
|
func TestPrintersWithoutAQueueOrAScheduler(t *testing.T) {
|
|
using(t, func(line string, c Cmd) Result {
|
|
if line == "lpstat -r" {
|
|
return ok("scheduler is running\n")
|
|
}
|
|
return Result{Status: 1, Stderr: "lpstat: No destinations added.\n"}
|
|
})
|
|
got, err := Printers()
|
|
if err != nil || len(got.Printers) != 0 {
|
|
t.Fatalf("no queue is an empty answer, not a failure: %+v %v", got, err)
|
|
}
|
|
using(t, func(string, Cmd) Result { return Result{Status: 1, Stderr: "lpstat: Scheduler is not running.\n"} })
|
|
if _, err := Printers(); err == nil || !strings.Contains(err.Error(), "scheduler is not running") {
|
|
t.Fatalf("%v", err)
|
|
}
|
|
}
|
|
|
|
func TestLpoptionsQuotingIsRead(t *testing.T) {
|
|
o := lpoptionsOf(`a=1 b='x y' c=p\ q d e=`)
|
|
if o["a"] != "1" || o["b"] != "x y" || o["c"] != "p q" || o["d"] != "" || o["e"] != "" {
|
|
t.Errorf("%v", o)
|
|
}
|
|
}
|
|
|
|
func TestQueueReadsJobsNewestFirstAndBounded(t *testing.T) {
|
|
f := using(t, func(string, Cmd) Result {
|
|
return ok("Brother-6 jochen 1024 Mon Jul 8 12:15:34 2024\nBrother-8 jochen 2048 Mon Jul 8 12:19:56 2024\nBrother-7 other 1024 Mon Jul 8 12:15:23 2024\n")
|
|
})
|
|
got, err := Queue("Brother", true, 2)
|
|
jobs := got["jobs"].([]Job)
|
|
if err != nil || got["count"] != 3 || len(jobs) != 2 || jobs[0].ID != "Brother-8" || jobs[0].Printer != "Brother" || jobs[0].Bytes != 2048 || jobs[0].Submitted != "Mon Jul 8 12:19:56 2024" {
|
|
t.Fatalf("%+v %v", got, err)
|
|
}
|
|
if f.lines()[0] != "lpstat -W completed -o Brother" {
|
|
t.Errorf("%v", f.lines())
|
|
}
|
|
if _, err := Queue("-h", false, 5); err == nil {
|
|
t.Error("an option as a printer")
|
|
}
|
|
}
|
|
|
|
func TestCancelTriesTheAccountThenRootWhenCUPSRefusesIt(t *testing.T) {
|
|
f := using(t, func(line string, c Cmd) Result {
|
|
if !strings.HasPrefix(line, "sudo") {
|
|
return Result{Status: 1, Stderr: "cancel: Forbidden\n"}
|
|
}
|
|
return ok("")
|
|
})
|
|
got, err := Cancel("Brother-12", "", false)
|
|
if err != nil || got["as_root"] != true {
|
|
t.Fatalf("%v %v", got, err)
|
|
}
|
|
if strings.Join(f.lines(), "|") != "cancel Brother-12|sudo -n cancel Brother-12" {
|
|
t.Errorf("%v", f.lines())
|
|
}
|
|
f = using(t, func(string, Cmd) Result { return ok("") })
|
|
if got, err := Cancel("", "Brother", true); err != nil || got["as_root"] != false || f.lines()[0] != "cancel -a Brother" {
|
|
t.Fatalf("%v %v %v", got, err, f.lines())
|
|
}
|
|
using(t, func(string, Cmd) Result { return Result{Status: 1, Stderr: "cancel: Unknown job 99\n"} })
|
|
if _, err := Cancel("99", "", false); err == nil || !strings.Contains(err.Error(), "Unknown job") {
|
|
t.Errorf("a failure that is not a refusal is not retried as root: %v", err)
|
|
}
|
|
for _, bad := range [][3]string{{"", "", ""}, {"12; rm", "", ""}, {"", "", "all"}} {
|
|
if _, err := Cancel(bad[0], bad[1], bad[2] == "all"); err == nil {
|
|
t.Errorf("%v accepted", bad)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPrintChecksTheFileAndOptionsAndAnswersTheJob(t *testing.T) {
|
|
was := statFile
|
|
defer func() { statFile = was }()
|
|
statFile = func(p string) error {
|
|
if p == "/home/op/doc.pdf" {
|
|
return nil
|
|
}
|
|
return errString("no such file")
|
|
}
|
|
f := using(t, func(string, Cmd) Result { return ok("request id is Brother-13 (1 file(s))\n") })
|
|
got, err := Print("/home/op/doc.pdf", "Brother", 2, map[string]string{"sides": "two-sided-long-edge", "media": "A4"}, "")
|
|
if err != nil || got["job"] != "Brother-13" {
|
|
t.Fatalf("%v %v", got, err)
|
|
}
|
|
if l := f.lines()[0]; l != "lp -d Brother -n 2 -o media=A4 -o sides=two-sided-long-edge -t doc.pdf -- /home/op/doc.pdf" {
|
|
t.Errorf("%s", l)
|
|
}
|
|
if _, err := Print("doc.pdf", "", 1, nil, ""); err == nil {
|
|
t.Error("a relative file")
|
|
}
|
|
if _, err := Print("/home/op/missing.pdf", "", 1, nil, ""); err == nil {
|
|
t.Error("a missing file")
|
|
}
|
|
if _, err := optionsOf(map[string]any{"options": map[string]any{"sides": "x y"}}); err == nil {
|
|
t.Error("an option value with a space")
|
|
}
|
|
if _, err := optionsOf(map[string]any{"options": map[string]any{"-o": "x"}}); err == nil {
|
|
t.Error("an option name that is an option")
|
|
}
|
|
using(t, func(string, Cmd) Result { return Result{Status: 1, Stderr: "lp: Error - No default destination."} })
|
|
if _, err := Print("/home/op/doc.pdf", "", 1, nil, ""); err == nil || !strings.Contains(err.Error(), "no default printer") {
|
|
t.Errorf("%v", err)
|
|
}
|
|
}
|
|
|
|
type errString string
|
|
|
|
func (e errString) Error() string { return string(e) }
|
|
|
|
func TestDefaultReadsAndSetsThroughSudo(t *testing.T) {
|
|
f := theDesktop(t, func(line string, c Cmd) (Result, bool) {
|
|
if strings.HasPrefix(line, "sudo -n lpadmin") {
|
|
return ok(""), true
|
|
}
|
|
return Result{}, false
|
|
})
|
|
got, err := Default("")
|
|
if err != nil || got["default"] != "Brother_MFC_Novox" {
|
|
t.Fatalf("%v %v", got, err)
|
|
}
|
|
got, err = Default("Kanjuro")
|
|
if err != nil || got["default"] != "Kanjuro" || got["was"] != "Brother_MFC_Novox" {
|
|
t.Fatalf("%v %v", got, err)
|
|
}
|
|
if l := f.lines(); l[len(l)-1] != "sudo -n lpadmin -d Kanjuro" {
|
|
t.Errorf("%v", l)
|
|
}
|
|
}
|
|
|
|
func TestResumeEnablesAndAcceptsThroughSudo(t *testing.T) {
|
|
f := using(t, func(string, Cmd) Result { return ok("") })
|
|
if _, err := Resume("Kanjuro"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.Join(f.lines(), "|") != "sudo -n cupsenable Kanjuro|sudo -n cupsaccept Kanjuro" {
|
|
t.Errorf("%v", f.lines())
|
|
}
|
|
}
|
|
|
|
func TestDriversNamesForeignDriverPackagesAndOnesNoQueueUses(t *testing.T) {
|
|
theDesktop(t, func(line string, c Cmd) (Result, bool) {
|
|
switch {
|
|
case strings.HasPrefix(line, "pacman -Qo"):
|
|
return ok("/usr/lib/cups/filter/ is owned by brother-mfc-l8390cdw 3.5.1-2\n/usr/lib/cups/filter/ is owned by cups 2:2.4.19-1\n/usr/lib/cups/filter/ is owned by cups-filters 2.0.1-2\n/usr/lib/cups/backend/ is owned by cnijfilter-mg4200 3.80-6\n/usr/lib/cups/backend/ is owned by cups 2:2.4.19-1\n"), true
|
|
case line == "pacman -Qqm":
|
|
return ok("brother-mfc-l8390cdw\ncnijfilter-mg4200\nsnapd\n"), true
|
|
case line == "lpinfo -m":
|
|
return ok("drv:///sample.drv/dymo.ppd DYMO Label Printer\ncanonmg4200.ppd Canon MG4200 series Ver.3.80\neverywhere IPP Everywhere\n"), true
|
|
}
|
|
return Result{}, false
|
|
})
|
|
got, err := Drivers("canon", 10)
|
|
if err != nil || len(got.Queues) != 2 || len(got.Packages) != 2 || got.Matched != 1 || got.Models[0].PPD != "canonmg4200.ppd" {
|
|
t.Fatalf("%+v %v", got, err)
|
|
}
|
|
if !got.Packages[0].Foreign || got.Packages[0].Package != "brother-mfc-l8390cdw" {
|
|
t.Errorf("%+v", got.Packages)
|
|
}
|
|
all := strings.Join(got.Findings, ";")
|
|
if !strings.Contains(all, "cnijfilter-mg4200 is not from the official") || strings.Contains(all, "every queue prints driverless") {
|
|
t.Errorf("the Canon queue uses its driver, so not every queue is driverless: %s", all)
|
|
}
|
|
}
|