clan-cli: Add test_clan_nix_attrset.py and minor fixups
This commit is contained in:
@@ -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 { })
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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": {}},
|
||||||
|
|||||||
203
pkgs/clan-cli/tests/test_clan_nix_attrset.py
Normal file
203
pkgs/clan-cli/tests/test_clan_nix_attrset.py
Normal 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)
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user