mesh/merge-gate head 3757d75d6acf merged into trunk 9aa97926141f — pass: builds brother → shanks; no bus step; every machine composes with the change as it…
mesh/repo-check head 3757d75d6acf merged into trunk 9aa97926141f — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
308 lines
12 KiB
Go
308 lines
12 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// 1452: the Brother ignores the whole body when it holds a line break before the closing root tag, and runs its
|
|
// default job (Platen, 200 dpi). The body is one line.
|
|
func TestScanSettingsAreOneLineAndTheCapturedRequest(t *testing.T) {
|
|
caps, err := parseCapabilities(fixture(t, "capabilities.xml"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
body, err := ScanSettings(caps, ScanOptions{Resolution: 200, ColorMode: "color", Feeder: true, Duplex: true})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
end := strings.Index(body, "</scan:ScanSettings>")
|
|
if end < 0 || strings.Contains(body[:end], "\n") {
|
|
t.Fatalf("the body has a line break before the closing root tag: %q", body)
|
|
}
|
|
// It is the request the real scanner took to feed three sheets, both sides (testdata/escl/settings.xml).
|
|
if want := strings.TrimSpace(string(fixture(t, "settings.xml"))); strings.TrimSpace(body) != want {
|
|
t.Fatalf("not the captured request\n got: %s\nwant: %s", body, want)
|
|
}
|
|
glass, _ := ScanSettings(caps, ScanOptions{Resolution: 300, ColorMode: "gray"})
|
|
if strings.Contains(glass[:strings.Index(glass, "</scan:ScanSettings>")], "\n") || strings.Contains(glass, "Duplex") ||
|
|
!strings.Contains(glass, "<pwg:InputSource>Platen</pwg:InputSource>") ||
|
|
!strings.Contains(glass, "<pwg:Width>2480</pwg:Width><pwg:Height>3507</pwg:Height>") {
|
|
t.Fatalf("the glass body is not one line of Platen, A4 cut to the glass's 3507: %q", glass)
|
|
}
|
|
}
|
|
|
|
// 1452: the fake is the real scanner in that: a body with a line break gets the defaults.
|
|
func TestTheFakeScannerIgnoresABodyWithALineBreak(t *testing.T) {
|
|
multi := strings.Replace(string(fixture(t, "settings.xml")), "<pwg:Version>", "\n<pwg:Version>", 1)
|
|
if j := readSettings(multi); j.Source != "Platen" || j.Resolution != 200 || j.Duplex {
|
|
t.Fatalf("a body with a line break was read as %+v", j)
|
|
}
|
|
if j := readSettings(strings.TrimSpace(string(fixture(t, "settings.xml"))) + "\n"); j.Source != "Feeder" || !j.Duplex || j.Resolution != 200 {
|
|
t.Fatalf("the captured body with a trailing newline was read as %+v", j)
|
|
}
|
|
}
|
|
|
|
// 1452: three sheets, both sides, are six images from one job, fetched until NextDocument answers 404.
|
|
func TestAFeederDuplexScanOfThreeSheetsGivesSixPagesAndClosesTheJob(t *testing.T) {
|
|
f := newFakeScanner(t, fixture(t, "scan.jpg"))
|
|
f.sheets = 3
|
|
pages, err := NewClient(f.base()).ScanPages(context.Background(), ScanOptions{Resolution: 200, ColorMode: "color", Feeder: true, Duplex: true})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if f.job.Source != "Feeder" || !f.job.Duplex || f.job.Resolution != 200 || f.job.Region != "2480x3508" {
|
|
t.Fatalf("the scanner took %+v", f.job)
|
|
}
|
|
if len(pages) != 6 {
|
|
t.Fatalf("%d pages, want 6", len(pages))
|
|
}
|
|
if f.open || f.deletes != 0 || f.documents != 7 {
|
|
t.Fatalf("job open %v, %d DELETEs, %d NextDocument requests, want closed, 0, 7 (6 pages and the 404)", f.open, f.deletes, f.documents)
|
|
}
|
|
}
|
|
|
|
// 1452: an empty feeder is an error that says so, and no job stays open.
|
|
func TestAnEmptyFeederIsAnErrorAndLeavesNoJob(t *testing.T) {
|
|
f := newFakeScanner(t, fixture(t, "scan.jpg"))
|
|
f.sheets = 0
|
|
_, err := NewClient(f.base()).ScanPages(context.Background(), ScanOptions{Resolution: 200, ColorMode: "color", Feeder: true})
|
|
if err == nil || !strings.Contains(err.Error(), "feeder") {
|
|
t.Fatalf("error = %v, want it to name the feeder", err)
|
|
}
|
|
if f.open {
|
|
t.Fatal("the job stays open")
|
|
}
|
|
}
|
|
|
|
// 1452: a feeder that never says 404 is cut at 100 pages and the job is deleted.
|
|
func TestAFeederThatNeverEndsIsCutAndTheJobDeleted(t *testing.T) {
|
|
f := newFakeScanner(t, fixture(t, "scan.jpg"))
|
|
f.sheets = 500
|
|
_, err := NewClient(f.base()).ScanPages(context.Background(), ScanOptions{Resolution: 100, ColorMode: "gray", Feeder: true})
|
|
if err == nil || !strings.Contains(err.Error(), "100") {
|
|
t.Fatalf("error = %v, want the 100-page bound", err)
|
|
}
|
|
if f.open || f.deletes != 1 {
|
|
t.Fatalf("job open %v, %d DELETEs, want closed by 1 DELETE", f.open, f.deletes)
|
|
}
|
|
}
|
|
|
|
// 1452: a page that cannot be had in the middle of a feeder job deletes the job.
|
|
func TestAFeederJobThatDropsMidwayIsDeleted(t *testing.T) {
|
|
f := newFakeScanner(t, fixture(t, "scan.jpg"))
|
|
f.sheets = 3
|
|
f.dropConn = true
|
|
if _, err := NewClient(f.base()).ScanPages(context.Background(), ScanOptions{Resolution: 200, ColorMode: "color", Feeder: true}); err == nil {
|
|
t.Fatal("a lost answer is a scan")
|
|
}
|
|
if f.open || f.deletes != 1 {
|
|
t.Fatalf("job open %v, %d DELETEs, want closed by 1 DELETE", f.open, f.deletes)
|
|
}
|
|
}
|
|
|
|
func scanArgs(source string, duplex bool) map[string]any {
|
|
a := map[string]any{}
|
|
if source != "" {
|
|
a["source"] = source
|
|
}
|
|
if duplex {
|
|
a["duplex"] = true
|
|
}
|
|
return a
|
|
}
|
|
|
|
// 1452: through the tool, a duplex feeder scan is ONE PDF of six pages, filed once, and scan.added says pages 6.
|
|
func TestTheToolFilesOneSixPagePDFForAFeederDuplexScan(t *testing.T) {
|
|
f := newFakeScanner(t, fixture(t, "scan.jpg"))
|
|
f.sheets = 3
|
|
r, folder := scanRig(t, f, Settings{Crop: true, Resolution: 200})
|
|
got, err := r.call(t, "scanner.scan", scanArgs("feeder", true))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
body := got.(map[string]any)
|
|
if body["pages"] != float64(6) || body["cropped"] != false {
|
|
t.Errorf("pages = %v, cropped = %v, want 6, false (a feeder page is a whole sheet)", body["pages"], body["cropped"])
|
|
}
|
|
pdf, err := os.ReadFile(body["path"].(string))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if n := bytes.Count(pdf, []byte("/Type /Page /Parent")); n != 6 || !bytes.Contains(pdf, []byte("/Count 6")) {
|
|
t.Errorf("the PDF has %d page objects, want 6 and /Count 6", n)
|
|
}
|
|
if f.open || f.deletes != 0 || f.posts != 1 {
|
|
t.Errorf("job open %v, %d DELETEs, %d posts", f.open, f.deletes, f.posts)
|
|
}
|
|
if entries, _ := os.ReadDir(folder); len(entries) != 1 {
|
|
t.Errorf("%d files in the scan folder, want 1", len(entries))
|
|
}
|
|
if out := *r.said; len(out) != 1 || out[0].body["pages"] != 6 || !out[0].seen {
|
|
t.Errorf("events = %+v, want one scan.added with pages 6, said after the scan was kept", out)
|
|
}
|
|
}
|
|
|
|
// 1452: the glass is one page, and duplex is for the feeder.
|
|
func TestDuplexOnTheGlassIsRefusedAndNothingIsAsked(t *testing.T) {
|
|
f := newFakeScanner(t, fixture(t, "scan.jpg"))
|
|
r, _ := scanRig(t, f, Settings{})
|
|
for _, args := range []map[string]any{scanArgs("", true), scanArgs("glass", true)} {
|
|
_, err := r.call(t, "scanner.scan", args)
|
|
if err == nil || !strings.Contains(err.Error(), "duplex") || !strings.Contains(err.Error(), "feeder") {
|
|
t.Errorf("%v: error = %v, want duplex is for the feeder", args, err)
|
|
}
|
|
}
|
|
if _, err := r.call(t, "scanner.scan", scanArgs("tray", false)); err == nil || !strings.Contains(err.Error(), "glass or feeder") {
|
|
t.Errorf("an unknown source: error = %v", err)
|
|
}
|
|
if r.bus.asks() != 0 || f.posts != 0 {
|
|
t.Errorf("%d asks queued, %d jobs posted", r.bus.asks(), f.posts)
|
|
}
|
|
}
|
|
|
|
// pdfCapture is a stand-in capture of a small PDF, ready to be released.
|
|
func standInCapture(release <-chan struct{}, fail error) func(ctx context.Context, req ScanRequest) ([]Capture, error) {
|
|
return func(ctx context.Context, req ScanRequest) ([]Capture, error) {
|
|
<-release
|
|
if fail != nil {
|
|
return nil, fail
|
|
}
|
|
pdf := pdfOf(5000)
|
|
return []Capture{{PDF: pdf, SHA256: sha256Hex(pdf), Resolution: 200, Pages: 3, At: fixedNow}}, nil
|
|
}
|
|
}
|
|
|
|
// waitFor polls until the condition holds.
|
|
func waitFor(t *testing.T, what string, cond func() bool) {
|
|
t.Helper()
|
|
for i := 0; i < 400; i++ {
|
|
if cond() {
|
|
return
|
|
}
|
|
time.Sleep(10 * time.Millisecond)
|
|
}
|
|
t.Fatalf("timed out waiting for %s", what)
|
|
}
|
|
|
|
// 1552 and 1539: a feeder scan not done within the quick wait is answered as started with its id; the scan goes on
|
|
// and is filed although the caller has gone; the id reads the outcome, running and then done with the path.
|
|
func TestALongFeederScanAnswersStartedAndIsFiledWithoutTheCaller(t *testing.T) {
|
|
f := newFakeScanner(t, fixture(t, "scan.jpg"))
|
|
r, folder := scanRig(t, f, Settings{})
|
|
old := quickWait
|
|
quickWait = 50 * time.Millisecond
|
|
t.Cleanup(func() { quickWait = old })
|
|
release := make(chan struct{})
|
|
r.worker.Capture = standInCapture(release, nil)
|
|
|
|
got, err := r.call(t, "scanner.scan", scanArgs("feeder", false))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
start := got.(map[string]any)
|
|
id, _ := start["id"].(string)
|
|
if start["started"] != true || id == "" || start["path"] != nil {
|
|
t.Fatalf("answer = %v, want started with an id", start)
|
|
}
|
|
running, err := r.call(t, "scanner.scan", map[string]any{"id": id})
|
|
if err != nil || running.(map[string]any)["state"] != "running" {
|
|
t.Fatalf("while it runs: %v, %v", running, err)
|
|
}
|
|
close(release)
|
|
// Nobody asks again: the scan is filed all the same.
|
|
waitFor(t, "the scan to be filed", func() bool { e, _ := os.ReadDir(folder); return len(e) == 1 })
|
|
// The file is renamed into place before the job is marked done: read the outcome until it is.
|
|
var d map[string]any
|
|
waitFor(t, "the outcome to be done", func() bool {
|
|
got, err := r.call(t, "scanner.scan", map[string]any{"id": id})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
d = got.(map[string]any)
|
|
return d["state"] != "running"
|
|
})
|
|
if d["state"] != "done" || d["pages"] != float64(3) || d["path"] == nil {
|
|
t.Fatalf("outcome = %v, want done with pages and the path", d)
|
|
}
|
|
if _, err := os.Stat(d["path"].(string)); err != nil {
|
|
t.Error(err)
|
|
}
|
|
}
|
|
|
|
// 1552: a scan that fails after the caller was told it started is read as failed, with why; an unknown id is an
|
|
// error that says so.
|
|
func TestAFailedLongScanIsReadAsFailedWithWhy(t *testing.T) {
|
|
f := newFakeScanner(t, fixture(t, "scan.jpg"))
|
|
r, _ := scanRig(t, f, Settings{})
|
|
old := quickWait
|
|
quickWait = 50 * time.Millisecond
|
|
t.Cleanup(func() { quickWait = old })
|
|
release := make(chan struct{})
|
|
r.worker.Capture = standInCapture(release, errors.New("the scanner dropped the job: 410 Gone"))
|
|
start, err := r.call(t, "scanner.scan", scanArgs("feeder", true))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
id := start.(map[string]any)["id"].(string)
|
|
close(release)
|
|
var out map[string]any
|
|
waitFor(t, "the failure", func() bool {
|
|
got, _ := r.call(t, "scanner.scan", map[string]any{"id": id})
|
|
out = got.(map[string]any)
|
|
return out["state"] == "failed"
|
|
})
|
|
if !strings.Contains(out["why"].(string), "410 Gone") {
|
|
t.Errorf("why = %v", out["why"])
|
|
}
|
|
if _, err := r.call(t, "scanner.scan", map[string]any{"id": "nosuchid"}); err == nil || !strings.Contains(err.Error(), "nosuchid") {
|
|
t.Errorf("an unknown id: %v", err)
|
|
}
|
|
}
|
|
|
|
// 1539: the feeder job's bound scales with its pages, from 3 to 30 minutes.
|
|
func TestTheFeederBoundScalesWithThePages(t *testing.T) {
|
|
for pages, want := range map[int]time.Duration{1: 3 * time.Minute, 9: 3 * time.Minute, 30: 10 * time.Minute, 100: 30 * time.Minute, 500: 30 * time.Minute} {
|
|
if got := FeederBound(pages); got != want {
|
|
t.Errorf("FeederBound(%d) = %s, want %s", pages, got, want)
|
|
}
|
|
}
|
|
f := newFakeScanner(t, fixture(t, "scan.jpg"))
|
|
f.sheets = 3
|
|
_, err := NewClient(f.base()).ScanPages(context.Background(), ScanOptions{Resolution: 200, ColorMode: "color", Feeder: true,
|
|
Bound: func(int) time.Duration { return 0 }})
|
|
if err == nil || !strings.Contains(err.Error(), "bound") {
|
|
t.Fatalf("error = %v, want the bound", err)
|
|
}
|
|
if f.open || f.deletes != 1 {
|
|
t.Errorf("job open %v, %d DELETEs, want the job deleted", f.open, f.deletes)
|
|
}
|
|
}
|
|
|
|
// 1539: a scan too large for the tool to fetch is not kept nor announced: it is refused with its size.
|
|
func TestAScanTooLargeToFetchIsNeitherKeptNorAnnounced(t *testing.T) {
|
|
f := newFakeScanner(t, fixture(t, "scan.jpg"))
|
|
r, folder := scanRig(t, f, Settings{})
|
|
release := make(chan struct{})
|
|
close(release)
|
|
huge := make([]byte, MostAnswerParts*AnswerPartSize+1)
|
|
r.worker.Capture = func(ctx context.Context, req ScanRequest) ([]Capture, error) {
|
|
return []Capture{{PDF: huge, SHA256: "x", Resolution: 200, Pages: 99, At: fixedNow}}, nil
|
|
}
|
|
_, err := r.call(t, "scanner.scan", scanArgs("feeder", false))
|
|
if err == nil || !strings.Contains(err.Error(), "not kept") {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
if len(*r.said) != 0 {
|
|
t.Errorf("%d events said for a scan not kept", len(*r.said))
|
|
}
|
|
if e, _ := os.ReadDir(folder); len(e) != 0 {
|
|
t.Errorf("filed: %v", e)
|
|
}
|
|
}
|