diff --git a/src/main/java/com/redhat/devtools/intellij/telemetry/core/configuration/TelemetryConfiguration.java b/src/main/java/com/redhat/devtools/intellij/telemetry/core/configuration/TelemetryConfiguration.java index 15e266dc..789b3f8f 100644 --- a/src/main/java/com/redhat/devtools/intellij/telemetry/core/configuration/TelemetryConfiguration.java +++ b/src/main/java/com/redhat/devtools/intellij/telemetry/core/configuration/TelemetryConfiguration.java @@ -25,7 +25,7 @@ public class TelemetryConfiguration extends CompositeConfiguration { private static final SaveableFileConfiguration FILE = new SaveableFileConfiguration( Directories.RED_HAT.resolve("com.redhat.devtools.intellij.telemetry")); - private static TelemetryConfiguration INSTANCE = new TelemetryConfiguration(); + private static final TelemetryConfiguration INSTANCE = new TelemetryConfiguration(); public static TelemetryConfiguration getInstance() { return INSTANCE; diff --git a/src/main/java/com/redhat/devtools/intellij/telemetry/core/configuration/limits/Configurations.java b/src/main/java/com/redhat/devtools/intellij/telemetry/core/configuration/limits/Configurations.java new file mode 100644 index 00000000..50edbd05 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/telemetry/core/configuration/limits/Configurations.java @@ -0,0 +1,120 @@ +/******************************************************************************* + * Copyright (c) 2024 Red Hat, Inc. + * Distributed under license by Red Hat, Inc. All rights reserved. + * This program is made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + ******************************************************************************/ +package com.redhat.devtools.intellij.telemetry.core.configuration.limits; + +import com.intellij.openapi.diagnostic.Logger; +import com.redhat.devtools.intellij.telemetry.core.util.Directories; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.FileTime; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +class Configurations { + + private static final Logger LOGGER = Logger.getInstance(Configurations.class); + static final Path LOCAL = Directories.RED_HAT.resolve("telemetry-config.json"); + static final String EMBEDDED = "/telemetry-config.json"; + static final String REMOTE = "https://raw.githubusercontent.com/adietish/intellij-redhat-telemetry/issue-82/src/main/resources/telemetry-config.json"; + + private final OkHttpClient client = new OkHttpClient.Builder() + .connectTimeout(5, TimeUnit.SECONDS) + .readTimeout(5, TimeUnit.SECONDS) + .writeTimeout(5, TimeUnit.SECONDS) + .build(); + + public String getRemote() { + return getRemote( + in -> Files.copy(in, LOCAL, StandardCopyOption.REPLACE_EXISTING), + client, + this::getLocal); + } + + /* for testing purposes */ + String getRemote(ThrowingInputStreamConsumer fileWriter, OkHttpClient client, ThrowingSupplier fileReader) { + Request request = new Request.Builder() + .url(REMOTE) + .addHeader("Content-Type", "application/json") + .build(); + try (Response response = client.newCall(request).execute()) { + if (response.body() != null) { + fileWriter.accept(response.body().byteStream()); + } + return fileReader.get(); + } catch (Throwable e) { + LOGGER.warn("Could not download remote limits configurations.", e); + return null; + } + } + + /* for testing purposes */ + interface ThrowingInputStreamConsumer { + void accept(InputStream in) throws IOException; + } + + public boolean localExists() { + return Files.exists(LOCAL); + } + + public FileTime getLocalLastModified() { + return getLocalLastModified( + () -> Files.exists(LOCAL), + () -> Files.readAttributes(LOCAL, BasicFileAttributes.class)); + } + + /* for testing purposes */ + FileTime getLocalLastModified(ThrowingSupplier exists, ThrowingSupplier attributesSupplier) { + try { + if (!exists.get()) { + return null; + } + BasicFileAttributes attributes = attributesSupplier.get(); + return attributes.lastModifiedTime(); + } catch (Throwable e) { + return null; + } + } + + interface ThrowingSupplier { + T get() throws E; + } + + public String getLocal() { + try { + return toString(Files.newInputStream(LOCAL)); + } catch (IOException e) { + return null; + } + } + + public String getEmbedded() { + return toString(MessageLimits.class.getResourceAsStream(EMBEDDED)); + } + + private String toString(InputStream in) { + if (in == null) { + return null; + } + BufferedReader reader = new BufferedReader(new InputStreamReader(in)); + return reader.lines().collect(Collectors.joining()); + } + +} \ No newline at end of file diff --git a/src/main/java/com/redhat/devtools/intellij/telemetry/core/configuration/limits/Enabled.java b/src/main/java/com/redhat/devtools/intellij/telemetry/core/configuration/limits/Enabled.java new file mode 100644 index 00000000..83e5b6d5 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/telemetry/core/configuration/limits/Enabled.java @@ -0,0 +1,42 @@ +/******************************************************************************* + * Copyright (c) 2024 Red Hat, Inc. + * Distributed under license by Red Hat, Inc. All rights reserved. + * This program is made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + ******************************************************************************/ +package com.redhat.devtools.intellij.telemetry.core.configuration.limits; + +import com.intellij.openapi.util.text.StringUtil; + +import java.util.Arrays; + +public enum Enabled { + ALL("all"), + ERROR("error"), + CRASH("crash"), + OFF("off"); + + private final String value; + + Enabled(String value) { + this.value = value; + } + + private boolean hasValue(String value) { + if (StringUtil.isEmptyOrSpaces(value)) { + return this.value == null; + } + return value.equals(this.value); + } + + public static Enabled safeValueOf(String value) { + return Arrays.stream(values()) + .filter(instance -> instance.hasValue(value)) + .findAny() + .orElse(ALL); + } +} \ No newline at end of file diff --git a/src/main/java/com/redhat/devtools/intellij/telemetry/core/configuration/limits/Filter.java b/src/main/java/com/redhat/devtools/intellij/telemetry/core/configuration/limits/Filter.java new file mode 100644 index 00000000..88a32e9e --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/telemetry/core/configuration/limits/Filter.java @@ -0,0 +1,86 @@ +/******************************************************************************* + * Copyright (c) 2024 Red Hat, Inc. + * Distributed under license by Red Hat, Inc. All rights reserved. + * This program is made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + ******************************************************************************/ +package com.redhat.devtools.intellij.telemetry.core.configuration.limits; + +import com.redhat.devtools.intellij.telemetry.core.service.Event; +import com.redhat.devtools.intellij.telemetry.core.util.BasicGlobPattern; + +public interface Filter { + + boolean isMatching(Event event); + + boolean isIncludedByRatio(float percentile); + + boolean isExcludedByRatio(float percentile); + + class EventPropertyFilter implements Filter { + private final String name; + private final BasicGlobPattern glob; + + EventPropertyFilter(String name, String valueGlob) { + this.name = name; + this.glob = BasicGlobPattern.compile(valueGlob); + } + + @Override + public boolean isMatching(Event event) { + String value = event.getProperties().get(name); + return glob.matches(value); + } + + @Override + public boolean isIncludedByRatio(float percentile) { + return true; + } + + @Override + public boolean isExcludedByRatio(float percentile) { + return false; + } + + } + + class EventNameFilter implements Filter { + private final BasicGlobPattern name; + private final float ratio; + private final String dailyLimit; + + EventNameFilter(String name, float ratio, String dailyLimit) { + this.name = BasicGlobPattern.compile(name); + this.ratio = ratio; + this.dailyLimit = dailyLimit; + } + + public float getRatio() { + return ratio; + } + + public String getDailyLimit() { + return dailyLimit; + } + + @Override + public boolean isMatching(Event event) { + return name.matches(event.getName()); + } + + @Override + public boolean isIncludedByRatio(float percentile) { + return ratio != 0 + && percentile <= ratio; + } + + @Override + public boolean isExcludedByRatio(float percentile) { + return 1 - ratio < percentile; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/redhat/devtools/intellij/telemetry/core/configuration/limits/MessageLimits.java b/src/main/java/com/redhat/devtools/intellij/telemetry/core/configuration/limits/MessageLimits.java new file mode 100644 index 00000000..edbecff7 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/telemetry/core/configuration/limits/MessageLimits.java @@ -0,0 +1,102 @@ +/******************************************************************************* + * Copyright (c) 2024 Red Hat, Inc. + * Distributed under license by Red Hat, Inc. All rights reserved. + * This program is made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + ******************************************************************************/ +package com.redhat.devtools.intellij.telemetry.core.configuration.limits; + +import com.intellij.openapi.util.text.StringUtil; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.nio.file.attribute.FileTime; +import java.time.Duration; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Collections; +import java.util.List; + +public class MessageLimits { + + private static final Duration DEFAULT_REFRESH_PERIOD = Duration.ofHours(6); + private static MessageLimits INSTANCE = null; + private final PluginLimitsFactory factory; + private final Configurations configuration; + private List limits; + + static MessageLimits getInstance() { + if (INSTANCE == null) { + INSTANCE = new MessageLimits(PluginLimitsDeserialization::create, new Configurations()); + } + return INSTANCE; + } + + interface PluginLimitsFactory { + List create(String json) throws IOException; + } + + MessageLimits(PluginLimitsFactory factory, Configurations configuration) { + this.factory = factory; + this.configuration = configuration; + } + + /** for testing purposes **/ + MessageLimits(List limits, PluginLimitsFactory factory, Configurations configuration) { + this(factory,configuration); + this.limits = limits; + } + + List get() { + PluginLimits defaultLimits = getDefaultLimits(limits); + Duration refreshAfter = getRefreshAfter(defaultLimits); + FileTime lastModified = configuration.getLocalLastModified(); + if (needsRefresh(refreshAfter, lastModified)) { + this.limits = createLimits(configuration.getRemote(), factory); + } + return limits; + } + + private boolean needsRefresh(Duration refreshAfter, FileTime modified) { + if (modified == null) { + return true; + } + LocalDateTime modificationLocalTime = LocalDateTime.ofInstant(modified.toInstant(), ZoneId.systemDefault()); + LocalDateTime refreshAt = modificationLocalTime.plus(refreshAfter); + return refreshAt.isBefore(LocalDateTime.now()); + } + + @Nullable + private PluginLimits getDefaultLimits(List limits) { + if (limits == null) { + return null; + } + return limits.stream() + .filter(PluginLimits::isDefault) + .findAny() + .orElse(null); + } + + private Duration getRefreshAfter(PluginLimits limits) { + if (limits == null + || limits.getRefresh() == -1) { + return DEFAULT_REFRESH_PERIOD; + } + return Duration.ofHours(limits.getRefresh()); + } + + private List createLimits(String config, PluginLimitsFactory factory) { + try { + if (StringUtil.isEmptyOrSpaces(config)) { + return Collections.emptyList(); + } + return factory.create(config); + } catch (IOException e) { + return Collections.emptyList(); + } + } +} diff --git a/src/main/java/com/redhat/devtools/intellij/telemetry/core/configuration/limits/PluginLimits.java b/src/main/java/com/redhat/devtools/intellij/telemetry/core/configuration/limits/PluginLimits.java new file mode 100644 index 00000000..3a2f4699 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/telemetry/core/configuration/limits/PluginLimits.java @@ -0,0 +1,120 @@ +/******************************************************************************* + * Copyright (c) 2024 Red Hat, Inc. + * Distributed under license by Red Hat, Inc. All rights reserved. + * This program is made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + ******************************************************************************/ +package com.redhat.devtools.intellij.telemetry.core.configuration.limits; + +import com.redhat.devtools.intellij.telemetry.core.service.Event; +import com.redhat.devtools.intellij.telemetry.core.service.UserId; + +import java.util.List; + +public class PluginLimits { + private final String name; + private final Enabled enabled; + private final int refresh; + private final float ratio; + private final List includes; + private final List excludes; + private final UserId userId; + + PluginLimits(String name, Enabled enabled, int refresh, float ratio, List includes, List excludes) { + this(name, enabled, refresh, ratio, includes, excludes, UserId.INSTANCE); + } + + PluginLimits(String name, Enabled enabled, int refresh, float ratio, List includes, List excludes, UserId userId) { + this.name = name; + this.enabled = enabled; + this.refresh = refresh; + this.ratio = ratio; + this.includes = includes; + this.excludes = excludes; + this.userId = userId; + } + + public boolean isDefault() { + return "*".equals(name); + } + + Enabled getEnabled() { + return enabled; + } + + int getRefresh() { + return refresh; + } + + float getRatio() { + return ratio; + } + + public boolean canSend(Event event) { + if (event == null) { + return false; + } + if (!isEnabled() + || (isErrorOnly() && !event.hasError())) { + return false; + } + + if (!isInRatio()) { + return false; + } + + return isIncluded(event) + && !isExcluded(event); + } + + private boolean isInRatio() { + if (userId == null) { + return true; + } + return ratio > 0 + && ratio >= userId.getPercentile(); + } + + boolean isEnabled() { + Enabled enabled = getEnabled(); + return enabled != null + && enabled != Enabled.OFF; + } + + boolean isErrorOnly() { + Enabled enabled = getEnabled(); + return enabled == Enabled.CRASH + || enabled == Enabled.ERROR; + } + + List getIncludes() { + return includes; + } + + boolean isIncluded(Event event) { + Filter matching = includes.stream() + .filter(filter -> filter.isMatching(event)) + .findAny() + .orElse(null); + return matching == null + || matching.isIncludedByRatio(userId.getPercentile()); + } + + boolean isExcluded(Event event) { + Filter matching = excludes.stream() + .filter(filter -> filter.isMatching(event)) + .findAny() + .orElse(null); + return matching != null + && matching.isExcludedByRatio(userId.getPercentile()); + } + + List getExcludes() { + return excludes; + } + +} \ No newline at end of file diff --git a/src/main/java/com/redhat/devtools/intellij/telemetry/core/configuration/limits/PluginLimitsDeserialization.java b/src/main/java/com/redhat/devtools/intellij/telemetry/core/configuration/limits/PluginLimitsDeserialization.java new file mode 100644 index 00000000..13f5bd24 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/telemetry/core/configuration/limits/PluginLimitsDeserialization.java @@ -0,0 +1,168 @@ +/******************************************************************************* + * Copyright (c) 2024 Red Hat, Inc. + * Distributed under license by Red Hat, Inc. All rights reserved. + * This program is made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + ******************************************************************************/ +package com.redhat.devtools.intellij.telemetry.core.configuration.limits; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.intellij.openapi.util.text.StringUtil; +import com.redhat.devtools.intellij.telemetry.core.configuration.limits.Filter.EventNameFilter; +import com.redhat.devtools.intellij.telemetry.core.configuration.limits.Filter.EventPropertyFilter; +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +class PluginLimitsDeserialization extends StdDeserializer> { + + public static final String FIELDNAME_ENABLED = "enabled"; + public static final String FIELDNAME_REFRESH = "refresh"; + public static final String FIELDNAME_RATIO = "ratio"; + public static final String FIELDNAME_INCLUDES = "includes"; + public static final String FIELDNAME_EXCLUDES = "excludes"; + public static final String FIELDNAME_PROPERTY = "property"; + public static final String FIELDNAME_VALUE = "value"; + public static final String FIELDNAME_DAILY_LIMIT = "dailyLimit"; + public static final String FIELDNAME_NAME = "name"; + + public static List create(String json) throws JsonProcessingException { + ObjectMapper mapper = new ObjectMapper(); + SimpleModule module = new SimpleModule(); + module.addDeserializer(List.class, new PluginLimitsDeserialization()); + mapper.registerModule(module); + return mapper.readValue(json, List.class); + } + + PluginLimitsDeserialization() { + this(null); + } + + PluginLimitsDeserialization(Class clazz) { + super(clazz); + } + + @Override + public List deserialize(JsonParser parser, DeserializationContext ctx) throws IOException { + JsonNode node = parser.getCodec().readTree(parser); + Spliterator> spliterator = Spliterators.spliteratorUnknownSize(node.fields(), Spliterator.IMMUTABLE); + return StreamSupport.stream(spliterator, false) + .map(this::createMessageLimit) + .collect(Collectors.toList()); + } + + @NotNull + private PluginLimits createMessageLimit(Map.Entry entry) { + String pattern = entry.getKey(); + JsonNode properties = entry.getValue(); + Enabled enabled = getEnabled(properties.get(FIELDNAME_ENABLED)); + int refresh = getRefresh(properties.get(FIELDNAME_REFRESH)); + float ratio = getRatio(properties.get(FIELDNAME_RATIO)); + List includes = getFilters(properties.get(FIELDNAME_INCLUDES)); + List excludes = getFilters(properties.get(FIELDNAME_EXCLUDES)); + + return new PluginLimits(pattern, enabled, refresh, ratio, includes, excludes); + } + + private Enabled getEnabled(JsonNode node) { + String value = node != null ? node.asText() : null; + return Enabled.safeValueOf(value); + } + + private int getRefresh(JsonNode node) { + int numeric = -1; + if (node != null) { + String refresh = getNumericPortion(node.asText().toCharArray()); + if (!StringUtil.isEmptyOrSpaces(refresh)) { + try { + numeric = Integer.parseInt(refresh); + } catch (NumberFormatException e) { + // swallow + } + } + } + return numeric; + } + + private List getFilters(JsonNode node) { + if (node == null + || !node.isArray()) { + return Collections.emptyList(); + } + Spliterator spliterator = Spliterators.spliteratorUnknownSize(node.elements(), Spliterator.IMMUTABLE); + return StreamSupport.stream(spliterator, false) + .map(this::createMessageLimitFilter) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + private Filter createMessageLimitFilter(JsonNode node) { + if (node.has(FIELDNAME_NAME)) { + return createEventNameFilter(node); + } else if (node.has(FIELDNAME_PROPERTY) + && node.has(FIELDNAME_VALUE)) { + return createEventPropertyFilter(node); + } else { + return null; + } + } + + private EventNameFilter createEventNameFilter(JsonNode node) { + String name = getStringValue(FIELDNAME_NAME, node); + float ratio = getRatio(node.get(FIELDNAME_RATIO)); + String dailyLimit = getStringValue(FIELDNAME_DAILY_LIMIT, node); + return new EventNameFilter(name, ratio, dailyLimit); + } + + private EventPropertyFilter createEventPropertyFilter(JsonNode node) { + String property = getStringValue(FIELDNAME_PROPERTY, node); + String value = getStringValue(FIELDNAME_VALUE, node); + return new EventPropertyFilter(property, value); + } + + private static float getRatio(JsonNode node) { + float numeric = 1f; + if (node != null) { + try { + numeric = Float.parseFloat(node.asText()); + } catch (NumberFormatException e) { + // swallow + } + } + return numeric; + } + + private static String getStringValue(String name, JsonNode node) { + if (node == null + || node.get(name) == null) { + return null; + } + return node.get(name).asText(); + } + + private static String getNumericPortion(char[] characters) { + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < characters.length && Character.isDigit(characters[i]); i++) { + builder.append(characters[i]); + } + return builder.toString(); + } +} \ No newline at end of file diff --git a/src/main/java/com/redhat/devtools/intellij/telemetry/core/service/Event.java b/src/main/java/com/redhat/devtools/intellij/telemetry/core/service/Event.java index 7b8ca510..803a3387 100644 --- a/src/main/java/com/redhat/devtools/intellij/telemetry/core/service/Event.java +++ b/src/main/java/com/redhat/devtools/intellij/telemetry/core/service/Event.java @@ -13,6 +13,8 @@ import java.util.HashMap; import java.util.Map; +import static com.redhat.devtools.intellij.telemetry.core.service.Message.PROP_ERROR; + public class Event { public enum Type { @@ -44,4 +46,9 @@ public String getName() { public Map getProperties() { return properties; } + + public boolean hasError() { + return properties != null + && properties.containsKey(PROP_ERROR); + } } diff --git a/src/main/java/com/redhat/devtools/intellij/telemetry/core/service/UserId.java b/src/main/java/com/redhat/devtools/intellij/telemetry/core/service/UserId.java index 6ebaba76..7bf588e8 100644 --- a/src/main/java/com/redhat/devtools/intellij/telemetry/core/service/UserId.java +++ b/src/main/java/com/redhat/devtools/intellij/telemetry/core/service/UserId.java @@ -31,6 +31,7 @@ public class UserId { private static final Pattern UUID_REGEX = Pattern.compile("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"); private final Lazy uuid = new Lazy<>(() -> loadOrCreate(UUID_FILE)); + private final Lazy percentile = new Lazy<>(this::createPercentile); /** for testing purposes */ protected UserId() {} @@ -91,4 +92,29 @@ protected void write(String uuid, Path uuidFile) { LOGGER.warn("Could not write redhat anonymous UUID to file at " + UUID_FILE.toAbsolutePath(), e); } } + + @Override + public int hashCode() { + int hash = 0; + String uuid = get(); + for (int i = 0; i < uuid.length(); i++) { + int code = uuid.codePointAt(i); + hash = ((hash << 5) - hash) + code; + } + return hash; + } + + public float getPercentile() { + return percentile.get(); + } + + private float createPercentile() { + try { + String hash = String.valueOf(Math.abs(hashCode())); + int length = Math.min(4, hash.length()); + return Float.parseFloat(hash.substring(hash.length() - length)) / 10000; // use at most last 4 chars + } catch (NumberFormatException e) { + return 0; + } + } } diff --git a/src/main/java/com/redhat/devtools/intellij/telemetry/core/util/BasicGlobPattern.java b/src/main/java/com/redhat/devtools/intellij/telemetry/core/util/BasicGlobPattern.java new file mode 100644 index 00000000..f8cb2486 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/telemetry/core/util/BasicGlobPattern.java @@ -0,0 +1,323 @@ +/************************************************************************* + * Copyright (c) 2024 Red Hat, Inc. + * License: GNU General Public License version 2 plus the Classpath exception + * + * Based on implementation at sun.nio.fs.Globs in jdk 11.0.2 + * + * Copyright (c) 2008, 2009, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + ******************************************************************************/ +package com.redhat.devtools.intellij.telemetry.core.util; + +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +/** + * A simple Glob Pattern that supports: + *
    + *
  • placeholders {@code ?}
  • + *
  • wildcard {@code *}
  • + *
  • brace expansions {@code {alternative1,alternative2,}}
  • + *
  • ranges {@code [1-4]}
  • + *
+ * It does not support extended (advanced-, posix-) glob expressions like alternatives {@code @(a|b) or +(a|b) etc )} + */ +public class BasicGlobPattern { + + private static final String regexMetaChars = ".^$+{[]|()"; + private static final String globMetaChars = "\\*?[{"; + + private final Pattern globPattern; + + public static BasicGlobPattern compile(String glob) { + return new Factory().create(glob); + } + + private BasicGlobPattern(Pattern globPattern) { + this.globPattern = globPattern; + } + + public boolean matches(String toMatch) { + return globPattern.matcher(toMatch).matches(); + } + + private static final class Factory { + + private static final class GlobParserContext { + + public static final char EOL = 0; + + private final String globPattern; + private final StringBuilder builder = new StringBuilder("^"); + + private boolean inGroup = false; + private boolean hasRangeStart = false; + private int index = 0; + private char lastRangeCharacter = 0; + + GlobParserContext(String globPattern) { + this.globPattern = globPattern; + } + + String getGlobPattern() { + return globPattern; + } + + int getGlobIndex() { + return index; + } + + boolean globEndReached() { + return index >= globPattern.length(); + } + + char peekGlob() { + if (index < globPattern.length()) { + return globPattern.charAt(index); + } + return EOL; + } + + char pollGlob() { + if (index < globPattern.length()) { + return globPattern.charAt(index++); + } + return EOL; + } + + void nextGlobChar() { + index++; + } + + GlobParserContext appendToRegex(String toAppend) { + builder.append(toAppend); + return this; + } + + GlobParserContext appendToRegex(char toAppend) { + builder.append(toAppend); + return this; + } + + GlobParserContext setInGroup(boolean inGroup) { + this.inGroup = inGroup; + return this; + } + + boolean isInGroup() { + return inGroup; + } + + GlobParserContext setInRange(boolean hasRangeStart) { + this.hasRangeStart = hasRangeStart; + return this; + } + + boolean isInRange() { + return hasRangeStart; + } + + void setLastRangeCharacter(char character) { + this.lastRangeCharacter = character; + } + + char getLastRangeCharacter() { + return lastRangeCharacter; + } + + String getRegex() { + return builder.toString(); + } + } + + private BasicGlobPattern create(String glob) { + Pattern globPattern = createRegex(glob); + return new BasicGlobPattern(globPattern); + } + + private Pattern createRegex(String globPattern) { + if (globPattern == null) { + return null; + } + GlobParserContext context = new GlobParserContext(globPattern); + while (!context.globEndReached()) { + char c = context.pollGlob(); + switch (c) { + case '\\': + handleEscape(context); + break; + case '/': + context.appendToRegex(c); + break; + case '[': + handleSquareOpen(context); + break; + case '{': + handleCurlyOpen(context); + break; + case '}': + handleCurlyClose(context); + break; + case ',': + handleComma(context); + break; + case '*': + handleWildcard(context); + break; + case '?': + handleQuestionMark(context); + break; + + default: + handleDefaultCharacter(c, context); + } + } + + if (context.isInGroup()) { + throw new PatternSyntaxException("Missing '}", globPattern, context.getGlobIndex() - 1); + } + + context.appendToRegex('$'); + return Pattern.compile(context.getRegex()); + } + + private void handleEscape(GlobParserContext context) { + // escape special characters + if (context.globEndReached()) { + throw new PatternSyntaxException("No character to escape", context.getGlobPattern(), context.getGlobIndex() - 1); + } + char next = context.pollGlob(); + if (isGlobMeta(next) || isRegexMeta(next)) { + context.appendToRegex('\\'); + } + context.appendToRegex(next); + } + + private void handleSquareOpen(GlobParserContext context) { + char character = '['; + // don't match name separator in class + context.appendToRegex("[[^/]&&["); + if (context.peekGlob() == '^') { + // escape the regex negation char if it appears + context.appendToRegex("\\^").nextGlobChar(); + } else { + // negation + if (context.peekGlob() == '!') { + context.appendToRegex('^').nextGlobChar(); + } + // hyphen allowed at start + if (context.peekGlob() == '-') { + context.appendToRegex('-').nextGlobChar(); + } + } + while (!context.globEndReached()) { + character = context.pollGlob(); + if (character == ']') { + break; + } + if (character == '/') { + throw new PatternSyntaxException("Explicit 'name separator' in class", context.getGlobPattern(), context.getGlobIndex() - 1); + } + // TBD: how to specify ']' in a class? + if (character == '\\' || character == '[' || + character == '&' && context.peekGlob() == '&') { + // escape '\', '[' or "&&" for regex class + context.appendToRegex('\\'); + } + context.appendToRegex(character); + + if (character == '-') { + if (!context.isInRange()) { + throw new PatternSyntaxException("Invalid range", context.getGlobPattern(), context.getGlobIndex() - 1); + } + character = context.pollGlob(); + if (character == GlobParserContext.EOL || character == ']') { + break; + } + if (character < context.getLastRangeCharacter()) { + throw new PatternSyntaxException("Invalid range", context.getGlobPattern(), context.getGlobIndex() - 3); + } + context.appendToRegex(character).setInRange(false); + } else { + context.setInRange(true).setLastRangeCharacter(character); + } + } + if (character != ']') { + throw new PatternSyntaxException("Missing ']", context.getGlobPattern(), context.getGlobIndex() - 1); + } + context.appendToRegex("]]"); + } + + private void handleCurlyOpen(GlobParserContext context) { + if (context.isInGroup()) { + throw new PatternSyntaxException("Cannot nest groups", context.getGlobPattern(), context.getGlobIndex() - 1); + } + context.setInGroup(true).appendToRegex("(?:(?:"); + } + + private void handleCurlyClose(GlobParserContext context) { + if (context.isInGroup()) { + context.appendToRegex("))").setInGroup(false); + } else { + context.appendToRegex('}'); + } + } + + private void handleComma(GlobParserContext context) { + if (context.isInGroup()) { + context.appendToRegex(")|(?:"); + } else { + context.appendToRegex(','); + } + } + + private void handleWildcard(GlobParserContext context) { + if (context.peekGlob() == '*') { + // crosses directory boundaries + context.appendToRegex(".*").nextGlobChar(); + } else { + // within directory boundary + context.appendToRegex("[^/]*"); + } + } + + private void handleQuestionMark(GlobParserContext context) { + context.appendToRegex("[^/]"); + } + + private void handleDefaultCharacter(char c, GlobParserContext context) { + if (isRegexMeta(c)) { + context.appendToRegex('\\'); + } + context.appendToRegex(c); + } + + private boolean isRegexMeta(char c) { + return regexMetaChars.indexOf(c) != -1; + } + + private boolean isGlobMeta(char c) { + return globMetaChars.indexOf(c) != -1; + } + } +} diff --git a/src/main/java/com/redhat/devtools/intellij/telemetry/core/util/LICENCE b/src/main/java/com/redhat/devtools/intellij/telemetry/core/util/LICENCE new file mode 100644 index 00000000..8b400c7a --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/telemetry/core/util/LICENCE @@ -0,0 +1,347 @@ +The GNU General Public License (GPL) + +Version 2, June 1991 + +Copyright (C) 1989, 1991 Free Software Foundation, Inc. +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Everyone is permitted to copy and distribute verbatim copies of this license +document, but changing it is not allowed. + +Preamble + +The licenses for most software are designed to take away your freedom to share +and change it. By contrast, the GNU General Public License is intended to +guarantee your freedom to share and change free software--to make sure the +software is free for all its users. This General Public License applies to +most of the Free Software Foundation's software and to any other program whose +authors commit to using it. (Some other Free Software Foundation software is +covered by the GNU Library General Public License instead.) You can apply it to +your programs, too. + +When we speak of free software, we are referring to freedom, not price. Our +General Public Licenses are designed to make sure that you have the freedom to +distribute copies of free software (and charge for this service if you wish), +that you receive source code or can get it if you want it, that you can change +the software or use pieces of it in new free programs; and that you know you +can do these things. + +To protect your rights, we need to make restrictions that forbid anyone to deny +you these rights or to ask you to surrender the rights. These restrictions +translate to certain responsibilities for you if you distribute copies of the +software, or if you modify it. + +For example, if you distribute copies of such a program, whether gratis or for +a fee, you must give the recipients all the rights that you have. You must +make sure that they, too, receive or can get the source code. And you must +show them these terms so they know their rights. + +We protect your rights with two steps: (1) copyright the software, and (2) +offer you this license which gives you legal permission to copy, distribute +and/or modify the software. + +Also, for each author's protection and ours, we want to make certain that +everyone understands that there is no warranty for this free software. If the +software is modified by someone else and passed on, we want its recipients to +know that what they have is not the original, so that any problems introduced +by others will not reflect on the original authors' reputations. + +Finally, any free program is threatened constantly by software patents. We +wish to avoid the danger that redistributors of a free program will +individually obtain patent licenses, in effect making the program proprietary. +To prevent this, we have made it clear that any patent must be licensed for +everyone's free use or not licensed at all. + +The precise terms and conditions for copying, distribution and modification +follow. + +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +0. This License applies to any program or other work which contains a notice +placed by the copyright holder saying it may be distributed under the terms of +this General Public License. The "Program", below, refers to any such program +or work, and a "work based on the Program" means either the Program or any +derivative work under copyright law: that is to say, a work containing the +Program or a portion of it, either verbatim or with modifications and/or +translated into another language. (Hereinafter, translation is included +without limitation in the term "modification".) Each licensee is addressed as +"you". + +Activities other than copying, distribution and modification are not covered by +this License; they are outside its scope. The act of running the Program is +not restricted, and the output from the Program is covered only if its contents +constitute a work based on the Program (independent of having been made by +running the Program). Whether that is true depends on what the Program does. + +1. You may copy and distribute verbatim copies of the Program's source code as +you receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice and +disclaimer of warranty; keep intact all the notices that refer to this License +and to the absence of any warranty; and give any other recipients of the +Program a copy of this License along with the Program. + +You may charge a fee for the physical act of transferring a copy, and you may +at your option offer warranty protection in exchange for a fee. + +2. You may modify your copy or copies of the Program or any portion of it, thus +forming a work based on the Program, and copy and distribute such modifications +or work under the terms of Section 1 above, provided that you also meet all of +these conditions: + + a) You must cause the modified files to carry prominent notices stating + that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in whole or + in part contains or is derived from the Program or any part thereof, to be + licensed as a whole at no charge to all third parties under the terms of + this License. + + c) If the modified program normally reads commands interactively when run, + you must cause it, when started running for such interactive use in the + most ordinary way, to print or display an announcement including an + appropriate copyright notice and a notice that there is no warranty (or + else, saying that you provide a warranty) and that users may redistribute + the program under these conditions, and telling the user how to view a copy + of this License. (Exception: if the Program itself is interactive but does + not normally print such an announcement, your work based on the Program is + not required to print an announcement.) + +These requirements apply to the modified work as a whole. If identifiable +sections of that work are not derived from the Program, and can be reasonably +considered independent and separate works in themselves, then this License, and +its terms, do not apply to those sections when you distribute them as separate +works. But when you distribute the same sections as part of a whole which is a +work based on the Program, the distribution of the whole must be on the terms +of this License, whose permissions for other licensees extend to the entire +whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest your +rights to work written entirely by you; rather, the intent is to exercise the +right to control the distribution of derivative or collective works based on +the Program. + +In addition, mere aggregation of another work not based on the Program with the +Program (or with a work based on the Program) on a volume of a storage or +distribution medium does not bring the other work under the scope of this +License. + +3. You may copy and distribute the Program (or a work based on it, under +Section 2) in object code or executable form under the terms of Sections 1 and +2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable source + code, which must be distributed under the terms of Sections 1 and 2 above + on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three years, to + give any third party, for a charge no more than your cost of physically + performing source distribution, a complete machine-readable copy of the + corresponding source code, to be distributed under the terms of Sections 1 + and 2 above on a medium customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer to + distribute corresponding source code. (This alternative is allowed only + for noncommercial distribution and only if you received the program in + object code or executable form with such an offer, in accord with + Subsection b above.) + +The source code for a work means the preferred form of the work for making +modifications to it. For an executable work, complete source code means all +the source code for all modules it contains, plus any associated interface +definition files, plus the scripts used to control compilation and installation +of the executable. However, as a special exception, the source code +distributed need not include anything that is normally distributed (in either +source or binary form) with the major components (compiler, kernel, and so on) +of the operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the source +code from the same place counts as distribution of the source code, even though +third parties are not compelled to copy the source along with the object code. + +4. You may not copy, modify, sublicense, or distribute the Program except as +expressly provided under this License. Any attempt otherwise to copy, modify, +sublicense or distribute the Program is void, and will automatically terminate +your rights under this License. However, parties who have received copies, or +rights, from you under this License will not have their licenses terminated so +long as such parties remain in full compliance. + +5. You are not required to accept this License, since you have not signed it. +However, nothing else grants you permission to modify or distribute the Program +or its derivative works. These actions are prohibited by law if you do not +accept this License. Therefore, by modifying or distributing the Program (or +any work based on the Program), you indicate your acceptance of this License to +do so, and all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + +6. Each time you redistribute the Program (or any work based on the Program), +the recipient automatically receives a license from the original licensor to +copy, distribute or modify the Program subject to these terms and conditions. +You may not impose any further restrictions on the recipients' exercise of the +rights granted herein. You are not responsible for enforcing compliance by +third parties to this License. + +7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), conditions +are imposed on you (whether by court order, agreement or otherwise) that +contradict the conditions of this License, they do not excuse you from the +conditions of this License. If you cannot distribute so as to satisfy +simultaneously your obligations under this License and any other pertinent +obligations, then as a consequence you may not distribute the Program at all. +For example, if a patent license would not permit royalty-free redistribution +of the Program by all those who receive copies directly or indirectly through +you, then the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply and +the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any patents or +other property right claims or to contest validity of any such claims; this +section has the sole purpose of protecting the integrity of the free software +distribution system, which is implemented by public license practices. Many +people have made generous contributions to the wide range of software +distributed through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing to +distribute software through any other system and a licensee cannot impose that +choice. + +This section is intended to make thoroughly clear what is believed to be a +consequence of the rest of this License. + +8. If the distribution and/or use of the Program is restricted in certain +countries either by patents or by copyrighted interfaces, the original +copyright holder who places the Program under this License may add an explicit +geographical distribution limitation excluding those countries, so that +distribution is permitted only in or among countries not thus excluded. In +such case, this License incorporates the limitation as if written in the body +of this License. + +9. The Free Software Foundation may publish revised and/or new versions of the +General Public License from time to time. Such new versions will be similar in +spirit to the present version, but may differ in detail to address new problems +or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any later +version", you have the option of following the terms and conditions either of +that version or of any later version published by the Free Software Foundation. +If the Program does not specify a version number of this License, you may +choose any version ever published by the Free Software Foundation. + +10. If you wish to incorporate parts of the Program into other free programs +whose distribution conditions are different, write to the author to ask for +permission. For software which is copyrighted by the Free Software Foundation, +write to the Free Software Foundation; we sometimes make exceptions for this. +Our decision will be guided by the two goals of preserving the free status of +all derivatives of our free software and of promoting the sharing and reuse of +software generally. + +NO WARRANTY + +11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR +THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE +STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE +PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND +PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, +YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL +ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE +PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR +INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA +BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER +OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest possible +use to the public, the best way to achieve this is to make it free software +which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest to attach +them to the start of each source file to most effectively convey the exclusion +of warranty; and each file should have at least the "copyright" line and a +pointer to where the full notice is found. + + One line to give the program's name and a brief idea of what it does. + + Copyright (C) + + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU General Public License as published by the Free + Software Foundation; either version 2 of the License, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this when it +starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author Gnomovision comes + with ABSOLUTELY NO WARRANTY; for details type 'show w'. This is free + software, and you are welcome to redistribute it under certain conditions; + type 'show c' for details. + +The hypothetical commands 'show w' and 'show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may be +called something other than 'show w' and 'show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your school, +if any, to sign a "copyright disclaimer" for the program, if necessary. Here +is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + 'Gnomovision' (which makes passes at compilers) written by James Hacker. + + signature of Ty Coon, 1 April 1989 + + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General Public +License instead of this License. + + +"CLASSPATH" EXCEPTION TO THE GPL + +Certain source files distributed by Oracle America and/or its affiliates are +subject to the following clarification and special exception to the GPL, but +only where Oracle has expressly included in the particular source file's header +the words "Oracle designates this particular file as subject to the "Classpath" +exception as provided by Oracle in the LICENSE file that accompanied this code." + + Linking this library statically or dynamically with other modules is making + a combined work based on this library. Thus, the terms and conditions of + the GNU General Public License cover the whole combination. + + As a special exception, the copyright holders of this library give you + permission to link this library with independent modules to produce an + executable, regardless of the license terms of these independent modules, + and to copy and distribute the resulting executable under terms of your + choice, provided that you also meet, for each linked independent module, + the terms and conditions of the license of that module. An independent + module is a module which is not derived from or based on this library. If + you modify this library, you may extend this exception to your version of + the library, but you are not obligated to do so. If you do not wish to do + so, delete this exception statement from your version. diff --git a/src/main/resources/telemetry-config.json b/src/main/resources/telemetry-config.json new file mode 100644 index 00000000..f08316e1 --- /dev/null +++ b/src/main/resources/telemetry-config.json @@ -0,0 +1,17 @@ +{ + "*": { + "enabled":"all", + "refresh": "12h", + "includes": [ + { + "name" : "*" + } + ], + "excludes": [ + { + "name": "shutdown", + "ratio": "1.0" + } + ] + } +} \ No newline at end of file diff --git a/src/test/java/com/redhat/devtools/intellij/telemetry/core/configuration/limits/ConfigurationsIntegrationTest.java b/src/test/java/com/redhat/devtools/intellij/telemetry/core/configuration/limits/ConfigurationsIntegrationTest.java new file mode 100644 index 00000000..3fc55f63 --- /dev/null +++ b/src/test/java/com/redhat/devtools/intellij/telemetry/core/configuration/limits/ConfigurationsIntegrationTest.java @@ -0,0 +1,125 @@ +/******************************************************************************* + * Copyright (c) 2024 Red Hat, Inc. + * Distributed under license by Red Hat, Inc. All rights reserved. + * This program is made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + ******************************************************************************/ +package com.redhat.devtools.intellij.telemetry.core.configuration.limits; + +import org.junit.jupiter.api.Test; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.util.stream.Collectors; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +public class ConfigurationsIntegrationTest { + + @Test + public void getRemote_can_download_remote_config() { + // given + Configurations configurations = new Configurations(); + // when + String remote = configurations.getRemote(); + // then + assertThat(remote).isNotEmpty(); + } + + @Test + public void getRemote_writes_remote_to_local_file() throws IOException { + // given + Path backup = Files.createTempFile(Configurations.LOCAL.getFileName().toString(), null); + boolean isBacked = safeMove(Configurations.LOCAL, backup); + assertThat(Files.exists(Configurations.LOCAL)).isFalse(); + Configurations configurations = new Configurations(); + // when + try { + configurations.getRemote(); + // then + assertThat(Files.exists(Configurations.LOCAL)).isTrue(); + } finally { + if (isBacked) { + safeMove(backup, Configurations.LOCAL); + } + } + } + + @Test + public void getRemote_returns_content_that_is_equal_to_local_file() throws IOException { + // given + Path backup = Files.createTempFile(Configurations.LOCAL.getFileName().toString(), null); + boolean isBacked = safeMove(Configurations.LOCAL, backup); + assertThat(Files.exists(Configurations.LOCAL)).isFalse(); + Configurations configurations = new Configurations(); + String remote = null; + // when + try { + remote = configurations.getRemote(); + String file = toString(Configurations.LOCAL); + // then + assertThat(remote).isEqualTo(file); + } finally { + if (isBacked) { + safeMove(backup, Configurations.LOCAL); + } + } + } + + @Test + public void getLocal_returns_content_of_local_file() throws IOException { + // given + Path backup = Files.createTempFile(Configurations.LOCAL.getFileName().toString(), null); + boolean isBacked = safeMove(Configurations.LOCAL, backup); + assertThat(Files.exists(Configurations.LOCAL)).isFalse(); + String expected = "yoda"; + Files.write(Configurations.LOCAL, expected.getBytes(), StandardOpenOption.CREATE); + Configurations configurations = new Configurations(); + // when + try { + String local = configurations.getLocal(); + // then + assertThat(local).isEqualTo(expected); + } finally { + if (isBacked) { + safeMove(backup, Configurations.LOCAL); + } + } + } + + private boolean safeMove(Path source, Path destination) { + try { + Files.move(source, destination, StandardCopyOption.REPLACE_EXISTING); + return true; + } catch (IOException e) { + return false; + } + } + + private String toString(Path path) throws IOException { + BufferedReader reader = new BufferedReader(new InputStreamReader(Files.newInputStream(path))); + return reader.lines().collect(Collectors.joining()); + } + + private void safeDelete(Path toDelete) { + try { + Files.delete(toDelete); + } catch (IOException e) { + // swallow + } + } +} diff --git a/src/test/java/com/redhat/devtools/intellij/telemetry/core/configuration/limits/ConfigurationsTest.java b/src/test/java/com/redhat/devtools/intellij/telemetry/core/configuration/limits/ConfigurationsTest.java new file mode 100644 index 00000000..7a3057f5 --- /dev/null +++ b/src/test/java/com/redhat/devtools/intellij/telemetry/core/configuration/limits/ConfigurationsTest.java @@ -0,0 +1,103 @@ +/******************************************************************************* + * Copyright (c) 2024 Red Hat, Inc. + * Distributed under license by Red Hat, Inc. All rights reserved. + * This program is made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + ******************************************************************************/ +package com.redhat.devtools.intellij.telemetry.core.configuration.limits; + +import okhttp3.Call; +import okhttp3.OkHttpClient; +import okhttp3.Response; +import okhttp3.ResponseBody; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.file.attribute.FileTime; + +import static com.redhat.devtools.intellij.telemetry.core.configuration.limits.Configurations.*; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static com.redhat.devtools.intellij.telemetry.core.configuration.limits.Configurations.ThrowingSupplier; +import static org.mockito.Mockito.verify; + +public class ConfigurationsTest { + + @Test + public void getLocalLastModified_should_return_null_if_file_doesnt_exist() throws Throwable { + // given + Configurations configurations = new Configurations(); + ThrowingSupplier fileExists = mock(ThrowingSupplier.class); + doReturn(false) + .when(fileExists).get(); + // when + FileTime modified = configurations.getLocalLastModified(fileExists, () -> null); + // then + assertThat(modified).isNull(); + } + + @Test + public void getLocalLastModified_should_return_null_if_checking_if_file_exists_throws() throws Throwable { + // given + Configurations configurations = new Configurations(); + ThrowingSupplier fileExists = mock(ThrowingSupplier.class); + doThrow(SecurityException.class) + .when(fileExists).get(); + // when + FileTime modified = configurations.getLocalLastModified(fileExists, () -> null); + // then + assertThat(modified).isNull(); + } + + @Test + public void getRemote_should_write_response_to_file() throws Throwable { + // given + Configurations configurations = new Configurations(); + OkHttpClient client = mockOkHttpClient("yoda"); + ThrowingInputStreamConsumer fileWriter = mock(ThrowingInputStreamConsumer.class); + ThrowingSupplier fileReader = mock(ThrowingSupplier.class); + // when + configurations.getRemote(fileWriter, client, fileReader); + // then + verify(fileWriter).accept(any()); + } + + @Test + public void getRemote_should_read_local_file() throws Throwable { + // given + Configurations configurations = new Configurations(); + OkHttpClient client = mockOkHttpClient("yoda"); + ThrowingInputStreamConsumer fileWriter = mock(ThrowingInputStreamConsumer.class); + ThrowingSupplier fileReader = mock(ThrowingSupplier.class); + // when + configurations.getRemote(fileWriter, client, fileReader); + // then + verify(fileReader).get(); + } + + private static OkHttpClient mockOkHttpClient(String responseString) throws IOException { + Response response = mock(Response.class); + ResponseBody body = mock(ResponseBody.class); + doReturn(new ByteArrayInputStream(responseString.getBytes())) + .when(body).byteStream(); + doReturn(body) + .when(response).body(); + + OkHttpClient client = mock(OkHttpClient.class); + Call call = mock(Call.class); + doReturn(response) + .when(call).execute(); + doReturn(call) + .when(client).newCall(any()); + return client; + } + +} diff --git a/src/test/java/com/redhat/devtools/intellij/telemetry/core/configuration/limits/EventNameFilterTest.java b/src/test/java/com/redhat/devtools/intellij/telemetry/core/configuration/limits/EventNameFilterTest.java new file mode 100644 index 00000000..f91890f4 --- /dev/null +++ b/src/test/java/com/redhat/devtools/intellij/telemetry/core/configuration/limits/EventNameFilterTest.java @@ -0,0 +1,67 @@ +/******************************************************************************* + * Copyright (c) 2024 Red Hat, Inc. + * Distributed under license by Red Hat, Inc. All rights reserved. + * This program is made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + ******************************************************************************/ +package com.redhat.devtools.intellij.telemetry.core.configuration.limits; + +import com.redhat.devtools.intellij.telemetry.core.configuration.limits.Filter.EventNameFilter; +import com.redhat.devtools.intellij.telemetry.core.service.Event; +import com.redhat.devtools.intellij.telemetry.core.service.UserId; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; + +public class EventNameFilterTest { + + @Test + public void isMatching_should_match_event_name() { + // given + Filter filter = new EventNameFilter("yoda", 0.42f, "42"); + Event event = new Event(Event.Type.USER, "yoda"); + // when + boolean matching = filter.isMatching(event); + // then + assertThat(matching).isTrue(); + } + + @Test + public void isMatching_should_NOT_match_event_name_that_is_different() { + // given + Filter filter = new EventNameFilter("yoda", 0.42f, "42"); + Event event = new Event(Event.Type.USER, "darthvader"); + // when + boolean matching = filter.isMatching(event); + // then + assertThat(matching).isFalse(); + } + + @Test + public void isMatching_should_match_event_name_when_pattern_is_wildcard() { + // given + Filter filter = new EventNameFilter("*", 0.42f, "42"); + Event event = new Event(Event.Type.USER, "skywalker"); + // when + boolean matching = filter.isMatching(event); + // then + assertThat(matching).isTrue(); + } + + @Test + public void isMatching_should_match_event_name_when_pattern_has_name_with_wildcards() { + // given + Filter filter = new EventNameFilter("*walk*", 0.42f, "42"); + Event event = new Event(Event.Type.USER, "skywalker"); + // when + boolean matching = filter.isMatching(event); + // then + assertThat(matching).isTrue(); + } +} diff --git a/src/test/java/com/redhat/devtools/intellij/telemetry/core/configuration/limits/EventPropertyFilterTest.java b/src/test/java/com/redhat/devtools/intellij/telemetry/core/configuration/limits/EventPropertyFilterTest.java new file mode 100644 index 00000000..e57faa2c --- /dev/null +++ b/src/test/java/com/redhat/devtools/intellij/telemetry/core/configuration/limits/EventPropertyFilterTest.java @@ -0,0 +1,72 @@ +/******************************************************************************* + * Copyright (c) 2024 Red Hat, Inc. + * Distributed under license by Red Hat, Inc. All rights reserved. + * This program is made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + ******************************************************************************/ +package com.redhat.devtools.intellij.telemetry.core.configuration.limits; + +import com.redhat.devtools.intellij.telemetry.core.configuration.limits.Filter.EventNameFilter; +import com.redhat.devtools.intellij.telemetry.core.service.Event; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static com.redhat.devtools.intellij.telemetry.core.configuration.limits.Filter.*; +import static org.assertj.core.api.Assertions.assertThat; + +public class EventPropertyFilterTest { + + @Test + public void isMatching_should_match_any_event_with_exact_property_name_and_value() { + // given + Filter filter = new EventPropertyFilter("yoda", "jedi"); + Event event = new Event(Event.Type.USER, "there are jedis in the rebellion", Map.of( + "yoda", "jedi")); + // when + boolean matching = filter.isMatching(event); + // then + assertThat(matching).isTrue(); + } + + @Test + public void isMatching_should_match_any_event_with_exact_property_name_and_wildcard_value() { + // given + Filter filter = new EventPropertyFilter("yoda", "*jedi*"); + Event event = new Event(Event.Type.USER, "there are jedis on both sides", Map.of( + "yoda", "is a master jedi!")); + // when + boolean matching = filter.isMatching(event); + // then + assertThat(matching).isTrue(); + } + + @Test + public void isMatching_should_NOT_match_event_that_doesnt_have_given_property_name() { + // given + Filter filter = new EventPropertyFilter("yoda", "*jedi*"); + Event event = new Event(Event.Type.USER, "there are jedis on both sides", Map.of( + "darth vader", "is a master jedi!")); // key doesnt match + // when + boolean matching = filter.isMatching(event); + // then + assertThat(matching).isFalse(); + } + + @Test + public void isMatching_should_NOT_match_event_that_has_given_property_name_but_doesnt_have_property_value() { + // given + Filter filter = new EventPropertyFilter("yoda", "*jedi*"); + Event event = new Event(Event.Type.USER, "there are jedis on both sides", Map.of( + "yoda", "is stronger than the emperor")); // value doesnt match + // when + boolean matching = filter.isMatching(event); + // then + assertThat(matching).isFalse(); + } + +} diff --git a/src/test/java/com/redhat/devtools/intellij/telemetry/core/configuration/limits/LimitsPersistentState.java b/src/test/java/com/redhat/devtools/intellij/telemetry/core/configuration/limits/LimitsPersistentState.java new file mode 100644 index 00000000..71082c58 --- /dev/null +++ b/src/test/java/com/redhat/devtools/intellij/telemetry/core/configuration/limits/LimitsPersistentState.java @@ -0,0 +1,52 @@ +/******************************************************************************* + * Copyright (c) 2024 Red Hat, Inc. + * Distributed under license by Red Hat, Inc. All rights reserved. + * This program is made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + ******************************************************************************/ +package com.redhat.devtools.intellij.telemetry.core.configuration.limits; + +import com.intellij.openapi.components.PersistentStateComponent; +import com.intellij.openapi.components.RoamingType; +import com.intellij.openapi.components.Service; +import com.intellij.openapi.components.State; +import com.intellij.openapi.components.Storage; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.HashMap; +import java.util.Map; + +@Service +@State(name="limitsState") +@Storage(value = "limitsState.xml", roamingType = RoamingType.PER_OS) +public class LimitsPersistentState implements PersistentStateComponent { + + private LimitsState state; + + @Override + public @Nullable LimitsPersistentState.LimitsState getState() { + return state; + } + + @Override + public void loadState(@NotNull LimitsState state) { + this.state = state; + } + + public static class LimitsState { + private Map properties = new HashMap<>(); + + public String get(String key) { + return properties.get(key); + } + + public void put(String key, String value) { + properties.put(key, value); + } + } +} diff --git a/src/test/java/com/redhat/devtools/intellij/telemetry/core/configuration/limits/MessageLimitsTest.java b/src/test/java/com/redhat/devtools/intellij/telemetry/core/configuration/limits/MessageLimitsTest.java new file mode 100644 index 00000000..5e9d93e6 --- /dev/null +++ b/src/test/java/com/redhat/devtools/intellij/telemetry/core/configuration/limits/MessageLimitsTest.java @@ -0,0 +1,142 @@ +/******************************************************************************* + * Copyright (c) 2024 Red Hat, Inc. + * Distributed under license by Red Hat, Inc. All rights reserved. + * This program is made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + ******************************************************************************/ +package com.redhat.devtools.intellij.telemetry.core.configuration.limits; + +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.file.attribute.FileTime; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.List; + +import static com.redhat.devtools.intellij.telemetry.core.configuration.limits.MessageLimits.PluginLimitsFactory; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +public class MessageLimitsTest { + + @Test + public void get_should_return_empty_list_of_limits_if_deserialization_throws() throws IOException { + // given + PluginLimitsFactory factory = mock(PluginLimitsFactory.class); + doThrow(new IOException()) + .when(factory).create(any()); + Configurations configurations = mock(Configurations.class); + doReturn("bogus") // needs to return non-null for factory to be invoked + .when(configurations).getRemote(); + MessageLimits limits = new MessageLimits(factory, configurations); + // when + List pluginLimits = limits.get(); + // then + assertThat(pluginLimits).isEmpty(); + } + + @Test + public void get_should_download_remote_if_local_file_has_no_modification_timestamp() { + // given + Configurations configurations = mock(Configurations.class); + doReturn(null) // no modification timestamp, file does not exist + .when(configurations).getLocalLastModified(); + MessageLimits limits = new MessageLimits(mock(PluginLimitsFactory.class), configurations); + // when + limits.get(); + // then + verify(configurations).getRemote(); + } + + @Test + public void get_should_download_remote_if_local_file_was_modified_7h_ago_and_no_default_limits_exist() { + // given + List noDefaultLimits = List.of(mock(PluginLimits.class)); + + PluginLimitsFactory factory = mock(PluginLimitsFactory.class); + + Configurations configurations = mock(Configurations.class); + // default refresh (without existing plugin limits) is 6h + doReturn(createFileTime(7)) // 7h ago + .when(configurations).getLocalLastModified(); + MessageLimits limits = new MessageLimits(noDefaultLimits, factory, configurations); + // when + limits.get(); + // then + verify(configurations).getRemote(); + } + + @Test + public void get_should_download_remote_if_local_file_was_modified_7h_ago_and_default_limits_has_no_refresh() { + // given + List noDefaultLimits = List.of(mockDefaultPluginLimitsWithRefresh(-1)); // no refresh specified + + PluginLimitsFactory factory = mock(PluginLimitsFactory.class); + + Configurations configurations = mock(Configurations.class); + // default refresh (with plugin limits without refresh) is 6h + doReturn(createFileTime(7)) // 7h ago + .when(configurations).getLocalLastModified(); + MessageLimits limits = new MessageLimits(noDefaultLimits, factory, configurations); + // when + limits.get(); + // then + verify(configurations).getRemote(); + } + + @Test + public void get_should_NOT_download_remote_if_local_file_was_modified_within_specified_refresh_period() { + // given + List pluginLimits = List.of(mockDefaultPluginLimitsWithRefresh(2)); // refresh after 2h + + PluginLimitsFactory factory = mock(PluginLimitsFactory.class); + + Configurations configurations = mock(Configurations.class); + doReturn(createFileTime(1)) // 1h ago + .when(configurations).getLocalLastModified(); + MessageLimits limits = new MessageLimits(pluginLimits, factory, configurations); + // when + limits.get(); + // then + verify(configurations, never()).getRemote(); + } + + @Test + public void get_should_return_empty_list_of_limits_if_downloadRemote_returns_null() throws IOException { + // given + PluginLimitsFactory factory = mock(PluginLimitsFactory.class); + Configurations configurations = mock(Configurations.class); + doReturn(null) + .when(configurations).getRemote(); + MessageLimits limits = new MessageLimits(factory, configurations); + // when + List pluginLimits = limits.get(); + // then + assertThat(pluginLimits).isEmpty(); + } + + private FileTime createFileTime(int createdHoursAgo) { + return FileTime.from( + Instant.now().minus(createdHoursAgo, ChronoUnit.HOURS)); + } + + private static PluginLimits mockDefaultPluginLimitsWithRefresh(int refresh) { + PluginLimits pluginLimit = mock(PluginLimits.class); + doReturn(refresh) + .when(pluginLimit).getRefresh(); + doReturn(true) + .when(pluginLimit).isDefault(); + return pluginLimit; + } + +} diff --git a/src/test/java/com/redhat/devtools/intellij/telemetry/core/configuration/limits/Mocks.java b/src/test/java/com/redhat/devtools/intellij/telemetry/core/configuration/limits/Mocks.java new file mode 100644 index 00000000..7a2e6bb4 --- /dev/null +++ b/src/test/java/com/redhat/devtools/intellij/telemetry/core/configuration/limits/Mocks.java @@ -0,0 +1,67 @@ +/******************************************************************************* + * Copyright (c) 2024 Red Hat, Inc. + * Distributed under license by Red Hat, Inc. All rights reserved. + * This program is made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + ******************************************************************************/ +package com.redhat.devtools.intellij.telemetry.core.configuration.limits; + +import com.redhat.devtools.intellij.telemetry.core.configuration.limits.Filter; +import com.redhat.devtools.intellij.telemetry.core.service.Event; +import com.redhat.devtools.intellij.telemetry.core.service.UserId; + +import java.util.HashMap; +import java.util.Map; + +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; + +public class Mocks { + + public static Event event(Map properties) { + return new Event(null, null, properties); + } + + public static Event event() { + return event(new HashMap<>()); + } + + public static UserId userId(float percentile) { + UserId userId = mock(UserId.class); + doReturn(percentile) + .when(userId).getPercentile(); + return userId; + } + + public static Filter.EventNameFilter eventNameFilterFake(final boolean isMatching, boolean isIncludedRatio, boolean isExcludedRatio) { + return new Filter.EventNameFilter( null, -1f, null) { + @Override + public boolean isMatching(Event event) { + return isMatching; + } + + @Override + public boolean isExcludedByRatio(float percentile) { + return isExcludedRatio; + } + + @Override + public boolean isIncludedByRatio(float percentile) { + return isIncludedRatio; + } + }; + } + + public static Filter.EventNameFilter eventNameFilterFakeMatchingWithRatio(float ratio) { + return new Filter.EventNameFilter( null, ratio, null) { + @Override + public boolean isMatching(Event event) { + return true; + } + }; + } +} diff --git a/src/test/java/com/redhat/devtools/intellij/telemetry/core/configuration/limits/PluginLimitRatioTest.java b/src/test/java/com/redhat/devtools/intellij/telemetry/core/configuration/limits/PluginLimitRatioTest.java new file mode 100644 index 00000000..ebe7538a --- /dev/null +++ b/src/test/java/com/redhat/devtools/intellij/telemetry/core/configuration/limits/PluginLimitRatioTest.java @@ -0,0 +1,116 @@ +/******************************************************************************* + * Copyright (c) 2024 Red Hat, Inc. + * Distributed under license by Red Hat, Inc. All rights reserved. + * This program is made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + ******************************************************************************/ +package com.redhat.devtools.intellij.telemetry.core.configuration.limits; + +import com.redhat.devtools.intellij.telemetry.core.service.Event; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Stream; + +import static com.redhat.devtools.intellij.telemetry.core.configuration.limits.Mocks.event; +import static com.redhat.devtools.intellij.telemetry.core.configuration.limits.Mocks.eventNameFilterFakeMatchingWithRatio; +import static com.redhat.devtools.intellij.telemetry.core.configuration.limits.Mocks.userId; +import static org.assertj.core.api.Assertions.assertThat; + +public class PluginLimitRatioTest { + + @ParameterizedTest + @MethodSource("canSend_for_include_ratio_and_percentile") + public void canSend_for_given_ratio_in_limits_and_user_percentile(float limitsRatio, float percentile, boolean shouldSend) { + // given + PluginLimits limits = new PluginLimits( + "jedis", + Enabled.ALL, // ignore + -1, // ignore + limitsRatio, + Collections.emptyList(), + Collections.emptyList(), + userId(percentile)); + Event event = event(); + // when + boolean canSend = limits.canSend(event); + // then + assertThat(canSend).isEqualTo(shouldSend); + } + + @ParameterizedTest + @MethodSource("canSend_for_include_ratio_and_percentile") + public void canSend_for_given_ratio_in_include_filter_and_user_percentile(float filterRatio, float percentile, boolean shouldSend) { + // given + PluginLimits limits = new PluginLimits( + "jedis", + Enabled.ALL, // ignore + -1, // ignore + 1f, // ratio 100% + List.of( + eventNameFilterFakeMatchingWithRatio(filterRatio) + ), + Collections.emptyList(), + userId(percentile)); + Event event = event(); + // when + boolean canSend = limits.canSend(event); + // then + assertThat(canSend).isEqualTo(shouldSend); + } + + private static Stream canSend_for_include_ratio_and_percentile() { + return Stream.of( + Arguments.of(0f, 0f, false), // ratio: 0, percentile: .2 -> false + Arguments.of(0f, .2f, false), // ratio: 0, percentile: .2 -> false + Arguments.of(.1f, .1f, true), // ratio: .1, percentile: .1 -> true + Arguments.of(.5f, .4f, true), // ratio: .5, percentile: .4 -> true + Arguments.of(.5f, .5f, true), // ratio: .5, percentile: .5 -> true + Arguments.of(.5f, .6f, false), // ratio: .5, percentile: .6 -> false + Arguments.of(1f, .6f, true), // ratio: 1, percentile: .6 -> true + Arguments.of(1f, 1f, true) // ratio: 1, percentile: 1 -> true + ); + } + + @ParameterizedTest + @MethodSource("canSend_for_exclude_ratio_and_percentile") + public void canSend_for_given_ratio_in_exclude_filter_and_user_percentile(float filterRatio, float percentile, boolean shouldSend) { + // given + PluginLimits limits = new PluginLimits( + "jedis", + Enabled.ALL, // ignore + -1, // ignore + 1f, // ratio 100% + Collections.emptyList(), + List.of( + eventNameFilterFakeMatchingWithRatio(filterRatio) + ), + userId(percentile)); + Event event = event(); + // when + boolean canSend = limits.canSend(event); + // then + assertThat(canSend).isEqualTo(shouldSend); + } + + private static Stream canSend_for_exclude_ratio_and_percentile() { + return Stream.of( + Arguments.of(0f, 0f, true), // exclude ratio: 0, percentile: .2 -> true + Arguments.of(0f, .2f, true), // exclude ratio: 0, percentile: .2 -> true + Arguments.of(.1f, .1f, true), // exclude ratio: .1, percentile: .1 -> true + Arguments.of(.5f, .4f, true), // exclude ratio: .5, percentile: .4 -> true + Arguments.of(.5f, .5f, true), // exclude ratio: .5, percentile: .5 -> true + Arguments.of(.5f, .6f, false), // exclude ratio: .5, percentile: .6 -> false + Arguments.of(1f, .6f, false), // exclude ratio: 1, percentile: .6 -> false + Arguments.of(1f, 1f, false) // exclude ratio: 1, percentile: 1 -> false + ); + } + +} diff --git a/src/test/java/com/redhat/devtools/intellij/telemetry/core/configuration/limits/PluginLimitTest.java b/src/test/java/com/redhat/devtools/intellij/telemetry/core/configuration/limits/PluginLimitTest.java new file mode 100644 index 00000000..c591b15e --- /dev/null +++ b/src/test/java/com/redhat/devtools/intellij/telemetry/core/configuration/limits/PluginLimitTest.java @@ -0,0 +1,366 @@ +/******************************************************************************* + * Copyright (c) 2024 Red Hat, Inc. + * Distributed under license by Red Hat, Inc. All rights reserved. + * This program is made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + ******************************************************************************/ +package com.redhat.devtools.intellij.telemetry.core.configuration.limits; + +import com.redhat.devtools.intellij.telemetry.core.service.Event; +import com.redhat.devtools.intellij.telemetry.core.service.UserId; +import org.junit.jupiter.api.Test; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static com.redhat.devtools.intellij.telemetry.core.configuration.limits.Mocks.event; +import static com.redhat.devtools.intellij.telemetry.core.configuration.limits.Mocks.eventNameFilterFake; +import static com.redhat.devtools.intellij.telemetry.core.configuration.limits.Mocks.eventNameFilterFakeMatchingWithRatio; +import static com.redhat.devtools.intellij.telemetry.core.configuration.limits.Mocks.userId; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +public class PluginLimitTest { + + @Test + public void canSend_should_return_false_if_enabled_is_OFF() { + // given + PluginLimits limits = new PluginLimits( + "yoda", + Enabled.OFF, + -1, // ignore + 1f, // ignore + Collections.emptyList(), + Collections.emptyList(), + null); // no ratio check + // when + boolean canSend = limits.canSend(null); + // then + assertThat(canSend).isFalse(); + } + + @Test + public void canSend_should_return_false_if_enabled_is_null() { + // given + PluginLimits limits = new PluginLimits( + "yoda", + null, // null enabled + -1, // ignore + 1f, // ignore + Collections.emptyList(), + Collections.emptyList(), + null); // no ratio check; + // when + boolean canSend = limits.canSend(null); + // then + assertThat(canSend).isFalse(); + } + + @Test + public void canSend_should_return_true_if_enabled_is_CRASH_and_event_has_error() { + // given + PluginLimits limits = new PluginLimits( + "yoda", + Enabled.CRASH, // only crash + -1, // ignore + 1f, // ignore + Collections.emptyList(), + Collections.emptyList(), + null); // no ratio check + Event error = event( + Map.of("error", "anakin turned to the dark side")); // error + // when + boolean canSend = limits.canSend(error); + // then + assertThat(canSend).isTrue(); + } + + @Test + public void canSend_should_return_false_if_enabled_is_CRASH_and_event_has_no_error() { + // given + PluginLimits limits = new PluginLimits( + "yoda", + Enabled.CRASH, // only crash + -1, // ignore + 1f, // ignore + Collections.emptyList(), + Collections.emptyList(), + null); // no ratio check + Event error = event(); // no error + // when + boolean canSend = limits.canSend(error); + // then + assertThat(canSend).isFalse(); + } + + @Test + public void canSend_should_return_true_if_enabled_is_ERROR_and_event_has_error() { + // given + PluginLimits limits = new PluginLimits( + "yoda", + Enabled.ERROR, // only errors + -1, // ignore + 1f, // ignore + Collections.emptyList(), + Collections.emptyList(), + null); // no ratio check + Event error = event( + Map.of("error", "anakin turned to the dark side")); // error + // when + boolean canSend = limits.canSend(error); + // then + assertThat(canSend).isTrue(); + } + + @Test + public void canSend_should_return_false_if_enabled_is_ERROR_and_event_has_no_error() { + // given + PluginLimits limits = new PluginLimits( + "yoda", + Enabled.ERROR, // only errors + -1, // ignore + 1f, // ignore + Collections.emptyList(), + Collections.emptyList(), + null); // no ratio check + Event event = event(); // no error + // when + boolean canSend = limits.canSend(event); + // then + assertThat(canSend).isFalse(); + } + + @Test + public void canSend_should_return_true_if_event_is_matched_by_inclusion_filter_with_ratio() { + // given + UserId userId = userId(1f); + PluginLimits limits = new PluginLimits( + "yoda is included", + Enabled.ALL, // all enabled + -1, // ignore + 1f, // ignore + List.of( + // matching & ratio + eventNameFilterFake(true,true, false) + ), + Collections.emptyList(), + userId); + Event event = event(); // no error + // when + boolean canSend = limits.canSend(event); + // then + assertThat(canSend).isTrue(); + } + + @Test + public void canSend_should_return_false_if_event_is_matched_by_exclusion_filter_with_ratio() { + // given + UserId userId = userId(1f); + PluginLimits limits = new PluginLimits( + "yoda is excluded", + Enabled.ALL, // all enabled + -1, // ignore + 1f, // ignore + Collections.emptyList(), + List.of( + eventNameFilterFake(true,false, true) + ), + userId); + Event event = event(); + // when + boolean canSend = limits.canSend(event); + // then + assertThat(canSend).isFalse(); + } + + @Test + public void canSend_should_return_false_if_event_is_matched_by_inclusion_and_exclusion_filter() { + // given + UserId userId = userId(1f); + PluginLimits limits = new PluginLimits( + "yoda cannot send", + Enabled.ALL, // all enabled + -1, // ignore + 1f, // ignore + List.of( + eventNameFilterFake(true,true, false) + ), + List.of( + eventNameFilterFake(true,false, true) + ), + userId); + Event event = event(); + // when + boolean canSend = limits.canSend(event); + // then + assertThat(canSend).isFalse(); + } + + @Test + public void isIncluded_should_return_true_if_there_is_no_include_filter() { + // given + PluginLimits limits = new PluginLimits( + null, // ignore + Enabled.ALL, // ignore + -1, // ignore + 1f, // ignore + Collections.emptyList(), + Collections.emptyList(), + mock(UserId.class)); + Event event = event(); + // when + boolean isIncluded = limits.isIncluded(event); + // then + assertThat(isIncluded).isTrue(); + } + + @Test + public void isIncluded_should_return_true_if_there_is_no_matching_include_filter() { + // given + PluginLimits limits = new PluginLimits( + null, // ignore + Enabled.ALL, // ignore + -1, // ignore + 1f, // ignore + List.of( + // is NOT matching + eventNameFilterFake(false,true, false) + ), + Collections.emptyList(), + mock(UserId.class)); + Event event = event(); + // when + boolean isIncluded = limits.isIncluded(event); + // then + assertThat(isIncluded).isTrue(); + } + + @Test + public void isIncluded_should_return_true_if_event_is_matching_filter_and_is_included() { + // given + PluginLimits limits = new PluginLimits( + null, // ignore + Enabled.ALL, // ignore + -1, // ignore + 1f, // ignore + List.of( + // is matching & is excluded in ratio + eventNameFilterFake(true,true, false) + ), + Collections.emptyList(), + mock(UserId.class)); + Event event = event(); + // when + boolean isIncluded = limits.isIncluded(event); + // then + assertThat(isIncluded).isTrue(); + } + + @Test + public void isIncluded_should_return_false_if_event_is_matching_filter_but_isnt_included() { + // given + PluginLimits limits = new PluginLimits( + null, // ignore + Enabled.ALL, // ignore + -1, // ignore + 1f, // ignore + List.of( + // is matching & is NOT included in ratio + eventNameFilterFake(true,false, false) + ), + Collections.emptyList(), + mock(UserId.class)); + Event event = event(); + // when + boolean isIncluded = limits.isIncluded(event); + // then + assertThat(isIncluded).isFalse(); + } + + @Test + public void isExcluded_should_return_false_if_there_is_no_filter() { + // given + PluginLimits limits = new PluginLimits( + null, // ignore + Enabled.ALL, // ignore + -1, // ignore + 1f, // ignore + Collections.emptyList(), + Collections.emptyList(), + mock(UserId.class)); + Event event = event(); + // when + boolean isExcluded = limits.isExcluded(event); + // then + assertThat(isExcluded).isFalse(); + } + + @Test + public void isExcluded_should_return_false_if_there_is_no_matching_filter() { + // given + PluginLimits limits = new PluginLimits( + null, // ignore + Enabled.ALL, // ignore + -1, // ignore + 1f, // ignore + List.of( + // is NOT matching + eventNameFilterFake(false,true, true) + ), + Collections.emptyList(), + mock(UserId.class)); + Event event = event(); + // when + boolean isExcluded = limits.isExcluded(event); + // then + assertThat(isExcluded).isFalse(); + } + + @Test + public void isExcluded_should_return_true_if_event_is_matching_filter_and_is_excluded_in_ratio() { + // given + PluginLimits limits = new PluginLimits( + null, // ignore + Enabled.ALL, // ignore + -1, // ignore + 1f, // ignore + Collections.emptyList(), + List.of( + // is matching & is excluded in ratio + eventNameFilterFake(true,true, true) + ), + mock(UserId.class)); + Event event = event(); + // when + boolean isExcluded = limits.isExcluded(event); + // then + assertThat(isExcluded).isTrue(); + } + + @Test + public void isExcluded_should_return_false_if_event_is_matching_filter_and_is_NOT_excluded_in_ratio() { + // given + PluginLimits limits = new PluginLimits( + null, // ignore + Enabled.ALL, // ignore + -1, // ignore + 1f, // ignore + Collections.emptyList(), + List.of( + // is matching & is NOT excluded in ratio + eventNameFilterFake(true,true, false) + ), + mock(UserId.class)); + Event event = event(); + // when + boolean isExcluded = limits.isExcluded(event); + // then + assertThat(isExcluded).isFalse(); + } + +} diff --git a/src/test/java/com/redhat/devtools/intellij/telemetry/core/configuration/limits/PluginLimitsDeserializationTest.java b/src/test/java/com/redhat/devtools/intellij/telemetry/core/configuration/limits/PluginLimitsDeserializationTest.java new file mode 100644 index 00000000..5438e8b8 --- /dev/null +++ b/src/test/java/com/redhat/devtools/intellij/telemetry/core/configuration/limits/PluginLimitsDeserializationTest.java @@ -0,0 +1,342 @@ +/******************************************************************************* + * Copyright (c) 2024 Red Hat, Inc. + * Distributed under license by Red Hat, Inc. All rights reserved. + * This program is made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + ******************************************************************************/ +package com.redhat.devtools.intellij.telemetry.core.configuration.limits; + +import com.fasterxml.jackson.core.JsonProcessingException; +import org.assertj.core.api.Condition; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class PluginLimitsDeserializationTest { + + @Test + public void get_should_return_3_limits_if_3_plugins_are_specified() throws JsonProcessingException { + // given + String config = + "{\n" + + " \"*\": {}," + + " \"yoda\": {},"+ + " \"obiwan\": {} "+ + "}"; + // when + List limits = PluginLimitsDeserialization.create(config); + // then + assertThat(limits).hasSize(3); + } + + @Test + public void getEnabled_should_return_ALL_if_no_value_present() throws JsonProcessingException { + // given + String config = + "{\n" + + " \"*\": {" + + " }" + + "}"; + List limits = PluginLimitsDeserialization.create(config); + PluginLimits limit = limits.get(0); + // when + Enabled enabled = limit.getEnabled(); + // then + assertThat(enabled).isEqualTo(Enabled.ALL); + } + + @Test + public void getEnabled_should_return_ALL_if_unknown_value_present() throws JsonProcessingException { + // given + String config = + "{\n" + + " \"*\": {" + + " \"enabled\" : \"bogus\"" + + " }" + + "}"; + List limits = PluginLimitsDeserialization.create(config); + PluginLimits limit = limits.get(0); + // when + Enabled enabled = limit.getEnabled(); + // then + assertThat(enabled).isEqualTo(Enabled.ALL); + } + + @Test + public void getEnabled_should_return_ERROR_if_error_is_specified() throws JsonProcessingException { + // given + String config = + "{\n" + + " \"*\": {" + + " \"enabled\" : \"error\"" + + " }" + + "}"; + List limits = PluginLimitsDeserialization.create(config); + PluginLimits limit = limits.get(0); + // when + Enabled enabled = limit.getEnabled(); + // then + assertThat(enabled).isEqualTo(Enabled.ERROR); + } + + @Test + public void getRefresh_should_return_negative_refresh_if_no_refresh_is_specified() throws JsonProcessingException { + // given + String config = + "{\n" + + " \"*\": {}" + + "}"; + List limits = PluginLimitsDeserialization.create(config); + PluginLimits limit = limits.get(0); + // when + int refresh = limit.getRefresh(); + // then + assertThat(refresh).isNegative(); + } + + @Test + public void getRefresh_should_return_negative_refresh_if_non_numeric_refresh_is_specified() throws JsonProcessingException { + // given + String config = + "{\n" + + " \"*\": {\n" + + " \"refresh\": \"bogus\"\n" + + " }\n" + + "}"; + List limits = PluginLimitsDeserialization.create(config); + PluginLimits limit = limits.get(0); + // when + int refresh = limit.getRefresh(); + // then + assertThat(refresh).isNegative(); + } + + @Test + public void getRefresh_should_return_numeric_portion_of_value_specified() throws JsonProcessingException { + // given + String config = + "{\n" + + " \"*\": {\n" + + " \"refresh\": \"12h\"\n" + + " }\n" + + "}"; + List limits = PluginLimitsDeserialization.create(config); + PluginLimits limit = limits.get(0); + // when + int refresh = limit.getRefresh(); + // then + assertThat(refresh).isEqualTo(12); + } + + @Test + public void getRefresh_should_return_numeric_value_specified_as_refresh_value() throws JsonProcessingException { + // given + String config = + "{\n" + + " \"*\": {\n" + + " \"refresh\": \"42\"\n" + + " }\n" + + "}"; + List limits = PluginLimitsDeserialization.create(config); + PluginLimits limit = limits.get(0); + // when + int refresh = limit.getRefresh(); + // then + assertThat(refresh).isEqualTo(42); + } + + @Test + public void getRatio_should_return_float_value_specified_as_ratio_value() throws JsonProcessingException { + // given + String config = + "{\n" + + " \"*\": {\n" + + " \"ratio\": \"0.42\"\n" + + " }\n" + + "}"; + List limits = PluginLimitsDeserialization.create(config); + PluginLimits limit = limits.get(0); + // when + float ratio = limit.getRatio(); + // then + assertThat(ratio).isEqualTo(0.42f); + } + + @Test + public void getRatio_should_return_1_if_no_ratio_is_specified() throws JsonProcessingException { + // given + String config = + "{\n" + + " \"*\": {\n" + + " }\n" + + "}"; + List limits = PluginLimitsDeserialization.create(config); + PluginLimits limit = limits.get(0); + // when + float ratio = limit.getRatio(); + // then + assertThat(ratio).isEqualTo(1f); + } + + @Test + public void getIncludes_should_return_no_filters_if_no_includes_are_specified() throws JsonProcessingException { + // given + String config = + "{\n" + + " \"*\": {}" + + "}"; + List limits = PluginLimitsDeserialization.create(config); + PluginLimits limit = limits.get(0); + // when + List includes = limit.getIncludes(); + // then + assertThat(includes).isEmpty(); + } + + @Test + public void getIncludes_should_return_no_filters_if_bogus_includes_are_specified() throws JsonProcessingException { + // given + String config = + "{\n" + + " \"*\": {\n" + + " \"includes\": [\"bogus\"]" + + " }" + + "}"; + List limits = PluginLimitsDeserialization.create(config); + PluginLimits limit = limits.get(0); + // when + List includes = limit.getIncludes(); + // then + assertThat(includes).isEmpty(); + } + + @Test + public void getIncludes_should_return_3_filters() throws JsonProcessingException { + // given + String config = + "{\n" + + " \"*\": {},\n" + + " \"jedis\": {\n" + + " \"includes\": [\n" + + " {\n" + + " \"name\" : \"yoda\"\n" + + " },\n" + + " {\n" + + " \"name\" : \"obiwan\"\n" + + " },\n" + + " {\n" + + " \"name\" : \"*\"\n" + + " }\n" + + " ]\n" + + " }" + + "}"; + List limits = PluginLimitsDeserialization.create(config); + PluginLimits limit = limits.get(1); // jedis + // when + List includes = limit.getIncludes(); + // then + assertThat(includes).hasSize(3); + } + + @Test + public void getIncludes_should_return_1_event_name_filter() throws JsonProcessingException { + // given + String config = + "{\n" + + " \"*\": {\n" + + " \"includes\": [\n" + + " {\n" + + " \"name\" : \"yoda\"\n" + + " }\n" + + " ]\n" + + " }" + + "}"; + List limits = PluginLimitsDeserialization.create(config); + PluginLimits limit = limits.get(0); // * + // when + List includes = limit.getIncludes(); + // then + assertThat(includes).are( + new Condition<>((filter) -> filter instanceof Filter.EventNameFilter, "is EventNameFilter")); + } + + @Test + public void getIncludes_should_return_1_event_name_filter_with_ratio_and_daily_limit() throws JsonProcessingException { + // given + String config = + "{\n" + + " \"*\": {\n" + + " \"includes\": [\n" + + " {\n" + + " \"name\" : \"yoda\",\n" + + " \"ratio\" : \"0.544\",\n" + + " \"dailyLimit\" : \"42\"\n" + + " }\n" + + " ]\n" + + " }" + + "}"; + List limits = PluginLimitsDeserialization.create(config); + PluginLimits limit = limits.get(0); // * + List includes = limit.getIncludes(); + assertThat(includes).hasSize(1); + Filter filter = includes.get(0); + assertThat(filter).isExactlyInstanceOf(Filter.EventNameFilter.class); + Filter.EventNameFilter nameFilter = (Filter.EventNameFilter) filter; + // when + float ratio = nameFilter.getRatio(); + String dailyLimit = nameFilter.getDailyLimit(); + // then + assertThat(ratio).isEqualTo(0.544f); + assertThat(dailyLimit).isEqualTo("42"); + } + + @Test + public void getIncludes_should_return_1_event_property_filter() throws JsonProcessingException { + // given + String config = + "{\n" + + " \"*\": {\n" + + " \"includes\": [\n" + + " {\n" + + " \"property\" : \"yoda\",\n" + + " \"value\" : \"jedi\"\n" + + " }\n" + + " ]\n" + + " }" + + "}"; + List limits = PluginLimitsDeserialization.create(config); + PluginLimits limit = limits.get(0); // * + // when + List includes = limit.getIncludes(); + // then + assertThat(includes).are(new Condition<>( + (filter) -> filter instanceof Filter.EventPropertyFilter, "is EventPropertyFilter")); + } + + @Test + public void getIncludes_should_NOT_have_property_value_filter_if_value_is_missing() throws JsonProcessingException { + // given + String config = + "{\n" + + " \"*\": {\n" + + " \"includes\": [\n" + + " {\n" + + " \"property\" : \"yoda\"\n" + + " }\n" + + " ]\n" + + " }" + + "}"; + List limits = PluginLimitsDeserialization.create(config); + PluginLimits limit = limits.get(0); // * + // when + List includes = limit.getIncludes(); + // then + assertThat(includes).isEmpty(); + } +} diff --git a/src/test/java/com/redhat/devtools/intellij/telemetry/core/service/TelemetryMessageBuilderTest.java b/src/test/java/com/redhat/devtools/intellij/telemetry/core/service/TelemetryMessageBuilderTest.java index bd93d529..805cfe25 100644 --- a/src/test/java/com/redhat/devtools/intellij/telemetry/core/service/TelemetryMessageBuilderTest.java +++ b/src/test/java/com/redhat/devtools/intellij/telemetry/core/service/TelemetryMessageBuilderTest.java @@ -16,7 +16,6 @@ import com.redhat.devtools.intellij.telemetry.core.service.TelemetryMessageBuilder.FeedbackServiceFacade; import com.redhat.devtools.intellij.telemetry.core.util.AnonymizeUtils; import com.redhat.devtools.intellij.telemetry.core.util.TimeUtils; -import org.assertj.core.api.Condition; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; @@ -25,8 +24,6 @@ import java.time.LocalDateTime; import java.time.LocalTime; import java.time.temporal.ChronoUnit; -import java.util.Map; -import java.util.function.Predicate; import static com.redhat.devtools.intellij.telemetry.core.service.Event.Type.ACTION; import static com.redhat.devtools.intellij.telemetry.core.service.Event.Type.STARTUP; diff --git a/src/test/java/com/redhat/devtools/intellij/telemetry/core/service/UserIdTest.java b/src/test/java/com/redhat/devtools/intellij/telemetry/core/service/UserIdTest.java index 38a28a9e..64da21b7 100644 --- a/src/test/java/com/redhat/devtools/intellij/telemetry/core/service/UserIdTest.java +++ b/src/test/java/com/redhat/devtools/intellij/telemetry/core/service/UserIdTest.java @@ -11,8 +11,12 @@ package com.redhat.devtools.intellij.telemetry.core.service; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import java.nio.file.Path; +import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; @@ -62,6 +66,78 @@ void get_should_write_if_file_exists_but_is_invalid() { assertThat(user.written).isTrue(); } + @ParameterizedTest + @MethodSource("percentile_for_hashCode") + public void getPercentile_should_return_value_for_hashCode(int hashCode, float expectedPercentile) { + // given + UserId userId = new FixedHashCodeUserId(hashCode); + // when + float percentile = userId.getPercentile(); + // then + assertThat(percentile).isEqualTo(expectedPercentile); + } + + @ParameterizedTest + @MethodSource("hashCode_for_uuid") + public void hashCode_should_return_value_for_uuid(String uuid, int expectedHashCode) { + // given + UserId userId = new TestableUserId(true, uuid); + // when + int hashCode = userId.hashCode(); + // then + assertThat(hashCode).isEqualTo(expectedHashCode); + } + + public static Stream hashCode_for_uuid() { + return Stream.of( + Arguments.of("6c4698ed-85f3-4448-9b0f-10897b8b4178", 349419899), // uuid -> hashCode + Arguments.of("870c8e59-9299-437f-a4dd-5bd331352ec7", -2018427608), + Arguments.of("c020f453-6811-4545-a3aa-3c5cc17d6fe8", -252979871), + Arguments.of("db3f9e5e-2dd5-4d81-aac8-aa75333c105c", 1140739481), + Arguments.of("8abd3beb-c930-46a0-b244-7f1c6f9857da", 82715988), + Arguments.of("d839a99f-6afc-4309-bcb7-5d1e78eb0241", 1829289193), + Arguments.of("08f87a61-077a-4cb3-b9f9-4e5751d4dc96", 1602451551), + Arguments.of("72f09a0e-1fa6-46d1-8322-48ac0ffa4252", -633581890), + Arguments.of("c1d68afc-a39e-4b89-bb95-e9d7684efe7c", -1103007680), + Arguments.of("a52ec11d-35bf-4579-88fd-72de5c6a0467", 158094785), + Arguments.of("d136d7b4-518a-43b6-bb1d-1dafd9e1e52b", 2110423401), + Arguments.of("ddf95114-333d-41e0-b1ba-d84bc6293634", 1889783579), + Arguments.of("fb833841-75de-435e-98d2-ab0988712340", 1464118621), + Arguments.of("71f327fa-e8ed-4fdc-92d9-5de6a1f47229", -367676488), + Arguments.of("82b4c9f4-73e3-4e4a-b243-dc4aff91b9f6", 224204832), + Arguments.of("16d122ec-9122-4392-a90f-71504ef40c6f", 1020229945), + Arguments.of("570447c7-168e-4d3d-be40-e5559dd4f86b", -690069930), + Arguments.of("cc4ee6ef-6862-4468-ac51-a64f237f84f5", -1247454805), + Arguments.of("b2ee8320-4dff-44a1-87e2-ca9daa9e24ed", -1381801037), + Arguments.of("4e97382d-6042-4001-889d-ecc0cb4e8862", -1911346601) + ); + } + + public static Stream percentile_for_hashCode() { + return Stream.of( + Arguments.of(349419899, 0.9899f), // hashCode -> percentile + Arguments.of(-2018427608, 0.7608f), + Arguments.of(-252979871, 0.9871f), + Arguments.of(1140739481, 0.9481f), + Arguments.of(82715988, 0.5988f), + Arguments.of(1829289193, 0.9193f), + Arguments.of(1602451551, 0.1551f), + Arguments.of(-633581890, 0.189f), + Arguments.of(-1103007680, 0.768f), + Arguments.of(158094785, 0.4785f), + Arguments.of(2110423401, 0.3401f), + Arguments.of(1889783579, 0.3579f), + Arguments.of(1464118621, 0.8621f), + Arguments.of(-367676488, 0.6488f), + Arguments.of(224204832, 0.4832f), + Arguments.of(1020229945, 0.9945f), + Arguments.of(-690069930, 0.993f), + Arguments.of(-1247454805, 0.4805f), + Arguments.of(-1381801037, 0.1037f), + Arguments.of(-1911346601, 0.6601f) + ); + } + private static class TestableUserId extends UserId { private final boolean exists; @@ -91,4 +167,17 @@ protected void write(String uuid, Path uuidFile) { } } + private static class FixedHashCodeUserId extends UserId { + + private final int hashCode; + + public FixedHashCodeUserId(int hashCode) { + this.hashCode = hashCode; + } + + @Override + public int hashCode() { + return hashCode; + } + } } diff --git a/src/test/java/com/redhat/devtools/intellij/telemetry/core/util/BasicGlobPatternTest.java b/src/test/java/com/redhat/devtools/intellij/telemetry/core/util/BasicGlobPatternTest.java new file mode 100644 index 00000000..24dac031 --- /dev/null +++ b/src/test/java/com/redhat/devtools/intellij/telemetry/core/util/BasicGlobPatternTest.java @@ -0,0 +1,140 @@ +/******************************************************************************* + * Copyright (c) 2024 Red Hat, Inc. + * Distributed under license by Red Hat, Inc. All rights reserved. + * This program is made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + ******************************************************************************/ +package com.redhat.devtools.intellij.telemetry.core.util; + +import org.junit.jupiter.api.Test; + +import java.util.regex.PatternSyntaxException; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class BasicGlobPatternTest { + + @Test + public void compile_should_throw_when_range_is_invalid() { + // given, when, then + assertThrows(PatternSyntaxException.class, () -> BasicGlobPattern.compile("[5-1] jedi")); + } + + @Test + public void compile_should_throw_when_brace_expansions_are_nested() { + // given, when, then + assertThrows(PatternSyntaxException.class, () -> BasicGlobPattern.compile("{{yoda,obiwan}")); + } + + @Test + public void compile_should_throw_when_brace_expansions_are_not_closed() { + // given, when, then + assertThrows(PatternSyntaxException.class, () -> BasicGlobPattern.compile("{yoda,obiwan")); + } + + @Test + public void machtes_should_match_expression_that_starts_and_ends_with_wildcard() { + // given + BasicGlobPattern glob = BasicGlobPattern.compile("*yoda*"); + // when, then + assertThat(glob.matches("master yoda is a jedi master")).isTrue(); + assertThat(glob.matches("yoda")).isTrue(); // * matches no character, too + assertThat(glob.matches("master yoda")).isTrue(); // * matches no character, too + assertThat(glob.matches("master obiwan is a jedi master, too")).isFalse(); + } + + @Test + public void machtes_should_match_expression_that_starts_with_wildcard() { + // given + BasicGlobPattern glob = BasicGlobPattern.compile("*yoda"); + // when, then + assertThat(glob.matches("master yoda")).isTrue(); + assertThat(glob.matches("yoda")).isTrue(); + assertThat(glob.matches("master obiwan")).isFalse(); + } + + @Test + public void machtes_should_match_expression_that_has_a_wildcard() { + // given + BasicGlobPattern glob = BasicGlobPattern.compile("y*da"); + // when, then + assertThat(glob.matches("yoda")).isTrue(); + assertThat(glob.matches("yooooda")).isTrue(); + } + + @Test + public void machtes_should_match_expression_that_starts_and_ends_with_placeholder() { + // given + BasicGlobPattern glob = BasicGlobPattern.compile("?this is yoda?"); + // when, then + assertThat(glob.matches("!this is yoda!")).isTrue(); + assertThat(glob.matches("!!this is yoda!")).isFalse(); + assertThat(glob.matches("this is yoda!")).isFalse(); + } + + @Test + public void machtes_should_match_expression_that_has_placeholders() { + // given + BasicGlobPattern glob = BasicGlobPattern.compile("y??a"); + // when, then + assertThat(glob.matches("yoda")).isTrue(); + assertThat(glob.matches("yiza")).isTrue(); + assertThat(glob.matches("yoooda")).isFalse(); + } + + @Test + public void machtes_should_match_expression_with_brace_expansions() { + // given + BasicGlobPattern glob = BasicGlobPattern.compile("{yoda,obiwan,skywalker} is a jedi"); + // when, then + assertThat(glob.matches("yoda is a jedi")).isTrue(); + assertThat(glob.matches("obiwan is a jedi")).isTrue(); + assertThat(glob.matches("skywalker is a jedi")).isTrue(); + assertThat(glob.matches("darthvader is a jedi")).isFalse(); + } + + @Test + public void machtes_should_match_empty_brace_expansion() { + // given + BasicGlobPattern glob = BasicGlobPattern.compile("{yoda,darth,} the jedi"); + // when, then + assertThat(glob.matches("yoda the jedi")).isTrue(); + assertThat(glob.matches(" the jedi")).isTrue(); // empty alternative + } + + @Test + public void machtes_should_match_expression_with_a_range() { + // given + BasicGlobPattern glob = BasicGlobPattern.compile("jedi [0-4]"); + // when, then + assertThat(glob.matches("jedi 0")).isTrue(); + assertThat(glob.matches("jedi 1")).isTrue(); + assertThat(glob.matches("jedi 4")).isTrue(); + assertThat(glob.matches("jedi 5")).isFalse(); + } + + @Test + public void machtes_should_match_expression_with_alternatives() { + // given + BasicGlobPattern glob = BasicGlobPattern.compile("jedi [abc]"); + // when, then + assertThat(glob.matches("jedi a")).isTrue(); + assertThat(glob.matches("jedi b")).isTrue(); + assertThat(glob.matches("jedi c")).isTrue(); + assertThat(glob.matches("jedi d")).isFalse(); + } + + @Test + public void machtes_should_match_parenthesis_and_pipe_as_normal_characters() { + // given + BasicGlobPattern glob = BasicGlobPattern.compile("jedi(s|42)"); + // when, then + assertThat(glob.matches("jedi(s|42)")).isTrue(); + assertThat(glob.matches("jedi(s)")).isFalse(); + } +}