diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index faa9103a8..f5b4ba4c9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,8 +31,14 @@ jobs: - name: Setup Gradle uses: gradle/gradle-build-action@v2 - - name: Run build with Gradle wrapper - run: ./gradlew build + - name: Run build and tests with Gradle wrapper + run: ./gradlew test build + + - name: Publish test report + uses: mikepenz/action-junit-report@v3 + if: success() || failure() + with: + report_paths: '**/build/test-results/test/TEST-*.xml' - name: Upload all artifacts uses: actions/upload-artifact@v3 diff --git a/common/build.gradle b/common/build.gradle index 7a36dc6a5..93948b2b2 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -3,9 +3,7 @@ plugins { } test { - useJUnitPlatform { - excludeTags 'dependency_checksum' - } + useJUnitPlatform {} } dependencies { @@ -14,7 +12,6 @@ dependencies { testImplementation 'org.junit.jupiter:junit-jupiter-params:5.9.1' testImplementation 'org.mockito:mockito-core:4.11.0' testImplementation 'org.mockito:mockito-junit-jupiter:4.11.0' - testImplementation 'com.h2database:h2:2.1.214' api project(':api') diff --git a/common/src/main/java/me/lucko/luckperms/common/dependencies/DependencyManager.java b/common/src/main/java/me/lucko/luckperms/common/dependencies/DependencyManager.java index 71a54a3f0..3ee7b502a 100644 --- a/common/src/main/java/me/lucko/luckperms/common/dependencies/DependencyManager.java +++ b/common/src/main/java/me/lucko/luckperms/common/dependencies/DependencyManager.java @@ -25,235 +25,40 @@ package me.lucko.luckperms.common.dependencies; -import com.google.common.collect.ImmutableSet; - -import me.lucko.luckperms.common.dependencies.classloader.IsolatedClassLoader; -import me.lucko.luckperms.common.dependencies.relocation.Relocation; -import me.lucko.luckperms.common.dependencies.relocation.RelocationHandler; -import me.lucko.luckperms.common.plugin.LuckPermsPlugin; -import me.lucko.luckperms.common.plugin.classpath.ClassPathAppender; import me.lucko.luckperms.common.storage.StorageType; -import me.lucko.luckperms.common.util.MoreFiles; -import net.luckperms.api.platform.Platform; - -import org.checkerframework.checker.nullness.qual.MonotonicNonNull; - -import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URL; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.EnumMap; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; import java.util.Set; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.Executor; /** * Loads and manages runtime dependencies for the plugin. */ -public class DependencyManager implements AutoCloseable { +public interface DependencyManager extends AutoCloseable { - /** A registry containing plugin specific behaviour for dependencies. */ - private final DependencyRegistry registry; - /** The path where library jars are cached. */ - private final Path cacheDirectory; - /** The classpath appender to preload dependencies into */ - private final ClassPathAppender classPathAppender; - /** The executor to use when loading dependencies */ - private final Executor loadingExecutor; + /** + * Loads dependencies. + * + * @param dependencies the dependencies to load + */ + void loadDependencies(Set dependencies); - /** A map of dependencies which have already been loaded. */ - private final EnumMap loaded = new EnumMap<>(Dependency.class); - /** A map of isolated classloaders which have been created. */ - private final Map, IsolatedClassLoader> loaders = new HashMap<>(); - /** Cached relocation handler instance. */ - private @MonotonicNonNull RelocationHandler relocationHandler = null; + /** + * Loads storage dependencies. + * + * @param storageTypes the storage types in use + * @param redis if redis is being used + * @param rabbitmq if rabbitmq is being used + * @param nats if nats is being used + */ + void loadStorageDependencies(Set storageTypes, boolean redis, boolean rabbitmq, boolean nats); - public DependencyManager(LuckPermsPlugin plugin) { - this.registry = new DependencyRegistry(plugin.getBootstrap().getType()); - this.cacheDirectory = setupCacheDirectory(plugin); - this.classPathAppender = plugin.getBootstrap().getClassPathAppender(); - this.loadingExecutor = plugin.getBootstrap().getScheduler().async(); - } - - public DependencyManager(Path cacheDirectory, Executor executor) { // standalone - this.registry = new DependencyRegistry(Platform.Type.STANDALONE); - this.cacheDirectory = cacheDirectory; - this.classPathAppender = null; - this.loadingExecutor = executor; - } - - private synchronized RelocationHandler getRelocationHandler() { - if (this.relocationHandler == null) { - this.relocationHandler = new RelocationHandler(this); - } - return this.relocationHandler; - } - - public IsolatedClassLoader obtainClassLoaderWith(Set dependencies) { - ImmutableSet set = ImmutableSet.copyOf(dependencies); - - for (Dependency dependency : dependencies) { - if (!this.loaded.containsKey(dependency)) { - throw new IllegalStateException("Dependency " + dependency + " is not loaded."); - } - } - - synchronized (this.loaders) { - IsolatedClassLoader classLoader = this.loaders.get(set); - if (classLoader != null) { - return classLoader; - } - - URL[] urls = set.stream() - .map(this.loaded::get) - .map(file -> { - try { - return file.toUri().toURL(); - } catch (MalformedURLException e) { - throw new RuntimeException(e); - } - }) - .toArray(URL[]::new); - - classLoader = new IsolatedClassLoader(urls); - this.loaders.put(set, classLoader); - return classLoader; - } - } - - public void loadStorageDependencies(Set storageTypes, boolean redis, boolean rabbitmq, boolean nats) { - loadDependencies(this.registry.resolveStorageDependencies(storageTypes, redis, rabbitmq, nats)); - } - - public void loadDependencies(Set dependencies) { - CountDownLatch latch = new CountDownLatch(dependencies.size()); - - for (Dependency dependency : dependencies) { - if (this.loaded.containsKey(dependency)) { - latch.countDown(); - continue; - } - - this.loadingExecutor.execute(() -> { - try { - loadDependency(dependency); - } catch (Throwable e) { - new RuntimeException("Unable to load dependency " + dependency.name(), e).printStackTrace(); - } finally { - latch.countDown(); - } - }); - } - - try { - latch.await(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - } - - private void loadDependency(Dependency dependency) throws Exception { - if (this.loaded.containsKey(dependency)) { - return; - } - - Path file = remapDependency(dependency, downloadDependency(dependency)); - - this.loaded.put(dependency, file); - - if (this.classPathAppender != null && this.registry.shouldAutoLoad(dependency)) { - this.classPathAppender.addJarToClasspath(file); - } - } - - private Path downloadDependency(Dependency dependency) throws DependencyDownloadException { - Path file = this.cacheDirectory.resolve(dependency.getFileName(null)); - - // if the file already exists, don't attempt to re-download it. - if (Files.exists(file)) { - return file; - } - - DependencyDownloadException lastError = null; - - // attempt to download the dependency from each repo in order. - for (DependencyRepository repo : DependencyRepository.values()) { - try { - repo.download(dependency, file); - return file; - } catch (DependencyDownloadException e) { - lastError = e; - } - } - - throw Objects.requireNonNull(lastError); - } - - private Path remapDependency(Dependency dependency, Path normalFile) throws Exception { - List rules = new ArrayList<>(dependency.getRelocations()); - this.registry.applyRelocationSettings(dependency, rules); - - if (rules.isEmpty()) { - return normalFile; - } - - Path remappedFile = this.cacheDirectory.resolve(dependency.getFileName(DependencyRegistry.isGsonRelocated() ? "remapped-legacy" : "remapped")); - - // if the remapped source exists already, just use that. - if (Files.exists(remappedFile)) { - return remappedFile; - } - - getRelocationHandler().remap(normalFile, remappedFile, rules); - return remappedFile; - } - - private static Path setupCacheDirectory(LuckPermsPlugin plugin) { - Path cacheDirectory = plugin.getBootstrap().getDataDirectory().resolve("libs"); - try { - MoreFiles.createDirectoriesIfNotExists(cacheDirectory); - } catch (IOException e) { - throw new RuntimeException("Unable to create libs directory", e); - } - - Path oldCacheDirectory = plugin.getBootstrap().getDataDirectory().resolve("lib"); - if (Files.exists(oldCacheDirectory)) { - try { - MoreFiles.deleteDirectory(oldCacheDirectory); - } catch (IOException e) { - plugin.getLogger().warn("Unable to delete lib directory", e); - } - } - - return cacheDirectory; - } + /** + * Obtains an isolated classloader containing the given dependencies. + * + * @param dependencies the dependencies + * @return the classloader + */ + ClassLoader obtainClassLoaderWith(Set dependencies); @Override - public void close() { - IOException firstEx = null; - - for (IsolatedClassLoader loader : this.loaders.values()) { - try { - loader.close(); - } catch (IOException ex) { - if (firstEx == null) { - firstEx = ex; - } else { - firstEx.addSuppressed(ex); - } - } - } - - if (firstEx != null) { - firstEx.printStackTrace(); - } - } - + void close(); } diff --git a/common/src/main/java/me/lucko/luckperms/common/dependencies/DependencyManagerImpl.java b/common/src/main/java/me/lucko/luckperms/common/dependencies/DependencyManagerImpl.java new file mode 100644 index 000000000..c8afb362f --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/dependencies/DependencyManagerImpl.java @@ -0,0 +1,262 @@ +/* + * 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.dependencies; + +import com.google.common.collect.ImmutableSet; + +import me.lucko.luckperms.common.dependencies.classloader.IsolatedClassLoader; +import me.lucko.luckperms.common.dependencies.relocation.Relocation; +import me.lucko.luckperms.common.dependencies.relocation.RelocationHandler; +import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import me.lucko.luckperms.common.plugin.classpath.ClassPathAppender; +import me.lucko.luckperms.common.storage.StorageType; +import me.lucko.luckperms.common.util.MoreFiles; + +import net.luckperms.api.platform.Platform; + +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; + +/** + * Loads and manages runtime dependencies for the plugin. + */ +public class DependencyManagerImpl implements DependencyManager { + + /** A registry containing plugin specific behaviour for dependencies. */ + private final DependencyRegistry registry; + /** The path where library jars are cached. */ + private final Path cacheDirectory; + /** The classpath appender to preload dependencies into */ + private final ClassPathAppender classPathAppender; + /** The executor to use when loading dependencies */ + private final Executor loadingExecutor; + + /** A map of dependencies which have already been loaded. */ + private final EnumMap loaded = new EnumMap<>(Dependency.class); + /** A map of isolated classloaders which have been created. */ + private final Map, IsolatedClassLoader> loaders = new HashMap<>(); + /** Cached relocation handler instance. */ + private @MonotonicNonNull RelocationHandler relocationHandler = null; + + public DependencyManagerImpl(LuckPermsPlugin plugin) { + this.registry = new DependencyRegistry(plugin.getBootstrap().getType()); + this.cacheDirectory = setupCacheDirectory(plugin); + this.classPathAppender = plugin.getBootstrap().getClassPathAppender(); + this.loadingExecutor = plugin.getBootstrap().getScheduler().async(); + } + + public DependencyManagerImpl(Path cacheDirectory, Executor executor) { // standalone pre-loader + this.registry = new DependencyRegistry(Platform.Type.STANDALONE); + this.cacheDirectory = cacheDirectory; + this.classPathAppender = null; + this.loadingExecutor = executor; + } + + private synchronized RelocationHandler getRelocationHandler() { + if (this.relocationHandler == null) { + this.relocationHandler = new RelocationHandler(this); + } + return this.relocationHandler; + } + + @Override + public ClassLoader obtainClassLoaderWith(Set dependencies) { + ImmutableSet set = ImmutableSet.copyOf(dependencies); + + for (Dependency dependency : dependencies) { + if (!this.loaded.containsKey(dependency)) { + throw new IllegalStateException("Dependency " + dependency + " is not loaded."); + } + } + + synchronized (this.loaders) { + IsolatedClassLoader classLoader = this.loaders.get(set); + if (classLoader != null) { + return classLoader; + } + + URL[] urls = set.stream() + .map(this.loaded::get) + .map(file -> { + try { + return file.toUri().toURL(); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + }) + .toArray(URL[]::new); + + classLoader = new IsolatedClassLoader(urls); + this.loaders.put(set, classLoader); + return classLoader; + } + } + + @Override + public void loadStorageDependencies(Set storageTypes, boolean redis, boolean rabbitmq, boolean nats) { + loadDependencies(this.registry.resolveStorageDependencies(storageTypes, redis, rabbitmq, nats)); + } + + @Override + public void loadDependencies(Set dependencies) { + CountDownLatch latch = new CountDownLatch(dependencies.size()); + + for (Dependency dependency : dependencies) { + if (this.loaded.containsKey(dependency)) { + latch.countDown(); + continue; + } + + this.loadingExecutor.execute(() -> { + try { + loadDependency(dependency); + } catch (Throwable e) { + new RuntimeException("Unable to load dependency " + dependency.name(), e).printStackTrace(); + } finally { + latch.countDown(); + } + }); + } + + try { + latch.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + private void loadDependency(Dependency dependency) throws Exception { + if (this.loaded.containsKey(dependency)) { + return; + } + + Path file = remapDependency(dependency, downloadDependency(dependency)); + + this.loaded.put(dependency, file); + + if (this.classPathAppender != null && this.registry.shouldAutoLoad(dependency)) { + this.classPathAppender.addJarToClasspath(file); + } + } + + private Path downloadDependency(Dependency dependency) throws DependencyDownloadException { + Path file = this.cacheDirectory.resolve(dependency.getFileName(null)); + + // if the file already exists, don't attempt to re-download it. + if (Files.exists(file)) { + return file; + } + + DependencyDownloadException lastError = null; + + // attempt to download the dependency from each repo in order. + for (DependencyRepository repo : DependencyRepository.values()) { + try { + repo.download(dependency, file); + return file; + } catch (DependencyDownloadException e) { + lastError = e; + } + } + + throw Objects.requireNonNull(lastError); + } + + private Path remapDependency(Dependency dependency, Path normalFile) throws Exception { + List rules = new ArrayList<>(dependency.getRelocations()); + this.registry.applyRelocationSettings(dependency, rules); + + if (rules.isEmpty()) { + return normalFile; + } + + Path remappedFile = this.cacheDirectory.resolve(dependency.getFileName(DependencyRegistry.isGsonRelocated() ? "remapped-legacy" : "remapped")); + + // if the remapped source exists already, just use that. + if (Files.exists(remappedFile)) { + return remappedFile; + } + + getRelocationHandler().remap(normalFile, remappedFile, rules); + return remappedFile; + } + + private static Path setupCacheDirectory(LuckPermsPlugin plugin) { + Path cacheDirectory = plugin.getBootstrap().getDataDirectory().resolve("libs"); + try { + MoreFiles.createDirectoriesIfNotExists(cacheDirectory); + } catch (IOException e) { + throw new RuntimeException("Unable to create libs directory", e); + } + + Path oldCacheDirectory = plugin.getBootstrap().getDataDirectory().resolve("lib"); + if (Files.exists(oldCacheDirectory)) { + try { + MoreFiles.deleteDirectory(oldCacheDirectory); + } catch (IOException e) { + plugin.getLogger().warn("Unable to delete lib directory", e); + } + } + + return cacheDirectory; + } + + @Override + public void close() { + IOException firstEx = null; + + for (IsolatedClassLoader loader : this.loaders.values()) { + try { + loader.close(); + } catch (IOException ex) { + if (firstEx == null) { + firstEx = ex; + } else { + firstEx.addSuppressed(ex); + } + } + } + + if (firstEx != null) { + firstEx.printStackTrace(); + } + } + +} diff --git a/common/src/main/java/me/lucko/luckperms/common/dependencies/relocation/RelocationHandler.java b/common/src/main/java/me/lucko/luckperms/common/dependencies/relocation/RelocationHandler.java index 6d0bb2a37..29a833e85 100644 --- a/common/src/main/java/me/lucko/luckperms/common/dependencies/relocation/RelocationHandler.java +++ b/common/src/main/java/me/lucko/luckperms/common/dependencies/relocation/RelocationHandler.java @@ -52,7 +52,7 @@ public class RelocationHandler { private final Method jarRelocatorRunMethod; public RelocationHandler(DependencyManager dependencyManager) { - IsolatedClassLoader classLoader = null; + ClassLoader classLoader = null; try { // download the required dependencies for remapping dependencyManager.loadDependencies(DEPENDENCIES); @@ -70,8 +70,8 @@ public class RelocationHandler { this.jarRelocatorRunMethod.setAccessible(true); } catch (Exception e) { try { - if (classLoader != null) { - classLoader.close(); + if (classLoader instanceof IsolatedClassLoader) { + ((IsolatedClassLoader) classLoader).close(); } } catch (IOException ex) { e.addSuppressed(ex); diff --git a/common/src/main/java/me/lucko/luckperms/common/plugin/AbstractLuckPermsPlugin.java b/common/src/main/java/me/lucko/luckperms/common/plugin/AbstractLuckPermsPlugin.java index c06cc04e7..27715187e 100644 --- a/common/src/main/java/me/lucko/luckperms/common/plugin/AbstractLuckPermsPlugin.java +++ b/common/src/main/java/me/lucko/luckperms/common/plugin/AbstractLuckPermsPlugin.java @@ -38,6 +38,7 @@ import me.lucko.luckperms.common.config.generic.adapter.SystemPropertyConfigAdap import me.lucko.luckperms.common.context.calculator.ConfigurationContextCalculator; import me.lucko.luckperms.common.dependencies.Dependency; import me.lucko.luckperms.common.dependencies.DependencyManager; +import me.lucko.luckperms.common.dependencies.DependencyManagerImpl; import me.lucko.luckperms.common.event.AbstractEventBus; import me.lucko.luckperms.common.event.EventDispatcher; import me.lucko.luckperms.common.event.gen.GeneratedEventClass; @@ -113,7 +114,7 @@ public abstract class AbstractLuckPermsPlugin implements LuckPermsPlugin { */ public final void load() { // load dependencies - this.dependencyManager = new DependencyManager(this); + this.dependencyManager = createDependencyManager(); this.dependencyManager.loadDependencies(getGlobalDependencies()); // load translations @@ -303,6 +304,10 @@ public abstract class AbstractLuckPermsPlugin implements LuckPermsPlugin { // hooks called during load + protected DependencyManager createDependencyManager() { + return new DependencyManagerImpl(this); + } + protected Set getGlobalDependencies() { return EnumSet.of( Dependency.ADVENTURE, diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/connection/file/H2ConnectionFactory.java b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/connection/file/H2ConnectionFactory.java index 49ff843f7..22ecdb2ad 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/connection/file/H2ConnectionFactory.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/connection/file/H2ConnectionFactory.java @@ -56,7 +56,7 @@ public class H2ConnectionFactory extends FlatfileConnectionFactory { public void init(LuckPermsPlugin plugin) { migrateOldDatabaseFile("luckperms.db.mv.db"); - IsolatedClassLoader classLoader = plugin.getDependencyManager().obtainClassLoaderWith(EnumSet.of(Dependency.H2_DRIVER)); + ClassLoader classLoader = plugin.getDependencyManager().obtainClassLoaderWith(EnumSet.of(Dependency.H2_DRIVER)); try { Class connectionClass = classLoader.loadClass("org.h2.jdbc.JdbcConnection"); this.connectionConstructor = connectionClass.getConstructor(String.class, Properties.class, String.class, Object.class, boolean.class); @@ -148,7 +148,7 @@ public class H2ConnectionFactory extends FlatfileConnectionFactory { private Constructor getConnectionConstructor() { this.plugin.getDependencyManager().loadDependencies(Collections.singleton(Dependency.H2_DRIVER_LEGACY)); - IsolatedClassLoader classLoader = this.plugin.getDependencyManager().obtainClassLoaderWith(EnumSet.of(Dependency.H2_DRIVER_LEGACY)); + ClassLoader classLoader = this.plugin.getDependencyManager().obtainClassLoaderWith(EnumSet.of(Dependency.H2_DRIVER_LEGACY)); try { Class connectionClass = classLoader.loadClass("org.h2.jdbc.JdbcConnection"); return connectionClass.getConstructor(String.class, Properties.class); diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/connection/file/SqliteConnectionFactory.java b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/connection/file/SqliteConnectionFactory.java index 85c82036a..b1d907cc4 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/connection/file/SqliteConnectionFactory.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/connection/file/SqliteConnectionFactory.java @@ -53,7 +53,7 @@ public class SqliteConnectionFactory extends FlatfileConnectionFactory { public void init(LuckPermsPlugin plugin) { migrateOldDatabaseFile("luckperms.sqlite"); - IsolatedClassLoader classLoader = plugin.getDependencyManager().obtainClassLoaderWith(EnumSet.of(Dependency.SQLITE_DRIVER)); + ClassLoader classLoader = plugin.getDependencyManager().obtainClassLoaderWith(EnumSet.of(Dependency.SQLITE_DRIVER)); try { Class connectionClass = classLoader.loadClass("org.sqlite.jdbc4.JDBC4Connection"); this.connectionConstructor = connectionClass.getConstructor(String.class, String.class, Properties.class); diff --git a/common/src/test/java/me/lucko/luckperms/common/bulkupdate/BulkUpdateTest.java b/common/src/test/java/me/lucko/luckperms/common/bulkupdate/BulkUpdateTest.java new file mode 100644 index 000000000..2a3245047 --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/bulkupdate/BulkUpdateTest.java @@ -0,0 +1,191 @@ +/* + * 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.bulkupdate; + +import com.google.common.collect.ImmutableSet; + +import me.lucko.luckperms.common.bulkupdate.action.Action; +import me.lucko.luckperms.common.bulkupdate.action.DeleteAction; +import me.lucko.luckperms.common.bulkupdate.action.UpdateAction; +import me.lucko.luckperms.common.bulkupdate.comparison.Constraint; +import me.lucko.luckperms.common.bulkupdate.comparison.StandardComparison; +import me.lucko.luckperms.common.bulkupdate.query.Query; +import me.lucko.luckperms.common.bulkupdate.query.QueryField; +import me.lucko.luckperms.common.model.HolderType; +import me.lucko.luckperms.common.node.types.Permission; + +import net.luckperms.api.node.Node; + +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.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Set; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class BulkUpdateTest { + + @Test + public void testUpdate() { + BulkUpdate update = BulkUpdateBuilder.create() + .action(UpdateAction.of(QueryField.SERVER, "foo")) + .query(Query.of(QueryField.WORLD, Constraint.of(StandardComparison.EQUAL, "bar"))) + .query(Query.of(QueryField.PERMISSION, Constraint.of(StandardComparison.SIMILAR, "hello%"))) + .trackStatistics(true) + .build(); + + Instant time = Instant.now().plus(1, ChronoUnit.HOURS); + + Set nodes = ImmutableSet.of( + Permission.builder().permission("test").build(), + Permission.builder().permission("hello").build(), + Permission.builder().permission("hello").withContext("world", "bar").build(), + Permission.builder().permission("hello.world").value(false).expiry(time).withContext("world", "bar").build(), + Permission.builder().permission("hello").withContext("world", "bar").withContext("server", "bar").build() + ); + Set expected = ImmutableSet.of( + Permission.builder().permission("test").build(), + Permission.builder().permission("hello").build(), + Permission.builder().permission("hello").withContext("world", "bar").withContext("server", "foo").build(), + Permission.builder().permission("hello.world").value(false).expiry(time).withContext("world", "bar").withContext("server", "foo").build(), + Permission.builder().permission("hello").withContext("world", "bar").withContext("server", "foo").build() + ); + + assertEquals(expected, update.apply(nodes, HolderType.USER)); + + BulkUpdateStatistics statistics = update.getStatistics(); + assertEquals(3, statistics.getAffectedNodes()); + assertEquals(1, statistics.getAffectedUsers()); + assertEquals(0, statistics.getAffectedGroups()); + } + + @Test + public void testDelete() { + BulkUpdate update = BulkUpdateBuilder.create() + .action(DeleteAction.create()) + .query(Query.of(QueryField.WORLD, Constraint.of(StandardComparison.EQUAL, "bar"))) + .query(Query.of(QueryField.PERMISSION, Constraint.of(StandardComparison.SIMILAR, "hello%"))) + .trackStatistics(true) + .build(); + + Instant time = Instant.now().plus(1, ChronoUnit.HOURS); + + Set nodes = ImmutableSet.of( + Permission.builder().permission("test").build(), + Permission.builder().permission("hello").build(), + Permission.builder().permission("hello").withContext("world", "bar").build(), + Permission.builder().permission("hello.world").value(false).expiry(time).withContext("world", "bar").build(), + Permission.builder().permission("hello").withContext("world", "bar").withContext("server", "bar").build() + ); + Set expected = ImmutableSet.of( + Permission.builder().permission("test").build(), + Permission.builder().permission("hello").build() + ); + + assertEquals(expected, update.apply(nodes, HolderType.USER)); + + BulkUpdateStatistics statistics = update.getStatistics(); + assertEquals(3, statistics.getAffectedNodes()); + assertEquals(1, statistics.getAffectedUsers()); + assertEquals(0, statistics.getAffectedGroups()); + } + + private static Stream testSimpleActionSql() { + return Stream.of( + Arguments.of("DELETE FROM {table}", DeleteAction.create()), + Arguments.of("UPDATE {table} SET permission=foo", UpdateAction.of(QueryField.PERMISSION, "foo")), + Arguments.of("UPDATE {table} SET server=foo", UpdateAction.of(QueryField.SERVER, "foo")), + Arguments.of("UPDATE {table} SET world=foo", UpdateAction.of(QueryField.WORLD, "foo")) + ); + } + + @ParameterizedTest(name = "[{index}] {0}") + @MethodSource + public void testSimpleActionSql(String expectedSql, Action action) { + BulkUpdate update = BulkUpdateBuilder.create() + .action(action) + .build(); + assertEquals(expectedSql, update.buildAsSql().toReadableString()); + } + + private static Stream testQueryFilterSql() { + return Stream.of( + Arguments.of( + "DELETE FROM {table} WHERE permission = foo", + DeleteAction.create(), + Query.of(QueryField.PERMISSION, Constraint.of(StandardComparison.EQUAL, "foo")) + ), + Arguments.of( + "DELETE FROM {table} WHERE permission != foo", + DeleteAction.create(), + Query.of(QueryField.PERMISSION, Constraint.of(StandardComparison.NOT_EQUAL, "foo")) + ), + Arguments.of( + "DELETE FROM {table} WHERE permission LIKE foo", + DeleteAction.create(), + Query.of(QueryField.PERMISSION, Constraint.of(StandardComparison.SIMILAR, "foo")) + ), + Arguments.of( + "DELETE FROM {table} WHERE permission NOT LIKE foo", + DeleteAction.create(), + Query.of(QueryField.PERMISSION, Constraint.of(StandardComparison.NOT_SIMILAR, "foo")) + ), + Arguments.of( + "UPDATE {table} SET server=foo WHERE world = bar", + UpdateAction.of(QueryField.SERVER, "foo"), + Query.of(QueryField.WORLD, Constraint.of(StandardComparison.EQUAL, "bar")) + ) + ); + } + + @ParameterizedTest(name = "[{index}] {0}") + @MethodSource + public void testQueryFilterSql(String expectedSql, Action action, Query query) { + BulkUpdate update = BulkUpdateBuilder.create() + .action(action) + .query(query) + .build(); + assertEquals(expectedSql, update.buildAsSql().toReadableString()); + } + + @Test + public void testQueryFilterMultipleSql() { + BulkUpdate update = BulkUpdateBuilder.create() + .action(UpdateAction.of(QueryField.SERVER, "foo")) + .query(Query.of(QueryField.WORLD, Constraint.of(StandardComparison.EQUAL, "bar"))) + .query(Query.of(QueryField.PERMISSION, Constraint.of(StandardComparison.SIMILAR, "baz"))) + .build(); + + String expected = "UPDATE {table} SET server=foo WHERE world = bar AND permission LIKE baz"; + assertEquals(expected, update.buildAsSql().toReadableString()); + } + +} diff --git a/common/src/test/java/me/lucko/luckperms/common/bulkupdate/ComparisonTest.java b/common/src/test/java/me/lucko/luckperms/common/bulkupdate/ComparisonTest.java new file mode 100644 index 000000000..b2f8ea434 --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/bulkupdate/ComparisonTest.java @@ -0,0 +1,85 @@ +/* + * 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.bulkupdate; + +import me.lucko.luckperms.common.bulkupdate.comparison.StandardComparison; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class ComparisonTest { + + @ParameterizedTest(name = "[{index}] {0} {1}") + @CsvSource({ + "foo, foo, true", + "foo, Foo, true", + "Foo, foo, true", + "foo, bar, false", + "foo, '', false", + "'', foo, false", + }) + public void testEquals(String expression, String test, boolean expected) { + assertEquals(expected, StandardComparison.EQUAL.compile(expression).test(test)); + assertEquals(!expected, StandardComparison.NOT_EQUAL.compile(expression).test(test)); + } + + @ParameterizedTest(name = "[{index}] {0} {1}") + @CsvSource({ + "foo, foo, true", + "foo, Foo, true", + "Foo, foo, true", + "foo, bar, false", + "foo, '', false", + "'', foo, false", + + "foo%, foobar, true", + "foo%, Foobar, true", + "foo%, foo, true", + "%bar%, bar, true", + "%bar%, foobar, true", + "%bar%, barbaz, true", + "%bar%, foobarbaz, true", + + "_ar, bar, true", + "_ar, far, true", + "_ar, BAR, true", + "_ar, FAR, true", + "_ar, ar, false", + "_ar, bbar, false", + }) + public void testSimilar(String expression, String test, boolean expected) { + assertEquals(expected, StandardComparison.SIMILAR.compile(expression).test(test)); + assertEquals(!expected, StandardComparison.NOT_SIMILAR.compile(expression).test(test)); + } + +} + diff --git a/common/src/test/java/me/lucko/luckperms/common/context/ContextSetComparatorTest.java b/common/src/test/java/me/lucko/luckperms/common/context/ContextSetComparatorTest.java new file mode 100644 index 000000000..76a1a305d --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/context/ContextSetComparatorTest.java @@ -0,0 +1,155 @@ +/* + * 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.context; + +import com.google.common.collect.Lists; + +import me.lucko.luckperms.common.context.comparator.ContextSetComparator; + +import net.luckperms.api.context.ImmutableContextSet; + +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.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class ContextSetComparatorTest { + + private static final ImmutableContextSet EMPTY = ImmutableContextSetImpl.EMPTY; + private static final ImmutableContextSet JUST_SERVER = ImmutableContextSetImpl.of("server", "foo"); + private static final ImmutableContextSet JUST_WORLD = ImmutableContextSetImpl.of("world", "foo"); + private static final ImmutableContextSet SERVER_AND_WORLD = new ImmutableContextSetImpl.BuilderImpl() + .add("server", "foo") + .add("world", "foo") + .build(); + private static final ImmutableContextSet MISC = new ImmutableContextSetImpl.BuilderImpl() + .add("foo", "foo") + .add("foo", "bar") + .build(); + private static final ImmutableContextSet SERVER_AND_MISC = new ImmutableContextSetImpl.BuilderImpl() + .add("server", "foo") + .add("foo", "foo") + .add("foo", "bar") + .build(); + private static final ImmutableContextSet WORLD_AND_MISC = new ImmutableContextSetImpl.BuilderImpl() + .add("world", "foo") + .add("foo", "foo") + .add("foo", "bar") + .build(); + private static final ImmutableContextSet SERVER_AND_WORLD_AND_MISC_1 = new ImmutableContextSetImpl.BuilderImpl() + .add("server", "foo") + .add("world", "foo") + .add("foo", "foo") + .build(); + private static final ImmutableContextSet SERVER_AND_WORLD_AND_MISC_2 = new ImmutableContextSetImpl.BuilderImpl() + .add("server", "foo") + .add("world", "foo") + .add("foo", "foo") + .add("foo", "bar") + .build(); + + private static Stream all() { + return Stream.of(EMPTY, JUST_SERVER, JUST_WORLD, SERVER_AND_WORLD, MISC, SERVER_AND_MISC, WORLD_AND_MISC, SERVER_AND_WORLD_AND_MISC_1, SERVER_AND_WORLD_AND_MISC_2); + } + + private static final Comparator INSTANCE = ContextSetComparator.normal(); + + @ParameterizedTest + @MethodSource("all") + @SuppressWarnings("EqualsWithItself") + public void testEquals(ImmutableContextSet set) { + assertEquals(0, INSTANCE.compare(set, set)); + } + + @Test + public void testEmpty() { + assertTrue(INSTANCE.compare(JUST_SERVER, EMPTY) > 0); + assertTrue(INSTANCE.compare(EMPTY, JUST_SERVER) < 0); + } + + @Test + public void testServerPresence() { + assertTrue(INSTANCE.compare(JUST_SERVER, MISC) > 0); + assertTrue(INSTANCE.compare(JUST_SERVER, WORLD_AND_MISC) > 0); + } + + @Test + public void testWorldPresence() { + assertTrue(INSTANCE.compare(JUST_WORLD, MISC) > 0); + } + + @Test + public void testOverallSize() { + assertTrue(INSTANCE.compare(SERVER_AND_MISC, JUST_SERVER) > 0); + assertTrue(INSTANCE.compare(WORLD_AND_MISC, JUST_WORLD) > 0); + } + + @ParameterizedTest + @MethodSource("all") + public void testOverallSizeAll(ImmutableContextSet other) { + if (other == SERVER_AND_WORLD_AND_MISC_2) return; + assertTrue(INSTANCE.compare(SERVER_AND_WORLD_AND_MISC_2, other) > 0); + } + + private static Stream testTransitivity() { + return Stream.of( + Arguments.of( + ImmutableContextSetImpl.of("a", "-"), + ImmutableContextSetImpl.of("b", "-"), + ImmutableContextSetImpl.of("c", "-") + ), + Arguments.of( + ImmutableContextSetImpl.of("-", "a"), + ImmutableContextSetImpl.of("-", "b"), + ImmutableContextSetImpl.of("-", "c") + ) + ); + } + + @ParameterizedTest + @MethodSource + public void testTransitivity(ImmutableContextSet a, ImmutableContextSet b, ImmutableContextSet c) { + List list = new ArrayList<>(); + list.add(a); + list.add(b); + list.add(c); + list.sort(INSTANCE); + + List reversed = new ArrayList<>(list); + reversed.sort(INSTANCE.reversed()); + + assertEquals(Lists.reverse(list), reversed); + } + +} diff --git a/common/src/test/java/me/lucko/luckperms/common/context/ContextSetJsonSerializerTest.java b/common/src/test/java/me/lucko/luckperms/common/context/ContextSetJsonSerializerTest.java new file mode 100644 index 000000000..3adb4c51d --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/context/ContextSetJsonSerializerTest.java @@ -0,0 +1,99 @@ +/* + * 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.context; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; + +import me.lucko.luckperms.common.context.serializer.ContextSetJsonSerializer; + +import net.luckperms.api.context.ContextSet; +import net.luckperms.api.context.ImmutableContextSet; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class ContextSetJsonSerializerTest { + + private static final Gson GSON = new Gson(); + + private static final ImmutableContextSet EXAMPLE_1 = new ImmutableContextSetImpl.BuilderImpl() + .add("server", "foo") + .add("world", "foo") + .add("foo", "foo") + .add("foo", "bar") + .build(); + + private static final ImmutableContextSet EXAMPLE_2 = new ImmutableContextSetImpl.BuilderImpl() + .add("cc", "foo") + .add("bb", "foo") + .add("aa", "foo") + .build(); + + @Test + public void testDeserialize() { + String string = "{\"foo\":[\"bar\",\"foo\"],\"server\":\"foo\",\"world\":[\"foo\"]}"; + ContextSet set = ContextSetJsonSerializer.deserialize(GSON, string); + assertEquals(EXAMPLE_1, set); + } + + @Test + public void testSerialize() { + JsonObject obj1 = ContextSetJsonSerializer.serialize(EXAMPLE_1); + assertEquals(3, obj1.size()); + assertEquals("{\"foo\":[\"bar\",\"foo\"],\"server\":\"foo\",\"world\":\"foo\"}", obj1.toString()); + + JsonObject obj2 = ContextSetJsonSerializer.serialize(EXAMPLE_2); + assertEquals(3, obj2.size()); + assertEquals("{\"aa\":\"foo\",\"bb\":\"foo\",\"cc\":\"foo\"}", obj2.toString()); + } + + @ParameterizedTest + @ValueSource(strings = { + "{}", + "{ }", + "" + }) + public void testDeserializeEmpty(String json) { + assertEquals(ImmutableContextSetImpl.EMPTY, ContextSetJsonSerializer.deserialize(GSON, json)); + } + + @ParameterizedTest + @ValueSource(strings = { + "null", + "[]", + "foo" + }) + public void testDeserializeThrows(String json) { + assertThrows(JsonParseException.class, () -> ContextSetJsonSerializer.deserialize(GSON, json)); + } + +} diff --git a/common/src/test/java/me/lucko/luckperms/common/dependencies/DependencyChecksumTest.java b/common/src/test/java/me/lucko/luckperms/common/dependencies/DependencyChecksumTest.java index b22a73c6f..4550a52c3 100644 --- a/common/src/test/java/me/lucko/luckperms/common/dependencies/DependencyChecksumTest.java +++ b/common/src/test/java/me/lucko/luckperms/common/dependencies/DependencyChecksumTest.java @@ -25,7 +25,6 @@ package me.lucko.luckperms.common.dependencies; -import org.junit.jupiter.api.Tag; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; @@ -37,7 +36,6 @@ public class DependencyChecksumTest { @ParameterizedTest @EnumSource - @Tag("dependency_checksum") public void checksumMatches(Dependency dependency) throws DependencyDownloadException { for (DependencyRepository repo : DependencyRepository.values()) { byte[] hash = Dependency.createDigest().digest(repo.downloadRaw(dependency)); diff --git a/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/LuckPermsApplication.java b/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/LuckPermsApplication.java index f872f3e94..27cfdd0f1 100644 --- a/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/LuckPermsApplication.java +++ b/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/LuckPermsApplication.java @@ -130,6 +130,14 @@ public class LuckPermsApplication implements AutoCloseable { this.healthReporter = healthReporter; } + public CommandExecutor getCommandExecutor() { + return this.commandExecutor; + } + + public HealthReporter getHealthReporter() { + return this.healthReporter; + } + public String getVersion() { return "@version@"; } diff --git a/standalone/build.gradle b/standalone/build.gradle index 499bd99d8..d1e0359f7 100644 --- a/standalone/build.gradle +++ b/standalone/build.gradle @@ -5,12 +5,25 @@ plugins { sourceCompatibility = 17 targetCompatibility = 17 +test { + useJUnitPlatform {} +} + dependencies { implementation project(':common') compileOnly project(':common:loader-utils') compileOnly project(':standalone:app') compileOnly 'org.spongepowered:configurate-yaml:3.7.2' + + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.1' + testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.9.1' + testImplementation 'org.junit.jupiter:junit-jupiter-params:5.9.1' + testImplementation 'org.mockito:mockito-core:4.11.0' + testImplementation 'org.mockito:mockito-junit-jupiter:4.11.0' + testImplementation 'com.h2database:h2:2.1.214' + testImplementation project(':standalone:app') + testImplementation project(':common:loader-utils') } shadowJar { diff --git a/standalone/src/main/java/me/lucko/luckperms/standalone/LPStandaloneBootstrap.java b/standalone/src/main/java/me/lucko/luckperms/standalone/LPStandaloneBootstrap.java index 3f19d41c1..1aa76dc84 100644 --- a/standalone/src/main/java/me/lucko/luckperms/standalone/LPStandaloneBootstrap.java +++ b/standalone/src/main/java/me/lucko/luckperms/standalone/LPStandaloneBootstrap.java @@ -36,6 +36,8 @@ import me.lucko.luckperms.standalone.app.LuckPermsApplication; import net.luckperms.api.platform.Platform; +import org.jetbrains.annotations.VisibleForTesting; + import java.nio.file.Path; import java.nio.file.Paths; import java.time.Instant; @@ -69,6 +71,21 @@ public class LPStandaloneBootstrap implements LuckPermsBootstrap, LoaderBootstra this.plugin = new LPStandalonePlugin(this); } + @VisibleForTesting + LPStandaloneBootstrap(LuckPermsApplication loader, ClassPathAppender classPathAppender) { + this.loader = loader; + + this.logger = new Log4jPluginLogger(LuckPermsApplication.LOGGER); + this.schedulerAdapter = new StandaloneSchedulerAdapter(this); + this.classPathAppender = classPathAppender; + this.plugin = createTestPlugin(); + } + + @VisibleForTesting + LPStandalonePlugin createTestPlugin() { + return new LPStandalonePlugin(this); + } + // provide adapters @Override diff --git a/standalone/src/main/java/me/lucko/luckperms/standalone/StandaloneDependencyPreloader.java b/standalone/src/main/java/me/lucko/luckperms/standalone/StandaloneDependencyPreloader.java index c38976d18..d2305f511 100644 --- a/standalone/src/main/java/me/lucko/luckperms/standalone/StandaloneDependencyPreloader.java +++ b/standalone/src/main/java/me/lucko/luckperms/standalone/StandaloneDependencyPreloader.java @@ -29,6 +29,7 @@ import com.google.common.util.concurrent.ThreadFactoryBuilder; import me.lucko.luckperms.common.dependencies.Dependency; import me.lucko.luckperms.common.dependencies.DependencyManager; +import me.lucko.luckperms.common.dependencies.DependencyManagerImpl; import me.lucko.luckperms.common.dependencies.relocation.RelocationHandler; import me.lucko.luckperms.common.util.MoreFiles; @@ -54,7 +55,7 @@ public class StandaloneDependencyPreloader { MoreFiles.createDirectoriesIfNotExists(cacheDirectory); ExecutorService executorService = Executors.newFixedThreadPool(8, new ThreadFactoryBuilder().setDaemon(true).build()); - DependencyManager dependencyManager = new DependencyManager(cacheDirectory, executorService); + DependencyManager dependencyManager = new DependencyManagerImpl(cacheDirectory, executorService); Set dependencies = new HashSet<>(Arrays.asList(Dependency.values())); System.out.println("Preloading " + dependencies.size() + " dependencies..."); diff --git a/standalone/src/test/java/me/lucko/luckperms/standalone/LPStandaloneTestBootstrap.java b/standalone/src/test/java/me/lucko/luckperms/standalone/LPStandaloneTestBootstrap.java new file mode 100644 index 000000000..23639f8b6 --- /dev/null +++ b/standalone/src/test/java/me/lucko/luckperms/standalone/LPStandaloneTestBootstrap.java @@ -0,0 +1,108 @@ +/* + * 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.standalone; + +import me.lucko.luckperms.common.dependencies.Dependency; +import me.lucko.luckperms.common.dependencies.DependencyManager; +import me.lucko.luckperms.common.plugin.classpath.ClassPathAppender; +import me.lucko.luckperms.common.storage.StorageType; +import me.lucko.luckperms.standalone.app.LuckPermsApplication; + +import java.nio.file.Path; +import java.util.Set; + +/** + * An extension standalone bootstrap for testing. + * + *

Key differences:

+ *

+ *

    + *
  • Dependency loading system is replaced with a no-op stub that delegates to the test classloader
  • + *
  • Translations aren't downloaded automatically
  • + *
+ *

+ */ +public final class LPStandaloneTestBootstrap extends LPStandaloneBootstrap { + private static final ClassPathAppender NOOP_APPENDER = file -> {}; + + private final Path dataDirectory; + private LPStandaloneTestPlugin plugin; + + LPStandaloneTestBootstrap(LuckPermsApplication app, Path dataDirectory) { + super(app, NOOP_APPENDER); + this.dataDirectory = dataDirectory; + } + + public LPStandaloneTestPlugin getPlugin() { + return this.plugin; + } + + @Override + public Path getDataDirectory() { + return this.dataDirectory; + } + + @Override + LPStandalonePlugin createTestPlugin() { + System.setProperty("luckperms.auto-install-translations", "false"); + this.plugin = new LPStandaloneTestPlugin(this); + return this.plugin; + } + + static final class LPStandaloneTestPlugin extends LPStandalonePlugin { + LPStandaloneTestPlugin(LPStandaloneBootstrap bootstrap) { + super(bootstrap); + } + + @Override + protected DependencyManager createDependencyManager() { + return new TestDependencyManager(); + } + } + + static final class TestDependencyManager implements DependencyManager { + + @Override + public void loadDependencies(Set dependencies) { + + } + + @Override + public void loadStorageDependencies(Set storageTypes, boolean redis, boolean rabbitmq, boolean nats) { + + } + + @Override + public ClassLoader obtainClassLoaderWith(Set dependencies) { + return getClass().getClassLoader(); + } + + @Override + public void close() { + + } + } +} diff --git a/standalone/src/test/java/me/lucko/luckperms/standalone/StandaloneIntegrationTests.java b/standalone/src/test/java/me/lucko/luckperms/standalone/StandaloneIntegrationTests.java new file mode 100644 index 000000000..28b48d4b2 --- /dev/null +++ b/standalone/src/test/java/me/lucko/luckperms/standalone/StandaloneIntegrationTests.java @@ -0,0 +1,122 @@ +/* + * 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.standalone; + +import me.lucko.luckperms.common.config.ConfigKeys; +import me.lucko.luckperms.common.model.Group; +import me.lucko.luckperms.common.node.types.Permission; +import me.lucko.luckperms.standalone.app.LuckPermsApplication; +import me.lucko.luckperms.standalone.app.integration.CommandExecutor; +import me.lucko.luckperms.standalone.app.integration.HealthReporter; + +import net.luckperms.api.model.data.DataType; +import net.luckperms.api.node.NodeEqualityPredicate; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * A set of 'integration tests' for the standalone LuckPerms app. + */ +public class StandaloneIntegrationTests { + + private @TempDir Path tempDir; + + @Test + public void testLoadEnableDisable() { + useTestPlugin((app, bootstrap, plugin) -> { + HealthReporter.Health health = app.getHealthReporter().poll(); + assertNotNull(health); + assertTrue(health.isUp()); + }); + } + + @Test + public void testRunCommand() { + useTestPlugin((app, bootstrap, plugin) -> { + CommandExecutor commandExecutor = app.getCommandExecutor(); + commandExecutor.execute("group default permission set test").join(); + + Group group = bootstrap.getPlugin().getStorage().loadGroup("default").join().orElse(null); + assertNotNull(group); + assertTrue(group.hasNode(DataType.NORMAL, Permission.builder().permission("test").build(), NodeEqualityPredicate.EXACT).asBoolean()); + }); + } + + @Test + public void testReloadConfig() throws IOException { + useTestPlugin((app, bootstrap, plugin) -> { + String server = plugin.getConfiguration().get(ConfigKeys.SERVER); + assertEquals("global", server); + + Integer syncTime = plugin.getConfiguration().get(ConfigKeys.SYNC_TIME); + assertEquals(-1, syncTime); + + Path config = this.tempDir.resolve("config.yml"); + assertTrue(Files.exists(config)); + + String configString = Files.readString(config) + .replace("server: global", "server: test") + .replace("sync-minutes: -1", "sync-minutes: 10"); + Files.writeString(config, configString); + + plugin.getConfiguration().reload(); + + server = plugin.getConfiguration().get(ConfigKeys.SERVER); + assertEquals("test", server); // changed + + syncTime = plugin.getConfiguration().get(ConfigKeys.SYNC_TIME); + assertEquals(-1, syncTime); // unchanged + }); + } + + private void useTestPlugin(TestPluginConsumer consumer) throws E { + LuckPermsApplication app = new LuckPermsApplication(() -> {}); + LPStandaloneTestBootstrap bootstrap = new LPStandaloneTestBootstrap(app, this.tempDir); + + bootstrap.onLoad(); + bootstrap.onEnable(); + + try { + consumer.accept(app, bootstrap, bootstrap.getPlugin()); + } finally { + bootstrap.onDisable(); + } + } + + interface TestPluginConsumer { + void accept(LuckPermsApplication app, LPStandaloneTestBootstrap bootstrap, LPStandalonePlugin plugin) throws E; + } + +} diff --git a/standalone/src/test/resources/log4j2.xml b/standalone/src/test/resources/log4j2.xml new file mode 100644 index 000000000..ffb070356 --- /dev/null +++ b/standalone/src/test/resources/log4j2.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file