Files
mesh-catalog/modules/brother/cmd/brother/helpers_test.go
T
jschoubben d5f9cd465c
mesh/merge-gate head d5f9cd465c96 merged into trunk d62c2edc35f3 — pass: builds brother → shanks; no bus step; every machine composes with the change as it…
mesh/repo-check head d5f9cd465c96 merged into trunk d62c2edc35f3 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
Send brother's scan settings on one line and scan from the feeder, both sides, into one PDF (issue 1452)
The Brother ignores the whole body when it holds a line break, so every setting was ignored and the glass was scanned at its default.
2026-10-21 01:05:55 +02:00

184 lines
5.2 KiB
Go

package main
import (
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"sync"
"testing"
)
// fixture reads a capture of the real scanner's answers (testdata/escl, 2026-10-20). Tests never reach a real scanner.
func fixture(t *testing.T, name string) []byte {
t.Helper()
b, err := os.ReadFile(filepath.Join("testdata", "escl", name))
if err != nil {
t.Fatal(err)
}
return b
}
// fakeScanner serves the captured eSCL answers and records what it was asked.
type fakeScanner struct {
*httptest.Server
mu sync.Mutex
image []byte
postBody string
posts int
documents int // NextDocument requests
refuse int // when set, the status ScanJobs answers instead of 201
noPage bool
// A real scanner keeps a job open (and answers a new POST with 503) until its NextDocument has answered 404
// or the job is DELETEd; the fake does the same.
open bool
delivered bool
pages int // pages a job holds beyond the first
served int
deletes int
gone bool // the first NextDocument answers 410 (the job was cancelled at the device)
dropConn bool // the first NextDocument loses its answer
stuck bool // the job cannot be closed: NextDocument and DELETE both fail
// sheets is what the feeder holds; a duplex job gives two pages a sheet.
sheets int
// job is what the scanner took of the last body. The real Brother ignores the WHOLE body when it holds a line
// break before the closing root tag (issue 1452) and runs its default job: Platen, 200 dpi, one page.
job scannerJob
}
type scannerJob struct {
Source string
Resolution int
Duplex bool
Region string // width x height
}
var (
reSource = regexp.MustCompile(`<pwg:InputSource>(\w+)</pwg:InputSource>`)
reXRes = regexp.MustCompile(`<scan:XResolution>(\d+)</scan:XResolution>`)
reRegion = regexp.MustCompile(`<pwg:Width>(\d+)</pwg:Width><pwg:Height>(\d+)</pwg:Height>`)
)
// readSettings is the real scanner's reading of a body: a line break before the closing root tag and every setting
// is ignored.
func readSettings(body string) scannerJob {
end := strings.Index(body, "</scan:ScanSettings>")
if end < 0 || strings.Contains(body[:end], "\n") {
return scannerJob{Source: "Platen", Resolution: 200, Region: "1696x2203"}
}
j := scannerJob{Source: "Platen", Resolution: 200, Duplex: strings.Contains(body, "<scan:Duplex>true</scan:Duplex>")}
if m := reSource.FindStringSubmatch(body); m != nil {
j.Source = m[1]
}
if m := reXRes.FindStringSubmatch(body); m != nil {
j.Resolution, _ = strconv.Atoi(m[1])
}
if m := reRegion.FindStringSubmatch(body); m != nil {
j.Region = m[1] + "x" + m[2]
}
return j
}
// total is how many pages the job holds.
func (f *fakeScanner) total() int {
if f.job.Source == "Feeder" {
if f.job.Duplex {
return 2 * f.sheets
}
return f.sheets
}
return 1 + f.pages
}
func newFakeScanner(t *testing.T, image []byte) *fakeScanner {
t.Helper()
f := &fakeScanner{image: image}
mux := http.NewServeMux()
mux.HandleFunc("/eSCL/ScannerCapabilities", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/xml")
w.Write(fixture(t, "capabilities.xml"))
})
mux.HandleFunc("/eSCL/ScannerStatus", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/xml")
w.Write(fixture(t, "status-idle.xml"))
})
mux.HandleFunc("/eSCL/ScanJobs", func(w http.ResponseWriter, r *http.Request) {
f.mu.Lock()
defer f.mu.Unlock()
if r.Method != http.MethodPost {
http.Error(w, "post only", http.StatusMethodNotAllowed)
return
}
b := make([]byte, 1<<16)
n, _ := r.Body.Read(b)
f.postBody = string(b[:n])
f.posts++
f.job = readSettings(f.postBody)
if f.refuse != 0 {
http.Error(w, "busy", f.refuse)
return
}
if f.open {
http.Error(w, "busy", http.StatusServiceUnavailable)
return
}
f.open, f.delivered, f.served = true, false, 0
// The captured Location names the real scanner's host; the test's own server stands in for it.
w.Header().Set("Location", f.URL+"/eSCL/ScanJobs/7320ead5")
w.WriteHeader(http.StatusCreated)
})
mux.HandleFunc("/eSCL/ScanJobs/7320ead5/NextDocument", func(w http.ResponseWriter, r *http.Request) {
f.mu.Lock()
defer f.mu.Unlock()
f.documents++
if f.stuck {
http.Error(w, "stuck", http.StatusInternalServerError)
return
}
if f.gone {
f.gone, f.open = false, false
http.Error(w, "gone", http.StatusGone)
return
}
if f.dropConn {
if hj, ok := w.(http.Hijacker); ok {
c, _, _ := hj.Hijack()
c.Close()
}
return
}
if f.noPage || f.served >= f.total() {
f.open = false
http.NotFound(w, r)
return
}
f.delivered = true
f.served++
w.Header().Set("Content-Type", "image/jpeg")
w.Write(f.image)
})
mux.HandleFunc("/eSCL/ScanJobs/7320ead5", func(w http.ResponseWriter, r *http.Request) {
f.mu.Lock()
defer f.mu.Unlock()
if r.Method != http.MethodDelete {
http.Error(w, "delete only", http.StatusMethodNotAllowed)
return
}
f.deletes++
if f.stuck {
http.Error(w, "stuck", http.StatusInternalServerError)
return
}
f.open = false
w.WriteHeader(http.StatusOK)
})
f.Server = httptest.NewServer(mux)
t.Cleanup(f.Close)
return f
}
func (f *fakeScanner) base() string { return f.URL + "/eSCL" }