Rework arena sign data store.

This commit constitutes a major rewrite of how arena signs are stored
and loaded. It fixes an issue where an unloaded or missing world would
cause MobArena to throw errors if there were any arena signs recorded in
said world.

The solution is to load signs of a given world when it is available,
rather than loading all signs at once indiscriminately. At startup,
signs for all _currently available_ worlds are loaded. This fixes the
errors. When a world loads and a WorldLoadEvent is fired, signs in that
world are loaded. This ensures that all valid signs in existing worlds
will _eventually_ load. To keep things tidy, a WorldUnloadEvent will
unload all signs for the unloaded world.

Bukkit's own YAML deserialization implementation doesn't re-throw all
deserialization errors, which means we can't actually catch the problem
of missing worlds without doing an awkward "scan" of the deserialized
objects. This prompted a rewrite of the serialization and data storage
into a custom CSV-like format which is both simpler and also provides a
lot more control over the process. Instead of relying on world _names_,
the new format uses world _UUIDs_. While the rest of the plugin won't
necessarily adapt well to a world being renamed, the signs data store
should be resilient enough to handle it.

Most of the actual sign rendering code and almost all of the template
code is intact, but quite a lot of other classes have been rewritten or
replaced. Some of the rewrites weren't strictly necessary, but because
the components were already fairly small, rewrites were much faster and
a lot less awkward than attempting to adapt existing code.

Fixes #645
This commit is contained in:
Andreas Troelsen 2021-03-25 10:20:49 +01:00
parent cf296704b0
commit 0da90f3963
48 changed files with 2714 additions and 1015 deletions

View File

@ -11,6 +11,8 @@ These changes will (most likely) be included in the next version.
## [Unreleased]
### Fixed
- Arena signs in unloaded or missing worlds no longer break the startup procedure. Sign data is stored in a new format that MobArena will automatically migrate to on a per-world basis during startup.
## [0.105] - 2020-11-08
### Minor breaking changes

View File

@ -13,7 +13,6 @@ import com.garbagemule.MobArena.metrics.IsolatedChatChart;
import com.garbagemule.MobArena.metrics.MonsterInfightChart;
import com.garbagemule.MobArena.metrics.PvpEnabledChart;
import com.garbagemule.MobArena.metrics.VaultChart;
import com.garbagemule.MobArena.signs.ArenaSign;
import com.garbagemule.MobArena.signs.SignBootstrap;
import com.garbagemule.MobArena.signs.SignListeners;
import com.garbagemule.MobArena.things.NothingPickerParser;
@ -29,7 +28,6 @@ import org.bukkit.ChatColor;
import org.bukkit.configuration.InvalidConfigurationException;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.configuration.serialization.ConfigurationSerialization;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.PluginManager;
import org.bukkit.plugin.RegisteredServiceProvider;
@ -94,7 +92,6 @@ public class MobArena extends JavaPlugin
arenaMaster = null;
}
loadsConfigFile = null;
ConfigurationSerialization.unregisterClass(ArenaSign.class);
}
private void setup() {
@ -103,7 +100,6 @@ public class MobArena extends JavaPlugin
setupArenaMaster();
setupCommandHandler();
registerConfigurationSerializers();
setupVault();
setupBossAbilities();
setupListeners();
@ -133,10 +129,6 @@ public class MobArena extends JavaPlugin
getCommand("ma").setExecutor(new CommandHandler(this));
}
private void registerConfigurationSerializers() {
ConfigurationSerialization.registerClass(ArenaSign.class);
}
private void setupVault() {
Plugin vaultPlugin = this.getServer().getPluginManager().getPlugin("Vault");
if (vaultPlugin == null) {

View File

@ -1,12 +1,8 @@
package com.garbagemule.MobArena.signs;
import org.bukkit.Location;
import org.bukkit.configuration.serialization.ConfigurationSerializable;
import java.util.HashMap;
import java.util.Map;
public class ArenaSign implements ConfigurationSerializable {
class ArenaSign {
final Location location;
final String templateId;
@ -20,28 +16,4 @@ public class ArenaSign implements ConfigurationSerializable {
this.type = type;
}
@Override
public Map<String, Object> serialize() {
Map<String, Object> result = new HashMap<>();
result.put("location", location);
result.put("templateId", templateId);
result.put("arenaId", arenaId);
result.put("type", type);
return result;
}
@SuppressWarnings("WeakerAccess")
public static ArenaSign deserialize(Map<String, Object> map) {
try {
Location location = (Location) map.get("location");
String templateId = (String) map.get("templateId");
String arenaId = (String) map.get("arenaId");
String type = (String) map.get("type");
return new ArenaSign(location, templateId, arenaId, type);
} catch (ClassCastException e) {
String msg = "An arena sign in " + SignStore.FILENAME + " is invalid! You may have to delete the file.";
throw new IllegalStateException(msg);
}
}
}

View File

@ -1,24 +1,35 @@
package com.garbagemule.MobArena.signs;
import com.garbagemule.MobArena.MobArena;
import com.garbagemule.MobArena.events.*;
import com.garbagemule.MobArena.events.ArenaEndEvent;
import com.garbagemule.MobArena.events.ArenaPlayerDeathEvent;
import com.garbagemule.MobArena.events.ArenaPlayerJoinEvent;
import com.garbagemule.MobArena.events.ArenaPlayerLeaveEvent;
import com.garbagemule.MobArena.events.ArenaPlayerReadyEvent;
import com.garbagemule.MobArena.events.ArenaStartEvent;
import com.garbagemule.MobArena.events.NewWaveEvent;
import com.garbagemule.MobArena.framework.Arena;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.scheduler.BukkitScheduler;
class RedrawsSignsOnUpdates implements Listener {
import java.util.List;
private final RedrawsArenaSigns redrawsArenaSigns;
class HandlesArenaUpdates implements Listener {
private final SignStore signStore;
private final SignRenderer signRenderer;
private final BukkitScheduler scheduler;
private final MobArena plugin;
RedrawsSignsOnUpdates(
RedrawsArenaSigns redrawsArenaSigns,
HandlesArenaUpdates(
SignStore signStore,
SignRenderer signRenderer,
MobArena plugin
) {
this.redrawsArenaSigns = redrawsArenaSigns;
this.signStore = signStore;
this.signRenderer = signRenderer;
this.scheduler = plugin.getServer().getScheduler();
this.plugin = plugin;
}
@ -59,7 +70,10 @@ class RedrawsSignsOnUpdates implements Listener {
}
private void handle(Arena arena) {
scheduler.runTask(plugin, () -> redrawsArenaSigns.redraw(arena));
scheduler.runTask(plugin, () -> {
List<ArenaSign> signs = signStore.findByArenaId(arena.configName());
signs.forEach(signRenderer::render);
});
}
}

View File

@ -20,7 +20,10 @@ class HandlesSignClicks implements Listener {
private final InvokesSignAction invokesSignAction;
private final Map<UUID, Long> cooldowns;
HandlesSignClicks(SignStore signStore, InvokesSignAction invokesSignAction) {
HandlesSignClicks(
SignStore signStore,
InvokesSignAction invokesSignAction
) {
this.signStore = signStore;
this.invokesSignAction = invokesSignAction;
this.cooldowns = new HashMap<>();
@ -35,8 +38,11 @@ class HandlesSignClicks implements Listener {
if (!(block.getState() instanceof Sign)) {
return;
}
signStore.findByLocation(block.getLocation())
.ifPresent(sign -> purgeAndInvoke(sign, event.getPlayer()));
ArenaSign sign = signStore.findByLocation(block.getLocation());
if (sign != null) {
purgeAndInvoke(sign, event.getPlayer());
}
}
private void purgeAndInvoke(ArenaSign sign, Player player) {

View File

@ -1,65 +1,79 @@
package com.garbagemule.MobArena.signs;
import com.garbagemule.MobArena.Messenger;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.bukkit.ChatColor;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.block.SignChangeEvent;
import java.util.stream.IntStream;
import java.util.logging.Level;
import java.util.logging.Logger;
class HandlesSignCreation implements Listener {
private final StoresNewSign storesNewSign;
private final RendersTemplateById rendersTemplate;
private final SignCreator creator;
private final SignWriter writer;
private final SignStore store;
private final SignRenderer renderer;
private final Messenger messenger;
private final Logger log;
HandlesSignCreation(
StoresNewSign storesNewSign,
RendersTemplateById rendersTemplate,
Messenger messenger
SignCreator creator,
SignWriter writer,
SignStore store,
SignRenderer renderer,
Messenger messenger,
Logger log
) {
this.storesNewSign = storesNewSign;
this.rendersTemplate = rendersTemplate;
this.creator = creator;
this.writer = writer;
this.store = store;
this.renderer = renderer;
this.messenger = messenger;
this.log = log;
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void on(SignChangeEvent event) {
if (!trim(event, 0).equalsIgnoreCase("[MA]")) {
ArenaSign sign;
try {
sign = creator.create(event);
if (sign == null) {
return;
}
} catch (IllegalArgumentException e) {
messenger.tell(event.getPlayer(), e.getMessage());
return;
}
Location location = event.getBlock().getLocation();
String arenaId = trim(event, 1);
String signType = trim(event, 2).toLowerCase();
String templateId = trim(event, 3).toLowerCase();
if (templateId.isEmpty()) {
templateId = signType;
}
Player player = event.getPlayer();
try {
storesNewSign.store(location, arenaId, templateId, signType);
messenger.tell(player, "New " + signType + " sign created for arena " + arenaId);
String[] lines = rendersTemplate.render(templateId, arenaId);
IntStream.range(0, 4)
.forEach(i -> event.setLine(i, lines[i]));
} catch (IllegalArgumentException e) {
messenger.tell(player, e.getMessage());
writer.write(sign);
} catch (Exception e) {
messenger.tell(event.getPlayer(), "Sign creation failed:\n" + ChatColor.RED + e.getMessage());
log.log(Level.SEVERE, "Failed to write arena sign to data file", e);
return;
}
}
private String trim(SignChangeEvent event, int index) {
String line = event.getLine(index);
if (line == null) {
return "";
}
return line.trim();
store.add(sign);
renderer.render(sign, event);
messenger.tell(event.getPlayer(), String.format(
"New %s sign created for arena %s.",
ChatColor.YELLOW + sign.type + ChatColor.RESET,
ChatColor.GREEN + sign.arenaId + ChatColor.RESET
));
log.info(String.format(
"%s created %s sign for '%s' at (%d,%d,%d) in '%s'.",
event.getPlayer().getName(),
sign.type,
sign.arenaId,
sign.location.getBlockX(),
sign.location.getBlockY(),
sign.location.getBlockZ(),
sign.location.getWorld().getName()
));
}
}

View File

@ -1,33 +1,67 @@
package com.garbagemule.MobArena.signs;
import com.garbagemule.MobArena.Messenger;
import org.bukkit.ChatColor;
import org.bukkit.Location;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockBreakEvent;
import java.util.logging.Level;
import java.util.logging.Logger;
class HandlesSignDestruction implements Listener {
private final RemovesSignAtLocation removesSignAtLocation;
private final SignStore store;
private final SignWriter writer;
private final Messenger messenger;
private final Logger log;
HandlesSignDestruction(
RemovesSignAtLocation removesSignAtLocation,
Messenger messenger
SignStore store,
SignWriter writer,
Messenger messenger,
Logger log
) {
this.removesSignAtLocation = removesSignAtLocation;
this.store = store;
this.writer = writer;
this.messenger = messenger;
this.log = log;
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void on(BlockBreakEvent event) {
Location location = event.getBlock().getLocation();
removesSignAtLocation.remove(location)
.ifPresent(sign -> messenger.tell(
event.getPlayer(),
"Removed " + sign.type + " sign for arena " + sign.arenaId
));
ArenaSign sign = store.removeByLocation(location);
if (sign == null) {
return;
}
try {
writer.erase(sign);
} catch (Exception e) {
messenger.tell(event.getPlayer(), "Sign destruction failed:\n" + ChatColor.RED + e.getMessage());
log.log(Level.SEVERE, "Failed to erase arena sign from data file", e);
return;
}
messenger.tell(event.getPlayer(), String.format(
"Removed %s sign for arena %s.",
ChatColor.YELLOW + sign.type + ChatColor.RESET,
ChatColor.GREEN + sign.arenaId + ChatColor.RESET
));
log.info(String.format(
"%s destroyed %s sign for '%s' at (%d,%d,%d) in '%s'.",
event.getPlayer().getName(),
sign.type,
sign.arenaId,
sign.location.getBlockX(),
sign.location.getBlockY(),
sign.location.getBlockZ(),
sign.location.getWorld().getName()
));
}
}

View File

@ -0,0 +1,57 @@
package com.garbagemule.MobArena.signs;
import org.bukkit.World;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.world.WorldLoadEvent;
import java.io.IOException;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
class HandlesWorldLoad implements Listener {
private final SignDataMigrator migrator;
private final SignReader reader;
private final SignStore store;
private final Logger log;
HandlesWorldLoad(
SignDataMigrator migrator,
SignReader reader,
SignStore store,
Logger log
) {
this.migrator = migrator;
this.reader = reader;
this.store = store;
this.log = log;
}
@EventHandler(priority = EventPriority.MONITOR)
public void on(WorldLoadEvent event) {
World world = event.getWorld();
try {
migrator.migrate(world);
} catch (IOException e) {
log.log(Level.SEVERE, "Failed to migrate sign data for world '" + world.getName() + "'", e);
}
List<ArenaSign> loaded;
try {
loaded = reader.read(world);
} catch (IOException e) {
log.log(Level.SEVERE, "Failed to read from arena sign data file", e);
return;
}
if (!loaded.isEmpty()) {
loaded.forEach(store::add);
log.info(loaded.size() + " arena sign(s) loaded due to loading of world '" + world.getName() + "' (" + world.getUID().toString() + ").");
}
}
}

View File

@ -0,0 +1,34 @@
package com.garbagemule.MobArena.signs;
import org.bukkit.World;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.world.WorldUnloadEvent;
import java.util.List;
import java.util.logging.Logger;
class HandlesWorldUnload implements Listener {
private final SignStore store;
private final Logger log;
HandlesWorldUnload(
SignStore store,
Logger log
) {
this.store = store;
this.log = log;
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void on(WorldUnloadEvent event) {
World world = event.getWorld();
List<ArenaSign> removed = store.removeByWorld(world);
if (!removed.isEmpty()) {
log.info(removed.size() + " arena sign(s) unloaded due to unloading of world '" + world.getName() + "' (" + world.getUID().toString() + ").");
}
}
}

View File

@ -1,46 +0,0 @@
package com.garbagemule.MobArena.signs;
import com.garbagemule.MobArena.MobArena;
import org.bukkit.configuration.InvalidConfigurationException;
import org.bukkit.configuration.file.YamlConfiguration;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
class LoadsSignStore {
private final MobArena plugin;
LoadsSignStore(MobArena plugin) {
this.plugin = plugin;
}
SignStore load() {
YamlConfiguration yaml = new YamlConfiguration();
try {
File data = new File(plugin.getDataFolder(), "data");
yaml.load(new File(data, SignStore.FILENAME));
} catch (FileNotFoundException e) {
return new SignStore(Collections.emptyList());
} catch (InvalidConfigurationException e) {
String msg = SignStore.FILENAME + " is invalid! You may have to delete it.";
throw new IllegalStateException(msg, e);
} catch (IOException e) {
throw new IllegalStateException(e);
}
List<ArenaSign> signs = yaml.getList("signs").stream()
.filter(raw -> raw instanceof ArenaSign)
.map(raw -> (ArenaSign) raw)
.collect(Collectors.toList());
plugin.getLogger().info("Loaded " + signs.size() + " arena signs.");
return new SignStore(signs);
}
}

View File

@ -13,13 +13,7 @@ import java.util.Map;
class LoadsTemplateStore {
private final MobArena plugin;
LoadsTemplateStore(MobArena plugin) {
this.plugin = plugin;
}
TemplateStore read() {
static TemplateStore load(MobArena plugin) {
YamlConfiguration yaml = new YamlConfiguration();
try {
File file = new File(plugin.getDataFolder(), TemplateStore.FILENAME);
@ -48,7 +42,7 @@ class LoadsTemplateStore {
return new TemplateStore(map);
}
private void validateTemplateNode(String key, YamlConfiguration yaml) {
private static void validateTemplateNode(String key, YamlConfiguration yaml) {
List<?> list = yaml.getList(key);
if (list == null) {
String msg = "Template " + key + " in " + TemplateStore.FILENAME + " is not a list!";
@ -62,21 +56,21 @@ class LoadsTemplateStore {
});
}
private String stripStateSuffix(String id) {
private static String stripStateSuffix(String id) {
if (hasStateSuffix(id)) {
return id.split("-", -1)[0];
}
return id;
}
private boolean hasStateSuffix(String id) {
private static boolean hasStateSuffix(String id) {
return id.endsWith("-idle")
|| id.endsWith("-joining")
|| id.endsWith("-ready")
|| id.endsWith("-running");
}
private Template loadTemplate(String id, YamlConfiguration yaml) {
private static Template loadTemplate(String id, YamlConfiguration yaml) {
String[] base = getLines(yaml, id);
String[] idle = getLines(yaml, id + "-idle");
String[] joining = getLines(yaml, id + "-joining");
@ -92,7 +86,7 @@ class LoadsTemplateStore {
.build();
}
private String[] getLines(YamlConfiguration config, String id) {
private static String[] getLines(YamlConfiguration config, String id) {
List<String> list = config.getStringList(id);
if (list.isEmpty()) {
return null;

View File

@ -1,61 +0,0 @@
package com.garbagemule.MobArena.signs;
import com.garbagemule.MobArena.framework.Arena;
import org.bukkit.ChatColor;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
class RedrawsArenaSigns {
private final SignStore signStore;
private final TemplateStore templateStore;
private final RendersTemplate rendersTemplate;
private final SetsLines setsSignLines;
RedrawsArenaSigns(
SignStore signStore,
TemplateStore templateStore,
RendersTemplate rendersTemplate,
SetsLines setsSignLines
) {
this.signStore = signStore;
this.templateStore = templateStore;
this.rendersTemplate = rendersTemplate;
this.setsSignLines = setsSignLines;
}
void redraw(Arena arena) {
List<ArenaSign> signs = signStore.findByArenaId(arena.configName());
Map<String, String[]> rendered = signs.stream()
.map(sign -> sign.templateId)
.distinct()
.collect(Collectors.toMap(
templateId -> templateId,
templateId -> render(templateId, arena)
));
signs.forEach(sign -> setsSignLines.set(
sign.location,
rendered.get(sign.templateId)
));
}
private String[] render(String templateId, Arena arena) {
return templateStore.findById(templateId)
.map(template -> rendersTemplate.render(template, arena))
.orElseGet(() -> notFound(templateId));
}
private static String[] notFound(String templateId) {
return new String[]{
String.join(ChatColor.MAGIC + "b" + ChatColor.RESET, "BROKEN".split("")),
"Template",
ChatColor.BOLD + templateId,
"not found! :("
};
}
}

View File

@ -1,26 +0,0 @@
package com.garbagemule.MobArena.signs;
import org.bukkit.Location;
import java.util.Optional;
class RemovesSignAtLocation {
private final SignStore signStore;
private final SavesSignStore savesSignStore;
RemovesSignAtLocation(
SignStore signStore,
SavesSignStore savesSignStore
) {
this.signStore = signStore;
this.savesSignStore = savesSignStore;
}
Optional<ArenaSign> remove(Location location) {
Optional<ArenaSign> sign = signStore.remove(location);
sign.ifPresent(s -> savesSignStore.save(signStore));
return sign;
}
}

View File

@ -1,7 +1,5 @@
package com.garbagemule.MobArena.signs;
import static java.lang.String.valueOf;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
@ -13,6 +11,8 @@ import com.garbagemule.MobArena.framework.Arena;
import org.bukkit.ChatColor;
import org.bukkit.entity.Player;
import static java.lang.String.valueOf;
class RendersTemplate {
// Regex Pattern for player list variables.

View File

@ -1,36 +0,0 @@
package com.garbagemule.MobArena.signs;
import com.garbagemule.MobArena.framework.Arena;
import com.garbagemule.MobArena.framework.ArenaMaster;
class RendersTemplateById {
private final ArenaMaster arenaMaster;
private final TemplateStore templateStore;
private final RendersTemplate rendersTemplate;
RendersTemplateById(
ArenaMaster arenaMaster,
TemplateStore templateStore,
RendersTemplate rendersTemplate
) {
this.arenaMaster = arenaMaster;
this.templateStore = templateStore;
this.rendersTemplate = rendersTemplate;
}
String[] render(String templateId, String arenaId) {
Template template = templateStore.findById(templateId)
.orElseThrow(() -> new IllegalArgumentException(
"Template " + templateId + " not found"
));
Arena arena = arenaMaster.getArenaWithName(arenaId);
if (arena == null) {
throw new IllegalStateException("Arena " + arenaId + " not found");
}
return rendersTemplate.render(template, arena);
}
}

View File

@ -1,32 +0,0 @@
package com.garbagemule.MobArena.signs;
import com.garbagemule.MobArena.MobArena;
import org.bukkit.configuration.file.YamlConfiguration;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
class SavesSignStore {
private final MobArena plugin;
SavesSignStore(MobArena plugin) {
this.plugin = plugin;
}
void save(SignStore signStore) {
YamlConfiguration yaml = new YamlConfiguration();
List<ArenaSign> values = new ArrayList<>(signStore.findAll());
yaml.set("signs", values);
try {
File data = new File(plugin.getDataFolder(), "data");
yaml.options().header("MobArena Sign Store\n\nPlease DON'T edit this file by hand!\n");
yaml.save(new File(data, SignStore.FILENAME));
} catch (IOException e) {
throw new IllegalStateException(e);
}
}
}

View File

@ -1,22 +0,0 @@
package com.garbagemule.MobArena.signs;
import org.bukkit.Location;
import org.bukkit.block.BlockState;
import org.bukkit.block.Sign;
class SetsLines {
void set(Location location, String[] lines) {
BlockState state = location.getBlock().getState();
if (!(state instanceof Sign)) {
return;
}
Sign sign = (Sign) state;
for (int i = 0; i < lines.length; i++) {
sign.setLine(i, lines[i]);
}
sign.update();
}
}

View File

@ -1,42 +1,54 @@
package com.garbagemule.MobArena.signs;
import com.garbagemule.MobArena.MobArena;
import org.bukkit.World;
import org.yaml.snakeyaml.Yaml;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
@SuppressWarnings("WeakerAccess")
public class SignBootstrap {
private final MobArena plugin;
private final SignStore signStore;
private final TemplateStore templateStore;
private InvokesSignAction invokesSignAction;
private RedrawsArenaSigns redrawsArenaSigns;
private RemovesSignAtLocation removesSignAtLocation;
private RendersTemplate rendersTemplate;
private RendersTemplateById rendersTemplateById;
private SetsLines setsLines;
private StoresNewSign storesNewSign;
private SavesSignStore savesSignStore;
private SignCreator signCreator;
private SignDataMigrator signDataMigrator;
private SignFile signFile;
private SignReader signReader;
private SignRenderer signRenderer;
private SignSerializer signSerializer;
private SignStore signStore;
private SignWriter signWriter;
private TemplateStore templateStore;
private SignBootstrap(
MobArena plugin,
SignStore signStore,
TemplateStore templateStore
) {
private SignBootstrap(MobArena plugin) {
this.plugin = plugin;
this.signStore = signStore;
this.templateStore = templateStore;
}
MobArena getPlugin() {
return plugin;
}
SignStore getSignStore() {
return signStore;
SignFile getSignFile() {
if (signFile == null) {
Path root = plugin.getDataFolder().toPath();
Path data = root.resolve("data");
Path file = data.resolve("signs.csv");
signFile = new SignFile(file);
}
return signFile;
}
TemplateStore getTemplateStore() {
if (templateStore == null) {
templateStore = LoadsTemplateStore.load(plugin);
}
return templateStore;
}
@ -50,28 +62,6 @@ public class SignBootstrap {
return invokesSignAction;
}
RedrawsArenaSigns getRedrawsArenaSigns() {
if (redrawsArenaSigns == null) {
redrawsArenaSigns = new RedrawsArenaSigns(
getSignStore(),
getTemplateStore(),
getRendersTemplate(),
getSetsLines()
);
}
return redrawsArenaSigns;
}
RemovesSignAtLocation getRemovesSignAtLocation() {
if (removesSignAtLocation == null) {
removesSignAtLocation = new RemovesSignAtLocation(
getSignStore(),
getSavesSignStore()
);
}
return removesSignAtLocation;
}
RendersTemplate getRendersTemplate() {
if (rendersTemplate == null) {
rendersTemplate = new RendersTemplate();
@ -79,52 +69,140 @@ public class SignBootstrap {
return rendersTemplate;
}
RendersTemplateById getRendersTemplateById() {
if (rendersTemplateById == null) {
rendersTemplateById = new RendersTemplateById(
SignCreator getSignCreator() {
if (signCreator == null) {
signCreator = new SignCreator(
plugin.getArenaMaster(),
getTemplateStore()
);
}
return signCreator;
}
SignDataMigrator getSignDataMigrator() {
if (signDataMigrator == null) {
Path root = plugin.getDataFolder().toPath();
Path data = root.resolve("data");
Path legacyFile = data.resolve("signs.data");
Path pendingFile = data.resolve("signs.tmp");
signDataMigrator = new SignDataMigrator(
legacyFile,
pendingFile,
new Yaml(),
getSignFile(),
plugin.getLogger()
);
}
return signDataMigrator;
}
SignReader getSignReader() {
if (signReader == null) {
signReader = new SignReader(
getSignFile(),
getSignSerializer(),
plugin.getLogger()
);
}
return signReader;
}
SignRenderer getSignRenderer() {
if (signRenderer == null) {
signRenderer = new SignRenderer(
getTemplateStore(),
plugin.getArenaMaster(),
getRendersTemplate()
);
}
return rendersTemplateById;
return signRenderer;
}
SetsLines getSetsLines() {
if (setsLines == null) {
setsLines = new SetsLines();
SignSerializer getSignSerializer() {
if (signSerializer == null) {
signSerializer = new SignSerializer();
}
return setsLines;
return signSerializer;
}
StoresNewSign getStoresNewSign() {
if (storesNewSign == null) {
storesNewSign = new StoresNewSign(
plugin.getArenaMaster(),
getTemplateStore(),
getSignStore(),
getSavesSignStore()
SignStore getSignStore() {
if (signStore == null) {
signStore = new SignStore();
}
return signStore;
}
SignWriter getSignWriter() {
if (signWriter == null) {
signWriter = new SignWriter(
getSignFile(),
getSignSerializer(),
plugin.getLogger()
);
}
return storesNewSign;
}
SavesSignStore getSavesSignStore() {
if (savesSignStore == null) {
savesSignStore = new SavesSignStore(plugin);
}
return savesSignStore;
return signWriter;
}
public static SignBootstrap create(MobArena plugin) {
TemplateStore templateStore = new LoadsTemplateStore(plugin).read();
SignStore signStore = new LoadsSignStore(plugin).load();
SignBootstrap bootstrap = new SignBootstrap(plugin);
SignBootstrap bootstrap = new SignBootstrap(plugin, signStore, templateStore);
plugin.getArenaMaster().getArenas()
.forEach(bootstrap.getRedrawsArenaSigns()::redraw);
migrateData(bootstrap);
loadSigns(bootstrap);
initialRender(bootstrap);
return bootstrap;
}
private static void migrateData(SignBootstrap bootstrap) {
SignDataMigrator migrator = bootstrap.getSignDataMigrator();
MobArena plugin = bootstrap.getPlugin();
Logger log = plugin.getLogger();
try {
migrator.init();
} catch (IOException e) {
log.log(Level.SEVERE, "Failed initial sign data migration step", e);
return;
}
for (World world : plugin.getServer().getWorlds()) {
try {
migrator.migrate(world);
} catch (IOException e) {
log.log(Level.SEVERE, "Failed to migrate sign data for world '" + world.getName() + "'", e);
}
}
}
private static void loadSigns(SignBootstrap bootstrap) {
SignReader reader = bootstrap.getSignReader();
SignStore store = bootstrap.getSignStore();
MobArena plugin = bootstrap.getPlugin();
Logger log = plugin.getLogger();
List<ArenaSign> loaded = new ArrayList<>();
try {
for (World world : plugin.getServer().getWorlds()) {
loaded.addAll(reader.read(world));
}
} catch (IOException e) {
log.log(Level.SEVERE, "Failed to read from arena sign data file", e);
}
if (!loaded.isEmpty()) {
loaded.forEach(store::add);
log.info(loaded.size() + " arena sign(s) loaded.");
}
}
private static void initialRender(SignBootstrap bootstrap) {
SignStore store = bootstrap.getSignStore();
SignRenderer renderer = bootstrap.getSignRenderer();
bootstrap.getPlugin().getArenaMaster().getArenas().forEach(arena -> {
List<ArenaSign> signs = store.findByArenaId(arena.configName());
signs.forEach(renderer::render);
});
}
}

View File

@ -0,0 +1,85 @@
package com.garbagemule.MobArena.signs;
import com.garbagemule.MobArena.framework.Arena;
import com.garbagemule.MobArena.framework.ArenaMaster;
import org.bukkit.Location;
import org.bukkit.event.block.SignChangeEvent;
class SignCreator {
private final ArenaMaster arenaMaster;
private final TemplateStore templateStore;
SignCreator(
ArenaMaster arenaMaster,
TemplateStore templateStore
) {
this.arenaMaster = arenaMaster;
this.templateStore = templateStore;
}
ArenaSign create(SignChangeEvent event) {
if (!trim(event, 0).equalsIgnoreCase("[MA]")) {
return null;
}
Location location = event.getBlock().getLocation();
String arenaId = getArenaId(event);
String signType = getSignType(event);
String templateId = getTemplateId(event, signType);
return new ArenaSign(location, templateId, arenaId, signType);
}
private String getArenaId(SignChangeEvent event) {
String arenaId = trim(event, 1);
if (arenaId.isEmpty()) {
throw new IllegalArgumentException("Missing arena name on line 2");
}
Arena arena = arenaMaster.getArenaWithName(arenaId);
if (arena == null) {
throw new IllegalArgumentException("Arena " + arenaId + " not found");
}
return arena.getSlug();
}
private String getSignType(SignChangeEvent event) {
String signType = trim(event, 2).toLowerCase();
if (signType.isEmpty()) {
throw new IllegalArgumentException("Missing sign type on line 3");
}
switch (signType) {
case "info":
case "join":
case "leave": {
return signType;
}
default: {
throw new IllegalArgumentException("Invalid sign type: " + signType);
}
}
}
private String getTemplateId(SignChangeEvent event, String signType) {
String line = trim(event, 3);
String templateId = !line.isEmpty() ? line : signType;
templateStore
.findById(templateId)
.orElseThrow(() -> new IllegalArgumentException("Template " + templateId + " not found"));
return templateId;
}
private String trim(SignChangeEvent event, int index) {
String line = event.getLine(index);
if (line == null) {
return "";
}
return line.trim();
}
}

View File

@ -0,0 +1,136 @@
package com.garbagemule.MobArena.signs;
import com.garbagemule.MobArena.util.Slugs;
import org.bukkit.World;
import org.yaml.snakeyaml.Yaml;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;
class SignDataMigrator {
private static final OpenOption[] WRITE_OPTIONS = {
StandardOpenOption.CREATE,
StandardOpenOption.WRITE,
StandardOpenOption.TRUNCATE_EXISTING,
StandardOpenOption.SYNC
};
private final Path legacyFile;
private final Path pendingFile;
private final Yaml yaml;
private final SignFile signFile;
private final Logger log;
SignDataMigrator(
Path legacyFile,
Path pendingFile,
Yaml yaml,
SignFile signFile,
Logger log
) {
this.legacyFile = legacyFile;
this.pendingFile = pendingFile;
this.yaml = yaml;
this.signFile = signFile;
this.log = log;
}
void init() throws IOException {
if (!Files.exists(legacyFile)) {
return;
}
log.info("Legacy sign data found, migrating...");
List<Map<String, ?>> signs = loadSignsInLegacyFile();
List<String> lines = new ArrayList<>(signs.size());
for (Map<String, ?> sign : signs) {
String line = convert(sign);
lines.add(line);
}
Files.write(pendingFile, lines, WRITE_OPTIONS);
Files.delete(legacyFile);
log.info("Legacy sign data migrated to temporary format.");
}
@SuppressWarnings("unchecked")
private List<Map<String, ?>> loadSignsInLegacyFile() throws IOException {
byte[] bytes = Files.readAllBytes(legacyFile);
Map<String, ?> map = yaml.load(new String(bytes));
if (map != null && map.containsKey("signs")) {
List<Map<String, ?>> signs = (List<Map<String, ?>>) map.get("signs");
if (signs != null) {
return signs;
}
}
return Collections.emptyList();
}
@SuppressWarnings("unchecked")
private String convert(Map<String, ?> sign) {
Map<String, ?> location = (Map<String, Object>) sign.get("location");
String world = (String) location.get("world");
String x = String.valueOf(((Number) location.get("x")).intValue());
String y = String.valueOf(((Number) location.get("y")).intValue());
String z = String.valueOf(((Number) location.get("z")).intValue());
String arenaId = Slugs.create((String) sign.get("arenaId"));
String type = (String) sign.get("type");
String templateId = (String) sign.get("templateId");
return String.join(";", world, x, y, z, arenaId, type, templateId);
}
void migrate(World world) throws IOException {
if (!Files.exists(pendingFile)) {
return;
}
List<String> lines = Files.readAllLines(pendingFile);
// Partition the lines into two lists; the ones to take from
// the file for migration and the ones to skip.
String name = world.getName();
String namePrefix = name + ";";
List<String> take = new ArrayList<>(lines.size());
List<String> skip = new ArrayList<>(lines.size());
for (String line : lines) {
if (line.startsWith(namePrefix)) {
take.add(line);
} else {
skip.add(line);
}
}
if (take.isEmpty()) {
return;
}
// Migrate all matching lines to the new sign data file.
log.info("Found temporary sign data for world '" + name + "', migrating...");
String id = world.getUID().toString();
String idPrefix = id + ";";
for (String line : take) {
signFile.append(idPrefix + line);
}
signFile.save();
log.info("Temporary data for " + take.size() + " sign(s) in world '" + name + "' migrated.");
// Write the remaining lines back down into the pending file
// if there are any left. Otherwise, migration has completed
// and we are done.
if (!skip.isEmpty()) {
Files.write(pendingFile, skip, WRITE_OPTIONS);
} else {
Files.delete(pendingFile);
log.info("Sign data migration complete.");
}
}
}

View File

@ -0,0 +1,67 @@
package com.garbagemule.MobArena.signs;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.List;
class SignFile {
private static final OpenOption[] WRITE_OPTIONS = {
StandardOpenOption.WRITE,
StandardOpenOption.TRUNCATE_EXISTING,
StandardOpenOption.SYNC
};
private final Path file;
private final List<String> lines;
private boolean stale;
SignFile(Path file) {
this.file = file;
this.lines = new ArrayList<>();
this.stale = true;
}
List<String> lines() throws IOException {
if (stale) {
load();
}
return new ArrayList<>(lines);
}
void append(String line) {
lines.add(line);
}
void erase(String line) {
lines.remove(line);
}
void load() throws IOException {
if (Files.notExists(file)) {
return;
}
lines.clear();
lines.addAll(Files.readAllLines(file));
stale = false;
}
void save() throws IOException {
if (Files.notExists(file)) {
Files.createDirectories(file.getParent());
Files.createFile(file);
}
Files.write(file, lines, WRITE_OPTIONS);
stale = true;
}
}

View File

@ -16,14 +16,32 @@ public class SignListeners {
}
public void register(SignBootstrap bootstrap) {
listeners.add(load(bootstrap));
listeners.add(unload(bootstrap));
listeners.add(clicks(bootstrap));
listeners.add(creation(bootstrap));
listeners.add(destruction(bootstrap));
listeners.add(redraw(bootstrap));
listeners.add(updates(bootstrap));
listeners.forEach(listener -> register(listener, bootstrap));
}
private HandlesWorldLoad load(SignBootstrap bootstrap) {
return new HandlesWorldLoad(
bootstrap.getSignDataMigrator(),
bootstrap.getSignReader(),
bootstrap.getSignStore(),
bootstrap.getPlugin().getLogger()
);
}
private HandlesWorldUnload unload(SignBootstrap bootstrap) {
return new HandlesWorldUnload(
bootstrap.getSignStore(),
bootstrap.getPlugin().getLogger()
);
}
private HandlesSignClicks clicks(SignBootstrap bootstrap) {
return new HandlesSignClicks(
bootstrap.getSignStore(),
@ -33,22 +51,28 @@ public class SignListeners {
private HandlesSignCreation creation(SignBootstrap bootstrap) {
return new HandlesSignCreation(
bootstrap.getStoresNewSign(),
bootstrap.getRendersTemplateById(),
bootstrap.getPlugin().getGlobalMessenger()
bootstrap.getSignCreator(),
bootstrap.getSignWriter(),
bootstrap.getSignStore(),
bootstrap.getSignRenderer(),
bootstrap.getPlugin().getGlobalMessenger(),
bootstrap.getPlugin().getLogger()
);
}
private HandlesSignDestruction destruction(SignBootstrap bootstrap) {
return new HandlesSignDestruction(
bootstrap.getRemovesSignAtLocation(),
bootstrap.getPlugin().getGlobalMessenger()
bootstrap.getSignStore(),
bootstrap.getSignWriter(),
bootstrap.getPlugin().getGlobalMessenger(),
bootstrap.getPlugin().getLogger()
);
}
private RedrawsSignsOnUpdates redraw(SignBootstrap bootstrap) {
return new RedrawsSignsOnUpdates(
bootstrap.getRedrawsArenaSigns(),
private HandlesArenaUpdates updates(SignBootstrap bootstrap) {
return new HandlesArenaUpdates(
bootstrap.getSignStore(),
bootstrap.getSignRenderer(),
bootstrap.getPlugin()
);
}

View File

@ -0,0 +1,44 @@
package com.garbagemule.MobArena.signs;
import org.bukkit.World;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
class SignReader {
private final SignFile file;
private final SignSerializer serializer;
private final Logger log;
SignReader(
SignFile file,
SignSerializer serializer,
Logger log
) {
this.file = file;
this.serializer = serializer;
this.log = log;
}
List<ArenaSign> read(World world) throws IOException {
List<ArenaSign> result = new ArrayList<>();
String id = world.getUID().toString();
String prefix = id + ";";
for (String line : file.lines()) {
if (line.startsWith(prefix)) {
try {
ArenaSign sign = serializer.deserialize(line, world);
result.add(sign);
} catch (Exception e) {
log.log(Level.SEVERE, "Failed to deserialize arena sign from line:\n" + line, e);
}
}
}
return result;
}
}

View File

@ -0,0 +1,80 @@
package com.garbagemule.MobArena.signs;
import com.garbagemule.MobArena.framework.Arena;
import com.garbagemule.MobArena.framework.ArenaMaster;
import org.bukkit.ChatColor;
import org.bukkit.block.BlockState;
import org.bukkit.block.Sign;
import org.bukkit.event.block.SignChangeEvent;
class SignRenderer {
private final TemplateStore templateStore;
private final ArenaMaster arenaMaster;
private final RendersTemplate rendersTemplate;
SignRenderer(
TemplateStore templateStore,
ArenaMaster arenaMaster,
RendersTemplate rendersTemplate
) {
this.templateStore = templateStore;
this.arenaMaster = arenaMaster;
this.rendersTemplate = rendersTemplate;
}
void render(ArenaSign sign) {
BlockState state = sign.location.getBlock().getState();
if (state instanceof Sign) {
Sign target = (Sign) state;
String[] lines = renderLines(sign);
for (int i = 0; i < lines.length; i++) {
target.setLine(i, lines[i]);
}
target.update();
}
}
void render(ArenaSign sign, SignChangeEvent event) {
String[] lines = renderLines(sign);
for (int i = 0; i < lines.length; i++) {
event.setLine(i, lines[i]);
}
}
private String[] renderLines(ArenaSign sign) {
String templateId = sign.templateId;
String arenaId = sign.arenaId;
Template template = templateStore.findById(templateId).orElse(null);
if (template == null) {
return templateNotFound(templateId);
}
Arena arena = arenaMaster.getArenaWithName(arenaId);
if (arena == null) {
return arenaNotFound(arenaId);
}
return rendersTemplate.render(template, arena);
}
private String[] templateNotFound(String templateId) {
return new String[]{
ChatColor.RED + "[ERROR]",
"Template",
ChatColor.YELLOW + templateId,
"not found :("
};
}
private String[] arenaNotFound(String arenaId) {
return new String[]{
ChatColor.RED + "[ERROR]",
"Arena",
ChatColor.YELLOW + arenaId,
"not found :("
};
}
}

View File

@ -0,0 +1,61 @@
package com.garbagemule.MobArena.signs;
import org.bukkit.Location;
import org.bukkit.World;
class SignSerializer {
String serialize(ArenaSign sign) {
String id = sign.location.getWorld().getUID().toString();
String name = sign.location.getWorld().getName();
String x = String.valueOf(sign.location.getBlockX());
String y = String.valueOf(sign.location.getBlockY());
String z = String.valueOf(sign.location.getBlockZ());
String arenaId = sign.arenaId;
String type = sign.type;
String templateId = sign.templateId;
return String.join(";", id, name, x, y, z, arenaId, type, templateId);
}
ArenaSign deserialize(String input, World world) {
String[] parts = input.split(";");
if (parts.length != 8) {
throw new IllegalArgumentException("Invalid input; expected 8 parts, got " + parts.length);
}
String id = parts[0];
if (!id.equals(world.getUID().toString())) {
throw new IllegalArgumentException("World mismatch");
}
int x = Integer.parseInt(parts[2]);
int y = Integer.parseInt(parts[3]);
int z = Integer.parseInt(parts[4]);
Location location = new Location(world, x, y, z);
String arenaId = parts[5];
String type = parts[6];
String templateId = parts[7];
return new ArenaSign(location, templateId, arenaId, type);
}
boolean equal(String line1, String line2) {
if (line1.equals(line2)) {
return true;
}
String[] parts1 = line1.split(";");
String[] parts2 = line2.split(";");
// World ID and (x,y,z) are all that matter
return parts1[0].equals(parts2[0])
&& parts1[2].equals(parts2[2])
&& parts1[3].equals(parts2[3])
&& parts1[4].equals(parts2[4]);
}
}

View File

@ -1,43 +1,21 @@
package com.garbagemule.MobArena.signs;
import org.bukkit.Location;
import org.bukkit.World;
import java.util.Collection;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
class SignStore {
static final String FILENAME = "signs.data";
private final Map<Location, ArenaSign> signs = new HashMap<>();
private final Map<Location, ArenaSign> signs;
SignStore(List<ArenaSign> signs) {
this.signs = signs.stream()
.collect(Collectors.toMap(
sign -> sign.location,
sign -> sign
));
}
void store(ArenaSign sign) {
signs.put(sign.location, sign);
}
Optional<ArenaSign> remove(Location location) {
ArenaSign sign = signs.remove(location);
return Optional.ofNullable(sign);
}
Collection<ArenaSign> findAll() {
return signs.values();
}
Optional<ArenaSign> findByLocation(Location location) {
ArenaSign sign = signs.get(location);
return Optional.ofNullable(sign);
ArenaSign findByLocation(Location location) {
return signs.get(location);
}
List<ArenaSign> findByArenaId(String arenaId) {
@ -46,4 +24,25 @@ class SignStore {
.collect(Collectors.toList());
}
void add(ArenaSign sign) {
signs.put(sign.location, sign);
}
ArenaSign removeByLocation(Location location) {
return signs.remove(location);
}
List<ArenaSign> removeByWorld(World world) {
List<ArenaSign> removed = new ArrayList<>();
Iterator<ArenaSign> iterator = signs.values().iterator();
while (iterator.hasNext()) {
ArenaSign sign = iterator.next();
if (sign.location.getWorld().equals(world)) {
removed.add(sign);
iterator.remove();
}
}
return removed;
}
}

View File

@ -0,0 +1,50 @@
package com.garbagemule.MobArena.signs;
import java.io.IOException;
import java.util.logging.Logger;
class SignWriter {
private final SignFile file;
private final SignSerializer serializer;
private final Logger log;
SignWriter(
SignFile file,
SignSerializer serializer,
Logger log
) {
this.file = file;
this.serializer = serializer;
this.log = log;
}
void write(ArenaSign sign) throws IOException {
String line = serializer.serialize(sign);
for (String candidate : file.lines()) {
if (serializer.equal(candidate, line)) {
log.warning("Erasing conflicting sign entry:\n" + candidate);
file.erase(candidate);
}
}
file.append(line);
file.save();
}
void erase(ArenaSign sign) throws IOException {
String line = serializer.serialize(sign);
for (String candidate : file.lines()) {
if (serializer.equal(candidate, line)) {
file.erase(candidate);
file.save();
return;
}
}
log.warning("No match found in sign data file for sign:\n" + line);
}
}

View File

@ -1,56 +0,0 @@
package com.garbagemule.MobArena.signs;
import com.garbagemule.MobArena.framework.Arena;
import com.garbagemule.MobArena.framework.ArenaMaster;
import org.bukkit.Location;
class StoresNewSign {
private final ArenaMaster arenaMaster;
private final TemplateStore templateStore;
private final SignStore signStore;
private final SavesSignStore savesSignStore;
StoresNewSign(
ArenaMaster arenaMaster,
TemplateStore templateStore,
SignStore signStore,
SavesSignStore savesSignStore
) {
this.arenaMaster = arenaMaster;
this.templateStore = templateStore;
this.signStore = signStore;
this.savesSignStore = savesSignStore;
}
void store(
Location location,
String arenaId,
String templateId,
String signType
) {
Arena arena = arenaMaster.getArenaWithName(arenaId);
if (arena == null) {
throw new IllegalArgumentException("Arena " + arenaId + " not found");
}
templateStore.findById(templateId)
.orElseThrow(() -> new IllegalArgumentException(
"Template " + templateId + " not found"
));
switch (signType) {
case "info":
case "join":
case "leave":
break;
default:
throw new IllegalArgumentException("Invalid sign type: " + signType);
}
signStore.store(new ArenaSign(location, templateId, arenaId, signType));
savesSignStore.save(signStore);
}
}

View File

@ -16,7 +16,7 @@ class Template {
static class Builder {
private String id;
private final String id;
private String[] base;
private String[] idle;
private String[] joining;

View File

@ -1,57 +0,0 @@
package com.garbagemule.MobArena.signs;
import static org.hamcrest.MatcherAssert.*;
import static org.hamcrest.Matchers.*;
import static org.mockito.Mockito.*;
import org.bukkit.Location;
import org.bukkit.World;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;
import java.util.HashMap;
import java.util.Map;
@RunWith(MockitoJUnitRunner.StrictStubs.class)
public class ArenaSignTest {
@Test
public void serialize() {
World world = mock(World.class);
Location location = new Location(world, 1, 2, 3);
String templateId = "a good template";
String arenaId = "cool arena";
String type = "join";
ArenaSign sign = new ArenaSign(location, templateId, arenaId, type);
Map<String, Object> result = sign.serialize();
assertThat(result.get("location"), equalTo(location));
assertThat(result.get("templateId"), equalTo(templateId));
assertThat(result.get("arenaId"), equalTo(arenaId));
assertThat(result.get("type"), equalTo(type));
}
@Test
public void deserialize() {
World world = mock(World.class);
Location location = new Location(world, 1, 2, 3);
String templateId = "a good template";
String arenaId = "cool arena";
String type = "join";
Map<String, Object> map = new HashMap<>();
map.put("location", location);
map.put("templateId", templateId);
map.put("arenaId", arenaId);
map.put("type", type);
ArenaSign result = ArenaSign.deserialize(map);
assertThat(result.location, is(equalTo(location)));
assertThat(result.templateId, is(equalTo(templateId)));
assertThat(result.arenaId, is(equalTo(arenaId)));
assertThat(result.type, is(equalTo(type)));
}
}

View File

@ -1,7 +1,5 @@
package com.garbagemule.MobArena.signs;
import static org.mockito.Mockito.*;
import org.bukkit.Location;
import org.bukkit.block.Block;
import org.bukkit.block.Chest;
@ -13,7 +11,7 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;
import java.util.Optional;
import static org.mockito.Mockito.*;
@SuppressWarnings("WeakerAccess")
@RunWith(MockitoJUnitRunner.StrictStubs.class)
@ -27,8 +25,6 @@ public class HandlesSignClicksTest {
@Before
public void setup() {
signStore = mock(SignStore.class);
when(signStore.findByLocation(any()))
.thenReturn(Optional.empty());
invokesSignAction = mock(InvokesSignAction.class);
subject = new HandlesSignClicks(signStore, invokesSignAction);
@ -40,7 +36,7 @@ public class HandlesSignClicksTest {
subject.on(event);
verifyZeroInteractions(signStore);
verifyNoInteractions(signStore, invokesSignAction);
}
@Test
@ -51,18 +47,19 @@ public class HandlesSignClicksTest {
subject.on(event);
verifyZeroInteractions(signStore);
verifyNoInteractions(signStore, invokesSignAction);
}
@Test
public void nonArenaSignNoFun() {
Block block = mock(Block.class);
when(block.getState()).thenReturn(mock(Sign.class));
when(signStore.findByLocation(any())).thenReturn(null);
PlayerInteractEvent event = event(null, block);
subject.on(event);
verifyZeroInteractions(signStore);
verifyNoInteractions(invokesSignAction);
}
@Test
@ -72,8 +69,7 @@ public class HandlesSignClicksTest {
when(block.getLocation()).thenReturn(location);
when(block.getState()).thenReturn(mock(Sign.class));
ArenaSign sign = new ArenaSign(location, "", "", "");
when(signStore.findByLocation(location))
.thenReturn(Optional.of(sign));
when(signStore.findByLocation(location)).thenReturn(sign);
Player player = mock(Player.class);
PlayerInteractEvent event = event(player, block);

View File

@ -1,10 +1,8 @@
package com.garbagemule.MobArena.signs;
import static org.mockito.Mockito.*;
import com.garbagemule.MobArena.Messenger;
import org.bukkit.Location;
import org.bukkit.block.Block;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.bukkit.event.block.SignChangeEvent;
import org.junit.Before;
@ -12,135 +10,139 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;
@SuppressWarnings("WeakerAccess")
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
@RunWith(MockitoJUnitRunner.StrictStubs.class)
public class HandlesSignCreationTest {
StoresNewSign storesNewSign;
RendersTemplateById rendersTemplate;
SignCreator creator;
SignWriter writer;
SignStore store;
SignRenderer renderer;
Messenger messenger;
Logger log;
HandlesSignCreation subject;
@Before
public void setup() {
storesNewSign = mock(StoresNewSign.class);
rendersTemplate = mock(RendersTemplateById.class);
when(rendersTemplate.render(any(), any()))
.thenReturn(new String[]{"", "", "", ""});
creator = mock(SignCreator.class);
writer = mock(SignWriter.class);
store = mock(SignStore.class);
renderer = mock(SignRenderer.class);
messenger = mock(Messenger.class);
log = mock(Logger.class);
subject = new HandlesSignCreation(
storesNewSign,
rendersTemplate,
messenger
creator,
writer,
store,
renderer,
messenger,
log
);
}
@Test
public void noHeaderNoAction() {
String[] lines = {"why", "so", "serious", "?"};
SignChangeEvent event = event(lines, null);
public void noSignCreationNoAction() {
SignChangeEvent event = new SignChangeEvent(null, null, null);
when(creator.create(event)).thenReturn(null);
subject.on(event);
verifyZeroInteractions(storesNewSign, rendersTemplate, messenger);
verifyNoInteractions(writer, store, messenger, log);
}
@Test
public void nullLinesHandledGracefully() {
String[] lines = {"[MA]", null, null, null};
SignChangeEvent event = event(lines, null);
subject.on(event);
}
@Test
public void useSignTypeIfTemplateNotAvailable() {
String arenaId = "castle";
String type = "join";
String[] lines = {"[MA]", arenaId, type, null};
Location location = mock(Location.class);
SignChangeEvent event = event(lines, location);
subject.on(event);
verify(storesNewSign).store(location, arenaId, type, type);
}
@Test
public void useTemplateIfAvailable() {
String arenaId = "castle";
String type = "join";
String templateId = "potato";
String[] lines = {"[MA]", arenaId, type, templateId};
Location location = mock(Location.class);
SignChangeEvent event = event(lines, location);
subject.on(event);
verify(storesNewSign).store(location, arenaId, templateId, type);
}
@Test
public void typeIsLowercased() {
String type = "JOIN";
String[] lines = {"[MA]", "", type, ""};
SignChangeEvent event = event(lines, null);
subject.on(event);
String lower = type.toLowerCase();
verify(storesNewSign).store(any(), any(), any(), eq(lower));
}
@Test
public void templateIdIsLowercased() {
String templateId = "BEST-TEMPLATE";
String[] lines = {"[MA]", "", "", templateId};
SignChangeEvent event = event(lines, null);
subject.on(event);
String lower = templateId.toLowerCase();
verify(storesNewSign).store(any(), any(), eq(lower), any());
}
@Test
public void rendersTemplateAfterStoring() {
String arenaId = "castle";
String type = "join";
String templateId = "potato";
String[] lines = {"[MA]", arenaId, type, templateId};
Location location = mock(Location.class);
SignChangeEvent event = event(lines, location);
subject.on(event);
verify(rendersTemplate).render(templateId, arenaId);
}
@Test
public void errorPassedToMessenger() {
String msg = "you messed up";
doThrow(new IllegalArgumentException(msg))
.when(storesNewSign).store(any(), any(), any(), any());
String[] lines = {"[MA]", "", "", ""};
SignChangeEvent event = event(lines, null);
subject.on(event);
verifyZeroInteractions(rendersTemplate);
verify(messenger).tell(event.getPlayer(), msg);
}
private SignChangeEvent event(String[] lines, Location location) {
Block block = mock(Block.class);
when(block.getLocation()).thenReturn(location);
public void passesSignFromCreator() throws Exception {
Player player = mock(Player.class);
return new SignChangeEvent(block, player, lines);
SignChangeEvent event = new SignChangeEvent(null, player, null);
ArenaSign sign = new ArenaSign(location(), null, null, null);
when(creator.create(event)).thenReturn(sign);
subject.on(event);
verify(writer).write(sign);
verify(store).add(sign);
verify(renderer).render(sign, event);
}
@Test
public void successMessageOnCreation() {
Player player = mock(Player.class);
SignChangeEvent event = new SignChangeEvent(null, player, null);
ArenaSign sign = new ArenaSign(location(), null, "castle", "join");
when(creator.create(event)).thenReturn(sign);
subject.on(event);
verify(messenger).tell(eq(player), anyString());
verify(log).info(anyString());
}
@Test
public void noWriteIfCreatorThrows() {
SignChangeEvent event = new SignChangeEvent(null, null, null);
doThrow(IllegalArgumentException.class).when(creator).create(event);
subject.on(event);
verifyNoInteractions(writer, store, renderer, log);
}
@Test
public void errorMessageIfCreatorThrows() {
Player player = mock(Player.class);
SignChangeEvent event = new SignChangeEvent(null, player, null);
String message = "it's bad";
doThrow(new IllegalArgumentException(message)).when(creator).create(event);
subject.on(event);
verify(messenger).tell(player, message);
}
@Test
public void noStorageIfWriterThrows() throws Exception {
SignChangeEvent event = new SignChangeEvent(null, null, null);
ArenaSign sign = new ArenaSign(null, null, null, null);
when(creator.create(event)).thenReturn(sign);
doThrow(IOException.class).when(writer).write(sign);
subject.on(event);
verifyNoInteractions(store, renderer);
}
@Test
public void errorMessageIfWriterThrows() throws Exception {
Player player = mock(Player.class);
SignChangeEvent event = new SignChangeEvent(null, player, null);
ArenaSign sign = new ArenaSign(null, null, null, null);
when(creator.create(event)).thenReturn(sign);
IOException exception = new IOException("it's bad");
doThrow(exception).when(writer).write(sign);
subject.on(event);
verify(messenger).tell(eq(player), anyString());
verify(log).log(eq(Level.SEVERE), anyString(), eq(exception));
}
private Location location() {
World world = mock(World.class);
when(world.getName()).thenReturn("world");
Location location = mock(Location.class);
when(location.getBlockX()).thenReturn(1);
when(location.getBlockY()).thenReturn(2);
when(location.getBlockZ()).thenReturn(3);
when(location.getWorld()).thenReturn(world);
return location;
}
}

View File

@ -1,8 +1,8 @@
package com.garbagemule.MobArena.signs;
import static org.mockito.Mockito.*;
import com.garbagemule.MobArena.Messenger;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.entity.Player;
import org.bukkit.event.block.BlockBreakEvent;
@ -11,53 +11,109 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;
import java.util.Optional;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
@SuppressWarnings("WeakerAccess")
@RunWith(MockitoJUnitRunner.StrictStubs.class)
public class HandlesSignDestructionTest {
RemovesSignAtLocation removesSignAtLocation;
SignStore store;
SignWriter writer;
Messenger messenger;
Logger log;
HandlesSignDestruction subject;
@Before
public void setup() {
removesSignAtLocation = mock(RemovesSignAtLocation.class);
store = mock(SignStore.class);
writer = mock(SignWriter.class);
messenger = mock(Messenger.class);
log = mock(Logger.class);
subject = new HandlesSignDestruction(
removesSignAtLocation,
messenger
store,
writer,
messenger,
log
);
}
@Test
public void doesNothingWithNonArenaSign() {
public void noSignNoAction() {
Location location = mock(Location.class);
Block block = mock(Block.class);
BlockBreakEvent event = new BlockBreakEvent(block, null);
when(block.getLocation()).thenReturn(location);
when(store.removeByLocation(location)).thenReturn(null);
subject.on(event);
verifyNoInteractions(writer, messenger, log);
}
@Test
public void erasesFoundSign() throws IOException {
Location location = location();
Block block = mock(Block.class);
Player player = mock(Player.class);
when(removesSignAtLocation.remove(any()))
.thenReturn(Optional.empty());
ArenaSign sign = new ArenaSign(location, "cool-sign", "castle", "join");
when(block.getLocation()).thenReturn(location);
when(store.removeByLocation(location)).thenReturn(sign);
BlockBreakEvent event = new BlockBreakEvent(block, player);
subject.on(event);
verifyZeroInteractions(messenger);
verify(writer).erase(sign);
}
@Test
public void reportsBreakageWithArenaSign() {
public void successMessageOnDestruction() {
Location location = location();
Block block = mock(Block.class);
Player player = mock(Player.class);
ArenaSign sign = new ArenaSign(null, "", "", "");
when(removesSignAtLocation.remove(any()))
.thenReturn(Optional.of(sign));
ArenaSign sign = new ArenaSign(location, "cool-sign", "castle", "join");
when(block.getLocation()).thenReturn(location);
when(store.removeByLocation(location)).thenReturn(sign);
BlockBreakEvent event = new BlockBreakEvent(block, player);
subject.on(event);
verify(messenger).tell(eq(player), anyString());
verify(log).info(anyString());
}
@Test
public void errorMessageIfWriterThrows() throws Exception {
Location location = mock(Location.class);
Block block = mock(Block.class);
Player player = mock(Player.class);
ArenaSign sign = new ArenaSign(location, "cool-sign", "castle", "join");
IOException exception = new IOException("it's bad");
when(block.getLocation()).thenReturn(location);
when(store.removeByLocation(location)).thenReturn(sign);
doThrow(exception).when(writer).erase(sign);
BlockBreakEvent event = new BlockBreakEvent(block, player);
subject.on(event);
verify(messenger).tell(eq(player), anyString());
verify(log).log(eq(Level.SEVERE), anyString(), eq(exception));
}
private Location location() {
World world = mock(World.class);
when(world.getName()).thenReturn("world");
Location location = mock(Location.class);
when(location.getBlockX()).thenReturn(1);
when(location.getBlockY()).thenReturn(2);
when(location.getBlockZ()).thenReturn(3);
when(location.getWorld()).thenReturn(world);
return location;
}
}

View File

@ -0,0 +1,122 @@
package com.garbagemule.MobArena.signs;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.event.world.WorldLoadEvent;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;
import java.io.IOException;
import java.util.Collections;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
@RunWith(MockitoJUnitRunner.StrictStubs.class)
public class HandlesWorldLoadTest {
SignDataMigrator migrator;
SignReader reader;
SignStore store;
Logger log;
HandlesWorldLoad subject;
@Before
public void setup() {
migrator = mock(SignDataMigrator.class);
reader = mock(SignReader.class);
store = mock(SignStore.class);
log = mock(Logger.class);
subject = new HandlesWorldLoad(
migrator,
reader,
store,
log
);
}
@Test
public void errorMessageIfDataMigrationThrows() throws IOException {
World world = mock(World.class);
when(world.getName()).thenReturn("world");
IOException exception = new IOException("it's bad");
doThrow(exception).when(migrator).migrate(world);
WorldLoadEvent event = new WorldLoadEvent(world);
subject.on(event);
verify(log).log(eq(Level.SEVERE), anyString(), eq(exception));
}
@Test
public void addsLoadedSignsToStore() throws IOException {
World world = mock(World.class);
when(world.getName()).thenReturn("world");
when(world.getUID()).thenReturn(UUID.fromString("cafebabe-ea75-dead-beef-deadcafebabe"));
Location location = mock(Location.class);
ArenaSign sign = new ArenaSign(location, "cool-sign", "castle", "join");
when(reader.read(world)).thenReturn(Collections.singletonList(sign));
WorldLoadEvent event = new WorldLoadEvent(world);
subject.on(event);
verify(store).add(sign);
}
@Test
public void logsMessageOnSignAddition() throws IOException {
World world = mock(World.class);
when(world.getName()).thenReturn("world");
when(world.getUID()).thenReturn(UUID.fromString("cafebabe-ea75-dead-beef-deadcafebabe"));
Location location = mock(Location.class);
ArenaSign sign = new ArenaSign(location, "cool-sign", "castle", "join");
when(reader.read(world)).thenReturn(Collections.singletonList(sign));
WorldLoadEvent event = new WorldLoadEvent(world);
subject.on(event);
verify(log).info(anyString());
}
@Test
public void addsNothingIfNoSignsLoaded() throws IOException {
World world = mock(World.class);
when(reader.read(world)).thenReturn(Collections.emptyList());
WorldLoadEvent event = new WorldLoadEvent(world);
subject.on(event);
verifyNoInteractions(store);
}
@Test
public void logsNothingIfNoSignsLoaded() throws IOException {
World world = mock(World.class);
when(reader.read(world)).thenReturn(Collections.emptyList());
WorldLoadEvent event = new WorldLoadEvent(world);
subject.on(event);
verifyNoInteractions(log);
}
@Test
public void errorMessageIfReaderThrows() throws IOException {
World world = mock(World.class);
IOException exception = new IOException("it's bad");
when(reader.read(world)).thenThrow(exception);
WorldLoadEvent event = new WorldLoadEvent(world);
subject.on(event);
verify(log).log(eq(Level.SEVERE), anyString(), eq(exception));
}
}

View File

@ -0,0 +1,65 @@
package com.garbagemule.MobArena.signs;
import org.bukkit.World;
import org.bukkit.event.world.WorldUnloadEvent;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.logging.Logger;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
@RunWith(MockitoJUnitRunner.StrictStubs.class)
public class HandlesWorldUnloadTest {
SignStore store;
Logger log;
HandlesWorldUnload subject;
@Before
public void setup() {
store = mock(SignStore.class);
log = mock(Logger.class);
subject = new HandlesWorldUnload(
store,
log
);
}
@Test
public void logsMessagesOnSignRemoval() {
UUID id = UUID.fromString("cafebabe-ea75-dead-beef-deadcafebabe");
String name = "world";
World world = mock(World.class);
when(world.getUID()).thenReturn(id);
when(world.getName()).thenReturn(name);
List<ArenaSign> signs = Arrays.asList(null, null);
when(store.removeByWorld(world)).thenReturn(signs);
WorldUnloadEvent event = new WorldUnloadEvent(world);
subject.on(event);
verify(log).info(anyString());
}
@Test
public void logsNothingIfNoSignsRemoved() {
World world = mock(World.class);
when(store.removeByWorld(world)).thenReturn(Collections.emptyList());
WorldUnloadEvent event = new WorldUnloadEvent(world);
subject.on(event);
verifyNoInteractions(log);
}
}

View File

@ -1,7 +1,5 @@
package com.garbagemule.MobArena.signs;
import static org.mockito.Mockito.*;
import com.garbagemule.MobArena.Messenger;
import com.garbagemule.MobArena.framework.Arena;
import com.garbagemule.MobArena.framework.ArenaMaster;
@ -11,6 +9,8 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;
import static org.mockito.Mockito.*;
@SuppressWarnings("WeakerAccess")
@RunWith(MockitoJUnitRunner.StrictStubs.class)
public class InvokesSignActionTest {
@ -36,7 +36,7 @@ public class InvokesSignActionTest {
subject.invoke(sign, player);
verifyZeroInteractions(arenaMaster);
verifyNoInteractions(arenaMaster);
}
@Test

View File

@ -1,141 +0,0 @@
package com.garbagemule.MobArena.signs;
import static org.mockito.Mockito.*;
import com.garbagemule.MobArena.framework.Arena;
import org.bukkit.Location;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
@SuppressWarnings("WeakerAccess")
@RunWith(MockitoJUnitRunner.StrictStubs.class)
public class RedrawsArenaSignsTest {
SignStore signStore;
TemplateStore templateStore;
RendersTemplate rendersTemplate;
SetsLines setsSignLines;
RedrawsArenaSigns subject;
@Before
public void setup() {
signStore = mock(SignStore.class);
when(signStore.findByArenaId(any()))
.thenReturn(Collections.emptyList());
templateStore = mock(TemplateStore.class);
when(templateStore.findById(any()))
.thenReturn(Optional.empty());
rendersTemplate = mock(RendersTemplate.class);
when(rendersTemplate.render(any(), any()))
.thenReturn(new String[]{"a", "b", "c", "d"});
setsSignLines = mock(SetsLines.class);
subject = new RedrawsArenaSigns(
signStore,
templateStore,
rendersTemplate,
setsSignLines
);
}
@Test
public void noSignsMeansNoRenderingOrLineSetting() {
Arena arena = arena("castle");
subject.redraw(arena);
verifyZeroInteractions(rendersTemplate);
verifyZeroInteractions(setsSignLines);
}
@Test
public void renderFoundTemplate() {
String arenaId = "castle";
Arena arena = arena(arenaId);
ArenaSign sign = sign("join", arenaId);
when(signStore.findByArenaId(arenaId))
.thenReturn(Collections.singletonList(sign));
Template template = template("template", "some", "info", "about", "arena");
when(templateStore.findById(sign.templateId))
.thenReturn(Optional.of(template));
subject.redraw(arena);
verify(rendersTemplate).render(template, arena);
}
@Test
public void setRenderedTemplateOnSign() {
String arenaId = "castle";
Arena arena = arena(arenaId);
ArenaSign sign = sign("join", arenaId);
when(signStore.findByArenaId(sign.arenaId))
.thenReturn(Collections.singletonList(sign));
Template template = template("template", "try", "with", "more", "fireballs");
when(templateStore.findById(sign.templateId))
.thenReturn(Optional.of(template));
String[] lines = new String[]{"this", "is", "a", "sign"};
when(rendersTemplate.render(template, arena))
.thenReturn(lines);
subject.redraw(arena);
verify(setsSignLines).set(sign.location, lines);
}
@Test
public void renderEachTemplateOnlyOnce() {
String arenaId = "castle";
Arena arena = arena(arenaId);
String templateId1 = "join";
String templateId2 = "info";
List<ArenaSign> signs = new ArrayList<>();
signs.add(sign(templateId1, arenaId));
signs.add(sign(templateId1, arenaId));
signs.add(sign(templateId1, arenaId));
signs.add(sign(templateId2, arenaId));
when(signStore.findByArenaId(arenaId))
.thenReturn(signs);
Template template1 = template(templateId1, "join", "a", "MobArena", "today!");
Template template2 = template(templateId2, "join", "another", "MobArena", "tomorrow!");
when(templateStore.findById(templateId1))
.thenReturn(Optional.of(template1));
when(templateStore.findById(templateId2))
.thenReturn(Optional.of(template2));
subject.redraw(arena);
verify(rendersTemplate, times(1)).render(template1, arena);
verify(rendersTemplate, times(1)).render(template2, arena);
verify(setsSignLines, times(signs.size())).set(any(), any());
}
private Arena arena(String arenaId) {
Arena arena = mock(Arena.class);
when(arena.configName()).thenReturn(arenaId);
return arena;
}
private ArenaSign sign(String templateId, String arenaId) {
Location location = mock(Location.class);
return new ArenaSign(location, templateId, arenaId, "join");
}
private Template template(String id, String l1, String l2, String l3, String l4) {
return new Template.Builder(id)
.withBase(new String[]{l1, l2, l3, l4})
.build();
}
}

View File

@ -1,56 +0,0 @@
package com.garbagemule.MobArena.signs;
import static org.mockito.Mockito.*;
import org.bukkit.Location;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;
import java.util.Optional;
@SuppressWarnings("WeakerAccess")
@RunWith(MockitoJUnitRunner.StrictStubs.class)
public class RemovesSignAtLocationTest {
SignStore signStore;
SavesSignStore savesSignStore;
RemovesSignAtLocation subject;
@Before
public void setup() {
signStore = mock(SignStore.class);
savesSignStore = mock(SavesSignStore.class);
subject = new RemovesSignAtLocation(
signStore,
savesSignStore
);
}
@Test
public void noSignMeansNoWrite() {
Location location = mock(Location.class);
when(signStore.remove(location))
.thenReturn(Optional.empty());
subject.remove(location);
verifyZeroInteractions(savesSignStore);
}
@Test
public void signRemovedWritesStore() {
Location location = mock(Location.class);
ArenaSign sign = new ArenaSign(location, "", "", "");
when(signStore.remove(location))
.thenReturn(Optional.of(sign));
subject.remove(location);
verify(savesSignStore).save(signStore);
}
}

View File

@ -1,9 +1,5 @@
package com.garbagemule.MobArena.signs;
import static org.hamcrest.MatcherAssert.*;
import static org.hamcrest.Matchers.*;
import static org.mockito.Mockito.*;
import com.garbagemule.MobArena.framework.Arena;
import com.garbagemule.MobArena.waves.WaveManager;
import org.bukkit.entity.Player;
@ -14,6 +10,10 @@ import org.mockito.junit.MockitoJUnitRunner;
import java.util.Collections;
import static org.hamcrest.MatcherAssert.*;
import static org.hamcrest.Matchers.*;
import static org.mockito.Mockito.*;
@SuppressWarnings("WeakerAccess")
@RunWith(MockitoJUnitRunner.StrictStubs.class)
public class RendersTemplateTest {

View File

@ -0,0 +1,211 @@
package com.garbagemule.MobArena.signs;
import com.garbagemule.MobArena.framework.Arena;
import com.garbagemule.MobArena.framework.ArenaMaster;
import org.bukkit.Location;
import org.bukkit.block.Block;
import org.bukkit.entity.Player;
import org.bukkit.event.block.SignChangeEvent;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;
import java.util.Optional;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.*;
import static org.junit.Assert.assertThrows;
import static org.mockito.Mockito.*;
@RunWith(MockitoJUnitRunner.StrictStubs.class)
public class SignCreatorTest {
ArenaMaster arenaMaster;
TemplateStore templateStore;
SignCreator subject;
@Before
public void setup() {
arenaMaster = mock(ArenaMaster.class);
templateStore = mock(TemplateStore.class);
subject = new SignCreator(
arenaMaster,
templateStore
);
}
@Test
public void noHeaderNoAction() {
String[] lines = {"ma", "castle", "join", "cool-sign"};
SignChangeEvent event = new SignChangeEvent(null, null, lines);
ArenaSign result = subject.create(event);
assertThat(result, nullValue());
verifyNoInteractions(arenaMaster, templateStore);
}
@Test
public void throwsOnMissingArena() {
String[] lines = {"[MA]", null, "join", "cool-sign"};
SignChangeEvent event = event(lines, null);
assertThrows(
IllegalArgumentException.class,
() -> subject.create(event)
);
verifyNoInteractions(arenaMaster);
}
@Test
public void throwsIfArenaNotFound() {
String arenaId = "castle";
String[] lines = {"[MA]", arenaId, "join", "cool-sign"};
when(arenaMaster.getArenaWithName(arenaId)).thenReturn(null);
SignChangeEvent event = event(lines, null);
assertThrows(
IllegalArgumentException.class,
() -> subject.create(event)
);
}
@Test
public void throwsOnMissingType() {
String arenaId = "castle";
String[] lines = {"[MA]", arenaId, null, "cool-sign"};
Arena arena = mock(Arena.class);
when(arena.getSlug()).thenReturn(arenaId);
when(arenaMaster.getArenaWithName(arenaId)).thenReturn(arena);
SignChangeEvent event = event(lines, null);
assertThrows(
IllegalArgumentException.class,
() -> subject.create(event)
);
}
@Test
public void throwsOnInvalidType() {
String arenaId = "castle";
String[] lines = {"[MA]", arenaId, "bob", "cool-sign"};
Arena arena = mock(Arena.class);
when(arena.getSlug()).thenReturn(arenaId);
when(arenaMaster.getArenaWithName(arenaId)).thenReturn(arena);
SignChangeEvent event = event(lines, null);
assertThrows(
IllegalArgumentException.class,
() -> subject.create(event)
);
}
@Test
public void throwsIfTemplateNotFound() {
String arenaId = "castle";
String templateId = "cool-sign";
String[] lines = {"[MA]", arenaId, "join", templateId};
Arena arena = mock(Arena.class);
when(arena.getSlug()).thenReturn(arenaId);
when(arenaMaster.getArenaWithName(arenaId)).thenReturn(arena);
when(templateStore.findById(templateId)).thenReturn(Optional.empty());
SignChangeEvent event = event(lines, null);
assertThrows(
IllegalArgumentException.class,
() -> subject.create(event)
);
}
@Test
public void completeSignDefinition() {
String arenaId = "castle";
String signType = "join";
String templateId = "cool-sign";
String[] lines = {"[MA]", arenaId, signType, templateId};
Location location = mock(Location.class);
Arena arena = mock(Arena.class);
when(arena.getSlug()).thenReturn(arenaId);
when(arenaMaster.getArenaWithName(arenaId)).thenReturn(arena);
when(templateStore.findById(templateId)).thenReturn(Optional.of(mock(Template.class)));
SignChangeEvent event = event(lines, location);
ArenaSign result = subject.create(event);
assertThat(result.location, equalTo(location));
assertThat(result.arenaId, equalTo(arenaId));
assertThat(result.type, equalTo(signType));
assertThat(result.templateId, equalTo(templateId));
}
@Test
public void slugifiedArenaId() {
String arenaId = "Area 52";
String arenaSlug = "area-52";
String signType = "join";
String templateId = "cool-sign";
String[] lines = {"[MA]", arenaId, signType, templateId};
Location location = mock(Location.class);
Arena arena = mock(Arena.class);
when(arena.getSlug()).thenReturn(arenaSlug);
when(arenaMaster.getArenaWithName(arenaId)).thenReturn(arena);
when(templateStore.findById(templateId)).thenReturn(Optional.of(mock(Template.class)));
SignChangeEvent event = event(lines, location);
ArenaSign result = subject.create(event);
assertThat(result.location, equalTo(location));
assertThat(result.arenaId, equalTo(arenaSlug));
assertThat(result.type, equalTo(signType.toLowerCase()));
assertThat(result.templateId, equalTo(templateId));
}
@Test
public void caseInsensitiveSignType() {
String arenaId = "castle";
String signType = "jOiN";
String templateId = "cool-sign";
String[] lines = {"[MA]", arenaId, signType, templateId};
Location location = mock(Location.class);
Arena arena = mock(Arena.class);
when(arena.getSlug()).thenReturn(arenaId);
when(arenaMaster.getArenaWithName(arenaId)).thenReturn(arena);
when(templateStore.findById(templateId)).thenReturn(Optional.of(mock(Template.class)));
SignChangeEvent event = event(lines, location);
ArenaSign result = subject.create(event);
assertThat(result.location, equalTo(location));
assertThat(result.arenaId, equalTo(arenaId));
assertThat(result.type, equalTo(signType.toLowerCase()));
assertThat(result.templateId, equalTo(templateId));
}
@Test
public void signWithoutTemplateUsesType() {
String arenaId = "castle";
String signType = "join";
String[] lines = {"[MA]", arenaId, signType, null};
Location location = mock(Location.class);
Arena arena = mock(Arena.class);
when(arena.getSlug()).thenReturn(arenaId);
when(arenaMaster.getArenaWithName(arenaId)).thenReturn(arena);
when(templateStore.findById(signType)).thenReturn(Optional.of(mock(Template.class)));
SignChangeEvent event = event(lines, location);
ArenaSign result = subject.create(event);
assertThat(result.templateId, equalTo(signType));
}
private SignChangeEvent event(String[] lines, Location location) {
Block block = mock(Block.class);
when(block.getLocation()).thenReturn(location);
Player player = mock(Player.class);
return new SignChangeEvent(block, player, lines);
}
}

View File

@ -0,0 +1,270 @@
package com.garbagemule.MobArena.signs;
import org.bukkit.World;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;
import org.yaml.snakeyaml.Yaml;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.logging.Logger;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
@RunWith(MockitoJUnitRunner.StrictStubs.class)
public class SignDataMigratorTest {
Path legacyFile;
Path pendingFile;
Yaml yaml;
SignFile signFile;
Logger log;
SignDataMigrator subject;
@Before
public void setup() throws IOException {
legacyFile = Files.createTempFile("SignDataMigratorTest-", ".data.tmp");
pendingFile = Files.createTempFile("SignDataMigratorTest-", ".tmp.tmp");
yaml = mock(Yaml.class);
signFile = mock(SignFile.class);
log = mock(Logger.class);
subject = new SignDataMigrator(
legacyFile,
pendingFile,
yaml,
signFile,
log
);
}
@After
public void teardown() throws IOException {
Files.deleteIfExists(legacyFile);
Files.deleteIfExists(pendingFile);
}
@Test
public void initDoesNothingIfNoLegacyFile() throws IOException {
Files.delete(pendingFile);
Files.delete(legacyFile);
subject.init();
verifyNoInteractions(yaml, log);
assertThat(Files.exists(legacyFile), equalTo(false));
assertThat(Files.exists(pendingFile), equalTo(false));
}
@Test
public void initCreatesPendingFileAndDeletesLegacyFile() throws IOException {
Files.delete(pendingFile);
Map<String, Object> map = new LinkedHashMap<>();
when(yaml.load(anyString())).thenReturn(map);
subject.init();
assertThat(Files.exists(legacyFile), equalTo(false));
assertThat(Files.exists(pendingFile), equalTo(true));
}
@Test
public void initLogsInfoMessagesOnConversion() throws IOException {
Files.delete(pendingFile);
Map<String, Object> map = new LinkedHashMap<>();
when(yaml.load(anyString())).thenReturn(map);
subject.init();
verify(log, times(2)).info(anyString());
}
@Test
public void initMigratesToPartialFormat() throws IOException {
Files.delete(pendingFile);
List<Map<String, Object>> signs = new ArrayList<>();
signs.add(sign("world", 46.0, 101.0, -191.0, "Area 52", "join", "cool-sign"));
signs.add(sign("lazy", 47.0, 102.0, -192.0, "Mr. Bob's Bus", "info", "status"));
signs.add(sign("eager", 48.0, 103.0, -194.0, "Mission: Impossible", "leave", "coward"));
signs.add(sign("world", 49.0, 104.0, -198.0, "castle", "leave", "bye-bye"));
Map<String, Object> root = new LinkedHashMap<>();
root.put("signs", signs);
when(yaml.load(anyString())).thenReturn(root);
subject.init();
List<String> lines = Files.readAllLines(pendingFile);
assertThat(lines.size(), equalTo(signs.size()));
assertThat(lines, hasItems(
"world;46;101;-191;area-52;join;cool-sign",
"lazy;47;102;-192;mr-bobs-bus;info;status",
"eager;48;103;-194;mission-impossible;leave;coward",
"world;49;104;-198;castle;leave;bye-bye"
));
}
@Test
public void migrateDoesNothingIfNoPendingFile() throws IOException {
Files.delete(pendingFile);
World world = mock(World.class);
subject.migrate(world);
verifyNoInteractions(signFile, log);
}
@Test
public void migrateDoesNothingIfNoMatchingLines() throws IOException {
List<String> lines = Arrays.asList(
"world;46;101;-191;castle;join;cool-sign",
"lazy;47;102;-192;jungle;info;status",
"world;48;103;-194;island;leave;coward"
);
Files.write(pendingFile, lines);
String name = "not-world";
World world = mock(World.class);
when(world.getName()).thenReturn(name);
subject.migrate(world);
verifyNoInteractions(signFile, log);
}
@Test
public void migratePassesMatchingLinesToSignFile() throws IOException {
List<String> lines = Arrays.asList(
"world;46;101;-191;castle;join;cool-sign",
"lazy;47;102;-192;jungle;info;status",
"world;48;103;-194;island;leave;coward"
);
Files.write(pendingFile, lines);
String name = "world";
String id = "cafebabe-ea75-dead-beef-deadcafebabe";
World world = mock(World.class);
when(world.getName()).thenReturn(name);
when(world.getUID()).thenReturn(UUID.fromString(id));
subject.migrate(world);
verify(signFile).append(id + ";" + lines.get(0));
verify(signFile).append(id + ";" + lines.get(2));
verify(signFile).save();
verifyNoMoreInteractions(signFile);
}
@Test
public void migrateDeletesMatchingLinesFromPendingFile() throws IOException {
List<String> lines = Arrays.asList(
"world;46;101;-191;castle;join;cool-sign",
"lazy;47;102;-192;jungle;info;status",
"world;48;103;-194;island;leave;coward"
);
Files.write(pendingFile, lines);
String name = "world";
String id = "cafebabe-ea75-dead-beef-deadcafebabe";
World world = mock(World.class);
when(world.getName()).thenReturn(name);
when(world.getUID()).thenReturn(UUID.fromString(id));
subject.migrate(world);
List<String> remaining = Files.readAllLines(pendingFile);
assertThat(remaining, equalTo(lines.subList(1, 2)));
}
@Test
public void migrateLogsInfoMessageOnMigration() throws IOException {
List<String> lines = Arrays.asList(
"world;46;101;-191;castle;join;cool-sign",
"lazy;47;102;-192;jungle;info;status",
"world;48;103;-194;island;leave;coward"
);
Files.write(pendingFile, lines);
String name = "world";
String id = "cafebabe-ea75-dead-beef-deadcafebabe";
World world = mock(World.class);
when(world.getName()).thenReturn(name);
when(world.getUID()).thenReturn(UUID.fromString(id));
subject.migrate(world);
verify(log, times(2)).info(anyString());
}
@Test
public void migrateDeletesPendingFileOnCompletion() throws IOException {
List<String> lines = Arrays.asList(
"world;46;101;-191;castle;join;cool-sign",
"world;48;103;-194;island;leave;coward"
);
Files.write(pendingFile, lines);
String name = "world";
String id = "cafebabe-ea75-dead-beef-deadcafebabe";
World world = mock(World.class);
when(world.getName()).thenReturn(name);
when(world.getUID()).thenReturn(UUID.fromString(id));
subject.migrate(world);
assertThat(Files.exists(pendingFile), equalTo(false));
}
@Test
public void migrateLogsAdditionallyOnCompletion() throws IOException {
List<String> lines = Arrays.asList(
"world;46;101;-191;castle;join;cool-sign",
"world;48;103;-194;island;leave;coward"
);
Files.write(pendingFile, lines);
String name = "world";
String id = "cafebabe-ea75-dead-beef-deadcafebabe";
World world = mock(World.class);
when(world.getName()).thenReturn(name);
when(world.getUID()).thenReturn(UUID.fromString(id));
subject.migrate(world);
verify(log, times(3)).info(anyString());
}
private Map<String, Object> sign(
String world,
double x,
double y,
double z,
String arenaId,
String type,
String templateId
) {
Map<String, Object> location = new LinkedHashMap<>();
location.put("==", "org.bukkit.Location");
location.put("world", world);
location.put("x", x);
location.put("y", y);
location.put("z", z);
location.put("pitch", 0.0);
location.put("yaw", 0.0);
Map<String, Object> result = new LinkedHashMap<>();
result.put("==", "com.garbagemule.MobArena.signs.ArenaSign");
result.put("arenaId", arenaId);
result.put("location", location);
result.put("templateId", templateId);
result.put("type", type);
return result;
}
}

View File

@ -0,0 +1,98 @@
package com.garbagemule.MobArena.signs;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.*;
@RunWith(MockitoJUnitRunner.StrictStubs.class)
public class SignFileTest {
Path file;
SignFile subject;
@Before
public void setup() throws IOException {
file = Files.createTempFile("SignFileTest-", ".tmp");
subject = new SignFile(file);
}
@After
public void teardown() throws IOException {
Files.delete(file);
}
@Test
public void emptyFileEmptyLines() throws IOException {
List<String> result = subject.lines();
assertThat(result.isEmpty(), equalTo(true));
}
@Test
public void emptyLinesIfAppendWithoutSave() throws IOException {
String line1 = "We will";
String line2 = "not be saved!";
subject.append(line1);
subject.append(line2);
List<String> result = subject.lines();
assertThat(result.isEmpty(), equalTo(true));
}
@Test
public void linePersistedOnSave() throws IOException {
String line1 = "We will";
String line2 = "be saved!";
subject.append(line1);
subject.append(line2);
subject.save();
List<String> result = subject.lines();
assertThat(result, equalTo(Arrays.asList(line1, line2)));
}
@Test
public void lineLingersWithoutSave() throws IOException {
String line1 = "We will";
String line2 = "be saved!";
subject.append(line1);
subject.append(line2);
subject.save();
subject.erase(line1);
List<String> result = subject.lines();
assertThat(result, equalTo(Arrays.asList(line1, line2)));
}
@Test
public void lineRemovedOnSave() throws IOException {
String line1 = "We will";
String line2 = "be saved!";
subject.append(line1);
subject.append(line2);
subject.save();
subject.erase(line1);
subject.save();
List<String> result = subject.lines();
assertThat(result, equalTo(Collections.singletonList(line2)));
}
}

View File

@ -0,0 +1,154 @@
package com.garbagemule.MobArena.signs;
import org.bukkit.World;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
@RunWith(MockitoJUnitRunner.StrictStubs.class)
public class SignReaderTest {
SignFile file;
SignSerializer serializer;
Logger log;
SignReader subject;
@Before
public void setup() {
file = mock(SignFile.class);
serializer = mock(SignSerializer.class);
log = mock(Logger.class);
subject = new SignReader(
file,
serializer,
log
);
}
@Test
public void deserializesNothingIfEmptyFile() throws IOException {
String id = "cafebabe-ea75-dead-beef-deadcafebabe";
World world = mock(World.class);
when(world.getUID()).thenReturn(UUID.fromString(id));
when(file.lines()).thenReturn(Collections.emptyList());
subject.read(world);
verifyNoInteractions(serializer);
}
@Test
public void logsNothingIfEmptyFile() throws IOException {
String id = "cafebabe-ea75-dead-beef-deadcafebabe";
World world = mock(World.class);
when(world.getUID()).thenReturn(UUID.fromString(id));
when(file.lines()).thenReturn(Collections.emptyList());
subject.read(world);
verifyNoInteractions(log);
}
@Test
public void deserializesLinesWithMatchingWorldId() throws IOException {
String id = "cafebabe-ea75-dead-beef-deadcafebabe";
World world = mock(World.class);
when(world.getUID()).thenReturn(UUID.fromString(id));
String line1 = "cafebeef-ea75-dead-babe-deadcafebeef;world;1;2;3;jungle;info;status";
String line2 = id + ";world;1;2;3;castle;join;cool-sign";
when(file.lines()).thenReturn(Arrays.asList(line1, line2));
ArenaSign sign = new ArenaSign(null, "cool-sign", "castle", "join");
when(serializer.deserialize(line2, world)).thenReturn(sign);
List<ArenaSign> result = subject.read(world);
assertThat(result.size(), equalTo(1));
assertThat(result, hasItem(sign));
verifyNoMoreInteractions(serializer);
}
@Test
public void logsNothingOnSuccess() throws IOException {
String id = "cafebabe-ea75-dead-beef-deadcafebabe";
World world = mock(World.class);
when(world.getUID()).thenReturn(UUID.fromString(id));
String line = id + ";world;1;2;3;castle;join;cool-sign";
when(file.lines()).thenReturn(Collections.singletonList(line));
ArenaSign sign = new ArenaSign(null, "cool-sign", "castle", "join");
when(serializer.deserialize(line, world)).thenReturn(sign);
subject.read(world);
verifyNoInteractions(log);
}
@Test
public void skipLineIfSerializerThrows() throws IOException {
String id = "cafebabe-ea75-dead-beef-deadcafebabe";
World world = mock(World.class);
when(world.getUID()).thenReturn(UUID.fromString(id));
String line1 = id + ";world;1;2;3;jungle;info;status";
String line2 = id + ";world;4;5;6;castle;join;cool-sign";
when(file.lines()).thenReturn(Arrays.asList(line1, line2));
IllegalArgumentException exception = new IllegalArgumentException("it's bad");
ArenaSign sign = new ArenaSign(null, "cool-sign", "castle", "join");
when(serializer.deserialize(line1, world)).thenThrow(exception);
when(serializer.deserialize(line2, world)).thenReturn(sign);
List<ArenaSign> result = subject.read(world);
assertThat(result.size(), equalTo(1));
assertThat(result, hasItem(sign));
}
@Test
public void errorMessageIfSerializerThrows() throws IOException {
String id = "cafebabe-ea75-dead-beef-deadcafebabe";
World world = mock(World.class);
when(world.getUID()).thenReturn(UUID.fromString(id));
String line1 = id + ";world;1;2;3;jungle;info;status";
String line2 = id + ";world;4;5;6;castle;join;cool-sign";
when(file.lines()).thenReturn(Arrays.asList(line1, line2));
IllegalArgumentException exception = new IllegalArgumentException("it's bad");
ArenaSign sign = new ArenaSign(null, "cool-sign", "castle", "join");
when(serializer.deserialize(line1, world)).thenThrow(exception);
when(serializer.deserialize(line2, world)).thenReturn(sign);
subject.read(world);
verify(log).log(eq(Level.SEVERE), anyString(), eq(exception));
verifyNoMoreInteractions(log);
}
@Test
public void propagatesExceptionsFromFile() throws IOException {
String id = "cafebabe-ea75-dead-beef-deadcafebabe";
World world = mock(World.class);
when(world.getUID()).thenReturn(UUID.fromString(id));
IOException exception = new IOException("it's bad");
when(file.lines()).thenThrow(exception);
Assert.assertThrows(
IOException.class,
() -> subject.read(world)
);
}
}

View File

@ -0,0 +1,161 @@
package com.garbagemule.MobArena.signs;
import com.garbagemule.MobArena.framework.Arena;
import com.garbagemule.MobArena.framework.ArenaMaster;
import org.bukkit.ChatColor;
import org.bukkit.Location;
import org.bukkit.block.Block;
import org.bukkit.block.Sign;
import org.bukkit.event.block.SignChangeEvent;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;
import java.util.Optional;
import static org.mockito.Mockito.*;
@RunWith(MockitoJUnitRunner.StrictStubs.class)
public class SignRendererTest {
TemplateStore templateStore;
ArenaMaster arenaMaster;
RendersTemplate rendersTemplate;
SignRenderer subject;
@Before
public void setup() {
templateStore = mock(TemplateStore.class);
arenaMaster = mock(ArenaMaster.class);
rendersTemplate = mock(RendersTemplate.class);
subject = new SignRenderer(
templateStore,
arenaMaster,
rendersTemplate
);
}
@Test
public void rendersErrorMessageOnSignIfTemplateNotFound() {
Location location = mock(Location.class);
Block block = mock(Block.class);
Sign target = mock(Sign.class);
when(location.getBlock()).thenReturn(block);
when(block.getState()).thenReturn(target);
String templateId = "cool-sign";
when(templateStore.findById(templateId)).thenReturn(Optional.empty());
ArenaSign sign = new ArenaSign(location, templateId, "castle", "join");
subject.render(sign);
verify(target).setLine(0, ChatColor.RED + "[ERROR]");
verify(target).setLine(1, "Template");
verify(target).setLine(2, ChatColor.YELLOW + "cool-sign");
verify(target).setLine(3, "not found :(");
}
@Test
public void rendersErrorMessageOnSignIfArenaNotFound() {
Location location = mock(Location.class);
Block block = mock(Block.class);
Sign target = mock(Sign.class);
when(location.getBlock()).thenReturn(block);
when(block.getState()).thenReturn(target);
String templateId = "cool-sign";
Template template = mock(Template.class);
String arenaId = "castle";
when(templateStore.findById(templateId)).thenReturn(Optional.of(template));
when(arenaMaster.getArenaWithName(arenaId)).thenReturn(null);
ArenaSign sign = new ArenaSign(location, templateId, arenaId, "join");
subject.render(sign);
verify(target).setLine(0, ChatColor.RED + "[ERROR]");
verify(target).setLine(1, "Arena");
verify(target).setLine(2, ChatColor.YELLOW + arenaId);
verify(target).setLine(3, "not found :(");
}
@Test
public void rendersResultsFromDelegateOnSign() {
Location location = mock(Location.class);
Block block = mock(Block.class);
Sign target = mock(Sign.class);
when(location.getBlock()).thenReturn(block);
when(block.getState()).thenReturn(target);
String templateId = "cool-sign";
Template template = mock(Template.class);
String arenaId = "castle";
Arena arena = mock(Arena.class);
String[] lines = new String[]{"this", "is", "a", "sign"};
when(templateStore.findById(templateId)).thenReturn(Optional.of(template));
when(arenaMaster.getArenaWithName(arenaId)).thenReturn(arena);
when(rendersTemplate.render(template, arena)).thenReturn(lines);
ArenaSign sign = new ArenaSign(location, templateId, arenaId, "join");
subject.render(sign);
verify(target).setLine(0, lines[0]);
verify(target).setLine(1, lines[1]);
verify(target).setLine(2, lines[2]);
verify(target).setLine(3, lines[3]);
}
@Test
public void rendersErrorMessageInEventIfTemplateNotFound() {
String templateId = "cool-sign";
when(templateStore.findById(templateId)).thenReturn(Optional.empty());
ArenaSign sign = new ArenaSign(null, templateId, "castle", "join");
SignChangeEvent event = mock(SignChangeEvent.class);
subject.render(sign, event);
verify(event).setLine(0, ChatColor.RED + "[ERROR]");
verify(event).setLine(1, "Template");
verify(event).setLine(2, ChatColor.YELLOW + "cool-sign");
verify(event).setLine(3, "not found :(");
}
@Test
public void rendersErrorMessageInEventIfArenaNotFound() {
String templateId = "cool-sign";
Template template = mock(Template.class);
String arenaId = "castle";
when(templateStore.findById(templateId)).thenReturn(Optional.of(template));
when(arenaMaster.getArenaWithName(arenaId)).thenReturn(null);
ArenaSign sign = new ArenaSign(null, templateId, arenaId, "join");
SignChangeEvent event = mock(SignChangeEvent.class);
subject.render(sign, event);
verify(event).setLine(0, ChatColor.RED + "[ERROR]");
verify(event).setLine(1, "Arena");
verify(event).setLine(2, ChatColor.YELLOW + arenaId);
verify(event).setLine(3, "not found :(");
}
@Test
public void rendersResultsFromDelegateInEvent() {
String templateId = "cool-sign";
Template template = mock(Template.class);
String arenaId = "castle";
Arena arena = mock(Arena.class);
String[] lines = new String[]{"this", "is", "a", "sign"};
when(templateStore.findById(templateId)).thenReturn(Optional.of(template));
when(arenaMaster.getArenaWithName(arenaId)).thenReturn(arena);
when(rendersTemplate.render(template, arena)).thenReturn(lines);
ArenaSign sign = new ArenaSign(null, templateId, arenaId, "join");
SignChangeEvent event = mock(SignChangeEvent.class);
subject.render(sign, event);
verify(event).setLine(0, lines[0]);
verify(event).setLine(1, lines[1]);
verify(event).setLine(2, lines[2]);
verify(event).setLine(3, lines[3]);
}
}

View File

@ -0,0 +1,165 @@
package com.garbagemule.MobArena.signs;
import org.bukkit.Location;
import org.bukkit.World;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;
import java.util.UUID;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.*;
import static org.mockito.Mockito.*;
@RunWith(MockitoJUnitRunner.StrictStubs.class)
public class SignSerializerTest {
SignSerializer subject;
@Before
public void setup() {
subject = new SignSerializer();
}
@Test
public void serializeReturnsSemicolonSeparatedRepresentation() {
World world = mock(World.class);
Location location = new Location(world, 1, 2, 3);
String arenaId = "castle";
String type = "join";
String templateId = "status";
when(world.getUID()).thenReturn(UUID.fromString("cafebabe-ea75-dead-beef-deadcafebabe"));
when(world.getName()).thenReturn("world");
ArenaSign sign = new ArenaSign(location, templateId, arenaId, type);
String result = subject.serialize(sign);
String expected = "cafebabe-ea75-dead-beef-deadcafebabe;world;1;2;3;castle;join;status";
assertThat(result, equalTo(expected));
}
@Test
public void deserializeThrowsIfLengthLessThan8() {
World world = mock(World.class);
String input = "1;2;3;4;5;6;7";
Assert.assertThrows(
"Invalid input; expected 8 parts, got 7",
IllegalArgumentException.class,
() -> subject.deserialize(input, world)
);
}
@Test
public void deserializeThrowsIfLengthGreaterThan8() {
World world = mock(World.class);
String input = "1;2;3;4;5;6;7;8;9";
Assert.assertThrows(
"Invalid input; expected 8 parts, got 9",
IllegalArgumentException.class,
() -> subject.deserialize(input, world)
);
}
@Test
public void deserializeThrowsIfWorldDoesNotMatchId() {
World world = mock(World.class);
when(world.getUID()).thenReturn(UUID.fromString("deadbeef-ea75-cafe-babe-deadcafebeef"));
String input = "wrong-id;world;1;2;3;castle;join;status";
Assert.assertThrows(
"World mismatch",
IllegalArgumentException.class,
() -> subject.deserialize(input, world)
);
}
@Test
public void deserializeReturnsIfWorldMatchesId() {
World world = mock(World.class);
when(world.getUID()).thenReturn(UUID.fromString("cafebabe-ea75-dead-beef-deadcafebabe"));
String input = "cafebabe-ea75-dead-beef-deadcafebabe;wrong-world;1;2;3;castle;join;status";
ArenaSign result = subject.deserialize(input, world);
Location location = new Location(world, 1, 2, 3);
assertThat(result.location, equalTo(location));
assertThat(result.arenaId, equalTo("castle"));
assertThat(result.type, equalTo("join"));
assertThat(result.templateId, equalTo("status"));
}
@Test
public void deserializeSerializeReflexivity() {
String id = "cafebabe-ea75-dead-beef-deadcafebabe";
String name = "world";
World world = mock(World.class);
when(world.getName()).thenReturn(name);
when(world.getUID()).thenReturn(UUID.fromString(id));
String line = id + ";" + name + ";1;2;3;castle;join;cool-sign";
String result = subject.serialize(subject.deserialize(line, world));
assertThat(result, equalTo(line));
}
@Test
public void serializeDeserializeReflexivity() {
String id = "cafebabe-ea75-dead-beef-deadcafebabe";
World world = mock(World.class);
when(world.getUID()).thenReturn(UUID.fromString(id));
Location location = new Location(world, 1, 2, 3);
ArenaSign sign = new ArenaSign(location, "cool-sign", "castle", "join");
ArenaSign result = subject.deserialize(subject.serialize(sign), world);
assertThat(result.location, equalTo(location));
assertThat(result.arenaId, equalTo("castle"));
assertThat(result.type, equalTo("join"));
assertThat(result.templateId, equalTo("cool-sign"));
}
@Test
public void equalReturnsTrueIfStringsAreEqual() {
String line = "cafebabe-ea75-dead-beef-deadcafebabe;world;1;2;3;castle;join;status";
boolean result = subject.equal(line, line);
assertThat(result, equalTo(true));
}
@Test
public void equalReturnsTrueIfWorldIdAndLocationMatch() {
String line1 = "cafebabe-ea75-dead-beef-deadcafebabe;right-world;1;2;3;jungle;leave;out";
String line2 = "cafebabe-ea75-dead-beef-deadcafebabe;wrong-world;1;2;3;castle;join;status";
boolean result = subject.equal(line1, line2);
assertThat(result, equalTo(true));
}
@Test
public void equalReturnsFalseIfOnlyWorldIdIsDifferent() {
String line1 = "cafebabe-ea75-dead-beef-deadcafebabe;right-world;1;2;3;castle;join;status";
String line2 = "deadbeef-feed-cafe-babe-a70ff1cecafe;right-world;1;2;3;castle;join;status";
boolean result = subject.equal(line1, line2);
assertThat(result, equalTo(false));
}
@Test
public void equalReturnsFalseIfOnlyCoordsAreDifferent() {
String line1 = "cafebabe-ea75-dead-beef-deadcafebabe;right-world;1;2;3;castle;join;status";
String line2 = "cafebabe-ea75-dead-beef-deadcafebabe;right-world;4;5;6;castle;join;status";
boolean result = subject.equal(line1, line2);
assertThat(result, equalTo(false));
}
}

View File

@ -0,0 +1,154 @@
package com.garbagemule.MobArena.signs;
import org.bukkit.Location;
import org.bukkit.World;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;
import java.util.List;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.*;
import static org.mockito.Mockito.*;
@RunWith(MockitoJUnitRunner.StrictStubs.class)
public class SignStoreTest {
SignStore subject;
@Before
public void setup() {
subject = new SignStore();
}
@Test
public void findByLocationReturnsNullIfSignDoesNotExist() {
Location location = mock(Location.class);
ArenaSign result = subject.findByLocation(location);
assertThat(result, nullValue());
}
@Test
public void findByLocationReturnsSignIfItExists() {
Location location = mock(Location.class);
ArenaSign sign = new ArenaSign(location, "cool-sign", "castle", "join");
subject.add(sign);
ArenaSign result = subject.findByLocation(location);
assertThat(result, equalTo(sign));
}
@Test
public void findByArenaIdReturnsEmptyListIfNoSignsMatch() {
Location location1 = mock(Location.class);
Location location2 = mock(Location.class);
ArenaSign sign1 = new ArenaSign(location1, "cool-sign", "castle", "join");
ArenaSign sign2 = new ArenaSign(location2, "lame-sign", "island", "leave");
subject.add(sign1);
subject.add(sign2);
List<ArenaSign> result = subject.findByArenaId("jungle");
assertThat(result.isEmpty(), equalTo(true));
}
@Test
public void findByArenaIdReturnsOnlyMatchingSigns() {
Location location1 = mock(Location.class);
Location location2 = mock(Location.class);
Location location3 = mock(Location.class);
ArenaSign sign1 = new ArenaSign(location1, "cool-sign", "castle", "join");
ArenaSign sign2 = new ArenaSign(location2, "lame-sign", "island", "leave");
ArenaSign sign3 = new ArenaSign(location3, "very-sign", "jungle", "info");
subject.add(sign1);
subject.add(sign2);
subject.add(sign3);
List<ArenaSign> result = subject.findByArenaId("island");
assertThat(result.size(), equalTo(1));
assertThat(result, hasItem(sign2));
}
@Test
public void removeReturnsNullIfSignDoesNotExist() {
Location location = mock(Location.class);
ArenaSign sign = new ArenaSign(location, "cool-sign", "castle", "join");
ArenaSign result = subject.removeByLocation(location);
assertThat(result, nullValue());
subject.add(sign);
assertThat(subject.removeByLocation(location), equalTo(sign));
assertThat(subject.removeByLocation(location), nullValue());
}
@Test
public void removeReturnsSignIfItExists() {
Location location = mock(Location.class);
ArenaSign sign = new ArenaSign(location, "cool-sign", "castle", "join");
subject.add(sign);
ArenaSign result = subject.removeByLocation(location);
assertThat(result, equalTo(sign));
}
@Test
public void removeReturnsNullIfSignWasRemoved() {
Location location = mock(Location.class);
ArenaSign sign = new ArenaSign(location, "cool-sign", "castle", "join");
subject.add(sign);
subject.removeByLocation(location);
ArenaSign result = subject.removeByLocation(location);
assertThat(result, nullValue());
}
@Test
public void removeByWorldReturnsOnlyMatchingSigns() {
World world1 = mock(World.class);
World world2 = mock(World.class);
Location location1 = mock(Location.class);
Location location2 = mock(Location.class);
when(location1.getWorld()).thenReturn(world1);
when(location2.getWorld()).thenReturn(world2);
ArenaSign sign1 = new ArenaSign(location1, "lame-sign", "jungle", "info");
ArenaSign sign2 = new ArenaSign(location2, "cool-sign", "castle", "join");
subject.add(sign1);
subject.add(sign2);
List<ArenaSign> result = subject.removeByWorld(world2);
assertThat(result, not(hasItem(sign1)));
assertThat(result, hasItem(sign2));
}
@Test
public void removeByWorldReturnsEmptyListIfSignsWereRemoved() {
World world1 = mock(World.class);
World world2 = mock(World.class);
Location location1 = mock(Location.class);
Location location2 = mock(Location.class);
when(location1.getWorld()).thenReturn(world1);
when(location2.getWorld()).thenReturn(world2);
ArenaSign sign1 = new ArenaSign(location1, "lame-sign", "jungle", "info");
ArenaSign sign2 = new ArenaSign(location2, "cool-sign", "castle", "join");
subject.add(sign1);
subject.add(sign2);
subject.removeByWorld(world2);
List<ArenaSign> result = subject.removeByWorld(world2);
assertThat(result.isEmpty(), equalTo(true));
}
}

View File

@ -0,0 +1,161 @@
package com.garbagemule.MobArena.signs;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Logger;
import static org.mockito.Mockito.*;
@RunWith(MockitoJUnitRunner.StrictStubs.class)
public class SignWriterTest {
SignFile file;
SignSerializer serializer;
Logger log;
SignWriter subject;
@Before
public void setup() {
file = mock(SignFile.class);
serializer = mock(SignSerializer.class);
log = mock(Logger.class);
subject = new SignWriter(
file,
serializer,
log
);
}
@Test
public void writeCallsAppendWithSerializedSign() throws IOException {
ArenaSign sign = new ArenaSign(null, "cool-sign", "castle", "join");
String line = "some arbitrary serialization";
when(serializer.serialize(sign)).thenReturn(line);
subject.write(sign);
verify(file).append(line);
verify(file).save();
}
@Test
public void writeCallsEraseOnConflicts() throws IOException {
ArenaSign sign = new ArenaSign(null, "cool-sign", "castle", "join");
String line = "some arbitrary serialization";
String c1 = "some conflicting line";
String c2 = "some non-conflicting line";
String c3 = "some other conflicting line";
when(serializer.serialize(sign)).thenReturn(line);
when(serializer.equal(c1, line)).thenReturn(true);
when(serializer.equal(c2, line)).thenReturn(false);
when(serializer.equal(c3, line)).thenReturn(true);
when(file.lines()).thenReturn(Arrays.asList(c1, c2, c3));
subject.write(sign);
verify(file).erase(c1);
verify(file).erase(c3);
}
@Test
public void writeLogsWarningOnConflicts() throws IOException {
ArenaSign sign = new ArenaSign(null, "cool-sign", "castle", "join");
String line = "some arbitrary serialization";
String c1 = "some conflicting line";
String c2 = "some non-conflicting line";
String c3 = "some other conflicting line";
when(serializer.serialize(sign)).thenReturn(line);
when(serializer.equal(c1, line)).thenReturn(true);
when(serializer.equal(c2, line)).thenReturn(false);
when(serializer.equal(c3, line)).thenReturn(true);
when(file.lines()).thenReturn(Arrays.asList(c1, c2, c3));
subject.write(sign);
verify(log, times(2)).warning(anyString());
}
@Test
public void successfulWriteLogsNothing() throws IOException {
ArenaSign sign = new ArenaSign(null, "cool-sign", "castle", "join");
String line = "some arbitrary serialization";
when(serializer.serialize(sign)).thenReturn(line);
subject.write(sign);
verifyNoInteractions(log);
}
@Test
public void eraseCallsEraseWithIdentifiedLineOnly() throws IOException {
ArenaSign sign = new ArenaSign(null, "cool-sign", "castle", "join");
String line = "right-id;wrong-name;some other stuff";
String c1 = "wrong-id;wrong-name;some stuff";
String c2 = "right-id;right-name;some other stuff";
String c3 = "wrong-id;wrong-name;some more stuff";
when(serializer.serialize(sign)).thenReturn(line);
when(serializer.equal(anyString(), anyString())).thenReturn(false);
when(serializer.equal(c2, line)).thenReturn(true);
when(file.lines()).thenReturn(Arrays.asList(c1, c2, c3));
subject.erase(sign);
verify(file).erase(c2);
verify(file).save();
verifyNoMoreInteractions(file);
}
@Test
public void successfulEraseLogsNothing() throws IOException {
ArenaSign sign = new ArenaSign(null, "cool-sign", "castle", "join");
String line = "right-id;wrong-name;some other stuff";
String candiate1 = "wrong-id;wrong-name;some stuff";
String candiate2 = "right-id;right-name;some other stuff";
String candiate3 = "wrong-id;wrong-name;some more stuff";
when(serializer.serialize(sign)).thenReturn(line);
when(serializer.equal(anyString(), anyString())).thenReturn(false);
when(serializer.equal(candiate2, line)).thenReturn(true);
when(file.lines()).thenReturn(Arrays.asList(candiate1, candiate2, candiate3));
subject.erase(sign);
verifyNoInteractions(log);
}
@Test
public void eraseBailsOnNoMatch() throws IOException {
ArenaSign sign = new ArenaSign(null, "cool-sign", "castle", "join");
String line = "right-id;right-name;some right stuff";
List<String> candidates = Arrays.asList("a", "b", "c");
when(serializer.serialize(sign)).thenReturn(line);
when(serializer.equal(anyString(), anyString())).thenReturn(false);
when(file.lines()).thenReturn(candidates);
subject.erase(sign);
verify(file, never()).erase(anyString());
}
@Test
public void eraseLogsWarningOnNoMatch() throws IOException {
ArenaSign sign = new ArenaSign(null, "cool-sign", "castle", "join");
String line = "right-id;right-name;some right stuff";
List<String> candidates = Arrays.asList("a", "b", "c");
when(serializer.serialize(sign)).thenReturn(line);
when(serializer.equal(anyString(), anyString())).thenReturn(false);
when(file.lines()).thenReturn(candidates);
subject.erase(sign);
verify(log).warning(anyString());
}
}

View File

@ -1,126 +0,0 @@
package com.garbagemule.MobArena.signs;
import static org.hamcrest.MatcherAssert.*;
import static org.hamcrest.Matchers.*;
import static org.mockito.Mockito.*;
import com.garbagemule.MobArena.framework.Arena;
import com.garbagemule.MobArena.framework.ArenaMaster;
import org.bukkit.Location;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.junit.MockitoJUnitRunner;
import java.util.Optional;
@SuppressWarnings("WeakerAccess")
@RunWith(MockitoJUnitRunner.StrictStubs.class)
public class StoresNewSignTest {
ArenaMaster arenaMaster;
TemplateStore templateStore;
SignStore signStore;
SavesSignStore savesSignStore;
StoresNewSign subject;
@Rule
public ExpectedException exception = ExpectedException.none();
@Before
public void setup() {
arenaMaster = mock(ArenaMaster.class);
when(arenaMaster.getArenaWithName(any()))
.thenReturn(null);
templateStore = mock(TemplateStore.class);
when(templateStore.findById(any()))
.thenReturn(Optional.empty());
signStore = mock(SignStore.class);
savesSignStore = mock(SavesSignStore.class);
subject = new StoresNewSign(
arenaMaster,
templateStore,
signStore,
savesSignStore
);
}
@Test
public void throwOnNonExistentArena() {
Location location = mock(Location.class);
String arenaId = "castle";
exception.expect(IllegalArgumentException.class);
subject.store(location, arenaId, "", "");
}
@Test
public void throwOnNonExistentTemplate() {
Location location = mock(Location.class);
String arenaId = "castle";
Arena arena = mock(Arena.class);
when(arenaMaster.getArenaWithName(arenaId))
.thenReturn(arena);
String templateId = "template";
exception.expect(IllegalArgumentException.class);
subject.store(location, arenaId, templateId, "");
}
@Test
public void throwOnNonInvalidSignType() {
Location location = mock(Location.class);
String arenaId = "castle";
Arena arena = mock(Arena.class);
when(arenaMaster.getArenaWithName(arenaId))
.thenReturn(arena);
String templateId = "a very nice template";
Template template = template(templateId);
when(templateStore.findById(templateId))
.thenReturn(Optional.of(template));
String signType = "not a real sign type";
exception.expect(IllegalArgumentException.class);
subject.store(location, arenaId, templateId, signType);
}
@Test
public void storesSignAndWritesToDisk() {
Location location = mock(Location.class);
String arenaId = "castle";
Arena arena = mock(Arena.class);
when(arenaMaster.getArenaWithName(arenaId))
.thenReturn(arena);
String templateId = "a very nice template";
Template template = template(templateId);
when(templateStore.findById(templateId))
.thenReturn(Optional.of(template));
String signType = "join";
subject.store(location, arenaId, templateId, signType);
ArgumentCaptor<ArenaSign> captor = ArgumentCaptor.forClass(ArenaSign.class);
verify(signStore).store(captor.capture());
verify(savesSignStore).save(signStore);
ArenaSign sign = captor.getValue();
assertThat(sign.location, equalTo(location));
assertThat(sign.arenaId, equalTo(arenaId));
assertThat(sign.templateId, equalTo(templateId));
assertThat(sign.type, equalTo(signType));
}
private Template template(String id) {
return new Template.Builder(id)
.withBase(new String[]{"", "", "", ""})
.build();
}
}