You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@felix.apache.org by gh...@apache.org on 2018/12/18 22:51:49 UTC

svn commit: r1849243 [4/5] - in /felix/trunk: api/ api/src/ api/src/main/ api/src/main/java/ api/src/main/java/org/ api/src/main/java/org/apache/ api/src/main/java/org/apache/felix/ api/src/main/java/org/apache/felix/hc/ api/src/main/java/org/apache/fe...

Added: felix/trunk/core/src/main/java/org/apache/felix/hc/jmx/impl/HealthCheckMBean.java
URL: http://svn.apache.org/viewvc/felix/trunk/core/src/main/java/org/apache/felix/hc/jmx/impl/HealthCheckMBean.java?rev=1849243&view=auto
==============================================================================
--- felix/trunk/core/src/main/java/org/apache/felix/hc/jmx/impl/HealthCheckMBean.java (added)
+++ felix/trunk/core/src/main/java/org/apache/felix/hc/jmx/impl/HealthCheckMBean.java Tue Dec 18 22:51:48 2018
@@ -0,0 +1,258 @@
+/*
+ * 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 SF 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.felix.hc.jmx.impl;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.management.Attribute;
+import javax.management.AttributeList;
+import javax.management.AttributeNotFoundException;
+import javax.management.DynamicMBean;
+import javax.management.InvalidAttributeValueException;
+import javax.management.MBeanAttributeInfo;
+import javax.management.MBeanException;
+import javax.management.MBeanInfo;
+import javax.management.ReflectionException;
+import javax.management.openmbean.CompositeDataSupport;
+import javax.management.openmbean.CompositeType;
+import javax.management.openmbean.OpenDataException;
+import javax.management.openmbean.OpenMBeanAttributeInfoSupport;
+import javax.management.openmbean.OpenType;
+import javax.management.openmbean.SimpleType;
+import javax.management.openmbean.TabularData;
+import javax.management.openmbean.TabularDataSupport;
+import javax.management.openmbean.TabularType;
+
+import org.apache.felix.hc.api.HealthCheck;
+import org.apache.felix.hc.api.Result;
+import org.apache.felix.hc.api.ResultLog;
+import org.apache.felix.hc.api.execution.HealthCheckExecutionResult;
+import org.apache.felix.hc.core.impl.executor.ExtendedHealthCheckExecutor;
+import org.osgi.framework.Constants;
+import org.osgi.framework.ServiceReference;
+
+/** A {@link DynamicMBean} used to execute a {@link HealthCheck} service */
+public class HealthCheckMBean implements DynamicMBean {
+
+    private static final String HC_OK_ATTRIBUTE_NAME = "ok";
+    private static final String HC_STATUS_ATTRIBUTE_NAME = "status";
+    private static final String HC_LOG_ATTRIBUTE_NAME = "log";
+    private static final String HC_TIMED_OUT_ATTRIBUTE_NAME = "timedOut";
+    private static final String HC_ELAPSED_TIMED_ATTRIBUTE_NAME = "elapsedTime";
+    private static final String HC_FINISHED_AT_ATTRIBUTE_NAME = "finishedAt";
+    private static CompositeType LOG_ROW_TYPE;
+    private static TabularType LOG_TABLE_TYPE;
+
+    private static final String INDEX_COLUMN = "index";
+    private static final String LEVEL_COLUMN = "level";
+    private static final String MESSAGE_COLUMN = "message";
+
+    /** The health check service to call. */
+    private final ServiceReference<HealthCheck> healthCheckRef;
+
+    /** The executor service. */
+    private final ExtendedHealthCheckExecutor executor;
+
+    /** The mbean info. */
+    private final MBeanInfo mbeanInfo;
+
+    /** The default attributes. */
+    private final Map<String, Object> defaultAttributes;
+
+    static {
+        try {
+            // Define the log row and table types
+            LOG_ROW_TYPE = new CompositeType(
+                    "LogLine",
+                    "A line in the result log",
+                    new String[] { INDEX_COLUMN, LEVEL_COLUMN, MESSAGE_COLUMN },
+                    new String[] { "log line index", "log level", "log message" },
+                    new OpenType[] { SimpleType.INTEGER, SimpleType.STRING, SimpleType.STRING });
+            final String[] indexes = { INDEX_COLUMN };
+            LOG_TABLE_TYPE = new TabularType("LogTable", "Result log messages", LOG_ROW_TYPE, indexes);
+        } catch (Exception ignore) {
+            // row or table type will be null if this happens
+        }
+    }
+
+    public HealthCheckMBean(final ServiceReference<HealthCheck> ref, final ExtendedHealthCheckExecutor executor) {
+        this.healthCheckRef = ref;
+        this.executor = executor;
+        this.mbeanInfo = this.createMBeanInfo(ref);
+        this.defaultAttributes = this.createDefaultAttributes(ref);
+    }
+
+    @Override
+    public Object getAttribute(final String attribute)
+            throws AttributeNotFoundException, MBeanException, ReflectionException {
+        // we should call getAttributes - and not vice versa to have the result
+        // of a single check call - and not do a check call for each attribute
+        final AttributeList result = this.getAttributes(new String[] { attribute });
+        if (result.size() == 0) {
+            throw new AttributeNotFoundException(attribute);
+        }
+        final Attribute attr = (Attribute) result.get(0);
+        return attr.getValue();
+    }
+
+    private TabularData logData(final Result er) throws OpenDataException {
+        final TabularDataSupport result = new TabularDataSupport(LOG_TABLE_TYPE);
+        int i = 1;
+        for (final ResultLog.Entry e : er) {
+            final Map<String, Object> data = new HashMap<String, Object>();
+            data.put(INDEX_COLUMN, i++);
+            data.put(LEVEL_COLUMN, e.getStatus().toString());
+            data.put(MESSAGE_COLUMN, e.getMessage());
+
+            result.put(new CompositeDataSupport(LOG_ROW_TYPE, data));
+        }
+        return result;
+    }
+
+    @Override
+    public AttributeList getAttributes(final String[] attributes) {
+        final AttributeList result = new AttributeList();
+        if (attributes != null) {
+            HealthCheckExecutionResult hcResult = null;
+            for (final String key : attributes) {
+                final Object defaultValue = this.defaultAttributes.get(key);
+                if (defaultValue != null) {
+                    result.add(new Attribute(key, defaultValue));
+                } else {
+                    // we assume that a valid attribute name is used
+                    // which is requesting a hc result
+                    if (hcResult == null) {
+                        hcResult = this.getHealthCheckResult();
+                    }
+
+                    if (HC_OK_ATTRIBUTE_NAME.equals(key)) {
+                        result.add(new Attribute(key, hcResult.getHealthCheckResult().isOk()));
+                    } else if (HC_LOG_ATTRIBUTE_NAME.equals(key)) {
+                        try {
+                            result.add(new Attribute(key, logData(hcResult.getHealthCheckResult())));
+                        } catch (final OpenDataException ignore) {
+                            // we ignore this and simply don't add the attribute
+                        }
+                    } else if (HC_STATUS_ATTRIBUTE_NAME.equals(key)) {
+                        result.add(new Attribute(key, hcResult.getHealthCheckResult().getStatus().toString()));
+                    } else if (HC_ELAPSED_TIMED_ATTRIBUTE_NAME.equals(key)) {
+                        result.add(new Attribute(key, hcResult.getElapsedTimeInMs()));
+                    } else if (HC_FINISHED_AT_ATTRIBUTE_NAME.equals(key)) {
+                        result.add(new Attribute(key, hcResult.getFinishedAt()));
+                    } else if (HC_TIMED_OUT_ATTRIBUTE_NAME.equals(key)) {
+                        result.add(new Attribute(key, hcResult.hasTimedOut()));
+                    }
+                }
+            }
+        }
+
+        return result;
+    }
+
+    /** Create the mbean info */
+    private MBeanInfo createMBeanInfo(final ServiceReference<HealthCheck> serviceReference) {
+        final List<MBeanAttributeInfo> attrs = new ArrayList<MBeanAttributeInfo>();
+
+        // add relevant service properties
+        if (serviceReference.getProperty(HealthCheck.NAME) != null) {
+            attrs.add(new MBeanAttributeInfo(HealthCheck.NAME, String.class.getName(), "The name of the health check service.", true, false,
+                    false));
+        }
+        if (serviceReference.getProperty(HealthCheck.TAGS) != null) {
+            attrs.add(new MBeanAttributeInfo(HealthCheck.TAGS, String.class.getName(), "The tags of the health check service.", true, false,
+                    false));
+        }
+
+        // add standard attributes
+        attrs.add(new MBeanAttributeInfo(HC_OK_ATTRIBUTE_NAME, Boolean.class.getName(), "The health check result", true, false, false));
+        attrs.add(new MBeanAttributeInfo(HC_STATUS_ATTRIBUTE_NAME, String.class.getName(), "The health check status", true, false, false));
+        attrs.add(new MBeanAttributeInfo(HC_ELAPSED_TIMED_ATTRIBUTE_NAME, Long.class.getName(), "The elapsed time in miliseconds", true,
+                false, false));
+        attrs.add(new MBeanAttributeInfo(HC_FINISHED_AT_ATTRIBUTE_NAME, Date.class.getName(), "The date when the execution finished", true,
+                false, false));
+        attrs.add(new MBeanAttributeInfo(HC_TIMED_OUT_ATTRIBUTE_NAME, Boolean.class.getName(), "Indicates of the execution timed out", true,
+                false, false));
+        attrs.add(new OpenMBeanAttributeInfoSupport(HC_LOG_ATTRIBUTE_NAME, "The health check result log", LOG_TABLE_TYPE, true, false,
+                false));
+
+        final String description;
+        if (serviceReference.getProperty(Constants.SERVICE_DESCRIPTION) != null) {
+            description = serviceReference.getProperty(Constants.SERVICE_DESCRIPTION).toString();
+        } else {
+            description = "Health check";
+        }
+        return new MBeanInfo(this.getClass().getName(),
+                description,
+                attrs.toArray(new MBeanAttributeInfo[attrs.size()]), null, null, null);
+    }
+
+    /** Create the default attributes. */
+    private Map<String, Object> createDefaultAttributes(final ServiceReference<HealthCheck> serviceReference) {
+        final Map<String, Object> list = new HashMap<String, Object>();
+        if (serviceReference.getProperty(HealthCheck.NAME) != null) {
+            list.put(HealthCheck.NAME, serviceReference.getProperty(HealthCheck.NAME).toString());
+        }
+        if (serviceReference.getProperty(HealthCheck.TAGS) != null) {
+            final Object value = serviceReference.getProperty(HealthCheck.TAGS);
+            if (value instanceof String[]) {
+                list.put(HealthCheck.TAGS, Arrays.toString((String[]) value));
+            } else {
+                list.put(HealthCheck.TAGS, value.toString());
+            }
+        }
+
+        return list;
+    }
+
+    @Override
+    public MBeanInfo getMBeanInfo() {
+        return this.mbeanInfo;
+    }
+
+    @Override
+    public Object invoke(final String actionName, final Object[] params, final String[] signature)
+            throws MBeanException, ReflectionException {
+        throw new MBeanException(new UnsupportedOperationException(getClass().getSimpleName() + " does not support operations."));
+    }
+
+    @Override
+    public void setAttribute(final Attribute attribute)
+            throws AttributeNotFoundException, InvalidAttributeValueException,
+            MBeanException, ReflectionException {
+        throw new MBeanException(new UnsupportedOperationException(getClass().getSimpleName() + " does not support setting attributes."));
+    }
+
+    @Override
+    public AttributeList setAttributes(final AttributeList attributes) {
+        return new AttributeList();
+    }
+
+    @Override
+    public String toString() {
+        return "HealthCheckMBean [healthCheck=" + this.healthCheckRef + "]";
+    }
+
+    private HealthCheckExecutionResult getHealthCheckResult() {
+        return this.executor.execute(this.healthCheckRef);
+    }
+}
\ No newline at end of file

Added: felix/trunk/core/src/main/java/org/apache/felix/hc/jmx/impl/HealthCheckMBeanCreator.java
URL: http://svn.apache.org/viewvc/felix/trunk/core/src/main/java/org/apache/felix/hc/jmx/impl/HealthCheckMBeanCreator.java?rev=1849243&view=auto
==============================================================================
--- felix/trunk/core/src/main/java/org/apache/felix/hc/jmx/impl/HealthCheckMBeanCreator.java (added)
+++ felix/trunk/core/src/main/java/org/apache/felix/hc/jmx/impl/HealthCheckMBeanCreator.java Tue Dec 18 22:51:48 2018
@@ -0,0 +1,185 @@
+/*
+ * 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 SF 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.felix.hc.jmx.impl;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Dictionary;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Map;
+
+import javax.management.DynamicMBean;
+
+import org.apache.felix.hc.api.HealthCheck;
+import org.apache.felix.hc.core.impl.executor.ExtendedHealthCheckExecutor;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceReference;
+import org.osgi.framework.ServiceRegistration;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.util.tracker.ServiceTracker;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Creates an {@link HealthCheckMBean} for every {@link HealthCheckMBean} service */
+@Component
+public class HealthCheckMBeanCreator {
+
+    private static final String JMX_TYPE_NAME = "HealthCheck";
+    private static final String JMX_DOMAIN = "org.apache.felix.healthcheck";
+
+    private final Logger logger = LoggerFactory.getLogger(getClass());
+
+    private final Map<ServiceReference<HealthCheck>, Registration> registeredServices = new HashMap<>();
+
+    private final Map<String, List<ServiceReference<HealthCheck>>> sortedRegistrations = new HashMap<>();
+
+    private ServiceTracker<HealthCheck, ?> hcTracker;
+
+    @Reference
+    private ExtendedHealthCheckExecutor executor;
+
+    @Activate
+    protected void activate(final BundleContext btx) {
+        this.hcTracker = new ServiceTracker<HealthCheck, Object>(btx, HealthCheck.class, null) {
+
+            @Override
+            public Object addingService(final ServiceReference<HealthCheck> reference) {
+                return registerHCMBean(btx, reference);
+            }
+
+            @Override
+            public void modifiedService(final ServiceReference<HealthCheck> reference,
+                    final Object service) {
+                unregisterHCMBean(btx, reference);
+                registerHCMBean(btx, reference);
+            }
+
+            @Override
+            public void removedService(final ServiceReference<HealthCheck> reference,
+                    final Object service) {
+                unregisterHCMBean(btx, reference);
+            }
+        };
+        this.hcTracker.open();
+    }
+
+    @Deactivate
+    protected void deactivate() {
+        if (this.hcTracker != null) {
+            this.hcTracker.close();
+            this.hcTracker = null;
+        }
+    }
+
+    /** Register an mbean for a health check service. The mbean is only registered if - the service has an mbean registration property - if
+     * there is no other service with the same name but a higher service ranking
+     *
+     * @param bundleContext The bundle context
+     * @param reference The service reference to the health check service
+     * @return The registered mbean or <code>null</code> */
+    private synchronized Object registerHCMBean(final BundleContext bundleContext, final ServiceReference<HealthCheck> reference) {
+        final Registration reg = getRegistration(reference);
+        if (reg != null) {
+            this.registeredServices.put(reference, reg);
+
+            List<ServiceReference<HealthCheck>> registered = this.sortedRegistrations.get(reg.name);
+            if (registered == null) {
+                registered = new ArrayList<>();
+                this.sortedRegistrations.put(reg.name, registered);
+            }
+            registered.add(reference);
+            // sort orders the references with lowest ranking first
+            // we want the highest!
+            Collections.sort(registered);
+            final int lastIndex = registered.size() - 1;
+            if (registered.get(lastIndex).equals(reference)) {
+                if (registered.size() > 1) {
+                    final ServiceReference<HealthCheck> prevRef = registered.get(lastIndex - 1);
+                    final Registration prevReg = this.registeredServices.get(prevRef);
+                    prevReg.unregister();
+                }
+                reg.register(bundleContext);
+            }
+        }
+        return reg;
+    }
+
+    private synchronized void unregisterHCMBean(final BundleContext bundleContext, final ServiceReference<HealthCheck> ref) {
+        final Registration reg = registeredServices.remove(ref);
+        if (reg != null) {
+            final boolean registerFirst = reg.unregister();
+            final List<ServiceReference<HealthCheck>> registered = this.sortedRegistrations.get(reg.name);
+            registered.remove(ref);
+            if (registered.size() == 0) {
+                this.sortedRegistrations.remove(reg.name);
+            } else if (registerFirst) {
+                final ServiceReference<HealthCheck> newRef = registered.get(0);
+                final Registration newReg = this.registeredServices.get(newRef);
+                newReg.register(bundleContext);
+            }
+            bundleContext.ungetService(ref);
+        }
+    }
+
+    private final class Registration {
+        private final String name;
+        private final HealthCheckMBean mbean;
+
+        private final String objectName;
+
+        private ServiceRegistration<DynamicMBean> registration;
+
+        Registration(final String name, final HealthCheckMBean mbean) {
+            this.name = name;
+            this.mbean = mbean;
+            objectName = String.format("%s:type=%s,name=%s", JMX_DOMAIN, JMX_TYPE_NAME, name);
+        }
+
+        void register(final BundleContext btx) {
+            logger.debug("Registering health check mbean {} with name {}", mbean, objectName);
+            final Dictionary<String, String> mbeanProps = new Hashtable<String, String>();
+            mbeanProps.put("jmx.objectname", objectName);
+            this.registration = btx.registerService(DynamicMBean.class, this.mbean, mbeanProps);
+        }
+
+        boolean unregister() {
+            if (this.registration != null) {
+                logger.debug("Unregistering health check mbean {} with name {}", mbean, objectName);
+                this.registration.unregister();
+                this.registration = null;
+                return true;
+            }
+            return false;
+        }
+    }
+
+    private Registration getRegistration(final ServiceReference<HealthCheck> ref) {
+        final Object nameObj = ref.getProperty(HealthCheck.MBEAN_NAME);
+        if (nameObj != null) {
+            final HealthCheckMBean mbean = new HealthCheckMBean(ref, executor);
+            return new Registration(nameObj.toString().replace(',', '.'), mbean);
+        }
+        return null;
+    }
+
+}

Added: felix/trunk/core/src/test/java/org/apache/felix/hc/core/impl/CompositeHealthCheckTest.java
URL: http://svn.apache.org/viewvc/felix/trunk/core/src/test/java/org/apache/felix/hc/core/impl/CompositeHealthCheckTest.java?rev=1849243&view=auto
==============================================================================
--- felix/trunk/core/src/test/java/org/apache/felix/hc/core/impl/CompositeHealthCheckTest.java (added)
+++ felix/trunk/core/src/test/java/org/apache/felix/hc/core/impl/CompositeHealthCheckTest.java Tue Dec 18 22:51:48 2018
@@ -0,0 +1,293 @@
+/*
+ * 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 SF 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.felix.hc.core.impl;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.argThat;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+
+import org.apache.felix.hc.api.HealthCheck;
+import org.apache.felix.hc.api.Result;
+import org.apache.felix.hc.api.execution.HealthCheckExecutionOptions;
+import org.apache.felix.hc.api.execution.HealthCheckExecutionResult;
+import org.apache.felix.hc.api.execution.HealthCheckExecutor;
+import org.apache.felix.hc.api.execution.HealthCheckSelector;
+import org.apache.felix.hc.core.impl.executor.ExecutionResult;
+import org.apache.felix.hc.util.HealthCheckFilter;
+import org.apache.felix.hc.util.HealthCheckMetadata;
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.hamcrest.TypeSafeMatcher;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Matchers;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.Spy;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.Constants;
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.component.ComponentConstants;
+import org.osgi.service.component.ComponentContext;
+
+public class CompositeHealthCheckTest {
+
+    @Spy
+    private CompositeHealthCheck compositeHealthCheck = new CompositeHealthCheck();
+
+    @Mock
+    private HealthCheckExecutor healthCheckExecutor;
+
+    @Mock
+    private ComponentContext componentContext;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        compositeHealthCheck.setHealthCheckExecutor(healthCheckExecutor);
+        compositeHealthCheck.setFilterTags(new String[] {});
+        compositeHealthCheck.setComponentContext(componentContext);
+    }
+
+    @Test
+    public void testExecution() {
+
+        doReturn((Result) null).when(compositeHealthCheck).checkForRecursion(Matchers.<ServiceReference> any(),
+                Matchers.<Set<String>> any());
+        String[] testTags = new String[] { "tag1" };
+        compositeHealthCheck.setFilterTags(testTags);
+
+        List<HealthCheckExecutionResult> executionResults = new LinkedList<HealthCheckExecutionResult>();
+        executionResults.add(createExecutionResult("Check 1", testTags, new Result(Result.Status.OK, "Good")));
+        executionResults.add(createExecutionResult("Check 2", testTags, new Result(Result.Status.CRITICAL, "Bad")));
+
+        when(healthCheckExecutor.execute(any(HealthCheckSelector.class), any(HealthCheckExecutionOptions.class)))
+                .thenReturn(executionResults);
+
+        Result result = compositeHealthCheck.execute();
+
+        verify(healthCheckExecutor, times(1)).execute(argThat(selectorWithTags(testTags)), argThat(andOptions));
+
+        assertEquals(Result.Status.CRITICAL, result.getStatus());
+
+    }
+
+    private Matcher<HealthCheckSelector> selectorWithTags(final String[] tags) {
+        return new TypeSafeMatcher<HealthCheckSelector>() {
+            @Override
+            protected boolean matchesSafely(HealthCheckSelector healthCheckSelector) {
+                return Arrays.equals(healthCheckSelector.tags(), tags) && healthCheckSelector.names() == null;
+            }
+
+            @Override
+            public void describeTo(Description description) {
+                description.appendText("a select with tags (" + Arrays.toString(tags) + ") and no names.");
+            }
+        };
+    }
+
+    private HealthCheckExecutionResult createExecutionResult(String name, String[] testTags, Result result) {
+        HealthCheckExecutionResult healthCheckExecutionResult = new ExecutionResult(
+                new HealthCheckMetadata(new DummyHcServiceReference(name, testTags,
+                        new String[0])),
+                result, 0L);
+        return healthCheckExecutionResult;
+    }
+
+    @Test
+    public void testSimpleRecursion() {
+
+        // composite check referencing itself
+        final String[] filterTags = new String[] { "check1" };
+        final DummyHcServiceReference hcRef = new DummyHcServiceReference("Check 1", new String[] { "check1" }, filterTags);
+
+        // test check is hcRef
+        doReturn(hcRef).when(componentContext).getServiceReference();
+        compositeHealthCheck.setFilterTags(filterTags);
+
+        compositeHealthCheck.setHealthCheckFilter(new HealthCheckFilter(null) {
+
+            @Override
+            public ServiceReference[] getHealthCheckServiceReferences(HealthCheckSelector selector) {
+                String[] tags = selector.tags();
+                ServiceReference[] result = new ServiceReference[] {};
+                if (tags.length > 0) {
+                    if (tags[0].equals(filterTags[0])) {
+                        result = new ServiceReference[] { hcRef };
+                    }
+                }
+                return result;
+            }
+
+        });
+
+        Result result = compositeHealthCheck.execute();
+
+        verify(healthCheckExecutor, never()).execute(any(HealthCheckSelector.class));
+        assertEquals(Result.Status.HEALTH_CHECK_ERROR, result.getStatus());
+    }
+
+    @Test
+    public void testCyclicRecursion() {
+
+        // three checks, cyclic
+        final String[] filterTags = new String[] { "check2" };
+        final DummyHcServiceReference hcRef1 = new DummyHcServiceReference("Check 1", new String[] { "check1" }, filterTags);
+        final DummyHcServiceReference hcRef2 = new DummyHcServiceReference("Check 2", new String[] { "check2" }, new String[] { "check3" });
+        final DummyHcServiceReference hcRef3 = new DummyHcServiceReference("Check 3", new String[] { "check3" }, new String[] { "check1" });
+
+        // test check is hcRef1
+        doReturn(hcRef1).when(componentContext).getServiceReference();
+        compositeHealthCheck.setFilterTags(filterTags);
+
+        compositeHealthCheck.setHealthCheckFilter(new HealthCheckFilter(null) {
+
+            @Override
+            public ServiceReference[] getHealthCheckServiceReferences(HealthCheckSelector selector, boolean combineTagsWithOr) {
+                String[] tags = selector.tags();
+                ServiceReference[] result = new ServiceReference[] {};
+                if (tags.length > 0) {
+                    if (tags[0].equals(filterTags[0])) {
+                        result = new ServiceReference[] { hcRef2 };
+                    } else if (tags[0].equals("check3")) {
+                        result = new ServiceReference[] { hcRef3 };
+                    } else if (tags[0].equals("check1")) {
+                        result = new ServiceReference[] { hcRef1 };
+                    }
+                }
+
+                return result;
+            }
+
+        });
+
+        Result result = compositeHealthCheck.execute();
+
+        verify(healthCheckExecutor, never()).execute(any(HealthCheckSelector.class));
+        assertEquals(Result.Status.HEALTH_CHECK_ERROR, result.getStatus());
+    }
+
+    @Test
+    public void testCombineWithOr() {
+
+        // composite check referencing itself
+        final String[] filterTags = new String[] { "check1" };
+        compositeHealthCheck.setFilterTags(filterTags);
+        compositeHealthCheck.setCombineTagsWithOr(true);
+
+        compositeHealthCheck.execute();
+
+        verify(healthCheckExecutor, times(1)).execute(argThat(selectorWithTags(filterTags)), argThat(orOptions));
+    }
+
+    private Matcher<HealthCheckExecutionOptions> orOptions = new TypeSafeMatcher<HealthCheckExecutionOptions>() {
+        @Override
+        protected boolean matchesSafely(HealthCheckExecutionOptions options) {
+            return options.isCombineTagsWithOr();
+        }
+
+        @Override
+        public void describeTo(Description description) {
+            description.appendText("options combining tags with or.");
+        }
+    };
+
+    private Matcher<HealthCheckExecutionOptions> andOptions = new TypeSafeMatcher<HealthCheckExecutionOptions>() {
+        @Override
+        protected boolean matchesSafely(HealthCheckExecutionOptions options) {
+            return !options.isCombineTagsWithOr();
+        }
+
+        @Override
+        public void describeTo(Description description) {
+            description.appendText("options combining tags with and.");
+        }
+    };
+
+    private static class DummyHcServiceReference implements ServiceReference {
+
+        private long id;
+        private String name;
+        private String[] tags;
+        private String[] filterTags;
+
+        public DummyHcServiceReference(String name, String[] tags, String[] filterTags) {
+            super();
+            this.id = (long) (Math.random() * Long.MAX_VALUE);
+            this.name = name;
+            this.tags = tags;
+            this.filterTags = filterTags;
+        }
+
+        @Override
+        public Object getProperty(String key) {
+
+            if (Constants.SERVICE_ID.equals(key)) {
+                return id;
+            } else if (HealthCheck.NAME.equals(key)) {
+                return name;
+            } else if (HealthCheck.MBEAN_NAME.equals(key)) {
+                return name;
+            } else if (HealthCheck.TAGS.equals(key)) {
+                return tags;
+            } else if (CompositeHealthCheck.PROP_FILTER_TAGS.equals(key)) {
+                return filterTags;
+            } else if (ComponentConstants.COMPONENT_NAME.equals(key)) {
+                return filterTags != null ? CompositeHealthCheck.class.getName() : "some.other.HealthCheck";
+            } else {
+                return null;
+            }
+        }
+
+        @Override
+        public String[] getPropertyKeys() {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public Bundle getBundle() {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public Bundle[] getUsingBundles() {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public boolean isAssignableTo(Bundle bundle, String className) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public int compareTo(Object reference) {
+            throw new UnsupportedOperationException();
+        }
+
+    }
+}

Added: felix/trunk/core/src/test/java/org/apache/felix/hc/core/impl/JmxAttributeHealthCheckTest.java
URL: http://svn.apache.org/viewvc/felix/trunk/core/src/test/java/org/apache/felix/hc/core/impl/JmxAttributeHealthCheckTest.java?rev=1849243&view=auto
==============================================================================
--- felix/trunk/core/src/test/java/org/apache/felix/hc/core/impl/JmxAttributeHealthCheckTest.java (added)
+++ felix/trunk/core/src/test/java/org/apache/felix/hc/core/impl/JmxAttributeHealthCheckTest.java Tue Dec 18 22:51:48 2018
@@ -0,0 +1,52 @@
+/*
+ * 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 SF 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.felix.hc.core.impl;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import org.apache.felix.hc.api.Result;
+import org.junit.Test;
+
+public class JmxAttributeHealthCheckTest {
+
+    static void assertJmxValue(String objectName, String attributeName, String constraint, boolean expected) {
+        final JmxAttributeHealthCheck hc = new JmxAttributeHealthCheck();
+
+        final JmxAttributeHealthCheckConfiguration configuration = mock(JmxAttributeHealthCheckConfiguration.class);
+        when(configuration.mbean_name()).thenReturn(objectName);
+        when(configuration.attribute_name()).thenReturn(attributeName);
+        when(configuration.attribute_value_constraint()).thenReturn(constraint);
+
+        hc.activate(configuration);
+
+        final Result r = hc.execute();
+        assertEquals("Expected result " + expected, expected, r.isOk());
+    }
+
+    @Test
+    public void testJmxAttributeMatch() {
+        assertJmxValue("java.lang:type=ClassLoading", "LoadedClassCount", "> 10", true);
+    }
+
+    @Test
+    public void testJmxAttributeNoMatch() {
+        assertJmxValue("java.lang:type=ClassLoading", "LoadedClassCount", "< 10", false);
+    }
+}

Added: felix/trunk/core/src/test/java/org/apache/felix/hc/core/impl/executor/HealthCheckExecutorImplTest.java
URL: http://svn.apache.org/viewvc/felix/trunk/core/src/test/java/org/apache/felix/hc/core/impl/executor/HealthCheckExecutorImplTest.java?rev=1849243&view=auto
==============================================================================
--- felix/trunk/core/src/test/java/org/apache/felix/hc/core/impl/executor/HealthCheckExecutorImplTest.java (added)
+++ felix/trunk/core/src/test/java/org/apache/felix/hc/core/impl/executor/HealthCheckExecutorImplTest.java Tue Dec 18 22:51:48 2018
@@ -0,0 +1,179 @@
+/*
+ * 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 SF 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.felix.hc.core.impl.executor;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import java.util.Collection;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+import java.util.TreeSet;
+
+import org.apache.felix.hc.api.Result;
+import org.apache.felix.hc.api.Result.Status;
+import org.apache.felix.hc.api.ResultLog.Entry;
+import org.apache.felix.hc.api.execution.HealthCheckExecutionResult;
+import org.apache.felix.hc.util.HealthCheckMetadata;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.Spy;
+
+public class HealthCheckExecutorImplTest {
+
+    @InjectMocks
+    private HealthCheckExecutorImpl healthCheckExecutorImpl = new HealthCheckExecutorImpl();;
+
+    @Mock
+    private HealthCheckFuture future;
+
+    @Mock
+    private HealthCheckMetadata HealthCheckMetadata;
+
+    @Spy
+    private HealthCheckResultCache healthCheckResultCache = new HealthCheckResultCache();
+
+    @Before
+    public void setup() {
+        initMocks(this);
+
+        when(future.getHealthCheckMetadata()).thenReturn(HealthCheckMetadata);
+        when(HealthCheckMetadata.getTitle()).thenReturn("Test Check");
+
+        // 2 sec normal timeout
+        healthCheckExecutorImpl.setTimeoutInMs(2000L);
+        // 10 sec timeout for critical
+        healthCheckExecutorImpl.setLongRunningFutureThresholdForRedMs(10000L);
+    }
+
+    @Test
+    public void testCollectResultsFromFutures() throws Exception {
+
+        List<HealthCheckFuture> futures = new LinkedList<HealthCheckFuture>();
+        futures.add(future);
+        Collection<HealthCheckExecutionResult> results = new TreeSet<HealthCheckExecutionResult>();
+
+        when(future.isDone()).thenReturn(true);
+        ExecutionResult testResult = new ExecutionResult(HealthCheckMetadata, new Result(Result.Status.OK, "test"), 10L);
+        when(future.get()).thenReturn(testResult);
+
+        healthCheckExecutorImpl.collectResultsFromFutures(futures, results);
+
+        verify(future, times(1)).get();
+
+        assertEquals(1, results.size());
+        assertTrue(results.contains(testResult));
+    }
+
+    @Test
+    public void testCollectResultsFromFuturesTimeout() throws Exception {
+
+        // add an earlier result with status ok (that will be shown as part of the log)
+        addResultToCache(Status.OK);
+
+        List<HealthCheckFuture> futures = new LinkedList<HealthCheckFuture>();
+        futures.add(future);
+        Set<HealthCheckExecutionResult> results = new TreeSet<HealthCheckExecutionResult>();
+
+        when(future.isDone()).thenReturn(false);
+        // simulating a future that was created 5sec ago
+        when(future.getCreatedTime()).thenReturn(new Date(new Date().getTime() - 1000 * 5));
+
+        healthCheckExecutorImpl.collectResultsFromFutures(futures, results);
+
+        verify(future, times(0)).get();
+
+        assertEquals(1, results.size());
+        HealthCheckExecutionResult result = results.iterator().next();
+
+        assertEquals(Result.Status.WARN, result.getHealthCheckResult().getStatus());
+
+        // 3 because previous result exists and is part of log
+        assertEquals(3, getLogEntryCount(result));
+    }
+
+    @Test
+    public void testCollectResultsFromFuturesCriticalTimeout() throws Exception {
+
+        List<HealthCheckFuture> futures = new LinkedList<HealthCheckFuture>();
+        futures.add(future);
+        Set<HealthCheckExecutionResult> results = new TreeSet<HealthCheckExecutionResult>();
+
+        when(future.isDone()).thenReturn(false);
+
+        // use an old date now (simulating a future that has run for an hour)
+        when(future.getCreatedTime()).thenReturn(new Date(new Date().getTime() - 1000 * 60 * 60));
+
+        healthCheckExecutorImpl.collectResultsFromFutures(futures, results);
+        assertEquals(1, results.size());
+        HealthCheckExecutionResult result = results.iterator().next();
+
+        verify(future, times(0)).get();
+
+        assertEquals(Result.Status.CRITICAL, result.getHealthCheckResult().getStatus());
+        assertEquals(1, getLogEntryCount(result));
+    }
+
+    @Test
+    public void testCollectResultsFromFuturesWarnTimeoutWithPreviousCritical() throws Exception {
+
+        // an earlier result with critical
+        addResultToCache(Status.CRITICAL);
+
+        List<HealthCheckFuture> futures = new LinkedList<HealthCheckFuture>();
+        futures.add(future);
+        Set<HealthCheckExecutionResult> results = new TreeSet<HealthCheckExecutionResult>();
+
+        when(future.isDone()).thenReturn(false);
+        // simulating a future that was created 5sec ago
+        when(future.getCreatedTime()).thenReturn(new Date(new Date().getTime() - 1000 * 5));
+
+        healthCheckExecutorImpl.collectResultsFromFutures(futures, results);
+        assertEquals(1, results.size());
+        HealthCheckExecutionResult result = results.iterator().next();
+
+        verify(future, times(0)).get();
+
+        // expect CRITICAL because previous result (before timeout) was CRITICAL (and not only WARN)
+        assertEquals(Result.Status.CRITICAL, result.getHealthCheckResult().getStatus());
+        assertEquals(3, getLogEntryCount(result));
+    }
+
+    private int getLogEntryCount(HealthCheckExecutionResult result) {
+        int logEntryCount = 0;
+        final Iterator<Entry> it = result.getHealthCheckResult().iterator();
+        while (it.hasNext()) {
+            it.next();
+            logEntryCount++;
+        }
+        return logEntryCount;
+    }
+
+    private void addResultToCache(Status status) {
+        healthCheckResultCache.updateWith(new ExecutionResult(HealthCheckMetadata, new Result(status, "Status " + status), 1000));
+    }
+}

Added: felix/trunk/core/src/test/java/org/apache/felix/hc/core/impl/executor/HealthCheckResultCacheTest.java
URL: http://svn.apache.org/viewvc/felix/trunk/core/src/test/java/org/apache/felix/hc/core/impl/executor/HealthCheckResultCacheTest.java?rev=1849243&view=auto
==============================================================================
--- felix/trunk/core/src/test/java/org/apache/felix/hc/core/impl/executor/HealthCheckResultCacheTest.java (added)
+++ felix/trunk/core/src/test/java/org/apache/felix/hc/core/impl/executor/HealthCheckResultCacheTest.java Tue Dec 18 22:51:48 2018
@@ -0,0 +1,204 @@
+/*
+ * 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 SF 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.felix.hc.core.impl.executor;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+
+import org.apache.felix.hc.api.HealthCheck;
+import org.apache.felix.hc.api.Result;
+import org.apache.felix.hc.api.ResultLog;
+import org.apache.felix.hc.api.execution.HealthCheckExecutionResult;
+import org.apache.felix.hc.util.HealthCheckMetadata;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.osgi.framework.Constants;
+import org.osgi.framework.ServiceReference;
+
+public class HealthCheckResultCacheTest {
+
+    private static final int HC_TIMEOUT_NOT_SET = -1;
+    private static final int DUR_1_MIN = 60 * 1000;
+    private static final int DUR_2_MIN = 2 * DUR_1_MIN;
+    private static final int DUR_3_MIN = 3 * DUR_1_MIN;
+    private static final int DUR_4_MIN = 4 * DUR_1_MIN;
+
+    private HealthCheckResultCache healthCheckResultCache = new HealthCheckResultCache();
+
+    @Mock
+    ServiceReference serviceRef;
+
+    @Before
+    public void setup() {
+        initMocks(this);
+    }
+
+    private HealthCheckMetadata setupHealthCheckMetadata(long id, long ttl) {
+        reset(serviceRef);
+        doReturn(id).when(serviceRef).getProperty(Constants.SERVICE_ID);
+        doReturn(ttl).when(serviceRef).getProperty(HealthCheck.RESULT_CACHE_TTL_IN_MS);
+        doReturn("HC id=" + id).when(serviceRef).getProperty(HealthCheck.NAME);
+        return new HealthCheckMetadata(serviceRef);
+    }
+
+    @Test
+    public void testHealthCheckResultCache() {
+
+        HealthCheckMetadata hc1 = setupHealthCheckMetadata(1, HC_TIMEOUT_NOT_SET);
+        ExecutionResult executionResult1 = spy(new ExecutionResult(hc1, new Result(Result.Status.OK, "result for hc1"), 1));
+        doReturn(new Date(new Date().getTime() - DUR_1_MIN)).when(executionResult1).getFinishedAt();
+        healthCheckResultCache.updateWith(executionResult1);
+
+        HealthCheckMetadata hc2 = setupHealthCheckMetadata(2, HC_TIMEOUT_NOT_SET);
+        ExecutionResult executionResult2 = spy(new ExecutionResult(hc2, new Result(Result.Status.OK, "result for hc2"), 1));
+        doReturn(new Date(new Date().getTime() - DUR_3_MIN)).when(executionResult2).getFinishedAt();
+        healthCheckResultCache.updateWith(executionResult2);
+
+        HealthCheckMetadata hc3 = setupHealthCheckMetadata(3, DUR_4_MIN);
+        ExecutionResult executionResult3 = spy(new ExecutionResult(hc3, new Result(Result.Status.OK, "result for hc3"), 1));
+        doReturn(new Date(new Date().getTime() - DUR_3_MIN)).when(executionResult3).getFinishedAt();
+        healthCheckResultCache.updateWith(executionResult3);
+
+        HealthCheckMetadata hc4 = setupHealthCheckMetadata(4, HC_TIMEOUT_NOT_SET);
+        // no result for this yet
+
+        List<HealthCheckMetadata> hcList = new ArrayList<HealthCheckMetadata>(Arrays.asList(hc1, hc2, hc3, hc4));
+        List<HealthCheckExecutionResult> results = new ArrayList<HealthCheckExecutionResult>();
+
+        healthCheckResultCache.useValidCacheResults(hcList, results, DUR_2_MIN);
+
+        assertTrue(hcList.contains(hc2)); // result too old, left in hcList for later execution
+        assertTrue(hcList.contains(hc4)); // no result was added to cache via updateWith()
+
+        assertTrue(results.contains(executionResult1)); // true <= result one min old, global timeout 2min
+        assertFalse(results.contains(executionResult2)); // false <= result three min old, global timeout 2min
+        assertTrue(results.contains(executionResult3)); // true <= result one three old, HC timeout 4min
+
+        // values not found in cache are left in hcList
+        assertEquals(2, hcList.size());
+        assertEquals(2, results.size());
+
+    }
+
+    @Test
+    public void testHealthCheckResultCacheTtl() {
+
+        // -- test cache miss due to HC TTL
+        HealthCheckMetadata hcWithTtl = setupHealthCheckMetadata(1, DUR_1_MIN);
+        ExecutionResult executionResult = spy(new ExecutionResult(hcWithTtl, new Result(Result.Status.OK, "result for hc"), 1));
+        doReturn(new Date(new Date().getTime() - DUR_2_MIN)).when(executionResult).getFinishedAt();
+        healthCheckResultCache.updateWith(executionResult);
+
+        HealthCheckExecutionResult result = healthCheckResultCache.getValidCacheResult(hcWithTtl, DUR_3_MIN);
+        assertNull(result); // even though global timeout would be ok (2min<3min, the hc timeout of 1min invalidates the result)
+
+        // -- test cache hit due to HC TTL
+        hcWithTtl = setupHealthCheckMetadata(2, DUR_3_MIN);
+        executionResult = spy(new ExecutionResult(hcWithTtl, new Result(Result.Status.OK, "result for hc"), 1));
+        doReturn(new Date(new Date().getTime() - DUR_2_MIN)).when(executionResult).getFinishedAt();
+        healthCheckResultCache.updateWith(executionResult);
+
+        result = healthCheckResultCache.getValidCacheResult(hcWithTtl, DUR_1_MIN);
+        assertEquals(executionResult, result); // even though global timeout would invalidate this result (1min<2min, the hc timeout of 3min
+                                               // allows the result)
+
+        // -- test Long.MAX_VALUE
+        hcWithTtl = setupHealthCheckMetadata(3, Long.MAX_VALUE);
+        executionResult = spy(new ExecutionResult(hcWithTtl, new Result(Result.Status.OK, "result for hc"), 1));
+        doReturn(new Date(new Date().getTime() - DUR_4_MIN)).when(executionResult).getFinishedAt();
+        healthCheckResultCache.updateWith(executionResult);
+
+        result = healthCheckResultCache.getValidCacheResult(hcWithTtl, DUR_1_MIN);
+        assertEquals(executionResult, result);
+
+    }
+
+    private HealthCheckMetadata setupHealthCheckMetadataWithStickyResults(long id, long warningsStickForMinutes) {
+        reset(serviceRef);
+        doReturn(id).when(serviceRef).getProperty(Constants.SERVICE_ID);
+        doReturn(warningsStickForMinutes).when(serviceRef).getProperty(HealthCheck.WARNINGS_STICK_FOR_MINUTES);
+        doReturn("HC id=" + id).when(serviceRef).getProperty(HealthCheck.NAME);
+        return new HealthCheckMetadata(serviceRef);
+    }
+
+    @Test
+    public void testCreateExecutionResultWithStickyResults() {
+
+        HealthCheckMetadata hcWithStickyResultsSet = setupHealthCheckMetadataWithStickyResults(1, 2 /* 2 minutes */);
+        ExecutionResult currentResult = spy(new ExecutionResult(hcWithStickyResultsSet, new Result(Result.Status.OK, "result for hc"), 1));
+        HealthCheckExecutionResult overallResultWithStickyResults = healthCheckResultCache
+                .createExecutionResultWithStickyResults(currentResult);
+        assertTrue("Exact same result is expected if no history exists", currentResult == overallResultWithStickyResults);
+
+        // add 4 minutes old WARN to cache
+        ExecutionResult oldWarnResult = spy(
+                new ExecutionResult(hcWithStickyResultsSet, new Result(Result.Status.WARN, "result for hc"), 1));
+        doReturn(new Date(System.currentTimeMillis() - DUR_4_MIN)).when(oldWarnResult).getFinishedAt();
+        healthCheckResultCache.updateWith(oldWarnResult);
+
+        // check that it is not used
+        currentResult = new ExecutionResult(hcWithStickyResultsSet, new Result(Result.Status.OK, "result for hc"), 1);
+        overallResultWithStickyResults = healthCheckResultCache.createExecutionResultWithStickyResults(currentResult);
+        assertTrue("Exact same result is expected if WARN HC Result is too old", currentResult == overallResultWithStickyResults);
+
+        // change WARN to 1 minute age
+        doReturn(new Date(System.currentTimeMillis() - DUR_1_MIN)).when(oldWarnResult).getFinishedAt();
+        overallResultWithStickyResults = healthCheckResultCache.createExecutionResultWithStickyResults(currentResult);
+        assertTrue("Expect newly created result as sticky result should be taken into account",
+                currentResult != overallResultWithStickyResults);
+        assertEquals("Expect status to be taken over from old, sticky WARN", Result.Status.WARN,
+                overallResultWithStickyResults.getHealthCheckResult().getStatus());
+        assertEquals("Expect 4 entries, two each for current and WARN", 4, getLogMsgCount(overallResultWithStickyResults));
+
+        // add 1 minutes old CRITICAL to cache
+        ExecutionResult oldCriticalResult = spy(
+                new ExecutionResult(hcWithStickyResultsSet, new Result(Result.Status.CRITICAL, "result for hc"), 1));
+        doReturn(new Date(System.currentTimeMillis() - DUR_1_MIN)).when(oldCriticalResult).getFinishedAt();
+        healthCheckResultCache.updateWith(oldCriticalResult);
+
+        overallResultWithStickyResults = healthCheckResultCache.createExecutionResultWithStickyResults(currentResult);
+        assertTrue("Expect newly created result as sticky result should be taken into account",
+                currentResult != overallResultWithStickyResults);
+        assertEquals("Expect status to be taken over from old, sticky CRITICAL", Result.Status.CRITICAL,
+                overallResultWithStickyResults.getHealthCheckResult().getStatus());
+        assertEquals("Expect six entries, two each for current, WARN and CRITICAL result", 6,
+                getLogMsgCount(overallResultWithStickyResults));
+
+    }
+
+    private int getLogMsgCount(HealthCheckExecutionResult result) {
+        int count = 0;
+        for (ResultLog.Entry entry : result.getHealthCheckResult()) {
+            count++;
+        }
+        return count;
+    }
+
+}

Added: felix/trunk/core/src/test/java/org/apache/felix/hc/core/impl/servlet/HealthCheckExecutorServletTest.java
URL: http://svn.apache.org/viewvc/felix/trunk/core/src/test/java/org/apache/felix/hc/core/impl/servlet/HealthCheckExecutorServletTest.java?rev=1849243&view=auto
==============================================================================
--- felix/trunk/core/src/test/java/org/apache/felix/hc/core/impl/servlet/HealthCheckExecutorServletTest.java (added)
+++ felix/trunk/core/src/test/java/org/apache/felix/hc/core/impl/servlet/HealthCheckExecutorServletTest.java Tue Dec 18 22:51:48 2018
@@ -0,0 +1,305 @@
+/*
+ * 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 SF 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.felix.hc.core.impl.servlet;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.argThat;
+import static org.mockito.Matchers.contains;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.felix.hc.api.Result;
+import org.apache.felix.hc.api.Result.Status;
+import org.apache.felix.hc.api.execution.HealthCheckExecutionOptions;
+import org.apache.felix.hc.api.execution.HealthCheckExecutionResult;
+import org.apache.felix.hc.api.execution.HealthCheckExecutor;
+import org.apache.felix.hc.api.execution.HealthCheckSelector;
+import org.apache.felix.hc.core.impl.executor.ExecutionResult;
+import org.apache.felix.hc.util.HealthCheckMetadata;
+import org.hamcrest.Description;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentMatcher;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.osgi.framework.Constants;
+import org.osgi.framework.ServiceReference;
+
+public class HealthCheckExecutorServletTest {
+
+    @InjectMocks
+    private HealthCheckExecutorServlet healthCheckExecutorServlet = new HealthCheckExecutorServlet();
+
+    @Mock
+    private HttpServletRequest request;
+
+    @Mock
+    private HttpServletResponse response;
+
+    @Mock
+    private HealthCheckExecutor healthCheckExecutor;
+
+    @Mock
+    private ResultHtmlSerializer htmlSerializer;
+
+    @Mock
+    private ResultJsonSerializer jsonSerializer;
+
+    @Mock
+    private ResultTxtSerializer txtSerializer;
+
+    @Mock
+    private ResultTxtVerboseSerializer verboseTxtSerializer;
+
+    @Mock
+    private ServiceReference hcServiceRef;
+
+    @Mock
+    private PrintWriter writer;
+
+    @Before
+    public void setup() throws IOException {
+        initMocks(this);
+
+        doReturn(500L).when(hcServiceRef).getProperty(Constants.SERVICE_ID);
+        doReturn(writer).when(response).getWriter();
+    }
+
+    @Test
+    public void testDoGetHtml() throws ServletException, IOException {
+
+        final String testTag = "testTag";
+        doReturn(testTag).when(request).getParameter(HealthCheckExecutorServlet.PARAM_TAGS.name);
+        doReturn("false").when(request).getParameter(HealthCheckExecutorServlet.PARAM_COMBINE_TAGS_WITH_OR.name);
+        final List<HealthCheckExecutionResult> executionResults = getExecutionResults(Result.Status.CRITICAL);
+        doReturn(executionResults).when(healthCheckExecutor).execute(selector(new String[] { testTag }, new String[0]),
+                eq(new HealthCheckExecutionOptions()));
+
+        healthCheckExecutorServlet.doGet(request, response);
+
+        verifyZeroInteractions(jsonSerializer);
+        verifyZeroInteractions(txtSerializer);
+        verifyZeroInteractions(verboseTxtSerializer);
+        verify(htmlSerializer)
+                .serialize(resultEquals(new Result(Result.Status.CRITICAL, "Overall Status CRITICAL")), eq(executionResults),
+                        contains("Supported URL parameters"), eq(false));
+    }
+
+    @Test
+    public void testDoGetNameAndTagInPath() throws ServletException, IOException {
+
+        final String testTag = "testTag";
+        final String testName = "test name";
+
+        doReturn(testTag + "," + testName).when(request).getPathInfo();
+        doReturn("false").when(request).getParameter(HealthCheckExecutorServlet.PARAM_COMBINE_TAGS_WITH_OR.name);
+        final List<HealthCheckExecutionResult> executionResults = getExecutionResults(Result.Status.CRITICAL);
+        doReturn(executionResults).when(healthCheckExecutor).execute(selector(new String[] { testTag }, new String[] { testName }),
+                eq(new HealthCheckExecutionOptions()));
+
+        healthCheckExecutorServlet.doGet(request, response);
+
+        verify(request, never()).getParameter(HealthCheckExecutorServlet.PARAM_TAGS.name);
+        verify(request, never()).getParameter(HealthCheckExecutorServlet.PARAM_NAMES.name);
+        verifyZeroInteractions(jsonSerializer);
+        verifyZeroInteractions(txtSerializer);
+        verifyZeroInteractions(verboseTxtSerializer);
+        verify(htmlSerializer)
+                .serialize(resultEquals(new Result(Result.Status.CRITICAL, "Overall Status CRITICAL")), eq(executionResults),
+                        contains("Supported URL parameters"), eq(false));
+    }
+
+    @Test
+    public void testDoGetJson() throws ServletException, IOException {
+
+        final String testTag = "testTag";
+        doReturn("true").when(request).getParameter(HealthCheckExecutorServlet.PARAM_COMBINE_TAGS_WITH_OR.name);
+        int timeout = 5000;
+        doReturn(timeout + "").when(request).getParameter(HealthCheckExecutorServlet.PARAM_OVERRIDE_GLOBAL_TIMEOUT.name);
+        doReturn("/" + testTag + ".json").when(request).getPathInfo();
+        final List<HealthCheckExecutionResult> executionResults = getExecutionResults(Result.Status.WARN);
+        HealthCheckExecutionOptions options = new HealthCheckExecutionOptions();
+        options.setCombineTagsWithOr(true);
+        options.setOverrideGlobalTimeout(timeout);
+        doReturn(executionResults).when(healthCheckExecutor).execute(selector(new String[] { testTag }, new String[0]), eq(options));
+
+        healthCheckExecutorServlet.doGet(request, response);
+
+        verifyZeroInteractions(htmlSerializer);
+        verifyZeroInteractions(txtSerializer);
+        verifyZeroInteractions(verboseTxtSerializer);
+        verify(jsonSerializer).serialize(resultEquals(new Result(Result.Status.WARN, "Overall Status WARN")), eq(executionResults),
+                anyString(),
+                eq(false));
+
+    }
+
+    @Test
+    public void testDoGetTxt() throws ServletException, IOException {
+
+        final String testTag = "testTag";
+        doReturn(testTag).when(request).getParameter(HealthCheckExecutorServlet.PARAM_TAGS.name);
+        doReturn(HealthCheckExecutorServlet.FORMAT_TXT).when(request).getParameter(HealthCheckExecutorServlet.PARAM_FORMAT.name);
+        doReturn("true").when(request).getParameter(HealthCheckExecutorServlet.PARAM_COMBINE_TAGS_WITH_OR.name);
+        int timeout = 5000;
+        doReturn(timeout + "").when(request).getParameter(HealthCheckExecutorServlet.PARAM_OVERRIDE_GLOBAL_TIMEOUT.name);
+        final List<HealthCheckExecutionResult> executionResults = getExecutionResults(Result.Status.WARN);
+        HealthCheckExecutionOptions options = new HealthCheckExecutionOptions();
+        options.setCombineTagsWithOr(true);
+        options.setOverrideGlobalTimeout(timeout);
+
+        doReturn(executionResults).when(healthCheckExecutor).execute(selector(new String[] { testTag }, new String[0]), eq(options));
+
+        healthCheckExecutorServlet.doGet(request, response);
+
+        verifyZeroInteractions(htmlSerializer);
+        verifyZeroInteractions(jsonSerializer);
+        verifyZeroInteractions(verboseTxtSerializer);
+        verify(txtSerializer).serialize(resultEquals(new Result(Result.Status.WARN, "Overall Status WARN")));
+
+    }
+
+    @Test
+    public void testDoGetVerboseTxt() throws ServletException, IOException {
+
+        String testTag = "testTag";
+        doReturn(testTag).when(request).getParameter(HealthCheckExecutorServlet.PARAM_TAGS.name);
+        doReturn(HealthCheckExecutorServlet.FORMAT_VERBOSE_TXT).when(request).getParameter(HealthCheckExecutorServlet.PARAM_FORMAT.name);
+
+        List<HealthCheckExecutionResult> executionResults = getExecutionResults(Result.Status.WARN);
+        doReturn(executionResults).when(healthCheckExecutor).execute(selector(new String[] { testTag }, new String[0]),
+                any(HealthCheckExecutionOptions.class));
+
+        healthCheckExecutorServlet.doGet(request, response);
+
+        verifyZeroInteractions(htmlSerializer);
+        verifyZeroInteractions(jsonSerializer);
+        verifyZeroInteractions(txtSerializer);
+        verify(verboseTxtSerializer).serialize(resultEquals(new Result(Result.Status.WARN, "Overall Status WARN")), eq(executionResults),
+                eq(false));
+
+    }
+
+    private List<HealthCheckExecutionResult> getExecutionResults(Result.Status worstStatus) {
+        List<HealthCheckExecutionResult> results = new ArrayList<HealthCheckExecutionResult>();
+        results.add(new ExecutionResult(new HealthCheckMetadata(hcServiceRef), new Result(worstStatus, worstStatus.name()), 100));
+        results.add(new ExecutionResult(new HealthCheckMetadata(hcServiceRef), new Result(Result.Status.OK, "OK"), 100));
+        return results;
+    }
+
+    @Test
+    public void testGetStatusMapping() throws ServletException {
+
+        Map<Status, Integer> statusMapping = healthCheckExecutorServlet.getStatusMapping("CRITICAL:503");
+        assertEquals(statusMapping.get(Result.Status.OK), (Integer) 200);
+        assertEquals(statusMapping.get(Result.Status.WARN), (Integer) 200);
+        assertEquals(statusMapping.get(Result.Status.CRITICAL), (Integer) 503);
+        assertEquals(statusMapping.get(Result.Status.HEALTH_CHECK_ERROR), (Integer) 503);
+
+        statusMapping = healthCheckExecutorServlet.getStatusMapping("OK:333");
+        assertEquals(statusMapping.get(Result.Status.OK), (Integer) 333);
+        assertEquals(statusMapping.get(Result.Status.WARN), (Integer) 333);
+        assertEquals(statusMapping.get(Result.Status.CRITICAL), (Integer) 333);
+        assertEquals(statusMapping.get(Result.Status.HEALTH_CHECK_ERROR), (Integer) 333);
+
+        statusMapping = healthCheckExecutorServlet.getStatusMapping("OK:200,WARN:418,CRITICAL:503,HEALTH_CHECK_ERROR:500");
+        assertEquals(statusMapping.get(Result.Status.OK), (Integer) 200);
+        assertEquals(statusMapping.get(Result.Status.WARN), (Integer) 418);
+        assertEquals(statusMapping.get(Result.Status.CRITICAL), (Integer) 503);
+        assertEquals(statusMapping.get(Result.Status.HEALTH_CHECK_ERROR), (Integer) 500);
+
+        statusMapping = healthCheckExecutorServlet.getStatusMapping("CRITICAL:503,HEALTH_CHECK_ERROR:500");
+        assertEquals(statusMapping.get(Result.Status.OK), (Integer) 200);
+        assertEquals(statusMapping.get(Result.Status.WARN), (Integer) 200);
+        assertEquals(statusMapping.get(Result.Status.CRITICAL), (Integer) 503);
+        assertEquals(statusMapping.get(Result.Status.HEALTH_CHECK_ERROR), (Integer) 500);
+
+    }
+
+    @Test(expected = ServletException.class)
+    public void testGetStatusMappingInvalidToken() throws ServletException {
+        healthCheckExecutorServlet.getStatusMapping("CRITICAL");
+    }
+
+    @Test(expected = ServletException.class)
+    public void testGetStatusMappingInvalidStatus() throws ServletException {
+        healthCheckExecutorServlet.getStatusMapping("INVALID:200");
+    }
+
+    @Test(expected = ServletException.class)
+    public void testGetStatusMappingInvalidStatusCode() throws ServletException {
+        healthCheckExecutorServlet.getStatusMapping("CRITICAL:xxx");
+    }
+
+    static Result resultEquals(Result expected) {
+        return argThat(new ResultMatcher(expected));
+    }
+
+    static class ResultMatcher extends ArgumentMatcher<Result> {
+
+        private final Result expectedResult;
+
+        public ResultMatcher(Result expected) {
+            this.expectedResult = expected;
+        }
+
+        @Override
+        public boolean matches(Object actual) {
+            Result actualResult = (Result) actual;
+            return actualResult.getStatus().equals(expectedResult.getStatus()); // simple status matching only sufficient for this test case
+        }
+
+        @Override
+        public void describeTo(Description description) {
+            description.appendText(expectedResult == null ? null : expectedResult.toString());
+        }
+    }
+
+    HealthCheckSelector selector(final String[] tags, final String[] names) {
+        return argThat(new ArgumentMatcher<HealthCheckSelector>() {
+            @Override
+            public boolean matches(Object actual) {
+                if (actual instanceof HealthCheckSelector) {
+                    HealthCheckSelector actualSelector = (HealthCheckSelector) actual;
+                    return Arrays.equals(actualSelector.tags(), tags.length == 0 ? new String[] { "" } : tags) &&
+                            Arrays.equals(actualSelector.names(), names.length == 0 ? new String[] { "" } : names);
+                } else {
+                    return false;
+                }
+            }
+        });
+    }
+
+}

Added: felix/trunk/core/src/test/java/org/apache/felix/hc/core/impl/servlet/ResultJsonSerializerTest.java
URL: http://svn.apache.org/viewvc/felix/trunk/core/src/test/java/org/apache/felix/hc/core/impl/servlet/ResultJsonSerializerTest.java?rev=1849243&view=auto
==============================================================================
--- felix/trunk/core/src/test/java/org/apache/felix/hc/core/impl/servlet/ResultJsonSerializerTest.java (added)
+++ felix/trunk/core/src/test/java/org/apache/felix/hc/core/impl/servlet/ResultJsonSerializerTest.java Tue Dec 18 22:51:48 2018
@@ -0,0 +1,72 @@
+/*
+ * 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 SF 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.felix.hc.core.impl.servlet;
+
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.when;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import java.util.Arrays;
+
+import org.apache.felix.hc.api.HealthCheck;
+import org.apache.felix.hc.api.Result;
+import org.apache.felix.hc.api.execution.HealthCheckExecutionResult;
+import org.apache.felix.hc.core.impl.executor.ExecutionResult;
+import org.apache.felix.hc.util.FormattingResultLog;
+import org.apache.felix.hc.util.HealthCheckMetadata;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.osgi.framework.Constants;
+import org.osgi.framework.ServiceReference;
+
+public class ResultJsonSerializerTest {
+
+    @Mock
+    private ServiceReference<HealthCheck> serviceReference;
+
+    ResultJsonSerializer resultJsonSerializer = new ResultJsonSerializer();
+
+    @Before
+    public void setup() {
+        initMocks(this);
+
+        when(serviceReference.getProperty(HealthCheck.NAME)).thenReturn("Test");
+        when(serviceReference.getProperty(HealthCheck.TAGS)).thenReturn(new String[] { "tag1", "tag2" });
+        when(serviceReference.getProperty(Constants.SERVICE_ID)).thenReturn(1L);
+    }
+
+    @Test
+    public void testJsonSerialisation() {
+
+        FormattingResultLog log = new FormattingResultLog();
+        log.info("test message");
+        Result result = new Result(log);
+        HealthCheckMetadata hcMetadata = new HealthCheckMetadata(serviceReference);
+        HealthCheckExecutionResult executionResult = new ExecutionResult(hcMetadata, result, 100);
+        Result overallResult = new Result(Result.Status.OK, "Overall status");
+
+        String json = resultJsonSerializer.serialize(overallResult, Arrays.asList(executionResult), null, false);
+        assertThat(json, containsString("\"overallResult\":\"OK\""));
+        assertThat(json, containsString("\"tags\":[\"tag1\",\"tag2\"]"));
+        assertThat(json, containsString("\"messages\":[{\"status\":\"OK\",\"message\":\"test message\"}]"));
+    }
+
+}

Added: felix/trunk/core/src/test/java/org/apache/felix/hc/core/it/AsyncHealthCheckIT.java
URL: http://svn.apache.org/viewvc/felix/trunk/core/src/test/java/org/apache/felix/hc/core/it/AsyncHealthCheckIT.java?rev=1849243&view=auto
==============================================================================
--- felix/trunk/core/src/test/java/org/apache/felix/hc/core/it/AsyncHealthCheckIT.java (added)
+++ felix/trunk/core/src/test/java/org/apache/felix/hc/core/it/AsyncHealthCheckIT.java Tue Dec 18 22:51:48 2018
@@ -0,0 +1,216 @@
+/*
+ * 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 SF 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.felix.hc.core.it;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.util.Dictionary;
+import java.util.Hashtable;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import javax.inject.Inject;
+
+import org.apache.felix.hc.api.HealthCheck;
+import org.apache.felix.hc.api.Result;
+import org.apache.felix.hc.api.execution.HealthCheckExecutor;
+import org.apache.felix.hc.api.execution.HealthCheckSelector;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.ops4j.pax.exam.Configuration;
+import org.ops4j.pax.exam.Option;
+import org.ops4j.pax.exam.junit.PaxExam;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceRegistration;
+
+@RunWith(PaxExam.class)
+public class AsyncHealthCheckIT {
+
+    @Inject
+    private HealthCheckExecutor executor;
+
+    @Inject
+    private BundleContext bundleContext;
+
+    @Configuration
+    public Option[] config() {
+        return U.config();
+    }
+
+    final AtomicInteger counter = new AtomicInteger(Integer.MIN_VALUE);
+
+    final static int MAX_VALUE = 12345678;
+
+    class TestHC implements HealthCheck {
+        @Override
+        public Result execute() {
+            final int v = counter.incrementAndGet();
+            return new Result(v > MAX_VALUE ? Result.Status.WARN : Result.Status.OK, "counter is now " + v);
+        }
+    }
+
+    private ServiceRegistration registerAsyncHc(HealthCheck hc, String id, Object async, int stickyMinutes) {
+        final Dictionary<String, Object> props = new Hashtable<String, Object>();
+        props.put(HealthCheck.NAME, "async_HC_" + id);
+        props.put(HealthCheck.TAGS, id);
+        if (async instanceof String) {
+            props.put(HealthCheck.ASYNC_CRON_EXPRESSION, async);
+        } else if (async instanceof Long) {
+            props.put(HealthCheck.ASYNC_INTERVAL_IN_SEC, async);
+        }
+
+        if (stickyMinutes > 0) {
+            props.put(HealthCheck.WARNINGS_STICK_FOR_MINUTES, stickyMinutes);
+        }
+
+        final ServiceRegistration result = bundleContext.registerService(HealthCheck.class.getName(), hc, props);
+
+        // Wait for HC to be registered
+        U.expectHealthChecks(1, executor, id);
+
+        return result;
+    }
+
+    private void assertStatus(String id, Result.Status expected, long maxMsec, String msg) throws InterruptedException {
+        final long timeout = System.currentTimeMillis() + 5000L;
+        while (System.currentTimeMillis() < timeout) {
+            final Result.Status actual = executor.execute(HealthCheckSelector.tags(id)).get(0).getHealthCheckResult().getStatus();
+            if (actual == expected) {
+                return;
+            }
+            Thread.sleep(100L);
+        }
+        fail("Did not get status " + expected + " after " + maxMsec + " msec " + msg);
+    }
+
+    @Test
+    public void testAsyncHealthCheckExecution() throws InterruptedException {
+
+        final String id = UUID.randomUUID().toString();
+        final HealthCheck hc = new TestHC();
+        final ServiceRegistration reg = registerAsyncHc(hc, id, "*/1 * * * * ?", 0);
+        final long maxMsec = 5000L;
+
+        try {
+            // Reset the counter and check that HC increments it without explicitly calling the executor
+            {
+                counter.set(0);
+                final long timeout = System.currentTimeMillis() + maxMsec;
+                while (System.currentTimeMillis() < timeout) {
+                    int currentVal = counter.get();
+                    if (currentVal > 0) {
+                        break;
+                    }
+                    Thread.sleep(100L);
+                }
+                assertTrue("Expecting counter to be incremented", counter.get() > 0);
+            }
+
+            // Verify that we get the right log
+            final String msg = executor.execute(HealthCheckSelector.tags(id)).get(0).getHealthCheckResult().iterator().next().getMessage();
+            assertTrue("Expecting the right message: " + msg, msg.contains("counter is now"));
+
+            // And verify that calling executor lots of times doesn't increment as much
+            final int previous = counter.get();
+            final int n = 100;
+            for (int i = 0; i < n; i++) {
+                executor.execute(HealthCheckSelector.tags(id));
+            }
+            assertTrue("Expecting counter to increment asynchronously", counter.get() < previous + n);
+
+            // Verify that results are not sticky
+            assertStatus(id, Result.Status.OK, maxMsec, "before WARN");
+            counter.set(MAX_VALUE + 1);
+            assertStatus(id, Result.Status.WARN, maxMsec, "right after WARN");
+            counter.set(0);
+            assertStatus(id, Result.Status.OK, maxMsec, "after resetting counter");
+
+        } finally {
+            reg.unregister();
+        }
+
+    }
+
+    @Test
+    public void testAsyncHealthCheckExecutionWithInterval() throws InterruptedException {
+
+        final String id = UUID.randomUUID().toString();
+        final HealthCheck hc = new TestHC();
+        final ServiceRegistration reg = registerAsyncHc(hc, id, new Long(2), 0);
+        final long maxMsec = 5000L;
+
+        try {
+            // Reset the counter and check that HC increments it without explicitly calling the executor
+            {
+                counter.set(0);
+                final long timeout = System.currentTimeMillis() + maxMsec;
+                while (System.currentTimeMillis() < timeout) {
+                    int currentVal = counter.get();
+                    if (currentVal > 0) {
+                        break;
+                    }
+                    Thread.sleep(100L);
+                }
+                assertTrue("Expecting counter to be incremented", counter.get() > 0);
+            }
+
+            // Verify that we get the right log
+            final String msg = executor.execute(HealthCheckSelector.tags(id)).get(0).getHealthCheckResult().iterator().next().getMessage();
+            assertTrue("Expecting the right message: " + msg, msg.contains("counter is now"));
+
+        } finally {
+            reg.unregister();
+        }
+
+    }
+
+    @Test
+    public void testAsyncHealthCheckWithStickyResults() throws InterruptedException {
+        final String id = UUID.randomUUID().toString();
+        final HealthCheck hc = new TestHC();
+        final long maxMsec = 5000L;
+        final int stickyMinutes = 1;
+        final ServiceRegistration reg = registerAsyncHc(hc, id, "*/1 * * * * ?", stickyMinutes);
+
+        try {
+            assertStatus(id, Result.Status.OK, maxMsec, "before WARN");
+            counter.set(MAX_VALUE + 1);
+            assertStatus(id, Result.Status.WARN, maxMsec, "right after WARN");
+            counter.set(0);
+
+            // Counter should be incremented after a while, and in range, but with sticky WARN result
+            final long timeout = System.currentTimeMillis() + maxMsec;
+            boolean ok = false;
+            while (System.currentTimeMillis() < timeout) {
+                if (counter.get() > 0 && counter.get() < MAX_VALUE) {
+                    ok = true;
+                    break;
+                }
+                Thread.sleep(100L);
+            }
+
+            assertTrue("expecting counter to be incremented", ok);
+            assertStatus(id, Result.Status.WARN, maxMsec, "after resetting counter, expecting sticky result");
+
+        } finally {
+            reg.unregister();
+        }
+    }
+
+}

Added: felix/trunk/core/src/test/java/org/apache/felix/hc/core/it/ExtendedHealthCheckExecutorIT.java
URL: http://svn.apache.org/viewvc/felix/trunk/core/src/test/java/org/apache/felix/hc/core/it/ExtendedHealthCheckExecutorIT.java?rev=1849243&view=auto
==============================================================================
--- felix/trunk/core/src/test/java/org/apache/felix/hc/core/it/ExtendedHealthCheckExecutorIT.java (added)
+++ felix/trunk/core/src/test/java/org/apache/felix/hc/core/it/ExtendedHealthCheckExecutorIT.java Tue Dec 18 22:51:48 2018
@@ -0,0 +1,113 @@
+/*
+ * 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 SF 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.felix.hc.core.it;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Dictionary;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.UUID;
+
+import javax.inject.Inject;
+
+import org.apache.felix.hc.api.HealthCheck;
+import org.apache.felix.hc.api.Result;
+import org.apache.felix.hc.api.execution.HealthCheckExecutionResult;
+import org.apache.felix.hc.api.execution.HealthCheckExecutor;
+import org.apache.felix.hc.api.execution.HealthCheckSelector;
+import org.apache.felix.hc.util.HealthCheckFilter;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.ops4j.pax.exam.Configuration;
+import org.ops4j.pax.exam.Option;
+import org.ops4j.pax.exam.junit.PaxExam;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceReference;
+import org.osgi.framework.ServiceRegistration;
+
+/** Additional executor tests */
+@RunWith(PaxExam.class)
+public class ExtendedHealthCheckExecutorIT {
+
+    @Inject
+    private HealthCheckExecutor executor;
+
+    @Inject
+    private BundleContext bundleContext;
+
+    @SuppressWarnings("rawtypes")
+    private List<ServiceRegistration> regs = new ArrayList<ServiceRegistration>();
+
+    private String testTag;
+    private final Result.Status testResult = Result.Status.OK;
+
+    @Configuration
+    public Option[] config() {
+        return U.config();
+    }
+
+    private void registerHC(final String... tags) {
+        final HealthCheck hc = new HealthCheck() {
+            @Override
+            public Result execute() {
+                return new Result(testResult, "Returning " + testResult + " for " + tags[0]);
+            }
+
+        };
+
+        final Dictionary<String, Object> props = new Hashtable<String, Object>();
+        props.put(HealthCheck.NAME, "name_" + tags[0]);
+        props.put(HealthCheck.TAGS, tags);
+
+        regs.add(bundleContext.registerService(HealthCheck.class.getName(), hc, props));
+    }
+
+    @Before
+    public void setup() {
+        testTag = "TEST_" + UUID.randomUUID().toString();
+        registerHC(testTag);
+        U.expectHealthChecks(1, executor, testTag);
+    }
+
+    @After
+    public void cleanup() {
+        for (ServiceRegistration reg : regs) {
+            reg.unregister();
+        }
+    }
+
+    @Test
+    public void testSingleExecution() throws Exception {
+        final HealthCheckFilter filter = new HealthCheckFilter(bundleContext);
+        final ServiceReference[] refs = filter.getHealthCheckServiceReferences(HealthCheckSelector.tags(testTag));
+        assertNotNull(refs);
+        assertEquals(1, refs.length);
+
+        // The ExtendedHealthCheckExecutor interface is not public, so we cheat
+        // to be able to test its implementation
+        final Method m = executor.getClass().getMethod("execute", ServiceReference.class);
+        final HealthCheckExecutionResult result = (HealthCheckExecutionResult) m.invoke(executor, refs[0]);
+        assertEquals(testResult, result.getHealthCheckResult().getStatus());
+    }
+}
\ No newline at end of file