From 589dde029e620164c0cd2b11f9a3182dbee5ecb8 Mon Sep 17 00:00:00 2001 From: Connor Baker Date: Wed, 14 May 2025 22:50:24 +0000 Subject: [PATCH 1/2] arrayUtilities: init Signed-off-by: Connor Baker (cherry picked from commit 38a823246126e6724699e1e5fcc7cf28adc5130a) --- ci/OWNERS | 1 + .../getSortedMapKeys/getSortedMapKeys.bash | 45 +++ .../getSortedMapKeys/package.nix | 17 + .../arrayUtilities/getSortedMapKeys/tests.nix | 80 ++++ .../isDeclaredArray/isDeclaredArray.bash | 14 + .../isDeclaredArray/package.nix | 9 + .../arrayUtilities/isDeclaredArray/tests.nix | 353 ++++++++++++++++ .../isDeclaredMap/isDeclaredMap.bash | 14 + .../arrayUtilities/isDeclaredMap/package.nix | 9 + .../arrayUtilities/isDeclaredMap/tests.nix | 377 ++++++++++++++++++ .../arrayUtilities/sortArray/package.nix | 11 + .../arrayUtilities/sortArray/sortArray.bash | 53 +++ .../arrayUtilities/sortArray/tests.nix | 178 +++++++++ pkgs/test/default.nix | 10 + pkgs/top-level/all-packages.nix | 17 + 15 files changed, 1188 insertions(+) create mode 100644 pkgs/build-support/setup-hooks/arrayUtilities/getSortedMapKeys/getSortedMapKeys.bash create mode 100644 pkgs/build-support/setup-hooks/arrayUtilities/getSortedMapKeys/package.nix create mode 100644 pkgs/build-support/setup-hooks/arrayUtilities/getSortedMapKeys/tests.nix create mode 100644 pkgs/build-support/setup-hooks/arrayUtilities/isDeclaredArray/isDeclaredArray.bash create mode 100644 pkgs/build-support/setup-hooks/arrayUtilities/isDeclaredArray/package.nix create mode 100644 pkgs/build-support/setup-hooks/arrayUtilities/isDeclaredArray/tests.nix create mode 100644 pkgs/build-support/setup-hooks/arrayUtilities/isDeclaredMap/isDeclaredMap.bash create mode 100644 pkgs/build-support/setup-hooks/arrayUtilities/isDeclaredMap/package.nix create mode 100644 pkgs/build-support/setup-hooks/arrayUtilities/isDeclaredMap/tests.nix create mode 100644 pkgs/build-support/setup-hooks/arrayUtilities/sortArray/package.nix create mode 100644 pkgs/build-support/setup-hooks/arrayUtilities/sortArray/sortArray.bash create mode 100644 pkgs/build-support/setup-hooks/arrayUtilities/sortArray/tests.nix diff --git a/ci/OWNERS b/ci/OWNERS index 712d9b8b091c..1b1756a4e705 100644 --- a/ci/OWNERS +++ b/ci/OWNERS @@ -59,6 +59,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 diff --git a/pkgs/build-support/setup-hooks/arrayUtilities/getSortedMapKeys/getSortedMapKeys.bash b/pkgs/build-support/setup-hooks/arrayUtilities/getSortedMapKeys/getSortedMapKeys.bash new file mode 100644 index 000000000000..436d7b0dbd6d --- /dev/null +++ b/pkgs/build-support/setup-hooks/arrayUtilities/getSortedMapKeys/getSortedMapKeys.bash @@ -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 +} diff --git a/pkgs/build-support/setup-hooks/arrayUtilities/getSortedMapKeys/package.nix b/pkgs/build-support/setup-hooks/arrayUtilities/getSortedMapKeys/package.nix new file mode 100644 index 000000000000..0a610db6eeed --- /dev/null +++ b/pkgs/build-support/setup-hooks/arrayUtilities/getSortedMapKeys/package.nix @@ -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 diff --git a/pkgs/build-support/setup-hooks/arrayUtilities/getSortedMapKeys/tests.nix b/pkgs/build-support/setup-hooks/arrayUtilities/getSortedMapKeys/tests.nix new file mode 100644 index 000000000000..3808f6e60cec --- /dev/null +++ b/pkgs/build-support/setup-hooks/arrayUtilities/getSortedMapKeys/tests.nix @@ -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 = [ + " " + " " + ]; + }; +} diff --git a/pkgs/build-support/setup-hooks/arrayUtilities/isDeclaredArray/isDeclaredArray.bash b/pkgs/build-support/setup-hooks/arrayUtilities/isDeclaredArray/isDeclaredArray.bash new file mode 100644 index 000000000000..695bef751429 --- /dev/null +++ b/pkgs/build-support/setup-hooks/arrayUtilities/isDeclaredArray/isDeclaredArray.bash @@ -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 ]] +} diff --git a/pkgs/build-support/setup-hooks/arrayUtilities/isDeclaredArray/package.nix b/pkgs/build-support/setup-hooks/arrayUtilities/isDeclaredArray/package.nix new file mode 100644 index 000000000000..2b8ba3942c98 --- /dev/null +++ b/pkgs/build-support/setup-hooks/arrayUtilities/isDeclaredArray/package.nix @@ -0,0 +1,9 @@ +{ + callPackages, + makeSetupHook, +}: +makeSetupHook { + name = "isDeclaredArray"; + passthru.tests = callPackages ./tests.nix { }; + meta.description = "Tests if an array is declared"; +} ./isDeclaredArray.bash diff --git a/pkgs/build-support/setup-hooks/arrayUtilities/isDeclaredArray/tests.nix b/pkgs/build-support/setup-hooks/arrayUtilities/isDeclaredArray/tests.nix new file mode 100644 index 000000000000..72ea53ce45f8 --- /dev/null +++ b/pkgs/build-support/setup-hooks/arrayUtilities/isDeclaredArray/tests.nix @@ -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!")''; + }; +} diff --git a/pkgs/build-support/setup-hooks/arrayUtilities/isDeclaredMap/isDeclaredMap.bash b/pkgs/build-support/setup-hooks/arrayUtilities/isDeclaredMap/isDeclaredMap.bash new file mode 100644 index 000000000000..47483a2f4c33 --- /dev/null +++ b/pkgs/build-support/setup-hooks/arrayUtilities/isDeclaredMap/isDeclaredMap.bash @@ -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 ]] +} diff --git a/pkgs/build-support/setup-hooks/arrayUtilities/isDeclaredMap/package.nix b/pkgs/build-support/setup-hooks/arrayUtilities/isDeclaredMap/package.nix new file mode 100644 index 000000000000..99d969312dab --- /dev/null +++ b/pkgs/build-support/setup-hooks/arrayUtilities/isDeclaredMap/package.nix @@ -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 diff --git a/pkgs/build-support/setup-hooks/arrayUtilities/isDeclaredMap/tests.nix b/pkgs/build-support/setup-hooks/arrayUtilities/isDeclaredMap/tests.nix new file mode 100644 index 000000000000..ef77291d64c7 --- /dev/null +++ b/pkgs/build-support/setup-hooks/arrayUtilities/isDeclaredMap/tests.nix @@ -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!")''; + }; +} diff --git a/pkgs/build-support/setup-hooks/arrayUtilities/sortArray/package.nix b/pkgs/build-support/setup-hooks/arrayUtilities/sortArray/package.nix new file mode 100644 index 000000000000..5e5c2ced69b0 --- /dev/null +++ b/pkgs/build-support/setup-hooks/arrayUtilities/sortArray/package.nix @@ -0,0 +1,11 @@ +{ + callPackages, + isDeclaredArray, + makeSetupHook, +}: +makeSetupHook { + name = "sortArray"; + propagatedBuildInputs = [ isDeclaredArray ]; + passthru.tests = callPackages ./tests.nix { }; + meta.description = "Sorts an array"; +} ./sortArray.bash diff --git a/pkgs/build-support/setup-hooks/arrayUtilities/sortArray/sortArray.bash b/pkgs/build-support/setup-hooks/arrayUtilities/sortArray/sortArray.bash new file mode 100644 index 000000000000..17e906553dc8 --- /dev/null +++ b/pkgs/build-support/setup-hooks/arrayUtilities/sortArray/sortArray.bash @@ -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 +} diff --git a/pkgs/build-support/setup-hooks/arrayUtilities/sortArray/tests.nix b/pkgs/build-support/setup-hooks/arrayUtilities/sortArray/tests.nix new file mode 100644 index 000000000000..67d5beda1e1f --- /dev/null +++ b/pkgs/build-support/setup-hooks/arrayUtilities/sortArray/tests.nix @@ -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" + ]; + }; +} diff --git a/pkgs/test/default.nix b/pkgs/test/default.nix index dfeeef7fcf4f..f055170a2aea 100644 --- a/pkgs/test/default.nix +++ b/pkgs/test/default.nix @@ -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 { }; diff --git a/pkgs/top-level/all-packages.nix b/pkgs/top-level/all-packages.nix index db81bcfb396e..e31af28f3430 100644 --- a/pkgs/top-level/all-packages.nix +++ b/pkgs/top-level/all-packages.nix @@ -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 { From 1a8bb1187cca6f88472b1e3a25029f90eee004b1 Mon Sep 17 00:00:00 2001 From: Connor Baker Date: Mon, 24 Mar 2025 03:41:54 +0000 Subject: [PATCH 2/2] testers.testEqualArrayOrMap: use arrayUtilities where possible Signed-off-by: Connor Baker (cherry picked from commit 4b80e5995ea038b5cce6d5279a709cfb1366d552) --- .../testEqualArrayOrMap/assert-equal-array.sh | 10 ++----- .../testEqualArrayOrMap/assert-equal-map.sh | 27 ++++++------------- .../testers/testEqualArrayOrMap/default.nix | 4 +++ 3 files changed, 14 insertions(+), 27 deletions(-) diff --git a/pkgs/build-support/testers/testEqualArrayOrMap/assert-equal-array.sh b/pkgs/build-support/testers/testEqualArrayOrMap/assert-equal-array.sh index ef43dedba625..b8c292ffed5e 100644 --- a/pkgs/build-support/testers/testEqualArrayOrMap/assert-equal-array.sh +++ b/pkgs/build-support/testers/testEqualArrayOrMap/assert-equal-array.sh @@ -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 diff --git a/pkgs/build-support/testers/testEqualArrayOrMap/assert-equal-map.sh b/pkgs/build-support/testers/testEqualArrayOrMap/assert-equal-map.sh index b601f9e424e9..1469f94565dd 100644 --- a/pkgs/build-support/testers/testEqualArrayOrMap/assert-equal-map.sh +++ b/pkgs/build-support/testers/testEqualArrayOrMap/assert-equal-map.sh @@ -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 diff --git a/pkgs/build-support/testers/testEqualArrayOrMap/default.nix b/pkgs/build-support/testers/testEqualArrayOrMap/default.nix index 4dc17d0e2b58..31bbc7fe3f12 100644 --- a/pkgs/build-support/testers/testEqualArrayOrMap/default.nix +++ b/pkgs/build-support/testers/testEqualArrayOrMap/default.nix @@ -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 ];