Files
mesh-catalog/modules/brother/cmd/brother/glass_test.go
T
jschoubben 6cf51173d5
mesh/repo-check head 6cf51173d586 merged into trunk b2f5ebb27dbf beside mesh-controller#424@550cf37d — pass: its merge-check.sh passed, each part in its to…
mesh/merge-gate head 6cf51173d586 merged into trunk b2f5ebb27dbf — pass: group feat/files-through-the-store composed: every machine composes with the chang…
mesh/delivery superseded: its walk was superseded and names no walk that took it over: a walk that took it over answers its merge, and it follows that wa…
mesh/delivery-group group feat/files-through-the-store closed: a member was closed before the group merged
Give the Service its own fetch client and open-document waits instead of reading package variables from background goroutines, which a test or reload changed meanwhile: data races under -race (issue 1601)
2026-10-21 18:03:58 +02:00

323 lines
13 KiB
Go

package main
import (
"bytes"
"context"
"encoding/json"
"errors"
"image"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
// withDensity puts a JFIF header of dpi into a JPEG that has none (Go's encoder writes none).
func withDensity(jpg []byte, dpi int) []byte {
app0 := []byte{0xFF, 0xE0, 0, 16, 'J', 'F', 'I', 'F', 0, 1, 1, 1, byte(dpi >> 8), byte(dpi), byte(dpi >> 8), byte(dpi), 0, 0}
return append(append(append([]byte{}, jpg[:2]...), app0...), jpg[2:]...)
}
// 1528: three receipts lying apart on the glass are three PDFs, each its own object, own scan.added and own file
// in the scan folder, one page each; the answer lists every document.
func TestSeveralDocumentsOnTheGlassAreFiledEachOnItsOwn(t *testing.T) {
scan := withDensity(onWhite(t, 11, image.Rect(100, 100, 300, 300), image.Rect(450, 100, 650, 400), image.Rect(100, 600, 400, 900)), 100)
f := newFakeScanner(t, scan)
r, folder := scanRig(t, f, Settings{Crop: true, Resolution: 100})
got, err := r.call(t, "scanner.scan", map[string]any{"apart": true})
if err != nil {
t.Fatal(err)
}
body := got.(map[string]any)
docs, _ := body["documents"].([]any)
if len(docs) != 3 || body["count"] != 3 {
t.Fatalf("answer = %v, want three documents", body)
}
seen := map[string]bool{}
for _, d := range docs {
m := d.(map[string]any)
path, _ := m["path"].(string)
if _, err := os.Stat(path); err != nil || m["pages"] != float64(1) || m["cropped"] != true || seen[path] {
t.Errorf("document %v: %v", m, err)
}
seen[path] = true
}
if e, _ := os.ReadDir(folder); len(e) != 3 {
t.Errorf("%d files in the scan folder, want 3", len(e))
}
out := *r.said
if len(out) != 3 {
t.Fatalf("%d events, want one each", len(out))
}
keys := map[any]bool{}
for _, ev := range out {
keys[ev.body["object"]] = true
if !ev.seen {
t.Error("an event was said before its PDF was kept")
}
}
if len(keys) != 3 {
t.Errorf("the events name %d different objects, want 3", len(keys))
}
}
// 1528: one document on the glass is answered as today: a flat body with its path.
func TestOneDocumentOnTheGlassIsAnsweredAsBefore(t *testing.T) {
scan := withDensity(onWhite(t, 12, image.Rect(100, 100, 300, 300)), 100)
f := newFakeScanner(t, scan)
r, _ := scanRig(t, f, Settings{Crop: true, Resolution: 100})
body, err := r.scan(t)
if err != nil {
t.Fatal(err)
}
if body["documents"] != nil || body["path"] == nil || len(*r.said) != 1 {
t.Errorf("answer = %v, %d events", body, len(*r.said))
}
}
func docArgs(document string, finish bool) map[string]any {
a := map[string]any{"document": document}
if finish {
a["finish"] = true
}
return a
}
// 1538: start scans page 1 into a new document and files nothing; the id adds the next page; finish closes the
// pages into one PDF, one object, one scan.added, one file, and scans nothing.
func TestAGlassDocumentOfTwoPagesIsOnePDFFiledOnce(t *testing.T) {
f := newFakeScanner(t, fixture(t, "scan.jpg"))
r, folder := scanRig(t, f, Settings{Crop: false, Resolution: 200})
got, err := r.call(t, "scanner.scan", docArgs("start", false))
if err != nil {
t.Fatal(err)
}
id, _ := got.(map[string]any)["document"].(string)
if id == "" || got.(map[string]any)["pages"] != float64(1) {
t.Fatalf("start answered %v", got)
}
if got, err = r.call(t, "scanner.scan", docArgs(id, false)); err != nil || got.(map[string]any)["pages"] != float64(2) {
t.Fatalf("second page: %v, %v", got, err)
}
if e, _ := os.ReadDir(folder); len(e) != 0 || len(*r.said) != 0 {
t.Fatalf("an open document filed %d files and said %d events", len(e), len(*r.said))
}
posts := f.posts
got, err = r.call(t, "scanner.scan", docArgs(id, true))
if err != nil {
t.Fatal(err)
}
body := got.(map[string]any)
if body["pages"] != float64(2) || body["path"] == nil || f.posts != posts {
t.Fatalf("finish answered %v (%d scans after, %d before)", body, f.posts, posts)
}
pdf, _ := os.ReadFile(body["path"].(string))
if !bytes.Contains(pdf, []byte("/Count 2")) || bytes.Count(pdf, []byte("/Type /Page /Parent")) != 2 {
t.Error("the filed PDF does not have two pages")
}
if e, _ := os.ReadDir(folder); len(e) != 1 || len(*r.said) != 1 || (*r.said)[0].body["pages"] != 2 {
t.Errorf("%d files, %d events", len(e), len(*r.said))
}
// A closed document takes no more.
if _, err := r.call(t, "scanner.scan", docArgs(id, false)); err == nil || !strings.Contains(err.Error(), "closed already") {
t.Errorf("a page into a closed document: %v", err)
}
}
// 1538: what is refused: finish with start, an unknown id, a feeder document, an id that is no id.
func TestGlassDocumentsRefuseWhatIsNotWellFormed(t *testing.T) {
f := newFakeScanner(t, fixture(t, "scan.jpg"))
r, _ := scanRig(t, f, Settings{})
for name, args := range map[string]map[string]any{
"finish with start": docArgs("start", true),
"finish alone": {"finish": true},
"unknown id": docArgs("nosuchdocument", false),
"unknown finish": docArgs("nosuchdocument", true),
"bad id": docArgs("../../etc", false),
"on the feeder": {"document": "start", "source": "feeder"},
} {
if _, err := r.call(t, "scanner.scan", args); err == nil {
t.Errorf("%s: accepted", name)
}
}
if f.posts != 0 {
t.Errorf("%d scans were put for refused asks", f.posts)
}
}
// 1538: an open document that gets no page is closed and filed as one PDF by the tool: its pages are not lost.
func TestAnUnfinishedGlassDocumentIsFiledAfterItsExpiry(t *testing.T) {
f := newFakeScanner(t, fixture(t, "scan.jpg"))
r, folder := scanRig(t, f, Settings{Crop: false, Resolution: 200})
old := docExpiry
docExpiry = 150 * time.Millisecond
t.Cleanup(func() { docExpiry = old })
got, err := r.call(t, "scanner.scan", docArgs("start", false))
if err != nil {
t.Fatal(err)
}
id := got.(map[string]any)["document"].(string)
if _, err := r.call(t, "scanner.scan", docArgs(id, false)); err != nil {
t.Fatal(err)
}
waitFor(t, "the expired document to be filed", func() bool { e, _ := os.ReadDir(folder); return len(e) == 1 })
e, _ := os.ReadDir(folder)
pdf, _ := os.ReadFile(filepath.Join(folder, e[0].Name()))
if !bytes.Contains(pdf, []byte("/Count 2")) || len(*r.said) != 1 {
t.Errorf("the expired document: %d events, two pages %v", len(*r.said), bytes.Contains(pdf, []byte("/Count 2")))
}
}
// 1538: finishing a document does not wait for the expiry: its timer is gone, so nothing is filed twice.
func TestAFinishedGlassDocumentIsNotFiledAgainByItsExpiry(t *testing.T) {
f := newFakeScanner(t, fixture(t, "scan.jpg"))
r, folder := scanRig(t, f, Settings{Crop: false, Resolution: 200})
old := docExpiry
docExpiry = 150 * time.Millisecond
t.Cleanup(func() { docExpiry = old })
got, _ := r.call(t, "scanner.scan", docArgs("start", false))
id := got.(map[string]any)["document"].(string)
if _, err := r.call(t, "scanner.scan", docArgs(id, true)); err != nil {
t.Fatal(err)
}
time.Sleep(400 * time.Millisecond)
if e, _ := os.ReadDir(folder); len(e) != 1 {
t.Errorf("%d files, want 1", len(e))
}
}
// 1539: an ask carries its deadline, and a device process that takes it up later does nothing for it: nothing is
// scanned, the answer says why, and the message ends.
func TestAnAskTakenUpAfterItsDeadlineIsDroppedBeforeAnyWork(t *testing.T) {
f := newFakeScanner(t, fixture(t, "scan.jpg"))
r, folder := scanRig(t, f, Settings{})
scans := 0
r.worker.Capture = func(ctx context.Context, req ScanRequest) ([]Capture, error) {
scans++
return nil, errors.New("scanned")
}
late, _ := json.Marshal(Ask{ID: "late", Op: OpScan, At: fixedNow.Add(-4 * time.Minute), Deadline: fixedNow.Add(-time.Second)})
if act, _ := r.worker.Handle(late, func() { t.Error("a late ask was acknowledged early as work to be done") }); act != actionTerm {
t.Errorf("action = %v, want the message ended", act)
}
if scans != 0 || len(*r.said) != 0 {
t.Errorf("%d scans, %d events for a late ask", scans, len(*r.said))
}
if raw := r.answers.get("late"); !strings.Contains(string(raw), "deadline") {
t.Errorf("answer = %s", raw)
}
onTime, _ := json.Marshal(Ask{ID: "ontime", Op: OpScan, At: fixedNow, Deadline: fixedNow.Add(time.Minute)})
r.worker.Handle(onTime, nil)
if scans != 1 {
t.Errorf("an ask in time was not scanned")
}
_ = folder
}
// 1539: a scan the device process took up in time but ran past the tool's start-by wait is still filed: the tool
// follows it for the run's own bound.
func TestAScanThatRunsPastTheStartByWaitIsStillFiled(t *testing.T) {
f := newFakeScanner(t, fixture(t, "scan.jpg"))
r, folder := scanRig(t, f, Settings{})
old := scanWait
scanWait = 100 * time.Millisecond
t.Cleanup(func() { scanWait = old })
release := make(chan struct{})
r.worker.Capture = standInCapture(release, nil)
got, err := r.call(t, "scanner.scan", map[string]any{})
if err != nil || got.(map[string]any)["started"] != true {
t.Fatalf("answer = %v, %v, want started", got, err)
}
time.Sleep(300 * time.Millisecond) // past the start-by wait; the answer comes only now
close(release)
waitFor(t, "the late scan to be filed", func() bool { e, _ := os.ReadDir(folder); return len(e) == 1 })
}
// 1538: an expired document whose closing fails is not forgotten: it is tried again.
func TestAnExpiredDocumentWhoseClosingFailsIsClosedOnTheNextTry(t *testing.T) {
f := newFakeScanner(t, fixture(t, "scan.jpg"))
r, folder := scanRig(t, f, Settings{Crop: false, Resolution: 200})
oldE, oldR := docExpiry, docRetry
docExpiry, docRetry = 100*time.Millisecond, 100*time.Millisecond
t.Cleanup(func() { docExpiry, docRetry = oldE, oldR })
askBus = &flakyBus{fakeAskBus: &fakeAskBus{worker: r.worker, answers: r.answers}, fails: 2,
match: func(a Ask) bool { return a.Op == OpScan && a.Finish }}
if _, err := r.call(t, "scanner.scan", docArgs("start", false)); err != nil {
t.Fatal(err)
}
waitFor(t, "the document to be filed on the third try", func() bool { e, _ := os.ReadDir(folder); return len(e) == 1 })
}
// 1538: after a restart of the tool process the open documents are found again in the bucket and their expiry
// armed from the time of their last page; closing a document deletes its pages.
func TestAnOpenDocumentIsFoundAgainAfterARestartAndItsPagesAreDeletedWhenClosed(t *testing.T) {
f := newFakeScanner(t, fixture(t, "scan.jpg"))
r, folder := scanRig(t, f, Settings{Crop: false, Resolution: 200})
got, err := r.call(t, "scanner.scan", docArgs("start", false))
if err != nil {
t.Fatal(err)
}
id := got.(map[string]any)["document"].(string)
// A new tool process: no timers. Its Recover finds the document and arms it.
old := docExpiry
docExpiry = 200 * time.Millisecond
t.Cleanup(func() { docExpiry = old })
s := &Service{Settings: Settings{Scanner: f.base(), Folder: folder, Resolution: 200, ColorMode: "color"}, Now: func() time.Time { return fixedNow },
Asks: askBus, AnswerWait: 5 * time.Second, ScanWait: 5 * time.Second, AnswerPoll: 5 * time.Millisecond, Jobs: NewJobs(), Fetch: fetchClient, DocExpiry: docExpiry}
if err := s.Recover(context.Background()); err != nil {
t.Fatal(err)
}
waitFor(t, "the recovered document to be closed", func() bool { e, _ := os.ReadDir(folder); return len(e) == 1 })
waitFor(t, "its pages to be deleted", func() bool {
for _, k := range r.objects.keys() {
if strings.HasPrefix(k, "pending/"+id+"/") && strings.HasSuffix(k, ".jpg") {
return false
}
}
return true
})
var hasManifest bool
for _, k := range r.objects.keys() {
hasManifest = hasManifest || k == "pending/"+id+"/manifest.json"
}
if !hasManifest {
t.Error("the manifest that says the document is closed was deleted too")
}
}
// 1552: a finished job's outcome is forgotten after jobKeep.
func TestAFinishedJobIsForgottenAfterAnHour(t *testing.T) {
old := jobKeep
jobKeep = 30 * time.Millisecond
t.Cleanup(func() { jobKeep = old })
j := NewJobs()
first := j.put("a")
close(first.done)
time.Sleep(60 * time.Millisecond)
j.put("b")
if j.get("a") != nil || j.get("b") == nil {
t.Errorf("a = %v, b = %v", j.get("a"), j.get("b"))
}
}
// 1528: without apart three documents on the glass are one, cut around all of them, as before.
func TestWithoutApartSeveralDocumentsOnTheGlassAreOneScan(t *testing.T) {
scan := withDensity(onWhite(t, 11, image.Rect(100, 100, 300, 300), image.Rect(450, 100, 650, 400), image.Rect(100, 600, 400, 900)), 100)
f := newFakeScanner(t, scan)
r, folder := scanRig(t, f, Settings{Crop: true, Resolution: 100})
body, err := r.scan(t)
if err != nil {
t.Fatal(err)
}
if body["documents"] != nil || body["path"] == nil || len(*r.said) != 1 {
t.Errorf("answer = %v, %d events", body, len(*r.said))
}
if e, _ := os.ReadDir(folder); len(e) != 1 {
t.Errorf("%d files, want 1", len(e))
}
if _, err := r.call(t, "scanner.scan", map[string]any{"apart": true, "source": "feeder"}); err == nil || !strings.Contains(err.Error(), "apart is for the glass") {
t.Errorf("apart on the feeder: %v", err)
}
}