Fix config lateinit errors and fix testing

This commit is contained in:
Norbi Peti 2023-06-26 23:03:19 +02:00
parent 5e39b6ef57
commit 529fef1c5f
No known key found for this signature in database
GPG key ID: DBA4C4549A927E56
8 changed files with 81 additions and 79 deletions

View file

@ -28,10 +28,13 @@ import org.bukkit.command.Command
import org.bukkit.command.CommandSender import org.bukkit.command.CommandSender
import org.bukkit.command.ConsoleCommandSender import org.bukkit.command.ConsoleCommandSender
import org.bukkit.entity.Player import org.bukkit.entity.Player
import org.bukkit.plugin.PluginDescriptionFile
import org.bukkit.plugin.java.JavaPluginLoader
import java.io.File
import java.util.* import java.util.*
import java.util.function.Supplier import java.util.function.Supplier
class MainPlugin : ButtonPlugin() { class MainPlugin : ButtonPlugin {
private var economy: Economy? = null private var economy: Economy? = null
/** /**
@ -62,6 +65,10 @@ class MainPlugin : ButtonPlugin() {
*/ // TODO: Combine the channel access test with command permissions (with a generic implementation) */ // TODO: Combine the channel access test with command permissions (with a generic implementation)
val externalPlayerPermissionGroup get() = iConfig.getData("externalPlayerPermissionGroup", "default") val externalPlayerPermissionGroup get() = iConfig.getData("externalPlayerPermissionGroup", "default")
constructor() : super()
constructor(loader: JavaPluginLoader, description: PluginDescriptionFile, dataFolder: File, file: File) : super(loader, description, dataFolder, file)
public override fun pluginEnable() { public override fun pluginEnable() {
instance = this instance = this
val pdf = description val pdf = description

View file

@ -1,50 +1,21 @@
package buttondevteam.core; package buttondevteam.core
import buttondevteam.core.component.channel.Channel; import be.seeseemelk.mockbukkit.MockBukkit
import buttondevteam.core.component.channel.ChannelComponent; import buttondevteam.core.component.channel.Channel
import buttondevteam.lib.ChromaUtils; import buttondevteam.core.component.channel.ChannelComponent
import buttondevteam.lib.architecture.Component; import buttondevteam.lib.ChromaUtils.isTest
import buttondevteam.lib.chat.Color; import buttondevteam.lib.architecture.Component.Companion.registerComponent
import buttondevteam.lib.chat.TBMCChatAPI; import buttondevteam.lib.chat.Color
import org.bukkit.Bukkit; import buttondevteam.lib.chat.TBMCChatAPI.registerChatChannel
import org.bukkit.Server; import org.bukkit.ChatColor
import org.bukkit.plugin.PluginManager;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.scheduler.BukkitScheduler;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import java.util.Collection; @Deprecated("Use MockBukkit")
import java.util.Collections; object TestPrepare {
import java.util.logging.Logger; @JvmStatic
fun prepareServer() {
public class TestPrepare { isTest = true //Needs to be in a separate class because of the potential lack of Mockito
MockBukkit.mock()
public static void PrepareServer() { registerComponent(MockBukkit.load(MainPlugin::class.java), ChannelComponent())
ChromaUtils.setTest(true); //Needs to be in a separate class because of the potential lack of Mockito registerChatChannel(Channel("${ChatColor.WHITE}g${ChatColor.WHITE}", Color.White, "g", null).also { Channel.globalChat = it })
Bukkit.setServer(Mockito.mock(Server.class, new Answer<Object>() { }
@Override
public Object answer(InvocationOnMock invocation) {
if (returns(invocation, String.class))
return "test";
if (returns(invocation, Logger.class))
return Logger.getAnonymousLogger();
if (returns(invocation, PluginManager.class))
return Mockito.mock(PluginManager.class);
if (returns(invocation, Collection.class))
return Collections.EMPTY_LIST;
if (returns(invocation, BukkitScheduler.class))
return Mockito.mock(BukkitScheduler.class);
return null;
}
boolean returns(InvocationOnMock invocation, Class<?> cl) {
return cl.isAssignableFrom(invocation.getMethod().getReturnType());
}
}));
Component.registerComponent(Mockito.mock(JavaPlugin.class), new ChannelComponent());
TBMCChatAPI.registerChatChannel(Channel.globalChat = new Channel("${ChatColor.WHITE}g${ChatColor.WHITE}", Color.White, "g", null));
}
} }

View file

@ -70,6 +70,17 @@ object ChromaUtils {
} else what.get() } else what.get()
} }
/**
* Log a warning message if the plugin is initialized. If not, just print a regular message.
*/
fun logWarn(message: String) {
if (MainPlugin.isInitialized) {
MainPlugin.instance.logger.warning(message)
} else {
println(message)
}
}
/** /**
* Returns true while unit testing. * Returns true while unit testing.
*/ */

View file

@ -8,14 +8,16 @@ import buttondevteam.lib.chat.ICommand2MC
import org.bukkit.configuration.InvalidConfigurationException import org.bukkit.configuration.InvalidConfigurationException
import org.bukkit.configuration.file.FileConfiguration import org.bukkit.configuration.file.FileConfiguration
import org.bukkit.configuration.file.YamlConfiguration import org.bukkit.configuration.file.YamlConfiguration
import org.bukkit.plugin.PluginDescriptionFile
import org.bukkit.plugin.java.JavaPlugin import org.bukkit.plugin.java.JavaPlugin
import org.bukkit.plugin.java.JavaPluginLoader
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.util.* import java.util.*
import java.util.function.Consumer import java.util.function.Consumer
@HasConfig(global = true) @HasConfig(global = true)
abstract class ButtonPlugin : JavaPlugin() { abstract class ButtonPlugin : JavaPlugin {
protected val iConfig = IHaveConfig(::saveConfig, section) protected val iConfig = IHaveConfig(::saveConfig, section)
private var yaml: YamlConfiguration? = null private var yaml: YamlConfiguration? = null
@ -31,6 +33,11 @@ abstract class ButtonPlugin : JavaPlugin() {
* Used to unregister components in the right order - and to reload configs * Used to unregister components in the right order - and to reload configs
*/ */
val componentStack = Stack<Component<*>>() val componentStack = Stack<Component<*>>()
// Support testing with the protected constructor (MockBukkit)
constructor() : super()
protected constructor(loader: JavaPluginLoader, description: PluginDescriptionFile, dataFolder: File, file: File) : super(loader, description, dataFolder, file)
protected abstract fun pluginEnable() protected abstract fun pluginEnable()
/** /**

View file

@ -19,7 +19,7 @@ import java.util.stream.Collectors
abstract class Component<TP : JavaPlugin> { abstract class Component<TP : JavaPlugin> {
var isEnabled = false var isEnabled = false
lateinit var config: IHaveConfig var config: IHaveConfig = IHaveConfig({ logWarn("Attempted to save config with null section!") }, null)
private set private set
lateinit var plugin: TP lateinit var plugin: TP
private set private set
@ -100,7 +100,11 @@ abstract class Component<TP : JavaPlugin> {
* @return A map containing configs * @return A map containing configs
*/ */
fun getConfigMap(key: String, defaultProvider: Map<String, Consumer<IHaveConfig>>): Map<String, IHaveConfig> { fun getConfigMap(key: String, defaultProvider: Map<String, Consumer<IHaveConfig>>): Map<String, IHaveConfig> {
val c: ConfigurationSection = config.config val c: ConfigurationSection? = config.config
if (c == null) {
logWarn("Config section is null when getting config map")
return defaultProvider.mapValues { kv -> IHaveConfig(plugin::saveConfig, null).also { kv.value.accept(it) } }
}
val cs = c.getConfigurationSection(key) ?: c.createSection(key) val cs = c.getConfigurationSection(key) ?: c.createSection(key)
val res = cs.getValues(false).entries.stream() val res = cs.getValues(false).entries.stream()
.filter { (_, value) -> value is ConfigurationSection } .filter { (_, value) -> value is ConfigurationSection }
@ -109,11 +113,7 @@ abstract class Component<TP : JavaPlugin> {
{ (_, value) -> IHaveConfig(plugin::saveConfig, value as ConfigurationSection) } { (_, value) -> IHaveConfig(plugin::saveConfig, value as ConfigurationSection) }
)) ))
if (res.isEmpty()) { if (res.isEmpty()) {
for ((key1, value) in defaultProvider) { defaultProvider.mapValuesTo(res) { kv -> IHaveConfig(plugin::saveConfig, cs.createSection(kv.key)).also { kv.value.accept(it) } }
val conf = IHaveConfig(plugin::saveConfig, cs.createSection(key1))
value.accept(conf)
res[key1] = conf
}
} }
return res return res
} }
@ -121,8 +121,7 @@ abstract class Component<TP : JavaPlugin> {
private val className: String get() = javaClass.simpleName private val className: String get() = javaClass.simpleName
internal fun updateConfig() { internal fun updateConfig() {
if (!this::config.isInitialized) this.config = IHaveConfig(plugin::saveConfig, getConfigSection()) this.config.reload(getConfigSection(), plugin::saveConfig)
else this.config.reload(getConfigSection())
} }
private fun getConfigSection(): ConfigurationSection { private fun getConfigSection(): ConfigurationSection {

View file

@ -14,16 +14,16 @@ import java.util.function.Function
* Use [Component.config] or [ButtonPlugin.iConfig] then [IHaveConfig.getData] to get an instance. * Use [Component.config] or [ButtonPlugin.iConfig] then [IHaveConfig.getData] to get an instance.
* *
* **Note:** The instance can become outdated if the config is reloaded. * **Note:** The instance can become outdated if the config is reloaded.
* @param config May be null for testing * @param config The config object to use for the whole file
* @param path The path to the config value * @param path The path to the config value
* @param primitiveDef The default value, as stored in the config. Non-nullable as it needs to be saved to the config * @param primitiveDef The default value, as stored in the config. Non-nullable as it needs to be saved sto the config
* @param getter Function to convert primtive types to [T]. The parameter is of a primitive type as returned by [Configuration.get] * @param getter Function to convert primtive types to [T]. The parameter is of a primitive type as returned by [Configuration.get]
* @param setter Function to convert [T] to a primitive type. The result should be a primitive type or string that can be retrieved correctly later * @param setter Function to convert [T] to a primitive type. The result should be a primitive type or string that can be retrieved correctly later
* @param readOnly If true, changing the value will have no effect * @param readOnly If true, changing the value will have no effect
* @param T The type of the config value. May be nullable if the getter cannot always return a value * @param T The type of the config value. May be nullable if the getter cannot always return a value
*/ */
class ConfigData<T : Any?> internal constructor( class ConfigData<T : Any?> internal constructor(
var config: IHaveConfig?, val config: IHaveConfig,
override val path: String, override val path: String,
private val primitiveDef: Any, private val primitiveDef: Any,
private val getter: Function<Any, T>, private val getter: Function<Any, T>,
@ -47,7 +47,7 @@ class ConfigData<T : Any?> internal constructor(
override fun get(): T { override fun get(): T {
val cachedValue = value val cachedValue = value
if (cachedValue != null) return cachedValue //Speed things up if (cachedValue != null) return cachedValue //Speed things up
val config = config?.config val config = config.config
val freshValue = config?.get(path) ?: primitiveDef.also { setInternal(it) } val freshValue = config?.get(path) ?: primitiveDef.also { setInternal(it) }
return getter.apply(convertPrimitiveType(freshValue)).also { value = it } return getter.apply(convertPrimitiveType(freshValue)).also { value = it }
} }
@ -76,9 +76,13 @@ class ConfigData<T : Any?> internal constructor(
} }
private fun setInternal(`val`: Any?) { private fun setInternal(`val`: Any?) {
val conf = config ?: return val config = config.config
conf.config.set(path, `val`) if (config != null) {
signalChange(conf) config.set(path, `val`)
signalChange(this.config)
} else {
ChromaUtils.logWarn("Attempted to get/set config value with no config! Path: $path, value: $`val`")
}
} }
/** /**
@ -95,13 +99,9 @@ class ConfigData<T : Any?> internal constructor(
fun signalChange(config: IHaveConfig) { fun signalChange(config: IHaveConfig) {
val cc = config.config val cc = config.config
val sa = config.saveAction val sa = config.saveAction
val root = cc.root val root = cc?.root
if (root == null) { if (root == null) {
if (MainPlugin.isInitialized) { ChromaUtils.logWarn("Attempted to save config with no root! Name: ${cc?.name ?: "NONEXISTENT CONFIG"}")
MainPlugin.instance.logger.warning("Attempted to save config with no root! Name: ${config.config.name}")
} else {
println("Attempted to save config with no root! Name: ${config.config.name}")
}
return return
} }
if (!MainPlugin.isInitialized) { if (!MainPlugin.isInitialized) {

View file

@ -15,12 +15,20 @@ class IHaveConfig(
/** /**
* The way the underlying configuration gets saved to disk * The way the underlying configuration gets saved to disk
*/ */
val saveAction: Runnable, saveAction: Runnable,
config: ConfigurationSection?
) {
/**
* The way the underlying configuration gets saved to disk
*/
var saveAction = saveAction
private set
/** /**
* Returns the Bukkit ConfigurationSection. Use [.signalChange] after changing it. * Returns the Bukkit ConfigurationSection. Use [.signalChange] after changing it.
*/ */
var config: ConfigurationSection var config = config
) { private set
private val datamap = HashMap<String, IConfigData<*>>() private val datamap = HashMap<String, IConfigData<*>>()
/** /**
@ -110,8 +118,9 @@ class IHaveConfig(
ConfigData.signalChange(this) ConfigData.signalChange(this)
} }
fun reload(section: ConfigurationSection) { fun reload(section: ConfigurationSection, saveAction: Runnable = this.saveAction) {
config = section config = section
this.saveAction = saveAction
datamap.forEach { it.value.reload() } datamap.forEach { it.value.reload() }
} }

View file

@ -7,7 +7,7 @@ import java.util.function.Predicate
import java.util.function.UnaryOperator import java.util.function.UnaryOperator
class ListConfigData<T> internal constructor( class ListConfigData<T> internal constructor(
config: IHaveConfig?, config: IHaveConfig,
path: String, path: String,
primitiveDef: ArrayList<*>, primitiveDef: ArrayList<*>,
private val elementGetter: Function<Any?, T>, private val elementGetter: Function<Any?, T>,
@ -27,9 +27,7 @@ class ListConfigData<T> internal constructor(
override val size: Int get() = primitiveList.size override val size: Int get() = primitiveList.size
private fun update() { private fun update() {
val config = listConfig.config val config = listConfig.config
if (config != null) { ConfigData.signalChange(config) //Update the config model and start save task if needed
ConfigData.signalChange(config) //Update the config model and start save task if needed
}
} }
override fun set(index: Int, element: T): T { override fun set(index: Int, element: T): T {