clan-cli: Add test_clan_nix_attrset.py and minor fixups

This commit is contained in:
Qubasa
2025-01-31 16:36:20 +07:00
parent 8dd4b92a10
commit caaafdf5f9
6 changed files with 254 additions and 46 deletions

View File

@@ -64,7 +64,6 @@
./nixosModules/flake-module.nix ./nixosModules/flake-module.nix
./pkgs/flake-module.nix ./pkgs/flake-module.nix
./templates/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 { }) (if pathExists ./flakeModules/clan.nix then import ./flakeModules/clan.nix inputs.self else { })

View File

@@ -111,14 +111,16 @@ def register_create_parser(parser: argparse.ArgumentParser) -> None:
parser.add_argument( parser.add_argument(
"--input", "--input",
type=str, 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", action="append",
default=[], default=[],
) )
parser.add_argument( parser.add_argument(
"--no-self-prio", "--no-self",
help="Do not prioritize 'self' input", help="Do not look into own flake for templates",
action="store_true", action="store_true",
default=False, default=False,
) )
@@ -145,7 +147,7 @@ def register_create_parser(parser: argparse.ArgumentParser) -> None:
) )
def create_flake_command(args: argparse.Namespace) -> 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)) input_prio = InputPrio.try_inputs(tuple(args.input))
else: else:
input_prio = InputPrio.try_self_then_inputs(tuple(args.input)) input_prio = InputPrio.try_self_then_inputs(tuple(args.input))

View File

@@ -7,15 +7,13 @@ from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
from typing import Literal, NewType, TypedDict from typing import Literal, NewType, TypedDict
from clan_cli.clan_uri import FlakeId # Custom FlakeId type for Nix Flakes from clan_cli.clan_uri import FlakeId
from clan_cli.cmd import run # Command execution utility from clan_cli.cmd import run
from clan_cli.errors import ClanError # Custom exception for Clan errors from clan_cli.errors import ClanError
from clan_cli.nix import nix_eval # Helper for Nix evaluation scripts from clan_cli.nix import nix_eval
# Configure the logging module for debugging
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
# Define custom types for better type annotations
InputName = NewType("InputName", str) InputName = NewType("InputName", str)
@@ -31,60 +29,61 @@ class InputVariant:
return self.input_name or "self" return self.input_name or "self"
TemplateName = NewType("TemplateName", str) # Represents the name of a template TemplateName = NewType("TemplateName", str)
TemplateType = Literal[ TemplateType = Literal["clan", "disko", "machine"]
"clan", "disko", "machine" ModuleName = NewType("ModuleName", str)
] # Literal to restrict template type to specific values
ModuleName = NewType("ModuleName", str) # Represents a module name in Clan exports
# TypedDict for ClanModule with required structure
class ClanModule(TypedDict): class ClanModule(TypedDict):
description: str # Description of the module description: str
path: str # Filepath of the module path: str
# TypedDict for a Template with required structure
class Template(TypedDict): class Template(TypedDict):
description: str # Template description description: str
path: str # Template path on disk path: str
# TypedDict for the structure of templates organized by type
class TemplateTypeDict(TypedDict): class TemplateTypeDict(TypedDict):
disko: dict[TemplateName, Template] # Templates under "disko" type disko: dict[TemplateName, Template] # Templates under "disko" type
clan: dict[TemplateName, Template] # Templates under "clan" type clan: dict[TemplateName, Template] # Templates under "clan" type
machine: dict[TemplateName, Template] # Templates under "machine" type machine: dict[TemplateName, Template] # Templates under "machine" type
# TypedDict for a Clan attribute set (attrset) with templates and modules
class ClanAttrset(TypedDict): class ClanAttrset(TypedDict):
templates: TemplateTypeDict # Organized templates by type templates: TemplateTypeDict
modules: dict[ModuleName, ClanModule] # Dictionary of modules by module name modules: dict[ModuleName, ClanModule]
# TypedDict to represent exported Clan attributes
class ClanExports(TypedDict): class ClanExports(TypedDict):
inputs: dict[ inputs: dict[InputName, ClanAttrset]
InputName, ClanAttrset self: ClanAttrset
] # Input names map to their corresponding attrsets
self: ClanAttrset # The attribute set for the flake itself
# Helper function to get Clan exports (Nix flake outputs) def get_clan_nix_attrset(clan_dir: FlakeId | None = None) -> ClanExports:
def get_clan_exports(clan_dir: FlakeId | None = None) -> ClanExports:
# Check if the clan directory is provided, otherwise use the environment variable # Check if the clan directory is provided, otherwise use the environment variable
if not clan_dir: if not clan_dir:
clan_core_path = os.environ.get("CLAN_CORE_PATH") clan_core_path = os.environ.get("CLAN_CORE_PATH")
if not clan_core_path: if not clan_core_path:
msg = "Environment var CLAN_CORE_PATH is not set, this shouldn't happen" msg = "Environment var CLAN_CORE_PATH is not set, this shouldn't happen"
raise ClanError(msg) raise ClanError(msg)
# Use the clan core path from the environment variable
clan_dir = FlakeId(clan_core_path) clan_dir = FlakeId(clan_core_path)
log.debug(f"Evaluating flake {clan_dir} for Clan attrsets") 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""" eval_script = f"""
let let
self = builtins.getFlake "{clan_dir}"; 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 {{}}; }} {{ inputs = inputsWithClan; self = self.clan or {{}}; }}
""" """
# Evaluate the Nix expression and run the command
cmd = nix_eval( cmd = nix_eval(
[ [
"--json", # Output the result as JSON "--json",
"--impure", # Allow impure evaluations (env vars or system state) "--impure",
"--expr", # Evaluate the given Nix expression "--expr",
eval_script, eval_script,
] ]
) )
res = run(cmd).stdout # Run the command and capture the JSON output res = run(cmd).stdout
return json.loads(res) # Parse and return as a Python dictionary
return json.loads(res)
# Dataclass to manage input prioritization for templates # Dataclass to manage input prioritization for templates
@@ -115,8 +114,6 @@ class InputPrio:
input_names: tuple[str, ...] # Tuple of input names (ordered priority list) input_names: tuple[str, ...] # Tuple of input names (ordered priority list)
prioritize_self: bool = True # Whether to prioritize "self" first prioritize_self: bool = True # Whether to prioritize "self" first
# Static factory methods for specific prioritization strategies
@staticmethod @staticmethod
def self_only() -> "InputPrio": def self_only() -> "InputPrio":
# Only consider "self" (no external inputs) # Only consider "self" (no external inputs)
@@ -174,7 +171,7 @@ class TemplateList:
def list_templates( def list_templates(
template_type: TemplateType, clan_dir: FlakeId | None = None template_type: TemplateType, clan_dir: FlakeId | None = None
) -> TemplateList: ) -> TemplateList:
clan_exports = get_clan_exports(clan_dir) clan_exports = get_clan_nix_attrset(clan_dir)
result = TemplateList() result = TemplateList()
fallback: ClanAttrset = { fallback: ClanAttrset = {
"templates": {"disko": {}, "clan": {}, "machine": {}}, "templates": {"disko": {}, "clan": {}, "machine": {}},

View File

@@ -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)

View File

@@ -12,8 +12,15 @@
inputs = inputs' // { inputs = inputs' // {
clan-core = fake-clan-core; clan-core = fake-clan-core;
}; };
lib = inputs.nixpkgs.lib;
in in
{ {
clan =
if lib.pathExists ./clan_attrs.json then
builtins.fromJSON (builtins.readFile ./clan_attrs.json)
else
{ };
nixosConfigurations.machine1 = inputs.nixpkgs.lib.nixosSystem { nixosConfigurations.machine1 = inputs.nixpkgs.lib.nixosSystem {
modules = [ modules = [
./nixosModules/machine1.nix ./nixosModules/machine1.nix

View File

@@ -1,7 +1,7 @@
{ self, inputs, ... }: { self, inputs, ... }:
{ {
flake = { flake = {
checks.x86_64-linux.new-template-minimal = checks.x86_64-linux.template-minimal =
let let
path = self.clan.templates.clan.minimal.path; path = self.clan.templates.clan.minimal.path;
initialized = inputs.nixpkgs.legacyPackages.x86_64-linux.runCommand "minimal-clan-flake" { } '' initialized = inputs.nixpkgs.legacyPackages.x86_64-linux.runCommand "minimal-clan-flake" { } ''
@@ -30,7 +30,7 @@
in in
{ {
type = "derivation"; type = "derivation";
name = "new-minimal-clan-flake-check"; name = "minimal-clan-flake-check";
inherit (evaled.nixosConfigurations.testmachine.config.system.build.toplevel) drvPath outPath; inherit (evaled.nixosConfigurations.testmachine.config.system.build.toplevel) drvPath outPath;
}; };
}; };