310 lines
9.6 KiB
Go
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)
|
|
}
|
|
}
|