Add tests for messages/components/translations

This commit is contained in:
Luck 2023-03-29 23:03:11 +01:00
parent 85a49f0f44
commit e61ac8bef6
No known key found for this signature in database
GPG Key ID: EFA9B3EC5FD90F8B
5 changed files with 362 additions and 5 deletions

View File

@ -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;
}

View File

@ -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");
}

View File

@ -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

View File

@ -0,0 +1,301 @@
/*
* This file is part of LuckPerms, licensed under the MIT License.
*
* Copyright (c) lucko (Luck) <luck@lucko.me>
* 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<Class<?>> 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<String> IGNORED_MISSING_TRANSLATION_KEYS = ImmutableSet.of(
"luckperms.command.misc.invalid-input-empty-stub"
);
private static TranslationRegistry registry;
private static Set<String> 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<Field> 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<Argument> 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<Component> 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<Component> 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<Method> 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;
}
}

View File

@ -0,0 +1,51 @@
/*
* This file is part of LuckPerms, licensed under the MIT License.
*
* Copyright (c) lucko (Luck) <luck@lucko.me>
* 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<String> keys = bundle.keySet();
assertTrue(keys.size() > 100);
for (String key : keys) {
assertTrue(key.startsWith("luckperms."), "key " + key + " should start with 'luckperms.'");
}
}
}