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
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.
337 lines
12 KiB
Go
337 lines
12 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
stdio "git.novox.be/novox/mesh-sdk/go"
|
|
)
|
|
|
|
type said struct {
|
|
body map[string]any
|
|
id string
|
|
// seen is whether the scan was kept in the bucket when it was said.
|
|
seen bool
|
|
}
|
|
|
|
// scanRig is the tools, the device worker and a bucket in memory over a settings file for the fake scanner and a
|
|
// scan folder of the test's own, with the real defaults laid under what s sets.
|
|
func scanRig(t *testing.T, f *fakeScanner, s Settings) (*printerRig, string) {
|
|
t.Helper()
|
|
if s.Scanner == "" {
|
|
s.Scanner = f.base()
|
|
}
|
|
if s.Folder == "" {
|
|
s.Folder = filepath.Join(t.TempDir(), "scans")
|
|
}
|
|
if s.Resolution == 0 {
|
|
s.Resolution = 300
|
|
}
|
|
if s.ColorMode == "" {
|
|
s.ColorMode = "color"
|
|
}
|
|
js, _ := json.Marshal(map[string]any{"scanner": s.Scanner, "scan-folder": s.Folder, "resolution": s.Resolution,
|
|
"color-mode": s.ColorMode, "crop": s.Crop})
|
|
return newRig(t, string(js), "pw"), s.Folder
|
|
}
|
|
|
|
func (r *printerRig) scan(t *testing.T) (map[string]any, error) {
|
|
t.Helper()
|
|
got, err := r.call(t, "scanner.scan", map[string]any{})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return got.(map[string]any), nil
|
|
}
|
|
|
|
// 1440/AC2: asked 300 dpi, the scanner gave 200: the event's resolution and millimetres, and the PDF's page,
|
|
// are the image's own.
|
|
func TestTheFiledScanIsAtTheImagesOwnResolution(t *testing.T) {
|
|
f := newFakeScanner(t, fixture(t, "scan.jpg"))
|
|
r, _ := scanRig(t, f, Settings{Crop: false, Resolution: 300})
|
|
body, err := r.scan(t)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if body["resolution"] != float64(200) {
|
|
t.Errorf("resolution = %v, want the image's 200 (300 was asked)", body["resolution"])
|
|
}
|
|
if body["width-mm"] != 215.4 || body["height-mm"] != 279.8 {
|
|
t.Errorf("size = %v x %v mm, want 215.4 x 279.8 (1696 x 2203 px at 200 dpi)", body["width-mm"], body["height-mm"])
|
|
}
|
|
pdf, _ := os.ReadFile(body["path"].(string))
|
|
if !strings.Contains(string(pdf), "/MediaBox [0 0 610.56 793.08]") {
|
|
t.Errorf("the page is not 200 dpi's size")
|
|
}
|
|
}
|
|
|
|
// 1440/AC6 and 1453/AC9: one scan.added, said by the device process after the PDF is kept in the bucket under its
|
|
// sha256, with every key, the object's key and never the image; the tool then files the PDF.
|
|
func TestScanNowEmitsOneEventAfterTheScanIsKeptUnderTheSha256(t *testing.T) {
|
|
f := newFakeScanner(t, fixture(t, "scan.jpg"))
|
|
r, folder := scanRig(t, f, Settings{Crop: true})
|
|
body, err := r.scan(t)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
out := *r.said
|
|
if len(out) != 1 {
|
|
t.Fatalf("%d events, want 1", len(out))
|
|
}
|
|
ev := out[0]
|
|
if !ev.seen {
|
|
t.Error("the event was said before the PDF was kept in the bucket")
|
|
}
|
|
for _, k := range []string{"object", "node", "bytes", "sha256", "width-mm", "height-mm", "resolution", "color-mode", "cropped", "pages", "scanned-at"} {
|
|
if _, ok := ev.body[k]; !ok {
|
|
t.Errorf("the event lacks %s", k)
|
|
}
|
|
}
|
|
if len(ev.body) != 11 {
|
|
t.Errorf("the event has %d keys, want the eleven named (no path: filing is the tool's)", len(ev.body))
|
|
}
|
|
pdf, err := os.ReadFile(body["path"].(string))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
sum := sha256.Sum256(pdf)
|
|
want := hex.EncodeToString(sum[:])
|
|
if ev.body["sha256"] != want || ev.id != want || ev.body["object"] != want+".pdf" {
|
|
t.Errorf("sha256 %v / message id %v / object %v, want the file's %s", ev.body["sha256"], ev.id, ev.body["object"], want)
|
|
}
|
|
if kept, err := r.objects.Get(t.Context(), want+".pdf"); err != nil || !bytes.Equal(kept, pdf) {
|
|
t.Errorf("the bucket does not hold the filed PDF under its sha256 (%v)", err)
|
|
}
|
|
if ev.body["bytes"] != len(pdf) || ev.body["node"] != "shanks" || ev.body["cropped"] != true || ev.body["color-mode"] != "color" {
|
|
t.Errorf("event = %v", ev.body)
|
|
}
|
|
if want := fixedNow.UTC().Format(time.RFC3339); ev.body["scanned-at"] != want {
|
|
t.Errorf("scanned-at = %v, want %v", ev.body["scanned-at"], want)
|
|
}
|
|
if bytes.Contains(pdf, []byte("scanned-at")) || len(body) != 12 {
|
|
t.Errorf("the tool's answer has %d keys, want the event's eleven and the path", len(body))
|
|
}
|
|
if _, has := body["key"]; has {
|
|
t.Errorf("the device process's own answer keys leak into the tool's: %v", body)
|
|
}
|
|
// The cropped scan is the receipt, about 80-84 mm wide (1440/AC3).
|
|
if w := ev.body["width-mm"].(float64); w < 80 || w > 84 {
|
|
t.Errorf("width = %v mm", w)
|
|
}
|
|
files, _ := os.ReadDir(folder)
|
|
if len(files) != 1 || filepath.Join(folder, files[0].Name()) != body["path"] {
|
|
t.Errorf("folder holds %v, want the one scan", files)
|
|
}
|
|
}
|
|
|
|
// 1453/AC9: the same scan twice is one object in the bucket: a key is kept once.
|
|
func TestTheSameScanTwiceIsOneObject(t *testing.T) {
|
|
f := newFakeScanner(t, fixture(t, "scan.jpg"))
|
|
r, _ := scanRig(t, f, Settings{Crop: true})
|
|
if _, err := r.scan(t); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
f.mu.Lock()
|
|
_ = f.documents
|
|
f.mu.Unlock()
|
|
if _, err := r.scan(t); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if r.objects.puts != 1 || len(r.objects.keys()) != 1 {
|
|
t.Errorf("%d puts, objects %v: want the one", r.objects.puts, r.objects.keys())
|
|
}
|
|
}
|
|
|
|
// 1440/AC6: a scanner that refuses keeps nothing, writes nothing, says nothing, and the error says why.
|
|
func TestAScannerRefusalKeepsWritesAndSaysNothing(t *testing.T) {
|
|
f := newFakeScanner(t, fixture(t, "scan.jpg"))
|
|
f.refuse = 503
|
|
r, folder := scanRig(t, f, Settings{})
|
|
_, err := r.scan(t)
|
|
if err == nil || !strings.Contains(err.Error(), "503") {
|
|
t.Fatalf("error = %v, want the refusal said", err)
|
|
}
|
|
if len(*r.said) != 0 || len(r.objects.keys()) != 0 {
|
|
t.Errorf("%d events, objects %v after a refusal", len(*r.said), r.objects.keys())
|
|
}
|
|
if _, err := os.Stat(folder); !os.IsNotExist(err) {
|
|
t.Errorf("the folder exists after a refusal: %v", err)
|
|
}
|
|
}
|
|
|
|
// 1440/AC6: a job that gives no page (the second NextDocument's 404) is the same.
|
|
func TestAJobThatGivesNoPageKeepsWritesAndSaysNothing(t *testing.T) {
|
|
f := newFakeScanner(t, fixture(t, "scan.jpg"))
|
|
f.noPage = true
|
|
r, folder := scanRig(t, f, Settings{})
|
|
if _, err := r.scan(t); err == nil || !strings.Contains(err.Error(), "404") {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
if len(*r.said) != 0 || len(r.objects.keys()) != 0 {
|
|
t.Errorf("%d events, objects %v", len(*r.said), r.objects.keys())
|
|
}
|
|
if _, err := os.Stat(folder); !os.IsNotExist(err) {
|
|
t.Errorf("the folder exists: %v", err)
|
|
}
|
|
}
|
|
|
|
// 1440/AC6: an image that is no JPEG (a scanner that sent something else) keeps and files nothing.
|
|
func TestAnImageThatIsNotAJPEGKeepsAndFilesNothing(t *testing.T) {
|
|
f := newFakeScanner(t, []byte("not an image"))
|
|
r, folder := scanRig(t, f, Settings{})
|
|
if _, err := r.scan(t); err == nil {
|
|
t.Fatal("a non-image was filed")
|
|
}
|
|
if len(*r.said) != 0 || len(r.objects.keys()) != 0 {
|
|
t.Errorf("%d events, objects %v", len(*r.said), r.objects.keys())
|
|
}
|
|
if _, err := os.Stat(folder); !os.IsNotExist(err) {
|
|
t.Errorf("the folder exists: %v", err)
|
|
}
|
|
}
|
|
|
|
// 1440/AC11: the first scan into a folder that is not there makes it.
|
|
func TestTheFirstScanMakesTheFolder(t *testing.T) {
|
|
f := newFakeScanner(t, fixture(t, "scan.jpg"))
|
|
r, folder := scanRig(t, f, Settings{})
|
|
if _, err := r.scan(t); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
info, err := os.Stat(folder)
|
|
if err != nil || info.Mode().Perm() != 0o755 {
|
|
t.Fatalf("folder %v %v", info, err)
|
|
}
|
|
}
|
|
|
|
func settingsFile(t *testing.T, json string) string {
|
|
t.Helper()
|
|
p := filepath.Join(t.TempDir(), "settings.json")
|
|
if err := os.WriteFile(p, []byte(json), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return p
|
|
}
|
|
|
|
func tool(t *testing.T, tl []stdio.Tool, name string) stdio.Tool {
|
|
t.Helper()
|
|
for _, x := range tl {
|
|
if x.Name == name {
|
|
return x
|
|
}
|
|
}
|
|
t.Fatalf("no tool %s", name)
|
|
return stdio.Tool{}
|
|
}
|
|
|
|
// 1440/AC7: scanner or folder unset: every tool says which, and nothing is written or asked.
|
|
func TestEveryToolSaysWhichValueIsUnsetAndWritesNothing(t *testing.T) {
|
|
f := newFakeScanner(t, fixture(t, "scan.jpg"))
|
|
for _, c := range []struct{ json, want string }{
|
|
{`{"scanner":"` + "%s" + `","scan-folder":""}`, "scan-folder"},
|
|
{`{"scanner":"","scan-folder":"FOLDER"}`, "scanner"},
|
|
{`{}`, "scanner and scan-folder"},
|
|
} {
|
|
folder := filepath.Join(t.TempDir(), "scans")
|
|
js := strings.ReplaceAll(strings.Replace(c.json, "%s", f.base(), 1), "FOLDER", folder)
|
|
r := newRig(t, js, "pw")
|
|
out := *r.said
|
|
for _, name := range []string{"scanner.scan", "scanner.status", "scanner.list"} {
|
|
_, err := tool(t, r.tl, name).Run(map[string]any{})
|
|
if err == nil || !strings.Contains(err.Error(), c.want) {
|
|
t.Errorf("%s with %s: error = %v, want it to name %q", name, c.json, err, c.want)
|
|
}
|
|
}
|
|
if f.posts != 0 || f.documents != 0 || len(out) != 0 || r.bus.asks() != 0 || len(r.objects.keys()) != 0 {
|
|
t.Errorf("the scanner was asked, an ask queued or an event said: %d posts, %d documents, %d events, %d asks", f.posts, f.documents, len(out), r.bus.asks())
|
|
}
|
|
if _, err := os.Stat(folder); !os.IsNotExist(err) {
|
|
t.Errorf("the folder was made: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 1440/AC7: the settings file is not there at all.
|
|
func TestAMissingSettingsFileIsSaid(t *testing.T) {
|
|
tl := tools(filepath.Join(t.TempDir(), "none.json"), time.Now)
|
|
if _, err := tool(t, tl, "scanner.status").Run(nil); err == nil || !strings.Contains(err.Error(), "settings") {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
}
|
|
|
|
// 1440/AC8: scan_status from the captured idle status; a scanner that does not answer is answers false.
|
|
func TestScanStatusSaysStateAndAdfState(t *testing.T) {
|
|
f := newFakeScanner(t, nil)
|
|
svc := &Service{Settings: Settings{Scanner: f.base(), Folder: "/x"}}
|
|
got, err := svc.ScanStatus(context.Background())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got["answers"] != true || got["state"] != "Idle" || got["adf-state"] != "ScannerAdfEmpty" {
|
|
t.Fatalf("status = %v", got)
|
|
}
|
|
f.Close()
|
|
got, err = svc.ScanStatus(context.Background())
|
|
if err != nil || got["answers"] != false || got["why"] == "" {
|
|
t.Fatalf("a scanner that is gone: %v, %v", got, err)
|
|
}
|
|
}
|
|
|
|
// 1440/AC8: scan_list through the tool, newest first.
|
|
func TestScanListToolListsTheNewestFirst(t *testing.T) {
|
|
folder := t.TempDir()
|
|
base := time.Date(2026, 10, 1, 12, 0, 0, 0, time.UTC)
|
|
for i, n := range []string{"scan-1.pdf", "scan-2.pdf"} {
|
|
p := filepath.Join(folder, n)
|
|
os.WriteFile(p, []byte("x"), 0o644)
|
|
os.Chtimes(p, base.Add(time.Duration(i)*time.Hour), base.Add(time.Duration(i)*time.Hour))
|
|
}
|
|
tl := tools(settingsFile(t, `{"scanner":"http://192.0.2.10/eSCL","scan-folder":"`+folder+`"}`), time.Now)
|
|
got, err := tool(t, tl, "scanner.list").Run(map[string]any{"limit": float64(5)})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
scans := got.(map[string]any)["scans"].([]Entry)
|
|
if len(scans) != 2 || filepath.Base(scans[0].Path) != "scan-2.pdf" {
|
|
t.Fatalf("scans = %v", scans)
|
|
}
|
|
if _, err := tool(t, tl, "scanner.list").Run(map[string]any{"limit": float64(0)}); err == nil {
|
|
t.Error("limit 0 accepted")
|
|
}
|
|
}
|
|
|
|
// 1440/AC1: the whole path through the tool, settings read from the file, with the real defaults.
|
|
func TestScanNowToolScansWithTheFileSettings(t *testing.T) {
|
|
f := newFakeScanner(t, fixture(t, "scan.jpg"))
|
|
folder := filepath.Join(t.TempDir(), "scans")
|
|
r := newRig(t, `{"scanner":"`+f.base()+`","scan-folder":"`+folder+`","resolution":300,"color-mode":"color","crop":true}`, "pw")
|
|
got, err := r.call(t, "scanner.scan", map[string]any{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(*r.said) != 1 || got.(map[string]any)["cropped"] != true {
|
|
t.Fatalf("%d events, body %v", len(*r.said), got)
|
|
}
|
|
}
|
|
|
|
func TestLoadSettingsLaysTheDefaultsUnder(t *testing.T) {
|
|
s, err := LoadSettings(settingsFile(t, `{"scanner":" http://192.0.2.10/eSCL ","scan-folder":"/srv/scans"}`))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if s.Scanner != "http://192.0.2.10/eSCL" || s.Folder != "/srv/scans" || s.Resolution != 300 || s.ColorMode != "color" || !s.Crop {
|
|
t.Fatalf("settings = %+v", s)
|
|
}
|
|
s, _ = LoadSettings(settingsFile(t, `{"resolution":600,"color-mode":"gray","crop":false}`))
|
|
if s.Resolution != 600 || s.ColorMode != "gray" || s.Crop {
|
|
t.Fatalf("settings = %+v", s)
|
|
}
|
|
}
|