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
323 lines
13 KiB
Go
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)
|
|
}
|
|
}
|