Prometheus exporter for NixOS-specific metrics including: - Generation collector: count, current, booted, age, config mismatch - Flake collector: input age, input info, revision behind Includes NixOS module, flake packaging, and documentation. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
179 lines
4.0 KiB
Go
179 lines
4.0 KiB
Go
package collector
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
func TestCountGenerations(t *testing.T) {
|
|
dir := t.TempDir()
|
|
|
|
// Create some generation symlinks
|
|
for _, name := range []string{
|
|
"system-1-link",
|
|
"system-2-link",
|
|
"system-10-link",
|
|
"system", // current system link, should not be counted
|
|
"other-file", // unrelated file
|
|
"system-x-link", // malformed, should not be counted
|
|
} {
|
|
path := filepath.Join(dir, name)
|
|
if err := os.Symlink("/nix/store/dummy", path); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
count, err := countGenerations(dir)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if count != 3 {
|
|
t.Errorf("expected 3 generations, got %d", count)
|
|
}
|
|
}
|
|
|
|
func TestGetCurrentGeneration(t *testing.T) {
|
|
dir := t.TempDir()
|
|
|
|
// Create system symlink pointing to a generation
|
|
if err := os.Symlink("system-42-link", filepath.Join(dir, "system")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
gen, err := getCurrentGeneration(dir)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if gen != 42 {
|
|
t.Errorf("expected generation 42, got %d", gen)
|
|
}
|
|
}
|
|
|
|
func TestGetBootedGeneration(t *testing.T) {
|
|
profileDir := t.TempDir()
|
|
bootedDir := t.TempDir()
|
|
|
|
storePath := "/nix/store/abc123-nixos-system"
|
|
|
|
// Create generation symlinks
|
|
if err := os.Symlink("/nix/store/other", filepath.Join(profileDir, "system-1-link")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Symlink(storePath, filepath.Join(profileDir, "system-2-link")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Symlink("/nix/store/another", filepath.Join(profileDir, "system-3-link")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Create booted-system symlink
|
|
bootedSystemPath := filepath.Join(bootedDir, "booted-system")
|
|
if err := os.Symlink(storePath, bootedSystemPath); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
gen, err := getBootedGeneration(profileDir, bootedSystemPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if gen != 2 {
|
|
t.Errorf("expected booted generation 2, got %d", gen)
|
|
}
|
|
}
|
|
|
|
func TestCheckConfigMismatch(t *testing.T) {
|
|
dir := t.TempDir()
|
|
|
|
currentPath := filepath.Join(dir, "current-system")
|
|
bootedPath := filepath.Join(dir, "booted-system")
|
|
|
|
// Same target = no mismatch
|
|
if err := os.Symlink("/nix/store/same", currentPath); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Symlink("/nix/store/same", bootedPath); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
mismatch, err := checkConfigMismatch(currentPath, bootedPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if mismatch {
|
|
t.Error("expected no mismatch when targets are the same")
|
|
}
|
|
|
|
// Different targets = mismatch
|
|
if err := os.Remove(currentPath); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Symlink("/nix/store/different", currentPath); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
mismatch, err = checkConfigMismatch(currentPath, bootedPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !mismatch {
|
|
t.Error("expected mismatch when targets differ")
|
|
}
|
|
}
|
|
|
|
func TestGetGenerationAge(t *testing.T) {
|
|
dir := t.TempDir()
|
|
|
|
// Create system symlink
|
|
if err := os.Symlink("system-1-link", filepath.Join(dir, "system")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
age, err := getGenerationAge(dir)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Age should be very small since we just created it
|
|
if age < 0 || age > 1 {
|
|
t.Errorf("expected age close to 0, got %f", age)
|
|
}
|
|
}
|
|
|
|
func TestGenerationPattern(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
match bool
|
|
genNum string
|
|
}{
|
|
{"system-1-link", true, "1"},
|
|
{"system-42-link", true, "42"},
|
|
{"system-123-link", true, "123"},
|
|
{"system", false, ""},
|
|
{"system-link", false, ""},
|
|
{"system--link", false, ""},
|
|
{"system-abc-link", false, ""},
|
|
{"other-1-link", false, ""},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
matches := generationPattern.FindStringSubmatch(tt.name)
|
|
if tt.match {
|
|
if matches == nil {
|
|
t.Errorf("expected %q to match", tt.name)
|
|
} else if matches[1] != tt.genNum {
|
|
t.Errorf("expected generation %q, got %q", tt.genNum, matches[1])
|
|
}
|
|
} else {
|
|
if matches != nil {
|
|
t.Errorf("expected %q not to match", tt.name)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|