From 0394756c84e447696193de69f2dcec56675f85c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Spie=C3=9F?= Date: Sat, 30 Dec 2023 14:07:38 +0100 Subject: [PATCH] Add ChannelCacheViewTest --- build.gradle.kts | 1 + .../middleman/AbstractGuildChannelImpl.java | 74 +---- .../jda/internal/utils/ChannelUtil.java | 88 ++++++ .../channel/ChannelCacheViewTest.java | 292 ++++++++++++++++++ 4 files changed, 383 insertions(+), 72 deletions(-) create mode 100644 src/test/java/net/dv8tion/jda/entities/channel/ChannelCacheViewTest.java diff --git a/build.gradle.kts b/build.gradle.kts index beabd400c6..f89f21d0ab 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -135,6 +135,7 @@ dependencies { testImplementation("org.junit.jupiter:junit-jupiter:5.8.2") testImplementation("org.reflections:reflections:0.10.2") + testImplementation("org.mockito:mockito-core:5.8.0") } val compileJava: JavaCompile by tasks diff --git a/src/main/java/net/dv8tion/jda/internal/entities/channel/middleman/AbstractGuildChannelImpl.java b/src/main/java/net/dv8tion/jda/internal/entities/channel/middleman/AbstractGuildChannelImpl.java index c24058af19..b8b648d9fa 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/channel/middleman/AbstractGuildChannelImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/channel/middleman/AbstractGuildChannelImpl.java @@ -16,15 +16,11 @@ package net.dv8tion.jda.internal.entities.channel.middleman; -import net.dv8tion.jda.api.entities.channel.attribute.ICategorizableChannel; -import net.dv8tion.jda.api.entities.channel.attribute.IPositionableChannel; -import net.dv8tion.jda.api.entities.channel.concrete.Category; -import net.dv8tion.jda.api.entities.channel.concrete.ThreadChannel; import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel; import net.dv8tion.jda.internal.entities.GuildImpl; import net.dv8tion.jda.internal.entities.channel.AbstractChannelImpl; import net.dv8tion.jda.internal.entities.channel.mixin.middleman.GuildChannelMixin; -import net.dv8tion.jda.internal.utils.Checks; +import net.dv8tion.jda.internal.utils.ChannelUtil; import javax.annotation.Nonnull; @@ -48,72 +44,6 @@ public GuildImpl getGuild() @Override public int compareTo(@Nonnull GuildChannel o) { - Checks.notNull(o, "Channel"); - - // Check thread positions - ThreadChannel thisThread = this instanceof ThreadChannel ? (ThreadChannel) this : null; - ThreadChannel otherThread = o instanceof ThreadChannel ? (ThreadChannel) o : null; - - if (thisThread != null && otherThread == null) - return thisThread.getParentChannel().compareTo(o); - if (thisThread == null && otherThread != null) - return this.compareTo(otherThread.getParentChannel()); - if (thisThread != null) - { - // If they are threads on the same channel - if (thisThread.getParentChannel().equals(otherThread.getParentChannel())) - return Long.compare(o.getIdLong(), id); // threads are ordered ascending by age - // If they are threads on different channels - return thisThread.getParentChannel().compareTo(otherThread.getParentChannel()); - } - - // Check category positions - Category thisParent = this instanceof ICategorizableChannel ? ((ICategorizableChannel) this).getParentCategory() : null; - Category otherParent = o instanceof ICategorizableChannel ? ((ICategorizableChannel) o).getParentCategory() : null; - - if (thisParent != null && otherParent == null) - { - if (o instanceof Category) - { - // The other channel is the parent category of this channel - if (o.equals(thisParent)) - return 1; - // The other channel is another category - return thisParent.compareTo(o); - } - return 1; - } - if (thisParent == null && otherParent != null) - { - if (this instanceof Category) - { - // This channel is parent of other channel - if (this.equals(otherParent)) - return -1; - // This channel is a category higher than the other channel's parent category - return this.compareTo(otherParent); //safe use of recursion since no circular parents exist - } - return -1; - } - // Both channels are in different categories, compare the categories instead - if (thisParent != null && !thisParent.equals(otherParent)) - return thisParent.compareTo(otherParent); - - // Check sort bucket (text/message is above audio) - if (getType().getSortBucket() != o.getType().getSortBucket()) - return Integer.compare(getType().getSortBucket(), o.getType().getSortBucket()); - - // Check actual position - if (o instanceof IPositionableChannel && this instanceof IPositionableChannel) - { - IPositionableChannel oPositionableChannel = (IPositionableChannel) o; - IPositionableChannel thisPositionableChannel = (IPositionableChannel) this; - - if (thisPositionableChannel.getPositionRaw() != oPositionableChannel.getPositionRaw()) - return Integer.compare(thisPositionableChannel.getPositionRaw(), oPositionableChannel.getPositionRaw()); - } - - // last resort by id - return Long.compareUnsigned(id, o.getIdLong()); + return ChannelUtil.compare(this, o); } } diff --git a/src/main/java/net/dv8tion/jda/internal/utils/ChannelUtil.java b/src/main/java/net/dv8tion/jda/internal/utils/ChannelUtil.java index 1f70447799..3af368d358 100644 --- a/src/main/java/net/dv8tion/jda/internal/utils/ChannelUtil.java +++ b/src/main/java/net/dv8tion/jda/internal/utils/ChannelUtil.java @@ -18,6 +18,11 @@ import net.dv8tion.jda.api.entities.channel.Channel; import net.dv8tion.jda.api.entities.channel.ChannelType; +import net.dv8tion.jda.api.entities.channel.attribute.ICategorizableChannel; +import net.dv8tion.jda.api.entities.channel.attribute.IPositionableChannel; +import net.dv8tion.jda.api.entities.channel.concrete.Category; +import net.dv8tion.jda.api.entities.channel.concrete.ThreadChannel; +import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel; import java.util.EnumSet; @@ -45,4 +50,87 @@ public static T safeChannelCast(Object instance, Class to String cleanedClassName = instance.getClass().getSimpleName().replace("Impl", ""); throw new IllegalStateException(Helpers.format("Cannot convert channel of type %s to %s!", cleanedClassName, toObjectClass.getSimpleName())); } + + public static int compare(GuildChannel a, GuildChannel b) + { + Checks.notNull(b, "Channel"); + + // Check thread positions + ThreadChannel thisThread = a instanceof ThreadChannel ? (ThreadChannel) a : null; + ThreadChannel otherThread = b instanceof ThreadChannel ? (ThreadChannel) b : null; + + if (thisThread != null && otherThread == null) + { + // Thread should be below its parent + if (thisThread.getParentChannel().getIdLong() == b.getIdLong()) + return 1; + // Otherwise compare parents + return thisThread.getParentChannel().compareTo(b); + } + if (thisThread == null && otherThread != null) + { + // Thread should be below its parent + if (otherThread.getParentChannel().getIdLong() == a.getIdLong()) + return -1; + // Otherwise compare parents + return a.compareTo(otherThread.getParentChannel()); + } + if (thisThread != null) + { + // If they are threads on the same channel + if (thisThread.getParentChannel().getIdLong() == otherThread.getParentChannel().getIdLong()) + return Long.compare(b.getIdLong(), a.getIdLong()); // threads are ordered ascending by age + // If they are threads on different channels + return thisThread.getParentChannel().compareTo(otherThread.getParentChannel()); + } + + // Check category positions + Category thisParent = a instanceof ICategorizableChannel ? ((ICategorizableChannel) a).getParentCategory() : null; + Category otherParent = b instanceof ICategorizableChannel ? ((ICategorizableChannel) b).getParentCategory() : null; + + if (thisParent != null && otherParent == null) + { + if (b instanceof Category) + { + // The other channel is the parent category of this channel + if (b.getIdLong() == thisParent.getIdLong()) + return 1; + // The other channel is another category + return thisParent.compareTo(b); + } + return 1; + } + if (thisParent == null && otherParent != null) + { + if (a instanceof Category) + { + // This channel is parent of other channel + if (a.getIdLong() == otherParent.getIdLong()) + return -1; + // This channel is a category higher than the other channel's parent category + return a.compareTo(otherParent); //safe use of recursion since no circular parents exist + } + return -1; + } + // Both channels are in different categories, compare the categories instead + if (thisParent != null && !thisParent.equals(otherParent)) + return thisParent.compareTo(otherParent); + + // Check sort bucket (text/message is above audio) + if (a.getType().getSortBucket() != b.getType().getSortBucket()) + return Integer.compare(a.getType().getSortBucket(), b.getType().getSortBucket()); + + // Check actual position + if (b instanceof IPositionableChannel && a instanceof IPositionableChannel) + { + IPositionableChannel oPositionableChannel = (IPositionableChannel) b; + IPositionableChannel thisPositionableChannel = (IPositionableChannel) a; + + if (thisPositionableChannel.getPositionRaw() != oPositionableChannel.getPositionRaw()) + return Integer.compare(thisPositionableChannel.getPositionRaw(), oPositionableChannel.getPositionRaw()); + } + + // last resort by id + return Long.compareUnsigned(a.getIdLong(), b.getIdLong()); + } } diff --git a/src/test/java/net/dv8tion/jda/entities/channel/ChannelCacheViewTest.java b/src/test/java/net/dv8tion/jda/entities/channel/ChannelCacheViewTest.java new file mode 100644 index 0000000000..70b4bd4eac --- /dev/null +++ b/src/test/java/net/dv8tion/jda/entities/channel/ChannelCacheViewTest.java @@ -0,0 +1,292 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dv8tion.jda.entities.channel; + +import net.dv8tion.jda.api.entities.channel.Channel; +import net.dv8tion.jda.api.entities.channel.ChannelType; +import net.dv8tion.jda.api.entities.channel.attribute.ICategorizableChannel; +import net.dv8tion.jda.api.entities.channel.attribute.IPositionableChannel; +import net.dv8tion.jda.api.entities.channel.attribute.IPostContainer; +import net.dv8tion.jda.api.entities.channel.concrete.Category; +import net.dv8tion.jda.api.entities.channel.concrete.ThreadChannel; +import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel; +import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel; +import net.dv8tion.jda.api.entities.channel.middleman.GuildMessageChannel; +import net.dv8tion.jda.api.entities.channel.unions.IThreadContainerUnion; +import net.dv8tion.jda.api.utils.cache.SortedChannelCacheView; +import net.dv8tion.jda.internal.utils.ChannelUtil; +import net.dv8tion.jda.internal.utils.cache.SortedChannelCacheViewImpl; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.NavigableSet; +import java.util.Objects; +import java.util.Set; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class ChannelCacheViewTest +{ + private static long counter = 0; + + private static final String VALID_SORT_ORDER = String.join("\n", + "TEXT without parent", + "NEWS without parent", + "TEXT parent of GUILD_PRIVATE_THREAD", + "GUILD_PRIVATE_THREAD", + "NEWS parent of GUILD_NEWS_THREAD", + "GUILD_NEWS_THREAD", + "FORUM parent of GUILD_PUBLIC_THREAD", + "GUILD_PUBLIC_THREAD", + "FORUM without parent", + "MEDIA without parent", + "VOICE without parent", + "STAGE without parent", + "CATEGORY parent of TEXT", + "TEXT with parent", + "CATEGORY parent of VOICE", + "VOICE with parent", + "CATEGORY without parent", + "CATEGORY parent of NEWS", + "NEWS with parent", + "CATEGORY parent of STAGE", + "STAGE with parent", + "CATEGORY parent of FORUM", + "FORUM with parent", + "CATEGORY parent of MEDIA", + "MEDIA with parent" + ); + + @SuppressWarnings("unchecked") + private static T mockChannel(ChannelType type, String name) + { + return (T) mockChannel(type.getInterface(), type, name); + } + + @SafeVarargs + private static T mockChannel(Class clazz, ChannelType type, String name, Class... extraInterfaces) + { + T mock = extraInterfaces.length > 0 ? mock(clazz, withSettings().extraInterfaces(extraInterfaces)) : mock(clazz); + when(mock.getType()) + .thenReturn(type); + when(mock.toString()) + .thenReturn(name); + when(mock.getName()) + .thenReturn(name); + when(mock.getIdLong()) + .thenReturn(type.ordinal() + (counter++)); + if (IPositionableChannel.class.isAssignableFrom(clazz)) + { + IPositionableChannel positionable = (IPositionableChannel) mock; + when(positionable.getPositionRaw()) + .thenReturn(type.ordinal() + (int) (counter++)); + } + if (GuildChannel.class.isAssignableFrom(clazz)) + { + GuildChannel comparable = (GuildChannel) mock; + when(comparable.compareTo(any())) + .then((args) -> ChannelUtil.compare((GuildChannel) args.getMock(), args.getArgument(0))); + } + return mock; + } + + private static IThreadContainerUnion getThreadContainer(ChannelType threadType) + { + switch (threadType) + { + case GUILD_PRIVATE_THREAD: + return mockChannel(IThreadContainerUnion.class, ChannelType.TEXT, "TEXT parent of " + threadType, GuildMessageChannel.class); + case GUILD_NEWS_THREAD: + return mockChannel(IThreadContainerUnion.class, ChannelType.NEWS, "NEWS parent of " + threadType, GuildMessageChannel.class); + case GUILD_PUBLIC_THREAD: + return mockChannel(IThreadContainerUnion.class, ChannelType.FORUM, "FORUM parent of " + threadType, IPostContainer.class); + default: + throw new IllegalStateException("Cannot map unknown thread type " + threadType); + } + } + + private static SortedChannelCacheViewImpl getMockedGuildCache() + { + SortedChannelCacheViewImpl view = new SortedChannelCacheViewImpl<>(GuildChannel.class); + + for (ChannelType type : ChannelType.values()) + { + Class channelType = type.getInterface(); + + if (ICategorizableChannel.class.isAssignableFrom(channelType)) + { + Category category = mockChannel(ChannelType.CATEGORY, "CATEGORY parent of " + type); + ICategorizableChannel channel = mockChannel(type, type + " with parent"); + long categoryId = category.getIdLong(); + + when(channel.getParentCategoryIdLong()) + .thenReturn(categoryId); + when(channel.getParentCategory()) + .thenReturn(category); + + view.put(category); + view.put(channel); + + GuildChannel noParent = mockChannel(type, type + " without parent"); + view.put(noParent); + } + else if (ThreadChannel.class.isAssignableFrom(channelType)) + { + IThreadContainerUnion parent = getThreadContainer(type); + ChannelType containerType = parent.getType(); + when(parent.toString()) + .thenReturn(containerType + " parent of " + type); + + ThreadChannel thread = mockChannel(type, type.name()); + when(thread.getParentChannel()) + .thenReturn(parent); + + view.put(parent); + view.put(thread); + } + else if (GuildChannel.class.isAssignableFrom(channelType)) + { + GuildChannel channel = mockChannel(type, type + " without parent"); + view.put(channel); + } + } + + return view; + } + + private static String toListString(Stream stream) + { + return stream.map(Objects::toString).collect(Collectors.joining("\n")); + } + + @Test + void testSortedStream() + { + SortedChannelCacheView cache = getMockedGuildCache(); + String output = toListString(cache.stream()); + assertEquals(VALID_SORT_ORDER, output); + + output = toListString(cache.parallelStream()); + assertEquals(VALID_SORT_ORDER, output); + } + + @Test + void testUnsortedStream() + { + SortedChannelCacheView cache = getMockedGuildCache(); + String output = toListString(cache.streamUnordered()); + assertNotEquals(VALID_SORT_ORDER, output); + + output = toListString(cache.parallelStreamUnordered()); + assertNotEquals(VALID_SORT_ORDER, output); + + output = cache.applyStream(ChannelCacheViewTest::toListString); + assertNotEquals(VALID_SORT_ORDER, output); + } + + @Test + void testAsListWorks() + { + SortedChannelCacheView cache = getMockedGuildCache(); + String output = toListString(cache.asList().stream()); + + assertEquals(VALID_SORT_ORDER, output); + + SortedChannelCacheView voiceView = cache.ofType(VoiceChannel.class); + List fromOfType = voiceView.asList(); + List voiceChannelFilter = cache.applyStream(stream -> stream.filter(VoiceChannel.class::isInstance).collect(Collectors.toList())); + + assertEquals(voiceView.size(), voiceChannelFilter.size()); + assertTrue(fromOfType.containsAll(voiceChannelFilter), "The filtered CacheView must contain all of VoiceChannel"); + assertTrue(voiceChannelFilter.containsAll(fromOfType), "The filtered CacheView must contain exactly all of VoiceChannel"); + } + + @Test + void testAsSetWorks() + { + SortedChannelCacheView cache = getMockedGuildCache(); + String output = toListString(cache.asSet().stream()); + + assertEquals(VALID_SORT_ORDER, output); + + SortedChannelCacheView voiceView = cache.ofType(VoiceChannel.class); + Set fromOfType = voiceView.asSet(); + Set voiceChannelFilter = cache.applyStream(stream -> stream.filter(VoiceChannel.class::isInstance).collect(Collectors.toSet())); + + assertEquals(voiceView.size(), voiceChannelFilter.size()); + assertTrue(fromOfType.containsAll(voiceChannelFilter), "The filtered CacheView must contain all of VoiceChannel"); + assertTrue(voiceChannelFilter.containsAll(fromOfType), "The filtered CacheView must contain exactly all of VoiceChannel"); + } + + @Test + void testSizeWorks() + { + SortedChannelCacheView cache = getMockedGuildCache(); + NavigableSet asSet = cache.asSet(); + + assertEquals(asSet.size(), cache.size()); + + SortedChannelCacheView ofTypeMessage = cache.ofType(GuildMessageChannel.class); + Set filterMessageType = asSet.stream().filter(GuildMessageChannel.class::isInstance).collect(Collectors.toSet()); + + assertEquals(filterMessageType.size(), ofTypeMessage.size()); + } + + @Test + void testEmptyWorks() + { + SortedChannelCacheView empty = new SortedChannelCacheViewImpl<>(GuildChannel.class); + + assertTrue(empty.isEmpty(), "New cache must be empty"); + + SortedChannelCacheViewImpl filled = getMockedGuildCache(); + + assertFalse(filled.ofType(GuildMessageChannel.class).isEmpty(), "Filtered cache must not be empty before remove"); + + filled.removeIf(GuildMessageChannel.class, (c) -> true); + + assertFalse(filled.isEmpty(), "Filled cache must not be empty"); + assertTrue(filled.ofType(GuildMessageChannel.class).isEmpty(), "Filtered cache must be empty"); + } + + @Test + void testRemoveWorks() + { + SortedChannelCacheViewImpl cache = getMockedGuildCache(); + Supplier> getByName = () -> cache.getElementsByName("TEXT without parent", true); + Supplier> getOfType = () -> cache.ofType(GuildMessageChannel.class).asList(); + + GuildChannel textWithoutParent = getByName.get().get(0); + + assertSame(textWithoutParent, cache.remove(textWithoutParent), "Remove returns instance"); + assertTrue(getByName.get().isEmpty(), "Channel should be removed"); + + List messageChannels = getOfType.get(); + + assertFalse(messageChannels.isEmpty(), "Message channels should not be removed"); + + cache.removeIf(GuildChannel.class, GuildMessageChannel.class::isInstance); + + messageChannels = getOfType.get(); + + assertTrue(messageChannels.isEmpty(), "Message channels should be removed"); + } +}