refactor(nix/*): drop yants and consumers, and some more
Change-Id: I96ab5890518c7bb0d4a676adbad20e4c49699b63
This commit is contained in:
parent
001556aa30
commit
cff6575948
33 changed files with 11 additions and 2414 deletions
|
|
@ -1,51 +0,0 @@
|
|||
{ lib, pkgs, depot, ... }:
|
||||
|
||||
# Takes a derivation and a list of binary names
|
||||
# and returns an attribute set of `name -> path`.
|
||||
# The list can also contain renames in the form of
|
||||
# `{ use, as }`, which goes `as -> usePath`.
|
||||
#
|
||||
# It is usually used to construct an attrset `bins`
|
||||
# containing all the binaries required in a file,
|
||||
# similar to a simple import system.
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# bins = getBins pkgs.hello [ "hello" ]
|
||||
# // getBins pkgs.coreutils [ "printf" "ln" "echo" ]
|
||||
# // getBins pkgs.execline
|
||||
# [ { use = "if"; as = "execlineIf" } ]
|
||||
# // getBins pkgs.s6-portable-utils
|
||||
# [ { use = "s6-test"; as = "test" }
|
||||
# { use = "s6-cat"; as = "cat" }
|
||||
# ];
|
||||
#
|
||||
# provides
|
||||
# bins.{hello,printf,ln,echo,execlineIf,test,cat}
|
||||
#
|
||||
|
||||
let
|
||||
getBins = drv: xs:
|
||||
let
|
||||
f = x:
|
||||
# TODO(Profpatsch): typecheck
|
||||
let x' = if builtins.isString x then { use = x; as = x; } else x;
|
||||
in {
|
||||
name = x'.as;
|
||||
value = "${lib.getBin drv}/bin/${x'.use}";
|
||||
};
|
||||
in
|
||||
builtins.listToAttrs (builtins.map f xs);
|
||||
|
||||
|
||||
tests = import ./tests.nix {
|
||||
inherit getBins;
|
||||
inherit (depot.nix) writeScriptBin;
|
||||
inherit (depot.nix.runTestsuite) assertEq it runTestsuite;
|
||||
};
|
||||
|
||||
in
|
||||
{
|
||||
__functor = _: getBins;
|
||||
inherit tests;
|
||||
}
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
{ writeScriptBin, assertEq, it, runTestsuite, getBins }:
|
||||
|
||||
let
|
||||
drv = writeScriptBin "hello" "it’s me";
|
||||
drv2 = writeScriptBin "goodbye" "tschau";
|
||||
|
||||
bins = getBins drv [
|
||||
"hello"
|
||||
{ use = "hello"; as = "also-hello"; }
|
||||
]
|
||||
// getBins drv2 [ "goodbye" ]
|
||||
;
|
||||
|
||||
simple = it "path is equal to the executable name" [
|
||||
(assertEq "path"
|
||||
bins.hello
|
||||
"${drv}/bin/hello")
|
||||
(assertEq "content"
|
||||
(builtins.readFile bins.hello)
|
||||
"it’s me")
|
||||
];
|
||||
|
||||
useAs = it "use/as can be used to rename attributes" [
|
||||
(assertEq "path"
|
||||
bins.also-hello
|
||||
"${drv}/bin/hello")
|
||||
];
|
||||
|
||||
secondDrv = it "by merging attrsets you can build up bins" [
|
||||
(assertEq "path"
|
||||
bins.goodbye
|
||||
"${drv2}/bin/goodbye")
|
||||
];
|
||||
|
||||
in
|
||||
runTestsuite "getBins" [
|
||||
simple
|
||||
useAs
|
||||
secondDrv
|
||||
]
|
||||
|
|
@ -1,192 +0,0 @@
|
|||
{ lib, depot, ... }:
|
||||
/*
|
||||
JSON Merge-Patch for nix
|
||||
Spec: https://tools.ietf.org/html/rfc7396
|
||||
|
||||
An algorithm for changing and removing fields in nested objects.
|
||||
|
||||
For example, given the following original document:
|
||||
|
||||
{
|
||||
a = "b";
|
||||
c = {
|
||||
d = "e";
|
||||
f = "g";
|
||||
}
|
||||
}
|
||||
|
||||
Changing the value of `a` and removing `f` can be achieved by merging the patch
|
||||
|
||||
{
|
||||
a = "z";
|
||||
c.f = null;
|
||||
}
|
||||
|
||||
which results in
|
||||
|
||||
{
|
||||
a = "z";
|
||||
c = {
|
||||
d = "e";
|
||||
};
|
||||
}
|
||||
|
||||
Pseudo-code:
|
||||
define MergePatch(Target, Patch):
|
||||
if Patch is an Object:
|
||||
if Target is not an Object:
|
||||
Target = {} # Ignore the contents and set it to an empty Object
|
||||
for each Name/Value pair in Patch:
|
||||
if Value is null:
|
||||
if Name exists in Target:
|
||||
remove the Name/Value pair from Target
|
||||
else:
|
||||
Target[Name] = MergePatch(Target[Name], Value)
|
||||
return Target
|
||||
else:
|
||||
return Patch
|
||||
*/
|
||||
|
||||
let
|
||||
foldlAttrs = op: init: attrs:
|
||||
lib.foldl' op init
|
||||
(lib.mapAttrsToList lib.nameValuePair attrs);
|
||||
|
||||
mergePatch = target: patch:
|
||||
if lib.isAttrs patch
|
||||
then
|
||||
let target' = if lib.isAttrs target then target else { };
|
||||
in foldlAttrs
|
||||
(acc: patchEl:
|
||||
if patchEl.value == null
|
||||
then removeAttrs acc [ patchEl.name ]
|
||||
else acc // {
|
||||
${patchEl.name} =
|
||||
mergePatch
|
||||
(acc.${patchEl.name} or "unnused")
|
||||
patchEl.value;
|
||||
})
|
||||
target'
|
||||
patch
|
||||
else patch;
|
||||
|
||||
inherit (depot.nix.runTestsuite)
|
||||
runTestsuite
|
||||
it
|
||||
assertEq
|
||||
;
|
||||
|
||||
tests =
|
||||
let
|
||||
# example target from the RFC
|
||||
testTarget = {
|
||||
a = "b";
|
||||
c = {
|
||||
d = "e";
|
||||
f = "g";
|
||||
};
|
||||
};
|
||||
# example patch from the RFC
|
||||
testPatch = {
|
||||
a = "z";
|
||||
c.f = null;
|
||||
};
|
||||
emptyPatch = it "the empty patch returns the original target" [
|
||||
(assertEq "id"
|
||||
(mergePatch testTarget { })
|
||||
testTarget)
|
||||
];
|
||||
nonAttrs = it "one side is a non-attrset value" [
|
||||
(assertEq "target is a value means the value is replaced by the patch"
|
||||
(mergePatch 42 testPatch)
|
||||
(mergePatch { } testPatch))
|
||||
(assertEq "patch is a value means it replaces target alltogether"
|
||||
(mergePatch testTarget 42)
|
||||
42)
|
||||
];
|
||||
rfcExamples = it "the examples from the RFC" [
|
||||
(assertEq "a subset is deleted and overwritten"
|
||||
(mergePatch testTarget testPatch)
|
||||
{
|
||||
a = "z";
|
||||
c = {
|
||||
d = "e";
|
||||
};
|
||||
})
|
||||
(assertEq "a more complicated example from the example section"
|
||||
(mergePatch
|
||||
{
|
||||
title = "Goodbye!";
|
||||
author = {
|
||||
givenName = "John";
|
||||
familyName = "Doe";
|
||||
};
|
||||
tags = [ "example" "sample" ];
|
||||
content = "This will be unchanged";
|
||||
}
|
||||
{
|
||||
title = "Hello!";
|
||||
phoneNumber = "+01-123-456-7890";
|
||||
author.familyName = null;
|
||||
tags = [ "example" ];
|
||||
})
|
||||
{
|
||||
title = "Hello!";
|
||||
phoneNumber = "+01-123-456-7890";
|
||||
author = {
|
||||
givenName = "John";
|
||||
};
|
||||
tags = [ "example" ];
|
||||
content = "This will be unchanged";
|
||||
})
|
||||
];
|
||||
|
||||
rfcTests =
|
||||
let
|
||||
r = index: target: patch: res:
|
||||
(assertEq "test number ${toString index}"
|
||||
(mergePatch target patch)
|
||||
res);
|
||||
in
|
||||
it "the test suite from the RFC" [
|
||||
(r 1 { "a" = "b"; } { "a" = "c"; } { "a" = "c"; })
|
||||
(r 2 { "a" = "b"; } { "b" = "c"; } { "a" = "b"; "b" = "c"; })
|
||||
(r 3 { "a" = "b"; } { "a" = null; } { })
|
||||
(r 4 { "a" = "b"; "b" = "c"; }
|
||||
{ "a" = null; }
|
||||
{ "b" = "c"; })
|
||||
(r 5 { "a" = [ "b" ]; } { "a" = "c"; } { "a" = "c"; })
|
||||
(r 6 { "a" = "c"; } { "a" = [ "b" ]; } { "a" = [ "b" ]; })
|
||||
(r 7 { "a" = { "b" = "c"; }; }
|
||||
{ "a" = { "b" = "d"; "c" = null; }; }
|
||||
{ "a" = { "b" = "d"; }; })
|
||||
(r 8 { "a" = [{ "b" = "c"; }]; }
|
||||
{ "a" = [ 1 ]; }
|
||||
{ "a" = [ 1 ]; })
|
||||
(r 9 [ "a" "b" ] [ "c" "d" ] [ "c" "d" ])
|
||||
(r 10 { "a" = "b"; } [ "c" ] [ "c" ])
|
||||
(r 11 { "a" = "foo"; } null null)
|
||||
(r 12 { "a" = "foo"; } "bar" "bar")
|
||||
(r 13 { "e" = null; } { "a" = 1; } { "e" = null; "a" = 1; })
|
||||
(r 14 [ 1 2 ]
|
||||
{ "a" = "b"; "c" = null; }
|
||||
{ "a" = "b"; })
|
||||
(r 15 { }
|
||||
{ "a" = { "bb" = { "ccc" = null; }; }; }
|
||||
{ "a" = { "bb" = { }; }; })
|
||||
];
|
||||
|
||||
in
|
||||
runTestsuite "mergePatch" [
|
||||
emptyPatch
|
||||
nonAttrs
|
||||
rfcExamples
|
||||
rfcTests
|
||||
];
|
||||
|
||||
in
|
||||
{
|
||||
__functor = _: mergePatch;
|
||||
|
||||
inherit tests;
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
raitobezarius
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
# nint — Nix INTerpreter
|
||||
|
||||
`nint` is a shebang compatible interpreter for nix. It is currently
|
||||
implemented as a fairly trivial wrapper around `nix-instantiate --eval`.
|
||||
It allows to run nix expressions as command line tools if they conform
|
||||
to the following calling convention:
|
||||
|
||||
* Every nix script needs to evaluate to a function which takes an
|
||||
attribute set as its single argument. Ideally a set pattern with
|
||||
an ellipsis should be used. By default `nint` passes the following
|
||||
arguments:
|
||||
|
||||
* `currentDir`: the current working directory as a nix path
|
||||
* `argv`: a list of arguments to the invokation including the
|
||||
program name at `builtins.head argv`.
|
||||
* Extra arguments can be manually passed as described below.
|
||||
|
||||
* The return value must either be
|
||||
|
||||
* A string which is rendered to `stdout`.
|
||||
|
||||
* An attribute set with the following optional attributes:
|
||||
|
||||
* `stdout`: A string that's rendered to `stdout`
|
||||
* `stderr`: A string that's rendered to `stderr`
|
||||
* `exit`: A number which is used as an exit code.
|
||||
If missing, nint always exits with 0 (or equivalent).
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
nint [ --arg ARG VALUE … ] script.nix [ ARGS … ]
|
||||
```
|
||||
|
||||
Instead of `--arg`, `--argstr` can also be used. They both work
|
||||
like the flags of the same name for `nix-instantiate` and may
|
||||
be specified any number of times as long as they are passed
|
||||
*before* the nix expression to run.
|
||||
|
||||
Below is a shebang which also passes `depot` as an argument
|
||||
(note the usage of `env -S` to get around the shebang limitation
|
||||
to two arguments).
|
||||
|
||||
```nix
|
||||
#!/usr/bin/env -S nint --arg depot /path/to/depot
|
||||
```
|
||||
|
||||
## Limitations
|
||||
|
||||
* No side effects except for writing to `stdout`.
|
||||
|
||||
* Output is not streaming, i. e. even if the output is incrementally
|
||||
calculated, nothing will be printed until the full output is available.
|
||||
With plain nix strings we can't do better anyways.
|
||||
|
||||
* Limited error handling for the script, no way to set the exit code etc.
|
||||
|
||||
Some of these limitations may be possible to address in the future by using
|
||||
an alternative nix interpreter and a more elaborate calling convention.
|
||||
|
||||
## Example
|
||||
|
||||
Below is a (very simple) implementation of a `ls(1)`-like program in nix:
|
||||
|
||||
```nix
|
||||
#!/usr/bin/env nint
|
||||
{ currentDir, argv, ... }:
|
||||
|
||||
let
|
||||
lib = import <nixpkgs/lib>;
|
||||
|
||||
dirs =
|
||||
let
|
||||
args = builtins.tail argv;
|
||||
in
|
||||
if args == []
|
||||
then [ currentDir ]
|
||||
else args;
|
||||
|
||||
makeAbsolute = p:
|
||||
if builtins.isPath p
|
||||
then p
|
||||
else if builtins.match "^/.*" p != null
|
||||
then p
|
||||
else "${toString currentDir}/${p}";
|
||||
in
|
||||
|
||||
lib.concatStringsSep "\n"
|
||||
(lib.flatten
|
||||
(builtins.map
|
||||
(d: (builtins.attrNames (builtins.readDir (makeAbsolute d))))
|
||||
dirs)) + "\n"
|
||||
```
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
{ depot, pkgs, ... }:
|
||||
|
||||
let
|
||||
inherit (depot.nix.writers)
|
||||
rustSimpleBin
|
||||
;
|
||||
in
|
||||
|
||||
rustSimpleBin
|
||||
{
|
||||
name = "nint";
|
||||
dependencies = [
|
||||
depot.third_party.rust-crates.serde_json
|
||||
];
|
||||
}
|
||||
(builtins.readFile ./nint.rs)
|
||||
149
nix/nint/nint.rs
149
nix/nint/nint.rs
|
|
@ -1,149 +0,0 @@
|
|||
extern crate serde_json;
|
||||
|
||||
use serde_json::Value;
|
||||
use std::convert::TryFrom;
|
||||
use std::ffi::OsString;
|
||||
use std::io::{stderr, stdout, Error, ErrorKind, Write};
|
||||
use std::os::unix::ffi::{OsStrExt, OsStringExt};
|
||||
use std::process::Command;
|
||||
|
||||
fn render_nix_string(s: &OsString) -> OsString {
|
||||
let mut rendered = Vec::new();
|
||||
|
||||
rendered.extend(b"\"");
|
||||
|
||||
for b in s.as_os_str().as_bytes() {
|
||||
match char::from(*b) {
|
||||
'\"' => rendered.extend(b"\\\""),
|
||||
'\\' => rendered.extend(b"\\\\"),
|
||||
'$' => rendered.extend(b"\\$"),
|
||||
_ => rendered.push(*b),
|
||||
}
|
||||
}
|
||||
|
||||
rendered.extend(b"\"");
|
||||
|
||||
OsString::from_vec(rendered)
|
||||
}
|
||||
|
||||
fn render_nix_list(arr: &[OsString]) -> OsString {
|
||||
let mut rendered = Vec::new();
|
||||
|
||||
rendered.extend(b"[ ");
|
||||
|
||||
for el in arr {
|
||||
rendered.extend(render_nix_string(el).as_os_str().as_bytes());
|
||||
rendered.extend(b" ");
|
||||
}
|
||||
|
||||
rendered.extend(b"]");
|
||||
|
||||
OsString::from_vec(rendered)
|
||||
}
|
||||
|
||||
/// Slightly overkill helper macro which takes a `Map<String, Value>` obtained
|
||||
/// from `Value::Object` and an output name (`stderr` or `stdout`) as an
|
||||
/// identifier. If a value exists for the given output in the object it gets
|
||||
/// written to the appropriate output.
|
||||
macro_rules! handle_set_output {
|
||||
($map_name:ident, $output_name:ident) => {
|
||||
match $map_name.get(stringify!($output_name)) {
|
||||
Some(Value::String(s)) => $output_name().write_all(s.as_bytes()),
|
||||
Some(_) => Err(Error::new(
|
||||
ErrorKind::Other,
|
||||
format!("Attribute {} must be a string!", stringify!($output_name)),
|
||||
)),
|
||||
None => Ok(()),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn main() -> std::io::Result<()> {
|
||||
let mut nix_args = Vec::new();
|
||||
|
||||
let mut args = std::env::args_os().into_iter();
|
||||
let mut in_args = true;
|
||||
|
||||
let mut argv: Vec<OsString> = Vec::new();
|
||||
|
||||
// skip argv[0]
|
||||
args.next();
|
||||
|
||||
loop {
|
||||
let arg = match args.next() {
|
||||
Some(a) => a,
|
||||
None => break,
|
||||
};
|
||||
|
||||
if !arg.to_str().map(|s| s.starts_with("-")).unwrap_or(false) {
|
||||
in_args = false;
|
||||
}
|
||||
|
||||
if in_args {
|
||||
match (arg.to_str()) {
|
||||
Some("--arg") | Some("--argstr") => {
|
||||
nix_args.push(arg);
|
||||
nix_args.push(args.next().unwrap());
|
||||
nix_args.push(args.next().unwrap());
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(Error::new(ErrorKind::Other, "unknown argument")),
|
||||
}?
|
||||
} else {
|
||||
argv.push(arg);
|
||||
}
|
||||
}
|
||||
|
||||
if argv.len() < 1 {
|
||||
Err(Error::new(ErrorKind::Other, "missing argv"))
|
||||
} else {
|
||||
let cd = std::env::current_dir()?.into_os_string();
|
||||
|
||||
nix_args.push(OsString::from("--arg"));
|
||||
nix_args.push(OsString::from("currentDir"));
|
||||
nix_args.push(cd);
|
||||
|
||||
nix_args.push(OsString::from("--arg"));
|
||||
nix_args.push(OsString::from("argv"));
|
||||
nix_args.push(render_nix_list(&argv[..]));
|
||||
|
||||
nix_args.push(OsString::from("--eval"));
|
||||
nix_args.push(OsString::from("--strict"));
|
||||
nix_args.push(OsString::from("--json"));
|
||||
|
||||
nix_args.push(argv[0].clone());
|
||||
|
||||
let run = Command::new("nix-instantiate").args(nix_args).output()?;
|
||||
|
||||
match serde_json::from_slice(&run.stdout[..]) {
|
||||
Ok(Value::String(s)) => stdout().write_all(s.as_bytes()),
|
||||
Ok(Value::Object(m)) => {
|
||||
handle_set_output!(m, stdout)?;
|
||||
handle_set_output!(m, stderr)?;
|
||||
|
||||
match m.get("exit") {
|
||||
Some(Value::Number(n)) => {
|
||||
let code = n.as_i64().and_then(|v| i32::try_from(v).ok());
|
||||
|
||||
match code {
|
||||
Some(i) => std::process::exit(i),
|
||||
None => {
|
||||
Err(Error::new(ErrorKind::Other, "Attribute exit is not an i32"))
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(_) => Err(Error::new(ErrorKind::Other, "exit must be a number")),
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
Ok(_) => Err(Error::new(
|
||||
ErrorKind::Other,
|
||||
"output must be a string or an object",
|
||||
)),
|
||||
_ => {
|
||||
stderr().write_all(&run.stderr[..]);
|
||||
Err(Error::new(ErrorKind::Other, "internal nix error"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,139 +0,0 @@
|
|||
{ depot, lib, ... }:
|
||||
|
||||
let
|
||||
inherit (depot.nix.runTestsuite)
|
||||
runTestsuite
|
||||
it
|
||||
assertEq
|
||||
assertThrows
|
||||
;
|
||||
|
||||
tree-ex = depot.nix.readTree {
|
||||
path = ./test-example;
|
||||
args = { };
|
||||
};
|
||||
|
||||
example = it "corresponds to the README example" [
|
||||
(assertEq "third_party attrset"
|
||||
(lib.isAttrs tree-ex.third_party
|
||||
&& (! lib.isDerivation tree-ex.third_party))
|
||||
true)
|
||||
(assertEq "third_party attrset other attribute"
|
||||
tree-ex.third_party.favouriteColour
|
||||
"orange")
|
||||
(assertEq "rustpkgs attrset aho-corasick"
|
||||
tree-ex.third_party.rustpkgs.aho-corasick
|
||||
"aho-corasick")
|
||||
(assertEq "rustpkgs attrset serde"
|
||||
tree-ex.third_party.rustpkgs.serde
|
||||
"serde")
|
||||
(assertEq "tools cheddear"
|
||||
"cheddar"
|
||||
tree-ex.tools.cheddar)
|
||||
(assertEq "tools roquefort"
|
||||
tree-ex.tools.roquefort
|
||||
"roquefort")
|
||||
];
|
||||
|
||||
tree-tl = depot.nix.readTree {
|
||||
path = ./test-tree-traversal;
|
||||
args = { };
|
||||
};
|
||||
|
||||
traversal-logic = it "corresponds to the traversal logic in the README" [
|
||||
(assertEq "skip-tree/a is read"
|
||||
tree-tl.skip-tree.a
|
||||
"a is read normally")
|
||||
(assertEq "skip-tree does not contain b"
|
||||
(builtins.attrNames tree-tl.skip-tree)
|
||||
[ "__readTree" "__readTreeChildren" "a" ])
|
||||
(assertEq "skip-tree children list does not contain b"
|
||||
tree-tl.skip-tree.__readTreeChildren
|
||||
[ "a" ])
|
||||
|
||||
(assertEq "skip subtree default.nix is read"
|
||||
tree-tl.skip-subtree.but
|
||||
"the default.nix is still read")
|
||||
(assertEq "skip subtree a/default.nix is skipped"
|
||||
(tree-tl.skip-subtree ? a)
|
||||
false)
|
||||
(assertEq "skip subtree b/c.nix is skipped"
|
||||
(tree-tl.skip-subtree ? b)
|
||||
false)
|
||||
(assertEq "skip subtree a/default.nix would be read without .skip-subtree"
|
||||
(tree-tl.no-skip-subtree.a)
|
||||
"am I subtree yet?")
|
||||
(assertEq "skip subtree b/c.nix would be read without .skip-subtree"
|
||||
(tree-tl.no-skip-subtree.b.c)
|
||||
"cool")
|
||||
|
||||
(assertEq "default.nix attrset is merged with siblings"
|
||||
tree-tl.default-nix.no
|
||||
"siblings should be read")
|
||||
(assertEq "default.nix means sibling isn’t read"
|
||||
(tree-tl.default-nix ? sibling)
|
||||
false)
|
||||
(assertEq "default.nix means subdirs are still read and merged into default.nix"
|
||||
(tree-tl.default-nix.subdir.a)
|
||||
"but I’m picked up")
|
||||
|
||||
(assertEq "default.nix can be not an attrset"
|
||||
tree-tl.default-nix.no-merge
|
||||
"I’m not merged with any children")
|
||||
(assertEq "default.nix is not an attrset -> children are not merged"
|
||||
(tree-tl.default-nix.no-merge ? subdir)
|
||||
false)
|
||||
|
||||
(assertEq "default.nix can contain a derivation"
|
||||
(lib.isDerivation tree-tl.default-nix.can-be-drv)
|
||||
true)
|
||||
(assertEq "Even if default.nix is a derivation, children are traversed and merged"
|
||||
tree-tl.default-nix.can-be-drv.subdir.a
|
||||
"Picked up through the drv")
|
||||
(assertEq "default.nix drv is not changed by readTree"
|
||||
tree-tl.default-nix.can-be-drv
|
||||
(import ./test-tree-traversal/default-nix/can-be-drv/default.nix { }))
|
||||
];
|
||||
|
||||
# these each call readTree themselves because the throws have to happen inside assertThrows
|
||||
wrong = it "cannot read these files and will complain" [
|
||||
(assertThrows "this file is not a function"
|
||||
(depot.nix.readTree {
|
||||
path = ./test-wrong-not-a-function;
|
||||
args = { };
|
||||
}).not-a-function)
|
||||
# can’t test for that, assertThrows can’t catch this error
|
||||
# (assertThrows "this file is a function but doesn’t have dots"
|
||||
# (depot.nix.readTree {} ./test-wrong-no-dots).no-dots-in-function)
|
||||
];
|
||||
|
||||
read-markers = depot.nix.readTree {
|
||||
path = ./test-marker;
|
||||
args = { };
|
||||
};
|
||||
|
||||
assertMarkerByPath = path:
|
||||
assertEq "${lib.concatStringsSep "." path} is marked correctly"
|
||||
(lib.getAttrFromPath path read-markers).__readTree
|
||||
path;
|
||||
|
||||
markers = it "marks nodes correctly" [
|
||||
(assertMarkerByPath [ "directory-marked" ])
|
||||
(assertMarkerByPath [ "directory-marked" "nested" ])
|
||||
(assertMarkerByPath [ "file-children" "one" ])
|
||||
(assertMarkerByPath [ "file-children" "two" ])
|
||||
(assertEq "nix file children are marked correctly"
|
||||
read-markers.file-children.__readTreeChildren [ "one" "two" ])
|
||||
(assertEq "directory children are marked correctly"
|
||||
read-markers.directory-marked.__readTreeChildren [ "nested" ])
|
||||
(assertEq "absence of children is marked"
|
||||
read-markers.directory-marked.nested.__readTreeChildren [ ])
|
||||
];
|
||||
|
||||
in
|
||||
runTestsuite "readTree" [
|
||||
example
|
||||
traversal-logic
|
||||
wrong
|
||||
markers
|
||||
]
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
{ depot, pkgs, lib, ... }:
|
||||
let
|
||||
runExecline = import ./runExecline.nix {
|
||||
inherit (pkgs) stdenv;
|
||||
inherit (depot.nix) escapeExecline getBins;
|
||||
inherit pkgs lib;
|
||||
};
|
||||
|
||||
runExeclineLocal = name: args: execline:
|
||||
runExecline name
|
||||
(args // {
|
||||
derivationArgs = args.derivationArgs or { } // {
|
||||
preferLocalBuild = true;
|
||||
allowSubstitutes = false;
|
||||
};
|
||||
})
|
||||
execline;
|
||||
|
||||
tests = import ./tests.nix {
|
||||
inherit runExecline runExeclineLocal;
|
||||
inherit (depot.nix) getBins writeScript;
|
||||
inherit (pkgs) stdenv coreutils;
|
||||
inherit pkgs;
|
||||
};
|
||||
|
||||
in
|
||||
{
|
||||
__functor = _: runExecline;
|
||||
local = runExeclineLocal;
|
||||
inherit tests;
|
||||
}
|
||||
|
|
@ -1,122 +0,0 @@
|
|||
{ pkgs, stdenv, lib, getBins, escapeExecline }:
|
||||
|
||||
# runExecline is a primitive building block
|
||||
# for writing non-kitchen sink builders.
|
||||
#
|
||||
# It’s conceptually similar to `runCommand`,
|
||||
# but instead of concatenating bash scripts left
|
||||
# and right, it actually *uses* the features of
|
||||
# `derivation`, passing things to `args`
|
||||
# and making it possible to overwrite the `builder`
|
||||
# in a sensible manner.
|
||||
#
|
||||
# Additionally, it provides a way to pass a nix string
|
||||
# to `stdin` of the build script.
|
||||
#
|
||||
# Similar to //nix/writeExecline, the passed script is
|
||||
# not a string, but a nested list of nix lists
|
||||
# representing execline blocks. Escaping is
|
||||
# done by the implementation, the user can just use
|
||||
# normal nix strings.
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# runExecline "my-drv" { stdin = "hi!"; } [
|
||||
# "importas" "out" "out"
|
||||
# # this pipes stdout of s6-cat to $out
|
||||
# # and s6-cat redirects from stdin to stdout
|
||||
# "redirfd" "-w" "1" "$out" bins.s6-cat
|
||||
# ]
|
||||
#
|
||||
# which creates a derivation with "hi!" in $out.
|
||||
#
|
||||
# See ./tests.nix for more examples.
|
||||
|
||||
|
||||
let
|
||||
bins = getBins pkgs.execline [
|
||||
"execlineb"
|
||||
{ use = "if"; as = "execlineIf"; }
|
||||
"redirfd"
|
||||
"importas"
|
||||
"exec"
|
||||
]
|
||||
// getBins pkgs.s6-portable-utils [
|
||||
"s6-cat"
|
||||
"s6-grep"
|
||||
"s6-touch"
|
||||
"s6-test"
|
||||
"s6-chmod"
|
||||
];
|
||||
|
||||
in
|
||||
|
||||
# TODO: move name into the attrset
|
||||
name:
|
||||
{
|
||||
# a string to pass as stdin to the execline script
|
||||
stdin ? ""
|
||||
# a program wrapping the acutal execline invocation;
|
||||
# should be in Bernstein-chaining style
|
||||
, builderWrapper ? bins.exec
|
||||
# additional arguments to pass to the derivation
|
||||
, derivationArgs ? { }
|
||||
}:
|
||||
# the execline script as a nested list of string,
|
||||
# representing the blocks;
|
||||
# see docs of `escapeExecline`.
|
||||
execline:
|
||||
|
||||
# those arguments can’t be overwritten
|
||||
assert !derivationArgs ? system;
|
||||
assert !derivationArgs ? name;
|
||||
assert !derivationArgs ? builder;
|
||||
assert !derivationArgs ? args;
|
||||
|
||||
derivation (derivationArgs // {
|
||||
# TODO(Profpatsch): what about cross?
|
||||
inherit (stdenv) system;
|
||||
inherit name;
|
||||
|
||||
# okay, `builtins.toFile` does not accept strings
|
||||
# that reference drv outputs. This means we need
|
||||
# to pass the script and stdin as envvar;
|
||||
# this might clash with another passed envar,
|
||||
# so we give it a long & unique name
|
||||
_runExeclineScript =
|
||||
let
|
||||
in escapeExecline execline;
|
||||
_runExeclineStdin = stdin;
|
||||
passAsFile = [
|
||||
"_runExeclineScript"
|
||||
"_runExeclineStdin"
|
||||
] ++ derivationArgs.passAsFile or [ ];
|
||||
|
||||
# the default, exec acts as identity executable
|
||||
builder = builderWrapper;
|
||||
|
||||
args = [
|
||||
bins.importas # import script file as $script
|
||||
"-ui" # drop the envvar afterwards
|
||||
"script" # substitution name
|
||||
"_runExeclineScriptPath" # passed script file
|
||||
|
||||
bins.importas # do the same for $stdin
|
||||
"-ui"
|
||||
"stdin"
|
||||
"_runExeclineStdinPath"
|
||||
|
||||
bins.redirfd # now we
|
||||
"-r" # read the file
|
||||
"0" # into the stdin of execlineb
|
||||
"$stdin" # that was given via stdin
|
||||
|
||||
bins.execlineb # the actual invocation
|
||||
# TODO(Profpatsch): depending on the use-case, -S0 might not be enough
|
||||
# in all use-cases, then a wrapper for execlineb arguments
|
||||
# should be added (-P, -S, -s).
|
||||
"-S0" # set $@ inside the execline script
|
||||
"-W" # die on syntax error
|
||||
"$script" # substituted by importas
|
||||
];
|
||||
})
|
||||
|
|
@ -1,117 +0,0 @@
|
|||
{ stdenv
|
||||
, pkgs
|
||||
, runExecline
|
||||
, runExeclineLocal
|
||||
, getBins
|
||||
, writeScript
|
||||
# https://www.mail-archive.com/skaware@list.skarnet.org/msg01256.html
|
||||
, coreutils
|
||||
}:
|
||||
|
||||
let
|
||||
|
||||
bins = getBins coreutils [ "mv" ]
|
||||
// getBins pkgs.execline [
|
||||
"execlineb"
|
||||
{ use = "if"; as = "execlineIf"; }
|
||||
"redirfd"
|
||||
"importas"
|
||||
]
|
||||
// getBins pkgs.s6-portable-utils [
|
||||
"s6-chmod"
|
||||
"s6-grep"
|
||||
"s6-touch"
|
||||
"s6-cat"
|
||||
"s6-test"
|
||||
];
|
||||
|
||||
# execline block of depth 1
|
||||
block = args: builtins.map (arg: " ${arg}") args ++ [ "" ];
|
||||
|
||||
# derivation that tests whether a given line exists
|
||||
# in the given file. Does not use runExecline, because
|
||||
# that should be tested after all.
|
||||
fileHasLine = line: file: derivation {
|
||||
name = "run-execline-test-file-${file.name}-has-line";
|
||||
inherit (stdenv) system;
|
||||
builder = bins.execlineIf;
|
||||
args =
|
||||
(block [
|
||||
bins.redirfd
|
||||
"-r"
|
||||
"0"
|
||||
file # read file to stdin
|
||||
bins.s6-grep
|
||||
"-F"
|
||||
"-q"
|
||||
line # and grep for the line
|
||||
])
|
||||
++ [
|
||||
# if the block succeeded, touch $out
|
||||
bins.importas
|
||||
"-ui"
|
||||
"out"
|
||||
"out"
|
||||
bins.s6-touch
|
||||
"$out"
|
||||
];
|
||||
preferLocalBuild = true;
|
||||
allowSubstitutes = false;
|
||||
};
|
||||
|
||||
# basic test that touches out
|
||||
basic = runExeclineLocal "run-execline-test-basic"
|
||||
{ } [
|
||||
"importas"
|
||||
"-ui"
|
||||
"out"
|
||||
"out"
|
||||
"${bins.s6-touch}"
|
||||
"$out"
|
||||
];
|
||||
|
||||
# whether the stdin argument works as intended
|
||||
stdin = fileHasLine "foo" (runExeclineLocal "run-execline-test-stdin"
|
||||
{
|
||||
stdin = "foo\nbar\nfoo";
|
||||
} [
|
||||
"importas"
|
||||
"-ui"
|
||||
"out"
|
||||
"out"
|
||||
# this pipes stdout of s6-cat to $out
|
||||
# and s6-cat redirects from stdin to stdout
|
||||
"redirfd"
|
||||
"-w"
|
||||
"1"
|
||||
"$out"
|
||||
bins.s6-cat
|
||||
]);
|
||||
|
||||
|
||||
wrapWithVar = runExeclineLocal "run-execline-test-wrap-with-var"
|
||||
{
|
||||
builderWrapper = writeScript "var-wrapper" ''
|
||||
#!${bins.execlineb} -S0
|
||||
export myvar myvalue $@
|
||||
'';
|
||||
} [
|
||||
"importas"
|
||||
"-ui"
|
||||
"v"
|
||||
"myvar"
|
||||
"if"
|
||||
[ bins.s6-test "myvalue" "=" "$v" ]
|
||||
"importas"
|
||||
"out"
|
||||
"out"
|
||||
bins.s6-touch
|
||||
"$out"
|
||||
];
|
||||
|
||||
in
|
||||
[
|
||||
basic
|
||||
stdin
|
||||
wrapWithVar
|
||||
]
|
||||
|
|
@ -1,199 +0,0 @@
|
|||
{ lib, pkgs, depot, ... }:
|
||||
|
||||
# Run a nix testsuite.
|
||||
#
|
||||
# The tests are simple assertions on the nix level,
|
||||
# and can use derivation outputs if IfD is enabled.
|
||||
#
|
||||
# You build a testsuite by bundling assertions into
|
||||
# “it”s and then bundling the “it”s into a testsuite.
|
||||
#
|
||||
# Running the testsuite will abort evaluation if
|
||||
# any assertion fails.
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# runTestsuite "myFancyTestsuite" [
|
||||
# (it "does an assertion" [
|
||||
# (assertEq "42 is equal to 42" "42" "42")
|
||||
# (assertEq "also 23" 23 23)
|
||||
# ])
|
||||
# (it "frmbls the brlbr" [
|
||||
# (assertEq true false)
|
||||
# ])
|
||||
# ]
|
||||
#
|
||||
# will fail the second it group because true is not false.
|
||||
|
||||
let
|
||||
inherit (depot.nix.yants)
|
||||
sum
|
||||
struct
|
||||
string
|
||||
any
|
||||
defun
|
||||
list
|
||||
drv
|
||||
bool
|
||||
;
|
||||
|
||||
bins = depot.nix.getBins pkgs.coreutils [ "printf" ]
|
||||
// depot.nix.getBins pkgs.s6-portable-utils [ "s6-touch" "s6-false" "s6-cat" ];
|
||||
|
||||
# Returns true if the given expression throws when `deepSeq`-ed
|
||||
throws = expr:
|
||||
!(builtins.tryEval (builtins.deepSeq expr { })).success;
|
||||
|
||||
# rewrite the builtins.partition result
|
||||
# to use `ok` and `err` instead of `right` and `wrong`.
|
||||
partitionTests = pred: xs:
|
||||
let res = builtins.partition pred xs;
|
||||
in {
|
||||
ok = res.right;
|
||||
err = res.wrong;
|
||||
};
|
||||
|
||||
AssertErrorContext =
|
||||
sum "AssertErrorContext" {
|
||||
not-equal = struct "not-equal" {
|
||||
left = any;
|
||||
right = any;
|
||||
};
|
||||
should-throw = struct "should-throw" {
|
||||
expr = any;
|
||||
};
|
||||
unexpected-throw = struct "unexpected-throw" { };
|
||||
};
|
||||
|
||||
# The result of an assert,
|
||||
# either it’s true (yep) or false (nope).
|
||||
# If it's nope we return an additional context
|
||||
# attribute which gives details on the failure
|
||||
# depending on the type of assert performed.
|
||||
AssertResult =
|
||||
sum "AssertResult" {
|
||||
yep = struct "yep" {
|
||||
test = string;
|
||||
};
|
||||
nope = struct "nope" {
|
||||
test = string;
|
||||
context = AssertErrorContext;
|
||||
};
|
||||
};
|
||||
|
||||
# Result of an it. An it is a bunch of asserts
|
||||
# bundled up with a good description of what is tested.
|
||||
ItResult =
|
||||
struct "ItResult" {
|
||||
it-desc = string;
|
||||
asserts = list AssertResult;
|
||||
};
|
||||
|
||||
# If the given boolean is true return a positive AssertResult.
|
||||
# If the given boolean is false return a negative AssertResult
|
||||
# with the provided AssertErrorContext describing the failure.
|
||||
#
|
||||
# This function is intended as a generic assert to implement
|
||||
# more assert types and is not exposed to the user.
|
||||
assertBoolContext = defun [ AssertErrorContext string bool AssertResult ]
|
||||
(context: desc: res:
|
||||
if res
|
||||
then { yep = { test = desc; }; }
|
||||
else {
|
||||
nope = {
|
||||
test = desc;
|
||||
inherit context;
|
||||
};
|
||||
});
|
||||
|
||||
# assert that left and right values are equal
|
||||
assertEq = defun [ string any any AssertResult ]
|
||||
(desc: left: right:
|
||||
let
|
||||
context = { not-equal = { inherit left right; }; };
|
||||
in
|
||||
assertBoolContext context desc (left == right));
|
||||
|
||||
# assert that the expression throws when `deepSeq`-ed
|
||||
assertThrows = defun [ string any AssertResult ]
|
||||
(desc: expr:
|
||||
let
|
||||
context = { should-throw = { inherit expr; }; };
|
||||
in
|
||||
assertBoolContext context desc (throws expr));
|
||||
|
||||
# assert that the expression does not throw when `deepSeq`-ed
|
||||
assertDoesNotThrow = defun [ string any AssertResult ]
|
||||
(desc: expr:
|
||||
assertBoolContext { unexpected-throw = { }; } desc (!(throws expr)));
|
||||
|
||||
# Annotate a bunch of asserts with a descriptive name
|
||||
it = desc: asserts: {
|
||||
it-desc = desc;
|
||||
inherit asserts;
|
||||
};
|
||||
|
||||
# Run a bunch of its and check whether all asserts are yep.
|
||||
# If not, abort evaluation with `throw`
|
||||
# and print the result of the test suite.
|
||||
#
|
||||
# Takes a test suite name as first argument.
|
||||
runTestsuite = defun [ string (list ItResult) drv ]
|
||||
(name: itResults:
|
||||
let
|
||||
goodAss = ass: AssertResult.match ass {
|
||||
yep = _: true;
|
||||
nope = _: false;
|
||||
};
|
||||
res = partitionTests
|
||||
(it:
|
||||
(partitionTests goodAss it.asserts).err == [ ]
|
||||
)
|
||||
itResults;
|
||||
prettyRes = lib.generators.toPretty { } res;
|
||||
in
|
||||
if res.err == [ ]
|
||||
then
|
||||
depot.nix.runExecline.local "testsuite-${name}-successful" { } [
|
||||
"importas"
|
||||
"out"
|
||||
"out"
|
||||
# force derivation to rebuild if test case list changes
|
||||
"ifelse"
|
||||
[ bins.s6-false ]
|
||||
[
|
||||
bins.printf
|
||||
""
|
||||
(builtins.hashString "sha512" prettyRes)
|
||||
]
|
||||
"if"
|
||||
[ bins.printf "%s\n" "testsuite ${name} successful!" ]
|
||||
bins.s6-touch
|
||||
"$out"
|
||||
]
|
||||
else
|
||||
depot.nix.runExecline.local "testsuite-${name}-failed"
|
||||
{
|
||||
stdin = prettyRes + "\n";
|
||||
} [
|
||||
"importas"
|
||||
"out"
|
||||
"out"
|
||||
"if"
|
||||
[ bins.printf "%s\n" "testsuite ${name} failed!" ]
|
||||
"if"
|
||||
[ bins.s6-cat ]
|
||||
"exit"
|
||||
"1"
|
||||
]);
|
||||
|
||||
in
|
||||
{
|
||||
inherit
|
||||
assertEq
|
||||
assertThrows
|
||||
assertDoesNotThrow
|
||||
it
|
||||
runTestsuite
|
||||
;
|
||||
}
|
||||
|
|
@ -1,110 +0,0 @@
|
|||
{ depot, ... }:
|
||||
|
||||
let
|
||||
inherit (depot.nix.runTestsuite)
|
||||
runTestsuite
|
||||
it
|
||||
assertEq
|
||||
;
|
||||
|
||||
inherit (depot.nix.stateMonad)
|
||||
pure
|
||||
run
|
||||
join
|
||||
fmap
|
||||
bind
|
||||
get
|
||||
set
|
||||
modify
|
||||
after
|
||||
for_
|
||||
getAttr
|
||||
setAttr
|
||||
modifyAttr
|
||||
;
|
||||
|
||||
runStateIndependent = run (throw "This should never be evaluated!");
|
||||
in
|
||||
|
||||
runTestsuite "stateMonad" [
|
||||
(it "behaves correctly independent of state" [
|
||||
(assertEq "pure" (runStateIndependent (pure 21)) 21)
|
||||
(assertEq "join pure" (runStateIndependent (join (pure (pure 42)))) 42)
|
||||
(assertEq "fmap pure" (runStateIndependent (fmap (builtins.mul 2) (pure 21))) 42)
|
||||
(assertEq "bind pure" (runStateIndependent (bind (pure 12) (x: pure x))) 12)
|
||||
])
|
||||
(it "behaves correctly with an integer state" [
|
||||
(assertEq "get" (run 42 get) 42)
|
||||
(assertEq "after set get" (run 21 (after (set 42) get)) 42)
|
||||
(assertEq "after modify get" (run 21 (after (modify (builtins.mul 2)) get)) 42)
|
||||
(assertEq "fmap get" (run 40 (fmap (builtins.add 2) get)) 42)
|
||||
(assertEq "stateful sum list"
|
||||
(run 0 (after
|
||||
(for_
|
||||
[
|
||||
15
|
||||
12
|
||||
10
|
||||
5
|
||||
]
|
||||
(x: modify (builtins.add x)))
|
||||
get))
|
||||
42)
|
||||
])
|
||||
(it "behaves correctly with an attr set state" [
|
||||
(assertEq "getAttr" (run { foo = 42; } (getAttr "foo")) 42)
|
||||
(assertEq "after setAttr getAttr"
|
||||
(run { foo = 21; } (after (setAttr "foo" 42) (getAttr "foo")))
|
||||
42)
|
||||
(assertEq "after modifyAttr getAttr"
|
||||
(run { foo = 10.5; }
|
||||
(after
|
||||
(modifyAttr "foo" (builtins.mul 4))
|
||||
(getAttr "foo")))
|
||||
42)
|
||||
(assertEq "fmap getAttr"
|
||||
(run { foo = 21; } (fmap (builtins.mul 2) (getAttr "foo")))
|
||||
42)
|
||||
(assertEq "after setAttr to insert getAttr"
|
||||
(run { } (after (setAttr "foo" 42) (getAttr "foo")))
|
||||
42)
|
||||
(assertEq "insert permutations"
|
||||
(run
|
||||
{
|
||||
a = 2;
|
||||
b = 3;
|
||||
c = 5;
|
||||
}
|
||||
(after
|
||||
(bind get
|
||||
(state:
|
||||
let
|
||||
names = builtins.attrNames state;
|
||||
in
|
||||
for_ names (name1:
|
||||
for_ names (name2:
|
||||
# this is of course a bit silly, but making it more cumbersome
|
||||
# makes sure the test exercises more of the code.
|
||||
(bind (getAttr name1)
|
||||
(value1:
|
||||
(bind (getAttr name2)
|
||||
(value2:
|
||||
setAttr "${name1}_${name2}" (value1 * value2)))))))))
|
||||
get))
|
||||
{
|
||||
a = 2;
|
||||
b = 3;
|
||||
c = 5;
|
||||
a_a = 4;
|
||||
a_b = 6;
|
||||
a_c = 10;
|
||||
b_a = 6;
|
||||
b_b = 9;
|
||||
b_c = 15;
|
||||
c_c = 25;
|
||||
c_a = 10;
|
||||
c_b = 15;
|
||||
}
|
||||
)
|
||||
])
|
||||
]
|
||||
|
|
@ -1,99 +0,0 @@
|
|||
{ depot, lib, verifyTag, discr, discrDef, match, matchLam }:
|
||||
|
||||
let
|
||||
inherit (depot.nix.runTestsuite)
|
||||
runTestsuite
|
||||
assertEq
|
||||
assertThrows
|
||||
it
|
||||
;
|
||||
|
||||
isTag-test = it "checks whether something is a tag" [
|
||||
(assertEq "is Tag"
|
||||
(verifyTag { foo = "bar"; })
|
||||
{
|
||||
isTag = true;
|
||||
name = "foo";
|
||||
val = "bar";
|
||||
errmsg = null;
|
||||
})
|
||||
(assertEq "is not Tag"
|
||||
(removeAttrs (verifyTag { foo = "bar"; baz = 42; }) [ "errmsg" ])
|
||||
{
|
||||
isTag = false;
|
||||
name = null;
|
||||
val = null;
|
||||
})
|
||||
];
|
||||
|
||||
discr-test = it "can discr things" [
|
||||
(assertEq "id"
|
||||
(discr [
|
||||
{ a = lib.const true; }
|
||||
] "x")
|
||||
{ a = "x"; })
|
||||
(assertEq "bools here, ints there"
|
||||
(discr [
|
||||
{ bool = lib.isBool; }
|
||||
{ int = lib.isInt; }
|
||||
] 25)
|
||||
{ int = 25; })
|
||||
(assertEq "bools here, ints there 2"
|
||||
(discr [
|
||||
{ bool = lib.isBool; }
|
||||
{ int = lib.isInt; }
|
||||
]
|
||||
true)
|
||||
{ bool = true; })
|
||||
(assertEq "fallback to default"
|
||||
(discrDef "def" [
|
||||
{ bool = lib.isBool; }
|
||||
{ int = lib.isInt; }
|
||||
] "foo")
|
||||
{ def = "foo"; })
|
||||
(assertThrows "throws failing to match"
|
||||
(discr [
|
||||
{ fish = x: x == 42; }
|
||||
] 21))
|
||||
];
|
||||
|
||||
match-test = it "can match things" [
|
||||
(assertEq "match example"
|
||||
(
|
||||
let
|
||||
success = { res = 42; };
|
||||
failure = { err = "no answer"; };
|
||||
matcher = {
|
||||
res = i: i + 1;
|
||||
err = _: 0;
|
||||
};
|
||||
in
|
||||
{
|
||||
one = match success matcher;
|
||||
two = match failure matcher;
|
||||
}
|
||||
)
|
||||
{
|
||||
one = 43;
|
||||
two = 0;
|
||||
})
|
||||
(assertEq "matchLam & pipe"
|
||||
(lib.pipe { foo = 42; } [
|
||||
(matchLam {
|
||||
foo = i: if i < 23 then { small = i; } else { big = i; };
|
||||
bar = _: { small = 5; };
|
||||
})
|
||||
(matchLam {
|
||||
small = i: "yay it was small";
|
||||
big = i: "whoo it was big!";
|
||||
})
|
||||
])
|
||||
"whoo it was big!")
|
||||
];
|
||||
|
||||
in
|
||||
runTestsuite "tag" [
|
||||
isTag-test
|
||||
discr-test
|
||||
match-test
|
||||
]
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
{ depot, lib, ... }:
|
||||
|
||||
let
|
||||
inherit (depot.nix.runTestsuite)
|
||||
runTestsuite
|
||||
it
|
||||
assertEq
|
||||
assertThrows
|
||||
assertDoesNotThrow
|
||||
;
|
||||
|
||||
inherit (depot.nix.utils)
|
||||
isDirectory
|
||||
isRegularFile
|
||||
isSymlink
|
||||
storePathName
|
||||
;
|
||||
|
||||
assertUtilsPred = msg: act: exp: [
|
||||
(assertDoesNotThrow "${msg} does not throw" act)
|
||||
(assertEq msg (builtins.tryEval act).value exp)
|
||||
];
|
||||
|
||||
pathPredicates = it "judges paths correctly" (lib.flatten [
|
||||
# isDirectory
|
||||
(assertUtilsPred "directory isDirectory"
|
||||
(isDirectory ./directory)
|
||||
true)
|
||||
(assertUtilsPred "symlink not isDirectory"
|
||||
(isDirectory ./symlink-directory)
|
||||
false)
|
||||
(assertUtilsPred "file not isDirectory"
|
||||
(isDirectory ./directory/file)
|
||||
false)
|
||||
# isRegularFile
|
||||
(assertUtilsPred "file isRegularFile"
|
||||
(isRegularFile ./directory/file)
|
||||
true)
|
||||
(assertUtilsPred "symlink not isRegularFile"
|
||||
(isRegularFile ./symlink-file)
|
||||
false)
|
||||
(assertUtilsPred "directory not isRegularFile"
|
||||
(isRegularFile ./directory)
|
||||
false)
|
||||
# isSymlink
|
||||
(assertUtilsPred "symlink to file isSymlink"
|
||||
(isSymlink ./symlink-file)
|
||||
true)
|
||||
(assertUtilsPred "symlink to directory isSymlink"
|
||||
(isSymlink ./symlink-directory)
|
||||
true)
|
||||
(assertUtilsPred "symlink to symlink isSymlink"
|
||||
(isSymlink ./symlink-symlink-file)
|
||||
true)
|
||||
(assertUtilsPred "symlink to missing file isSymlink"
|
||||
(isSymlink ./missing)
|
||||
true)
|
||||
(assertUtilsPred "directory not isSymlink"
|
||||
(isSymlink ./directory)
|
||||
false)
|
||||
(assertUtilsPred "file not isSymlink"
|
||||
(isSymlink ./directory/file)
|
||||
false)
|
||||
# missing files throw
|
||||
(assertThrows "isDirectory throws on missing file"
|
||||
(isDirectory ./does-not-exist))
|
||||
(assertThrows "isRegularFile throws on missing file"
|
||||
(isRegularFile ./does-not-exist))
|
||||
(assertThrows "isSymlink throws on missing file"
|
||||
(isSymlink ./does-not-exist))
|
||||
]);
|
||||
|
||||
magratheaStorePath =
|
||||
builtins.unsafeDiscardStringContext depot.tools.magrathea.outPath;
|
||||
|
||||
cleanedSource = lib.cleanSource ./.;
|
||||
|
||||
storePathNameTests = it "correctly gets the basename of a store path" [
|
||||
(assertEq "base name of a derivation"
|
||||
(storePathName depot.tools.magrathea)
|
||||
depot.tools.magrathea.name)
|
||||
(assertEq "base name of a store path string"
|
||||
(storePathName magratheaStorePath)
|
||||
depot.tools.magrathea.name)
|
||||
(assertEq "base name of a path within a store path"
|
||||
(storePathName "${magratheaStorePath}/bin/mg") "mg")
|
||||
(assertEq "base name of a path"
|
||||
(storePathName ../default.nix) "default.nix")
|
||||
(assertEq "base name of a cleanSourced path"
|
||||
(storePathName cleanedSource)
|
||||
cleanedSource.name)
|
||||
];
|
||||
in
|
||||
|
||||
runTestsuite "nix.utils" [
|
||||
pathPredicates
|
||||
storePathNameTests
|
||||
]
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
{ depot, pkgs, ... }:
|
||||
|
||||
{ name, src, deps ? (_: [ ]), emacs ? pkgs.emacs-nox }:
|
||||
|
||||
let
|
||||
inherit (pkgs) emacsPackages emacsPackagesFor;
|
||||
inherit (builtins) isString toFile;
|
||||
|
||||
finalEmacs = (emacsPackagesFor emacs).emacsWithPackages deps;
|
||||
|
||||
srcFile =
|
||||
if isString src
|
||||
then toFile "${name}.el" src
|
||||
else src;
|
||||
|
||||
in
|
||||
depot.nix.writeScriptBin name ''
|
||||
#!/bin/sh
|
||||
${finalEmacs}/bin/emacs --batch --no-site-file --script ${srcFile} $@
|
||||
''
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
{ pkgs, depot, ... }:
|
||||
|
||||
# Write an execline script, represented as nested nix lists.
|
||||
# Everything is escaped correctly.
|
||||
# https://skarnet.org/software/execline/
|
||||
|
||||
# TODO(Profpatsch) upstream into nixpkgs
|
||||
|
||||
name:
|
||||
{
|
||||
# "var": substitute readNArgs variables and start $@
|
||||
# from the (readNArgs+1)th argument
|
||||
# "var-full": substitute readNArgs variables and start $@ from $0
|
||||
# "env": don’t substitute, set # and 0…n environment vaariables, where n=$#
|
||||
# "none": don’t substitute or set any positional arguments
|
||||
# "env-no-push": like "env", but bypass the push-phase. Not recommended.
|
||||
argMode ? "var"
|
||||
, # Number of arguments to be substituted as variables (passed to "var"/"-s" or "var-full"/"-S"
|
||||
readNArgs ? 0
|
||||
,
|
||||
}:
|
||||
# Nested list of lists of commands.
|
||||
# Inner lists are translated to execline blocks.
|
||||
argList:
|
||||
|
||||
let
|
||||
env =
|
||||
if argMode == "var" then "s${toString readNArgs}"
|
||||
else if argMode == "var-full" then "S${toString readNArgs}"
|
||||
else if argMode == "env" then ""
|
||||
else if argMode == "none" then "P"
|
||||
else if argMode == "env-no-push" then "p"
|
||||
else abort ''"${toString argMode}" is not a valid argMode, use one of "var", "var-full", "env", "none", "env-no-push".'';
|
||||
|
||||
in
|
||||
depot.nix.writeScript name ''
|
||||
#!${pkgs.execline}/bin/execlineb -W${env}
|
||||
${depot.nix.escapeExecline argList}
|
||||
''
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
{ pkgs, depot, ... }:
|
||||
|
||||
# Write the given string to $out
|
||||
# and make it executable.
|
||||
|
||||
let
|
||||
bins = depot.nix.getBins pkgs.s6-portable-utils [
|
||||
"s6-cat"
|
||||
"s6-chmod"
|
||||
];
|
||||
|
||||
in
|
||||
name:
|
||||
# string of the executable script that is put in $out
|
||||
script:
|
||||
|
||||
depot.nix.runExecline name
|
||||
{
|
||||
stdin = script;
|
||||
derivationArgs = {
|
||||
preferLocalBuild = true;
|
||||
allowSubstitutes = false;
|
||||
};
|
||||
} [
|
||||
"importas"
|
||||
"out"
|
||||
"out"
|
||||
# this pipes stdout of s6-cat to $out
|
||||
# and s6-cat redirects from stdin to stdout
|
||||
"if"
|
||||
[ "redirfd" "-w" "1" "$out" bins.s6-cat ]
|
||||
bins.s6-chmod
|
||||
"0755"
|
||||
"$out"
|
||||
]
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
{ depot, ... }:
|
||||
|
||||
# Like writeScript,
|
||||
# but put the script into `$out/bin/${name}`.
|
||||
|
||||
name:
|
||||
script:
|
||||
|
||||
depot.nix.binify {
|
||||
exe = (depot.nix.writeScript name script);
|
||||
inherit name;
|
||||
}
|
||||
|
|
@ -1,112 +0,0 @@
|
|||
{ depot, pkgs, lib, ... }:
|
||||
|
||||
let
|
||||
bins = depot.nix.getBins pkgs.s6-portable-utils [ "s6-ln" "s6-ls" "s6-touch" ]
|
||||
;
|
||||
|
||||
linkTo = name: path: depot.nix.runExecline.local name { } [
|
||||
"importas"
|
||||
"out"
|
||||
"out"
|
||||
bins.s6-ln
|
||||
"-s"
|
||||
path
|
||||
"$out"
|
||||
];
|
||||
|
||||
# Build a rust executable, $out is the executable.
|
||||
rustSimple = args@{ name, ... }: src:
|
||||
linkTo name "${rustSimpleBin args src}/bin/${name}";
|
||||
|
||||
# Like `rustSimple`, but put the binary in `$out/bin/`.
|
||||
rustSimpleBin =
|
||||
{ name
|
||||
, dependencies ? [ ]
|
||||
, doCheck ? true
|
||||
}: src:
|
||||
(if doCheck then testRustSimple else pkgs.lib.id)
|
||||
(pkgs.buildRustCrate ({
|
||||
pname = name;
|
||||
version = "1.0.0";
|
||||
crateName = name;
|
||||
crateBin = [ name ];
|
||||
dependencies = dependencies;
|
||||
src = pkgs.runCommandLocal "write-main.rs"
|
||||
{
|
||||
src = src;
|
||||
passAsFile = [ "src" ];
|
||||
} ''
|
||||
mkdir -p $out/src/bin
|
||||
cp "$srcPath" $out/src/bin/${name}.rs
|
||||
find $out
|
||||
'';
|
||||
}));
|
||||
|
||||
# Build a rust library, that can be used as dependency to `rustSimple`.
|
||||
# Wrapper around `pkgs.buildRustCrate`, takes all its arguments.
|
||||
rustSimpleLib =
|
||||
{ name
|
||||
, dependencies ? [ ]
|
||||
, doCheck ? true
|
||||
,
|
||||
}: src:
|
||||
(if doCheck then testRustSimple else pkgs.lib.id)
|
||||
(pkgs.buildRustCrate ({
|
||||
pname = name;
|
||||
version = "1.0.0";
|
||||
crateName = name;
|
||||
dependencies = dependencies;
|
||||
src = pkgs.runCommandLocal "write-lib.rs"
|
||||
{
|
||||
src = src;
|
||||
passAsFile = [ "src" ];
|
||||
} ''
|
||||
mkdir -p $out/src
|
||||
cp "$srcPath" $out/src/lib.rs
|
||||
find $out
|
||||
'';
|
||||
}));
|
||||
|
||||
/* Takes a `buildRustCrate` derivation as an input,
|
||||
* builds it with `{ buildTests = true; }` and runs
|
||||
* all tests found in its `tests` dir. If they are
|
||||
* all successful, `$out` will point to the crate
|
||||
* built with `{ buildTests = false; }`, otherwise
|
||||
* it will fail to build.
|
||||
*
|
||||
* See also `nix.drvSeqL` which is used to implement
|
||||
* this behavior.
|
||||
*/
|
||||
testRustSimple = rustDrv:
|
||||
let
|
||||
crate = buildTests: rustDrv.override { inherit buildTests; };
|
||||
tests = depot.nix.runExecline.local "${rustDrv.name}-tests-run" { } [
|
||||
"importas"
|
||||
"out"
|
||||
"out"
|
||||
"if"
|
||||
[
|
||||
"pipeline"
|
||||
[ bins.s6-ls "${crate true}/tests" ]
|
||||
"forstdin"
|
||||
"-o0"
|
||||
"test"
|
||||
"importas"
|
||||
"test"
|
||||
"test"
|
||||
"${crate true}/tests/$test"
|
||||
]
|
||||
bins.s6-touch
|
||||
"$out"
|
||||
];
|
||||
in
|
||||
depot.nix.drvSeqL [ tests ] (crate false);
|
||||
|
||||
in
|
||||
{
|
||||
inherit
|
||||
rustSimple
|
||||
rustSimpleBin
|
||||
rustSimpleLib
|
||||
;
|
||||
}
|
||||
|
|
@ -1,76 +0,0 @@
|
|||
{ depot, pkgs, ... }:
|
||||
|
||||
let
|
||||
inherit (depot.nix.writers)
|
||||
rustSimple
|
||||
rustSimpleLib
|
||||
rustSimpleBin
|
||||
;
|
||||
|
||||
inherit (pkgs)
|
||||
coreutils
|
||||
;
|
||||
|
||||
run = drv: depot.nix.runExecline.local "run-${drv.name}" { } [
|
||||
"if"
|
||||
[ drv ]
|
||||
"importas"
|
||||
"out"
|
||||
"out"
|
||||
"${coreutils}/bin/touch"
|
||||
"$out"
|
||||
];
|
||||
|
||||
rustTransitiveLib = rustSimpleLib
|
||||
{
|
||||
name = "transitive";
|
||||
} ''
|
||||
pub fn transitive(s: &str) -> String {
|
||||
let mut new = s.to_string();
|
||||
new.push_str(" 1 2 3");
|
||||
new
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_transitive() {
|
||||
assert_eq!(transitive("foo").as_str(), "foo 1 2 3")
|
||||
}
|
||||
}
|
||||
'';
|
||||
|
||||
rustTestLib = rustSimpleLib
|
||||
{
|
||||
name = "test_lib";
|
||||
dependencies = [ rustTransitiveLib ];
|
||||
} ''
|
||||
extern crate transitive;
|
||||
use transitive::{transitive};
|
||||
pub fn test() -> String {
|
||||
transitive("test")
|
||||
}
|
||||
'';
|
||||
|
||||
rustWithLib = run (rustSimple
|
||||
{
|
||||
name = "rust-with-lib";
|
||||
dependencies = [ rustTestLib ];
|
||||
} ''
|
||||
extern crate test_lib;
|
||||
|
||||
fn main() {
|
||||
assert_eq!(test_lib::test(), String::from("test 1 2 3"));
|
||||
}
|
||||
'');
|
||||
|
||||
|
||||
in
|
||||
depot.nix.readTree.drvTargets {
|
||||
inherit
|
||||
rustTransitiveLib
|
||||
rustWithLib
|
||||
;
|
||||
}
|
||||
|
|
@ -1,88 +0,0 @@
|
|||
yants
|
||||
=====
|
||||
|
||||
This is a tiny type-checker for data in Nix, written in Nix.
|
||||
|
||||
# Features
|
||||
|
||||
* Checking of primitive types (`int`, `string` etc.)
|
||||
* Checking polymorphic types (`option`, `list`, `either`)
|
||||
* Defining & checking struct/record types
|
||||
* Defining & matching enum types
|
||||
* Defining & matching sum types
|
||||
* Defining function signatures (including curried functions)
|
||||
* Types are composable! `option string`! `list (either int (option float))`!
|
||||
* Type errors also compose!
|
||||
|
||||
Currently lacking:
|
||||
|
||||
* Any kind of inference
|
||||
* Convenient syntax for attribute-set function signatures
|
||||
|
||||
## Primitives & simple polymorphism
|
||||
|
||||

|
||||
|
||||
## Structs
|
||||
|
||||

|
||||
|
||||
## Nested structs!
|
||||
|
||||

|
||||
|
||||
## Enums!
|
||||
|
||||

|
||||
|
||||
## Functions!
|
||||
|
||||

|
||||
|
||||
# Usage
|
||||
|
||||
Yants can be imported from its `default.nix`. A single attribute (`lib`) can be
|
||||
passed, which will otherwise be imported from `<nixpkgs>`.
|
||||
|
||||
TIP: You do not need to clone the entire TVL repository to use Yants!
|
||||
You can clone just this project through josh: `git clone
|
||||
https://code.tvl.fyi/depot.git:/nix/yants.git`
|
||||
|
||||
Examples for the most common import methods would be:
|
||||
|
||||
1. Import into scope with `with`:
|
||||
```nix
|
||||
with (import ./default.nix {});
|
||||
# ... Nix code that uses yants ...
|
||||
```
|
||||
|
||||
2. Import as a named variable:
|
||||
```nix
|
||||
let yants = import ./default.nix {};
|
||||
in yants.string "foo" # or other uses ...
|
||||
````
|
||||
|
||||
3. Overlay into `pkgs.lib`:
|
||||
```nix
|
||||
# wherever you import your package set (e.g. from <nixpkgs>):
|
||||
import <nixpkgs> {
|
||||
overlays = [
|
||||
(self: super: {
|
||||
lib = super.lib // { yants = import ./default.nix { inherit (super) lib; }; };
|
||||
})
|
||||
];
|
||||
}
|
||||
|
||||
# yants now lives at lib.yants, besides the other library functions!
|
||||
```
|
||||
|
||||
Please see my [Nix one-pager](https://github.com/tazjin/nix-1p) for more generic
|
||||
information about the Nix language and what the above constructs mean.
|
||||
|
||||
# Stability
|
||||
|
||||
The current API of Yants is **not yet** considered stable, but it works fine and
|
||||
should continue to do so even if used at an older version.
|
||||
|
||||
Yants' tests use Nix versions above 2.2 - compatibility with older versions is
|
||||
not guaranteed.
|
||||
|
|
@ -1,368 +0,0 @@
|
|||
# Copyright 2019 Google LLC
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
# Provides a "type-system" for Nix that provides various primitive &
|
||||
# polymorphic types as well as the ability to define & check records.
|
||||
#
|
||||
# All types (should) compose as expected.
|
||||
|
||||
{ lib ? (import <nixpkgs> { }).lib, ... }:
|
||||
|
||||
with builtins; let
|
||||
prettyPrint = lib.generators.toPretty { };
|
||||
|
||||
# typedef' :: struct {
|
||||
# name = string;
|
||||
# checkType = function; (a -> result)
|
||||
# checkToBool = option function; (result -> bool)
|
||||
# toError = option function; (a -> result -> string)
|
||||
# def = option any;
|
||||
# match = option function;
|
||||
# } -> type
|
||||
# -> (a -> b)
|
||||
# -> (b -> bool)
|
||||
# -> (a -> b -> string)
|
||||
# -> type
|
||||
#
|
||||
# This function creates an attribute set that acts as a type.
|
||||
#
|
||||
# It receives a type name, a function that is used to perform a
|
||||
# check on an arbitrary value, a function that can translate the
|
||||
# return of that check to a boolean that informs whether the value
|
||||
# is type-conformant, and a function that can construct error
|
||||
# messages from the check result.
|
||||
#
|
||||
# This function is the low-level primitive used to create types. For
|
||||
# many cases the higher-level 'typedef' function is more appropriate.
|
||||
typedef' =
|
||||
{ name
|
||||
, checkType
|
||||
, checkToBool ? (result: result.ok)
|
||||
, toError ? (_: result: result.err)
|
||||
, def ? null
|
||||
, match ? null
|
||||
}: {
|
||||
inherit name checkToBool toError;
|
||||
|
||||
# check :: a -> bool
|
||||
#
|
||||
# This function is used to determine whether a given type is
|
||||
# conformant.
|
||||
check = value: checkToBool (checkType value);
|
||||
|
||||
# checkType :: a -> struct { ok = bool; err = option string; }
|
||||
#
|
||||
# This function checks whether the passed value is type conformant
|
||||
# and returns an optional type error string otherwise.
|
||||
inherit checkType;
|
||||
|
||||
# __functor :: a -> a
|
||||
#
|
||||
# This function checks whether the passed value is type conformant
|
||||
# and throws an error if it is not.
|
||||
#
|
||||
# The name of this function is a special attribute in Nix that
|
||||
# makes it possible to execute a type attribute set like a normal
|
||||
# function.
|
||||
__functor = self: value:
|
||||
let result = self.checkType value;
|
||||
in if checkToBool result then value
|
||||
else throw (toError value result);
|
||||
};
|
||||
|
||||
typeError = type: val:
|
||||
"expected type '${type}', but value '${prettyPrint val}' is of type '${typeOf val}'";
|
||||
|
||||
# typedef :: string -> (a -> bool) -> type
|
||||
#
|
||||
# typedef is the simplified version of typedef' which uses a default
|
||||
# error message constructor.
|
||||
typedef = name: check: typedef' {
|
||||
inherit name;
|
||||
checkType = v:
|
||||
let res = check v;
|
||||
in {
|
||||
ok = res;
|
||||
} // (lib.optionalAttrs (!res) {
|
||||
err = typeError name v;
|
||||
});
|
||||
};
|
||||
|
||||
checkEach = name: t: l: foldl'
|
||||
(acc: e:
|
||||
let
|
||||
res = t.checkType e;
|
||||
isT = t.checkToBool res;
|
||||
in
|
||||
{
|
||||
ok = acc.ok && isT;
|
||||
err =
|
||||
if isT
|
||||
then acc.err
|
||||
else acc.err + "${prettyPrint e}: ${t.toError e res}\n";
|
||||
})
|
||||
{ ok = true; err = "expected type ${name}, but found:\n"; }
|
||||
l;
|
||||
in
|
||||
lib.fix (self: {
|
||||
# Primitive types
|
||||
any = typedef "any" (_: true);
|
||||
unit = typedef "unit" (v: v == { });
|
||||
int = typedef "int" isInt;
|
||||
bool = typedef "bool" isBool;
|
||||
float = typedef "float" isFloat;
|
||||
string = typedef "string" isString;
|
||||
path = typedef "path" (x: typeOf x == "path");
|
||||
drv = typedef "derivation" (x: isAttrs x && x ? "type" && x.type == "derivation");
|
||||
function = typedef "function" (x: isFunction x || (isAttrs x && x ? "__functor"
|
||||
&& isFunction x.__functor));
|
||||
|
||||
# Type for types themselves. Useful when defining polymorphic types.
|
||||
type = typedef "type" (x:
|
||||
isAttrs x
|
||||
&& hasAttr "name" x && self.string.check x.name
|
||||
&& hasAttr "checkType" x && self.function.check x.checkType
|
||||
&& hasAttr "checkToBool" x && self.function.check x.checkToBool
|
||||
&& hasAttr "toError" x && self.function.check x.toError
|
||||
);
|
||||
|
||||
# Polymorphic types
|
||||
option = t: typedef' rec {
|
||||
name = "option<${t.name}>";
|
||||
checkType = v:
|
||||
let res = t.checkType v;
|
||||
in {
|
||||
ok = isNull v || (self.type t).checkToBool res;
|
||||
err = "expected type ${name}, but value does not conform to '${t.name}': "
|
||||
+ t.toError v res;
|
||||
};
|
||||
};
|
||||
|
||||
eitherN = tn: typedef "either<${concatStringsSep ", " (map (x: x.name) tn)}>"
|
||||
(x: any (t: (self.type t).check x) tn);
|
||||
|
||||
either = t1: t2: self.eitherN [ t1 t2 ];
|
||||
|
||||
list = t: typedef' rec {
|
||||
name = "list<${t.name}>";
|
||||
|
||||
checkType = v:
|
||||
if isList v
|
||||
then checkEach name (self.type t) v
|
||||
else {
|
||||
ok = false;
|
||||
err = typeError name v;
|
||||
};
|
||||
};
|
||||
|
||||
attrs = t: typedef' rec {
|
||||
name = "attrs<${t.name}>";
|
||||
|
||||
checkType = v:
|
||||
if isAttrs v
|
||||
then checkEach name (self.type t) (attrValues v)
|
||||
else {
|
||||
ok = false;
|
||||
err = typeError name v;
|
||||
};
|
||||
};
|
||||
|
||||
# Structs / record types
|
||||
#
|
||||
# Checks that all fields match their declared types, no optional
|
||||
# fields are missing and no unexpected fields occur in the struct.
|
||||
#
|
||||
# Anonymous structs are supported (e.g. for nesting) by omitting the
|
||||
# name.
|
||||
#
|
||||
# TODO: Support open records?
|
||||
struct =
|
||||
# Struct checking is more involved than the simpler types above.
|
||||
# To make the actual type definition more readable, several
|
||||
# helpers are defined below.
|
||||
let
|
||||
# checkField checks an individual field of the struct against
|
||||
# its definition and creates a typecheck result. These results
|
||||
# are aggregated during the actual checking.
|
||||
checkField = def: name: value:
|
||||
let result = def.checkType value; in rec {
|
||||
ok = def.checkToBool result;
|
||||
err =
|
||||
if !ok && isNull value
|
||||
then "missing required ${def.name} field '${name}'\n"
|
||||
else "field '${name}': ${def.toError value result}\n";
|
||||
};
|
||||
|
||||
# checkExtraneous determines whether a (closed) struct contains
|
||||
# any fields that are not part of the definition.
|
||||
checkExtraneous = def: has: acc:
|
||||
if (length has) == 0 then acc
|
||||
else if (hasAttr (head has) def)
|
||||
then checkExtraneous def (tail has) acc
|
||||
else
|
||||
checkExtraneous def (tail has) {
|
||||
ok = false;
|
||||
err = acc.err + "unexpected struct field '${head has}'\n";
|
||||
};
|
||||
|
||||
# checkStruct combines all structure checks and creates one
|
||||
# typecheck result from them
|
||||
checkStruct = def: value:
|
||||
let
|
||||
init = { ok = true; err = ""; };
|
||||
extraneous = checkExtraneous def (attrNames value) init;
|
||||
|
||||
checkedFields = map
|
||||
(n:
|
||||
let v = if hasAttr n value then value."${n}" else null;
|
||||
in checkField def."${n}" n v)
|
||||
(attrNames def);
|
||||
|
||||
combined = foldl'
|
||||
(acc: res: {
|
||||
ok = acc.ok && res.ok;
|
||||
err = if !res.ok then acc.err + res.err else acc.err;
|
||||
})
|
||||
init
|
||||
checkedFields;
|
||||
in
|
||||
{
|
||||
ok = combined.ok && extraneous.ok;
|
||||
err = combined.err + extraneous.err;
|
||||
};
|
||||
|
||||
struct' = name: def: typedef' {
|
||||
inherit name def;
|
||||
checkType = value:
|
||||
if isAttrs value
|
||||
then (checkStruct (self.attrs self.type def) value)
|
||||
else { ok = false; err = typeError name value; };
|
||||
|
||||
toError = _: result: "expected '${name}'-struct, but found:\n" + result.err;
|
||||
};
|
||||
in
|
||||
arg: if isString arg then (struct' arg) else (struct' "anon" arg);
|
||||
|
||||
# Enums & pattern matching
|
||||
enum =
|
||||
let
|
||||
plain = name: def: typedef' {
|
||||
inherit name def;
|
||||
|
||||
checkType = (x: isString x && elem x def);
|
||||
checkToBool = x: x;
|
||||
toError = value: _: "'${prettyPrint value} is not a member of enum ${name}";
|
||||
};
|
||||
enum' = name: def: lib.fix (e: (plain name def) // {
|
||||
match = x: actions: deepSeq (map e (attrNames actions)) (
|
||||
let
|
||||
actionKeys = attrNames actions;
|
||||
missing = foldl' (m: k: if (elem k actionKeys) then m else m ++ [ k ]) [ ] def;
|
||||
in
|
||||
if (length missing) > 0
|
||||
then throw "Missing match action for members: ${prettyPrint missing}"
|
||||
else actions."${e x}"
|
||||
);
|
||||
});
|
||||
in
|
||||
arg: if isString arg then (enum' arg) else (enum' "anon" arg);
|
||||
|
||||
# Sum types
|
||||
#
|
||||
# The representation of a sum type is an attribute set with only one
|
||||
# value, where the key of the value denotes the variant of the type.
|
||||
sum =
|
||||
let
|
||||
plain = name: def: typedef' {
|
||||
inherit name def;
|
||||
checkType = (x:
|
||||
let variant = elemAt (attrNames x) 0;
|
||||
in if isAttrs x && length (attrNames x) == 1 && hasAttr variant def
|
||||
then
|
||||
let
|
||||
t = def."${variant}";
|
||||
v = x."${variant}";
|
||||
res = t.checkType v;
|
||||
in
|
||||
if t.checkToBool res
|
||||
then { ok = true; }
|
||||
else {
|
||||
ok = false;
|
||||
err = "while checking '${name}' variant '${variant}': "
|
||||
+ t.toError v res;
|
||||
}
|
||||
else { ok = false; err = typeError name x; }
|
||||
);
|
||||
};
|
||||
sum' = name: def: lib.fix (s: (plain name def) // {
|
||||
match = x: actions:
|
||||
let
|
||||
variant = deepSeq (s x) (elemAt (attrNames x) 0);
|
||||
actionKeys = attrNames actions;
|
||||
defKeys = attrNames def;
|
||||
missing = foldl' (m: k: if (elem k actionKeys) then m else m ++ [ k ]) [ ] defKeys;
|
||||
in
|
||||
if (length missing) > 0
|
||||
then throw "Missing match action for variants: ${prettyPrint missing}"
|
||||
else actions."${variant}" x."${variant}";
|
||||
});
|
||||
in
|
||||
arg: if isString arg then (sum' arg) else (sum' "anon" arg);
|
||||
|
||||
# Typed function definitions
|
||||
#
|
||||
# These definitions wrap the supplied function in type-checking
|
||||
# forms that are evaluated when the function is called.
|
||||
#
|
||||
# Note that typed functions themselves are not types and can not be
|
||||
# used to check values for conformity.
|
||||
defun =
|
||||
let
|
||||
mkFunc = sig: f: {
|
||||
inherit sig;
|
||||
__toString = self: foldl' (s: t: "${s} -> ${t.name}")
|
||||
"λ :: ${(head self.sig).name}"
|
||||
(tail self.sig);
|
||||
__functor = _: f;
|
||||
};
|
||||
|
||||
defun' = sig: func:
|
||||
if length sig > 2
|
||||
then mkFunc sig (x: defun' (tail sig) (func ((head sig) x)))
|
||||
else mkFunc sig (x: ((head (tail sig)) (func ((head sig) x))));
|
||||
|
||||
in
|
||||
sig: func:
|
||||
if length sig < 2
|
||||
then (throw "Signature must at least have two types (a -> b)")
|
||||
else defun' sig func;
|
||||
|
||||
# Restricting types
|
||||
#
|
||||
# `restrict` wraps a type `t`, and uses a predicate `pred` to further
|
||||
# restrict the values, giving the restriction a descriptive `name`.
|
||||
#
|
||||
# First, the wrapped type definition is checked (e.g. int) and then the
|
||||
# value is checked with the predicate, so the predicate can already
|
||||
# depend on the value being of the wrapped type.
|
||||
restrict = name: pred: t:
|
||||
let restriction = "${t.name}[${name}]"; in typedef' {
|
||||
name = restriction;
|
||||
checkType = v:
|
||||
let res = t.checkType v;
|
||||
in
|
||||
if !(t.checkToBool res)
|
||||
then res
|
||||
else
|
||||
let
|
||||
iok = pred v;
|
||||
in
|
||||
if isBool iok then {
|
||||
ok = iok;
|
||||
err = "${prettyPrint v} does not conform to restriction '${restriction}'";
|
||||
} else
|
||||
# use throw here to avoid spamming the build log
|
||||
throw "restriction '${restriction}' predicate returned unexpected value '${prettyPrint iok}' instead of boolean";
|
||||
};
|
||||
|
||||
})
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 40 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 32 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 69 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 42 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 68 KiB |
|
|
@ -1,158 +0,0 @@
|
|||
{ depot, pkgs, ... }:
|
||||
|
||||
with depot.nix.yants;
|
||||
|
||||
# Note: Derivations are not included in the tests below as they cause
|
||||
# issues with deepSeq.
|
||||
|
||||
let
|
||||
|
||||
inherit (depot.nix.runTestsuite)
|
||||
runTestsuite
|
||||
it
|
||||
assertEq
|
||||
assertThrows
|
||||
assertDoesNotThrow
|
||||
;
|
||||
|
||||
# this derivation won't throw if evaluated with deepSeq
|
||||
# unlike most things even remotely related with nixpkgs
|
||||
trivialDerivation = derivation {
|
||||
name = "trivial-derivation";
|
||||
inherit (pkgs.stdenv) system;
|
||||
builder = "/bin/sh";
|
||||
args = [ "-c" "echo hello > $out" ];
|
||||
};
|
||||
|
||||
testPrimitives = it "checks that all primitive types match" [
|
||||
(assertDoesNotThrow "unit type" (unit { }))
|
||||
(assertDoesNotThrow "int type" (int 15))
|
||||
(assertDoesNotThrow "bool type" (bool false))
|
||||
(assertDoesNotThrow "float type" (float 13.37))
|
||||
(assertDoesNotThrow "string type" (string "Hello!"))
|
||||
(assertDoesNotThrow "function type" (function (x: x * 2)))
|
||||
(assertDoesNotThrow "path type" (path /nix))
|
||||
(assertDoesNotThrow "derivation type" (drv trivialDerivation))
|
||||
];
|
||||
|
||||
testPoly = it "checks that polymorphic types work as intended" [
|
||||
(assertDoesNotThrow "option type" (option int null))
|
||||
(assertDoesNotThrow "list type" (list string [ "foo" "bar" ]))
|
||||
(assertDoesNotThrow "either type" (either int float 42))
|
||||
];
|
||||
|
||||
# Test that structures work as planned.
|
||||
person = struct "person" {
|
||||
name = string;
|
||||
age = int;
|
||||
|
||||
contact = option (struct {
|
||||
email = string;
|
||||
phone = option string;
|
||||
});
|
||||
};
|
||||
|
||||
testStruct = it "checks that structures work as intended" [
|
||||
(assertDoesNotThrow "person struct" (person {
|
||||
name = "Brynhjulf";
|
||||
age = 42;
|
||||
contact.email = "brynhjulf@yants.nix";
|
||||
}))
|
||||
];
|
||||
|
||||
# Test enum definitions & matching
|
||||
colour = enum "colour" [ "red" "blue" "green" ];
|
||||
colourMatcher = {
|
||||
red = "It is in fact red!";
|
||||
blue = "It should not be blue!";
|
||||
green = "It should not be green!";
|
||||
};
|
||||
|
||||
testEnum = it "checks enum definitions and matching" [
|
||||
(assertEq "enum is matched correctly"
|
||||
"It is in fact red!"
|
||||
(colour.match "red" colourMatcher))
|
||||
(assertThrows "out of bounds enum fails"
|
||||
(colour.match "alpha" (colourMatcher // {
|
||||
alpha = "This should never happen";
|
||||
}))
|
||||
)
|
||||
];
|
||||
|
||||
# Test sum type definitions
|
||||
creature = sum "creature" {
|
||||
human = struct {
|
||||
name = string;
|
||||
age = option int;
|
||||
};
|
||||
|
||||
pet = enum "pet" [ "dog" "lizard" "cat" ];
|
||||
};
|
||||
some-human = creature {
|
||||
human = {
|
||||
name = "Brynhjulf";
|
||||
age = 42;
|
||||
};
|
||||
};
|
||||
|
||||
testSum = it "checks sum types definitions and matching" [
|
||||
(assertDoesNotThrow "creature sum type" some-human)
|
||||
(assertEq "sum type is matched correctly"
|
||||
"It's a human named Brynhjulf"
|
||||
(creature.match some-human {
|
||||
human = v: "It's a human named ${v.name}";
|
||||
pet = v: "It's not supposed to be a pet!";
|
||||
})
|
||||
)
|
||||
];
|
||||
|
||||
# Test curried function definitions
|
||||
func = defun [ string int string ]
|
||||
(name: age: "${name} is ${toString age} years old");
|
||||
|
||||
testFunctions = it "checks function definitions" [
|
||||
(assertDoesNotThrow "function application" (func "Brynhjulf" 42))
|
||||
];
|
||||
|
||||
# Test that all types are types.
|
||||
assertIsType = name: t:
|
||||
assertDoesNotThrow "${name} is a type" (type t);
|
||||
testTypes = it "checks that all types are types" [
|
||||
(assertIsType "any" any)
|
||||
(assertIsType "bool" bool)
|
||||
(assertIsType "drv" drv)
|
||||
(assertIsType "float" float)
|
||||
(assertIsType "int" int)
|
||||
(assertIsType "string" string)
|
||||
(assertIsType "path" path)
|
||||
|
||||
(assertIsType "attrs int" (attrs int))
|
||||
(assertIsType "eitherN [ ... ]" (eitherN [ int string bool ]))
|
||||
(assertIsType "either int string" (either int string))
|
||||
(assertIsType "enum [ ... ]" (enum [ "foo" "bar" ]))
|
||||
(assertIsType "list string" (list string))
|
||||
(assertIsType "option int" (option int))
|
||||
(assertIsType "option (list string)" (option (list string)))
|
||||
(assertIsType "struct { ... }" (struct { a = int; b = option string; }))
|
||||
(assertIsType "sum { ... }" (sum { a = int; b = option string; }))
|
||||
];
|
||||
|
||||
testRestrict = it "checks restrict types" [
|
||||
(assertDoesNotThrow "< 42" ((restrict "< 42" (i: i < 42) int) 25))
|
||||
(assertDoesNotThrow "list length < 3"
|
||||
((restrict "not too long" (l: builtins.length l < 3) (list int)) [ 1 2 ]))
|
||||
(assertDoesNotThrow "list eq 5"
|
||||
(list (restrict "eq 5" (v: v == 5) any) [ 5 5 5 ]))
|
||||
];
|
||||
|
||||
in
|
||||
runTestsuite "yants" [
|
||||
testPrimitives
|
||||
testPoly
|
||||
testStruct
|
||||
testEnum
|
||||
testSum
|
||||
testFunctions
|
||||
testTypes
|
||||
testRestrict
|
||||
]
|
||||
Loading…
Add table
Add a link
Reference in a new issue