Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Harden tests' bash #5754

Merged
merged 1 commit into from
Mar 8, 2023
Merged

Conversation

Ericson2314
Copy link
Member

@Ericson2314 Ericson2314 commented Dec 9, 2021

Motivation

Use set -u and set -o pipefail to catch accidental mistakes and failures more strongly.

  • set -u catches the use of undefined variables
  • set -o pipefail catches failures (like set -e) earlier in the pipeline.

This makes the tests a bit more robust. It is nice to read code not worrying about these spurious success paths (via uncaught) errors undermining the tests. Indeed, I caught some bugs doing this.

Context

There are a few tests where we run a command that should fail, and then search its output to make sure the failure message is one that we expect. Before, since the grep was the last command in the pipeline the exit code of those failing programs was silently ignored. Now with set -o pipefail it won't be, and we have to do something so the expected failure doesn't accidentally fail the test.

expectStderr

To do that we use expect and a new expectStderr to check for the. See the comments on each for why.

grepQuiet

grep -q is replaced with grepQuiet, see the comments on that function for why.

grepInverse

grep -v when we just want the exit code is replaced with grepInverse, see the comments on that function for why.

grepQuietInverse

grep -q -v together is, surprise surprise, replaced with grepQuietInverse, which is both combined.

Checklist for maintainers

Maintainers: tick if completed or explain if not relevant

  • agreed on idea
  • agreed on implementation strategy
  • tests, as appropriate
    • functional tests - tests/**.sh
    • unit tests - src/*/tests
    • integration tests - tests/nixos/*
  • documentation in the manual
  • code and comments are self-explanatory
  • commit message explains why the change was made
  • new feature or incompatible change: updated release notes

@Ericson2314 Ericson2314 changed the title Harden tests set u Harden tests set -u Dec 9, 2021
@Ericson2314 Ericson2314 marked this pull request as draft December 9, 2021 17:39
@Ericson2314 Ericson2314 marked this pull request as ready for review December 9, 2021 19:54
@Ericson2314 Ericson2314 marked this pull request as draft December 9, 2021 20:05
@Ericson2314 Ericson2314 force-pushed the harden-tests-set-u branch 3 times, most recently from fd33ded to 7a9e6e6 Compare December 15, 2021 01:32
@stale
Copy link

stale bot commented Jun 13, 2022

I marked this as stale due to inactivity. → More info

@stale stale bot added the stale label Jun 13, 2022
@stale stale bot removed the stale label Jan 16, 2023
@Ericson2314 Ericson2314 added the with-tests Issues related to testing. PRs with tests have some priority label Jan 16, 2023
@Ericson2314 Ericson2314 marked this pull request as ready for review January 16, 2023 20:10
@Ericson2314 Ericson2314 marked this pull request as draft January 16, 2023 20:51
@Ericson2314 Ericson2314 force-pushed the harden-tests-set-u branch 2 times, most recently from 06bcc77 to 073ee96 Compare February 23, 2023 23:44
@Ericson2314 Ericson2314 changed the title Harden tests set -u Harden tests' bash Feb 23, 2023
@Ericson2314 Ericson2314 marked this pull request as ready for review February 23, 2023 23:58
@roberth
Copy link
Member

roberth commented Feb 24, 2023

Writing tests in "modern bash" will be great.

! cmd || false might look redundant, but it is in fact necessary to make set -e insist that ! cmd does in fact fail (return a non-0 exit status).

I am a bit puzzled by this. How would I reproduce this problem?

$ (set -euo pipefail; ({ ! sh -c 'exit 0'; } | cat; echo $?)); echo $?
1

$ (set -euo pipefail; ({ ! sh -c 'exit 2'; } | cat; echo $?)); echo $?
0
0

If this is related to pipefail, perhaps we could prioritize unset/-u first, although of course I'd prefer the whole thing if feasible.

@Ericson2314
Copy link
Member Author

Ericson2314 commented Feb 24, 2023

It's a set -e thing, unrelated to pipefile. It just came up because pipefail means "more" -set -e, and so now these failing commands which newly have the abilty to abort the test must be dealt with.

Here is a simaple example:

bash -e -c 'true; echo foo'
bash -e -c '! true; echo bar'
bash -e -c '! true || false; echo baz'

prints

foo
bar

@roberth
Copy link
Member

roberth commented Feb 24, 2023

Perhaps we should avoid the ! + errexit/-e interaction altogether.

expectExit() {
  local r=0 expect=$1
  shift
  "$@" || r=$?
  if [[ $r != $expect ]]; then
    echo "Got exit code $r, expecting $expect from: ${*@Q}" 1>&2
    return 1
  fi
}
$ (set -e; ! true; echo bar)
bar

$ (set -e; expectExit 1 true; echo bar)
Got exit code 0, expecting 1 from: 'true'

Copy link
Member

@thufschmitt thufschmitt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome (and it managed to find a couple fishy things, so it proved its worth).

Just one real concern regarding the removal tests/plugins/libplugintest.$(SO_EXT) from the test dependencies, but looks good otherwise

@@ -10,7 +10,8 @@ expect_trace() {
--trace-function-calls \
--expr "$expr" 2>&1 \
| grep "function-trace" \
| sed -e 's/ [0-9]*$//'
| sed -e 's/ [0-9]*$//' \
|| true
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(nit) Since there's a set +e just below, I guess we can just move it a few lines above to make the whole function more consistent

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I have a way to just get rid of all the set +e instead 😀

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does that mean that you want to keep doing more work on this? Or do you mean as a follow-up?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did as part of this PR (small change), but now I have a seemingly unrelated failure I couldn't reproduce just running make installcheck. Frustrating..

tests/local.mk Show resolved Hide resolved
@@ -96,6 +96,8 @@ nix develop -f "$shellDotNix" shellDrv -c echo foo |& grep -q foo
# Test 'nix print-dev-env'.
[[ $(nix print-dev-env -f "$shellDotNix" shellDrv --json | jq -r .variables.arr1.value[2]) = '3 4' ]]

set +u # TODO fix?

source <(nix print-dev-env -f "$shellDotNix" shellDrv)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opened #7951 to track that

@Ericson2314 Ericson2314 force-pushed the harden-tests-set-u branch 2 times, most recently from a239a30 to da2b291 Compare March 3, 2023 17:41
@Ericson2314 Ericson2314 mentioned this pull request Mar 3, 2023
7 tasks
@Ericson2314
Copy link
Member Author

I changed one little thing (in the overall runner) and now this broken pipe error in nix flake registry | grep -q '^global showed up. Anyone have any idea what might be causing that?

@roberth
Copy link
Member

roberth commented Mar 6, 2023

I changed one little thing (in the overall runner) and now this broken pipe error in nix flake registry | grep -q '^global showed up. Anyone have any idea what might be causing that?

Could be grep -q exiting too soon, while nix still needs to write to its stdout pipe whose other end is now dead.

   -q, --quiet, --silent
         Quiet; do not write anything to standard output.  Exit immediately with zero status if any match is found, even if an error was detected.

Maybe use >/dev/null instead of -q?

@Ericson2314 Ericson2314 force-pushed the harden-tests-set-u branch 3 times, most recently from 4cefd4e to e542dad Compare March 6, 2023 15:04
@Ericson2314 Ericson2314 marked this pull request as draft March 6, 2023 15:58
@Ericson2314 Ericson2314 force-pushed the harden-tests-set-u branch 3 times, most recently from 68df860 to 1259169 Compare March 7, 2023 18:36
Comment on lines 173 to 182
# Run a command failing if it didn't exit with the expected exit code.
#
# Has two advantages over the built-in `!`:
#
# 1. `!` conflates all non-0 codes. `expect` allows testing for an exact code.
#
# 2. `!` unexpectedly negates `set -e`, and cannot be used on individual
# pipeline stages with `set -o pipefail`. It only works on the entire pipeline,
# which is useless if we want, say, `nix ...` invocation to *fail*, but a grep
# on the error message it outputs to *succeed*.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doc and explanation

Comment on lines 195 to 196
# Better than just doing `expect ... >&2` becaise the "Expected..." message
# below will *not* be redirected.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doc and explanation

Comment on lines 237 to 242
# grep -v doesn't work well for exit codes. We want `!(exist line l. l
# matches)`. It gives us `exist line l. !(l matches)`.
#
# BTW `!` normally doesn't work well with `set -e`, but when we wrap in a
# function it *does*.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doc and explanation

Comment on lines 246 to 255
# A shorthand `> /dev/null` is a bit noisy.
#
# `grep q` would seem to do this, no function necessary, but it is a bad fit
# with pipes and `pipefail`: `-q` will exit after the first match, and then
# subsequent writes will result in broken pipes.
#
# Note that reproducing the above is a bit tricky as it depends on
# non-deterministic properties such as the timing between the match and the
# closing of the pipe, the buffering of the pipe, and the speed of the producer
# into the pipe. But rest assured we've seen it happen in CI reliably.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doc and explanation

@Ericson2314 Ericson2314 marked this pull request as ready for review March 7, 2023 18:53
grepQuietInverse() {
! grep "$@" > /dev/null
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add tests for the new test support functions?
We'll inevitably want to improve them and it'd be a waste not to reify the requirements that we've figured out for them.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent idea. I've added a test for that @roberth. It felt like putting on a mischievous bash magic show, kinda fun to write!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I figured you'd enjoy taming the shell! 🦁 🪄

tests/test-infra.sh Outdated Show resolved Hide resolved
Use `set -u` and `set -o pipefail` to catch accidental mistakes and
failures more strongly.

 - `set -u` catches the use of undefined variables
 - `set -o pipefail` catches failures (like `set -e`) earlier in the
   pipeline.

This makes the tests a bit more robust. It is nice to read code not
worrying about these spurious success paths (via uncaught) errors
undermining the tests. Indeed, I caught some bugs doing this.

There are a few tests where we run a command that should fail, and then
search its output to make sure the failure message is one that we
expect. Before, since the `grep` was the last command in the pipeline
the exit code of those failing programs was silently ignored. Now with
`set -o pipefail` it won't be, and we have to do something so the
expected failure doesn't accidentally fail the test.

To do that we use `expect` and a new `expectStderr` to check for the
exact failing exit code. See the comments on each for why.

`grep -q` is replaced with `grepQuiet`, see the comments on that
function for why.

`grep -v` when we just want the exit code is replaced with `grepInverse,
see the comments on that function for why.

`grep -q -v` together is, surprise surprise, replaced with
`grepQuietInverse`, which is both combined.

Co-authored-by: Robert Hensing <[email protected]>
Copy link
Member

@thufschmitt thufschmitt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

A bit cumbersome to review, but I like the extra trust gained for the tests!

@thufschmitt thufschmitt merged commit d25322e into NixOS:master Mar 8, 2023
@Ericson2314 Ericson2314 deleted the harden-tests-set-u branch March 8, 2023 21:16
@Ericson2314
Copy link
Member Author

Ericson2314 commented Mar 8, 2023

Thanks @thufschmitt! Yes sorry it grew and grew, but hopefully this helps us catch things faster going forward.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
with-tests Issues related to testing. PRs with tests have some priority
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants