[Backport release-25.05] arrayUtilities: init (#416144)

This commit is contained in:
Philip Taron
2025-06-14 14:23:35 -07:00
committed by GitHub
18 changed files with 1202 additions and 27 deletions

View File

@@ -62,6 +62,7 @@
/pkgs/build-support/cc-wrapper @Ericson2314
/pkgs/build-support/bintools-wrapper @Ericson2314
/pkgs/build-support/setup-hooks @Ericson2314
/pkgs/build-support/setup-hooks/arrayUtilities @ConnorBaker
/pkgs/build-support/setup-hooks/auto-patchelf.sh @layus
/pkgs/by-name/au/auto-patchelf @layus

View File

@@ -0,0 +1,45 @@
# shellcheck shell=bash
# getSortedMapKeys
# Stores the sorted keys of the input associative array referenced by inputMapRef in the indexed arrray referenced by
# outputArrRef.
#
# Note from the Bash manual on arrays:
# There is no maximum limit on the size of an array, nor any requirement that members be indexed or assigned contiguously.
# - https://www.gnu.org/software/bash/manual/html_node/Arrays.html
#
# Since no guarantees are made about the order in which associative maps are traversed, this function is primarly
# useful for getting rid of yet another source of non-determinism. As an added benefit, it checks that the arguments
# provided are of correct type, unlike native parameter expansion which will accept expansions of strings.
#
# Arguments:
# - inputMapRef: a reference to an associative array (not mutated)
# - outputArrRef: a reference to an indexed array (contents are replaced entirely)
#
# Returns 0.
getSortedMapKeys() {
if (($# != 2)); then
nixErrorLog "expected two arguments!"
nixErrorLog "usage: getSortedMapKeys inputMapRef outputArrRef"
exit 1
fi
local -rn inputMapRef="$1"
# shellcheck disable=SC2178
# Don't warn about outputArrRef being used as an array because it is an array.
local -rn outputArrRef="$2"
if ! isDeclaredMap "${!inputMapRef}"; then
nixErrorLog "first argument inputMapRef must be a reference to an associative array"
exit 1
elif ! isDeclaredArray "${!outputArrRef}"; then
nixErrorLog "second argument outputArrRef must be a reference to an indexed array"
exit 1
fi
# shellcheck disable=SC2034
local -a keys=("${!inputMapRef[@]}")
sortArray keys "${!outputArrRef}"
return 0
}

View File

@@ -0,0 +1,17 @@
{
callPackages,
isDeclaredArray,
isDeclaredMap,
makeSetupHook,
sortArray,
}:
makeSetupHook {
name = "getSortedMapKeys";
propagatedBuildInputs = [
isDeclaredArray
isDeclaredMap
sortArray
];
passthru.tests = callPackages ./tests.nix { };
meta.description = "Gets the sorted indices of an associative array";
} ./getSortedMapKeys.bash

View File

@@ -0,0 +1,80 @@
# NOTE: Tests related to getSortedMapKeys go here.
{
getSortedMapKeys,
lib,
testers,
}:
let
inherit (lib.attrsets) recurseIntoAttrs;
inherit (testers) shellcheck shfmt testEqualArrayOrMap;
check =
{
name,
valuesMap,
expectedArray,
}:
(testEqualArrayOrMap {
inherit name valuesMap expectedArray;
script = ''
set -eu
nixLog "running getSortedMapKeys with valuesMap to populate actualArray"
getSortedMapKeys valuesMap actualArray
'';
}).overrideAttrs
(prevAttrs: {
nativeBuildInputs = prevAttrs.nativeBuildInputs or [ ] ++ [ getSortedMapKeys ];
});
in
recurseIntoAttrs {
shellcheck = shellcheck {
name = "getSortedMapKeys";
src = ./getSortedMapKeys.bash;
};
shfmt = shfmt {
name = "getSortedMapKeys";
src = ./getSortedMapKeys.bash;
};
empty = check {
name = "empty";
valuesMap = { };
expectedArray = [ ];
};
singleton = check {
name = "singleton";
valuesMap = {
"apple" = "fruit";
};
expectedArray = [ "apple" ];
};
keysAreSorted = check {
name = "keysAreSorted";
valuesMap = {
"apple" = "fruit";
"bee" = "insect";
"carrot" = "vegetable";
};
expectedArray = [
"apple"
"bee"
"carrot"
];
};
# NOTE: While keys can be whitespace, they cannot be null (empty).
keysCanBeWhitespace = check {
name = "keysCanBeWhitespace";
valuesMap = {
" " = 1;
" " = 2;
};
expectedArray = [
" "
" "
];
};
}

View File

@@ -0,0 +1,14 @@
# shellcheck shell=bash
# isDeclaredArray
# Tests if inputArrayRef refers to a declared, indexed array.
#
# Arguments:
# - inputArrayRef: a reference to an indexed array (not mutated)
#
# Returns 0 if the indexed array is declared, 1 otherwise.
isDeclaredArray() {
# NOTE: We must dereference the name ref to get the type of the underlying variable.
# shellcheck disable=SC2034
local -nr inputArrayRef="$1" && [[ ${!inputArrayRef@a} =~ a ]]
}

View File

@@ -0,0 +1,9 @@
{
callPackages,
makeSetupHook,
}:
makeSetupHook {
name = "isDeclaredArray";
passthru.tests = callPackages ./tests.nix { };
meta.description = "Tests if an array is declared";
} ./isDeclaredArray.bash

View File

@@ -0,0 +1,353 @@
# NOTE: Tests related to isDeclaredArray go here.
{
isDeclaredArray,
lib,
runCommand,
testers,
}:
let
inherit (lib.attrsets) recurseIntoAttrs;
inherit (testers) shellcheck shfmt testBuildFailure';
commonArgs = {
__structuredAttrs = true;
strictDeps = true;
preferLocalBuild = true;
nativeBuildInputs = [ isDeclaredArray ];
};
check =
let
mkLine =
intro: values:
"${if intro == null then "" else intro + " "}check${if values == null then "" else "=" + values}";
mkScope =
scope: line:
if scope == null then
line
else if scope == "function" then
''
foo() {
${line}
}
foo
''
else
builtins.throw "Invalid scope: ${scope}";
in
{
name,
scope,
intro,
values,
}:
runCommand name commonArgs ''
set -eu
${mkScope scope (mkLine intro values)}
if isDeclaredArray check; then
nixLog "test passed"
touch "$out"
else
nixErrorLog "test failed"
exit 1
fi
'';
in
recurseIntoAttrs {
shellcheck = shellcheck {
name = "isDeclaredArray";
src = ./isDeclaredArray.bash;
};
shfmt = shfmt {
name = "isDeclaredArray";
src = ./isDeclaredArray.bash;
};
undeclaredFails = testBuildFailure' {
name = "undeclaredFails";
drv = runCommand "undeclared" commonArgs ''
set -eu
if isDeclaredArray undeclared; then
nixLog "test passed"
touch "$out"
else
nixErrorLog "test failed"
exit 1
fi
'';
expectedBuilderLogEntries = [
"test failed"
];
};
mapFails = testBuildFailure' {
name = "mapFails";
drv = runCommand "map" commonArgs ''
set -eu
local -A map
if isDeclaredArray map; then
nixLog "test passed"
touch "$out"
else
nixErrorLog "test failed"
exit 1
fi
'';
expectedBuilderLogEntries = [
"test failed"
];
};
emptyStringNamerefFails = testBuildFailure' {
name = "emptyStringNamerefFails";
drv = runCommand "emptyStringNameref" commonArgs ''
set -eu
if isDeclaredArray ""; then
nixLog "test passed"
touch "$out"
else
nixErrorLog "test failed"
exit 1
fi
'';
expectedBuilderLogEntries = [
"local: `': not a valid identifier"
"test failed"
];
};
namerefToEmptyStringFails = testBuildFailure' {
name = "namerefToEmptyStringFails";
drv = check {
name = "namerefToEmptyString";
scope = null;
intro = "local -n";
values = "";
};
expectedBuilderLogEntries = [
"local: `': not a valid identifier"
# The test fails in such a way that it exits immediately, without returning to the else branch.
];
};
sameScopeEmptyStringFails = testBuildFailure' {
name = "sameScopeEmptyStringFails";
drv = check {
name = "sameScopeEmptyString";
scope = null;
intro = null;
values = "";
};
expectedBuilderLogEntries = [
"test failed"
];
};
sameScopeEmptyArray = check {
name = "sameScopeEmptyArray";
scope = null;
intro = null;
values = "()";
};
sameScopeSingletonArray = check {
name = "sameScopeSingletonArray";
scope = null;
intro = null;
values = ''("hello!")'';
};
sameScopeLocalUnsetArray = check {
name = "sameScopeLocalUnsetArray";
scope = null;
intro = "local -a";
values = null;
};
sameScopeLocalEmptyArray = check {
name = "sameScopeLocalEmptyArray";
scope = null;
intro = "local -a";
values = "()";
};
sameScopeLocalSingletonArray = check {
name = "sameScopeLocalSingletonArray";
scope = null;
intro = "local -a";
values = ''("hello!")'';
};
sameScopeDeclareUnsetArray = check {
name = "sameScopeDeclareUnsetArray";
scope = null;
intro = "declare -a";
values = null;
};
sameScopeDeclareEmptyArray = check {
name = "sameScopeDeclareEmptyArray";
scope = null;
intro = "declare -a";
values = "()";
};
sameScopeDeclareSingletonArray = check {
name = "sameScopeDeclareSingletonArray";
scope = null;
intro = "declare -a";
values = ''("hello!")'';
};
previousScopeEmptyStringFails = testBuildFailure' {
name = "previousScopeEmptyStringFails";
drv = check {
name = "previousScopeEmptyString";
scope = "function";
intro = null;
values = "";
};
expectedBuilderLogEntries = [
"test failed"
];
};
# Works because the variable isn't lexically scoped.
previousScopeEmptyArray = check {
name = "previousScopeEmptyArray";
scope = "function";
intro = null;
values = "()";
};
# Works because the variable isn't lexically scoped.
previousScopeSingletonArray = check {
name = "previousScopeSingletonArray";
scope = "function";
intro = null;
values = ''("hello!")'';
};
previousScopeLocalUnsetArrayFails = testBuildFailure' {
name = "previousScopeLocalUnsetArrayFails";
drv = check {
name = "previousScopeLocalUnsetArray";
scope = "function";
intro = "local -a";
values = null;
};
expectedBuilderLogEntries = [
"test failed"
];
};
previousScopeLocalEmptyArrayFails = testBuildFailure' {
name = "previousScopeLocalEmptyArrayFails";
drv = check {
name = "previousScopeLocalEmptyArray";
scope = "function";
intro = "local -a";
values = "()";
};
expectedBuilderLogEntries = [
"test failed"
];
};
previousScopeLocalSingletonArrayFails = testBuildFailure' {
name = "previousScopeLocalSingletonArrayFails";
drv = check {
name = "previousScopeLocalSingletonArray";
scope = "function";
intro = "local -a";
values = ''("hello!")'';
};
expectedBuilderLogEntries = [
"test failed"
];
};
previousScopeLocalGlobalUnsetArray = check {
name = "previousScopeLocalGlobalUnsetArray";
scope = "function";
intro = "local -ag";
values = null;
};
previousScopeLocalGlobalEmptyArray = check {
name = "previousScopeLocalGlobalEmptyArray";
scope = "function";
intro = "local -ag";
values = "()";
};
previousScopeLocalGlobalSingletonArray = check {
name = "previousScopeLocalGlobalSingletonArray";
scope = "function";
intro = "local -ag";
values = ''("hello!")'';
};
previousScopeDeclareUnsetArrayFails = testBuildFailure' {
name = "previousScopeDeclareUnsetArrayFails";
drv = check {
name = "previousScopeDeclareUnsetArray";
scope = "function";
intro = "declare -a";
values = null;
};
expectedBuilderLogEntries = [
"test failed"
];
};
previousScopeDeclareEmptyArrayFails = testBuildFailure' {
name = "previousScopeDeclareEmptyArrayFails";
drv = check {
name = "previousScopeDeclareEmptyArray";
scope = "function";
intro = "declare -a";
values = "()";
};
expectedBuilderLogEntries = [
"test failed"
];
};
previousScopeDeclareSingletonArrayFails = testBuildFailure' {
name = "previousScopeDeclareSingletonArrayFails";
drv = check {
name = "previousScopeDeclareSingletonArray";
scope = "function";
intro = "declare -a";
values = ''("hello!")'';
};
expectedBuilderLogEntries = [
"test failed"
];
};
previousScopeDeclareGlobalUnsetArray = check {
name = "previousScopeDeclareGlobalUnsetArray";
scope = "function";
intro = "declare -ag";
values = null;
};
previousScopeDeclareGlobalEmptyArray = check {
name = "previousScopeDeclareGlobalEmptyArray";
scope = "function";
intro = "declare -ag";
values = "()";
};
previousScopeDeclareGlobalSingletonArray = check {
name = "previousScopeDeclareGlobalSingletonArray";
scope = "function";
intro = "declare -ag";
values = ''("hello!")'';
};
}

View File

@@ -0,0 +1,14 @@
# shellcheck shell=bash
# isDeclaredMap
# Tests if inputMapRef refers to a declared, associative array.
#
# Arguments:
# - inputMapRef: a reference to an associative array (not mutated)
#
# Returns 0 if the associative array is declared, 1 otherwise.
isDeclaredMap() {
# NOTE: We must dereference the name ref to get the type of the underlying variable.
# shellcheck disable=SC2034
local -nr inputMapRef="$1" && [[ ${!inputMapRef@a} =~ A ]]
}

View File

@@ -0,0 +1,9 @@
{
callPackages,
makeSetupHook,
}:
makeSetupHook {
name = "isDeclaredMap";
passthru.tests = callPackages ./tests.nix { };
meta.description = "Tests if an associative array is declared";
} ./isDeclaredMap.bash

View File

@@ -0,0 +1,377 @@
# NOTE: Tests related to isDeclaredMap go here.
{
isDeclaredMap,
lib,
runCommand,
testers,
}:
let
inherit (lib.attrsets) recurseIntoAttrs;
inherit (testers) shellcheck shfmt testBuildFailure';
commonArgs = {
__structuredAttrs = true;
strictDeps = true;
preferLocalBuild = true;
nativeBuildInputs = [ isDeclaredMap ];
};
check =
let
mkLine =
intro: values:
"${if intro == null then "" else intro + " "}check${if values == null then "" else "=" + values}";
mkScope =
scope: line:
if scope == null then
line
else if scope == "function" then
''
foo() {
${line}
}
foo
''
else
builtins.throw "Invalid scope: ${scope}";
in
{
name,
scope,
intro,
values,
}:
runCommand name commonArgs ''
set -eu
${mkScope scope (mkLine intro values)}
if isDeclaredMap check; then
nixLog "test passed"
touch "$out"
else
nixErrorLog "test failed"
exit 1
fi
'';
in
recurseIntoAttrs {
shellcheck = shellcheck {
name = "isDeclaredMap";
src = ./isDeclaredMap.bash;
};
shfmt = shfmt {
name = "isDeclaredMap";
src = ./isDeclaredMap.bash;
};
undeclaredFails = testBuildFailure' {
name = "undeclaredFails";
drv = runCommand "undeclared" commonArgs ''
set -eu
if isDeclaredMap undeclared; then
nixLog "test passed"
touch "$out"
else
nixErrorLog "test failed"
exit 1
fi
'';
expectedBuilderLogEntries = [
"test failed"
];
};
arrayFails = testBuildFailure' {
name = "arrayFails";
drv = runCommand "array" commonArgs ''
set -eu
local -a array
if isDeclaredMap array; then
nixLog "test passed"
touch "$out"
else
nixErrorLog "test failed"
exit 1
fi
'';
expectedBuilderLogEntries = [
"test failed"
];
};
emptyStringNamerefFails = testBuildFailure' {
name = "emptyStringNamerefFails";
drv = runCommand "emptyStringNameref" commonArgs ''
set -eu
if isDeclaredMap ""; then
nixLog "test passed"
touch "$out"
else
nixErrorLog "test failed"
exit 1
fi
'';
expectedBuilderLogEntries = [
"local: `': not a valid identifier"
"test failed"
];
};
namerefToEmptyStringFails = testBuildFailure' {
name = "namerefToEmptyStringFails";
drv = check {
name = "namerefToEmptyString";
scope = null;
intro = "local -n";
values = "";
};
expectedBuilderLogEntries = [
"local: `': not a valid identifier"
# The test fails in such a way that it exits immediately, without returning to the else branch.
];
};
sameScopeEmptyStringFails = testBuildFailure' {
name = "sameScopeEmptyStringFails";
drv = check {
name = "sameScopeEmptyString";
scope = null;
intro = null;
values = "";
};
expectedBuilderLogEntries = [
"test failed"
];
};
sameScopeEmptyMapFails = testBuildFailure' {
name = "sameScopeEmptyMapFails";
drv = check {
name = "sameScopeEmptyMap";
scope = null;
intro = null;
values = "()";
};
expectedBuilderLogEntries = [
"test failed"
];
};
# Fails because maps must be declared with the -A flag.
sameScopeSingletonMapFails = testBuildFailure' {
name = "sameScopeSingletonMapFails";
drv = check {
name = "sameScopeSingletonMap";
scope = null;
intro = null;
values = ''([greeting]="hello!")'';
};
expectedBuilderLogEntries = [
"greeting: unbound variable"
];
};
sameScopeLocalUnsetMap = check {
name = "sameScopeLocalUnsetMap";
scope = null;
intro = "local -A";
values = null;
};
sameScopeLocalEmptyMap = check {
name = "sameScopeLocalEmptyMap";
scope = null;
intro = "local -A";
values = "()";
};
sameScopeLocalSingletonMap = check {
name = "sameScopeLocalSingletonMap";
scope = null;
intro = "local -A";
values = ''([greeting]="hello!")'';
};
sameScopeDeclareUnsetMap = check {
name = "sameScopeDeclareUnsetMap";
scope = null;
intro = "declare -A";
values = null;
};
sameScopeDeclareEmptyMap = check {
name = "sameScopeDeclareEmptyMap";
scope = null;
intro = "declare -A";
values = "()";
};
sameScopeDeclareSingletonMap = check {
name = "sameScopeDeclareSingletonMap";
scope = null;
intro = "declare -A";
values = ''([greeting]="hello!")'';
};
previousScopeEmptyStringFails = testBuildFailure' {
name = "previousScopeEmptyStringFails";
drv = check {
name = "previousScopeEmptyString";
scope = "function";
intro = null;
values = "";
};
expectedBuilderLogEntries = [
"test failed"
];
};
# Fails because () is ambiguous and defaults to array rather than associative array.
previousScopeEmptyMapFails = testBuildFailure' {
name = "previousScopeEmptyMapFails";
drv = check {
name = "previousScopeEmptyMap";
scope = "function";
intro = null;
values = "()";
};
expectedBuilderLogEntries = [
"test failed"
];
};
previousScopeSingletonMapFails = testBuildFailure' {
name = "previousScopeSingletonMapFails";
drv = check {
name = "previousScopeSingletonMap";
scope = "function";
intro = null;
values = ''([greeting]="hello!")'';
};
expectedBuilderLogEntries = [
"greeting: unbound variable"
];
};
previousScopeLocalUnsetMapFails = testBuildFailure' {
name = "previousScopeLocalUnsetMapFails";
drv = check {
name = "previousScopeLocalUnsetMap";
scope = "function";
intro = "local -A";
values = null;
};
expectedBuilderLogEntries = [
"test failed"
];
};
previousScopeLocalEmptyMapFails = testBuildFailure' {
name = "previousScopeLocalEmptyMapFails";
drv = check {
name = "previousScopeLocalEmptyMap";
scope = "function";
intro = "local -A";
values = "()";
};
expectedBuilderLogEntries = [
"test failed"
];
};
previousScopeLocalSingletonMapFails = testBuildFailure' {
name = "previousScopeLocalSingletonMapFails";
drv = check {
name = "previousScopeLocalSingletonMap";
scope = "function";
intro = "local -A";
values = ''([greeting]="hello!")'';
};
expectedBuilderLogEntries = [
"test failed"
];
};
previousScopeLocalGlobalUnsetMap = check {
name = "previousScopeLocalGlobalUnsetMap";
scope = "function";
intro = "local -Ag";
values = null;
};
previousScopeLocalGlobalEmptyMap = check {
name = "previousScopeLocalGlobalEmptyMap";
scope = "function";
intro = "local -Ag";
values = "()";
};
previousScopeLocalGlobalSingletonMap = check {
name = "previousScopeLocalGlobalSingletonMap";
scope = "function";
intro = "local -Ag";
values = ''([greeting]="hello!")'';
};
previousScopeDeclareUnsetMapFails = testBuildFailure' {
name = "previousScopeDeclareUnsetMapFails";
drv = check {
name = "previousScopeDeclareUnsetMap";
scope = "function";
intro = "declare -A";
values = null;
};
expectedBuilderLogEntries = [
"test failed"
];
};
previousScopeDeclareEmptyMapFails = testBuildFailure' {
name = "previousScopeDeclareEmptyMapFails";
drv = check {
name = "previousScopeDeclareEmptyMap";
scope = "function";
intro = "declare -A";
values = "()";
};
expectedBuilderLogEntries = [
"test failed"
];
};
previousScopeDeclareSingletonMapFails = testBuildFailure' {
name = "previousScopeDeclareSingletonMapFails";
drv = check {
name = "previousScopeDeclareSingletonMap";
scope = "function";
intro = "declare -A";
values = ''([greeting]="hello!")'';
};
expectedBuilderLogEntries = [
"test failed"
];
};
previousScopeDeclareGlobalUnsetMap = check {
name = "previousScopeDeclareGlobalUnsetMap";
scope = "function";
intro = "declare -Ag";
values = null;
};
previousScopeDeclareGlobalEmptyMap = check {
name = "previousScopeDeclareGlobalEmptyMap";
scope = "function";
intro = "declare -Ag";
values = "()";
};
previousScopeDeclareGlobalSingletonMap = check {
name = "previousScopeDeclareGlobalSingletonMap";
scope = "function";
intro = "declare -Ag";
values = ''([greeting]="hello!")'';
};
}

View File

@@ -0,0 +1,11 @@
{
callPackages,
isDeclaredArray,
makeSetupHook,
}:
makeSetupHook {
name = "sortArray";
propagatedBuildInputs = [ isDeclaredArray ];
passthru.tests = callPackages ./tests.nix { };
meta.description = "Sorts an array";
} ./sortArray.bash

View File

@@ -0,0 +1,53 @@
# shellcheck shell=bash
# sortArray
# Sorts the indexed array referenced by inputArrRef and stores the result in the indexed array referenced by
# outputArrRef.
#
# Arguments:
# - inputArrRef: a reference to an indexed array (not mutated, may alias outputArrRef)
# - outputArrRef: a reference to an indexed array (contents are replaced entirely, may alias inputArrRef)
#
# Returns 0.
sortArray() {
if (($# != 2)); then
nixErrorLog "expected two arguments!"
nixErrorLog "usage: sortArray inputArrRef outputArrRef"
exit 1
fi
local -rn inputArrRef="$1"
local -rn outputArrRef="$2"
if ! isDeclaredArray "${!inputArrRef}"; then
nixErrorLog "first argument inputArrRef must be a reference to an indexed array"
exit 1
elif ! isDeclaredArray "${!outputArrRef}"; then
nixErrorLog "second argument outputArrRef must be a reference to an indexed array"
exit 1
fi
local -a sortedArray=()
# Guard on the length of the input array, as empty array will expand to nothing, but printf will still see it as an
# argument, producing an empty string.
if ((${#inputArrRef[@]} > 0)); then
# NOTE from Bash's printf documentation:
# The format is reused as necessary to consume all of the arguments. If the format requires more arguments than
# are supplied, the extra format specifications behave as if a zero value or null string, as appropriate, had
# been supplied.
# - https://www.gnu.org/software/bash/manual/html_node/Bash-Builtins.html#index-printf
# NOTE from sort manpage:
# If you use a non-POSIX locale (e.g., by setting LC_ALL to 'en_US'), then sort may produce output that is sorted
# differently than you're accustomed to. In that case, set the LC_ALL environment variable to 'C'. Setting only
# LC_COLLATE has two problems. First, it is ineffective if LC_ALL is also set. Second, it has undefined behavior
# if LC_CTYPE (or LANG, if LC_CTYPE is unset) is set to an incompatible value. For example, you get undefined
# behavior if LC_CTYPE is ja_JP.PCK but LC_COLLATE is en_US.UTF-8.
# - https://www.gnu.org/software/coreutils/manual/html_node/sort-invocation.html#FOOT1
mapfile -d $'\0' -t sortedArray < <(printf '%s\0' "${inputArrRef[@]}" | LC_ALL=C sort --stable --zero-terminated)
fi
outputArrRef=("${sortedArray[@]}")
return 0
}

View File

@@ -0,0 +1,178 @@
# NOTE: Tests related to sortArray go here.
{
lib,
sortArray,
testers,
}:
let
inherit (lib.attrsets) recurseIntoAttrs;
inherit (testers) shellcheck shfmt testEqualArrayOrMap;
check =
{
name,
valuesArray,
expectedArray,
}:
(testEqualArrayOrMap {
inherit name valuesArray expectedArray;
script = ''
set -eu
nixLog "running sortArray with valuesArray to populate actualArray"
sortArray valuesArray actualArray
'';
}).overrideAttrs
(prevAttrs: {
nativeBuildInputs = prevAttrs.nativeBuildInputs or [ ] ++ [ sortArray ];
});
checkInPlace =
{
name,
valuesArray,
expectedArray,
}:
(testEqualArrayOrMap {
inherit name valuesArray expectedArray;
script = ''
set -eu
nixLog "running sortArray with valuesArray as input and output"
sortArray valuesArray valuesArray
nixLog "copying valuesArray to actualArray"
actualArray=("''${valuesArray[@]}")
'';
}).overrideAttrs
(prevAttrs: {
nativeBuildInputs = prevAttrs.nativeBuildInputs or [ ] ++ [ sortArray ];
});
in
recurseIntoAttrs {
shellcheck = shellcheck {
name = "sortArray";
src = ./sortArray.bash;
};
shfmt = shfmt {
name = "sortArray";
src = ./sortArray.bash;
};
empty = check {
name = "empty";
valuesArray = [ ];
expectedArray = [ ];
};
singleton = check {
name = "singleton";
valuesArray = [ "apple" ];
expectedArray = [ "apple" ];
};
oneDuplicate = check {
name = "oneDuplicate";
valuesArray = [
"apple"
"apple"
];
expectedArray = [
"apple"
"apple"
];
};
oneUnique = check {
name = "oneUnique";
valuesArray = [
"bee"
"apple"
"bee"
];
expectedArray = [
"apple"
"bee"
"bee"
];
};
duplicatesWithSpacesAndLineBreaks = check {
name = "duplicatesWithSpacesAndLineBreaks";
valuesArray = [
"dog"
"bee"
''
line
break
''
"cat"
"zebra"
"bee"
"cat"
"elephant"
"dog with spaces"
''
line
break
''
];
expectedArray = [
"bee"
"bee"
"cat"
"cat"
"dog"
"dog with spaces"
"elephant"
# NOTE: lead whitespace is removed, so the following entries start with `l`.
''
line
break
''
''
line
break
''
"zebra"
];
};
duplicatesWithSpacesAndLineBreaksInPlace = checkInPlace {
name = "duplicatesWithSpacesAndLineBreaksInPlace";
valuesArray = [
"dog"
"bee"
''
line
break
''
"cat"
"zebra"
"bee"
"cat"
"elephant"
"dog with spaces"
''
line
break
''
];
expectedArray = [
"bee"
"bee"
"cat"
"cat"
"dog"
"dog with spaces"
"elephant"
# NOTE: lead whitespace is removed, so the following entries start with `l`.
''
line
break
''
''
line
break
''
"zebra"
];
};
}

View File

@@ -1,11 +1,5 @@
# shellcheck shell=bash
# Tests if an array is declared.
isDeclaredArray() {
# shellcheck disable=SC2034
local -nr arrayRef="$1" && [[ ${!arrayRef@a} =~ a ]]
}
# Asserts that two arrays are equal, printing out differences if they are not.
# Does not short circuit on the first difference.
assertEqualArray() {
@@ -19,12 +13,12 @@ assertEqualArray() {
local -nr actualArrayRef="$2"
if ! isDeclaredArray "${!expectedArrayRef}"; then
nixErrorLog "first arugment expectedArrayRef must be an array reference to a declared array"
nixErrorLog "first argument expectedArrayRef must be a reference to an indexed array"
exit 1
fi
if ! isDeclaredArray "${!actualArrayRef}"; then
nixErrorLog "second arugment actualArrayRef must be an array reference to a declared array"
nixErrorLog "second argument actualArrayRef must be a reference to an indexed array"
exit 1
fi

View File

@@ -1,11 +1,5 @@
# shellcheck shell=bash
# Tests if a map is declared.
isDeclaredMap() {
# shellcheck disable=SC2034
local -nr mapRef="$1" && [[ ${!mapRef@a} =~ A ]]
}
# Asserts that two maps are equal, printing out differences if they are not.
# Does not short circuit on the first difference.
assertEqualMap() {
@@ -19,26 +13,15 @@ assertEqualMap() {
local -nr actualMapRef="$2"
if ! isDeclaredMap "${!expectedMapRef}"; then
nixErrorLog "first arugment expectedMapRef must be an associative array reference to a declared associative array"
nixErrorLog "first argument expectedMapRef must be a reference to an associative array"
exit 1
fi
if ! isDeclaredMap "${!actualMapRef}"; then
nixErrorLog "second arugment actualMapRef must be an associative array reference to a declared associative array"
nixErrorLog "second argument actualMapRef must be a reference to an associative array"
exit 1
fi
# NOTE:
# From the `sort` manpage: "The locale specified by the environment affects sort order. Set LC_ALL=C to get the
# traditional sort order that uses native byte values."
# We specify the environment variable in a subshell to avoid polluting the caller's environment.
local -a sortedExpectedKeys
mapfile -d '' -t sortedExpectedKeys < <(printf '%s\0' "${!expectedMapRef[@]}" | LC_ALL=C sort --stable --zero-terminated)
local -a sortedActualKeys
mapfile -d '' -t sortedActualKeys < <(printf '%s\0' "${!actualMapRef[@]}" | LC_ALL=C sort --stable --zero-terminated)
local -ir expectedLength=${#expectedMapRef[@]}
local -ir actualLength=${#actualMapRef[@]}
@@ -49,6 +32,12 @@ assertEqualMap() {
hasDiff=1
fi
local -a sortedExpectedKeys=()
getSortedMapKeys "${!expectedMapRef}" sortedExpectedKeys
local -a sortedActualKeys=()
getSortedMapKeys "${!actualMapRef}" sortedActualKeys
local -i expectedKeyIdx=0
local expectedKey
local expectedValue

View File

@@ -1,4 +1,5 @@
{
arrayUtilities,
lib,
stdenvNoCC,
}:
@@ -21,7 +22,10 @@ lib.makeOverridable (
inherit name;
nativeBuildInputs = [
arrayUtilities.isDeclaredArray
./assert-equal-array.sh
arrayUtilities.isDeclaredMap
arrayUtilities.getSortedMapKeys
./assert-equal-map.sh
];

View File

@@ -201,6 +201,16 @@ with pkgs;
auto-patchelf-hook = callPackage ./auto-patchelf-hook { };
# Accumulate all passthru.tests from arrayUtilities into a single attribute set.
arrayUtilities = recurseIntoAttrs (
lib.concatMapAttrs (
name: value:
lib.optionalAttrs (value ? passthru.tests) {
${name} = value.passthru.tests;
}
) arrayUtilities
);
srcOnly = callPackage ../build-support/src-only/tests.nix { };
systemd = callPackage ./systemd { };

View File

@@ -177,6 +177,23 @@ with pkgs;
__flattenIncludeHackHook = callPackage ../build-support/setup-hooks/flatten-include-hack { };
arrayUtilities =
let
arrayUtilitiesPackages = makeScopeWithSplicing' {
otherSplices = generateSplicesForMkScope "arrayUtilities";
f =
finalArrayUtilities:
{
callPackages = lib.callPackagesWith (pkgs // finalArrayUtilities);
}
// lib.packagesFromDirectoryRecursive {
inherit (finalArrayUtilities) callPackage;
directory = ../build-support/setup-hooks/arrayUtilities;
};
};
in
recurseIntoAttrs arrayUtilitiesPackages;
addBinToPathHook = callPackage (
{ makeSetupHook }:
makeSetupHook {