From caaafdf5f9191d11ac40cfa9d2fe0463abcfeded Mon Sep 17 00:00:00 2001 From: Qubasa Date: Fri, 31 Jan 2025 16:36:20 +0700 Subject: [PATCH] clan-cli: Add test_clan_nix_attrset.py and minor fixups --- flake.nix | 1 - pkgs/clan-cli/clan_cli/clan/create.py | 10 +- pkgs/clan-cli/clan_cli/templates.py | 75 ++++--- pkgs/clan-cli/tests/test_clan_nix_attrset.py | 203 +++++++++++++++++++ pkgs/clan-cli/tests/test_flake/flake.nix | 7 + templates/flake-module.nix | 4 +- 6 files changed, 254 insertions(+), 46 deletions(-) create mode 100644 pkgs/clan-cli/tests/test_clan_nix_attrset.py diff --git a/flake.nix b/flake.nix index 7e6077210..7fc2ad726 100644 --- a/flake.nix +++ b/flake.nix @@ -64,7 +64,6 @@ ./nixosModules/flake-module.nix ./pkgs/flake-module.nix ./templates/flake-module.nix - ./new-templates/flake-module.nix ] ++ [ (if pathExists ./flakeModules/clan.nix then import ./flakeModules/clan.nix inputs.self else { }) diff --git a/pkgs/clan-cli/clan_cli/clan/create.py b/pkgs/clan-cli/clan_cli/clan/create.py index fe2446e0f..139c870ea 100644 --- a/pkgs/clan-cli/clan_cli/clan/create.py +++ b/pkgs/clan-cli/clan_cli/clan/create.py @@ -111,14 +111,16 @@ def register_create_parser(parser: argparse.ArgumentParser) -> None: parser.add_argument( "--input", type=str, - help="Flake input name to use as template source", + help="""Flake input name to use as template source + can be specified multiple times, inputs are tried in order of definition + """, action="append", default=[], ) parser.add_argument( - "--no-self-prio", - help="Do not prioritize 'self' input", + "--no-self", + help="Do not look into own flake for templates", action="store_true", default=False, ) @@ -145,7 +147,7 @@ def register_create_parser(parser: argparse.ArgumentParser) -> None: ) def create_flake_command(args: argparse.Namespace) -> None: - if args.no_self_prio: + if args.no_self: input_prio = InputPrio.try_inputs(tuple(args.input)) else: input_prio = InputPrio.try_self_then_inputs(tuple(args.input)) diff --git a/pkgs/clan-cli/clan_cli/templates.py b/pkgs/clan-cli/clan_cli/templates.py index 5b482fc3b..689fa95a0 100644 --- a/pkgs/clan-cli/clan_cli/templates.py +++ b/pkgs/clan-cli/clan_cli/templates.py @@ -7,15 +7,13 @@ from dataclasses import dataclass, field from pathlib import Path from typing import Literal, NewType, TypedDict -from clan_cli.clan_uri import FlakeId # Custom FlakeId type for Nix Flakes -from clan_cli.cmd import run # Command execution utility -from clan_cli.errors import ClanError # Custom exception for Clan errors -from clan_cli.nix import nix_eval # Helper for Nix evaluation scripts +from clan_cli.clan_uri import FlakeId +from clan_cli.cmd import run +from clan_cli.errors import ClanError +from clan_cli.nix import nix_eval -# Configure the logging module for debugging log = logging.getLogger(__name__) -# Define custom types for better type annotations InputName = NewType("InputName", str) @@ -31,60 +29,61 @@ class InputVariant: return self.input_name or "self" -TemplateName = NewType("TemplateName", str) # Represents the name of a template -TemplateType = Literal[ - "clan", "disko", "machine" -] # Literal to restrict template type to specific values -ModuleName = NewType("ModuleName", str) # Represents a module name in Clan exports +TemplateName = NewType("TemplateName", str) +TemplateType = Literal["clan", "disko", "machine"] +ModuleName = NewType("ModuleName", str) -# TypedDict for ClanModule with required structure class ClanModule(TypedDict): - description: str # Description of the module - path: str # Filepath of the module + description: str + path: str -# TypedDict for a Template with required structure class Template(TypedDict): - description: str # Template description - path: str # Template path on disk + description: str + path: str -# TypedDict for the structure of templates organized by type class TemplateTypeDict(TypedDict): disko: dict[TemplateName, Template] # Templates under "disko" type clan: dict[TemplateName, Template] # Templates under "clan" type machine: dict[TemplateName, Template] # Templates under "machine" type -# TypedDict for a Clan attribute set (attrset) with templates and modules class ClanAttrset(TypedDict): - templates: TemplateTypeDict # Organized templates by type - modules: dict[ModuleName, ClanModule] # Dictionary of modules by module name + templates: TemplateTypeDict + modules: dict[ModuleName, ClanModule] -# TypedDict to represent exported Clan attributes class ClanExports(TypedDict): - inputs: dict[ - InputName, ClanAttrset - ] # Input names map to their corresponding attrsets - self: ClanAttrset # The attribute set for the flake itself + inputs: dict[InputName, ClanAttrset] + self: ClanAttrset -# Helper function to get Clan exports (Nix flake outputs) -def get_clan_exports(clan_dir: FlakeId | None = None) -> ClanExports: +def get_clan_nix_attrset(clan_dir: FlakeId | None = None) -> ClanExports: # Check if the clan directory is provided, otherwise use the environment variable if not clan_dir: clan_core_path = os.environ.get("CLAN_CORE_PATH") if not clan_core_path: msg = "Environment var CLAN_CORE_PATH is not set, this shouldn't happen" raise ClanError(msg) - # Use the clan core path from the environment variable + clan_dir = FlakeId(clan_core_path) log.debug(f"Evaluating flake {clan_dir} for Clan attrsets") - # Nix evaluation script to compute the relevant exports + # from clan_cli.nix import nix_metadata + # from urllib.parse import urlencode + # myurl = f"path://{clan_dir}" + + # metadata = nix_metadata(myurl)["locked"] + # query_params = { + # "lastModified": metadata["lastModified"], + # "narHash": metadata["narHash"] + # } + # url = f"{myurl}?{urlencode(query_params)}" + + # Nix evaluation script to compute find inputs that have a "clan" attribute eval_script = f""" let self = builtins.getFlake "{clan_dir}"; @@ -96,17 +95,17 @@ def get_clan_exports(clan_dir: FlakeId | None = None) -> ClanExports: {{ inputs = inputsWithClan; self = self.clan or {{}}; }} """ - # Evaluate the Nix expression and run the command cmd = nix_eval( [ - "--json", # Output the result as JSON - "--impure", # Allow impure evaluations (env vars or system state) - "--expr", # Evaluate the given Nix expression + "--json", + "--impure", + "--expr", eval_script, ] ) - res = run(cmd).stdout # Run the command and capture the JSON output - return json.loads(res) # Parse and return as a Python dictionary + res = run(cmd).stdout + + return json.loads(res) # Dataclass to manage input prioritization for templates @@ -115,8 +114,6 @@ class InputPrio: input_names: tuple[str, ...] # Tuple of input names (ordered priority list) prioritize_self: bool = True # Whether to prioritize "self" first - # Static factory methods for specific prioritization strategies - @staticmethod def self_only() -> "InputPrio": # Only consider "self" (no external inputs) @@ -174,7 +171,7 @@ class TemplateList: def list_templates( template_type: TemplateType, clan_dir: FlakeId | None = None ) -> TemplateList: - clan_exports = get_clan_exports(clan_dir) + clan_exports = get_clan_nix_attrset(clan_dir) result = TemplateList() fallback: ClanAttrset = { "templates": {"disko": {}, "clan": {}, "machine": {}}, diff --git a/pkgs/clan-cli/tests/test_clan_nix_attrset.py b/pkgs/clan-cli/tests/test_clan_nix_attrset.py new file mode 100644 index 000000000..31e4756fd --- /dev/null +++ b/pkgs/clan-cli/tests/test_clan_nix_attrset.py @@ -0,0 +1,203 @@ +# mypy: disable-error-code="var-annotated" + +import json +from pathlib import Path +from typing import Any + +import pytest +from clan_cli.clan_uri import FlakeId +from clan_cli.locked_open import locked_open +from clan_cli.templates import get_clan_nix_attrset +from fixtures_flakes import FlakeForTest + + +# Function to write clan attributes to a file +def write_clan_attr(clan_attrset: dict[str, Any], flake: FlakeForTest) -> None: + file = flake.path / "clan_attrs.json" + with locked_open(file, "w") as cfile: + json.dump(clan_attrset, cfile, indent=2) + + +# Common function to test clan nix attrset +def nix_attr_tester( + test_flake: FlakeForTest, + injected: dict[str, Any], + expected: dict[str, Any], + test_number: int, +) -> None: + write_clan_attr(injected, test_flake) + nix_attrset = get_clan_nix_attrset(FlakeId(str(test_flake.path))) + + assert json.dumps(nix_attrset, indent=2) == json.dumps(expected, indent=2) + + +# Test Case 1: Minimal input with empty templates +@pytest.mark.impure +def test_clan_get_nix_attrset_case_1( + monkeypatch: pytest.MonkeyPatch, temporary_home: Path, test_flake: FlakeForTest +) -> None: + test_number = 1 + injected = {"templates": {"clan": {}}} + expected = {"inputs": {}, "self": {"templates": {"clan": {}}}} + nix_attr_tester(test_flake, injected, expected, test_number) + + +# Test Case 2: Input with one template under 'clan' +@pytest.mark.impure +def test_clan_get_nix_attrset_case_2( + monkeypatch: pytest.MonkeyPatch, temporary_home: Path, test_flake: FlakeForTest +) -> None: + test_number = 2 + injected = { + "templates": { + "clan": { + "example_template": { + "description": "An example clan template.", + "path": "/example/path", + } + } + } + } + expected = { + "inputs": {}, + "self": { + "templates": { + "clan": { + "example_template": { + "description": "An example clan template.", + "path": "/example/path", + } + } + } + }, + } + nix_attr_tester(test_flake, injected, expected, test_number) + + +# Test Case 3: Input with templates under multiple types +@pytest.mark.impure +def test_clan_get_nix_attrset_case_3( + monkeypatch: pytest.MonkeyPatch, temporary_home: Path, test_flake: FlakeForTest +) -> None: + test_number = 3 + injected = { + "templates": { + "clan": { + "clan_template": { + "description": "A clan template.", + "path": "/clan/path", + } + }, + "disko": { + "disko_template": { + "description": "A disko template.", + "path": "/disko/path", + } + }, + "machine": { + "machine_template": { + "description": "A machine template.", + "path": "/machine/path", + } + }, + } + } + expected = { + "inputs": {}, + "self": { + "templates": { + "clan": { + "clan_template": { + "description": "A clan template.", + "path": "/clan/path", + } + }, + "disko": { + "disko_template": { + "description": "A disko template.", + "path": "/disko/path", + } + }, + "machine": { + "machine_template": { + "description": "A machine template.", + "path": "/machine/path", + } + }, + } + }, + } + nix_attr_tester(test_flake, injected, expected, test_number) + + +# Test Case 4: Input with modules only +@pytest.mark.impure +def test_clan_get_nix_attrset_case_4( + monkeypatch: pytest.MonkeyPatch, temporary_home: Path, test_flake: FlakeForTest +) -> None: + test_number = 4 + injected = { + "modules": { + "module1": {"description": "First module", "path": "/module1/path"}, + "module2": {"description": "Second module", "path": "/module2/path"}, + } + } + expected = { + "inputs": {}, + "self": { + "modules": { + "module1": {"description": "First module", "path": "/module1/path"}, + "module2": {"description": "Second module", "path": "/module2/path"}, + }, + }, + } + nix_attr_tester(test_flake, injected, expected, test_number) + + +# Test Case 5: Input with both templates and modules +@pytest.mark.impure +def test_clan_get_nix_attrset_case_5( + monkeypatch: pytest.MonkeyPatch, temporary_home: Path, test_flake: FlakeForTest +) -> None: + test_number = 5 + injected = { + "templates": { + "clan": { + "clan_template": { + "description": "A clan template.", + "path": "/clan/path", + } + } + }, + "modules": { + "module1": {"description": "First module", "path": "/module1/path"} + }, + } + expected = { + "inputs": {}, + "self": { + "modules": { + "module1": {"description": "First module", "path": "/module1/path"} + }, + "templates": { + "clan": { + "clan_template": { + "description": "A clan template.", + "path": "/clan/path", + } + } + }, + }, + } + nix_attr_tester(test_flake, injected, expected, test_number) + + +# Test Case 6: Input with missing 'templates' and 'modules' (empty clan attrset) +@pytest.mark.impure +def test_clan_get_nix_attrset_case_6( + monkeypatch: pytest.MonkeyPatch, temporary_home: Path, test_flake: FlakeForTest +) -> None: + test_number = 6 + injected = {} + expected = {"inputs": {}, "self": {}} + nix_attr_tester(test_flake, injected, expected, test_number) diff --git a/pkgs/clan-cli/tests/test_flake/flake.nix b/pkgs/clan-cli/tests/test_flake/flake.nix index 46a3e09ca..6ac75b68c 100644 --- a/pkgs/clan-cli/tests/test_flake/flake.nix +++ b/pkgs/clan-cli/tests/test_flake/flake.nix @@ -12,8 +12,15 @@ inputs = inputs' // { clan-core = fake-clan-core; }; + lib = inputs.nixpkgs.lib; in { + clan = + if lib.pathExists ./clan_attrs.json then + builtins.fromJSON (builtins.readFile ./clan_attrs.json) + else + { }; + nixosConfigurations.machine1 = inputs.nixpkgs.lib.nixosSystem { modules = [ ./nixosModules/machine1.nix diff --git a/templates/flake-module.nix b/templates/flake-module.nix index a48b38ce3..12ebec728 100644 --- a/templates/flake-module.nix +++ b/templates/flake-module.nix @@ -1,7 +1,7 @@ { self, inputs, ... }: { flake = { - checks.x86_64-linux.new-template-minimal = + checks.x86_64-linux.template-minimal = let path = self.clan.templates.clan.minimal.path; initialized = inputs.nixpkgs.legacyPackages.x86_64-linux.runCommand "minimal-clan-flake" { } '' @@ -30,7 +30,7 @@ in { type = "derivation"; - name = "new-minimal-clan-flake-check"; + name = "minimal-clan-flake-check"; inherit (evaled.nixosConfigurations.testmachine.config.system.build.toplevel) drvPath outPath; }; };