brother: the scanner seat declared and claimed, its verbs scan, status and list; preferences untrusted (issue 1453, 1446)
This commit is contained in:
@@ -41,9 +41,9 @@ makes it (mode 0755) when it files the first scan.
|
||||
|
||||
| tool | does |
|
||||
|---|---|
|
||||
| `scan_now` | scans, crops, files and emits; answers the event's body |
|
||||
| `scan_status` | whether the scanner answers, its state (Idle or Processing) and its document feeder's state; a scanner that does not answer is `answers: false` with why. This is how its health is read: the module has no long-running resource to declare a health check on |
|
||||
| `scan_list` | the newest filed scans, newest first (`limit`, default 20). Read-only |
|
||||
| `scanner.scan` | scans, crops, files and emits; answers the event's body |
|
||||
| `scanner.status` | whether the scanner answers, its state (Idle or Processing) and its document feeder's state; a scanner that does not answer is `answers: false` with why. This is how its health is read: the module has no long-running resource to declare a health check on |
|
||||
| `scanner.list` | the newest filed scans, newest first (`limit`, default 20). Read-only |
|
||||
|
||||
## The event
|
||||
|
||||
@@ -58,7 +58,7 @@ Each directory or archive this module declares says how it treats a file it did
|
||||
- `${dir:state}`: everything in it is the program's; the mesh moves nothing out (ADR 0422 §2).
|
||||
- The scan folder (`scan-folder`) is declared by no manifest. The module writes only the `scan-*.pdf` files it
|
||||
files and their `.scan-*.tmp` while they are written; it never changes, moves or removes any other file there,
|
||||
and `scan_list` only reads.
|
||||
and `scanner.list` only reads.
|
||||
|
||||
## Tests
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ func emit(body map[string]any, id string) error {
|
||||
return stdio.Emit(EventAdded, body, stdio.EmitOptions{MessageID: id})
|
||||
}
|
||||
|
||||
// tools are the three tools. The settings are read at each call, so a changed config value is the next
|
||||
// tools are the scanner seat's three verbs, served as <seat>.<verb> (ADR 0467 clause 2). The settings are read at each call, so a changed config value is the next
|
||||
// call's.
|
||||
func tools(settingsFile string, say Emitter, now func() time.Time) []stdio.Tool {
|
||||
service := func() (*Service, error) {
|
||||
@@ -33,7 +33,7 @@ func tools(settingsFile string, say Emitter, now func() time.Time) []stdio.Tool
|
||||
}
|
||||
return []stdio.Tool{
|
||||
{
|
||||
Name: "scan_now",
|
||||
Name: "scanner.scan",
|
||||
Description: "Scan the glass of the configured network scanner, cut the document out of the platen's white, " +
|
||||
"file it as a one-page PDF in the scan folder and say scan.added on the bus. Answers the event's body: " +
|
||||
"the file's path, its size and sha256, its real size in millimetres, the resolution the scanner " +
|
||||
@@ -47,7 +47,7 @@ func tools(settingsFile string, say Emitter, now func() time.Time) []stdio.Tool
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "scan_status",
|
||||
Name: "scanner.status",
|
||||
Description: "Whether the configured scanner answers, and what it is doing: its state (Idle or Processing) " +
|
||||
"and its document feeder's state. A scanner that does not answer is said as answers false with why. Read-only.",
|
||||
Run: func(map[string]any) (any, error) {
|
||||
@@ -61,7 +61,7 @@ func tools(settingsFile string, say Emitter, now func() time.Time) []stdio.Tool
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "scan_list",
|
||||
Name: "scanner.list",
|
||||
Description: "The newest scans filed in the scan folder, newest first, with their size and time. Read-only.",
|
||||
Input: map[string]any{
|
||||
"type": "object",
|
||||
|
||||
@@ -11,6 +11,8 @@ import (
|
||||
|
||||
type manifest struct {
|
||||
Emits []string `json:"emits"`
|
||||
Seats []seat `json:"seats"`
|
||||
Claims []seat `json:"claims"`
|
||||
Tools []string `json:"tools"`
|
||||
Config map[string]map[string]any `json:"config"`
|
||||
Resources []map[string]any `json:"resources"`
|
||||
@@ -19,6 +21,12 @@ type manifest struct {
|
||||
} `json:"build"`
|
||||
}
|
||||
|
||||
type seat struct {
|
||||
Name string `json:"name"`
|
||||
Scope string `json:"scope"`
|
||||
Serves []string `json:"serves"`
|
||||
}
|
||||
|
||||
func readManifest(t *testing.T) manifest {
|
||||
t.Helper()
|
||||
raw, err := os.ReadFile(filepath.Join("..", "..", "module.json"))
|
||||
@@ -116,3 +124,41 @@ func TestTheConfigReachesTheCodeThroughItsSettingsFile(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 1453/AC6: the manifest declares the scanner seat, held once for the mesh, and claims it, with the verbs
|
||||
// ADR 0467 clause 2 names; the module serves each verb as the tool <seat>.<verb>.
|
||||
func TestTheManifestDeclaresAndClaimsTheScannerSeat(t *testing.T) {
|
||||
m := readManifest(t)
|
||||
want := seat{Name: "scanner", Scope: "mesh", Serves: []string{"scan", "status", "list"}}
|
||||
for what, got := range map[string][]seat{"seats": m.Seats, "claims": m.Claims} {
|
||||
found := false
|
||||
for _, s := range got {
|
||||
if s.Name == "scanner" {
|
||||
found = true
|
||||
if !reflect.DeepEqual(s, want) {
|
||||
t.Errorf("%s: scanner = %+v, want %+v", what, s, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("%s does not hold the scanner seat", what)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 1453/AC6 (issue 1446): the three preferences are not trusted, so a person's own setting is not refused.
|
||||
func TestThePreferencesAreNotTrusted(t *testing.T) {
|
||||
m := readManifest(t)
|
||||
for _, k := range []string{"resolution", "color-mode", "crop"} {
|
||||
if v, ok := m.Config[k]["trusted"]; !ok || v != false {
|
||||
t.Errorf("%s: trusted = %v, want false", k, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 1453/AC6: the scan module is gone from the catalogue.
|
||||
func TestTheScanModuleIsGone(t *testing.T) {
|
||||
if _, err := os.Stat(filepath.Join("..", "..", "..", "scan")); !os.IsNotExist(err) {
|
||||
t.Errorf("modules/scan still exists (err %v)", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,7 +209,7 @@ func TestEveryToolSaysWhichValueIsUnsetAndWritesNothing(t *testing.T) {
|
||||
js := strings.ReplaceAll(strings.Replace(c.json, "%s", f.base(), 1), "FOLDER", folder)
|
||||
var out []said
|
||||
tl := tools(settingsFile(t, js), func(b map[string]any, id string) error { out = append(out, said{b, id, true}); return nil }, time.Now)
|
||||
for _, name := range []string{"scan_now", "scan_status", "scan_list"} {
|
||||
for _, name := range []string{"scanner.scan", "scanner.status", "scanner.list"} {
|
||||
_, err := tool(t, 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)
|
||||
@@ -227,7 +227,7 @@ func TestEveryToolSaysWhichValueIsUnsetAndWritesNothing(t *testing.T) {
|
||||
// 1440/AC7: the settings file is not there at all.
|
||||
func TestAMissingSettingsFileIsSaid(t *testing.T) {
|
||||
tl := tools(filepath.Join(t.TempDir(), "none.json"), nil, time.Now)
|
||||
if _, err := tool(t, tl, "scan_status").Run(nil); err == nil || !strings.Contains(err.Error(), "settings") {
|
||||
if _, err := tool(t, tl, "scanner.status").Run(nil); err == nil || !strings.Contains(err.Error(), "settings") {
|
||||
t.Fatalf("error = %v", err)
|
||||
}
|
||||
}
|
||||
@@ -260,7 +260,7 @@ func TestScanListToolListsTheNewestFirst(t *testing.T) {
|
||||
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+`"}`), nil, time.Now)
|
||||
got, err := tool(t, tl, "scan_list").Run(map[string]any{"limit": float64(5)})
|
||||
got, err := tool(t, tl, "scanner.list").Run(map[string]any{"limit": float64(5)})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -268,7 +268,7 @@ func TestScanListToolListsTheNewestFirst(t *testing.T) {
|
||||
if len(scans) != 2 || filepath.Base(scans[0].Path) != "scan-2.pdf" {
|
||||
t.Fatalf("scans = %v", scans)
|
||||
}
|
||||
if _, err := tool(t, tl, "scan_list").Run(map[string]any{"limit": float64(0)}); err == nil {
|
||||
if _, err := tool(t, tl, "scanner.list").Run(map[string]any{"limit": float64(0)}); err == nil {
|
||||
t.Error("limit 0 accepted")
|
||||
}
|
||||
}
|
||||
@@ -280,7 +280,7 @@ func TestScanNowToolScansWithTheFileSettings(t *testing.T) {
|
||||
var out []said
|
||||
tl := tools(settingsFile(t, `{"scanner":"`+f.base()+`","scan-folder":"`+folder+`","resolution":300,"color-mode":"color","crop":true}`),
|
||||
func(b map[string]any, id string) error { out = append(out, said{b, id, true}); return nil }, time.Now)
|
||||
got, err := tool(t, tl, "scan_now").Run(map[string]any{})
|
||||
got, err := tool(t, tl, "scanner.scan").Run(map[string]any{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
300,
|
||||
600
|
||||
],
|
||||
"trusted": false,
|
||||
"default": 300
|
||||
},
|
||||
"color-mode": {
|
||||
@@ -37,18 +38,42 @@
|
||||
"color",
|
||||
"gray"
|
||||
],
|
||||
"trusted": false,
|
||||
"default": "color"
|
||||
},
|
||||
"crop": {
|
||||
"type": "boolean",
|
||||
"description": "Whether the document is cut out of the platen's white lid; false files the whole platen.",
|
||||
"trusted": false,
|
||||
"default": true
|
||||
}
|
||||
},
|
||||
"seats": [
|
||||
{
|
||||
"name": "scanner",
|
||||
"scope": "mesh",
|
||||
"serves": [
|
||||
"scan",
|
||||
"status",
|
||||
"list"
|
||||
]
|
||||
}
|
||||
],
|
||||
"claims": [
|
||||
{
|
||||
"name": "scanner",
|
||||
"scope": "mesh",
|
||||
"serves": [
|
||||
"scan",
|
||||
"status",
|
||||
"list"
|
||||
]
|
||||
}
|
||||
],
|
||||
"tools": [
|
||||
"scan_now",
|
||||
"scan_status",
|
||||
"scan_list"
|
||||
"scanner.scan",
|
||||
"scanner.status",
|
||||
"scanner.list"
|
||||
],
|
||||
"resources": [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user