diff --git a/src/test/java/io/kestra/plugin/aws/kinesis/RealtimeTriggerTest.java b/src/test/java/io/kestra/plugin/aws/kinesis/RealtimeTriggerTest.java index 5afc384b..c2565a8e 100644 --- a/src/test/java/io/kestra/plugin/aws/kinesis/RealtimeTriggerTest.java +++ b/src/test/java/io/kestra/plugin/aws/kinesis/RealtimeTriggerTest.java @@ -3,25 +3,19 @@ import java.io.File; import java.nio.file.Files; import java.util.List; -import java.util.UUID; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.*; +import io.kestra.core.junit.annotations.KestraTest; import io.kestra.core.models.executions.Execution; import io.kestra.core.models.property.Property; import io.kestra.core.queues.QueueFactoryInterface; import io.kestra.core.queues.QueueInterface; import io.kestra.core.repositories.LocalFlowRepositoryLoader; -import io.kestra.core.runners.FlowListeners; import io.kestra.core.utils.TestsUtils; -import io.kestra.jdbc.runner.JdbcScheduler; import io.kestra.plugin.aws.kinesis.model.Record; -import io.kestra.scheduler.AbstractScheduler; -import io.kestra.worker.DefaultWorker; - -import io.micronaut.context.ApplicationContext; import jakarta.inject.Inject; import jakarta.inject.Named; import reactor.core.publisher.Flux; @@ -30,13 +24,8 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.*; +@KestraTest(startRunner = true, startScheduler = true) class RealtimeTriggerTest extends AbstractKinesisTest { - @Inject - ApplicationContext applicationContext; - - @Inject - FlowListeners flowListeners; - @Inject @Named(QueueFactoryInterface.EXECUTION_NAMED) QueueInterface executionQueue; @@ -51,60 +40,53 @@ void evaluate() throws Exception { Flux received = TestsUtils.receive(executionQueue, e -> latch.countDown()); - DefaultWorker worker = applicationContext.createBean(DefaultWorker.class, UUID.randomUUID().toString(), 8, null); - try (AbstractScheduler scheduler = new JdbcScheduler(applicationContext, flowListeners)) { - - worker.run(); - scheduler.run(); - - String yaml = """ - id: realtime - namespace: company.team - - tasks: - - id: log - type: io.kestra.plugin.core.log.Log - message: "{{ trigger.data }}" - - triggers: - - id: realtime - type: io.kestra.plugin.aws.kinesis.RealtimeTrigger - streamName: "%s" - consumerArn: "%s" - region: "us-east-1" - accessKeyId: "test" - secretKeyId: "test" - endpointOverride: "http://localhost:4566" - iteratorType: TRIM_HORIZON - """ - .formatted(streamName, consumerArn); - - File tempFlow = File.createTempFile("kinesis-realtime", ".yaml"); - Files.writeString(tempFlow.toPath(), yaml); - - repositoryLoader.load(tempFlow); - - Record record = Record.builder() - .partitionKey("pk") - .data("hello") - .build(); - - var put = PutRecords.builder() - .endpointOverride(Property.ofValue(localstack.getEndpoint().toString())) - .region(Property.ofValue(localstack.getRegion())) - .accessKeyId(Property.ofValue(localstack.getAccessKey())) - .secretKeyId(Property.ofValue(localstack.getSecretKey())) - .streamName(Property.ofValue(streamName)) - .records(List.of(record)) - .build(); - - put.run(runContextFactory.of()); - - boolean done = latch.await(30, TimeUnit.SECONDS); - assertThat(done, is(true)); - - Execution exec = received.blockLast(); - assertThat(exec.getTrigger().getVariables().get("data"), is("hello")); - } + String yaml = """ + id: realtime + namespace: company.team + + tasks: + - id: log + type: io.kestra.plugin.core.log.Log + message: "{{ trigger.data }}" + + triggers: + - id: realtime + type: io.kestra.plugin.aws.kinesis.RealtimeTrigger + streamName: "%s" + consumerArn: "%s" + region: "us-east-1" + accessKeyId: "test" + secretKeyId: "test" + endpointOverride: "http://localhost:4566" + iteratorType: TRIM_HORIZON + """ + .formatted(streamName, consumerArn); + + File tempFlow = File.createTempFile("kinesis-realtime", ".yaml"); + Files.writeString(tempFlow.toPath(), yaml); + + repositoryLoader.load(tempFlow); + + Record record = Record.builder() + .partitionKey("pk") + .data("hello") + .build(); + + var put = PutRecords.builder() + .endpointOverride(Property.ofValue(localstack.getEndpoint().toString())) + .region(Property.ofValue(localstack.getRegion())) + .accessKeyId(Property.ofValue(localstack.getAccessKey())) + .secretKeyId(Property.ofValue(localstack.getSecretKey())) + .streamName(Property.ofValue(streamName)) + .records(List.of(record)) + .build(); + + put.run(runContextFactory.of()); + + boolean done = latch.await(30, TimeUnit.SECONDS); + assertThat(done, is(true)); + + Execution exec = received.blockLast(); + assertThat(exec.getTrigger().getVariables().get("data"), is("hello")); } -} \ No newline at end of file +} diff --git a/src/test/java/io/kestra/plugin/aws/s3/TriggerTest.java b/src/test/java/io/kestra/plugin/aws/s3/TriggerTest.java index 95d7348a..d80dfc2f 100644 --- a/src/test/java/io/kestra/plugin/aws/s3/TriggerTest.java +++ b/src/test/java/io/kestra/plugin/aws/s3/TriggerTest.java @@ -4,7 +4,6 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; -import java.util.UUID; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; @@ -12,6 +11,7 @@ import org.junit.jupiter.api.Test; import org.testcontainers.containers.localstack.LocalStackContainer; +import io.kestra.core.junit.annotations.KestraTest; import io.kestra.core.models.conditions.ConditionContext; import io.kestra.core.models.executions.Execution; import io.kestra.core.models.property.Property; @@ -19,15 +19,9 @@ import io.kestra.core.queues.QueueFactoryInterface; import io.kestra.core.queues.QueueInterface; import io.kestra.core.repositories.LocalFlowRepositoryLoader; -import io.kestra.core.runners.FlowListeners; import io.kestra.core.utils.IdUtils; import io.kestra.core.utils.TestsUtils; -import io.kestra.jdbc.runner.JdbcScheduler; import io.kestra.plugin.aws.s3.models.S3Object; -import io.kestra.scheduler.AbstractScheduler; -import io.kestra.worker.DefaultWorker; - -import io.micronaut.context.ApplicationContext; import jakarta.inject.Inject; import jakarta.inject.Named; import reactor.core.publisher.Flux; @@ -35,13 +29,8 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; +@KestraTest(startRunner = true, startScheduler = true) class TriggerTest extends AbstractTest { - @Inject - private ApplicationContext applicationContext; - - @Inject - private FlowListeners flowListenersService; - @Inject @Named(QueueFactoryInterface.EXECUTION_NAMED) private QueueInterface executionQueue; @@ -54,56 +43,35 @@ void deleteAction() throws Exception { String bucket = "trigger-test"; this.createBucket(bucket); List listTask = list().bucket(Property.ofValue(bucket)).build(); - - // mock flow listeners CountDownLatch queueCount = new CountDownLatch(1); + AtomicReference last = new AtomicReference<>(); + Flux receive = TestsUtils.receive(executionQueue, executionWithError -> { + Execution execution = executionWithError.getLeft(); - // scheduler - DefaultWorker worker = applicationContext.createBean(DefaultWorker.class, UUID.randomUUID().toString(), 8, null); - try ( - AbstractScheduler scheduler = new JdbcScheduler( - this.applicationContext, - this.flowListenersService - ) - ) { - AtomicReference last = new AtomicReference<>(); - - // wait for execution - Flux receive = TestsUtils.receive(executionQueue, executionWithError -> - { - Execution execution = executionWithError.getLeft(); - - if (execution.getFlowId().equals("s3-listen")) { - last.set(execution); - queueCount.countDown(); - } - }); - - upload("trigger/s3", bucket); - upload("trigger/s3", bucket); - - worker.run(); - scheduler.run(); - repositoryLoader.load(Objects.requireNonNull(TriggerTest.class.getClassLoader().getResource("flows/s3/s3-listen.yaml"))); - - boolean await = queueCount.await(10, TimeUnit.SECONDS); - try { - assertThat(await, is(true)); - } finally { - worker.shutdown(); - receive.blockLast(); + if (execution.getFlowId().equals("s3-listen")) { + last.set(execution); + queueCount.countDown(); } + }); - @SuppressWarnings("unchecked") - java.util.List trigger = (java.util.List) last.get().getTrigger().getVariables().get("objects"); + upload("trigger/s3", bucket); + upload("trigger/s3", bucket); - assertThat(trigger.size(), is(2)); + repositoryLoader.load(Objects.requireNonNull(TriggerTest.class.getClassLoader().getResource("flows/s3/s3-listen.yaml"))); - int remainingFilesOnBucket = listTask.run(runContext(listTask)) - .getObjects() - .size(); - assertThat(remainingFilesOnBucket, is(0)); - } + boolean await = queueCount.await(10, TimeUnit.SECONDS); + assertThat(await, is(true)); + receive.blockLast(); + + @SuppressWarnings("unchecked") + java.util.List trigger = (java.util.List) last.get().getTrigger().getVariables().get("objects"); + + assertThat(trigger.size(), is(2)); + + int remainingFilesOnBucket = listTask.run(runContext(listTask)) + .getObjects() + .size(); + assertThat(remainingFilesOnBucket, is(0)); } @Test @@ -112,11 +80,9 @@ void noneAction() throws Exception { this.createBucket(bucket); List listTask = list().bucket(Property.ofValue(bucket)).build(); - // wait for execution CountDownLatch queueCount = new CountDownLatch(1); AtomicReference last = new AtomicReference<>(); - Flux receive = TestsUtils.receive(executionQueue, executionWithError -> - { + Flux receive = TestsUtils.receive(executionQueue, executionWithError -> { Execution execution = executionWithError.getLeft(); if (execution.getFlowId().equals("s3-listen-none-action")) { @@ -125,39 +91,24 @@ void noneAction() throws Exception { } }); - // scheduler - DefaultWorker worker = applicationContext.createBean(DefaultWorker.class, UUID.randomUUID().toString(), 8, null); - try ( - AbstractScheduler scheduler = new JdbcScheduler( - this.applicationContext, - this.flowListenersService - ) - ) { - upload("trigger/s3", bucket); - upload("trigger/s3", bucket); - - worker.run(); - scheduler.run(); - repositoryLoader.load(Objects.requireNonNull(TriggerTest.class.getClassLoader().getResource("flows/s3/s3-listen-none-action.yaml"))); - - boolean await = queueCount.await(10, TimeUnit.SECONDS); - try { - assertThat(await, is(true)); - } finally { - worker.shutdown(); - receive.blockLast(); - } + upload("trigger/s3", bucket); + upload("trigger/s3", bucket); - @SuppressWarnings("unchecked") - java.util.List trigger = (java.util.List) last.get().getTrigger().getVariables().get("objects"); + repositoryLoader.load(Objects.requireNonNull(TriggerTest.class.getClassLoader().getResource("flows/s3/s3-listen-none-action.yaml"))); - assertThat(trigger.size(), is(2)); + boolean await = queueCount.await(10, TimeUnit.SECONDS); + assertThat(await, is(true)); + receive.blockLast(); - int remainingFilesOnBucket = listTask.run(runContext(listTask)) - .getObjects() - .size(); - assertThat(remainingFilesOnBucket, is(2)); - } + @SuppressWarnings("unchecked") + java.util.List trigger = (java.util.List) last.get().getTrigger().getVariables().get("objects"); + + assertThat(trigger.size(), is(2)); + + int remainingFilesOnBucket = listTask.run(runContext(listTask)) + .getObjects() + .size(); + assertThat(remainingFilesOnBucket, is(2)); } @Test @@ -167,52 +118,34 @@ void forcePathStyleWithSimpleLocalhost() throws Exception { List listTask = list().bucket(Property.ofValue(bucket)).build(); CountDownLatch queueCount = new CountDownLatch(1); + AtomicReference last = new AtomicReference<>(); + Flux receive = TestsUtils.receive(executionQueue, executionWithError -> { + Execution execution = executionWithError.getLeft(); - // scheduler - DefaultWorker worker = applicationContext.createBean(DefaultWorker.class, UUID.randomUUID().toString(), 8, null); - try ( - AbstractScheduler scheduler = new JdbcScheduler( - this.applicationContext, - this.flowListenersService - ) - ) { - AtomicReference last = new AtomicReference<>(); - - Flux receive = TestsUtils.receive(executionQueue, executionWithError -> - { - Execution execution = executionWithError.getLeft(); - - if (execution.getFlowId().equals("s3-listen-localhost-force-path-style")) { - last.set(execution); - queueCount.countDown(); - } - }); - - upload("trigger/s3", bucket); - upload("trigger/s3", bucket); - - worker.run(); - scheduler.run(); - repositoryLoader.load(Objects.requireNonNull(TriggerTest.class.getClassLoader().getResource("flows/s3/s3-listen-localhost-force-path-style.yaml"))); - - boolean await = queueCount.await(15, TimeUnit.SECONDS); - try { - assertThat("trigger should work with localhost endpoint + forcePathStyle", await, is(true)); - } finally { - worker.shutdown(); - receive.blockLast(); + if (execution.getFlowId().equals("s3-listen-localhost-force-path-style")) { + last.set(execution); + queueCount.countDown(); } + }); - @SuppressWarnings("unchecked") - java.util.List trigger = (java.util.List) last.get().getTrigger().getVariables().get("objects"); + upload("trigger/s3", bucket); + upload("trigger/s3", bucket); - assertThat(trigger.size(), is(2)); + repositoryLoader.load(Objects.requireNonNull(TriggerTest.class.getClassLoader().getResource("flows/s3/s3-listen-localhost-force-path-style.yaml"))); - int remainingFilesOnBucket = listTask.run(runContext(listTask)) - .getObjects() - .size(); - assertThat(remainingFilesOnBucket, is(0)); - } + boolean await = queueCount.await(15, TimeUnit.SECONDS); + assertThat("trigger should work with localhost endpoint + forcePathStyle", await, is(true)); + receive.blockLast(); + + @SuppressWarnings("unchecked") + java.util.List trigger = (java.util.List) last.get().getTrigger().getVariables().get("objects"); + + assertThat(trigger.size(), is(2)); + + int remainingFilesOnBucket = listTask.run(runContext(listTask)) + .getObjects() + .size(); + assertThat(remainingFilesOnBucket, is(0)); } @Test diff --git a/src/test/java/io/kestra/plugin/aws/sqs/RealtimeTriggerTest.java b/src/test/java/io/kestra/plugin/aws/sqs/RealtimeTriggerTest.java index f34a8c85..cba5a81e 100644 --- a/src/test/java/io/kestra/plugin/aws/sqs/RealtimeTriggerTest.java +++ b/src/test/java/io/kestra/plugin/aws/sqs/RealtimeTriggerTest.java @@ -2,26 +2,20 @@ import java.util.List; import java.util.Objects; -import java.util.UUID; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.Test; import org.testcontainers.containers.localstack.LocalStackContainer; +import io.kestra.core.junit.annotations.KestraTest; import io.kestra.core.models.executions.Execution; import io.kestra.core.models.property.Property; import io.kestra.core.queues.QueueFactoryInterface; import io.kestra.core.queues.QueueInterface; import io.kestra.core.repositories.LocalFlowRepositoryLoader; -import io.kestra.core.runners.FlowListeners; import io.kestra.core.utils.TestsUtils; -import io.kestra.jdbc.runner.JdbcScheduler; import io.kestra.plugin.aws.sqs.model.Message; -import io.kestra.scheduler.AbstractScheduler; -import io.kestra.worker.DefaultWorker; - -import io.micronaut.context.ApplicationContext; import jakarta.inject.Inject; import jakarta.inject.Named; import reactor.core.publisher.Flux; @@ -29,13 +23,8 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; +@KestraTest(startRunner = true, startScheduler = true) class RealtimeTriggerTest extends AbstractSqsTest { - @Inject - private ApplicationContext applicationContext; - - @Inject - private FlowListeners flowListenersService; - @Inject @Named(QueueFactoryInterface.EXECUTION_NAMED) private QueueInterface executionQueue; @@ -45,54 +34,35 @@ class RealtimeTriggerTest extends AbstractSqsTest { @Test void flow() throws Exception { - // mock flow listeners CountDownLatch queueCount = new CountDownLatch(1); - - // scheduler - DefaultWorker worker = applicationContext.createBean(DefaultWorker.class, UUID.randomUUID().toString(), 8, null); - try ( - AbstractScheduler scheduler = new JdbcScheduler( - this.applicationContext, - this.flowListenersService - ) - ) { - // wait for execution - Flux receive = TestsUtils.receive(executionQueue, execution -> - { - queueCount.countDown(); - assertThat(execution.getLeft().getFlowId(), is("realtime")); - }); - - worker.run(); - scheduler.run(); - - repositoryLoader.load(Objects.requireNonNull(RealtimeTriggerTest.class.getClassLoader().getResource("flows/sqs/realtime.yaml"))); - - // publish two messages to trigger the flow - Publish task = Publish.builder() - .endpointOverride(Property.ofValue(localstack.getEndpointOverride(LocalStackContainer.Service.SQS).toString())) - .queueUrl(Property.ofValue(queueUrl())) - .region(Property.ofValue(localstack.getRegion())) - .accessKeyId(Property.ofValue(localstack.getAccessKey())) - .secretKeyId(Property.ofValue(localstack.getSecretKey())) - .from( - List.of( - Message.builder().data("Hello World").build() - ) + Flux receive = TestsUtils.receive(executionQueue, execution -> { + queueCount.countDown(); + assertThat(execution.getLeft().getFlowId(), is("realtime")); + }); + + repositoryLoader.load(Objects.requireNonNull(RealtimeTriggerTest.class.getClassLoader().getResource("flows/sqs/realtime.yaml"))); + + Publish task = Publish.builder() + .endpointOverride(Property.ofValue(localstack.getEndpointOverride(LocalStackContainer.Service.SQS).toString())) + .queueUrl(Property.ofValue(queueUrl())) + .region(Property.ofValue(localstack.getRegion())) + .accessKeyId(Property.ofValue(localstack.getAccessKey())) + .secretKeyId(Property.ofValue(localstack.getSecretKey())) + .from( + List.of( + Message.builder().data("Hello World").build() ) - .build(); - - var runContext = runContextFactory.of(); + ) + .build(); - task.run(runContext); + task.run(runContextFactory.of()); - boolean await = queueCount.await(1, TimeUnit.MINUTES); - assertThat(await, is(true)); + boolean await = queueCount.await(1, TimeUnit.MINUTES); + assertThat(await, is(true)); - Execution last = receive.blockLast(); - assertThat(last.getTrigger().getVariables().size(), is(1)); - assertThat(last.getTrigger().getVariables().get("data"), is("Hello World")); - } + Execution last = receive.blockLast(); + assertThat(last.getTrigger().getVariables().size(), is(1)); + assertThat(last.getTrigger().getVariables().get("data"), is("Hello World")); } } diff --git a/src/test/java/io/kestra/plugin/aws/sqs/TriggerTest.java b/src/test/java/io/kestra/plugin/aws/sqs/TriggerTest.java index 49439e61..41b8b01d 100644 --- a/src/test/java/io/kestra/plugin/aws/sqs/TriggerTest.java +++ b/src/test/java/io/kestra/plugin/aws/sqs/TriggerTest.java @@ -2,27 +2,21 @@ import java.util.List; import java.util.Objects; -import java.util.UUID; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.Test; import org.testcontainers.containers.localstack.LocalStackContainer; +import io.kestra.core.junit.annotations.KestraTest; import io.kestra.core.models.executions.Execution; import io.kestra.core.models.property.Property; import io.kestra.core.queues.QueueFactoryInterface; import io.kestra.core.queues.QueueInterface; import io.kestra.core.repositories.LocalFlowRepositoryLoader; -import io.kestra.core.runners.FlowListeners; import io.kestra.core.runners.RunContextFactory; import io.kestra.core.utils.TestsUtils; -import io.kestra.jdbc.runner.JdbcScheduler; import io.kestra.plugin.aws.sqs.model.Message; -import io.kestra.scheduler.AbstractScheduler; -import io.kestra.worker.DefaultWorker; - -import io.micronaut.context.ApplicationContext; import jakarta.inject.Inject; import jakarta.inject.Named; import reactor.core.publisher.Flux; @@ -31,13 +25,8 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; +@KestraTest(startRunner = true, startScheduler = true) class TriggerTest extends AbstractSqsTest { - @Inject - private ApplicationContext applicationContext; - - @Inject - private FlowListeners flowListenersService; - @Inject @Named(QueueFactoryInterface.EXECUTION_NAMED) private QueueInterface executionQueue; @@ -50,57 +39,38 @@ class TriggerTest extends AbstractSqsTest { @Test void flow() throws Exception { - // mock flow listeners CountDownLatch queueCount = new CountDownLatch(1); - - // scheduler - DefaultWorker worker = applicationContext.createBean(DefaultWorker.class, UUID.randomUUID().toString(), 8, null); - try ( - AbstractScheduler scheduler = new JdbcScheduler( - this.applicationContext, - this.flowListenersService - ) - ) { - // wait for execution - Flux receive = TestsUtils.receive(executionQueue, execution -> - { - queueCount.countDown(); - assertThat(execution.getLeft().getFlowId(), is("sqs-listen")); - }); - - worker.run(); - scheduler.run(); - - repositoryLoader.load(Objects.requireNonNull(TriggerTest.class.getClassLoader().getResource("flows/sqs/sqs-listen.yaml"))); - - // publish two messages to trigger the flow - Publish task = Publish.builder() - .endpointOverride(Property.ofValue(localstack.getEndpointOverride(LocalStackContainer.Service.SQS).toString())) - .queueUrl(Property.ofValue(queueUrl())) - .region(Property.ofValue(localstack.getRegion())) - .accessKeyId(Property.ofValue(localstack.getAccessKey())) - .secretKeyId(Property.ofValue(localstack.getSecretKey())) - .from( - List.of( - Message.builder().data("Hello World").build(), - Message.builder().data("Hello Kestra").delaySeconds(5).build() - ) + Flux receive = TestsUtils.receive(executionQueue, execution -> { + queueCount.countDown(); + assertThat(execution.getLeft().getFlowId(), is("sqs-listen")); + }); + + repositoryLoader.load(Objects.requireNonNull(TriggerTest.class.getClassLoader().getResource("flows/sqs/sqs-listen.yaml"))); + + Publish task = Publish.builder() + .endpointOverride(Property.ofValue(localstack.getEndpointOverride(LocalStackContainer.Service.SQS).toString())) + .queueUrl(Property.ofValue(queueUrl())) + .region(Property.ofValue(localstack.getRegion())) + .accessKeyId(Property.ofValue(localstack.getAccessKey())) + .secretKeyId(Property.ofValue(localstack.getSecretKey())) + .from( + List.of( + Message.builder().data("Hello World").build(), + Message.builder().data("Hello Kestra").delaySeconds(5).build() ) - .build(); - - var runContext = runContextFactory.of(); + ) + .build(); - task.run(runContext); + task.run(runContextFactory.of()); - boolean await = queueCount.await(1, TimeUnit.MINUTES); - assertThat(await, is(true)); + boolean await = queueCount.await(1, TimeUnit.MINUTES); + assertThat(await, is(true)); - Execution last = receive.blockLast(); - var count = (Integer) last.getTrigger().getVariables().get("count"); - var uri = (String) last.getTrigger().getVariables().get("uri"); - assertThat(count, is(2)); - assertThat(uri, is(notNullValue())); - } + Execution last = receive.blockLast(); + var count = (Integer) last.getTrigger().getVariables().get("count"); + var uri = (String) last.getTrigger().getVariables().get("uri"); + assertThat(count, is(2)); + assertThat(uri, is(notNullValue())); } }