Skip to content

Commit

Permalink
testEqualArrayOrMap: init
Browse files Browse the repository at this point in the history
  • Loading branch information
ConnorBaker committed Feb 19, 2025
1 parent 85fda9a commit deb66f5
Show file tree
Hide file tree
Showing 8 changed files with 614 additions and 0 deletions.
91 changes: 91 additions & 0 deletions doc/build-helpers/testers.chapter.md
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,97 @@ testers.testEqualContents {

:::

## `testEqualArrayOrMap` {#tester-testEqualArrayOrMap}

Check that bash arrays (including associative arrays, referred to as "maps") are populated correctly.

This can be used to ensure setup hooks are registered in a certain order, or write unit tests for shell functions which transform arrays.

:::{.example #ex-testEqualArrayOrMap-test-function-add-cowbell}

# Test a function which appends a value to an array

```nix
testers.testEqualArrayOrMap {
name = "test-function-add-cowbell";
valuesArray = [
"cowbell"
"cowbell"
];
expectedArray = [
"cowbell"
"cowbell"
"cowbell"
];
checkSetupScript = ''
addCowbell() {
local -rn arrayNameRef="$1"
arrayNameRef+=( "cowbell" )
}
nixLog "appending all values in valuesArray to actualArray"
for value in "''${valuesArray[@]}"; do
actualArray+=( "$value" )
done
nixLog "applying addCowbell"
addCowbell actualArray
'';
}
```

:::

### Inputs {#tester-testEqualArrayOrMap-inputs}

NOTE: Internally, this tester uses `__structuredAttrs` to handle marhsalling between Nix expressions and shell variables.
This imposes the restriction that arrays and "maps" have values which are string-coercible.

NOTE: At least one of `expectedArray` and `expectedMap` must be provided.

`name` (string)

: The name of the test.

`checkSetupScript` (string)

: The singular task of `checkSetupScript` is to populate `actualArray` or `actualMap` (it may populate both).
To do this, checkSetupScript may access the following shell variables:

- `valuesArray`
- `valuesMap`
- `actualArray`
- `actualMap`

While both `expectedArray` and `expectedMap` are in scope during the execution of `checkSetupScript`, they *must not* be accessed or modified from within `checkSetupScript`.

`valuesArray` (array of string-like values, optional)

: An array of string-coercible values.
This array may be used within `checkSetupScript`.

`valuesMap` (attribute set of string-like values, optional)

: An attribute set of string-coercible values.
This attribute set may be used within `checkSetupScript`.

`expectedArray` (array of string-like values, optional)

: An array of string-coercible values.
This array *must not* be accessed or modified from within `checkSetupScript`.
When provided, `checkSetupScript` is expected to populate `actualArray`.

`expectedMap` (attribute set of string-like values, optional)

: An attribute set of string-coercible values.
This attribute set *must not* be accessed or modified from within `checkSetupScript`.
When provided, `checkSetupScript` is expected to populate `actualMap`.

### Return value {#tester-testEqualArrayOrMap-return}

The tester produces an empty output and only succeeds when `expectedArray` and `expectedMap` match `actualArray` and `actualMap`, respectively, when non-null.
The build log will contain differences encountered.

## `testEqualDerivation` {#tester-testEqualDerivation}

Checks that two packages produce the exact same build instructions.
Expand Down
12 changes: 12 additions & 0 deletions doc/redirects.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
"ex-build-helpers-extendMkDerivation": [
"index.html#ex-build-helpers-extendMkDerivation"
],
"ex-testEqualArrayOrMap-test-function-add-cowbell": [
"index.html#ex-testEqualArrayOrMap-test-function-add-cowbell"
],
"neovim": [
"index.html#neovim"
],
Expand Down Expand Up @@ -332,6 +335,15 @@
"footnote-stdenv-find-inputs-location.__back.0": [
"index.html#footnote-stdenv-find-inputs-location.__back.0"
],
"tester-testEqualArrayOrMap": [
"index.html#tester-testEqualArrayOrMap"
],
"tester-testEqualArrayOrMap-inputs": [
"index.html#tester-testEqualArrayOrMap-inputs"
],
"tester-testEqualArrayOrMap-return": [
"index.html#tester-testEqualArrayOrMap-return"
],
"variables-specifying-dependencies": [
"index.html#variables-specifying-dependencies"
],
Expand Down
5 changes: 5 additions & 0 deletions pkgs/build-support/testers/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@
fi
'';

# See https://nixos.org/manual/nixpkgs/unstable/#tester-testEqualArrayOrMap
# or doc/build-helpers/testers.chapter.md
# NOTE: Must be `import`-ed rather than `callPackage`-d to preserve the `override` attribute.
testEqualArrayOrMap = import ./testEqualArrayOrMap/tester.nix { inherit lib runCommand; };

# See https://nixos.org/manual/nixpkgs/unstable/#tester-testVersion
# or doc/build-helpers/testers.chapter.md
testVersion =
Expand Down
2 changes: 2 additions & 0 deletions pkgs/build-support/testers/test/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -352,4 +352,6 @@ lib.recurseIntoAttrs {
touch -- "$out"
'';
};

testEqualArrayOrMap = lib.recurseIntoAttrs (pkgs.callPackages ../testEqualArrayOrMap/tests.nix { });
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# shellcheck shell=bash

# Asserts that two arrays are equal, printing out differences if they are not.
# Does not short circuit on the first difference.
assertEqualArray() {
if (($# != 2)); then
nixErrorLog "expected two arguments!"
nixErrorLog "usage: assertEqualArray expectedArrayRef actualArrayRef"
exit 1
fi

local -nr expectedArrayRef="$1"
local -nr actualArrayRef="$2"

if [[ ! ${expectedArrayRef@a} =~ a ]]; then
nixErrorLog "first arugment expectedArrayRef must be an array reference"
exit 1
fi

if [[ ! ${actualArrayRef@a} =~ a ]]; then
nixErrorLog "second arugment actualArrayRef must be an array reference"
exit 1
fi

local -ir expectedLength=${#expectedArrayRef[@]}
local -ir actualLength=${#actualArrayRef[@]}

local -i hasDiff=0

if ((expectedLength != actualLength)); then
nixErrorLog "arrays differ in length: expectedArray has length $expectedLength but actualArray has length $actualLength"
hasDiff=1
fi

local -i idx=0
local expectedValue
local actualValue

# We iterate so long as at least one array has indices we've not considered.
# This means that `idx` is a valid index to *at least one* of the arrays.
for ((idx = 0; idx < expectedLength || idx < actualLength; idx++)); do
# Update values for variables which are still in range/valid.
if ((idx < expectedLength)); then
expectedValue="${expectedArrayRef[idx]}"
fi

if ((idx < actualLength)); then
actualValue="${actualArrayRef[idx]}"
fi

# Handle comparisons.
if ((idx >= expectedLength)); then
nixErrorLog "arrays differ at index $idx: expectedArray has no such index but actualArray has value '$actualValue'"
hasDiff=1
elif ((idx >= actualLength)); then
nixErrorLog "arrays differ at index $idx: expectedArray has value '$expectedValue' but actualArray has no such index"
hasDiff=1
elif [[ $expectedValue != "$actualValue" ]]; then
nixErrorLog "arrays differ at index $idx: expectedArray has value '$expectedValue' but actualArray has value '$actualValue'"
hasDiff=1
fi
done

((hasDiff)) && exit 1 || return 0
}
96 changes: 96 additions & 0 deletions pkgs/build-support/testers/testEqualArrayOrMap/assert-equal-map.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# shellcheck shell=bash

# Asserts that two maps are equal, printing out differences if they are not.
# Does not short circuit on the first difference.
assertEqualMap() {
if (($# != 2)); then
nixErrorLog "expected two arguments!"
nixErrorLog "usage: assertEqualMap expectedMapRef actualMapRef"
exit 1
fi

local -nr expectedMapRef="$1"
local -nr actualMapRef="$2"

if [[ ! ${expectedMapRef@a} =~ A ]]; then
nixErrorLog "first arugment expectedMapRef must be an associative array reference"
exit 1
fi

if [[ ! ${actualMapRef@a} =~ A ]]; then
nixErrorLog "second arugment actualMapRef must be an associative array reference"
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[@]}

local -i hasDiff=0

if ((expectedLength != actualLength)); then
nixErrorLog "maps differ in length: expectedMap has length $expectedLength but actualMap has length $actualLength"
hasDiff=1
fi

local -i expectedKeyIdx=0
local expectedKey
local expectedValue
local -i actualKeyIdx=0
local actualKey
local actualValue

# We iterate so long as at least one map has keys we've not considered.
while ((expectedKeyIdx < expectedLength || actualKeyIdx < actualLength)); do
# Update values for variables which are still in range/valid.
if ((expectedKeyIdx < expectedLength)); then
expectedKey="${sortedExpectedKeys["$expectedKeyIdx"]}"
expectedValue="${expectedMapRef["$expectedKey"]}"
fi

if ((actualKeyIdx < actualLength)); then
actualKey="${sortedActualKeys["$actualKeyIdx"]}"
actualValue="${actualMapRef["$actualKey"]}"
fi

# In the case we've exhausted the keys from expectedMap, or expectedKey comes after actualKey,
# actualMap has an extra key relative to expectedMap.
if ((expectedKeyIdx >= expectedLength)) || [[ $expectedKey > $actualKey ]]; then
nixErrorLog "maps differ at key '$actualKey': expectedMap has no such key but actualMap has value '$actualValue'"
hasDiff=1
actualKeyIdx+=1

# In the case we've exhausted the keys from actualMap, or expectedKey comes before actualKey,
# expectedMap has an extra key relative to actualMap.
elif ((actualKeyIdx >= actualLength)) || [[ $expectedKey < $actualKey ]]; then
nixErrorLog "maps differ at key '$expectedKey': expectedMap has value '$expectedValue' but actualMap has no such key"
hasDiff=1
expectedKeyIdx+=1

# In the case where both maps have keys we have not processed.
elif [[ $expectedKey == "$actualKey" ]]; then
if [[ $expectedValue != "$actualValue" ]]; then
nixErrorLog "maps differ at key '$expectedKey': expectedMap has value '$expectedValue' but actualMap has value '$actualValue'"
hasDiff=1
fi

expectedKeyIdx+=1
actualKeyIdx+=1
else
nixErrorLog "this case should be unreachable"
exit 1
fi
done

((hasDiff)) && exit 1 || return 0
}
Loading

0 comments on commit deb66f5

Please sign in to comment.