diff --git a/cf-agent/files_edit.h b/cf-agent/files_edit.h index eb3a243ca64..91c1d97eb5d 100644 --- a/cf-agent/files_edit.h +++ b/cf-agent/files_edit.h @@ -41,6 +41,11 @@ copy equal to the copied template file - not the copied + edited file. */ +/* The ifelapsed of the promises of edit bundles: CF_EDIT_IFELAPSED, unless the + * default ifelapsed is lower, e.g. 0 for the bundles cf-reactor runs on events, + * which may happen less than a minute apart */ +#define EDIT_IFELAPSED MIN(CF_EDIT_IFELAPSED, VIFELAPSED) + typedef struct { char *filename; diff --git a/cf-agent/files_editline.c b/cf-agent/files_editline.c index 39ecde4e457..ffe76d53047 100644 --- a/cf-agent/files_editline.c +++ b/cf-agent/files_editline.c @@ -373,7 +373,7 @@ static PromiseResult VerifyLineDeletions(EvalContext *ctx, const Promise *pp, Ed char lockname[CF_BUFSIZE]; Attributes a = GetDeletionAttributes(ctx, pp); - a.transaction.ifelapsed = CF_EDIT_IFELAPSED; + a.transaction.ifelapsed = EDIT_IFELAPSED; if (!SanityCheckDeletions(&a, pp)) { @@ -453,7 +453,7 @@ static PromiseResult VerifyColumnEdits(EvalContext *ctx, const Promise *pp, Edit char lockname[CF_BUFSIZE]; Attributes a = GetColumnAttributes(ctx, pp); - a.transaction.ifelapsed = CF_EDIT_IFELAPSED; + a.transaction.ifelapsed = EDIT_IFELAPSED; if (a.column.column_separator == NULL) { @@ -547,7 +547,7 @@ static PromiseResult VerifyPatterns(EvalContext *ctx, const Promise *pp, EditCon /* Are we working in a restricted region? */ Attributes a = GetReplaceAttributes(ctx, pp); - a.transaction.ifelapsed = CF_EDIT_IFELAPSED; + a.transaction.ifelapsed = EDIT_IFELAPSED; if (!a.replace.replace_value) { @@ -733,7 +733,7 @@ static PromiseResult VerifyLineInsertions(EvalContext *ctx, const Promise *pp, E Attributes a = GetInsertionAttributes(ctx, pp); int allow_multi_lines = StringEqual(a.sourcetype, "preserve_all_lines"); - a.transaction.ifelapsed = CF_EDIT_IFELAPSED; + a.transaction.ifelapsed = EDIT_IFELAPSED; if (!SanityCheckInsertions(&a)) { diff --git a/cf-agent/files_editxml.c b/cf-agent/files_editxml.c index 0bc01b5b8a2..545db719396 100644 --- a/cf-agent/files_editxml.c +++ b/cf-agent/files_editxml.c @@ -353,7 +353,7 @@ static bool VerifyXPathBuild(EvalContext *ctx, const Attributes *attr, const Pro CfLock thislock; char lockname[CF_BUFSIZE], rawxpath[CF_BUFSIZE] = { 0 }; - a.transaction.ifelapsed = CF_EDIT_IFELAPSED; + a.transaction.ifelapsed = EDIT_IFELAPSED; int ret = snprintf(rawxpath, sizeof(rawxpath), "%s", a.xml.havebuildxpath ? a.xml.build_xpath : pp->promiser); @@ -417,7 +417,7 @@ static PromiseResult VerifyTreeDeletions(EvalContext *ctx, const Attributes *att CfLock thislock; char lockname[CF_BUFSIZE]; - a.transaction.ifelapsed = CF_EDIT_IFELAPSED; + a.transaction.ifelapsed = EDIT_IFELAPSED; if (!SanityCheckTreeDeletions(&a)) { @@ -497,7 +497,7 @@ static PromiseResult VerifyTreeInsertions(EvalContext *ctx, const Attributes *at CfLock thislock; char lockname[CF_BUFSIZE]; - a.transaction.ifelapsed = CF_EDIT_IFELAPSED; + a.transaction.ifelapsed = EDIT_IFELAPSED; PromiseResult result = PROMISE_RESULT_NOOP; if (!SanityCheckTreeInsertions(&a, edcontext)) @@ -590,7 +590,7 @@ static PromiseResult VerifyAttributeDeletions(EvalContext *ctx, const Attributes CfLock thislock; char lockname[CF_BUFSIZE]; - a.transaction.ifelapsed = CF_EDIT_IFELAPSED; + a.transaction.ifelapsed = EDIT_IFELAPSED; PromiseResult result = PROMISE_RESULT_NOOP; if (!SanityCheckAttributeDeletions(&a)) @@ -673,7 +673,7 @@ static PromiseResult VerifyAttributeSet(EvalContext *ctx, const Attributes *attr CfLock thislock; char lockname[CF_BUFSIZE]; - a.transaction.ifelapsed = CF_EDIT_IFELAPSED; + a.transaction.ifelapsed = EDIT_IFELAPSED; PromiseResult result = PROMISE_RESULT_NOOP; if (!SanityCheckAttributeSet(&a)) @@ -756,7 +756,7 @@ static PromiseResult VerifyTextDeletions(EvalContext *ctx, const Attributes *att CfLock thislock; char lockname[CF_BUFSIZE]; - a.transaction.ifelapsed = CF_EDIT_IFELAPSED; + a.transaction.ifelapsed = EDIT_IFELAPSED; PromiseResult result = PROMISE_RESULT_NOOP; if (!SanityCheckTextDeletions(&a)) @@ -839,7 +839,7 @@ static PromiseResult VerifyTextSet(EvalContext *ctx, const Attributes *attr, con CfLock thislock; char lockname[CF_BUFSIZE]; - a.transaction.ifelapsed = CF_EDIT_IFELAPSED; + a.transaction.ifelapsed = EDIT_IFELAPSED; PromiseResult result = PROMISE_RESULT_NOOP; if (!SanityCheckTextSet(&a)) @@ -922,7 +922,7 @@ static PromiseResult VerifyTextInsertions(EvalContext *ctx, const Attributes *at CfLock thislock; char lockname[CF_BUFSIZE]; - a.transaction.ifelapsed = CF_EDIT_IFELAPSED; + a.transaction.ifelapsed = EDIT_IFELAPSED; PromiseResult result = PROMISE_RESULT_NOOP; if (!SanityCheckTextInsertions(&a)) diff --git a/cf-reactor/reactor_transform.c b/cf-reactor/reactor_transform.c index ce03fc86f0a..d7a9cc971b9 100644 --- a/cf-reactor/reactor_transform.c +++ b/cf-reactor/reactor_transform.c @@ -505,7 +505,25 @@ static PromiseResult KeepEventsPromiseOnEvent(EvalContext *ctx, const Promise *p return PROMISE_RESULT_SKIPPED; } - return RunThenBundles(ctx, pp); + PromiseResult result; + switch (event->check) + { + case WATCHER_CHECK_EVENT: + result = RunThenBundles(ctx, pp); + break; + + case WATCHER_CHECK_ERROR: + /* Already logged as an error by the watcher, see HandleQueuedEvents() */ + result = PROMISE_RESULT_FAIL; + break; + + default: + ProgrammingError("Unexpected watcher check result %d for reactor events promise '%s'", + (int) event->check, pp->promiser); + } + + RecordEventsPromiseOutcome(ctx, pp, result); + return result; } void HandleReactorEvent(EvalContext *ctx, const Promise *pp, const char *promise_hash, WatcherCheckResult check) diff --git a/cf-reactor/watcher.c b/cf-reactor/watcher.c index c72bd8ffb9c..9d42156cd31 100644 --- a/cf-reactor/watcher.c +++ b/cf-reactor/watcher.c @@ -299,7 +299,9 @@ bool EventWatcherInitialize(int *fd) *fd = WakeupChannelReadFd(&wakeup_channel); - Log(LOG_LEVEL_VERBOSE, "Started cf-reactor event watcher with %zu watcher(s)", MapSize(watchers)); + /* No watchers yet, they are registered from the policy afterwards and + * counted by EventWatcherWakeUp() */ + Log(LOG_LEVEL_VERBOSE, "Started cf-reactor event watcher"); return true; } @@ -395,6 +397,12 @@ void EventWatcherResume(void) void EventWatcherWakeUp(void) { + /* Called once the watchers of the (re-)read policy are registered */ + ThreadLock(&watchers_mutex); + const size_t count = MapSize(watchers); + ThreadUnlock(&watchers_mutex); + Log(LOG_LEVEL_VERBOSE, "cf-reactor event watcher has %zu watcher(s)", count); + if (watcher_thread != NULL) { StoppableThreadWake(watcher_thread); diff --git a/cf-reactor/watcher.h b/cf-reactor/watcher.h index 60134096091..c23768a7a80 100644 --- a/cf-reactor/watcher.h +++ b/cf-reactor/watcher.h @@ -132,7 +132,8 @@ void EventWatcherResume(void); * immediately. Without watchers, the watcher thread sleeps until woken up, so * this must be called after registering watchers outside of an * EventWatcherPause()/EventWatcherResume() pair (which wakes it up itself), - * i.e. after the watchers of the initial policy. + * i.e. after the watchers of the initial policy. Logs the number of + * registered watchers. */ void EventWatcherWakeUp(void); void EventWatcherFinalize(void); diff --git a/libpromises/cf3parse_logic.h b/libpromises/cf3parse_logic.h index 550d5e4cf44..56a71005f24 100644 --- a/libpromises/cf3parse_logic.h +++ b/libpromises/cf3parse_logic.h @@ -230,11 +230,13 @@ static bool RelevantBundle(const char *agent, const char *blocktype) return true; } - /* Here are some additional bundle types handled by cfAgent */ + /* Here are some additional bundle types handled by cfAgent, and by + * cf-reactor for the agent bundles it runs */ Item *ip = SplitString("edit_line,edit_xml", ','); - if (strcmp(agent, CF_AGENTTYPES[AGENT_TYPE_AGENT]) == 0) + if (StringEqual(agent, CF_AGENTTYPES[AGENT_TYPE_AGENT]) || + StringEqual(agent, CF_AGENTTYPES[AGENT_TYPE_REACTOR])) { if (IsItemIn(ip, blocktype)) { diff --git a/tests/reactor/lib.cf b/tests/reactor/lib.cf index d1f11667a5a..e20aedc265c 100644 --- a/tests/reactor/lib.cf +++ b/tests/reactor/lib.cf @@ -5,3 +5,28 @@ bundle agent record_event(file, message) with => strftime("localtime", "%Y-%m-%d %H:%M:%S", now()), report_to_file => "$(file)"; } + +# Defines a class for the outcome of the promise it is used on: +# _kept, _repaired or _failed +body classes then_outcome(prefix) +{ + promise_kept => { "$(prefix)_kept" }; + promise_repaired => { "$(prefix)_repaired" }; + repair_failed => { "$(prefix)_failed" }; + repair_denied => { "$(prefix)_failed" }; + repair_timeout => { "$(prefix)_failed" }; +} + +# Records " " for the outcome classes of then_outcome(prefix) +# defined so far. Nothing is recorded if the promise was not evaluated (e.g. +# skipped because of a lock). +bundle agent record_then_outcome(file, prefix) +{ + vars: + "outcomes" slist => { "kept", "repaired", "failed" }; + + methods: + "$(prefix)_$(outcomes)" + if => "$(prefix)_$(outcomes)", + usebundle => record_event("$(file)", "$(prefix) $(outcomes)"); +} diff --git a/tests/reactor/lib.sh b/tests/reactor/lib.sh index 6b24ef43038..559367df808 100644 --- a/tests/reactor/lib.sh +++ b/tests/reactor/lib.sh @@ -19,6 +19,21 @@ wait_for_log() { 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)" + then + echo "Timed out waiting for $1 watcher(s) in reactor log:" >&2 + cat "$LOG" >&2 + return 1 + fi +} + # 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 ' @@ -39,6 +54,18 @@ wait_for_lines() { fi } +# Fail unless line $2 of file $1, without the timestamp written by +# record_event, is $3. +check_event() { + line=$(sed -n "$2p" "$1" | cut -d' ' -f3-) + if [ "$line" != "$3" ] + then + echo "Expected line $2 of '$1' to be '$3', got '$line':" >&2 + cat "$1" >&2 + return 1 + fi +} + # Start cf-reactor in the background with policy file $1, logging to $LOG. # It is stopped when the test exits. start_reactor() { diff --git a/tests/reactor/run.sh b/tests/reactor/run.sh index e6acc659a3d..324a04acc61 100644 --- a/tests/reactor/run.sh +++ b/tests/reactor/run.sh @@ -3,7 +3,7 @@ fail=0 banner() { - name=" $(basename "$1") " + name=" $(basename "$(dirname "$1")")/$(basename "$1") " width=72 left=$(( (width - ${#name}) / 2 )) right=$(( width - ${#name} - left )) @@ -23,7 +23,7 @@ error() { CF_BINDIR=${CF_BINDIR:-/var/cfengine/bin} export CF_BINDIR -for t in "$PWD"/tests/*.sh; do +for t in "$PWD"/tests/*/*.sh; do banner "$t" # create throwaway work dir diff --git a/tests/reactor/tests/00_basics/001_vars.cf b/tests/reactor/tests/00_basics/001_vars.cf new file mode 100644 index 00000000000..e585cb84d03 --- /dev/null +++ b/tests/reactor/tests/00_basics/001_vars.cf @@ -0,0 +1,29 @@ +####################################################### +# +# Test vars promises in reactor bundle +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +bundle reactor test +{ + vars: + "test" string => getenv("TESTFILE", 999); + "events_file" string => getenv("EVENTS_FILE", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + events: + "$(promise_name)" + when => deleted("$(test)"), + then => record_event("$(events_file)", "$(test) was deleted"); +} diff --git a/tests/reactor/tests/001_vars.sh b/tests/reactor/tests/00_basics/001_vars.sh similarity index 93% rename from tests/reactor/tests/001_vars.sh rename to tests/reactor/tests/00_basics/001_vars.sh index 588e951218f..5fcd0838bd4 100644 --- a/tests/reactor/tests/001_vars.sh +++ b/tests/reactor/tests/00_basics/001_vars.sh @@ -2,7 +2,7 @@ set -ex -. "$(dirname "$0")/../lib.sh" +. "$(dirname "$0")/../../lib.sh" TESTFILE="$WORKDIR/tmp/test" EVENTS_FILE="$WORKDIR/tmp/events" diff --git a/tests/reactor/tests/00_basics/002_classes.cf b/tests/reactor/tests/00_basics/002_classes.cf new file mode 100644 index 00000000000..20bd6119d7d --- /dev/null +++ b/tests/reactor/tests/00_basics/002_classes.cf @@ -0,0 +1,38 @@ +####################################################### +# +# Test classes promises in reactor bundle +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +bundle reactor test +{ + vars: + "test" string => getenv("TESTFILE", 999); + "events_file" string => getenv("EVENTS_FILE", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + classes: + "true" expression => "true"; + + events: + true:: + "$(promise_name)" + when => deleted("$(test)"), + then => record_event("$(events_file)", "$(test) was deleted"); + + false:: + "$(promise_name)_2" + when => deleted("$(test)"), + then => record_event("$(events_file)", "$(test) was deleted"); +} diff --git a/tests/reactor/tests/00_basics/002_classes.sh b/tests/reactor/tests/00_basics/002_classes.sh new file mode 100644 index 00000000000..cf026fcb9af --- /dev/null +++ b/tests/reactor/tests/00_basics/002_classes.sh @@ -0,0 +1,26 @@ +#!/bin/sh + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +TESTFILE="$WORKDIR/tmp/test" +EVENTS_FILE="$WORKDIR/tmp/events" +export TESTFILE EVENTS_FILE + +# create initial file +touch "$TESTFILE" + +start_reactor "$1" + +wait_for_watchers 1 + +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 1 + +# recreate the file and wait for the watcher to see it before deleting it again +touch "$TESTFILE" +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 2 diff --git a/tests/reactor/tests/00_basics/003_meta.cf b/tests/reactor/tests/00_basics/003_meta.cf new file mode 100644 index 00000000000..0d8d966382c --- /dev/null +++ b/tests/reactor/tests/00_basics/003_meta.cf @@ -0,0 +1,33 @@ +####################################################### +# +# Test meta promises in reactor bundle +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +bundle reactor test +{ + meta: + "test" string => getenv("TESTFILE", 999); + "events_file" string => getenv("EVENTS_FILE", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + events: + # only the meta variables, in the test_meta scope, so that this would + # fail if meta promises were evaluated like vars promises + "$(test_meta.promise_name)" + when => deleted("$(test_meta.test)"), + then => record_event( + "$(test_meta.events_file)", "$(test_meta.test) was deleted" + ); +} diff --git a/tests/reactor/tests/00_basics/003_meta.sh b/tests/reactor/tests/00_basics/003_meta.sh new file mode 100644 index 00000000000..62f384bc8b4 --- /dev/null +++ b/tests/reactor/tests/00_basics/003_meta.sh @@ -0,0 +1,23 @@ +#!/bin/sh + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +TESTFILE="$WORKDIR/tmp/test" +EVENTS_FILE="$WORKDIR/tmp/events" +export TESTFILE EVENTS_FILE + +# create initial file +touch "$TESTFILE" + +start_reactor "$1" + +wait_for_watchers 1 + +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 1 + +# the meta variables also expanded in the arguments of the "then" bundle +grep -F "$TESTFILE was deleted" "$EVENTS_FILE" diff --git a/tests/reactor/tests/00_basics/004_multiple_reactor_bundles.cf b/tests/reactor/tests/00_basics/004_multiple_reactor_bundles.cf new file mode 100644 index 00000000000..7e2fef79e9e --- /dev/null +++ b/tests/reactor/tests/00_basics/004_multiple_reactor_bundles.cf @@ -0,0 +1,32 @@ +####################################################### +# +# Test that every reactor bundle is evaluated +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "init", "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +bundle reactor init +{ + vars: + "test" string => getenv("TESTFILE", 999); + "events_file" string => getenv("EVENTS_FILE", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); +} + +bundle reactor test +{ + events: + "$(init.promise_name)" + when => deleted("$(init.test)"), + then => record_event("$(init.events_file)", "$(init.test) was deleted"); +} diff --git a/tests/reactor/tests/00_basics/004_multiple_reactor_bundles.sh b/tests/reactor/tests/00_basics/004_multiple_reactor_bundles.sh new file mode 100644 index 00000000000..5fcd0838bd4 --- /dev/null +++ b/tests/reactor/tests/00_basics/004_multiple_reactor_bundles.sh @@ -0,0 +1,24 @@ +#!/bin/sh + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +TESTFILE="$WORKDIR/tmp/test" +EVENTS_FILE="$WORKDIR/tmp/events" +export TESTFILE EVENTS_FILE + +# create initial file +touch "$TESTFILE" + +start_reactor "$1" + +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 1 + +# recreate the file and wait for the watcher to see it before deleting it again +touch "$TESTFILE" +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 2 diff --git a/tests/reactor/tests/00_basics/005_multiple_events.cf b/tests/reactor/tests/00_basics/005_multiple_events.cf new file mode 100644 index 00000000000..39dc6fefabc --- /dev/null +++ b/tests/reactor/tests/00_basics/005_multiple_events.cf @@ -0,0 +1,34 @@ +####################################################### +# +# Test several events promise in one reactor bundle +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +bundle reactor test +{ + vars: + "test_A" string => getenv("TESTFILE_A", 999); + "test_B" string => getenv("TESTFILE_B", 999); + "events_file" string => getenv("EVENTS_FILE", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + events: + "$(promise_name)_A" + when => deleted("$(test_A)"), + then => record_event("$(events_file)", "$(test_A) was deleted"); + + "$(promise_name)_B" + when => deleted("$(test_B)"), + then => record_event("$(events_file)", "$(test_B) was deleted"); +} diff --git a/tests/reactor/tests/00_basics/005_multiple_events.sh b/tests/reactor/tests/00_basics/005_multiple_events.sh new file mode 100644 index 00000000000..48527b53d24 --- /dev/null +++ b/tests/reactor/tests/00_basics/005_multiple_events.sh @@ -0,0 +1,32 @@ +#!/bin/sh + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +TESTFILE_A="$WORKDIR/tmp/test_A" +TESTFILE_B="$WORKDIR/tmp/test_B" + +EVENTS_FILE="$WORKDIR/tmp/events" +export TESTFILE_A TESTFILE_B EVENTS_FILE + +# create initial file +touch "$TESTFILE_A" +touch "$TESTFILE_B" + +start_reactor "$1" + +wait_for_watchers 2 + +wait_for_log "File '$TESTFILE_A' is present, watching for its deletion" +rm "$TESTFILE_A" +rm "$TESTFILE_B" +wait_for_lines "$EVENTS_FILE" 2 + +# recreate the file and wait for the watcher to see it before deleting it again +touch "$TESTFILE_A" +touch "$TESTFILE_B" +wait_for_log "File '$TESTFILE_A' is present, watching for its deletion" +rm "$TESTFILE_A" +rm "$TESTFILE_B" +wait_for_lines "$EVENTS_FILE" 2 diff --git a/tests/reactor/tests/00_basics/006_identical_events.cf b/tests/reactor/tests/00_basics/006_identical_events.cf new file mode 100644 index 00000000000..fecf75378cc --- /dev/null +++ b/tests/reactor/tests/00_basics/006_identical_events.cf @@ -0,0 +1,34 @@ +####################################################### +# +# Test duplicate event promises +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +bundle reactor test +{ + vars: + "test" string => getenv("TESTFILE", 999); + "events_file" string => getenv("EVENTS_FILE", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + events: + "$(promise_name)" + when => deleted("$(test)"), + then => record_event("$(events_file)", "$(test) was deleted"); + + events: + "$(promise_name)" + when => deleted("$(test)"), + then => record_event("$(events_file)", "$(test) was deleted"); +} diff --git a/tests/reactor/tests/00_basics/006_identical_events.sh b/tests/reactor/tests/00_basics/006_identical_events.sh new file mode 100644 index 00000000000..cf026fcb9af --- /dev/null +++ b/tests/reactor/tests/00_basics/006_identical_events.sh @@ -0,0 +1,26 @@ +#!/bin/sh + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +TESTFILE="$WORKDIR/tmp/test" +EVENTS_FILE="$WORKDIR/tmp/events" +export TESTFILE EVENTS_FILE + +# create initial file +touch "$TESTFILE" + +start_reactor "$1" + +wait_for_watchers 1 + +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 1 + +# recreate the file and wait for the watcher to see it before deleting it again +touch "$TESTFILE" +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 2 diff --git a/tests/reactor/tests/00_basics/007_promiser_list.cf b/tests/reactor/tests/00_basics/007_promiser_list.cf new file mode 100644 index 00000000000..f7b5d3e1791 --- /dev/null +++ b/tests/reactor/tests/00_basics/007_promiser_list.cf @@ -0,0 +1,30 @@ +####################################################### +# +# Test promiser list expansion +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +bundle reactor test +{ + vars: + "suffix" slist => { "A", "B" }; + "test_$(suffix)" string => getenv("TESTFILE_$(suffix)", 999); + "events_file" string => getenv("EVENTS_FILE", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + events: + "$(promise_name)_$(suffix)" + when => deleted("$(test_$(suffix))"), + then => record_event("$(events_file)", "$(test_$(suffix)) was deleted"); +} diff --git a/tests/reactor/tests/00_basics/007_promiser_list.sh b/tests/reactor/tests/00_basics/007_promiser_list.sh new file mode 100644 index 00000000000..48527b53d24 --- /dev/null +++ b/tests/reactor/tests/00_basics/007_promiser_list.sh @@ -0,0 +1,32 @@ +#!/bin/sh + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +TESTFILE_A="$WORKDIR/tmp/test_A" +TESTFILE_B="$WORKDIR/tmp/test_B" + +EVENTS_FILE="$WORKDIR/tmp/events" +export TESTFILE_A TESTFILE_B EVENTS_FILE + +# create initial file +touch "$TESTFILE_A" +touch "$TESTFILE_B" + +start_reactor "$1" + +wait_for_watchers 2 + +wait_for_log "File '$TESTFILE_A' is present, watching for its deletion" +rm "$TESTFILE_A" +rm "$TESTFILE_B" +wait_for_lines "$EVENTS_FILE" 2 + +# recreate the file and wait for the watcher to see it before deleting it again +touch "$TESTFILE_A" +touch "$TESTFILE_B" +wait_for_log "File '$TESTFILE_A' is present, watching for its deletion" +rm "$TESTFILE_A" +rm "$TESTFILE_B" +wait_for_lines "$EVENTS_FILE" 2 diff --git a/tests/reactor/tests/00_basics/008_inherit_from.cf b/tests/reactor/tests/00_basics/008_inherit_from.cf new file mode 100644 index 00000000000..663d398bd08 --- /dev/null +++ b/tests/reactor/tests/00_basics/008_inherit_from.cf @@ -0,0 +1,34 @@ +####################################################### +# +# Test inherit_from +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when parent(file) +{ + file_deleted => "$(file)"; +} + +body when deleted(file) +{ + inherit_from => parent("$(file)"); +} + +bundle reactor test +{ + vars: + "test" string => getenv("TESTFILE", 999); + "events_file" string => getenv("EVENTS_FILE", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + events: + "$(promise_name)" + when => deleted("$(test)"), + then => record_event("$(events_file)", "$(test) was deleted"); +} diff --git a/tests/reactor/tests/00_basics/008_inherit_from.sh b/tests/reactor/tests/00_basics/008_inherit_from.sh new file mode 100644 index 00000000000..5fcd0838bd4 --- /dev/null +++ b/tests/reactor/tests/00_basics/008_inherit_from.sh @@ -0,0 +1,24 @@ +#!/bin/sh + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +TESTFILE="$WORKDIR/tmp/test" +EVENTS_FILE="$WORKDIR/tmp/events" +export TESTFILE EVENTS_FILE + +# create initial file +touch "$TESTFILE" + +start_reactor "$1" + +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 1 + +# recreate the file and wait for the watcher to see it before deleting it again +touch "$TESTFILE" +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 2 diff --git a/tests/reactor/tests/001_vars.cf b/tests/reactor/tests/00_basics/009_interval.cf similarity index 72% rename from tests/reactor/tests/001_vars.cf rename to tests/reactor/tests/00_basics/009_interval.cf index 43d1400ca09..008cf55c893 100644 --- a/tests/reactor/tests/001_vars.cf +++ b/tests/reactor/tests/00_basics/009_interval.cf @@ -1,6 +1,11 @@ +####################################################### +# +# Test interval +# +####################################################### body common control { - inputs => { "../lib.cf" }; + inputs => { "../../lib.cf" }; bundlesequence => { "test" }; version => "1.0"; } @@ -8,6 +13,7 @@ body common control body when deleted(file) { file_deleted => "$(file)"; + interval => 10; } bundle reactor test diff --git a/tests/reactor/tests/00_basics/009_interval.sh b/tests/reactor/tests/00_basics/009_interval.sh new file mode 100644 index 00000000000..7768a69e1db --- /dev/null +++ b/tests/reactor/tests/00_basics/009_interval.sh @@ -0,0 +1,33 @@ +#!/bin/sh + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +TESTFILE="$WORKDIR/tmp/test" +EVENTS_FILE="$WORKDIR/tmp/events" +export TESTFILE EVENTS_FILE + +# create initial file +touch "$TESTFILE" + +start_reactor "$1" + +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 1 +t1=$(date +%s) + +# recreate the file and wait for the watcher to see it before deleting it again +touch "$TESTFILE" +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 2 +t2=$(date +%s) + +# checking that the interval is at minimum 10 seconds +delta=$((t2 - t1)) +if [ "$delta" -lt 10 ]; then + echo "FAIL: expected >= 10s, got ${delta}s" >&2 + exit 1 +fi diff --git a/tests/reactor/tests/00_basics/010_then_bundle_list.cf b/tests/reactor/tests/00_basics/010_then_bundle_list.cf new file mode 100644 index 00000000000..93ba04c0af4 --- /dev/null +++ b/tests/reactor/tests/00_basics/010_then_bundle_list.cf @@ -0,0 +1,33 @@ +####################################################### +# +# Test an array of bundles to run on event +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +bundle reactor test +{ + vars: + "test" string => getenv("TESTFILE", 999); + "events_file" string => getenv("EVENTS_FILE", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + events: + "$(promise_name)" + when => deleted("$(test)"), + then => { + record_event("$(events_file)", "$(test) first"), + record_event("$(events_file)", "$(test) second"), + record_event("$(events_file)", "$(test) third"), + }; +} diff --git a/tests/reactor/tests/00_basics/010_then_bundle_list.sh b/tests/reactor/tests/00_basics/010_then_bundle_list.sh new file mode 100644 index 00000000000..58d2ac0599e --- /dev/null +++ b/tests/reactor/tests/00_basics/010_then_bundle_list.sh @@ -0,0 +1,24 @@ +#!/bin/sh + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +TESTFILE="$WORKDIR/tmp/test" +EVENTS_FILE="$WORKDIR/tmp/events" +export TESTFILE EVENTS_FILE + +# create initial file +touch "$TESTFILE" + +start_reactor "$1" + +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 3 + +# recreate the file and wait for the watcher to see it before deleting it again +touch "$TESTFILE" +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 6 diff --git a/tests/reactor/tests/00_basics/011_then_common_bundle.cf b/tests/reactor/tests/00_basics/011_then_common_bundle.cf new file mode 100644 index 00000000000..54ea8c624d7 --- /dev/null +++ b/tests/reactor/tests/00_basics/011_then_common_bundle.cf @@ -0,0 +1,48 @@ +####################################################### +# +# Test common bundle +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +bundle common setup(f, m) +{ + vars: + "file" string => "$(f)"; + "msg" string => "$(m)"; + + classes: + "should_run" expression => "true"; +} + +bundle agent run_if(b) +{ + methods: + should_run:: + "any" usebundle => $(b)("$(setup.file)", "$(setup.msg)"); +} + +bundle reactor test +{ + vars: + "test" string => getenv("TESTFILE", 999); + "events_file" string => getenv("EVENTS_FILE", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + events: + "$(promise_name)" + when => deleted("$(test)"), + then => { + setup("$(events_file)", "$(test) was deleted"), run_if("record_event") + }; +} diff --git a/tests/reactor/tests/00_basics/011_then_common_bundle.sh b/tests/reactor/tests/00_basics/011_then_common_bundle.sh new file mode 100644 index 00000000000..5fcd0838bd4 --- /dev/null +++ b/tests/reactor/tests/00_basics/011_then_common_bundle.sh @@ -0,0 +1,24 @@ +#!/bin/sh + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +TESTFILE="$WORKDIR/tmp/test" +EVENTS_FILE="$WORKDIR/tmp/events" +export TESTFILE EVENTS_FILE + +# create initial file +touch "$TESTFILE" + +start_reactor "$1" + +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 1 + +# recreate the file and wait for the watcher to see it before deleting it again +touch "$TESTFILE" +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 2 diff --git a/tests/reactor/tests/00_basics/012_then_namespaced_bundle.cf b/tests/reactor/tests/00_basics/012_then_namespaced_bundle.cf new file mode 100644 index 00000000000..e26cc2e5502 --- /dev/null +++ b/tests/reactor/tests/00_basics/012_then_namespaced_bundle.cf @@ -0,0 +1,57 @@ +####################################################### +# +# Test bundles in another namespace in "then" +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +bundle reactor test +{ + vars: + "test" string => getenv("TESTFILE", 999); + "events_file" string => getenv("EVENTS_FILE", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + events: + "$(promise_name)" + when => deleted("$(test)"), + then => { + "test_ns:record_without_args", + test_ns:record_with_args( + "$(events_file)", "test_ns:record_with_args ran" + ), + }; +} + +body file control +{ + namespace => "test_ns"; +} + +bundle agent record_without_args +{ + vars: + "events_file" string => getenv("EVENTS_FILE", 999); + + methods: + "any" + usebundle => default:record_event( + "$(events_file)", "test_ns:record_without_args ran" + ); +} + +bundle agent record_with_args(file, msg) +{ + methods: + "any" usebundle => default:record_event("$(file)", "$(msg)"); +} diff --git a/tests/reactor/tests/00_basics/012_then_namespaced_bundle.sh b/tests/reactor/tests/00_basics/012_then_namespaced_bundle.sh new file mode 100644 index 00000000000..c909241c803 --- /dev/null +++ b/tests/reactor/tests/00_basics/012_then_namespaced_bundle.sh @@ -0,0 +1,27 @@ +#!/bin/sh + +# check that a bundle in another namespace can be run in "then", by its name +# (then => "ns:my_bundle") and called with arguments (then => ns:my_bundle(...)) + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +TESTFILE="$WORKDIR/tmp/test" +EVENTS_FILE="$WORKDIR/tmp/events" +export TESTFILE EVENTS_FILE + +# create initial file +touch "$TESTFILE" + +start_reactor "$1" + +wait_for_watchers 1 + +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 2 + +# both bundles ran, in the order of the "then" list +sed -n 1p "$EVENTS_FILE" | grep -F "test_ns:record_without_args ran" +sed -n 2p "$EVENTS_FILE" | grep -F "test_ns:record_with_args ran" diff --git a/tests/reactor/tests/00_basics/013_then_state_reset.cf b/tests/reactor/tests/00_basics/013_then_state_reset.cf new file mode 100644 index 00000000000..cf9af7c3adb --- /dev/null +++ b/tests/reactor/tests/00_basics/013_then_state_reset.cf @@ -0,0 +1,58 @@ +####################################################### +# +# Test that classes and vars are reset between "then" bundle runs +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +bundle agent run_A(b, file, msg, marker) +{ + classes: + "marker" expression => fileexists("$(marker)"); + + methods: + !marker:: + "any" usebundle => $(b)("$(file)", "$(msg)"); +} + +bundle agent run_B(b, file) +{ + vars: + "date_now" int => now(); + + methods: + "any" usebundle => $(b)("$(file)", "$(date_now)"); +} + +bundle reactor test +{ + vars: + "test" string => getenv("TESTFILE", 999); + "events_file_A" string => getenv("EVENTS_FILE_A", 999); + "events_file_B" string => getenv("EVENTS_FILE_B", 999); + "marker" string => getenv("MARKER_FILE", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + events: + "$(promise_name)" + when => deleted("$(test)"), + then => { + run_A( + "record_event", + "$(events_file_A)", + "$(test) was deleted", + "$(marker)" + ), + run_B("record_event", "$(events_file_B)"), + }; +} diff --git a/tests/reactor/tests/00_basics/013_then_state_reset.sh b/tests/reactor/tests/00_basics/013_then_state_reset.sh new file mode 100644 index 00000000000..8675b44a817 --- /dev/null +++ b/tests/reactor/tests/00_basics/013_then_state_reset.sh @@ -0,0 +1,37 @@ +#!/bin/sh + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +TESTFILE="$WORKDIR/tmp/test" +EVENTS_FILE_A="$WORKDIR/tmp/events_A" +EVENTS_FILE_B="$WORKDIR/tmp/events_B" +MARKER_FILE="$WORKDIR/tmp/marker" +export TESTFILE EVENTS_FILE_A EVENTS_FILE_B MARKER_FILE + +# create initial file +touch "$TESTFILE" + +start_reactor "$1" + +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE_A" 1 +wait_for_lines "$EVENTS_FILE_B" 1 + +# recreate the file and wait for the watcher to see it before deleting it again +touch "$TESTFILE" +touch "$MARKER_FILE" + +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +# record_event should not have written to EVENTS_FILE_A since MARKER_FILE exists +wait_for_lines "$EVENTS_FILE_A" 1 +wait_for_lines "$EVENTS_FILE_B" 2 + +if [ "$(sed -n 1p "$EVENTS_FILE_B")" = "$(sed -n 2p "$EVENTS_FILE_B")" ]; then + echo "Lines in '$EVENTS_FILE_B' are identical:" >&2 + cat "$EVENTS_FILE_B" >&2 + exit 1 +fi \ No newline at end of file diff --git a/tests/reactor/tests/00_basics/014_persistent_classes.cf b/tests/reactor/tests/00_basics/014_persistent_classes.cf new file mode 100644 index 00000000000..af4d1b763f6 --- /dev/null +++ b/tests/reactor/tests/00_basics/014_persistent_classes.cf @@ -0,0 +1,45 @@ +####################################################### +# +# Test that persistent class remaine defined between "then" bundle runs +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +body classes if_ok(class) +{ + promise_kept => { "$(class)" }; + promise_repaired => { "$(class)" }; + persist_time => "1"; +} + +bundle agent run_if(b, file, msg) +{ + methods: + !has_run:: + "any" + usebundle => $(b)("$(file)", "$(msg)"), + classes => if_ok("has_run"); +} + +bundle reactor test +{ + vars: + "test" string => getenv("TESTFILE", 999); + "events_file" string => getenv("EVENTS_FILE", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + events: + "$(promise_name)" + when => deleted("$(test)"), + then => run_if("record_event", "$(events_file)", "$(test) was deleted"); +} diff --git a/tests/reactor/tests/00_basics/014_persistent_classes.sh b/tests/reactor/tests/00_basics/014_persistent_classes.sh new file mode 100644 index 00000000000..0d53e7bb5ff --- /dev/null +++ b/tests/reactor/tests/00_basics/014_persistent_classes.sh @@ -0,0 +1,38 @@ +#!/bin/sh + +# check that persistent class stay alive between "then" bundle runs + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +TESTFILE="$WORKDIR/tmp/test" +EVENTS_FILE="$WORKDIR/tmp/events" +export TESTFILE EVENTS_FILE + +# create initial file +touch "$TESTFILE" + +start_reactor "$1" + +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 1 + +# recreate the file and wait for the watcher to see it before deleting it again +touch "$TESTFILE" +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" + +# wait for the "then" bundle to have run, it doesn't record the event as the +# persistent class has_run is still defined +wait_for_log "B: END bundle run_if" +wait_for_lines "$EVENTS_FILE" 1 + +touch "$TESTFILE" +wait_for_log "File '$TESTFILE' is present, watching for its deletion" + +# sleep for the duration of the persistent class +sleep 60 +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 2 diff --git a/tests/reactor/tests/00_basics/015_outcome_setup_failed.cf b/tests/reactor/tests/00_basics/015_outcome_setup_failed.cf new file mode 100644 index 00000000000..3c422043f28 --- /dev/null +++ b/tests/reactor/tests/00_basics/015_outcome_setup_failed.cf @@ -0,0 +1,49 @@ +####################################################### +# +# Test that an events promise is not kept when its watcher can't be set up +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +body classes outcome(prefix) +{ + promise_kept => { "$(prefix)_kept" }; + promise_repaired => { "$(prefix)_repaired" }; + repair_failed => { "$(prefix)_failed" }; + repair_denied => { "$(prefix)_failed" }; + repair_timeout => { "$(prefix)_failed" }; +} + +bundle reactor test +{ + vars: + "test" string => getenv("TESTFILE", 999); + "check" string => getenv("CHECK_FILE", 999); + "events_file" string => getenv("EVENTS_FILE", 999); + + # only known when running, so cf-promises can't reject it + "missing_bundle" string => getenv("MISSING_BUNDLE", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + events: + "$(promise_name)_broken" + when => deleted("$(test)"), + then => { "$(missing_bundle)" }, + classes => outcome("broken"); + + # only set up if the outcome of the broken promise is as expected + broken_failed.!broken_kept.!broken_repaired:: + "$(promise_name)_check" + when => deleted("$(check)"), + then => record_event("$(events_file)", "$(check) was deleted"); +} diff --git a/tests/reactor/tests/00_basics/015_outcome_setup_failed.sh b/tests/reactor/tests/00_basics/015_outcome_setup_failed.sh new file mode 100644 index 00000000000..86a07ccca54 --- /dev/null +++ b/tests/reactor/tests/00_basics/015_outcome_setup_failed.sh @@ -0,0 +1,29 @@ +#!/bin/sh + +# events promise is NOT KEPT when couldn't set up watchers + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +TESTFILE="$WORKDIR/tmp/test" +CHECK_FILE="$WORKDIR/tmp/check" +EVENTS_FILE="$WORKDIR/tmp/events" +MISSING_BUNDLE="no_such_bundle" +export TESTFILE CHECK_FILE EVENTS_FILE MISSING_BUNDLE + +# create initial files +touch "$TESTFILE" +touch "$CHECK_FILE" + +start_reactor "$1" + +wait_for_log "refers to unknown bundle '$MISSING_BUNDLE'" + +# only the watcher of the check promise, which is only set up if the broken +# promise was not kept +wait_for_watchers 1 + +wait_for_log "File '$CHECK_FILE' is present, watching for its deletion" +rm "$CHECK_FILE" +wait_for_lines "$EVENTS_FILE" 1 diff --git a/tests/reactor/tests/00_basics/016_outcome_setup_kept.cf b/tests/reactor/tests/00_basics/016_outcome_setup_kept.cf new file mode 100644 index 00000000000..f6905df06e7 --- /dev/null +++ b/tests/reactor/tests/00_basics/016_outcome_setup_kept.cf @@ -0,0 +1,46 @@ +####################################################### +# +# Test that an events promise is kept when its watcher is set up +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +body classes outcome(prefix) +{ + promise_kept => { "$(prefix)_kept" }; + promise_repaired => { "$(prefix)_repaired" }; + repair_failed => { "$(prefix)_failed" }; + repair_denied => { "$(prefix)_failed" }; + repair_timeout => { "$(prefix)_failed" }; +} + +bundle reactor test +{ + vars: + "test" string => getenv("TESTFILE", 999); + "check" string => getenv("CHECK_FILE", 999); + "events_file" string => getenv("EVENTS_FILE", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + events: + "$(promise_name)_ok" + when => deleted("$(test)"), + then => record_event("$(events_file)", "$(test) was deleted"), + classes => outcome("ok"); + + # only set up if the outcome of the ok promise is as expected + ok_kept.!ok_repaired.!ok_failed:: + "$(promise_name)_check" + when => deleted("$(check)"), + then => record_event("$(events_file)", "$(check) was deleted"); +} diff --git a/tests/reactor/tests/00_basics/016_outcome_setup_kept.sh b/tests/reactor/tests/00_basics/016_outcome_setup_kept.sh new file mode 100644 index 00000000000..e383f7d0188 --- /dev/null +++ b/tests/reactor/tests/00_basics/016_outcome_setup_kept.sh @@ -0,0 +1,27 @@ +#!/bin/sh + +# when could set up watchers, promise is kept + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +TESTFILE="$WORKDIR/tmp/test" +CHECK_FILE="$WORKDIR/tmp/check" +EVENTS_FILE="$WORKDIR/tmp/events" +export TESTFILE CHECK_FILE EVENTS_FILE + +# create initial files +touch "$TESTFILE" +touch "$CHECK_FILE" + +start_reactor "$1" + +# the watcher of the ok promise, and the one of the check promise, which is +# only set up if the ok promise was kept +wait_for_watchers 2 + +wait_for_log "File '$CHECK_FILE' is present, watching for its deletion" +rm "$CHECK_FILE" +wait_for_lines "$EVENTS_FILE" 1 +grep -F "$CHECK_FILE was deleted" "$EVENTS_FILE" diff --git a/tests/reactor/tests/00_basics/017_outcome_polling_failed.cf b/tests/reactor/tests/00_basics/017_outcome_polling_failed.cf new file mode 100644 index 00000000000..c3581a742cb --- /dev/null +++ b/tests/reactor/tests/00_basics/017_outcome_polling_failed.cf @@ -0,0 +1,60 @@ +####################################################### +# +# Test that an events promise is not kept when its watcher fails to check +# for its event, and that its "then" bundles are not run +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +# The classes defined on an event are undone once it is handled, except the +# persistent ones, which the next events see +body classes persistent_outcome(prefix) +{ + promise_kept => { "$(prefix)_kept" }; + promise_repaired => { "$(prefix)_repaired" }; + repair_failed => { "$(prefix)_failed" }; + repair_denied => { "$(prefix)_failed" }; + repair_timeout => { "$(prefix)_failed" }; + persist_time => "10"; +} + +bundle agent record_outcome(file, prefix) +{ + methods: + "$(prefix)_failed" + if => "$(prefix)_failed", + usebundle => record_event("$(file)", "$(prefix)_failed"); + + "not $(prefix)_failed" + unless => "$(prefix)_failed", + usebundle => record_event("$(file)", "not $(prefix)_failed"); +} + +bundle reactor test +{ + vars: + "test" string => getenv("TESTFILE", 999); + "check" string => getenv("CHECK_FILE", 999); + "events_file" string => getenv("EVENTS_FILE", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + events: + "$(promise_name)_failing" + when => deleted("$(test)"), + then => record_event("$(events_file)", "$(test) was deleted"), + classes => persistent_outcome("failing"); + + "$(promise_name)_check" + when => deleted("$(check)"), + then => record_outcome("$(events_file)", "failing"); +} diff --git a/tests/reactor/tests/00_basics/017_outcome_polling_failed.sh b/tests/reactor/tests/00_basics/017_outcome_polling_failed.sh new file mode 100644 index 00000000000..bb6d2fd92b7 --- /dev/null +++ b/tests/reactor/tests/00_basics/017_outcome_polling_failed.sh @@ -0,0 +1,38 @@ +#!/bin/sh + +# when could set up watchers, but the polling thread is failing, return NOT KEPT + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +TESTDIR="$WORKDIR/tmp/dir" +TESTFILE="$TESTDIR/test" +CHECK_FILE="$WORKDIR/tmp/check" +EVENTS_FILE="$WORKDIR/tmp/events" +export TESTFILE CHECK_FILE EVENTS_FILE + +# create initial files +mkdir "$TESTDIR" +touch "$TESTFILE" +touch "$CHECK_FILE" + +start_reactor "$1" + +wait_for_watchers 2 + +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +wait_for_log "File '$CHECK_FILE' is present, watching for its deletion" + +# replace the directory of the watched file with a symlink to itself, so that +# checking the file fails (ELOOP, even as root) rather than seeing it deleted +rm -r "$TESTDIR" +ln -s "$TESTDIR" "$TESTDIR" +wait_for_log "Reactor watcher '${PROMISE_NAME}_failing' failed to check for its event" + +# the outcome of the failing promise is only visible to the next event +rm "$CHECK_FILE" +wait_for_lines "$EVENTS_FILE" 1 + +# the "then" bundle of the failing promise didn't run, and it wasn't kept +[ "$(cut -d' ' -f3- "$EVENTS_FILE")" = "failing_failed" ] diff --git a/tests/reactor/tests/00_basics/018_outcome_then_bundle.cf b/tests/reactor/tests/00_basics/018_outcome_then_bundle.cf new file mode 100644 index 00000000000..dd8f6c602ce --- /dev/null +++ b/tests/reactor/tests/00_basics/018_outcome_then_bundle.cf @@ -0,0 +1,83 @@ +####################################################### +# +# Test that the outcome of an events promise on its event is the outcome of +# its "then" bundles +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +# The classes defined on an event are undone once it is handled, except the +# persistent ones, which the next events see +body classes persistent_outcome(prefix) +{ + promise_repaired => { "$(prefix)_repaired" }; + repair_failed => { "$(prefix)_failed" }; + repair_denied => { "$(prefix)_failed" }; + repair_timeout => { "$(prefix)_failed" }; + persist_time => "10"; +} + +bundle agent fail +{ + commands: + "/bin/false"; +} + +bundle agent repair(file) +{ + files: + "$(file)" create => "true"; +} + +bundle agent record_outcomes(file) +{ + vars: + "classes" + slist => { + "failing_failed", + "failing_repaired", + "repairing_failed", + "repairing_repaired", + }; + + methods: + "$(classes)" + if => "$(classes)", + usebundle => record_event("$(file)", "$(classes)"); +} + +bundle reactor test +{ + vars: + "test_failing" string => getenv("TESTFILE_FAILING", 999); + "test_repairing" string => getenv("TESTFILE_REPAIRING", 999); + "created" string => getenv("CREATED_FILE", 999); + "check" string => getenv("CHECK_FILE", 999); + "events_file" string => getenv("EVENTS_FILE", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + events: + "$(promise_name)_failing" + when => deleted("$(test_failing)"), + then => fail, + classes => persistent_outcome("failing"); + + "$(promise_name)_repairing" + when => deleted("$(test_repairing)"), + then => repair("$(created)"), + classes => persistent_outcome("repairing"); + + "$(promise_name)_check" + when => deleted("$(check)"), + then => record_outcomes("$(events_file)"); +} diff --git a/tests/reactor/tests/00_basics/018_outcome_then_bundle.sh b/tests/reactor/tests/00_basics/018_outcome_then_bundle.sh new file mode 100644 index 00000000000..3b77499ecf8 --- /dev/null +++ b/tests/reactor/tests/00_basics/018_outcome_then_bundle.sh @@ -0,0 +1,40 @@ +#!/bin/sh + +# if set up and polls correctly, the outcome should be the outcome of the run bundle in "then" + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +TESTFILE_FAILING="$WORKDIR/tmp/test_failing" +TESTFILE_REPAIRING="$WORKDIR/tmp/test_repairing" +CREATED_FILE="$WORKDIR/tmp/created" +CHECK_FILE="$WORKDIR/tmp/check" +EVENTS_FILE="$WORKDIR/tmp/events" +export TESTFILE_FAILING TESTFILE_REPAIRING CREATED_FILE CHECK_FILE EVENTS_FILE + +# create initial files +touch "$TESTFILE_FAILING" +touch "$TESTFILE_REPAIRING" +touch "$CHECK_FILE" + +start_reactor "$1" + +wait_for_watchers 3 + +wait_for_log "File '$TESTFILE_FAILING' is present, watching for its deletion" +wait_for_log "File '$TESTFILE_REPAIRING' is present, watching for its deletion" +wait_for_log "File '$CHECK_FILE' is present, watching for its deletion" + +# one event at a time, the events are handled in order +rm "$TESTFILE_FAILING" +wait_for_log "Reactor watcher '${PROMISE_NAME}_failing' fired" +rm "$TESTFILE_REPAIRING" +wait_for_log "Reactor watcher '${PROMISE_NAME}_repairing' fired" + +# the outcomes of the previous events are only visible to the next event +rm "$CHECK_FILE" +wait_for_lines "$EVENTS_FILE" 2 + +[ -f "$CREATED_FILE" ] +[ "$(cut -d' ' -f3- "$EVENTS_FILE" | sort)" = "$(printf 'failing_failed\nrepairing_repaired')" ] diff --git a/tests/reactor/tests/00_basics/019_override_ifelapsed.cf b/tests/reactor/tests/00_basics/019_override_ifelapsed.cf new file mode 100644 index 00000000000..15d1947bb88 --- /dev/null +++ b/tests/reactor/tests/00_basics/019_override_ifelapsed.cf @@ -0,0 +1,51 @@ +####################################################### +# +# Test that the ifelapsed of 0 that cf-reactor uses by default can be +# overridden by the promises of the "then" bundles +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +body action if_elapsed(minutes) +{ + ifelapsed => "$(minutes)"; +} + +bundle agent record(file_default, file_ifelapsed, msg) +{ + reports: + # first, so that it is skipped or not by the time the other one is made + "$(msg)" + report_to_file => "$(file_ifelapsed)", + action => if_elapsed("60"); + + "$(msg)" report_to_file => "$(file_default)"; +} + +bundle reactor test +{ + vars: + "test" string => getenv("TESTFILE", 999); + "events_file_default" string => getenv("EVENTS_FILE_DEFAULT", 999); + "events_file_ifelapsed" string => getenv("EVENTS_FILE_IFELAPSED", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + events: + "$(promise_name)" + when => deleted("$(test)"), + then => record( + "$(events_file_default)", + "$(events_file_ifelapsed)", + "$(test) was deleted" + ); +} diff --git a/tests/reactor/tests/00_basics/019_override_ifelapsed.sh b/tests/reactor/tests/00_basics/019_override_ifelapsed.sh new file mode 100644 index 00000000000..29ff637cc86 --- /dev/null +++ b/tests/reactor/tests/00_basics/019_override_ifelapsed.sh @@ -0,0 +1,34 @@ +#!/bin/sh + +# cf-reactor defaults ifelapsed to 0, but it can be overridden + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +TESTFILE="$WORKDIR/tmp/test" +EVENTS_FILE_DEFAULT="$WORKDIR/tmp/events_default" +EVENTS_FILE_IFELAPSED="$WORKDIR/tmp/events_ifelapsed" +export TESTFILE EVENTS_FILE_DEFAULT EVENTS_FILE_IFELAPSED + +# create initial file +touch "$TESTFILE" + +start_reactor "$1" + +wait_for_watchers 1 + +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE_DEFAULT" 1 +wait_for_lines "$EVENTS_FILE_IFELAPSED" 1 + +# recreate the file and wait for the watcher to see it before deleting it again +touch "$TESTFILE" +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" + +# with the default ifelapsed of 0, the report is made again, but not the one +# with an ifelapsed of 60 minutes +wait_for_lines "$EVENTS_FILE_DEFAULT" 2 +wait_for_lines "$EVENTS_FILE_IFELAPSED" 1 diff --git a/tests/reactor/tests/00_basics/020_policy_reload.cf b/tests/reactor/tests/00_basics/020_policy_reload.cf new file mode 100644 index 00000000000..082b9bcb833 --- /dev/null +++ b/tests/reactor/tests/00_basics/020_policy_reload.cf @@ -0,0 +1,37 @@ +####################################################### +# +# Test that a watcher keeps the state of its event over a policy reload, so +# that an event happening while reloading is detected +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +bundle reactor test +{ + vars: + "test" string => getenv("TESTFILE", 999); + "events_file" string => getenv("EVENTS_FILE", 999); + "reload_marker" string => getenv("RELOAD_MARKER", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + # Evaluated on start and on every policy reload, after the watchers were + # set aside: tells the test it can change the file, and keeps the watchers + # paused long enough for it to do so + "slow" + string => execresult("touch '$(reload_marker)' && sleep 3", "useshell"); + + events: + "$(promise_name)" + when => deleted("$(test)"), + then => record_event("$(events_file)", "$(test) was deleted"); +} diff --git a/tests/reactor/tests/00_basics/020_policy_reload.sh b/tests/reactor/tests/00_basics/020_policy_reload.sh new file mode 100644 index 00000000000..7a6aafac1c6 --- /dev/null +++ b/tests/reactor/tests/00_basics/020_policy_reload.sh @@ -0,0 +1,79 @@ +#!/bin/sh + +# check that a watcher keeps the state of its event (whether the file existed +# at its last check) over a policy reload, i.e. that what the first check after +# the reload sees is compared to the state from before the reload, so that an +# event is neither missed nor reported twice: +# 1. last check: exists, reload, file deleted -> next check: event +# 2. last check: missing, reload, still missing -> next check: no event, +# the deletion was already reported +# 3. last check: missing, reload, file created -> next check: no event + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +TESTFILE="$WORKDIR/tmp/test" +EVENTS_FILE="$WORKDIR/tmp/events" +RELOAD_MARKER="$WORKDIR/tmp/reload_marker" +export TESTFILE EVENTS_FILE RELOAD_MARKER + +# Reload the policy and wait until its watchers are set aside, which they stay +# for 3 seconds, see the "slow" vars promise. The file can't be checked until +# then, so changing it now is only seen by the watcher of the reloaded policy. +reload_and_wait_for_paused_watchers() { + # created when the policy was last evaluated + rm "$RELOAD_MARKER" + + kill -HUP "$reactor_pid" + wait_for_log "Rereading policy file" + + if ! timeout 60 sh -c 'until [ -f "$1" ]; do sleep 0.1; done' _ "$RELOAD_MARKER" + then + echo "Timed out waiting for the policy to be reloaded, reactor log:" >&2 + cat "$LOG" >&2 + exit 1 + fi +} + +# create initial file +touch "$TESTFILE" + +start_reactor "$1" + +wait_for_watchers 1 +wait_for_log "File '$TESTFILE' is present, watching for its deletion" + +# 1. last check: exists. Without the state from before the reload, the first +# check after it would only find the file missing, which is not an event. +reload_and_wait_for_paused_watchers +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 1 + +# 2. last check: missing (the deletion above, already reported). Without the +# state from before the reload, a watcher assuming the file existed would +# report the deletion again. +rm "$RELOAD_MARKER" +kill -HUP "$reactor_pid" +wait_for_log "Rereading policy file" +# the watchers of the reloaded policy are registered and checked again +wait_for_log "cf-reactor event watcher has 1 watcher(s)" + +# Proving that something doesn't happen needs a wait, give it a couple of +# checks +sleep 2 +wait_for_lines "$EVENTS_FILE" 1 + +# 3. last check: missing. The file appearing is not an event, the watcher only +# starts watching for its deletion again. +reload_and_wait_for_paused_watchers +touch "$TESTFILE" +wait_for_log "File '$TESTFILE' is present, watching for its deletion" + +# An event would have been queued by the check that found the file present, +# give it a couple of checks to be handled +sleep 2 +wait_for_lines "$EVENTS_FILE" 1 + +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 2 diff --git a/tests/reactor/tests/00_basics/021_no_file.cf b/tests/reactor/tests/00_basics/021_no_file.cf new file mode 100644 index 00000000000..4edb8670d74 --- /dev/null +++ b/tests/reactor/tests/00_basics/021_no_file.cf @@ -0,0 +1,30 @@ +####################################################### +# +# Test a file_deleted event on a file that doesn't exist yet when the watcher +# is set up +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +bundle reactor test +{ + vars: + "test" string => getenv("TESTFILE", 999); + "events_file" string => getenv("EVENTS_FILE", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + events: + "$(promise_name)" + when => deleted("$(test)"), + then => record_event("$(events_file)", "$(test) was deleted"); +} diff --git a/tests/reactor/tests/00_basics/021_no_file.sh b/tests/reactor/tests/00_basics/021_no_file.sh new file mode 100644 index 00000000000..d4d9d769ed2 --- /dev/null +++ b/tests/reactor/tests/00_basics/021_no_file.sh @@ -0,0 +1,34 @@ +#!/bin/sh + +# check that a watcher set up while its file doesn't exist doesn't fire, and +# fires once the file is created and then deleted + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +TESTFILE="$WORKDIR/tmp/test" +EVENTS_FILE="$WORKDIR/tmp/events" +export TESTFILE EVENTS_FILE + +# no initial file + +start_reactor "$1" + +# the watcher is set up, its check on registration found the file missing +wait_for_watchers 1 + +touch "$TESTFILE" +wait_for_log "File '$TESTFILE' is present, watching for its deletion" + +# the missing file at the first checks was not an event: those checks were +# made at least a second before the one which found the file present +if grep -F "Reactor watcher '$PROMISE_NAME' fired" "$LOG" || [ -e "$EVENTS_FILE" ] +then + echo "Watcher fired while the file didn't exist yet, reactor log:" >&2 + cat "$LOG" >&2 + exit 1 +fi + +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 1 diff --git a/tests/reactor/tests/01_event_types/001_file_deleted.cf b/tests/reactor/tests/01_event_types/001_file_deleted.cf new file mode 100644 index 00000000000..884afbfcfcc --- /dev/null +++ b/tests/reactor/tests/01_event_types/001_file_deleted.cf @@ -0,0 +1,48 @@ +####################################################### +# +# Test what is a deletion for a file_deleted event +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +bundle reactor test +{ + vars: + "file_a" string => getenv("FILE_A", 999); + "file_b" string => getenv("FILE_B", 999); + "file_c" string => getenv("FILE_C", 999); + "file_d" string => getenv("FILE_D", 999); + "events_file" string => getenv("EVENTS_FILE", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + events: + # a regular file + "$(promise_name)_A" + when => deleted("$(file_a)"), + then => record_event("$(events_file)", "A deleted"); + + # a file in a directory + "$(promise_name)_B" + when => deleted("$(file_b)"), + then => record_event("$(events_file)", "B deleted"); + + # a symlink + "$(promise_name)_C" + when => deleted("$(file_c)"), + then => record_event("$(events_file)", "C deleted"); + + # a directory + "$(promise_name)_D" + when => deleted("$(file_d)"), + then => record_event("$(events_file)", "D deleted"); +} diff --git a/tests/reactor/tests/01_event_types/001_file_deleted.sh b/tests/reactor/tests/01_event_types/001_file_deleted.sh new file mode 100644 index 00000000000..32f3de78b90 --- /dev/null +++ b/tests/reactor/tests/01_event_types/001_file_deleted.sh @@ -0,0 +1,71 @@ +#!/bin/sh + +# check what is a deletion for a file_deleted event, which only looks at +# whether the path exists, without following it if it is a symlink: +# A. a regular file: modifying it is not a deletion, removing it is +# B. a file whose parent turns into a regular file (ENOTDIR) is deleted +# C. a symlink: removing its target is not a deletion, removing it is +# D. a directory: removing it is a deletion + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +FILE_A="$WORKDIR/tmp/file_a" +DIR_B="$WORKDIR/tmp/dir_b" +FILE_B="$DIR_B/file_b" +TARGET_C="$WORKDIR/tmp/target_c" +FILE_C="$WORKDIR/tmp/link_c" +FILE_D="$WORKDIR/tmp/dir_d" +EVENTS_FILE="$WORKDIR/tmp/events" +export FILE_A FILE_B FILE_C FILE_D EVENTS_FILE + +# create initial files +touch "$FILE_A" +mkdir "$DIR_B" +touch "$FILE_B" +touch "$TARGET_C" +ln -s "$TARGET_C" "$FILE_C" +mkdir "$FILE_D" + +start_reactor "$1" + +wait_for_watchers 4 + +# checked on registration, in the order of the promises +wait_for_log "File '$FILE_A' is present, watching for its deletion" +wait_for_log "File '$FILE_B' is present, watching for its deletion" +wait_for_log "File '$FILE_C' is present, watching for its deletion" +wait_for_log "File '$FILE_D' is present, watching for its deletion" + +# not deletions +echo "modified" >> "$FILE_A" +touch "$FILE_A" +chmod 600 "$FILE_A" +rm "$TARGET_C" + +# give the watchers a couple of checks to see them, in case they would fire. +# Proving that something doesn't happen needs a wait. +sleep 2 +if [ -e "$EVENTS_FILE" ] +then + echo "Fired on something that is not a deletion:" >&2 + cat "$EVENTS_FILE" >&2 + exit 1 +fi + +# deletions +rm "$FILE_A" + +# make the parent of the file a regular file, so that its path has a regular +# file as directory (ENOTDIR). A check right between the two commands would +# see it missing (ENOENT) instead, which is also a deletion. +rm -r "$DIR_B" +touch "$DIR_B" + +rm "$FILE_C" +rmdir "$FILE_D" + +# each fired once +wait_for_lines "$EVENTS_FILE" 4 +[ "$(cut -d' ' -f3- "$EVENTS_FILE" | sort)" = "$(printf 'A deleted\nB deleted\nC deleted\nD deleted')" ] diff --git a/tests/reactor/tests/02_then_bundles/001_meta.cf b/tests/reactor/tests/02_then_bundles/001_meta.cf new file mode 100644 index 00000000000..e43111b40e6 --- /dev/null +++ b/tests/reactor/tests/02_then_bundles/001_meta.cf @@ -0,0 +1,44 @@ +####################################################### +# +# Test meta promises in a "then" bundle +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +# Not named run_meta: the meta variables of a bundle whose name ends with +# "_meta" are in the scope of the bundle itself, not in _meta +bundle agent run_meta_promises(events_file, value_file) +{ + meta: + "value" string => readfile("$(value_file)"); + + methods: + "any" + usebundle => record_event( + "$(events_file)", "meta $(run_meta_promises_meta.value)" + ); +} + +bundle reactor test +{ + vars: + "test" string => getenv("TESTFILE", 999); + "events_file" string => getenv("EVENTS_FILE", 999); + "value_file" string => getenv("VALUE_FILE", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + events: + "$(promise_name)" + when => deleted("$(test)"), + then => run_meta_promises("$(events_file)", "$(value_file)"); +} diff --git a/tests/reactor/tests/02_then_bundles/001_meta.sh b/tests/reactor/tests/02_then_bundles/001_meta.sh new file mode 100644 index 00000000000..8473eb7056e --- /dev/null +++ b/tests/reactor/tests/02_then_bundles/001_meta.sh @@ -0,0 +1,36 @@ +#!/bin/sh + +# check that meta promises in a "then" bundle are evaluated again on every +# event, in the _meta scope + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +TESTFILE="$WORKDIR/tmp/test" +EVENTS_FILE="$WORKDIR/tmp/events" +VALUE_FILE="$WORKDIR/tmp/value" +export TESTFILE EVENTS_FILE VALUE_FILE + +printf "first" > "$VALUE_FILE" +touch "$TESTFILE" + +start_reactor "$1" + +wait_for_watchers 1 + +# first event +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 1 +check_event "$EVENTS_FILE" 1 "meta first" + +printf "second" > "$VALUE_FILE" + +# second event: recreate the file and wait for the watcher to see it before +# deleting it again +touch "$TESTFILE" +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 2 +check_event "$EVENTS_FILE" 2 "meta second" diff --git a/tests/reactor/tests/02_then_bundles/002_vars.cf b/tests/reactor/tests/02_then_bundles/002_vars.cf new file mode 100644 index 00000000000..e2aefbe08c4 --- /dev/null +++ b/tests/reactor/tests/02_then_bundles/002_vars.cf @@ -0,0 +1,40 @@ +####################################################### +# +# Test vars promises in a "then" bundle +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +bundle agent run_vars(events_file, value_file) +{ + vars: + # execresult() is a cached function: the cache must not outlive an event + "value" string => execresult("/bin/cat $(value_file)", "noshell"); + + methods: + "any" usebundle => record_event("$(events_file)", "vars $(value)"); +} + +bundle reactor test +{ + vars: + "test" string => getenv("TESTFILE", 999); + "events_file" string => getenv("EVENTS_FILE", 999); + "value_file" string => getenv("VALUE_FILE", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + events: + "$(promise_name)" + when => deleted("$(test)"), + then => run_vars("$(events_file)", "$(value_file)"); +} diff --git a/tests/reactor/tests/02_then_bundles/002_vars.sh b/tests/reactor/tests/02_then_bundles/002_vars.sh new file mode 100644 index 00000000000..4c961c7f78a --- /dev/null +++ b/tests/reactor/tests/02_then_bundles/002_vars.sh @@ -0,0 +1,36 @@ +#!/bin/sh + +# check that vars promises in a "then" bundle are evaluated again on every +# event, without the value of a cached function from the previous one + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +TESTFILE="$WORKDIR/tmp/test" +EVENTS_FILE="$WORKDIR/tmp/events" +VALUE_FILE="$WORKDIR/tmp/value" +export TESTFILE EVENTS_FILE VALUE_FILE + +printf "first" > "$VALUE_FILE" +touch "$TESTFILE" + +start_reactor "$1" + +wait_for_watchers 1 + +# first event +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 1 +check_event "$EVENTS_FILE" 1 "vars first" + +printf "second" > "$VALUE_FILE" + +# second event: recreate the file and wait for the watcher to see it before +# deleting it again +touch "$TESTFILE" +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 2 +check_event "$EVENTS_FILE" 2 "vars second" diff --git a/tests/reactor/tests/02_then_bundles/003_defaults.cf b/tests/reactor/tests/02_then_bundles/003_defaults.cf new file mode 100644 index 00000000000..0f604a5d507 --- /dev/null +++ b/tests/reactor/tests/02_then_bundles/003_defaults.cf @@ -0,0 +1,40 @@ +####################################################### +# +# Test defaults promises in a "then" bundle +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +bundle agent run_defaults(events_file, msg) +{ + defaults: + "msg" + string => "default message", + if_match_regex => ""; + + methods: + "any" usebundle => record_event("$(events_file)", "defaults $(msg)"); +} + +bundle reactor test +{ + vars: + "test" string => getenv("TESTFILE", 999); + "events_file" string => getenv("EVENTS_FILE", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + events: + "$(promise_name)" + when => deleted("$(test)"), + then => run_defaults("$(events_file)", ""); +} diff --git a/tests/reactor/tests/02_then_bundles/003_defaults.sh b/tests/reactor/tests/02_then_bundles/003_defaults.sh new file mode 100644 index 00000000000..231f2031ad6 --- /dev/null +++ b/tests/reactor/tests/02_then_bundles/003_defaults.sh @@ -0,0 +1,32 @@ +#!/bin/sh + +# check that defaults promises in a "then" bundle are evaluated on every event + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +TESTFILE="$WORKDIR/tmp/test" +EVENTS_FILE="$WORKDIR/tmp/events" + +export TESTFILE EVENTS_FILE + +touch "$TESTFILE" + +start_reactor "$1" + +wait_for_watchers 1 + +# first event +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 1 +check_event "$EVENTS_FILE" 1 "defaults default message" + +# second event: recreate the file and wait for the watcher to see it before +# deleting it again +touch "$TESTFILE" +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 2 +check_event "$EVENTS_FILE" 2 "defaults default message" diff --git a/tests/reactor/tests/02_then_bundles/004_classes.cf b/tests/reactor/tests/02_then_bundles/004_classes.cf new file mode 100644 index 00000000000..0b7c32a8982 --- /dev/null +++ b/tests/reactor/tests/02_then_bundles/004_classes.cf @@ -0,0 +1,47 @@ +####################################################### +# +# Test classes promises in a "then" bundle +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +bundle agent run_classes(events_file, marker) +{ + classes: + # returnszero() is a cached function: the cache must not outlive an event + "marker_exists" + expression => returnszero("test -f '$(marker)'", "useshell"); + + methods: + marker_exists:: + "any" + usebundle => record_event("$(events_file)", "classes marker exists"); + + !marker_exists:: + "any" + usebundle => record_event("$(events_file)", "classes marker missing"); +} + +bundle reactor test +{ + vars: + "test" string => getenv("TESTFILE", 999); + "events_file" string => getenv("EVENTS_FILE", 999); + "marker" string => getenv("MARKER_FILE", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + events: + "$(promise_name)" + when => deleted("$(test)"), + then => run_classes("$(events_file)", "$(marker)"); +} diff --git a/tests/reactor/tests/02_then_bundles/004_classes.sh b/tests/reactor/tests/02_then_bundles/004_classes.sh new file mode 100644 index 00000000000..2bdbe0f3038 --- /dev/null +++ b/tests/reactor/tests/02_then_bundles/004_classes.sh @@ -0,0 +1,35 @@ +#!/bin/sh + +# check that classes promises in a "then" bundle are evaluated again on +# every event, without the value of a cached function from the previous one + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +TESTFILE="$WORKDIR/tmp/test" +EVENTS_FILE="$WORKDIR/tmp/events" +MARKER_FILE="$WORKDIR/tmp/marker" +export TESTFILE EVENTS_FILE MARKER_FILE + +touch "$TESTFILE" + +start_reactor "$1" + +wait_for_watchers 1 + +# first event +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 1 +check_event "$EVENTS_FILE" 1 "classes marker missing" + +touch "$MARKER_FILE" + +# second event: recreate the file and wait for the watcher to see it before +# deleting it again +touch "$TESTFILE" +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 2 +check_event "$EVENTS_FILE" 2 "classes marker exists" diff --git a/tests/reactor/tests/02_then_bundles/005_users.cf b/tests/reactor/tests/02_then_bundles/005_users.cf new file mode 100644 index 00000000000..bb77ae735e6 --- /dev/null +++ b/tests/reactor/tests/02_then_bundles/005_users.cf @@ -0,0 +1,41 @@ +####################################################### +# +# Test users promises in a "then" bundle +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +bundle agent run_users(user) +{ + users: + # the user running the test, so that it is kept without being root + "$(user)" + policy => "present", + classes => then_outcome("users"); +} + +bundle reactor test +{ + vars: + "test" string => getenv("TESTFILE", 999); + "events_file" string => getenv("EVENTS_FILE", 999); + "user" string => getenv("TEST_USER", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + events: + "$(promise_name)" + when => deleted("$(test)"), + then => { + run_users("$(user)"), record_then_outcome("$(events_file)", "users") + }; +} diff --git a/tests/reactor/tests/02_then_bundles/005_users.sh b/tests/reactor/tests/02_then_bundles/005_users.sh new file mode 100644 index 00000000000..6cdcea50b9d --- /dev/null +++ b/tests/reactor/tests/02_then_bundles/005_users.sh @@ -0,0 +1,35 @@ +#!/bin/sh + +# check that users promises in a "then" bundle are evaluated on every event + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +TESTFILE="$WORKDIR/tmp/test" +EVENTS_FILE="$WORKDIR/tmp/events" + +export TESTFILE EVENTS_FILE + +TEST_USER=$(id -un) +export TEST_USER + +touch "$TESTFILE" + +start_reactor "$1" + +wait_for_watchers 1 + +# first event +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 1 +check_event "$EVENTS_FILE" 1 "users kept" + +# second event: recreate the file and wait for the watcher to see it before +# deleting it again +touch "$TESTFILE" +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 2 +check_event "$EVENTS_FILE" 2 "users kept" diff --git a/tests/reactor/tests/02_then_bundles/006_files.cf b/tests/reactor/tests/02_then_bundles/006_files.cf new file mode 100644 index 00000000000..90a482eee6f --- /dev/null +++ b/tests/reactor/tests/02_then_bundles/006_files.cf @@ -0,0 +1,48 @@ +####################################################### +# +# Test files promises in a "then" bundle +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +bundle agent run_files(file, line) +{ + files: + "$(file)" + create => "true", + edit_line => insert_line("$(line)"), + classes => then_outcome("files"); +} + +bundle edit_line insert_line(line) +{ + insert_lines: + "$(line)"; +} + +bundle reactor test +{ + vars: + "test" string => getenv("TESTFILE", 999); + "events_file" string => getenv("EVENTS_FILE", 999); + "created" string => getenv("CREATED_FILE", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + events: + "$(promise_name)" + when => deleted("$(test)"), + then => { + run_files("$(created)", "created on event"), + record_then_outcome("$(events_file)", "files"), + }; +} diff --git a/tests/reactor/tests/02_then_bundles/006_files.sh b/tests/reactor/tests/02_then_bundles/006_files.sh new file mode 100644 index 00000000000..bbe8df3b46c --- /dev/null +++ b/tests/reactor/tests/02_then_bundles/006_files.sh @@ -0,0 +1,37 @@ +#!/bin/sh + +# check that files promises in a "then" bundle are evaluated again on every +# event: the file deleted after the first one is created again + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +TESTFILE="$WORKDIR/tmp/test" +EVENTS_FILE="$WORKDIR/tmp/events" +CREATED_FILE="$WORKDIR/tmp/created" +export TESTFILE EVENTS_FILE CREATED_FILE + +touch "$TESTFILE" + +start_reactor "$1" + +wait_for_watchers 1 + +# first event +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 1 +check_event "$EVENTS_FILE" 1 "files repaired" +[ "$(cat "$CREATED_FILE")" = "created on event" ] + +rm "$CREATED_FILE" + +# second event: recreate the file and wait for the watcher to see it before +# deleting it again +touch "$TESTFILE" +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 2 +check_event "$EVENTS_FILE" 2 "files repaired" +[ "$(cat "$CREATED_FILE")" = "created on event" ] diff --git a/tests/reactor/tests/02_then_bundles/007_packages.cf b/tests/reactor/tests/02_then_bundles/007_packages.cf new file mode 100644 index 00000000000..84bcd32de98 --- /dev/null +++ b/tests/reactor/tests/02_then_bundles/007_packages.cf @@ -0,0 +1,50 @@ +####################################################### +# +# Test packages promises in a "then" bundle +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +# Queries the installed packages on every evaluation (query_installed_ifelapsed +# of 0), so the package removed between events is installed again +body package_module reactor_test +{ + query_installed_ifelapsed => "0"; + query_updates_ifelapsed => "0"; + default_options => { "$(test.installed)" }; +} + +bundle agent run_packages +{ + packages: + "reactor_test_pkg" + policy => "present", + package_module => reactor_test, + classes => then_outcome("packages"); +} + +bundle reactor test +{ + vars: + "test" string => getenv("TESTFILE", 999); + "events_file" string => getenv("EVENTS_FILE", 999); + "installed" string => getenv("INSTALLED_FILE", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + events: + "$(promise_name)" + when => deleted("$(test)"), + then => { + "run_packages", record_then_outcome("$(events_file)", "packages") + }; +} diff --git a/tests/reactor/tests/02_then_bundles/007_packages.sh b/tests/reactor/tests/02_then_bundles/007_packages.sh new file mode 100644 index 00000000000..c4bb57f8193 --- /dev/null +++ b/tests/reactor/tests/02_then_bundles/007_packages.sh @@ -0,0 +1,102 @@ +#!/bin/sh + +# check that packages promises in a "then" bundle are evaluated again on +# every event: the package removed after the first one is installed again + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +TESTFILE="$WORKDIR/tmp/test" +EVENTS_FILE="$WORKDIR/tmp/events" +INSTALLED_FILE="$WORKDIR/tmp/installed" +export TESTFILE EVENTS_FILE INSTALLED_FILE + +# A package module installing packages by adding them to the file given as +# option, see tests/acceptance/07_packages/package_module_path.cf.module +mkdir -p "$WORKDIR/modules/packages" +cat > "$WORKDIR/modules/packages/reactor_test" <<'MODULE' +#!/bin/sh + +set -e + +remove_prefix() +{ + echo "$1" | sed "s/$2//" +} + +case "$1" in + supports-api-version) + echo 1 + ;; + get-package-data) + while read line; do + case "$line" in + File=*) + echo PackageType=repo + echo Name=`remove_prefix "${line}" "File="` + ;; + esac + done + ;; + list-installed) + while read line; do + case "$line" in + options=*) + OUTPUT=`remove_prefix "${line}" "options="` + ;; + esac + done + if [ -f "$OUTPUT" ]; then + cat "$OUTPUT" + fi + ;; + list-*) + cat > /dev/null + ;; + repo-install) + while read line; do + case "$line" in + options=*) + OUTPUT=`remove_prefix "${line}" "options="` + ;; + Name=*) + NAME=`remove_prefix "${line}" "Name="` + ;; + esac + done + echo "Name=$NAME" >> "$OUTPUT" + echo "Version=1.0" >> "$OUTPUT" + echo "Architecture=generic" >> "$OUTPUT" + ;; + *) + exit 1 + ;; +esac +MODULE +chmod 700 "$WORKDIR/modules/packages/reactor_test" + +touch "$TESTFILE" + +start_reactor "$1" + +wait_for_watchers 1 + +# first event +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 1 +check_event "$EVENTS_FILE" 1 "packages repaired" +grep -x "Name=reactor_test_pkg" "$INSTALLED_FILE" + +# remove the package +rm "$INSTALLED_FILE" + +# second event: recreate the file and wait for the watcher to see it before +# deleting it again +touch "$TESTFILE" +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 2 +check_event "$EVENTS_FILE" 2 "packages repaired" +grep -x "Name=reactor_test_pkg" "$INSTALLED_FILE" diff --git a/tests/reactor/tests/02_then_bundles/008_guest_environments.cf b/tests/reactor/tests/02_then_bundles/008_guest_environments.cf new file mode 100644 index 00000000000..1297334a885 --- /dev/null +++ b/tests/reactor/tests/02_then_bundles/008_guest_environments.cf @@ -0,0 +1,41 @@ +####################################################### +# +# Test guest_environments promises in a "then" bundle +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +bundle agent run_guest_environments +{ + guest_environments: + # the guest that the "test" hypervisor of libvirt (test:///default) runs. + # Without libvirt, guest_environments promises are kept without doing + # anything, and without defining the classes of their outcome, so there's + # no classes body here: the test looks at the log instead. + "test" + environment_type => "test", + environment_host => "$(sys.host)", + environment_state => "running"; +} + +bundle reactor test +{ + vars: + "test" string => getenv("TESTFILE", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + events: + "$(promise_name)" + when => deleted("$(test)"), + then => "run_guest_environments"; +} diff --git a/tests/reactor/tests/02_then_bundles/008_guest_environments.sh b/tests/reactor/tests/02_then_bundles/008_guest_environments.sh new file mode 100644 index 00000000000..c61b868ca72 --- /dev/null +++ b/tests/reactor/tests/02_then_bundles/008_guest_environments.sh @@ -0,0 +1,39 @@ +#!/bin/sh + +# check that guest_environments promises in a "then" bundle are evaluated on +# every event. Without libvirt, they don't define the classes of their +# outcome, so this looks at the log instead. + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +TESTFILE="$WORKDIR/tmp/test" + +export TESTFILE + +touch "$TESTFILE" + +start_reactor "$1" + +wait_for_watchers 1 + +# The promise is evaluated, and logged, on every pass of the bundle: wait for +# the end of the bundle, so that the next event only sees the logs of its run +wait_for_evaluation() { + wait_for_log "P: BEGIN guest_environments promise (test)" + wait_for_log "A: Promise was KEPT" + wait_for_log "B: END bundle run_guest_environments" +} + +# first event +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_evaluation + +# second event: recreate the file and wait for the watcher to see it before +# deleting it again +touch "$TESTFILE" +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_evaluation diff --git a/tests/reactor/tests/02_then_bundles/009_methods.cf b/tests/reactor/tests/02_then_bundles/009_methods.cf new file mode 100644 index 00000000000..ab373e2a142 --- /dev/null +++ b/tests/reactor/tests/02_then_bundles/009_methods.cf @@ -0,0 +1,44 @@ +####################################################### +# +# Test methods promises in a "then" bundle +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +bundle agent run_methods(events_file, value_file) +{ + methods: + "any" + usebundle => record_event("$(events_file)", "methods $(value)"), + classes => then_outcome("methods"); + + vars: + "value" string => readfile("$(value_file)"); +} + +bundle reactor test +{ + vars: + "test" string => getenv("TESTFILE", 999); + "events_file" string => getenv("EVENTS_FILE", 999); + "value_file" string => getenv("VALUE_FILE", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + events: + "$(promise_name)" + when => deleted("$(test)"), + then => { + run_methods("$(events_file)", "$(value_file)"), + record_then_outcome("$(events_file)", "methods"), + }; +} diff --git a/tests/reactor/tests/02_then_bundles/009_methods.sh b/tests/reactor/tests/02_then_bundles/009_methods.sh new file mode 100644 index 00000000000..6de68176703 --- /dev/null +++ b/tests/reactor/tests/02_then_bundles/009_methods.sh @@ -0,0 +1,38 @@ +#!/bin/sh + +# check that methods promises in a "then" bundle are evaluated again on every +# event, with the arguments of the event + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +TESTFILE="$WORKDIR/tmp/test" +EVENTS_FILE="$WORKDIR/tmp/events" +VALUE_FILE="$WORKDIR/tmp/value" +export TESTFILE EVENTS_FILE VALUE_FILE + +printf "first" > "$VALUE_FILE" +touch "$TESTFILE" + +start_reactor "$1" + +wait_for_watchers 1 + +# first event +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 2 +check_event "$EVENTS_FILE" 1 "methods first" +check_event "$EVENTS_FILE" 2 "methods kept" + +printf "second" > "$VALUE_FILE" + +# second event: recreate the file and wait for the watcher to see it before +# deleting it again +touch "$TESTFILE" +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 4 +check_event "$EVENTS_FILE" 3 "methods second" +check_event "$EVENTS_FILE" 4 "methods kept" diff --git a/tests/reactor/tests/02_then_bundles/010_processes.cf b/tests/reactor/tests/02_then_bundles/010_processes.cf new file mode 100644 index 00000000000..f25585e625b --- /dev/null +++ b/tests/reactor/tests/02_then_bundles/010_processes.cf @@ -0,0 +1,39 @@ +####################################################### +# +# Test processes promises in a "then" bundle +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +bundle agent run_processes +{ + processes: + "sleep 978654" + signals => { "term" }, + classes => then_outcome("processes"); +} + +bundle reactor test +{ + vars: + "test" string => getenv("TESTFILE", 999); + "events_file" string => getenv("EVENTS_FILE", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + events: + "$(promise_name)" + when => deleted("$(test)"), + then => { + "run_processes", record_then_outcome("$(events_file)", "processes") + }; +} diff --git a/tests/reactor/tests/02_then_bundles/010_processes.sh b/tests/reactor/tests/02_then_bundles/010_processes.sh new file mode 100644 index 00000000000..0d5eebb9532 --- /dev/null +++ b/tests/reactor/tests/02_then_bundles/010_processes.sh @@ -0,0 +1,58 @@ +#!/bin/sh + +# check that processes promises in a "then" bundle are evaluated again on +# every event, with the process table of the event: the process started +# after the first one is killed by the second one + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +TESTFILE="$WORKDIR/tmp/test" +EVENTS_FILE="$WORKDIR/tmp/events" +PID_FILE="$WORKDIR/tmp/pid" +export TESTFILE EVENTS_FILE PID_FILE + +# Start a process to be matched by the processes promise, detached from this +# shell so that it doesn't stay a zombie of it once killed +start_process() { + ( sleep 978654 & echo $! > "$PID_FILE" ) +} + +wait_for_process_killed() { + if ! timeout 60 sh -c 'while kill -0 "$1" 2>/dev/null; do sleep 0.1; done' _ "$(cat "$PID_FILE")" + then + echo "Process $(cat "$PID_FILE") not killed, reactor log:" >&2 + cat "$LOG" >&2 + return 1 + fi +} + +start_process +touch "$TESTFILE" + +start_reactor "$1" + +wait_for_watchers 1 + +# first event +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +# like in cf-agent, killing the process defines both classes +wait_for_lines "$EVENTS_FILE" 2 +check_event "$EVENTS_FILE" 1 "processes kept" +check_event "$EVENTS_FILE" 2 "processes repaired" +wait_for_process_killed + +# a new process, not in the process table of the first event +start_process + +# second event: recreate the file and wait for the watcher to see it before +# deleting it again +touch "$TESTFILE" +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 4 +check_event "$EVENTS_FILE" 3 "processes kept" +check_event "$EVENTS_FILE" 4 "processes repaired" +wait_for_process_killed diff --git a/tests/reactor/tests/02_then_bundles/011_services.cf b/tests/reactor/tests/02_then_bundles/011_services.cf new file mode 100644 index 00000000000..900f05466a6 --- /dev/null +++ b/tests/reactor/tests/02_then_bundles/011_services.cf @@ -0,0 +1,46 @@ +####################################################### +# +# Test services promises in a "then" bundle +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +body service_method record_service(events_file, service) +{ + service_bundle => record_event( + "$(events_file)", "services $(service) $(this.service_policy)" + ); +} + +bundle agent run_services(events_file) +{ + services: + "reactor_test_service" + service_policy => "start", + service_method => record_service( + "$(events_file)", "reactor_test_service" + ); +} + +bundle reactor test +{ + vars: + "test" string => getenv("TESTFILE", 999); + "events_file" string => getenv("EVENTS_FILE", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + events: + "$(promise_name)" + when => deleted("$(test)"), + then => run_services("$(events_file)"); +} diff --git a/tests/reactor/tests/02_then_bundles/011_services.sh b/tests/reactor/tests/02_then_bundles/011_services.sh new file mode 100644 index 00000000000..66b04a7778c --- /dev/null +++ b/tests/reactor/tests/02_then_bundles/011_services.sh @@ -0,0 +1,33 @@ +#!/bin/sh + +# check that services promises in a "then" bundle are evaluated on every +# event, running the bundle of their service method + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +TESTFILE="$WORKDIR/tmp/test" +EVENTS_FILE="$WORKDIR/tmp/events" + +export TESTFILE EVENTS_FILE + +touch "$TESTFILE" + +start_reactor "$1" + +wait_for_watchers 1 + +# first event +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 1 +check_event "$EVENTS_FILE" 1 "services reactor_test_service start" + +# second event: recreate the file and wait for the watcher to see it before +# deleting it again +touch "$TESTFILE" +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 2 +check_event "$EVENTS_FILE" 2 "services reactor_test_service start" diff --git a/tests/reactor/tests/02_then_bundles/012_commands.cf b/tests/reactor/tests/02_then_bundles/012_commands.cf new file mode 100644 index 00000000000..55579d73934 --- /dev/null +++ b/tests/reactor/tests/02_then_bundles/012_commands.cf @@ -0,0 +1,55 @@ +####################################################### +# +# Test commands promises in a "then" bundle +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +body contain in_shell +{ + useshell => "useshell"; +} + +bundle agent run_commands(events_file, output, module_file) +{ + commands: + "echo ran >> '$(output)'" + contain => in_shell, + classes => then_outcome("commands"); + + # defines reactor_test.value from the output of the command + "/bin/cat $(module_file)" module => "true"; + + reports: + "$(with): commands module value $(reactor_test.value)" + with => strftime("localtime", "%Y-%m-%d %H:%M:%S", now()), + report_to_file => "$(events_file)"; +} + +bundle reactor test +{ + vars: + "test" string => getenv("TESTFILE", 999); + "events_file" string => getenv("EVENTS_FILE", 999); + "output" string => getenv("OUTPUT_FILE", 999); + "module_file" string => getenv("MODULE_FILE", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + events: + "$(promise_name)" + when => deleted("$(test)"), + then => { + run_commands("$(events_file)", "$(output)", "$(module_file)"), + record_then_outcome("$(events_file)", "commands"), + }; +} diff --git a/tests/reactor/tests/02_then_bundles/012_commands.sh b/tests/reactor/tests/02_then_bundles/012_commands.sh new file mode 100644 index 00000000000..ebe2a6c9bc2 --- /dev/null +++ b/tests/reactor/tests/02_then_bundles/012_commands.sh @@ -0,0 +1,42 @@ +#!/bin/sh + +# check that commands promises in a "then" bundle are run again on every +# event, and that the variables of a module command are the ones of its +# output of the event + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +TESTFILE="$WORKDIR/tmp/test" +EVENTS_FILE="$WORKDIR/tmp/events" +OUTPUT_FILE="$WORKDIR/tmp/output" +MODULE_FILE="$WORKDIR/tmp/module" +export TESTFILE EVENTS_FILE OUTPUT_FILE MODULE_FILE + +printf "^context=reactor_test\n=value=first\n" > "$MODULE_FILE" +touch "$TESTFILE" + +start_reactor "$1" + +wait_for_watchers 1 + +# first event +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 2 +check_event "$EVENTS_FILE" 1 "commands module value first" +check_event "$EVENTS_FILE" 2 "commands repaired" +[ "$(cat "$OUTPUT_FILE")" = "ran" ] + +printf "^context=reactor_test\n=value=second\n" > "$MODULE_FILE" + +# second event: recreate the file and wait for the watcher to see it before +# deleting it again +touch "$TESTFILE" +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 4 +check_event "$EVENTS_FILE" 3 "commands module value second" +check_event "$EVENTS_FILE" 4 "commands repaired" +[ "$(cat "$OUTPUT_FILE")" = "$(printf 'ran\nran')" ] diff --git a/tests/reactor/tests/02_then_bundles/013_storage.cf b/tests/reactor/tests/02_then_bundles/013_storage.cf new file mode 100644 index 00000000000..9968a0e9c14 --- /dev/null +++ b/tests/reactor/tests/02_then_bundles/013_storage.cf @@ -0,0 +1,44 @@ +####################################################### +# +# Test storage promises in a "then" bundle +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +body volume any_free_space +{ + freespace => "1k"; +} + +bundle agent run_storage +{ + storage: + "$(sys.workdir)" + volume => any_free_space, + classes => then_outcome("storage"); +} + +bundle reactor test +{ + vars: + "test" string => getenv("TESTFILE", 999); + "events_file" string => getenv("EVENTS_FILE", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + events: + "$(promise_name)" + when => deleted("$(test)"), + then => { + "run_storage", record_then_outcome("$(events_file)", "storage") + }; +} diff --git a/tests/reactor/tests/02_then_bundles/013_storage.sh b/tests/reactor/tests/02_then_bundles/013_storage.sh new file mode 100644 index 00000000000..18be5f74f7e --- /dev/null +++ b/tests/reactor/tests/02_then_bundles/013_storage.sh @@ -0,0 +1,32 @@ +#!/bin/sh + +# check that storage promises in a "then" bundle are evaluated on every event + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +TESTFILE="$WORKDIR/tmp/test" +EVENTS_FILE="$WORKDIR/tmp/events" + +export TESTFILE EVENTS_FILE + +touch "$TESTFILE" + +start_reactor "$1" + +wait_for_watchers 1 + +# first event +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 1 +check_event "$EVENTS_FILE" 1 "storage kept" + +# second event: recreate the file and wait for the watcher to see it before +# deleting it again +touch "$TESTFILE" +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 2 +check_event "$EVENTS_FILE" 2 "storage kept" diff --git a/tests/reactor/tests/02_then_bundles/014_databases.cf b/tests/reactor/tests/02_then_bundles/014_databases.cf new file mode 100644 index 00000000000..881c8cda5e2 --- /dev/null +++ b/tests/reactor/tests/02_then_bundles/014_databases.cf @@ -0,0 +1,49 @@ +####################################################### +# +# Test databases promises in a "then" bundle +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +# No database server is running, so the promise can't be kept, but it is +# evaluated +body database_server no_server +{ + db_server_owner => "nobody"; + db_server_password => ""; + db_server_host => "localhost"; + db_server_type => "postgres"; + db_server_connection_db => "postgres"; +} + +bundle agent run_databases +{ + databases: + "reactor_test" + database_operation => "create", + database_type => "sql", + database_server => no_server; +} + +bundle reactor test +{ + vars: + "test" string => getenv("TESTFILE", 999); + "events_file" string => getenv("EVENTS_FILE", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + events: + "$(promise_name)" + when => deleted("$(test)"), + then => "run_databases"; +} diff --git a/tests/reactor/tests/02_then_bundles/014_databases.sh b/tests/reactor/tests/02_then_bundles/014_databases.sh new file mode 100644 index 00000000000..02c7712e726 --- /dev/null +++ b/tests/reactor/tests/02_then_bundles/014_databases.sh @@ -0,0 +1,32 @@ +#!/bin/sh + +# check that databases promises in a "then" bundle are evaluated on every +# event. Like in cf-agent, no outcome classes are defined when the server +# can't be connected to, so this looks for the error of each evaluation. + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +TESTFILE="$WORKDIR/tmp/test" +EVENTS_FILE="$WORKDIR/tmp/events" + +export TESTFILE EVENTS_FILE + +touch "$TESTFILE" + +start_reactor "$1" + +wait_for_watchers 1 + +# first event +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_log "Could not connect to the sql_db server for 'reactor_test'" + +# second event: recreate the file and wait for the watcher to see it before +# deleting it again +touch "$TESTFILE" +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_log "Could not connect to the sql_db server for 'reactor_test'" diff --git a/tests/reactor/tests/02_then_bundles/015_reports.cf b/tests/reactor/tests/02_then_bundles/015_reports.cf new file mode 100644 index 00000000000..7d962d2129e --- /dev/null +++ b/tests/reactor/tests/02_then_bundles/015_reports.cf @@ -0,0 +1,41 @@ +####################################################### +# +# Test reports promises in a "then" bundle +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +bundle agent run_reports(events_file, value_file) +{ + vars: + "value" string => readfile("$(value_file)"); + + reports: + "$(with): reports $(value)" + with => strftime("localtime", "%Y-%m-%d %H:%M:%S", now()), + report_to_file => "$(events_file)"; +} + +bundle reactor test +{ + vars: + "test" string => getenv("TESTFILE", 999); + "events_file" string => getenv("EVENTS_FILE", 999); + "value_file" string => getenv("VALUE_FILE", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + events: + "$(promise_name)" + when => deleted("$(test)"), + then => run_reports("$(events_file)", "$(value_file)"); +} diff --git a/tests/reactor/tests/02_then_bundles/015_reports.sh b/tests/reactor/tests/02_then_bundles/015_reports.sh new file mode 100644 index 00000000000..44b8687dccd --- /dev/null +++ b/tests/reactor/tests/02_then_bundles/015_reports.sh @@ -0,0 +1,36 @@ +#!/bin/sh + +# check that reports promises in a "then" bundle are made again on every +# event, with the values of the event + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +TESTFILE="$WORKDIR/tmp/test" +EVENTS_FILE="$WORKDIR/tmp/events" +VALUE_FILE="$WORKDIR/tmp/value" +export TESTFILE EVENTS_FILE VALUE_FILE + +printf "first" > "$VALUE_FILE" +touch "$TESTFILE" + +start_reactor "$1" + +wait_for_watchers 1 + +# first event +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 1 +check_event "$EVENTS_FILE" 1 "reports first" + +printf "second" > "$VALUE_FILE" + +# second event: recreate the file and wait for the watcher to see it before +# deleting it again +touch "$TESTFILE" +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 2 +check_event "$EVENTS_FILE" 2 "reports second" diff --git a/tests/reactor/tests/02_then_bundles/016_custom.cf b/tests/reactor/tests/02_then_bundles/016_custom.cf new file mode 100644 index 00000000000..7238fd37a83 --- /dev/null +++ b/tests/reactor/tests/02_then_bundles/016_custom.cf @@ -0,0 +1,47 @@ +####################################################### +# +# Test custom promises in a "then" bundle +# +####################################################### +body common control +{ + inputs => { "../../lib.cf" }; + bundlesequence => { "test" }; + version => "1.0"; +} + +body when deleted(file) +{ + file_deleted => "$(file)"; +} + +promise agent reactor_test +{ + interpreter => "/bin/sh"; + path => "$(sys.workdir)/modules/promises/reactor_test.sh"; +} + +bundle agent run_custom(output) +{ + reactor_test: + "$(output)" + message => "ran", + classes => then_outcome("custom"); +} + +bundle reactor test +{ + vars: + "test" string => getenv("TESTFILE", 999); + "events_file" string => getenv("EVENTS_FILE", 999); + "output" string => getenv("OUTPUT_FILE", 999); + "promise_name" string => basename("$(this.promise_filename)", ".cf"); + + events: + "$(promise_name)" + when => deleted("$(test)"), + then => { + run_custom("$(output)"), + record_then_outcome("$(events_file)", "custom"), + }; +} diff --git a/tests/reactor/tests/02_then_bundles/016_custom.sh b/tests/reactor/tests/02_then_bundles/016_custom.sh new file mode 100644 index 00000000000..f21069abbe6 --- /dev/null +++ b/tests/reactor/tests/02_then_bundles/016_custom.sh @@ -0,0 +1,81 @@ +#!/bin/sh + +# check that custom promises in a "then" bundle are evaluated again on every +# event, with a promise module started for the event and terminated after it + +set -ex + +. "$(dirname "$0")/../../lib.sh" + +TESTFILE="$WORKDIR/tmp/test" +EVENTS_FILE="$WORKDIR/tmp/events" +OUTPUT_FILE="$WORKDIR/tmp/output" +MODULE_LOG="$WORKDIR/tmp/module_log" +export TESTFILE EVENTS_FILE OUTPUT_FILE MODULE_LOG + +# A promise module appending the message attribute to the promiser file, and +# its start and termination to $MODULE_LOG +mkdir -p "$WORKDIR/modules/promises" +cat > "$WORKDIR/modules/promises/reactor_test.sh" <<'MODULE' +echo "started" >> "$MODULE_LOG" + +# skip the header of the agent, up to the empty line +while read -r line && [ -n "$line" ]; do :; done +echo "reactor_test 0.0.1 v1 line_based" +echo "" + +while true; do + operation="" + promiser="" + message="" + while read -r line && [ -n "$line" ]; do + case "$line" in + operation=*) operation="${line#*=}" ;; + promiser=*) promiser="${line#*=}" ;; + attribute_message=*) message="${line#*=}" ;; + esac + done + + case "$operation" in + validate_promise) + result="valid" ;; + evaluate_promise) + if echo "$message" >> "$promiser"; then + result="repaired" + else + result="not_kept" + fi ;; + terminate) + echo "terminated" >> "$MODULE_LOG" + printf 'operation=terminate\nresult=success\n\n' + exit 0 ;; + *) + result="error" ;; + esac + printf 'operation=%s\nresult=%s\n\n' "$operation" "$result" +done +MODULE + +touch "$TESTFILE" + +start_reactor "$1" + +wait_for_watchers 1 + +# first event +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 1 +check_event "$EVENTS_FILE" 1 "custom repaired" +[ "$(cat "$OUTPUT_FILE")" = "ran" ] +[ "$(cat "$MODULE_LOG")" = "$(printf 'started\nterminated')" ] + +# second event: recreate the file and wait for the watcher to see it before +# deleting it again +touch "$TESTFILE" +wait_for_log "File '$TESTFILE' is present, watching for its deletion" +rm "$TESTFILE" +wait_for_lines "$EVENTS_FILE" 2 +check_event "$EVENTS_FILE" 2 "custom repaired" +[ "$(cat "$OUTPUT_FILE")" = "$(printf 'ran\nran')" ] +[ "$(cat "$MODULE_LOG")" = "$(printf 'started\nterminated\nstarted\nterminated')" ]