From 20a01d1592c8e83d2dfe51cbd192857537ae7d4e Mon Sep 17 00:00:00 2001 From: Simon Marchi Date: Wed, 18 Mar 2020 12:11:18 -0400 Subject: [PATCH] CLI: add-trigger: add --capture option to `on-event` condition MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit This patch adds a `--capture` option to the `on-event` condition, to allow specifying the values of payload or context fields to capture. The filter parser is re-used, as the syntax of the capture expression is a subset of the filter expression syntax. Allowed forms are: - payload field name: foo - context field name: $ctx.foo - app-specific field name: $app.foo:bar After any of these, array indexing can be used. For example, `$ctx.foo[2]`. Change-Id: I6246148634053b32294956d1f7a03f2798fd1d71 Signed-off-by: Simon Marchi Signed-off-by: Philippe Proulx Signed-off-by: Jérémie Galarneau Depends-on: lttng-ust: I5a800fc92e588c2a6a0e26282b0ad5f31c044479 --- src/bin/lttng/commands/add_trigger.c | 377 ++++++++++++++++-- src/common/filter/filter-ir.h | 65 +++ .../tools/trigger/test_add_trigger_cli | 75 +++- 3 files changed, 484 insertions(+), 33 deletions(-) diff --git a/src/bin/lttng/commands/add_trigger.c b/src/bin/lttng/commands/add_trigger.c index 83fd1c447..19a1c59e7 100644 --- a/src/bin/lttng/commands/add_trigger.c +++ b/src/bin/lttng/commands/add_trigger.c @@ -19,6 +19,9 @@ /* For lttng_event_rule_type_str(). */ #include #include +#include "common/filter/filter-ast.h" +#include "common/filter/filter-ir.h" +#include "common/dynamic-array.h" #if (LTTNG_SYMBOL_NAME_LEN == 256) #define LTTNG_SYMBOL_NAME_LEN_SCANF_IS_A_BROKEN_API "255" @@ -65,6 +68,8 @@ enum { OPT_CTRL_URL, OPT_URL, OPT_PATH, + + OPT_CAPTURE, }; static const struct argpar_opt_descr event_rule_opt_descrs[] = { @@ -88,6 +93,9 @@ static const struct argpar_opt_descr event_rule_opt_descrs[] = { { OPT_SYSCALL, '\0', "syscall" }, { OPT_TRACEPOINT, '\0', "tracepoint" }, + /* Capture descriptor */ + { OPT_CAPTURE, '\0', "capture", true }, + ARGPAR_OPT_DESCR_SENTINEL }; @@ -291,9 +299,233 @@ end: return ret; } -static struct lttng_event_rule *parse_event_rule(int *argc, const char ***argv) +static +struct lttng_event_expr *ir_op_load_expr_to_event_expr( + const struct ir_load_expression *load_exp, const char *capture_str) +{ + char *provider_name = NULL; + struct lttng_event_expr *event_expr = NULL; + const struct ir_load_expression_op *load_expr_op = load_exp->child; + + switch (load_expr_op->type) { + case IR_LOAD_EXPRESSION_GET_PAYLOAD_ROOT: + case IR_LOAD_EXPRESSION_GET_CONTEXT_ROOT: + { + const char *field_name; + + load_expr_op = load_expr_op->next; + assert(load_expr_op); + assert(load_expr_op->type == IR_LOAD_EXPRESSION_GET_SYMBOL); + field_name = load_expr_op->u.symbol; + assert(field_name); + + event_expr = load_expr_op->type == IR_LOAD_EXPRESSION_GET_PAYLOAD_ROOT ? + lttng_event_expr_event_payload_field_create(field_name) : + lttng_event_expr_channel_context_field_create(field_name); + if (!event_expr) { + ERR("Failed to create %s event expression: field name = `%s`.", + load_expr_op->type == IR_LOAD_EXPRESSION_GET_PAYLOAD_ROOT ? + "payload field" : "channel context", + field_name); + goto error; + } + + break; + } + case IR_LOAD_EXPRESSION_GET_APP_CONTEXT_ROOT: + { + const char *colon; + const char *type_name; + const char *field_name; + + load_expr_op = load_expr_op->next; + assert(load_expr_op); + assert(load_expr_op->type == IR_LOAD_EXPRESSION_GET_SYMBOL); + field_name = load_expr_op->u.symbol; + assert(field_name); + + /* + * The field name needs to be of the form PROVIDER:TYPE. We + * split it here. + */ + colon = strchr(field_name, ':'); + if (!colon) { + ERR("Invalid app-specific context field name: missing colon in `%s`.", + field_name); + goto error; + } + + type_name = colon + 1; + if (*type_name == '\0') { + ERR("Invalid app-specific context field name: missing type name after colon in `%s`.", + field_name); + goto error; + } + + provider_name = strndup(field_name, colon - field_name); + if (!provider_name) { + PERROR("Failed to allocate field name string"); + goto error; + } + + event_expr = lttng_event_expr_app_specific_context_field_create( + provider_name, type_name); + if (!event_expr) { + ERR("Failed to create app-specific context field event expression: provider name = `%s`, type name = `%s`", + provider_name, type_name); + goto error; + } + + break; + } + default: + ERR("%s: unexpected load expr type %d.", __func__, + load_expr_op->type); + abort(); + } + + load_expr_op = load_expr_op->next; + + /* There may be a single array index after that. */ + if (load_expr_op->type == IR_LOAD_EXPRESSION_GET_INDEX) { + struct lttng_event_expr *index_event_expr; + const uint64_t index = load_expr_op->u.index; + + index_event_expr = lttng_event_expr_array_field_element_create(event_expr, index); + if (!index_event_expr) { + ERR("Failed to create array field element event expression."); + goto error; + } + + event_expr = index_event_expr; + load_expr_op = load_expr_op->next; + } + + switch (load_expr_op->type) { + case IR_LOAD_EXPRESSION_LOAD_FIELD: + /* + * This is what we expect, IR_LOAD_EXPRESSION_LOAD_FIELD is + * always found at the end of the chain. + */ + break; + case IR_LOAD_EXPRESSION_GET_SYMBOL: + ERR("While parsing expression `%s`: Capturing subfields is not supported.", + capture_str); + goto error; + + default: + ERR("%s: unexpected load expression operator %s.", __func__, + ir_load_expression_type_str(load_expr_op->type)); + abort(); + } + + goto end; + +error: + lttng_event_expr_destroy(event_expr); + event_expr = NULL; + +end: + free(provider_name); + + return event_expr; +} + +static +struct lttng_event_expr *ir_op_load_to_event_expr( + const struct ir_op *ir, const char *capture_str) +{ + struct lttng_event_expr *event_expr = NULL; + + assert(ir->op == IR_OP_LOAD); + + switch (ir->data_type) { + case IR_DATA_EXPRESSION: + { + const struct ir_load_expression *ir_load_expr = + ir->u.load.u.expression; + + event_expr = ir_op_load_expr_to_event_expr( + ir_load_expr, capture_str); + break; + } + default: + ERR("%s: unexpected data type: %s.", __func__, + ir_data_type_str(ir->data_type)); + abort(); + } + + return event_expr; +} + +static +const char *ir_operator_type_human_str(enum ir_op_type op) +{ + const char *name; + + switch (op) { + case IR_OP_BINARY: + name = "Binary"; + break; + case IR_OP_UNARY: + name = "Unary"; + break; + case IR_OP_LOGICAL: + name = "Logical"; + break; + default: + abort(); + } + + return name; +} + +static +struct lttng_event_expr *ir_op_root_to_event_expr(const struct ir_op *ir, + const char *capture_str) +{ + struct lttng_event_expr *event_expr = NULL; + + assert(ir->op == IR_OP_ROOT); + ir = ir->u.root.child; + + switch (ir->op) { + case IR_OP_LOAD: + event_expr = ir_op_load_to_event_expr(ir, capture_str); + break; + case IR_OP_BINARY: + case IR_OP_UNARY: + case IR_OP_LOGICAL: + ERR("While parsing expression `%s`: %s operators are not allowed in capture expressions.", + capture_str, + ir_operator_type_human_str(ir->op)); + break; + default: + ERR("%s: unexpected IR op type: %s.", __func__, + ir_op_type_str(ir->op)); + abort(); + } + + return event_expr; +} + +static +void destroy_event_expr(void *ptr) +{ + lttng_event_expr_destroy(ptr); +} + +struct parse_event_rule_res { + /* Owned by this. */ + struct lttng_event_rule *er; + + /* Array of `struct lttng_event_expr *` */ + struct lttng_dynamic_pointer_array capture_descriptors; +}; + +static +struct parse_event_rule_res parse_event_rule(int *argc, const char ***argv) { - struct lttng_event_rule *er = NULL; enum lttng_domain_type domain_type = LTTNG_DOMAIN_NONE; enum lttng_event_rule_type event_rule_type = LTTNG_EVENT_RULE_TYPE_UNKNOWN; @@ -303,6 +535,9 @@ static struct lttng_event_rule *parse_event_rule(int *argc, const char ***argv) int consumed_args = -1; struct lttng_kernel_probe_location *kernel_probe_location = NULL; struct lttng_userspace_probe_location *userspace_probe_location = NULL; + struct parse_event_rule_res res = { 0 }; + struct lttng_event_expr *event_expr = NULL; + struct filter_parser_ctx *parser_ctx = NULL; /* Was the -a/--all flag provided? */ bool all_events = false; @@ -324,6 +559,8 @@ static struct lttng_event_rule *parse_event_rule(int *argc, const char ***argv) char *loglevel_str = NULL; bool loglevel_only = false; + lttng_dynamic_pointer_array_init(&res.capture_descriptors, + destroy_event_expr); state = argpar_state_create(*argc, *argv, event_rule_opt_descrs); if (!state) { ERR("Failed to allocate an argpar state."); @@ -455,6 +692,45 @@ static struct lttng_event_rule *parse_event_rule(int *argc, const char ***argv) loglevel_only = item_opt->descr->id == OPT_LOGLEVEL_ONLY; break; + case OPT_CAPTURE: + { + int ret; + const char *capture_str = item_opt->arg; + + ret = filter_parser_ctx_create_from_filter_expression( + capture_str, &parser_ctx); + if (ret) { + ERR("Failed to parse capture expression `%s`.", + capture_str); + goto error; + } + + event_expr = ir_op_root_to_event_expr( + parser_ctx->ir_root, + capture_str); + if (!event_expr) { + /* + * ir_op_root_to_event_expr has printed + * an error message. + */ + goto error; + } + + ret = lttng_dynamic_pointer_array_add_pointer( + &res.capture_descriptors, + event_expr); + if (ret) { + goto error; + } + + /* + * The ownership of event expression was + * transferred to the dynamic array. + */ + event_expr = NULL; + + break; + } default: abort(); } @@ -599,15 +875,15 @@ static struct lttng_event_rule *parse_event_rule(int *argc, const char ***argv) { enum lttng_event_rule_status event_rule_status; - er = lttng_event_rule_tracepoint_create(domain_type); - if (!er) { + res.er = lttng_event_rule_tracepoint_create(domain_type); + if (!res.er) { ERR("Failed to create tracepoint event rule."); goto error; } /* Set pattern. */ event_rule_status = lttng_event_rule_tracepoint_set_pattern( - er, tracepoint_name); + res.er, tracepoint_name); if (event_rule_status != LTTNG_EVENT_RULE_STATUS_OK) { ERR("Failed to set tracepoint event rule's pattern to '%s'.", tracepoint_name); @@ -617,7 +893,7 @@ static struct lttng_event_rule *parse_event_rule(int *argc, const char ***argv) /* Set filter. */ if (filter) { event_rule_status = lttng_event_rule_tracepoint_set_filter( - er, filter); + res.er, filter); if (event_rule_status != LTTNG_EVENT_RULE_STATUS_OK) { ERR("Failed to set tracepoint event rule's filter to '%s'.", filter); @@ -631,7 +907,7 @@ static struct lttng_event_rule *parse_event_rule(int *argc, const char ***argv) for (n = 0; exclusion_list[n]; n++) { event_rule_status = lttng_event_rule_tracepoint_add_exclusion( - er, + res.er, exclusion_list[n]); if (event_rule_status != LTTNG_EVENT_RULE_STATUS_OK) { @@ -660,10 +936,12 @@ static struct lttng_event_rule *parse_event_rule(int *argc, const char ***argv) if (loglevel_only) { event_rule_status = lttng_event_rule_tracepoint_set_log_level( - er, loglevel); + res.er, + loglevel); } else { event_rule_status = lttng_event_rule_tracepoint_set_log_level_range_lower_bound( - er, loglevel); + res.er, + loglevel); } if (event_rule_status != LTTNG_EVENT_RULE_STATUS_OK) { @@ -679,8 +957,8 @@ static struct lttng_event_rule *parse_event_rule(int *argc, const char ***argv) int ret; enum lttng_event_rule_status event_rule_status; - er = lttng_event_rule_kprobe_create(); - if (!er) { + res.er = lttng_event_rule_kprobe_create(); + if (!res.er) { ERR("Failed to create kprobe event rule."); goto error; } @@ -691,14 +969,14 @@ static struct lttng_event_rule *parse_event_rule(int *argc, const char ***argv) goto error; } - event_rule_status = lttng_event_rule_kprobe_set_name(er, tracepoint_name); + event_rule_status = lttng_event_rule_kprobe_set_name(res.er, tracepoint_name); if (event_rule_status != LTTNG_EVENT_RULE_STATUS_OK) { ERR("Failed to set kprobe event rule's name to '%s'.", tracepoint_name); goto error; } assert(kernel_probe_location); - event_rule_status = lttng_event_rule_kprobe_set_location(er, kernel_probe_location); + event_rule_status = lttng_event_rule_kprobe_set_location(res.er, kernel_probe_location); if (event_rule_status != LTTNG_EVENT_RULE_STATUS_OK) { ERR("Failed to set kprobe event rule's location."); goto error; @@ -718,21 +996,21 @@ static struct lttng_event_rule *parse_event_rule(int *argc, const char ***argv) goto error; } - er = lttng_event_rule_uprobe_create(); - if (!er) { - ERR("Failed to create user space probe event rule."); + res.er = lttng_event_rule_uprobe_create(); + if (!res.er) { + ERR("Failed to create userspace probe event rule."); goto error; } event_rule_status = lttng_event_rule_uprobe_set_location( - er, userspace_probe_location); + res.er, userspace_probe_location); if (event_rule_status != LTTNG_EVENT_RULE_STATUS_OK) { ERR("Failed to set user space probe event rule's location."); goto error; } event_rule_status = lttng_event_rule_uprobe_set_name( - er, tracepoint_name); + res.er, tracepoint_name); if (event_rule_status != LTTNG_EVENT_RULE_STATUS_OK) { ERR("Failed to set user space probe event rule's name to '%s'.", tracepoint_name); @@ -745,14 +1023,14 @@ static struct lttng_event_rule *parse_event_rule(int *argc, const char ***argv) { enum lttng_event_rule_status event_rule_status; - er = lttng_event_rule_syscall_create(); - if (!er) { + res.er = lttng_event_rule_syscall_create(); + if (!res.er) { ERR("Failed to create syscall event rule."); goto error; } event_rule_status = lttng_event_rule_syscall_set_pattern( - er, tracepoint_name); + res.er, tracepoint_name); if (event_rule_status != LTTNG_EVENT_RULE_STATUS_OK) { ERR("Failed to set syscall event rule's pattern to '%s'.", tracepoint_name); @@ -761,7 +1039,7 @@ static struct lttng_event_rule *parse_event_rule(int *argc, const char ***argv) if (filter) { event_rule_status = lttng_event_rule_syscall_set_filter( - er, filter); + res.er, filter); if (event_rule_status != LTTNG_EVENT_RULE_STATUS_OK) { ERR("Failed to set syscall event rule's filter to '%s'.", filter); @@ -779,10 +1057,16 @@ static struct lttng_event_rule *parse_event_rule(int *argc, const char ***argv) goto end; error: - lttng_event_rule_destroy(er); - er = NULL; + lttng_event_rule_destroy(res.er); + res.er = NULL; + lttng_dynamic_pointer_array_reset(&res.capture_descriptors); end: + if (parser_ctx) { + filter_parser_ctx_free(parser_ctx); + } + + lttng_event_expr_destroy(event_expr); argpar_item_destroy(item); free(error); argpar_state_destroy(state); @@ -793,28 +1077,57 @@ end: strutils_free_null_terminated_array_of_strings(exclusion_list); lttng_kernel_probe_location_destroy(kernel_probe_location); lttng_userspace_probe_location_destroy(userspace_probe_location); - return er; + return res; } static struct lttng_condition *handle_condition_event(int *argc, const char ***argv) { - struct lttng_event_rule *er; + struct parse_event_rule_res res; struct lttng_condition *c; + size_t i; - er = parse_event_rule(argc, argv); - if (!er) { + res = parse_event_rule(argc, argv); + if (!res.er) { c = NULL; - goto end; + goto error; } - c = lttng_condition_event_rule_create(er); - lttng_event_rule_destroy(er); + c = lttng_condition_event_rule_create(res.er); + lttng_event_rule_destroy(res.er); + res.er = NULL; if (!c) { - goto end; + goto error; } + for (i = 0; i < lttng_dynamic_pointer_array_get_count(&res.capture_descriptors); + i++) { + enum lttng_condition_status status; + struct lttng_event_expr **expr = + lttng_dynamic_array_get_element( + &res.capture_descriptors.array, i); + + assert(expr); + assert(*expr); + status = lttng_condition_event_rule_append_capture_descriptor( + c, *expr); + if (status != LTTNG_CONDITION_STATUS_OK) { + goto error; + } + + /* Ownership of event expression moved to `c` */ + *expr = NULL; + } + + goto end; + +error: + lttng_condition_destroy(c); + c = NULL; + end: + lttng_dynamic_pointer_array_reset(&res.capture_descriptors); + lttng_event_rule_destroy(res.er); return c; } diff --git a/src/common/filter/filter-ir.h b/src/common/filter/filter-ir.h index d62c0ee0c..5775e8004 100644 --- a/src/common/filter/filter-ir.h +++ b/src/common/filter/filter-ir.h @@ -31,6 +31,29 @@ enum ir_data_type { IR_DATA_EXPRESSION, }; +static inline +const char *ir_data_type_str(enum ir_data_type type) +{ + switch (type) { + case IR_DATA_UNKNOWN: + return "IR_DATA_UNKNOWN"; + case IR_DATA_STRING: + return "IR_DATA_STRING"; + case IR_DATA_NUMERIC: + return "IR_DATA_NUMERIC"; + case IR_DATA_FLOAT: + return "IR_DATA_FLOAT"; + case IR_DATA_FIELD_REF: + return "IR_DATA_FIELD_REF"; + case IR_DATA_GET_CONTEXT_REF: + return "IR_DATA_GET_CONTEXT_REF"; + case IR_DATA_EXPRESSION: + return "IR_DATA_EXPRESSION"; + default: + abort(); + } +} + enum ir_op_type { IR_OP_UNKNOWN = 0, IR_OP_ROOT, @@ -40,6 +63,27 @@ enum ir_op_type { IR_OP_LOGICAL, }; +static inline +const char *ir_op_type_str(enum ir_op_type type) +{ + switch (type) { + case IR_OP_UNKNOWN: + return "IR_OP_UNKNOWN"; + case IR_OP_ROOT: + return "IR_OP_ROOT"; + case IR_OP_LOAD: + return "IR_OP_LOAD"; + case IR_OP_UNARY: + return "IR_OP_UNARY"; + case IR_OP_BINARY: + return "IR_OP_BINARY"; + case IR_OP_LOGICAL: + return "IR_OP_LOGICAL"; + default: + abort(); + } +} + /* left or right child */ enum ir_side { IR_SIDE_UNKNOWN = 0, @@ -71,6 +115,27 @@ enum ir_load_expression_type { IR_LOAD_EXPRESSION_LOAD_FIELD, }; +static inline +const char *ir_load_expression_type_str(enum ir_load_expression_type type) +{ + switch (type) { + case IR_LOAD_EXPRESSION_GET_CONTEXT_ROOT: + return "IR_LOAD_EXPRESSION_GET_CONTEXT_ROOT"; + case IR_LOAD_EXPRESSION_GET_APP_CONTEXT_ROOT: + return "IR_LOAD_EXPRESSION_GET_APP_CONTEXT_ROOT"; + case IR_LOAD_EXPRESSION_GET_PAYLOAD_ROOT: + return "IR_LOAD_EXPRESSION_GET_PAYLOAD_ROOT"; + case IR_LOAD_EXPRESSION_GET_SYMBOL: + return "IR_LOAD_EXPRESSION_GET_SYMBOL"; + case IR_LOAD_EXPRESSION_GET_INDEX: + return "IR_LOAD_EXPRESSION_GET_INDEX"; + case IR_LOAD_EXPRESSION_LOAD_FIELD: + return "IR_LOAD_EXPRESSION_LOAD_FIELD"; + default: + abort(); + } +} + struct ir_load_expression_op { struct ir_load_expression_op *next; enum ir_load_expression_type type; diff --git a/tests/regression/tools/trigger/test_add_trigger_cli b/tests/regression/tools/trigger/test_add_trigger_cli index acaf45631..db325a90d 100755 --- a/tests/regression/tools/trigger/test_add_trigger_cli +++ b/tests/regression/tools/trigger/test_add_trigger_cli @@ -23,7 +23,7 @@ TESTDIR="$CURDIR/../../.." # shellcheck source=../../../utils/utils.sh source "$TESTDIR/utils/utils.sh" -plan_tests 174 +plan_tests 222 FULL_LTTNG_BIN="${TESTDIR}/../src/bin/lttng/${LTTNG_BIN}" @@ -158,6 +158,34 @@ test_success "--action notify" \ --condition on-event some-event-notify -u \ --action notify +test_success "--action notify --capture foo" \ + --condition on-event some-event-notify-foo -u \ + --capture foo --action notify + +test_success "--action notify --capture foo[2]" \ + --condition on-event some-event-notify-foo2 -u \ + --capture 'foo[2]' --action notify + +test_success '--action notify --capture $ctx.foo' \ + --condition on-event some-event-notify-ctx-foo -u \ + --capture '$ctx.foo' --action notify + +test_success '--action notify --capture $ctx.foo[2]' \ + --condition on-event some-event-notify-ctx-foo2 -u \ + --capture '$ctx.foo[2]' --action notify + +test_success '--action notify --capture $app.prov:type' \ + --condition on-event some-event-notify-app-prov-type -u \ + --capture '$app.prov:type' --action notify + +test_success '--action notify --capture $app.prov:type[2]' \ + --condition on-event some-event-notify-app-prov-type-2 -u \ + --capture '$app.prov:type[2]' --action notify + +test_success '--action notify multiple captures' \ + --condition on-event some-event-notify-multiple-captures -u \ + --capture foo --capture '$app.hello:world' --action notify + # `--action start-session` successes test_success "--action start-session" \ --condition on-event some-event-start-session -u \ @@ -273,6 +301,51 @@ test_failure "--condition on-event: both -a and a tracepoint name with --syscall "Error: Can't provide a tracepoint name with -a/--all." \ --condition on-event -k --syscall -a open +test_failure "--condition on-event --capture: missing argument (end of arg list)" \ + 'Error: While parsing argument #3 (`--capture`): Missing required argument for option `--capture`' \ + --action notify \ + --condition on-event -u -a --capture + +test_failure "--condition on-event --capture: missing argument (before another option)" \ + 'Error: While parsing expression `--action`: Unary operators are not allowed in capture expressions.' \ + --condition on-event -u -a --capture \ + --action notify \ + +test_failure "--condition on-event --capture: binary operator" \ + 'Error: While parsing expression `foo == 2`: Binary operators are not allowed in capture expressions.' \ + --condition on-event -u -a \ + --capture 'foo == 2' --action notify + +test_failure "--condition on-event --capture: unary operator" \ + 'Error: While parsing expression `!foo`: Unary operators are not allowed in capture expressions.' \ + --condition on-event -u -a \ + --capture '!foo' --action notify + +test_failure "--condition on-event --capture: logical operator" \ + 'Error: While parsing expression `foo || bar`: Logical operators are not allowed in capture expressions.' \ + --condition on-event -u -a \ + --capture 'foo || bar' --action notify + +test_failure "--condition on-event --capture: accessing a sub-field" \ + 'Error: While parsing expression `foo.bar`: Capturing subfields is not supported.' \ + --condition on-event -u -a \ + --capture 'foo.bar' --action notify + +test_failure "--condition on-event --capture: accessing the sub-field of an array element" \ + 'Error: While parsing expression `foo[3].bar`: Capturing subfields is not supported.' \ + --condition on-event -u -a \ + --capture 'foo[3].bar' --action notify + +test_failure "--condition on-event --capture: missing colon in app-specific context field" \ + 'Error: Invalid app-specific context field name: missing colon in `foo`.' \ + --condition on-event -u -a \ + --capture '$app.foo' --action notify + +test_failure "--condition on-event --capture: missing colon in app-specific context field" \ + 'Error: Invalid app-specific context field name: missing type name after colon in `foo:`.' \ + --condition on-event -u -a \ + --capture '$app.foo:' --action notify + # `--action` failures test_failure "missing args after --action" \ "Error: Missing action name." \ -- 2.34.1