diff --git a/README.md b/README.md index 55bfc20..bf58798 100644 --- a/README.md +++ b/README.md @@ -848,13 +848,15 @@ command, the following context is set: In the `sub` and `tap` commands, the following bindings are available: -* the current received message is bound to the variable [msg](#message-type), +* the current received message is bound to the variable [rt_msg](#message-type), which allows access to the message-metadata and the body +* the current count of messages received that passed the filter is bound to + `rt_count` * Helper function are provided for accessing the message body: - * the `toStr` function converts a byte buffer into a string, e.g. `let - b=toJSON(toStr(msg.Body))` - * the `gunzip` function decompresses the given byte buffer `let - b=toJSON(toStr(gunzip(msg.Body)))`, allowing to inspect a compressed body + * the `rt_toStr` function converts a byte buffer into a string, e.g. `let + b=toJSON(rt_toStr(rt_msg.Body))` + * the `rt_gunzip` function decompresses the given byte buffer `let + b=toJSON(rt_toStr(rt_gunzip(rt_msg.Body)))`, allowing to inspect a compressed body ##### Examples @@ -869,9 +871,9 @@ broker to be used, e.g. `http://guest:guest@localhost:15672/api`). before, but consider only exchanges of type `topic`. * `rabtap info --filter "queue.Consumers > 0" --omit --stats --consumers` - print all queues with at least one consume -* `rabtap sub JDQ --filter="msg.RoutingKey == 'test'"` - print only messages that +* `rabtap sub JDQ --filter="rt_msg.RoutingKey == 'test'"` - print only messages that were sent with the routing key `test`. -* `rabtap sub JDQ --filter="let b=fromJSON(toStr(gunzip(msg.Body))); b.Name == 'JAN'"` - +* `rabtap sub JDQ --filter="let b=fromJSON(rt_toStr(rt_gunzip(rt_msg.Body))); b.Name == 'JAN'"` - print only messages that have `.Name == "JAN"` in their gzipped payload, interpreted as `JSON` diff --git a/cmd/rabtap/cmd_subscribe.go b/cmd/rabtap/cmd_subscribe.go index 0494aef..1207278 100644 --- a/cmd/rabtap/cmd_subscribe.go +++ b/cmd/rabtap/cmd_subscribe.go @@ -20,8 +20,8 @@ type CmdSubscribeArg struct { queue string tlsConfig *tls.Config messageReceiveFunc MessageReceiveFunc - messageReceiveLoopPred MessagePred - filterPred MessagePred + messageReceiveLoopPred Predicate + filterPred Predicate reject bool requeue bool args rabtap.KeyValueMap diff --git a/cmd/rabtap/cmd_subscribe_test.go b/cmd/rabtap/cmd_subscribe_test.go index 303bd84..33407a6 100644 --- a/cmd/rabtap/cmd_subscribe_test.go +++ b/cmd/rabtap/cmd_subscribe_test.go @@ -14,7 +14,6 @@ import ( "io" "net/url" "os" - "syscall" "testing" "time" @@ -37,7 +36,7 @@ func TestCmdSubFailsEarlyWhenBrokerIsNotAvailable(t *testing.T) { queue: "queue", tlsConfig: &tls.Config{}, messageReceiveFunc: func(rabtap.TapMessage) error { return nil }, - messageReceiveLoopPred: func(rabtap.TapMessage) (bool, error) { return false, nil }, + messageReceiveLoopPred: &constantPred{false}, timeout: time.Second * 10, }) done <- true @@ -83,8 +82,8 @@ func TestCmdSub(t *testing.T) { queue: testQueue, tlsConfig: tlsConfig, messageReceiveFunc: receiveFunc, - filterPred: func(rabtap.TapMessage) (bool, error) { return true, nil }, - messageReceiveLoopPred: func(rabtap.TapMessage) (bool, error) { return false, nil }, + filterPred: &constantPred{true}, + messageReceiveLoopPred: &constantPred{false}, timeout: time.Second * 10, }) @@ -150,16 +149,12 @@ func TestCmdSubIntegration(t *testing.T) { }) require.Nil(t, err) - go func() { - time.Sleep(time.Second * 2) - syscall.Kill(syscall.Getpid(), syscall.SIGINT) - }() - oldArgs := os.Args defer func() { os.Args = oldArgs }() os.Args = []string{"rabtap", "sub", "--uri", amqpURL.String(), testQueue, + "--limit=1", "--format=raw", "--no-color"} output := testcommon.CaptureOutput(main) diff --git a/cmd/rabtap/cmd_tap.go b/cmd/rabtap/cmd_tap.go index ed31d94..5a4fd38 100644 --- a/cmd/rabtap/cmd_tap.go +++ b/cmd/rabtap/cmd_tap.go @@ -17,8 +17,8 @@ type CmdTapArg struct { tapConfig []rabtap.TapConfiguration tlsConfig *tls.Config messageReceiveFunc MessageReceiveFunc - termPred MessagePred - filterPred MessagePred + termPred Predicate + filterPred Predicate timeout time.Duration } diff --git a/cmd/rabtap/cmd_tap_test.go b/cmd/rabtap/cmd_tap_test.go index de0aa0e..0899242 100644 --- a/cmd/rabtap/cmd_tap_test.go +++ b/cmd/rabtap/cmd_tap_test.go @@ -9,7 +9,6 @@ import ( "context" "crypto/tls" "os" - "syscall" "testing" "time" @@ -45,15 +44,12 @@ func TestCmdTap(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) - filterPred := func(rabtap.TapMessage) (bool, error) { return true, nil } - termPred := func(rabtap.TapMessage) (bool, error) { return true, nil } - // when go cmdTap(ctx, CmdTapArg{tapConfig: tapConfig, tlsConfig: &tls.Config{}, messageReceiveFunc: receiveFunc, - filterPred: filterPred, - termPred: termPred, + filterPred: &constantPred{true}, + termPred: &constantPred{false}, timeout: time.Second * 10}) time.Sleep(time.Second * 1) @@ -84,8 +80,9 @@ func TestCmdTapIntegration(t *testing.T) { testKey := testQueue testExchange := "amq.topic" + // message must be published, after rabtap tap command is started go func() { - time.Sleep(time.Second * 1) + time.Sleep(3 * time.Second) _, ch := testcommon.IntegrationTestConnection(t, "", "", 0, false) err := ch.Publish( testExchange, @@ -99,8 +96,6 @@ func TestCmdTapIntegration(t *testing.T) { Headers: amqp.Table{}, }) require.Nil(t, err) - time.Sleep(time.Second * 1) - syscall.Kill(syscall.Getpid(), syscall.SIGINT) }() oldArgs := os.Args @@ -108,6 +103,7 @@ func TestCmdTapIntegration(t *testing.T) { os.Args = []string{"rabtap", "tap", "--uri", testcommon.IntegrationURIFromEnv().String(), "amq.topic:" + testKey, + "--limit=1", "--format=raw", "--no-color"} output := testcommon.CaptureOutput(main) diff --git a/cmd/rabtap/main.go b/cmd/rabtap/main.go index ea84f66..be99e7e 100644 --- a/cmd/rabtap/main.go +++ b/cmd/rabtap/main.go @@ -159,7 +159,8 @@ func startCmdSubscribe(ctx context.Context, args CommandLineArgs) { messageReceiveFunc, err := createMessageReceiveFunc(opts) failOnError(err, "options", os.Exit) - termPred := createCountingMessageReceivePred(args.Limit) + termPred, err := createCountingMessageReceivePred(args.Limit) + failOnError(err, "invalid message limit predicate", os.Exit) filterPred, err := NewExprPredicate(args.Filter) failOnError(err, fmt.Sprintf("invalid message filter predicate '%s'", args.Filter), os.Exit) @@ -170,7 +171,7 @@ func startCmdSubscribe(ctx context.Context, args CommandLineArgs) { reject: args.Reject, tlsConfig: getTLSConfig(args.InsecureTLS, args.TLSCertFile, args.TLSKeyFile, args.TLSCaFile), messageReceiveFunc: messageReceiveFunc, - filterPred: createMessagePred(filterPred), + filterPred: filterPred, messageReceiveLoopPred: termPred, args: args.Args, timeout: args.IdleTimeout, @@ -188,7 +189,8 @@ func startCmdTap(ctx context.Context, args CommandLineArgs) { } messageReceiveFunc, err := createMessageReceiveFunc(opts) failOnError(err, "options", os.Exit) - termPred := createCountingMessageReceivePred(args.Limit) + termPred, err := createCountingMessageReceivePred(args.Limit) + failOnError(err, "invalid message limit predicate", os.Exit) filterPred, err := NewExprPredicate(args.Filter) failOnError(err, fmt.Sprintf("invalid message filter predicate '%s'", args.Filter), os.Exit) @@ -197,7 +199,7 @@ func startCmdTap(ctx context.Context, args CommandLineArgs) { tapConfig: args.TapConfig, tlsConfig: getTLSConfig(args.InsecureTLS, args.TLSCertFile, args.TLSKeyFile, args.TLSCaFile), messageReceiveFunc: messageReceiveFunc, - filterPred: createMessagePred(filterPred), + filterPred: filterPred, termPred: termPred, timeout: args.IdleTimeout, }) diff --git a/cmd/rabtap/predicate_expr.go b/cmd/rabtap/predicate_expr.go index 56e0db3..422d089 100644 --- a/cmd/rabtap/predicate_expr.go +++ b/cmd/rabtap/predicate_expr.go @@ -11,10 +11,11 @@ import ( // ExprPredicate is a Predicate that evaluates using the expr package type ExprPredicate struct { - prog *vm.Program + initialEnv map[string]interface{} + prog *vm.Program } -// NewExprPredicate creates a new predicate expression +// NewExprPredicate creates a new predicate expression with an optional initial environment func NewExprPredicate(exprstr string) (*ExprPredicate, error) { prog, err := expr.Compile(exprstr) if err != nil { @@ -23,8 +24,25 @@ func NewExprPredicate(exprstr string) (*ExprPredicate, error) { return &ExprPredicate{prog: prog}, nil } +// NewExprPredicate creates a new predicate expression with an optional initial environment +func NewExprPredicateWithEnv(exprstr string, env map[string]interface{}) (*ExprPredicate, error) { + options := []expr.Option{ + expr.Env(env), + expr.AllowUndefinedVariables(), + expr.AsBool()} + + prog, err := expr.Compile(exprstr, options...) + if err != nil { + return nil, err + } + return &ExprPredicate{prog: prog, initialEnv: env}, nil +} + // Eval evaluates the expression with a given set of parameters func (s ExprPredicate) Eval(env map[string]interface{}) (bool, error) { + for k, v := range s.initialEnv { + env[k] = v + } result, err := expr.Run(s.prog, env) if err != nil { return false, err diff --git a/cmd/rabtap/predicate_expr_test.go b/cmd/rabtap/predicate_expr_test.go index 51fe37c..1f01118 100644 --- a/cmd/rabtap/predicate_expr_test.go +++ b/cmd/rabtap/predicate_expr_test.go @@ -25,14 +25,28 @@ func TestExprPredicateFalse(t *testing.T) { assert.False(t, res) } -func TestExprPredicateWithEnv(t *testing.T) { +func TestExprPredicateWithInitalEnv(t *testing.T) { + initEnv := map[string]interface{}{"a": 1337} + f, err := NewExprPredicateWithEnv(`b < a`, initEnv) + require.NoError(t, err) + + env := map[string]interface{}{"b": 100} + res, err := f.Eval(env) + + require.NoError(t, err) + assert.True(t, res) +} +func TestExprPredicateWithEvalEnv(t *testing.T) { f, err := NewExprPredicate(`a == 1337 && b.X == 42 && c == "JD"`) require.NoError(t, err) - params := make(map[string]interface{}, 1) - params["a"] = 1337 - params["b"] = struct{ X int }{X: 42} - params["c"] = "JD" - res, err := f.Eval(params) + env := map[string]interface{}{ + "a": 1337, + "b": struct{ X int }{X: 42}, + "c": "JD", + } + + res, err := f.Eval(env) + require.NoError(t, err) assert.True(t, res) } @@ -45,13 +59,16 @@ func TestExprPredicateReturnsErrorOnInvalidSyntax(t *testing.T) { func TestExprPredicateReturnsErrorOnEvalError(t *testing.T) { f, err := NewExprPredicate("(1/a) == 1") require.NoError(t, err) + _, err = f.Eval(nil) assert.ErrorContains(t, err, "invalid operation") } func TestExprPredicateReturnsErrorOnNonBoolReturnValue(t *testing.T) { f, err := NewExprPredicate("1+1") require.NoError(t, err) + params := map[string]interface{}{} _, err = f.Eval(params) + assert.ErrorContains(t, err, "expression does not evaluate to bool") } diff --git a/cmd/rabtap/subscribe.go b/cmd/rabtap/subscribe.go index a03c4e5..0ac575d 100644 --- a/cmd/rabtap/subscribe.go +++ b/cmd/rabtap/subscribe.go @@ -12,6 +12,7 @@ import ( "time" rabtap "github.com/jandelgado/rabtap/pkg" + amqp "github.com/rabbitmq/amqp091-go" ) // ErrIdleTimeout is returned by the message loop when the loop was terminated @@ -39,9 +40,42 @@ type MessageReceiveFunc func(rabtap.TapMessage) error // MessagePred is a predicate function on a message type MessagePred func(rabtap.TapMessage) (bool, error) +type MessagePredEnv struct { + msg *amqp.Delivery + count int64 + toStr func([]byte) string + gunzip func([]byte) ([]byte, error) +} + +func createMessagePredEnv(msg rabtap.TapMessage, count int64) map[string]interface{} { + return map[string]interface{}{ + "rt_msg": msg.AmqpMessage, + "rt_count": count, + "rt_toStr": func(b []byte) string { return string(b) }, + "rt_gunzip": func(b []byte) ([]byte, error) { + return gunzip(bytes.NewReader(b)) + }, + } +} + +// cerateMessagePredEnv returns an environment to evaluate predicates in the +// context of received messages +// func createMessagePredEnv(msg rabtap.TapMessage, count int64) map[string]interface{} { +/* func createMessagePredEnv(msg rabtap.TapMessage, count int64) MessagePredEnv { + // expose the message and some helper function + return MessagePredEnv{ + msg: msg.AmqpMessage, + count: count, + toStr: func(b []byte) string { return string(b) }, + gunzip: func(b []byte) ([]byte, error) { + return gunzip(bytes.NewReader(b)) + }, + } +} */ + // createMessagePred creates a MessagePred predicate function that uses a // PredicateExpression -func createMessagePred(expr Predicate) MessagePred { +/* func createMessagePred(expr Predicate) MessagePred { return func(m rabtap.TapMessage) (bool, error) { // expose the message and some helper function env := map[string]interface{}{ @@ -53,13 +87,21 @@ func createMessagePred(expr Predicate) MessagePred { } return expr.Eval(env) } +} */ + +// createCountingMessageReceivePred creates the default message loop termination +// predicate (loop terminates when predicate is true). When limit is 0, loop +// will never terminate. Expectes a variable "rt_count" in the context, that +// holds the current number of messages received. The limit is provided by configuration. +// To unify predicate handling (see filter predicate), we use the same mechanism +// here. In later versions, the termination predicate may be defined by the +// user, so that rabtap quits if a certain condition is met. +func createCountingMessageReceivePred(limit int64) (Predicate, error) { + env := map[string]interface{}{"rt_limit": limit} + return NewExprPredicateWithEnv("(rt_limit > 0) && (rt_count >= rt_limit)", env) } -// createCountingMessageReceivePred returns a (stateful) predicate that will -// return true after it is called num times, thus limiting the number of -// messages received. If num is 0, a predicate always returning false is -// returned. -func createCountingMessageReceivePred(num int64) MessagePred { +/* func createCountingMessageReceivePred(num int64) MessagePred { if num == 0 { return func(_ rabtap.TapMessage) (bool, error) { @@ -73,7 +115,7 @@ func createCountingMessageReceivePred(num int64) MessagePred { return counter > num, nil } -} +} */ // createAcknowledgeFunc returns the function used to acknowledge received // functions, wich will either be ACKed or REJECTED with optional REQUEUE @@ -104,20 +146,21 @@ func messageReceiveLoop(ctx context.Context, messageChan rabtap.TapChannel, errorChan rabtap.SubscribeErrorChannel, messageReceiveFunc MessageReceiveFunc, - filterPred MessagePred, - termPred MessagePred, + filterPred Predicate, + termPred Predicate, acknowledger AcknowledgeFunc, timeout time.Duration) error { timeoutTicker := time.NewTicker(timeout) defer timeoutTicker.Stop() + count := int64(0) // counts not filtered messages for { select { case <-ctx.Done(): log.Debugf("subscribe: cancel") - return nil + return ctx.Err() case err, more := <-errorChan: if more { @@ -137,20 +180,28 @@ func messageReceiveLoop(ctx context.Context, log.Error(err) } - passed, err := filterPred(message) + env := createMessagePredEnv(message, count) + passed, err := filterPred.Eval(env) if err != nil { - log.Errorf("filter evaluation: %s", err.Error()) + log.Errorf("filter expression evaluation: %s", err.Error()) } + if !passed { + log.Debugf("message with MessageId=%s was filtered out", message.AmqpMessage.MessageId) continue } + count += 1 if err := messageReceiveFunc(message); err != nil { log.Error(err) } - // TODO ok to count only on not-filtered out messages? - if terminate, _ := termPred(message); terminate { + env = createMessagePredEnv(message, count) + terminate, err := termPred.Eval(env) + if err != nil { + log.Errorf("terminate expression evaluation: %s", err.Error()) + } + if terminate { return nil } diff --git a/cmd/rabtap/subscribe_test.go b/cmd/rabtap/subscribe_test.go index 1213020..81bd6a3 100644 --- a/cmd/rabtap/subscribe_test.go +++ b/cmd/rabtap/subscribe_test.go @@ -23,6 +23,20 @@ import ( "github.com/stretchr/testify/require" ) +// a Predicate returning a constant value +type constantPred struct{ val bool } + +func (s *constantPred) Eval(_ map[string]interface{}) (bool, error) { + return s.val, nil +} + +// a predicate that lets only pass message with MessageID set to a given value +type testPred struct{ match string } + +func (s *testPred) Eval(env map[string]interface{}) (bool, error) { + return env["rt_msg"].(*amqp.Delivery).MessageId == s.match, nil +} + // a mocked amqp.Acknowldger to test our AcknowledgeFunc type MockAcknowledger struct { // store values in a map so being able to manipulate in a value receiver @@ -56,22 +70,14 @@ func (s MockAcknowledger) Reject(tag uint64, requeue bool) error { func TestCreateMessagePredicateProvidesMessageContext(t *testing.T) { - // given a predicate that accesses message attributes - pred, err := NewExprPredicate("msg.MessageId=='match123'") - require.NoError(t, err) - filterPred := createMessagePred(pred) - // when we evalute the predicate for the test Messages - expectedMatch := rabtap.TapMessage{AmqpMessage: &amqp.Delivery{MessageId: "match123"}} - res, err := filterPred(expectedMatch) - require.NoError(t, err) - // then we expect the expression evaluated in the given context - assert.True(t, res) + msg := rabtap.TapMessage{AmqpMessage: &amqp.Delivery{MessageId: "match123"}} + env := createMessagePredEnv(msg, 123) - expectedNoMatch := rabtap.TapMessage{AmqpMessage: &amqp.Delivery{MessageId: "no match"}} - res, err = filterPred(expectedNoMatch) - require.NoError(t, err) - assert.False(t, res) + assert.Equal(t, msg.AmqpMessage, env["rt_msg"]) + assert.Equal(t, int64(123), env["rt_count"]) + assert.NotNil(t, env["rt_gunzip"]) + assert.NotNil(t, env["rt_toStr"]) } func TestCreateAcknowledgeFuncReturnedFuncCorreclyAcknowledgesTheMessage(t *testing.T) { @@ -105,22 +111,33 @@ func TestCreateAcknowledgeFuncReturnedFuncCorreclyAcknowledgesTheMessage(t *test } } -func TestCreateCountingMessageReceivePredReturnsTrueIfNumIsZero(t *testing.T) { - pred := createCountingMessageReceivePred(0) - res, err := pred(rabtap.TapMessage{}) - assert.NoError(t, err) - assert.False(t, res) +func TestCreateCountingMessageReceivePredReturnsFalseIfLimitIsZero(t *testing.T) { + pred, err := createCountingMessageReceivePred(0) + require.NoError(t, err) + + for _, tc := range []int64{0, 1, 2, 100} { + env := map[string]interface{}{"rt_count": tc} + res, err := pred.Eval(env) + + require.NoError(t, err) + assert.False(t, res) + } } -func TestCreateCountingMessageReceivePredReturnsTrueOnNthCall(t *testing.T) { - pred := createCountingMessageReceivePred(2) +func TestCreateCountingMessageReceivePredReturnsTrueOnWhenLimitIsReached(t *testing.T) { + pred, err := createCountingMessageReceivePred(3) + require.NoError(t, err) + + testcases := map[int64]bool{0: false, 1: false, 2: false, 3: true, 4: true} + for probe, expected := range testcases { + t.Run(fmt.Sprintf("term_predicate(%v, %v)", probe, expected), func(t *testing.T) { + env := map[string]interface{}{"rt_count": probe} + actual, err := pred.Eval(env) - res, err := pred(rabtap.TapMessage{}) - assert.NoError(t, err) - assert.False(t, res) - res, err = pred(rabtap.TapMessage{}) - assert.NoError(t, err) - assert.True(t, res) + require.NoError(t, err) + assert.Equal(t, expected, actual) + }) + } } func TestChainMessageReceiveFuncCallsBothFunctions(t *testing.T) { @@ -230,7 +247,6 @@ func TestCreateMessageReceiveFuncJSON(t *testing.T) { assert.True(t, strings.Count(b.String(), "\n") > 1) assert.True(t, strings.Contains(b.String(), "\"Body\": \"VGVzdG1lc3NhZ2U=\"")) - } func TestCreateMessageReceiveFuncJSONNoPPToFile(t *testing.T) { @@ -277,8 +293,8 @@ func TestMessageReceiveLoopForwardsMessagesOnChannel(t *testing.T) { done <- true return nil } - termPred := func(rabtap.TapMessage) (bool, error) { return false, nil } - passPred := func(rabtap.TapMessage) (bool, error) { return true, nil } + termPred := &constantPred{val: false} + passPred := &constantPred{val: true} acknowledger := func(rabtap.TapMessage) error { return nil } go func() { _ = messageReceiveLoop(ctx, messageChan, errorChan, receiveFunc, passPred, termPred, acknowledger, time.Second*10) @@ -294,8 +310,8 @@ func TestMessageReceiveLoopExitsOnChannelClose(t *testing.T) { ctx := context.Background() messageChan := make(rabtap.TapChannel) errorChan := make(rabtap.SubscribeErrorChannel) - termPred := func(rabtap.TapMessage) (bool, error) { return false, nil } - passPred := func(rabtap.TapMessage) (bool, error) { return true, nil } + termPred := &constantPred{val: false} + passPred := &constantPred{val: true} close(messageChan) acknowledger := func(rabtap.TapMessage) error { return nil } @@ -308,8 +324,8 @@ func TestMessageReceiveLoopExitsWhenTermPredReturnsTrue(t *testing.T) { ctx := context.Background() messageChan := make(rabtap.TapChannel, 1) errorChan := make(rabtap.SubscribeErrorChannel) - termPred := func(rabtap.TapMessage) (bool, error) { return true, nil } - passPred := func(rabtap.TapMessage) (bool, error) { return true, nil } + termPred := &constantPred{val: true} + passPred := &constantPred{val: true} messageChan <- rabtap.TapMessage{} acknowledger := func(rabtap.TapMessage) error { return nil } @@ -328,9 +344,8 @@ func TestMessageReceiveLoopIgnoresFilteredMessages(t *testing.T) { received++ return nil } - termPred := func(rabtap.TapMessage) (bool, error) { return false, nil } - // create a message predicate that lets only pass message with MessageID set to "test" - filterPred := func(m rabtap.TapMessage) (bool, error) { return m.AmqpMessage.MessageId == "test", nil } + termPred := &constantPred{val: false} + filterPred := &testPred{match: "test"} acknowledger := func(rabtap.TapMessage) error { return nil } // when we send 3 messages @@ -351,8 +366,8 @@ func TestMessageReceiveLoopExitsWithErrorWhenIdle(t *testing.T) { ctx := context.Background() messageChan := make(rabtap.TapChannel) errorChan := make(rabtap.SubscribeErrorChannel) - termPred := func(rabtap.TapMessage) (bool, error) { return false, nil } - passPred := func(rabtap.TapMessage) (bool, error) { return true, nil } + termPred := &constantPred{val: false} + passPred := &constantPred{val: true} acknowledger := func(rabtap.TapMessage) error { return nil } // when