From e61ac8bef6861163e313a65aceea0bf3c20f3461 Mon Sep 17 00:00:00 2001 From: Luck Date: Wed, 29 Mar 2023 23:03:11 +0100 Subject: [PATCH] Add tests for messages/components/translations --- .../common/command/spec/Argument.java | 7 +- .../common/command/spec/CommandSpec.java | 5 +- .../main/resources/luckperms_en.properties | 3 + .../luckperms/common/locale/MessageTest.java | 301 ++++++++++++++++++ .../common/locale/TranslationTest.java | 51 +++ 5 files changed, 362 insertions(+), 5 deletions(-) create mode 100644 common/src/test/java/me/lucko/luckperms/common/locale/MessageTest.java create mode 100644 common/src/test/java/me/lucko/luckperms/common/locale/TranslationTest.java diff --git a/common/src/main/java/me/lucko/luckperms/common/command/spec/Argument.java b/common/src/main/java/me/lucko/luckperms/common/command/spec/Argument.java index f9788a3a6..660b3e408 100644 --- a/common/src/main/java/me/lucko/luckperms/common/command/spec/Argument.java +++ b/common/src/main/java/me/lucko/luckperms/common/command/spec/Argument.java @@ -28,13 +28,14 @@ package me.lucko.luckperms.common.command.spec; import me.lucko.luckperms.common.locale.Message; import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.TranslatableComponent; public class Argument { private final String name; private final boolean required; - private final Component description; + private final TranslatableComponent description; - Argument(String name, boolean required, Component description) { + Argument(String name, boolean required, TranslatableComponent description) { this.name = name; this.required = required; this.description = description; @@ -48,7 +49,7 @@ public class Argument { return this.required; } - public Component getDescription() { + public TranslatableComponent getDescription() { return this.description; } diff --git a/common/src/main/java/me/lucko/luckperms/common/command/spec/CommandSpec.java b/common/src/main/java/me/lucko/luckperms/common/command/spec/CommandSpec.java index f749a3117..35bbe104f 100644 --- a/common/src/main/java/me/lucko/luckperms/common/command/spec/CommandSpec.java +++ b/common/src/main/java/me/lucko/luckperms/common/command/spec/CommandSpec.java @@ -28,6 +28,7 @@ package me.lucko.luckperms.common.command.spec; import me.lucko.luckperms.common.util.ImmutableCollectors; import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.TranslatableComponent; import java.util.Arrays; import java.util.List; @@ -418,7 +419,7 @@ public enum CommandSpec { this.args = args.length == 0 ? null : Arrays.stream(args) .map(builder -> { String key = builder.id.replace(".", "").replace(' ', '-'); - Component description = Component.translatable("luckperms.usage." + key() + ".argument." + key); + TranslatableComponent description = Component.translatable("luckperms.usage." + key() + ".argument." + key); return new Argument(builder.name, builder.required, description); }) .collect(ImmutableCollectors.toList()); @@ -428,7 +429,7 @@ public enum CommandSpec { this(null, args); } - public Component description() { + public TranslatableComponent description() { return Component.translatable("luckperms.usage." + this.key() + ".description"); } diff --git a/common/src/main/resources/luckperms_en.properties b/common/src/main/resources/luckperms_en.properties index 8703a62ef..c8668e2ac 100644 --- a/common/src/main/resources/luckperms_en.properties +++ b/common/src/main/resources/luckperms_en.properties @@ -432,6 +432,8 @@ luckperms.usage.translations.argument.install=subcommand to install translations luckperms.usage.apply-edits.description=Applies permission changes made from the web editor luckperms.usage.apply-edits.argument.code=the unique code for the data luckperms.usage.apply-edits.argument.target=who to apply the data to +luckperms.usage.trust-editor.description=Trusts an editor session to apply changes without a confirmation +luckperms.usage.trust-editor.argument.id=the id of the session to trust luckperms.usage.create-group.description=Create a new group luckperms.usage.create-group.argument.name=the name of the group luckperms.usage.create-group.argument.weight=the weight of the group @@ -439,6 +441,7 @@ luckperms.usage.create-group.argument.display-name=the display name of the group luckperms.usage.delete-group.description=Delete a group luckperms.usage.delete-group.argument.name=the name of the group luckperms.usage.list-groups.description=List all groups on the platform +luckperms.usage.list-groups.argument.page=the page to view luckperms.usage.create-track.description=Create a new track luckperms.usage.create-track.argument.name=the name of the track luckperms.usage.delete-track.description=Delete a track diff --git a/common/src/test/java/me/lucko/luckperms/common/locale/MessageTest.java b/common/src/test/java/me/lucko/luckperms/common/locale/MessageTest.java new file mode 100644 index 000000000..097be8e3f --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/locale/MessageTest.java @@ -0,0 +1,301 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package me.lucko.luckperms.common.locale; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; + +import me.lucko.luckperms.common.actionlog.LoggedAction; +import me.lucko.luckperms.common.command.spec.Argument; +import me.lucko.luckperms.common.command.spec.CommandSpec; +import me.lucko.luckperms.common.context.ImmutableContextSetImpl; +import me.lucko.luckperms.common.extension.SimpleExtensionManager; +import me.lucko.luckperms.common.model.HolderType; +import me.lucko.luckperms.common.model.InheritanceOrigin; +import me.lucko.luckperms.common.model.PermissionHolder; +import me.lucko.luckperms.common.model.PermissionHolderIdentifier; +import me.lucko.luckperms.common.node.types.Inheritance; +import me.lucko.luckperms.common.node.types.Meta; +import me.lucko.luckperms.common.node.types.Permission; +import me.lucko.luckperms.common.node.types.Prefix; +import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import me.lucko.luckperms.common.plugin.bootstrap.LuckPermsBootstrap; +import me.lucko.luckperms.common.storage.Storage; + +import net.kyori.adventure.key.Key; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.ComponentIteratorFlag; +import net.kyori.adventure.text.ComponentIteratorType; +import net.kyori.adventure.text.TranslatableComponent; +import net.kyori.adventure.translation.TranslationRegistry; +import net.kyori.adventure.util.UTF8ResourceBundleControl; +import net.luckperms.api.actionlog.Action; +import net.luckperms.api.context.ContextSet; +import net.luckperms.api.model.data.DataType; +import net.luckperms.api.node.Node; +import net.luckperms.api.node.metadata.types.InheritanceOriginMetadata; +import net.luckperms.api.node.types.ChatMetaNode; +import net.luckperms.api.node.types.InheritanceNode; +import net.luckperms.api.node.types.MetaNode; +import net.luckperms.api.platform.Platform; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Answers; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.text.MessageFormat; +import java.time.Instant; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.Optional; +import java.util.ResourceBundle; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; + +@ExtendWith(MockitoExtension.class) +public class MessageTest { + + private static final Set> MESSAGE_CLASSES = ImmutableSet.of( + Message.Args0.class, + Message.Args1.class, + Message.Args2.class, + Message.Args3.class, + Message.Args4.class, + Message.Args5.class, + Message.Args6.class + ); + + private static final Set IGNORED_MISSING_TRANSLATION_KEYS = ImmutableSet.of( + "luckperms.command.misc.invalid-input-empty-stub" + ); + + private static TranslationRegistry registry; + private static Set translationKeys; + + @BeforeAll + public static void setupRenderer() { + registry = TranslationRegistry.create(Key.key("luckperms", "test")); + + ResourceBundle bundle = ResourceBundle.getBundle("luckperms", Locale.ENGLISH, UTF8ResourceBundleControl.get()); + translationKeys = ImmutableSet.copyOf(bundle.keySet()); + registry.registerAll(Locale.ENGLISH, bundle, false); + } + + private static Stream getMessageFields() { + return Arrays.stream(Message.class.getDeclaredFields()) + .filter(f -> Modifier.isStatic(f.getModifiers())) + .filter(f -> MESSAGE_CLASSES.contains(f.getType())); + } + + @ParameterizedTest + @MethodSource("getMessageFields") + public void testMessage(Field field) { + Component baseComponent = buildMessage(field); + for (Component part : getNestedComponents(baseComponent)) { + if (part instanceof TranslatableComponent) { + TranslatableComponent component = (TranslatableComponent) part; + assertTranslatableComponentValid(component); + } + } + } + + @ParameterizedTest + @EnumSource + public void testCommandUsageMessages(CommandSpec commandSpec) { + assertTranslatableComponentValid(commandSpec.description()); + + List args = commandSpec.args(); + if (args != null) { + for (Argument arg : args) { + assertTranslatableComponentValid(arg.getDescription()); + } + } + } + + private static void assertTranslatableComponentValid(TranslatableComponent component) { + String key = component.key(); + + if (IGNORED_MISSING_TRANSLATION_KEYS.contains(key)) { + return; + } + + assertTrue(translationKeys.contains(key), "unknown translation key: " + key); + + List args = component.args(); + MessageFormat fmt = registry.translate(key, Locale.ENGLISH); + assertNotNull(fmt); + assertEquals(fmt.getFormats().length, args.size(), "number of formats in translation for " + key + " does not match number of arguments"); + } + + private static Iterable getNestedComponents(Component component) { + return component.iterable( + ComponentIteratorType.BREADTH_FIRST, + ImmutableSet.of(ComponentIteratorFlag.INCLUDE_TRANSLATABLE_COMPONENT_ARGUMENTS, ComponentIteratorFlag.INCLUDE_HOVER_SHOW_TEXT_COMPONENT) + ); + } + + private static Component buildMessage(Field field) { + Class type = field.getType(); + + List buildMethods = Arrays.stream(type.getDeclaredMethods()) + .filter(method -> method.getName().equals("build")) + .collect(Collectors.toList()); + + if (buildMethods.size() != 1) { + throw new IllegalStateException("Expected exactly one build() method - " + buildMethods); + } + + Method buildMethod = buildMethods.get(0); + Object[] parameters = new Object[buildMethod.getParameterCount()]; + + if (buildMethod.getParameterCount() != 0) { + Type genericType = field.getGenericType(); + Type[] typeArguments = ((ParameterizedType) genericType).getActualTypeArguments(); + for (int i = 0; i < typeArguments.length; i++) { + Type typeArgument = typeArguments[i]; + parameters[i] = mockArgument(typeArgument); + } + } + + try { + Object builder = field.get(null); + return (Component) buildMethod.invoke(builder, parameters); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + + private static Object mockArgument(Type type) { + if (type instanceof ParameterizedType) { + return mockArgument(((ParameterizedType) type).getRawType()); + } + + Class clazz = (Class) type; + + if (clazz == String.class) { + return "stub"; + } else if (clazz == Integer.class) { + return 0; + } else if (clazz == Boolean.class) { + return false; + } else if (clazz == Double.class) { + return 0d; + } else if (clazz == LoggedAction.class) { + return LoggedAction.build() + .source(UUID.randomUUID()) + .sourceName("stub") + .targetType(Action.Target.Type.GROUP) + .targetName("stub") + .description("stub") + .build(); + } else if (clazz == Node.class) { + return Permission.builder().permission("stub").expiry(1, TimeUnit.MINUTES).build(); + } else if (clazz == InheritanceNode.class) { + return Inheritance.builder().group("stub").expiry(1, TimeUnit.MINUTES).build(); + } else if (clazz == MetaNode.class) { + return Meta.builder("stub", "stub") + .withMetadata(InheritanceOriginMetadata.KEY, new InheritanceOrigin( + new PermissionHolderIdentifier(HolderType.GROUP, "stub"), + DataType.NORMAL + )) + .build(); + } else if (clazz == ChatMetaNode.class) { + return Prefix.builder("stub", 1) + .withMetadata(InheritanceOriginMetadata.KEY, new InheritanceOrigin( + new PermissionHolderIdentifier(HolderType.GROUP, "stub"), + DataType.NORMAL + )) + .build(); + } else if (clazz == ContextSet.class) { + return ImmutableContextSetImpl.of("stub", "stub"); + } else if (clazz == Component.class) { + return Component.text("stub"); + } else if (clazz == List.class) { + return ImmutableList.of(); + } else if (clazz == Collection.class) { + return ImmutableList.of(); + } + + Object mock; + if (clazz == LuckPermsPlugin.class) { + mock = mock(clazz, Answers.RETURNS_DEEP_STUBS); + } else { + mock = mock(clazz, Answers.RETURNS_SMART_NULLS); + } + + if (mock instanceof LuckPermsBootstrap) { + LuckPermsBootstrap bootstrap = (LuckPermsBootstrap) mock; + lenient().when(bootstrap.getType()).thenReturn(Platform.Type.BUKKIT); + lenient().when(bootstrap.getStartupTime()).thenReturn(Instant.now()); + } else if (mock instanceof LuckPermsPlugin) { + LuckPermsPlugin plugin = (LuckPermsPlugin) mock; + + LuckPermsBootstrap bootstrap = (LuckPermsBootstrap) mockArgument(LuckPermsBootstrap.class); + lenient().when(plugin.getBootstrap()).thenReturn(bootstrap); + + Storage storage = (Storage) mockArgument(Storage.class); + lenient().when(plugin.getStorage()).thenReturn(storage); + + SimpleExtensionManager extManager = (SimpleExtensionManager) mockArgument(SimpleExtensionManager.class); + lenient().when(plugin.getExtensionManager()).thenReturn(extManager); + + lenient().when(plugin.getMessagingService()).thenReturn(Optional.empty()); + } else if (mock instanceof PermissionHolder) { + PermissionHolder holder = (PermissionHolder) mock; + + LuckPermsPlugin plugin = (LuckPermsPlugin) mockArgument(LuckPermsPlugin.class); + lenient().when(holder.getPlugin()).thenReturn(plugin); + + lenient().when(holder.getFormattedDisplayName()).thenReturn(Component.text("stub")); + } else if (mock instanceof SimpleExtensionManager) { + SimpleExtensionManager manager = (SimpleExtensionManager) mock; + lenient().when(manager.getLoadedExtensions()).thenReturn(ImmutableList.of()); + } + + return mock; + } + +} diff --git a/common/src/test/java/me/lucko/luckperms/common/locale/TranslationTest.java b/common/src/test/java/me/lucko/luckperms/common/locale/TranslationTest.java new file mode 100644 index 000000000..0a8d49ef3 --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/locale/TranslationTest.java @@ -0,0 +1,51 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package me.lucko.luckperms.common.locale; + +import net.kyori.adventure.util.UTF8ResourceBundleControl; + +import org.junit.jupiter.api.Test; + +import java.util.Locale; +import java.util.ResourceBundle; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class TranslationTest { + + @Test + public void testBundleParse() { + ResourceBundle bundle = ResourceBundle.getBundle("luckperms", Locale.ENGLISH, UTF8ResourceBundleControl.get()); + Set keys = bundle.keySet(); + assertTrue(keys.size() > 100); + + for (String key : keys) { + assertTrue(key.startsWith("luckperms."), "key " + key + " should start with 'luckperms.'"); + } + } + +}