Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ stamp-h1
/tests/acceptance/25_cf-execd/Makefile
/tests/acceptance/Makefile
/tests/load/Makefile
/tests/reactor/Makefile
/tests/unit/Makefile
/tests/static-check/Makefile
/tests/valgrind-check/Makefile
Expand Down
1 change: 1 addition & 0 deletions configure.ac
Original file line number Diff line number Diff line change
Expand Up @@ -2032,6 +2032,7 @@ AC_CONFIG_FILES([Makefile
tests/acceptance/25_cf-execd/Makefile
tests/unit/Makefile
tests/load/Makefile
tests/reactor/Makefile
tests/static-check/Makefile
tests/valgrind-check/Makefile
tests/asan-check/Makefile])
Expand Down
2 changes: 1 addition & 1 deletion tests/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@
# (COSL) may apply to this file if you as a licensee so wish it. See
# included file COSL.txt.
#
SUBDIRS = unit load acceptance static-check valgrind-check asan-check
SUBDIRS = unit load acceptance reactor static-check valgrind-check asan-check
44 changes: 44 additions & 0 deletions tests/reactor/Makefile.am
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#
# Copyright 2026 Northern.tech AS
#
# This file is part of CFEngine 3 - written and maintained by Northern.tech AS.
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation; version 3.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA
#
# To the extent this program is licensed as part of the Enterprise
# versions of CFEngine, the applicable Commercial Open Source License
# (COSL) may apply to this file if you as a licensee so wish it. See
# included file COSL.txt.
#

# The test scripts, with their policy next to them
REACTOR_TEST_SH = $(sort $(wildcard \
$(srcdir)/tests/*/*.sh \
$(srcdir)/tests/*/unsafe/*.sh))
REACTOR_TEST_CF = $(REACTOR_TEST_SH:.sh=.cf)

# cf-reactor is not built with builtin extensions (e.g. on Windows)
if !BUILTIN_EXTENSIONS
TESTS = $(patsubst $(srcdir)/%,%,$(REACTOR_TEST_SH))
endif

# Each test is run by run_test.sh, with the binaries of this build. The ones
# in an unsafe/ directory are skipped unless UNSAFE_TESTS=1.
TESTS_ENVIRONMENT = env \
CF_REACTOR=$(abs_top_builddir)/cf-reactor/cf-reactor \
CF_PROMISES=$(abs_top_builddir)/cf-promises/cf-promises \
$(SHELL) $(srcdir)/run_test.sh

EXTRA_DIST = lib.cf lib.sh run.sh run_test.sh \
$(patsubst $(srcdir)/%,%,$(REACTOR_TEST_SH) $(REACTOR_TEST_CF))
82 changes: 68 additions & 14 deletions tests/reactor/lib.sh
Original file line number Diff line number Diff line change
@@ -1,32 +1,89 @@
# Helpers sourced by the tests. Expects CF_REACTOR and LOG to be exported by
# run.sh.
# run_test.sh.
#
# Only POSIX tools are used, as the tests are run on all the platforms (e.g.
# no timeout(1), grep -m or date +%s).

# Seconds to wait for something before giving up
WAIT_TIMEOUT=60

# number of log lines already consumed by wait_for_log
log_offset=0

# Run command $@ until it succeeds, for at most WAIT_TIMEOUT seconds, polling
# every 0.1 second, or every second where sleep only takes whole seconds.
# Fails if it never succeeded. Runs in a subshell without tracing, so that
# set -x doesn't log every poll.
wait_until() {
(
set +x
if sleep 0.1 2>/dev/null; then
interval=0.1 polls=$((WAIT_TIMEOUT * 10))
else
interval=1 polls=$WAIT_TIMEOUT
fi
until "$@"; do
[ "$polls" -gt 0 ] || exit 1
polls=$((polls - 1))
sleep "$interval"
done
)
}

# Print the current time in seconds since the Epoch. date +%s is not POSIX;
# without it, srand() seeds with the time of day and returns the last seed.
now() {
t=$(date +%s 2>/dev/null)
case "$t" in
''|*[!0-9]*) awk 'BEGIN { srand(); print srand() }' ;;
*) echo "$t" ;;
esac
}

# Print the first line of the reactor log after line $1 that contains $2,
# prefixed with its number counted from line $1. Fails if there is none.
log_match() {
tail -n "+$(($1 + 1))" "$LOG" 2>/dev/null | grep -n -F -- "$2" | head -n 1 | grep .
}

# Fail unless the reactor log contains $1.
log_contains() {
grep -q -F -- "$1" "$LOG" 2>/dev/null
}

# Print the number of lines of file $1, 0 if it doesn't exist.
count_lines() {
if [ -f "$1" ]; then
# without the padding of some wc implementations
echo $(( $(wc -l < "$1") ))
else
echo 0
fi
}

# Fail unless file $1 has at least $2 lines.
has_lines() {
[ "$(count_lines "$1")" -ge "$2" ]
}

# Wait until the reactor log contains $1 in a line after the last match, then
# skip past that line, so that the next wait only sees newer output.
wait_for_log() {
if ! match=$(timeout 60 sh -c '
until tail -n "+$(($2 + 1))" "$1" | grep -n -F -m 1 -- "$3"; do
sleep 0.1
done' _ "$LOG" "$log_offset" "$1")
if ! wait_until log_match "$log_offset" "$1" >/dev/null
then
echo "Timed out waiting for '$1' in reactor log:" >&2
cat "$LOG" >&2
return 1
fi
match=$(log_match "$log_offset" "$1")
log_offset=$((log_offset + ${match%%:*}))
}

# Wait until cf-reactor has registered the watchers of the policy, $1 of them.
# The watcher thread may log before this line, so search the whole log and
# leave the offset of wait_for_log as is.
wait_for_watchers() {
if ! timeout 60 sh -c '
until grep -q -F -- "$2" "$1"; do
sleep 0.1
done' _ "$LOG" "verbose: cf-reactor event watcher has $1 watcher(s)"
if ! wait_until log_contains "verbose: cf-reactor event watcher has $1 watcher(s)"
then
echo "Timed out waiting for $1 watcher(s) in reactor log:" >&2
cat "$LOG" >&2
Expand All @@ -36,17 +93,14 @@ wait_for_watchers() {

# Wait until file $1 has at least $2 lines, then fail if it has more than $2.
wait_for_lines() {
if ! timeout 60 sh -c '
until [ "$(cat "$1" 2>/dev/null | wc -l)" -ge "$2" ]; do
sleep 0.1
done' _ "$1" "$2"
if ! wait_until has_lines "$1" "$2"
then
echo "Timed out waiting for $2 line(s) in '$1', reactor log:" >&2
cat "$LOG" >&2
return 1
fi

lines=$(wc -l < "$1")
lines=$(count_lines "$1")
if [ "$lines" -ne "$2" ]
then
echo "Expected $2 line(s) in '$1', got $lines" >&2
Expand Down
139 changes: 61 additions & 78 deletions tests/reactor/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,96 +2,79 @@

# Usage: run.sh [--unsafe]
#
# Must be run from the tests/reactor directory.
# Runs all the reactor tests, see run_test.sh, and prints a summary of their
# results. The output of a test is only printed if it failed. They are also
# run by "make check".
#
# Tests in an unsafe/ directory modify the system they are running on, so they
# are only run with --unsafe.
# are skipped unless run with --unsafe (or UNSAFE_TESTS=1).
#
# The binaries are taken from $CF_BINDIR if it is set, otherwise from the
# CFEngine build tree this directory is part of if they are built, otherwise
# from /var/cfengine/bin.

fail=0

unsafe=0
if [ "$1" = "--unsafe" ]; then
unsafe=1
UNSAFE_TESTS=1
fi

banner() {
name=" $(basename "$(dirname "$1")")/$(basename "$1") "
width=72
left=$(( (width - ${#name}) / 2 ))
right=$(( width - ${#name} - left ))
[ "$left" -lt 0 ] && left=0
[ "$right" -lt 0 ] && right=0
printf '%s%s%s\n' \
"$(printf "%${left}s" '' | tr ' ' '=')" \
"$name" \
"$(printf "%${right}s" '' | tr ' ' '=')"
}

error() {
echo "test '$1' FAILED" >&2
fail=1
}

# Print the path of binary $1, see the usage above.
find_binary() {
if [ -n "$CF_BINDIR" ]; then
echo "$CF_BINDIR/$1"
elif [ -x "$PWD/../../$1/$1" ]; then
# in-tree build, may be a libtool wrapper script
echo "$PWD/../../$1/$1"
else
echo "/var/cfengine/bin/$1"
fi
export UNSAFE_TESTS

dir=$(cd "$(dirname "$0")" && pwd)

# for now()
. "$dir/lib.sh"

passed=0
failed=0
skipped=0
total=0
failures=""
suite_start=$(now)

# Run test $1 and print its result, with its output if it failed
run_test() {
name=${1#"$dir/tests/"}
name=${name%.sh}
total=$((total + 1))

start=$(now)
output=$(sh "$dir/run_test.sh" "$1" 2>&1)
result=$?
elapsed=$(($(now) - start))

case $result in
0)
passed=$((passed + 1))
echo "--- PASS: $name (${elapsed}s)"
;;
77)
skipped=$((skipped + 1))
echo "--- SKIP: $name (${elapsed}s)"
;;
*)
failed=$((failed + 1))
failures="$failures $name
"
echo "--- FAIL: $name (${elapsed}s)"
echo "$output"
echo "---"
;;
esac
}

CF_REACTOR=$(find_binary cf-reactor)
CF_PROMISES=$(find_binary cf-promises)
export CF_REACTOR

for b in "$CF_REACTOR" "$CF_PROMISES"; do
if [ ! -x "$b" ]; then
echo "'$b' not found, build CFEngine or set CF_BINDIR" >&2
exit 2
fi
done
echo "Using $CF_REACTOR and $CF_PROMISES"

for t in "$PWD"/tests/*/*.sh "$PWD"/tests/*/unsafe/*.sh; do
for t in "$dir"/tests/*/*.sh "$dir"/tests/*/unsafe/*.sh; do
# skip the pattern itself if there are no unsafe tests
[ -f "$t" ] || continue
case "$t" in
*/unsafe/*) [ "$unsafe" = 1 ] || continue ;;
esac

banner "$t"

# create throwaway work dir
WORKDIR=$(mktemp -d)
mkdir -p "$WORKDIR/bin" "$WORKDIR/tmp"
ln -s "$CF_PROMISES" "$WORKDIR/bin/cf-promises"

# Run a copy of the policy, keeping its path relative to lib.cf, as
# CFEngine refuses to read policy files writable by group or others, which
# those of a checkout made with a umask of 002 are
rel=${t#"$PWD"/}
policy="$WORKDIR/policy/${rel%.sh}.cf"
mkdir -p "$(dirname "$policy")"
cp lib.cf "$WORKDIR/policy/"
cp "${t%.sh}.cf" "$policy"
chmod 600 "$WORKDIR/policy/lib.cf" "$policy"

CFENGINE_TEST_OVERRIDE_WORKDIR="$WORKDIR"
LOG="$WORKDIR/reactor.log"
PROMISE_NAME=$(basename "$t" .sh)
export WORKDIR CFENGINE_TEST_OVERRIDE_WORKDIR LOG PROMISE_NAME

sh "$t" "$policy" || error "$t"

rm -rf "$WORKDIR"
run_test "$t"
done

exit $fail
echo ""
echo "=============================="
echo "Test Results: $passed passed, $failed failed, $skipped skipped (total: $total, $(($(now) - suite_start))s)"
if [ "$failed" -gt 0 ]; then
echo ""
echo "Failed tests:"
printf '%s' "$failures"
exit 1
fi
echo "=============================="
echo "All reactor tests passed"
Loading
Loading