flatpak: the package, Flathub with it, and the installations as tools (hq to-be 42 phase 2.9)

The package ships Flathub in /usr/share/flatpak/remotes.d, so the module
declares no remote of its own and checks it instead. Eleven Go tools: list,
runtimes, remotes (naming the desktop's duplicate user Flathub), updates,
unused, disk usage, and install, remove, update and remove-unused, the
system installation's acts through sudo -n and the account's without.

uninstall --unused has no dry run, so flatpak_unused works it out from
flatpak's own answers: an application's runtime and SDK, the extension
points of what is used, and pins. Acts run as jobs inside the bundle,
because an install outlasts a call.
This commit is contained in:
jochen
2026-10-04 13:02:23 +02:00
parent db297e8bdd
commit 152ef9621d
12 changed files with 1900 additions and 0 deletions
@@ -0,0 +1,249 @@
package main
import (
"strings"
"testing"
)
func TestTheManifestIsFlatpaksPackageAndFlathubComesWithIt(t *testing.T) {
m := readManifest(t)
holdsTheBundle(t, m, "flatpak")
if got := strings.Join(m.packages(), ","); got != "flatpak" {
t.Errorf("packages %s", got)
}
// The package ships flathub in /usr/share/flatpak/remotes.d: the module declares no remote file.
if len(m.Resources) != 1 {
t.Errorf("one resource: %v", m.Resources)
}
}
const nb = " "
// The desktop's installation on 2026-10-04, as flatpak list prints it.
var apps = "com.nextcloud.desktopclient.nextcloud\tNextcloud Desktop\t3.14\tstable\tx86_64\tflathub\tsystem\t275.8" + nb + "MB\tcom.nextcloud.desktopclient.nextcloud/x86_64/stable\n" +
"tv.plex.PlexDesktop\tPlex\t1.9\tstable\tx86_64\tflathub\tsystem\t368.0" + nb + "MB\ttv.plex.PlexDesktop/x86_64/stable\n"
var runtimes = strings.Join([]string{
"org.freedesktop.Platform\tFreedesktop Platform\t22.08.1\t22.08\tx86_64\tflathub\tsystem\t576.7" + nb + "MB\torg.freedesktop.Platform/x86_64/22.08",
"org.freedesktop.Platform\tFreedesktop Platform\t23.08.1\t23.08\tx86_64\tflathub\tsystem\t598.3" + nb + "MB\torg.freedesktop.Platform/x86_64/23.08",
"org.freedesktop.Platform.GL.default\tMesa\t\t22.08\tx86_64\tflathub\tsystem\t442.1" + nb + "MB\torg.freedesktop.Platform.GL.default/x86_64/22.08",
"org.freedesktop.Platform.GL.default\tMesa\t\t23.08\tx86_64\tflathub\tsystem\t533.8" + nb + "MB\torg.freedesktop.Platform.GL.default/x86_64/23.08",
"org.freedesktop.Platform.GL.default\tMesa\t\t23.08-extra\tx86_64\tflathub\tsystem\t533.8" + nb + "MB\torg.freedesktop.Platform.GL.default/x86_64/23.08-extra",
"org.freedesktop.Platform.openh264\topenh264\t\t2.2.0\tx86_64\tflathub\tsystem\t790.0" + nb + "kB\torg.freedesktop.Platform.openh264/x86_64/2.2.0",
"org.freedesktop.Sdk\tSdk\t\t23.08\tx86_64\tflathub\tsystem\t1.6" + nb + "GB\torg.freedesktop.Sdk/x86_64/23.08",
"org.kde.Platform\tKDE\t\t6.7\tx86_64\tflathub\tsystem\t931.4" + nb + "MB\torg.kde.Platform/x86_64/6.7",
"org.kde.KStyle.Adwaita\tAdwaita\t\t5.15-21.08\tx86_64\tflathub\tsystem\t16.3" + nb + "MB\torg.kde.KStyle.Adwaita/x86_64/5.15-21.08",
"org.kde.KStyle.Adwaita\tAdwaita\t\t6.7\tx86_64\tflathub\tsystem\t20.6" + nb + "MB\torg.kde.KStyle.Adwaita/x86_64/6.7",
}, "\n") + "\n"
const platformMeta = `[Runtime]
name=org.freedesktop.Platform
[Extension org.freedesktop.Platform.GL]
versions = 23.08;23.08-extra;1.4
version = 1.4
directory = lib/x86_64-linux-gnu/GL
subdirectories = true
[Extension org.freedesktop.Platform.openh264]
directory = lib/openh264
version = 2.2.0
[Extension org.freedesktop.Platform.Timezones]
directory = share/zoneinfo
`
const kdeMeta = `[Runtime]
name=org.kde.Platform
[Extension org.kde.KStyle]
directory = lib/plugins/styles
subdirectories = true
version = 6.7
[Extension org.freedesktop.Platform.GL]
versions = 23.08;1.4
subdirectories = true
`
func theDesktop(t *testing.T) *fake {
return using(t, func(line string, c Cmd) Result {
switch {
case strings.HasPrefix(line, "flatpak list --app"):
return ok(apps)
case strings.HasPrefix(line, "flatpak list --runtime"):
return ok(runtimes)
case strings.Contains(line, "--show-runtime com.nextcloud"):
return ok("org.kde.Platform/x86_64/6.7\n")
case strings.Contains(line, "--show-runtime tv.plex"):
return ok("org.freedesktop.Platform/x86_64/23.08\n")
case strings.Contains(line, "--show-sdk tv.plex"):
return ok("org.freedesktop.Sdk/x86_64/23.08\n")
case strings.Contains(line, "--show-sdk"):
return ok("org.kde.Sdk/x86_64/6.7\n")
case strings.Contains(line, "--show-metadata org.freedesktop.Platform/x86_64/23.08"):
return ok(platformMeta)
case strings.Contains(line, "--show-metadata org.kde.Platform"):
return ok(kdeMeta)
case strings.Contains(line, "--show-metadata"):
return ok("[Application]\nname=x\n")
case strings.HasPrefix(line, "flatpak pin"):
return ok("")
case strings.HasPrefix(line, "flatpak remotes"):
return ok("flathub\thttps://dl.flathub.org/repo/\tsystem\t1\nflathub\thttps://dl.flathub.org/repo/\tuser\t1\n")
}
return Result{Status: 9, Stderr: "unexpected " + line}
})
}
func TestUnusedIsWhatNoApplicationNeedsDirectlyOrThroughAnExtensionPoint(t *testing.T) {
f := theDesktop(t)
got, err := Unused()
if err != nil {
t.Fatal(err)
}
names := []string{}
for _, r := range got.Unused {
names = append(names, r.Ref)
}
want := "org.freedesktop.Platform.GL.default/x86_64/22.08,org.freedesktop.Platform/x86_64/22.08,org.kde.KStyle.Adwaita/x86_64/5.15-21.08"
if strings.Join(names, ",") != want {
t.Errorf("unused\n%s\nwant\n%s", strings.Join(names, ","), want)
}
if got.Bytes != 576700000+442100000+16300000 {
t.Errorf("bytes %d", got.Bytes)
}
for _, l := range f.lines() {
if strings.Contains(l, "uninstall") || strings.HasPrefix(l, "sudo") {
t.Errorf("unused only reads: %s", l)
}
}
}
func TestAnExtensionPointAcceptsItsVersionsOrTheDeclaringBranch(t *testing.T) {
ext := extensionsOf(platformMeta, "23.08")
if len(ext) != 3 {
t.Fatalf("%+v", ext)
}
gl, h264, tz := ext[0], ext[1], ext[2]
if !gl.Subdirs || strings.Join(gl.Versions, ";") != "23.08;23.08-extra;1.4" {
t.Errorf("versions win over version: %+v", gl)
}
if h264.Subdirs || strings.Join(h264.Versions, ";") != "2.2.0" {
t.Errorf("%+v", h264)
}
if strings.Join(tz.Versions, ";") != "23.08" {
t.Errorf("no version is the declaring branch: %+v", tz)
}
if !gl.fills(Ref{ID: "org.freedesktop.Platform.GL.default", Branch: "23.08-extra"}) || gl.fills(Ref{ID: "org.freedesktop.Platform.GL.default", Branch: "22.08"}) {
t.Error("a subdirectory extension, by branch")
}
if h264.fills(Ref{ID: "org.freedesktop.Platform.openh264.x", Branch: "2.2.0"}) {
t.Error("without subdirectories only the id itself")
}
}
func TestRemotesFindTheDuplicateAndTheEmptyOne(t *testing.T) {
theDesktop(t)
got, err := Remotes()
if err != nil || len(got.Remotes) != 2 || !got.Flathub {
t.Fatalf("%+v %v", got, err)
}
if got.Remotes[0].Installed != 12 || got.Remotes[1].Installed != 0 {
t.Errorf("%+v", got.Remotes)
}
all := strings.Join(got.Findings, ";")
if !strings.Contains(all, "flathub in the user installation has nothing installed") || !strings.Contains(all, "configured 2 times") {
t.Errorf("%s", all)
}
}
func TestListReadsSizesAsBytes(t *testing.T) {
theDesktop(t)
got, err := List("app", "")
if err != nil || got.Count != 2 || got.Refs[0].Bytes != 275800000 || got.Refs[0].Size != "275.8 MB" || got.Bytes != 643800000 {
t.Fatalf("%+v %v", got, err)
}
for in, want := range map[string]int64{"1.6" + nb + "GB": 1600000000, "790.0 kB": 790000, "12 bytes": 12, "": 0} {
if b := sizeBytes(in); b != want {
t.Errorf("%q: %d", in, b)
}
}
}
func TestActsOnTheSystemInstallationEscalateAndTheAccountsDoNot(t *testing.T) {
f := using(t, func(string, Cmd) Result { return ok("done") })
if got, err := Install("org.gimp.GIMP", "flathub", "system"); err != nil || got.Job.Running {
t.Fatalf("%+v %v", got, err)
}
if _, err := Install("org.gimp.GIMP", "flathub", "user"); err != nil {
t.Fatal(err)
}
if _, err := Remove("org.gimp.GIMP", "system", true); err != nil {
t.Fatal(err)
}
if _, err := Update("", "user"); err != nil {
t.Fatal(err)
}
if _, err := RemoveUnused("system"); err != nil {
t.Fatal(err)
}
want := []string{
"sudo -n flatpak install --system --noninteractive -y flathub org.gimp.GIMP",
"flatpak install --user --noninteractive -y flathub org.gimp.GIMP",
"sudo -n flatpak uninstall --system --noninteractive -y --delete-data org.gimp.GIMP",
"flatpak update --user --noninteractive -y",
"sudo -n flatpak uninstall --system --noninteractive -y --unused",
}
if got := strings.Join(f.lines(), "\n"); got != strings.Join(want, "\n") {
t.Errorf("asked\n%s", got)
}
if _, err := Install("org.gimp.GIMP", "--from=x", "system"); err == nil {
t.Error("an option as a remote")
}
for _, bad := range []string{"--assumeyes", "gimp", "org.gimp.GIMP; rm", "org/../x"} {
if checkRef(bad) == nil {
t.Errorf("%q accepted as a ref", bad)
}
}
for _, good := range []string{"org.gimp.GIMP", "org.freedesktop.Platform/x86_64/23.08", "app/org.gimp.GIMP/x86_64/stable", "org.freedesktop.Platform.GL.default//23.08-extra"} {
if err := checkRef(good); err != nil {
t.Errorf("%v", err)
}
}
}
func TestAFailedInstallIsAnErrorAndAMissingFlatpakIsSaid(t *testing.T) {
using(t, func(string, Cmd) Result {
return Result{Status: 1, Stderr: "error: Nothing matches org.nope.Nope in remote flathub"}
})
if _, err := Install("org.nope.Nope", "flathub", "user"); err == nil || !strings.Contains(err.Error(), "Nothing matches") {
t.Fatalf("%v", err)
}
using(t, func(string, Cmd) Result { return Result{Status: 127, Error: "not-found"} })
if _, err := List("app", ""); err == nil || !strings.Contains(err.Error(), "not installed") {
t.Fatalf("%v", err)
}
}
func TestDiskUsageReadsEachInstallationAndToleratesAnUnreadableFile(t *testing.T) {
t.Setenv("MESH_OPERATOR_HOME", "/home/op")
using(t, func(line string, c Cmd) Result {
switch {
case line == "du -sb /var/lib/flatpak":
return Result{Status: 1, Stdout: "5094728394\t/var/lib/flatpak\n", Stderr: "du: cannot read directory 'x': Permission denied"}
case line == "du -sb /home/op/.local/share/flatpak":
return Result{Status: 1, Stderr: "du: cannot access '/home/op/.local/share/flatpak': No such file or directory"}
case strings.Contains(line, "--app"):
return ok(apps)
}
return ok(runtimes)
})
got, err := DiskUsage()
if err != nil || got.Installations["system"] != 5094728394 || got.Installations["user"] != 0 || got.Note == "" {
t.Fatalf("%+v %v", got, err)
}
if got.Largest[0].ID != "org.freedesktop.Sdk" {
t.Errorf("largest first: %+v", got.Largest[0])
}
}