Files
mesh-catalog/modules/i3status-rust/cmd/i3status-rust-tools/manifest_helpers_test.go
T

310 lines
9.6 KiB
Go

package main
import (
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
)
// The module's manifest, read the way the catalogue reads it, for the manifest tests. The same in
// every desktop module that carries it.
type manifest struct {
Module string `json:"module"`
Version string `json:"version"`
Moved bool `json:"moved"`
Items []item `json:"items"`
Capabilities []string `json:"capabilities"`
Requires []string `json:"requires"`
Claims []claim `json:"claims"`
Seats []any `json:"seats"`
Tools []string `json:"tools"`
Environment *environment `json:"environment"`
Shell []shellCode `json:"shell"`
Resources []map[string]any `json:"resources"`
// Lines for other modules' seats (novox/hq ADR 0212): the window manager's, here.
Contributions []any `json:"contributions"`
Build struct {
Artifacts []map[string]any `json:"artifacts"`
} `json:"build"`
}
type claim struct {
Name string `json:"name"`
Scope string `json:"scope"`
Serves []string `json:"serves"`
// How the holder renders each kind its seat receives as data (novox/hq ADR 0255).
Renders map[string]string `json:"renders"`
}
type environment struct {
Variables map[string]string `json:"variables"`
Path []map[string]any `json:"path"`
}
type shellCode struct {
For string `json:"for"`
Slot string `json:"slot"`
Code string `json:"code"`
}
// An item of a collection (novox/hq ADR 0349 clause 13): its text is the template file beside the manifest.
type item struct {
Collection string `json:"collection"`
Name string `json:"name"`
Attributes map[string]string `json:"attributes"`
Template string `json:"template"`
}
// shell is the code the module adds to a shell's or the session's start, read from the templates of its items of
// the collections shell:<file>:<slot>, in the order they are declared.
func (m manifest) shell(t *testing.T) []shellCode {
t.Helper()
var out []shellCode
for _, it := range m.Items {
parts := strings.Split(it.Collection, ":")
if len(parts) != 3 || parts[0] != "shell" {
continue
}
text, err := os.ReadFile(filepath.Join("..", "..", it.Template))
if err != nil {
t.Fatal(err)
}
out = append(out, shellCode{For: parts[1], Slot: parts[2], Code: string(text)})
}
return out
}
// environmentOf is the variables the module sets, read from the templates of its items of the environment.
func (m manifest) environmentOf(t *testing.T) *environment {
t.Helper()
var env *environment
for _, it := range m.Items {
if it.Collection != "environment" || it.Attributes["kind"] != "variable" {
continue
}
text, err := os.ReadFile(filepath.Join("..", "..", it.Template))
if err != nil {
t.Fatal(err)
}
if env == nil {
env = &environment{Variables: map[string]string{}}
}
env.Variables[it.Attributes["name"]] = strings.TrimSuffix(string(text), "\n")
}
return env
}
func readManifest(t *testing.T) manifest {
t.Helper()
raw, err := os.ReadFile(filepath.Join("..", "..", "module.json"))
if err != nil {
t.Fatal(err)
}
dec := json.NewDecoder(strings.NewReader(string(raw)))
dec.DisallowUnknownFields()
var m manifest
if err := dec.Decode(&m); err != nil {
t.Fatalf("module.json: %v", err)
}
m.Environment = m.environmentOf(t)
return m
}
func (m manifest) resource(t *testing.T, id string) map[string]any {
t.Helper()
for _, r := range m.Resources {
if r["id"] == id {
return r
}
}
t.Fatalf("no resource %q", id)
return nil
}
func (m manifest) packages() (present, absent []string) {
for _, r := range m.Resources {
if r["type"] == "package" {
if r["absent"] == true {
absent = append(absent, r["package"].(string))
} else {
present = append(present, r["package"].(string))
}
}
}
return present, absent
}
// fromTemplate checks that a file resource takes its text from its template file in the module's
// repository (novox/hq ADR 0349 §2): it names the template, carries no text of its own beside it, and the
// template is there. The readable file in the repository is the one place the machine's text is written.
func (m manifest) fromTemplate(t *testing.T, id, template string) {
t.Helper()
r := m.resource(t, id)
if r["type"] != "file" {
t.Fatalf("%s is a %v, not a file", id, r["type"])
}
if r["template"] != template {
t.Fatalf("resource %s does not take its text from %s: template %v", id, template, r["template"])
}
if _, inline := r["content"]; inline {
t.Fatalf("resource %s keeps its text inline beside its template: a file's text is in one place", id)
}
if _, err := os.Stat(filepath.Join("..", "..", template)); err != nil {
t.Fatalf("resource %s names %s: %v", id, template, err)
}
if r["owner"] != "${machine:account}" && !strings.HasPrefix(r["path"].(string), "/etc/") {
t.Fatalf("%s under the home is the account's", id)
}
}
// everyFileIsATemplate checks that no file resource carries its text inline, and that the build packs
// files/ into the one archive the controller reads the templates back from (novox/hq ADR 0349 §2).
func (m manifest) everyFileIsATemplate(t *testing.T) {
t.Helper()
for _, r := range m.Resources {
if _, inline := r["content"]; inline {
t.Errorf("resource %v keeps its text inline: keep it in files/ and name it as the template", r["id"])
}
}
packs := 0
for _, a := range m.Build.Artifacts {
if a["kind"] == "archive" && a["from"] == "files" {
packs++
if a["name"] != "files" {
t.Errorf("the templates archive is named %v, not files", a["name"])
}
}
}
if packs != 1 {
t.Errorf("the build packs files/ into %d archives, not one", packs)
}
}
// text is the text a file resource places on the machine: its template file's.
func (m manifest) text(t *testing.T, id string) string {
t.Helper()
name, _ := m.resource(t, id)["template"].(string)
if name == "" {
t.Fatalf("resource %s names no template", id)
}
raw, err := os.ReadFile(filepath.Join("..", "..", name))
if err != nil {
t.Fatal(err)
}
return string(raw)
}
// checkTheToolsAgree checks that the manifest lists the module's own tools exactly, that the bundle
// serves each seat verb the claims promise as <seat>.<verb>, and that the Go bundle is declared.
func checkTheToolsAgree(t *testing.T, m manifest) {
t.Helper()
own, seat := map[string]bool{}, map[string]bool{}
for _, tool := range tools() {
if strings.Contains(tool.Name, ".") {
seat[tool.Name] = true
} else {
own[tool.Name] = true
}
if strings.TrimSpace(tool.Description) == "" {
t.Errorf("%s has no description", tool.Name)
}
}
listed := map[string]bool{}
for _, name := range m.Tools {
listed[name] = true
if !own[name] {
t.Errorf("module.json lists %s, which the bundle does not serve", name)
}
}
for name := range own {
if !listed[name] {
t.Errorf("the bundle serves %s, which module.json does not list", name)
}
if !strings.HasPrefix(name, strings.ReplaceAll(m.Module, "-", "_")+"_") {
t.Errorf("%s is not prefixed with the module's name", name)
}
}
promised := map[string]bool{}
for _, c := range m.Claims {
for _, verb := range c.Serves {
promised[c.Name+"."+verb] = true
if !seat[c.Name+"."+verb] {
t.Errorf("the claim on %s promises %s, which the bundle does not serve", c.Name, verb)
}
}
}
for name := range seat {
if !promised[name] {
t.Errorf("the bundle serves %s, which no claim promises", name)
}
}
var bundle map[string]any
for _, a := range m.Build.Artifacts {
if a["kind"] == "bundle" {
bundle = a
}
}
if bundle == nil || bundle["language"] != "go" || bundle["system"] != "arch" ||
bundle["from"] != "cmd/"+m.Module+"-tools" || bundle["binary"] != m.Module+"-tools" {
t.Errorf("the Go tools bundle: %v", bundle)
}
}
// checkNoSecretsOrInstallationNames refuses what a catalogue manifest must never carry.
func checkNoSecretsOrInstallationNames(t *testing.T) {
t.Helper()
raw, err := os.ReadFile(filepath.Join("..", "..", "module.json"))
if err != nil {
t.Fatal(err)
}
s := strings.ToLower(string(raw))
for _, never := range []string{"/home/", "jochen", "g14", "shanks", "novox.be", "api_key", ".hal/", "greenclip daemon"} {
if strings.Contains(s, never) {
t.Errorf("module.json names %q", never)
}
}
}
// displaySession is the collection the module's lines for the window manager are an item of.
const displaySession = "contribution:node-display-session:config"
// i3Lines is what the module contributes to the window manager: the text of its item of the collection
// contribution:node-display-session:config (design 57 L3, issue 848).
func (m manifest) i3Lines() string {
var out string
for _, it := range m.Items {
if it.Collection == displaySession {
text, err := os.ReadFile(filepath.Join("..", "..", it.Template))
if err != nil {
return ""
}
out += string(text)
}
}
return out
}
// i3LinesAreSource checks the window-manager lines are one item whose template is the source file, and that no
// old-form contribution to node-display-session is left.
func (m manifest) i3LinesAreSource(t *testing.T, source string) {
t.Helper()
if len(m.Contributions) != 0 {
t.Fatalf("the module still holds contributions: the lines are an item of %s", displaySession)
}
var templates []string
for _, it := range m.Items {
if it.Collection == displaySession {
templates = append(templates, it.Template)
}
}
if len(templates) != 1 || templates[0] != source {
t.Fatalf("the items of %s are %v, want the one template %s", displaySession, templates, source)
}
if m.i3Lines() == "" {
t.Fatalf("the template %s holds no line", source)
}
}