diff --git a/flake.nix b/flake.nix index 3fa8842..47711b2 100644 --- a/flake.nix +++ b/flake.nix @@ -113,5 +113,25 @@ # alias for flake-parts compatibility homeModules.default = self.homeManagerModules.default; homeModules.snitch = self.homeManagerModules.default; + + checks = eachSystem (system: + let + pkgs = import nixpkgs { + inherit system; + overlays = [ self.overlays.default ]; + }; + in + { + # home manager module tests + hm-module = import ./nix/tests/hm-module-test.nix { + inherit pkgs; + lib = pkgs.lib; + hmModule = self.homeManagerModules.default; + }; + + # package builds correctly + package = self.packages.${system}.default; + } + ); }; } diff --git a/nix/tests/hm-module-test.nix b/nix/tests/hm-module-test.nix new file mode 100644 index 0000000..fd0c706 --- /dev/null +++ b/nix/tests/hm-module-test.nix @@ -0,0 +1,429 @@ +# home manager module tests +# +# run with: nix build .#checks.x86_64-linux.hm-module +# +# tests cover: +# - module evaluation with various configurations +# - type validation for all options +# - generated TOML content verification +# - edge cases (disabled, empty settings, full settings) +{ pkgs, lib, hmModule }: + +let + # minimal home-manager stub for standalone module testing + hmLib = { + hm.types.dagOf = lib.types.attrsOf; + dag.entryAnywhere = x: x; + }; + + # evaluate the hm module with a given config + evalModule = testConfig: + lib.evalModules { + modules = [ + hmModule + # stub home-manager's expected structure + { + options = { + home.packages = lib.mkOption { + type = lib.types.listOf lib.types.package; + default = [ ]; + }; + xdg.configFile = lib.mkOption { + type = lib.types.attrsOf (lib.types.submodule { + options = { + source = lib.mkOption { type = lib.types.path; }; + text = lib.mkOption { type = lib.types.str; default = ""; }; + }; + }); + default = { }; + }; + }; + } + testConfig + ]; + specialArgs = { inherit pkgs lib; }; + }; + + # read generated TOML file content + readGeneratedToml = evalResult: + let + configFile = evalResult.config.xdg.configFile."snitch/snitch.toml" or null; + in + if configFile != null && configFile ? source + then builtins.readFile configFile.source + else null; + + # test cases + tests = { + # test 1: module evaluates when disabled + moduleDisabled = { + name = "module-disabled"; + config = { + programs.snitch.enable = false; + }; + assertions = evalResult: [ + { + assertion = evalResult.config.home.packages == [ ]; + message = "packages should be empty when disabled"; + } + { + assertion = !(evalResult.config.xdg.configFile ? "snitch/snitch.toml"); + message = "config file should not exist when disabled"; + } + ]; + }; + + # test 2: module evaluates with enable only (defaults) + moduleEnabledDefaults = { + name = "module-enabled-defaults"; + config = { + programs.snitch.enable = true; + }; + assertions = evalResult: [ + { + assertion = builtins.length evalResult.config.home.packages == 1; + message = "package should be installed when enabled"; + } + ]; + }; + + # test 3: all theme values are valid + themeValidation = { + name = "theme-validation"; + config = { + programs.snitch = { + enable = true; + settings.defaults.theme = "catppuccin-mocha"; + }; + }; + assertions = evalResult: + let + toml = readGeneratedToml evalResult; + in + [ + { + assertion = toml != null; + message = "TOML config should be generated"; + } + { + assertion = lib.hasInfix "catppuccin-mocha" toml; + message = "theme should be set in TOML"; + } + ]; + }; + + # test 4: full configuration with all options + fullConfiguration = { + name = "full-configuration"; + config = { + programs.snitch = { + enable = true; + settings.defaults = { + interval = "2s"; + numeric = true; + fields = [ "pid" "process" "proto" ]; + theme = "nord"; + units = "si"; + color = "always"; + resolve = false; + dns_cache = false; + ipv4 = true; + ipv6 = false; + no_headers = true; + output_format = "json"; + sort_by = "pid"; + }; + }; + }; + assertions = evalResult: + let + toml = readGeneratedToml evalResult; + in + [ + { + assertion = toml != null; + message = "TOML config should be generated"; + } + { + assertion = lib.hasInfix "interval = \"2s\"" toml; + message = "interval should be 2s"; + } + { + assertion = lib.hasInfix "numeric = true" toml; + message = "numeric should be true"; + } + { + assertion = lib.hasInfix "theme = \"nord\"" toml; + message = "theme should be nord"; + } + { + assertion = lib.hasInfix "units = \"si\"" toml; + message = "units should be si"; + } + { + assertion = lib.hasInfix "color = \"always\"" toml; + message = "color should be always"; + } + { + assertion = lib.hasInfix "resolve = false" toml; + message = "resolve should be false"; + } + { + assertion = lib.hasInfix "output_format = \"json\"" toml; + message = "output_format should be json"; + } + { + assertion = lib.hasInfix "sort_by = \"pid\"" toml; + message = "sort_by should be pid"; + } + ]; + }; + + # test 5: output format enum validation + outputFormatCsv = { + name = "output-format-csv"; + config = { + programs.snitch = { + enable = true; + settings.defaults.output_format = "csv"; + }; + }; + assertions = evalResult: + let + toml = readGeneratedToml evalResult; + in + [ + { + assertion = lib.hasInfix "output_format = \"csv\"" toml; + message = "output_format should accept csv"; + } + ]; + }; + + # test 6: units enum validation + unitsIec = { + name = "units-iec"; + config = { + programs.snitch = { + enable = true; + settings.defaults.units = "iec"; + }; + }; + assertions = evalResult: + let + toml = readGeneratedToml evalResult; + in + [ + { + assertion = lib.hasInfix "units = \"iec\"" toml; + message = "units should accept iec"; + } + ]; + }; + + # test 7: color never value + colorNever = { + name = "color-never"; + config = { + programs.snitch = { + enable = true; + settings.defaults.color = "never"; + }; + }; + assertions = evalResult: + let + toml = readGeneratedToml evalResult; + in + [ + { + assertion = lib.hasInfix "color = \"never\"" toml; + message = "color should accept never"; + } + ]; + }; + + # test 8: freeform type allows custom keys + freeformCustomKeys = { + name = "freeform-custom-keys"; + config = { + programs.snitch = { + enable = true; + settings = { + defaults.theme = "dracula"; + custom_section = { + custom_key = "custom_value"; + }; + }; + }; + }; + assertions = evalResult: + let + toml = readGeneratedToml evalResult; + in + [ + { + assertion = lib.hasInfix "custom_key" toml; + message = "freeform type should allow custom keys"; + } + ]; + }; + + # test 9: all themes evaluate correctly + allThemes = + let + themes = [ + "ansi" + "catppuccin-mocha" + "catppuccin-macchiato" + "catppuccin-frappe" + "catppuccin-latte" + "gruvbox-dark" + "gruvbox-light" + "dracula" + "nord" + "tokyo-night" + "tokyo-night-storm" + "tokyo-night-light" + "solarized-dark" + "solarized-light" + "one-dark" + "mono" + "auto" + ]; + in + { + name = "all-themes"; + # use the last theme as the test config + config = { + programs.snitch = { + enable = true; + settings.defaults.theme = "auto"; + }; + }; + assertions = evalResult: + let + # verify all themes can be set by evaluating them + themeResults = map + (theme: + let + result = evalModule { + programs.snitch = { + enable = true; + settings.defaults.theme = theme; + }; + }; + toml = readGeneratedToml result; + in + { + inherit theme; + success = toml != null && lib.hasInfix theme toml; + } + ) + themes; + allSucceeded = lib.all (r: r.success) themeResults; + in + [ + { + assertion = allSucceeded; + message = "all themes should evaluate correctly: ${ + lib.concatMapStringsSep ", " + (r: "${r.theme}=${if r.success then "ok" else "fail"}") + themeResults + }"; + } + ]; + }; + + # test 10: fields list serialization + fieldsListSerialization = { + name = "fields-list-serialization"; + config = { + programs.snitch = { + enable = true; + settings.defaults.fields = [ "pid" "process" "proto" "state" ]; + }; + }; + assertions = evalResult: + let + toml = readGeneratedToml evalResult; + in + [ + { + assertion = lib.hasInfix "pid" toml && lib.hasInfix "process" toml; + message = "fields list should be serialized correctly"; + } + ]; + }; + }; + + # run all tests and collect results + runTests = + let + testResults = lib.mapAttrsToList + (name: test: + let + evalResult = evalModule test.config; + assertions = test.assertions evalResult; + failures = lib.filter (a: !a.assertion) assertions; + in + { + inherit name; + testName = test.name; + passed = failures == [ ]; + failures = map (f: f.message) failures; + } + ) + tests; + + allPassed = lib.all (r: r.passed) testResults; + failedTests = lib.filter (r: !r.passed) testResults; + + summary = '' + ======================================== + home manager module test results + ======================================== + total tests: ${toString (builtins.length testResults)} + passed: ${toString (builtins.length (lib.filter (r: r.passed) testResults))} + failed: ${toString (builtins.length failedTests)} + ======================================== + ${lib.concatMapStringsSep "\n" (r: + if r.passed + then "[yes] ${r.testName}" + else "[no] ${r.testName}\n ${lib.concatStringsSep "\n " r.failures}" + ) testResults} + ======================================== + ''; + in + { + inherit testResults allPassed failedTests summary; + }; + + results = runTests; + +in +pkgs.runCommand "hm-module-test" +{ + passthru = { + inherit results; + # expose for debugging + inherit evalModule tests; + }; +} + ( + if results.allPassed + then '' + echo "${results.summary}" + echo "all tests passed" + touch $out + '' + else '' + echo "${results.summary}" + echo "" + echo "failed tests:" + ${lib.concatMapStringsSep "\n" (t: '' + echo " - ${t.testName}: ${lib.concatStringsSep ", " t.failures}" + '') results.failedTests} + exit 1 + '' + ) +