Copilot commented on code in PR #776:
URL: https://github.com/apache/unomi/pull/776#discussion_r3433349608


##########
services/src/test/java/org/apache/unomi/services/impl/TestEventAdmin.java:
##########
@@ -426,24 +418,23 @@ public int getHandlerCount() {
      */
     public boolean waitForEventProcessing(long timeoutMs) {
         long deadline = System.currentTimeMillis() + timeoutMs;
-
-        // Wait until all queues are drained AND all in-flight handleEvent 
calls have returned.
-        // Checking only queue.isEmpty() is insufficient: workers remove 
events from the queue
-        // before calling handleEvent, so the queue can appear empty while 
processing is ongoing.
-        while (System.currentTimeMillis() < deadline) {
-            boolean allQueuesEmpty = 
handlerQueues.values().stream().allMatch(BlockingQueue::isEmpty);
-            if (allQueuesEmpty && inFlightCount.get() == 0) {
-                return true;
+        
+        // Wait for all handler queues to be empty
+        for (BlockingQueue<Event> queue : handlerQueues.values()) {
+            while (!queue.isEmpty() && System.currentTimeMillis() < deadline) {
+                try {
+                    Thread.sleep(10);
+                } catch (InterruptedException e) {

Review Comment:
   `waitForEventProcessing` only waits for handler queues to become empty, but 
in `registerHandler` the worker removes events from the queue *before* calling 
`handleEvent`. This can make the queue empty while a handler is still 
processing, causing this method to return `true` too early and introduce test 
flakiness.



##########
services/src/test/java/org/apache/unomi/services/impl/TestEventAdmin.java:
##########
@@ -125,9 +117,13 @@ public class TestEventAdmin implements EventAdmin {
      */
     private final List<Event> sentEvents = new CopyOnWriteArrayList<>();
 
+    /**
+     * Creates a TestEventAdmin with a single-threaded executor for ordered 
event delivery.
+     */
     public TestEventAdmin() {
-        this.asyncExecutor = Executors.newCachedThreadPool(r -> {
-            Thread t = new Thread(r, "TestEventAdmin-Handler-" + 
System.identityHashCode(this));
+        // Use single-threaded executor to guarantee ordered delivery
+        this.asyncExecutor = Executors.newSingleThreadExecutor(r -> {
+            Thread t = new Thread(r, "TestEventAdmin-Async-" + 
System.identityHashCode(this));
             t.setDaemon(true);
             return t;
         });

Review Comment:
   `asyncExecutor` is created as a single-threaded executor, but 
`registerHandler` submits a long-lived worker per handler that blocks on 
`queue.take()`. With a single thread, the first worker will block forever and 
all subsequently registered handlers will never get a running worker, so their 
queued events will never be delivered.



##########
services/src/test/java/org/apache/unomi/services/impl/TestConditionEvaluators.java:
##########
@@ -431,7 +423,7 @@ private static boolean evaluateCollectionCondition(Object 
actualValue, Condition
         switch (operator) {
             case "in": return actual.stream().anyMatch(expected::contains);
             case "inContains": return actual.stream().anyMatch(a ->
-                (a instanceof String) && expected.stream().anyMatch(b -> (b 
instanceof String) && ((String) a).contains((String) b)));
+                expected.stream().anyMatch(b -> ((String) a).contains((String) 
b)));
             case "notIn": return actual.stream().noneMatch(expected::contains);

Review Comment:
   The `inContains` branch casts collection elements to `String` 
unconditionally. If `actual` or `expected` contains a non-string element, this 
will throw `ClassCastException` during evaluation.



##########
services/src/test/java/org/apache/unomi/services/impl/TestConditionEvaluators.java:
##########
@@ -373,13 +370,8 @@ private static boolean evaluateBetweenCondition(Object 
actualValue, Condition co
     private static boolean evaluateDateCondition(Object actualValue, Object 
expectedValueDate,
                                                Object expectedValueDateExpr, 
String operator) {
         Object expectedDate = expectedValueDate == null ? 
expectedValueDateExpr : expectedValueDate;
-        Date actualDateVal = getDate(actualValue);
-        Date expectedDateVal = getDate(expectedDate);
-        if (actualDateVal == null || expectedDateVal == null) {
-            return false;
-        }
-        boolean isSameDay = 
YEAR_MONTH_DAY_FORMAT.format(actualDateVal.toInstant())
-                
.equals(YEAR_MONTH_DAY_FORMAT.format(expectedDateVal.toInstant()));
+        boolean isSameDay = yearMonthDayDateFormat.format(getDate(actualValue))
+                .equals(yearMonthDayDateFormat.format(getDate(expectedDate)));
         return operator.equals("isDay") ? isSameDay : !isSameDay;

Review Comment:
   `evaluateDateCondition` now calls 
`yearMonthDayDateFormat.format(getDate(...))` without checking for null. 
`DateUtils.getDate` returns null for unparsable inputs, so this can throw 
`NullPointerException` and make evaluators fail unexpectedly.



##########
services/src/test/java/org/apache/unomi/services/impl/TestConditionEvaluators.java:
##########
@@ -532,13 +524,8 @@ private static ConditionEvaluator 
createPastEventConditionEvaluator() {
                 String key = (String) parameters.get("generatedPropertyKey");
                 tracer.trace(condition, "Using generated property key: " + 
key);
 
-                Object pastEventsObj = 
profile.getSystemProperties().get("pastEvents");
-                if (!(pastEventsObj instanceof List)) {
-                    tracer.trace(condition, "No pastEvents found in profile 
system properties");
-                    count = 0;
-                } else {
-                    @SuppressWarnings("unchecked")
-                    List<Map<String, Object>> pastEvents = (List<Map<String, 
Object>>) pastEventsObj;
+                List<Map<String, Object>> pastEvents = (ArrayList<Map<String, 
Object>>) profile.getSystemProperties().get("pastEvents");
+                if (pastEvents != null) {
                     tracer.trace(condition, "Found pastEvents in profile 
system properties");
                     Number l = (Number) pastEvents
                             .stream()

Review Comment:
   `pastEvents` is cast to `ArrayList<Map<String,Object>>`, which will throw 
`ClassCastException` if the stored list is another `List` implementation (or if 
the value isn't a list at all). The previous code handled missing/wrong types 
gracefully.



##########
services/src/test/java/org/apache/unomi/services/TestHelper.java:
##########
@@ -651,7 +796,7 @@ public static void cleanDefaultStorageDirectory(int 
maxRetries) {
                     return;
                 }
             } catch (IOException e) {
-                LOGGER.warn("Error deleting default storage directory, will 
retry", e);
+                LOGGER.warn("Error deleting default storage directory, will 
retry: {}", e.getMessage());
             }

Review Comment:
   This log line drops the exception stack trace by only logging 
`e.getMessage()`. When test cleanup fails intermittently on CI, having the full 
exception (and its causes) is important for diagnosis.



##########
services/src/test/java/org/apache/unomi/services/impl/TestConditionEvaluators.java:
##########
@@ -83,15 +86,9 @@ private static ConditionEvaluator 
createBooleanConditionEvaluator() {
         return (condition, item, context, dispatcher) -> {
             tracer.startOperation("boolean", "Evaluating boolean condition 
with operator: " + condition.getParameter("operator"), condition);
             String operator = (String) condition.getParameter("operator");
-            Object subConditionsParam = 
condition.getParameter("subConditions");
-            if (!(subConditionsParam instanceof List)) {
-                tracer.endOperation(true, "No subconditions found, returning 
true");
-                return true;
-            }
-            @SuppressWarnings("unchecked")
-            List<Condition> subConditions = (List<Condition>) 
subConditionsParam;
+            List<Condition> subConditions = (List<Condition>) 
condition.getParameter("subConditions");
 
-            if (subConditions.isEmpty()) {
+            if (subConditions == null || subConditions.isEmpty()) {
                 tracer.endOperation(true, "No subconditions found, returning 
true");

Review Comment:
   `subConditions` is cast directly to `List<Condition>` without checking the 
runtime type. If the parameter is missing or not a `List`, this will throw 
`ClassCastException` instead of behaving like the previous implementation 
(treating it as no subconditions).



##########
services/src/test/java/org/apache/unomi/services/impl/TestConditionEvaluators.java:
##########
@@ -916,7 +906,7 @@ private static Parameter createParameterRecommended(String 
name, String type, St
     }
 
     public static Map<String, ConditionType> getConditionTypes() {
-        return Collections.unmodifiableMap(conditionTypes);
+        return conditionTypes;
     }

Review Comment:
   `getConditionTypes()` returns the mutable static map directly. Because this 
map is shared across the entire test JVM, callers can accidentally mutate it 
and create cross-test interference; the previous unmodifiable view avoided that.



##########
services/src/test/java/org/apache/unomi/services/TestHelper.java:
##########
@@ -749,7 +894,7 @@ public static <T> T retryQueryUntilAvailable(Supplier<T> 
querySupplier, Integer
                 Thread.sleep(retryDelay);
             } catch (InterruptedException e) {
                 Thread.currentThread().interrupt();
-                throw new RuntimeException("Retry interrupted", e);
+                break;
             }

Review Comment:
   On `InterruptedException`, the loop breaks and the method later throws a 
generic timeout exception, discarding the original interrupt cause. Tests that 
are interrupted (timeouts, shutdown) should preserve the interrupt reason to 
avoid confusing failures.



##########
services/src/test/java/org/apache/unomi/services/impl/TestBundleContext.java:
##########
@@ -149,7 +148,7 @@ public <S> ServiceReference<S> getServiceReference(Class<S> 
clazz) {
 
     @Override
     public <S> Collection<ServiceReference<S>> getServiceReferences(Class<S> 
clazz, String filter) {
-        return Collections.emptyList();
+        return null;
     }

Review Comment:
   `BundleContext#getServiceReferences(Class, String)` should return an empty 
collection when no references exist; returning `null` can cause 
`NullPointerException` in code under test that iterates over the result.



##########
services/src/test/java/org/apache/unomi/services/impl/segments/SegmentServiceImplTest.java:
##########
@@ -0,0 +1,1163 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.unomi.services.impl.segments;
+
+import org.apache.unomi.api.Event;
+import org.apache.unomi.api.Metadata;
+import org.apache.unomi.api.PartialList;
+import org.apache.unomi.api.Profile;
+import org.apache.unomi.api.actions.Action;
+import org.apache.unomi.api.conditions.Condition;
+import org.apache.unomi.api.conditions.ConditionType;
+import org.apache.unomi.api.exceptions.BadSegmentConditionException;
+import org.apache.unomi.api.rules.Rule;
+import org.apache.unomi.api.segments.Segment;
+import org.apache.unomi.api.segments.SegmentsAndScores;
+import org.apache.unomi.api.services.EventService;
+import org.apache.unomi.api.services.SchedulerService;
+import org.apache.unomi.api.services.cache.CacheableTypeConfig;
+import org.apache.unomi.persistence.spi.PersistenceService;
+import 
org.apache.unomi.persistence.spi.conditions.evaluator.ConditionEvaluatorDispatcher;
+import org.apache.unomi.services.TestHelper;
+import org.apache.unomi.services.common.security.ExecutionContextManagerImpl;
+import org.apache.unomi.services.common.security.KarafSecurityService;
+import org.apache.unomi.services.impl.InMemoryPersistenceServiceImpl;
+import org.apache.unomi.services.impl.TestConditionEvaluators;
+import org.apache.unomi.services.impl.TestEventAdmin;
+import org.apache.unomi.services.impl.TestTenantService;
+import org.apache.unomi.services.impl.cache.MultiTypeCacheServiceImpl;
+import org.apache.unomi.services.impl.definitions.DefinitionsServiceImpl;
+import org.apache.unomi.services.impl.events.EventServiceImpl;
+import org.apache.unomi.services.impl.rules.RulesServiceImpl;
+import org.apache.unomi.services.impl.rules.TestActionExecutorDispatcher;
+import org.apache.unomi.services.impl.rules.TestEvaluateProfileSegmentsAction;
+import org.apache.unomi.services.impl.rules.TestSetEventOccurrenceCountAction;
+import org.apache.unomi.tracing.api.RequestTracer;
+import org.apache.unomi.tracing.api.TracerService;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.junit.jupiter.MockitoSettings;
+import org.mockito.quality.Strictness;
+import org.osgi.framework.BundleContext;
+
+import java.io.IOException;
+import java.net.URL;
+import java.util.*;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+@MockitoSettings(strictness = Strictness.LENIENT)
+public class SegmentServiceImplTest {
+
+    private SegmentServiceImpl segmentService;
+    private EventServiceImpl eventService;
+    private TestTenantService tenantService;
+    private PersistenceService persistenceService;
+    private DefinitionsServiceImpl definitionsService;
+    private MultiTypeCacheServiceImpl multiTypeCacheService;
+    private ExecutionContextManagerImpl executionContextManager;
+    private KarafSecurityService securityService;
+    private RulesServiceImpl rulesService;
+    private TestActionExecutorDispatcher actionExecutorDispatcher;
+
+    private BundleContext bundleContext;
+    private TracerService tracerService;
+    private RequestTracer requestTracer;
+
+    private SchedulerService schedulerService;
+
+    private static final String TENANT_1 = "tenant1";
+    private static final String TENANT_2 = "tenant2";
+    private static final String SYSTEM_TENANT = "system";
+
+    @BeforeEach
+    public void setUp() throws IOException {
+
+        tenantService = new TestTenantService();
+        securityService = TestHelper.createSecurityService();
+        executionContextManager = 
TestHelper.createExecutionContextManager(securityService);
+        TracerService tracerService = TestHelper.createTracerService();
+
+        // Create tenants using TestHelper
+        TestHelper.setupCommonTestData(tenantService);
+
+        ConditionEvaluatorDispatcher conditionEvaluatorDispatcher = 
TestConditionEvaluators.createDispatcher();
+
+        // Set up bundle context using TestHelper
+        bundleContext = TestHelper.createMockBundleContext();
+
+        persistenceService = new 
InMemoryPersistenceServiceImpl(executionContextManager, 
conditionEvaluatorDispatcher);
+
+        // Create scheduler service using TestHelper
+        schedulerService = 
TestHelper.createSchedulerService("segment-service-scheduler-node", 
persistenceService, executionContextManager, bundleContext, null, -1, true, 
true);
+
+        multiTypeCacheService = new MultiTypeCacheServiceImpl();
+
+        // Create definitions service with EventAdmin
+        java.util.Map.Entry<DefinitionsServiceImpl, TestEventAdmin> 
servicePair =
+            
TestHelper.createDefinitionServiceWithEventAdmin(persistenceService, 
bundleContext, schedulerService,
+                multiTypeCacheService, executionContextManager, tenantService);
+        definitionsService = servicePair.getKey();
+        TestEventAdmin testEventAdmin = servicePair.getValue();
+
+        // Inject definitionsService into the dispatcher
+        
TestHelper.injectDefinitionsServiceIntoDispatcher(conditionEvaluatorDispatcher, 
definitionsService);
+
+        TestConditionEvaluators.getConditionTypes().forEach((key, value) -> 
definitionsService.setConditionType(value));
+
+        // Set up event service using TestHelper
+        eventService = TestHelper.createEventService(persistenceService, 
bundleContext, definitionsService, tenantService, tracerService);
+        TestConditionEvaluators.setEventService(eventService);
+
+        // Set up action executor dispatcher
+        actionExecutorDispatcher = new 
TestActionExecutorDispatcher(definitionsService, persistenceService);
+        
actionExecutorDispatcher.setDefaultReturnValue(EventService.PROFILE_UPDATED);
+        actionExecutorDispatcher.setTracer(requestTracer);
+
+        // Set up rules service using TestHelper with EventAdmin
+        rulesService = TestHelper.createRulesService(persistenceService, 
bundleContext, schedulerService, definitionsService, eventService, 
executionContextManager, tenantService, multiTypeCacheService, 
actionExecutorDispatcher, testEventAdmin);
+        rulesService.setTracerService(tracerService);
+
+        // Set up segment service
+        segmentService = new SegmentServiceImpl();
+        segmentService.setBundleContext(bundleContext);
+        segmentService.setPersistenceService(persistenceService);
+        segmentService.setDefinitionsService(definitionsService);
+        segmentService.setRulesService(rulesService);
+        segmentService.setEventService(eventService);
+        segmentService.setContextManager(executionContextManager);
+        segmentService.setSchedulerService(schedulerService);
+        segmentService.setCacheService(multiTypeCacheService);
+        segmentService.setTenantService(tenantService);
+        segmentService.setTracerService(tracerService);
+
+        actionExecutorDispatcher.addExecutor("setEventOccurenceCountAction",
+                new TestSetEventOccurrenceCountAction(definitionsService, 
persistenceService));
+        actionExecutorDispatcher.addExecutor("evaluateProfileSegments",
+                new TestEvaluateProfileSegmentsAction(segmentService));
+
+        // Register TestEvaluateProfileSegmentsAction
+        actionExecutorDispatcher.addExecutor("evaluateProfileSegments", new 
TestEvaluateProfileSegmentsAction(segmentService));
+

Review Comment:
   `evaluateProfileSegments` executor is registered twice. This is redundant 
and makes it easier for future edits to accidentally diverge (e.g., different 
executor instance/config).



##########
services/src/test/java/org/apache/unomi/services/impl/rules/TestSetEventOccurrenceCountAction.java:
##########
@@ -32,7 +32,7 @@
 
 /**
  * Test implementation of the SetEventOccurrenceCountAction.
- * TODO: This is a duplicate of the SetEventOccurrenceCountAction in the 
unomi-plugins-base project to avoid introducing a dependency on it but should 
be cleaned up.
+ * @TODO This is a duplicate of the SetEventOccurrenceCountAction in the 
unomi-plugins-base project to avoid introducing a depending to it but should be 
cleaned up.
  */

Review Comment:
   Javadoc typo/grammar: "introducing a depending" should be "introducing a 
dependency". Also `@TODO` isn't a standard Javadoc tag; use `TODO:` in plain 
text to avoid Javadoc tooling warnings.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to