. "$BST_LIB" # liblognorm 2.0.6.master (SRCREV 46c6ee4, meta-oe recipes-extended) -- Adiscon's # samples-based log normaliser: it turns a free-form log line into a structured # event by matching it against a RULEBASE and emitting the extracted fields as # JSON. It is the third of the rsyslog family on this image, after libestr # (81-libestr.test, its string library) and libfastjson (81-libfastjson.test, # which does the JSON serialisation here). # # Unlike those two it ships a CLI -- /usr/bin/lognormalizer -- so this suite is # driven end-to-end through the tool rather than through a cross-linked probe: # the payload is a rulebase and a set of sample lines, and the assertion is the # JSON that comes back. # # --------------------------------------------------------------------------- # WHERE THE EXPECTED VALUES COME FROM, AND WHY THEY ARE NOT THIS ARM'S OUTPUT # # Every expected parse below is UPSTREAM'S OWN RECORDED ANSWER, lifted from # liblognorm's tests/ corpus at the release this image builds -- field_number.sh, # field_hexnumber.sh, field_float.sh, field_mac48.sh, field_kernel_timestamp.sh, # field_duration.sh, field_name_value.sh and field_v2-iptables.sh. None of it was # read off a target run and pasted back, which is the failure mode a port suite # is most exposed to: BRINTOS-217 is the precedent, where oniguruma compiled and # packaged perfectly and then returned silently wrong offsets, so a suite written # from its own output would have gone green on exactly the defect the port # existed to find. # # The serialisation is pinned the same way, from libfastjson's source rather than # from observation: fjson_object_to_json_string() passes FJSON_TO_STRING_SPACED, # and under that flag json_object.c writes '{', ' ', '"key": ', value, ' }'. So a # one-field event is exactly `{ "num": "1234" }`, and the json/exact-format case # below asserts that literally. Two consequences worth knowing before reading a # red run: a '/' inside a value comes back ESCAPED as '\/' (upstream's # field_rest.sh records the same), and keys appear in INSERTION order, which for # these rules is parse order. # # THE UNMATCHED LINE IS THE POINT OF THE SUITE, not an edge case. liblognorm does # not drop a line it cannot parse: src/lognormalizer.c classifies an event as # unparsed iff the returned object carries "unparsed-data", counts it separately, # and still prints it (OUTPUT_UNPARSED_RECS is on by default) with "originalmsg" # holding the whole input. The unparsed/* cases below assert all three halves -- # the record is emitted, it names the original line, and -H counts it as unparsed # -- because "silently dropped" and "reported as unparsed" are indistinguishable # in a summary that only counts parsed events. # # The suite reads each command's output ONCE into a variable and matches with # builtin `case`, rather than re-running lognormalizer per assertion: on this # runtime a multi-MB wasm module's start-up dominates, and the cjson suite next # door was killed by the per-suite bound at 156 cases written the other way. Six # lognormalizer invocations cover 60-odd cases here. # --------------------------------------------------------------------------- tool=/usr/bin/lognormalizer lib=/usr/lib/liblognorm.a hdr=/usr/include/liblognorm.h pc=/usr/lib/pkgconfig/lognorm.pc rb=$BST_DATA/lognorm-emcraft.rulebase samples=$BST_DATA/lognorm-emcraft.samples nl=' ' # has -- ok iff contains the literal . has() { case "$3" in *"$2"*) bst_ok "$1" ;; *) bst_notok "$1" "not found: $2 -- in: $(_bst_1line "$3")" ;; esac; } # hasnt -- ok iff it does NOT. hasnt(){ case "$3" in *"$2"*) bst_notok "$1" "unexpectedly present: $2" ;; *) bst_ok "$1" ;; esac; } hasfile() { if [ -s "$2" ]; then bst_ok "$1"; else bst_notok "$1" "absent or empty: $2"; fi; } # An image built without liblognorm is a coverage gap, not a failure: skip once # and stop rather than reporting 60 reds for an absent package. if [ ! -x "$tool" ]; then bst_skip liblognorm/present "lognormalizer not installed on this image" exit 0 fi bst_ok liblognorm/present # --- the shipped payload ----------------------------------------------------- hasfile liblognorm/tool "$tool" hasfile liblognorm/archive "$lib" hasfile liblognorm/header "$hdr" hasfile liblognorm/pkgconfig "$pc" hasfile liblognorm/rulebase "$rb" hasfile liblognorm/samples "$samples" # There is deliberately no no-shared-object case: shared objects are refused # distro-wide on this arm, so an absent liblognorm.so is the distro's property # and not this port's. pctext=$(cat "$pc" 2>/dev/null) has liblognorm/pc-name "Name: lognorm" "$pctext" has liblognorm/pc-version "Version: 2.0.6" "$pctext" has liblognorm/pc-libs "-llognorm" "$pctext" # The dependency pair this port rests on, declared in its own pkgconfig: the # public Requires is libfastjson (its types appear in liblognorm.h) while libestr # is Libs.private (used internally only). Both are already on this image. has liblognorm/pc-requires-fastjson "Requires: libfastjson" "$pctext" has liblognorm/pc-private-estr "Libs.private: -lestr" "$pctext" hdrtext=$(cat "$hdr" 2>/dev/null) has liblognorm/hdr-normalize "ln_normalize" "$hdrtext" has liblognorm/hdr-loadsamples "ln_loadSamples" "$hdrtext" has liblognorm/hdr-initctx "ln_initCtx" "$hdrtext" has liblognorm/hdr-exitctx "ln_exitCtx" "$hdrtext" has liblognorm/hdr-version "ln_version" "$hdrtext" # REGEXP SUPPORT IS OFF, and that is a recipe decision rather than a defect: the # stock PACKAGECONFIG is "testbench tools" and does not include regexp, so the # recipe never pulls pcre2 and lognorm-features.h ships LOGNORM_REGEX_SUPPORTED # behind `#if 0`. Asserted so that turning it on later is a CHANGED case here # rather than a silent widening of what this image can parse. feat=$(cat /usr/include/lognorm-features.h 2>/dev/null) has liblognorm/regex-disabled "#if 0" "$feat" # --- symbols in the shipped archive ------------------------------------------ arcsyms=$(grep -ao 'ln_[A-Za-z0-9_]*' "$lib" 2>/dev/null | sort -u) hasline() { case "$nl$3$nl" in *"$nl$2$nl"*) bst_ok "$1" ;; *) bst_notok "$1" "not in archive: $2" ;; esac; } hasline liblognorm/sym-normalize ln_normalize "$arcsyms" hasline liblognorm/sym-initctx ln_initCtx "$arcsyms" hasline liblognorm/sym-loadsamples ln_loadSamples "$arcsyms" # --- the tool itself --------------------------------------------------------- vout=$("$tool" -V 2>&1) has liblognorm/version-banner "2.0.6" "$vout" # An unreadable rulebase is the tool's one deterministic hard error: # ln_loadSamples fails, main prints "fatal error: cannot load rulebase" and # exit(1)s BEFORE reaching the read loop. Deliberately not tested by omitting -r # instead: with no -r at all usedRB stays 0, neither load branch runs, and the # tool proceeds to normalize() against an empty rulebase -- i.e. it BLOCKS on # stdin rather than failing, which in a suite is a timeout, not a case. stdin is # closed here anyway so a future change of that behaviour cannot hang the run. nrb=$("$tool" -r /nonexistent/lognorm.rulebase < /dev/null 2>&1); nrc=$? check_eq liblognorm/bad-rulebase-rc 1 "$nrc" has liblognorm/bad-rulebase-msg "cannot load rulebase" "$nrb" # ============================================================================= # THE STAGED RULEBASE -- the ticket's pass criterion # # One invocation over data/lognorm-emcraft.samples: 9 lines, 8 of which match a # rule and 1 of which matches none. stdout is the 9 JSON events in input order; # -H puts the tally on stderr, so the two are captured separately and the tally # cannot be mistaken for an event. # ============================================================================= outf=${TMPDIR:-/tmp}/bst-lognorm-out.$$ errf=${TMPDIR:-/tmp}/bst-lognorm-err.$$ ( "$tool" -r "$rb" -e json -H < "$samples" > "$outf" 2> "$errf" ) prc=$? out=$(cat "$outf" 2>/dev/null) err=$(cat "$errf" 2>/dev/null) rm -f "$outf" "$errf" check_eq liblognorm/run-exit0 0 "$prc" # Split the run into one variable per input line, without forking per line. i=0 while IFS= read -r ln; do i=$((i + 1)) eval "ev$i=\$ln" done < {"num": "1234"} has json/number '"num": "1234"' "$ev1" # The serialiser's exact shape, pinned once from libfastjson's SPACED flag. check_eq json/exact-format '{ "num": "1234" }' "$ev1" # tests/field_hexnumber.sh -> {"num": "0x1234"} has json/hexnumber '"num": "0x1234"' "$ev2" # tests/field_float.sh -> {"num": "15.9"} has json/float '"num": "15.9"' "$ev3" # tests/field_mac48.sh -> {"field": "f0:f6:1c:5f:cc:a2"} has json/mac48 '"field": "f0:f6:1c:5f:cc:a2"' "$ev4" # tests/field_kernel_timestamp.sh-> {"timestamp": "[12345.123456]"} has json/kernel-ts '"timestamp": "[12345.123456]"' "$ev5" # tests/field_duration.sh -> {"field": "0:00:42"} has json/duration '"field": "0:00:42"' "$ev6" # tests/field_name_value.sh -> a NESTED object, not a flat string. This is the # case that proves the JSON layer really is building sub-objects rather than # stringifying: the three pairs must come back as members of "f". has json/nvl-nested '"f": {' "$ev7" has json/nvl-1 '"name1": "value1"' "$ev7" has json/nvl-2 '"name2": "value2"' "$ev7" has json/nvl-3 '"name3": "value3"' "$ev7" # tests/field_v2-iptables.sh -- the richest single parse on this image: a nested # object of 13 members from one line. Three of its properties are asserted by # name because each is a distinct behaviour rather than another key/value pair: # ipt-empty IN= carries an EMPTY STRING, not null and not a missing key. # ipt-valueless DF appears with no '=' and comes back as JSON null. # ipt-len-last LEN appears TWICE in the input (32 near the front, 12 at the # end). Upstream records "12", i.e. the later assignment wins and # replaces the earlier in place -- so this pins last-wins rather # than first-wins or a duplicated key. has json/ipt-nested '"field": {' "$ev8" has json/ipt-empty '"IN": ""' "$ev8" has json/ipt-out '"OUT": "eth0"' "$ev8" has json/ipt-src '"SRC": "176.9.56.141"' "$ev8" has json/ipt-dst '"DST": "168.192.14.3"' "$ev8" has json/ipt-tos '"TOS": "0x00"' "$ev8" has json/ipt-ttl '"TTL": "64"' "$ev8" has json/ipt-id '"ID": "39110"' "$ev8" has json/ipt-proto '"PROTO": "UDP"' "$ev8" has json/ipt-spt '"SPT": "49564"' "$ev8" has json/ipt-dpt '"DPT": "2010"' "$ev8" has json/ipt-valueless '"DF": null' "$ev8" has json/ipt-len-last '"LEN": "12"' "$ev8" hasnt json/ipt-len-first '"LEN": "32"' "$ev8" # --- THE UNMATCHED LINE -- reported, not dropped ----------------------------- # All three halves of the contract, because a tally alone cannot tell "reported # as unparsed" from "silently dropped". has unparsed/record-emitted '"unparsed-data"' "$ev9" has unparsed/original-message '"originalmsg": "this line matches no rule in the staged rulebase"' "$ev9" # and it must NOT have been mistaken for a parse hasnt unparsed/no-fields '"num"' "$ev9" # The tally on stderr, which is where -H writes it. has summary/line "9 records processed, 8 parsed, 1 unparsed" "$err" # stdout carried the events and NOT the tally -- the two streams stay separate. hasnt summary/not-on-stdout "records processed" "$out" # ============================================================================= # UPSTREAM'S RULEBASES, BYTE-IDENTICAL -- the exact unparsed-data values # # The staged rulebase above adds a literal keyword to each upstream rule so that # one file can hold all eight without the parse DAG cross-matching. That is safe # for the PARSED answers, but it moves the offset at which a partial match gives # up -- so the exact "unparsed-data" text is asserted here instead, against # rulebases written exactly as upstream's tests/ has them. # ============================================================================= rb2=${TMPDIR:-/tmp}/bst-lognorm-up.$$ in2=${TMPDIR:-/tmp}/bst-lognorm-upin.$$ # tests/field_number.sh, verbatim printf 'version=2\nrule=:here is a number %%num:number%% in dec form\n' > "$rb2" printf '%s\n%s\n' 'here is a number 1234 in dec form' 'here is a number 1234in dec form' > "$in2" uout=$("$tool" -r "$rb2" -e json < "$in2" 2>/dev/null) j=0 while IFS= read -r ln; do j=$((j + 1)) eval "up$j=\$ln" done < "$rb2" printf '%s\n%s\n' 'f0-f6:1c:5f:cc-a2' 'f0:f6:1c:xf:cc:a2' > "$in2" mout=$("$tool" -r "$rb2" -e json < "$in2" 2>/dev/null) k=0 while IFS= read -r ln; do k=$((k + 1)) eval "mc$k=\$ln" done </dev/null) Ponly=$("$tool" -r "$rb" -e json -P < "$samples" 2>/dev/null) np=0 while IFS= read -r ln; do np=$((np + 1)); done </dev/null) has encoder/csv-value '"1234"' "$csv" # The negative control for the paragraph above: without -E the same command is # empty. Pinned so that a future release which starts defaulting the field list # changes a case here rather than silently altering what -e csv means. csvbare=$("$tool" -r "$rb" -e csv < "$samples" 2>/dev/null) hasnt encoder/csv-needs-E "1234" "$csvbare" rm -f "$rb2" "$in2"