From b92aedf872d5c86f8a76414d1b9ab018c9acfe5c Mon Sep 17 00:00:00 2001 From: Andrew Lalis Date: Sat, 29 May 2021 11:18:43 +0200 Subject: [PATCH] Added unit test for shard io. --- .gitignore | 113 ++++++++++++++++++ designs/cluster_node_icon.svg | 78 ++++++++++++ designs/shard_node_icon.svg | 78 ++++++++++++ pom.xml | 13 ++ .../andrewlalis/crystalkeep/CrystalKeep.java | 36 +----- .../crystalkeep/model/Cluster.java | 1 - .../andrewlalis/crystalkeep/model/Shard.java | 9 +- .../model/serialization/ClusterLoader.java | 8 +- .../serialization/ClusterSerializer.java | 44 +++---- .../model/serialization/ShardSerializer.java | 4 +- .../model/shards/LoginCredentialsShard.java | 9 +- .../crystalkeep/model/shards/TextShard.java | 8 +- .../serialization/ClusterSerializerTest.java | 54 +++++++++ .../andrewlalis/crystalkeep/package-info.java | 4 + 14 files changed, 379 insertions(+), 80 deletions(-) create mode 100644 .gitignore create mode 100644 designs/cluster_node_icon.svg create mode 100644 designs/shard_node_icon.svg create mode 100644 src/test/java/nl/andrewlalis/crystalkeep/model/serialization/ClusterSerializerTest.java create mode 100644 src/test/java/nl/andrewlalis/crystalkeep/package-info.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9356e88 --- /dev/null +++ b/.gitignore @@ -0,0 +1,113 @@ +### Java template +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Maven template +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties +# https://github.com/takari/maven-wrapper#usage-without-binary-jar +.mvn/wrapper/maven-wrapper.jar + +.idea/ +clusters/ diff --git a/designs/cluster_node_icon.svg b/designs/cluster_node_icon.svg new file mode 100644 index 0000000..6dbe761 --- /dev/null +++ b/designs/cluster_node_icon.svg @@ -0,0 +1,78 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/designs/shard_node_icon.svg b/designs/shard_node_icon.svg new file mode 100644 index 0000000..24fc994 --- /dev/null +++ b/designs/shard_node_icon.svg @@ -0,0 +1,78 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/pom.xml b/pom.xml index f5ff024..3926ebb 100644 --- a/pom.xml +++ b/pom.xml @@ -48,5 +48,18 @@ provided true + + + org.junit.jupiter + junit-jupiter-engine + 5.7.2 + test + + + org.junit.jupiter + junit-jupiter-params + 5.7.0 + test + \ No newline at end of file diff --git a/src/main/java/nl/andrewlalis/crystalkeep/CrystalKeep.java b/src/main/java/nl/andrewlalis/crystalkeep/CrystalKeep.java index 045a046..7d06dba 100644 --- a/src/main/java/nl/andrewlalis/crystalkeep/CrystalKeep.java +++ b/src/main/java/nl/andrewlalis/crystalkeep/CrystalKeep.java @@ -7,17 +7,13 @@ import javafx.stage.Stage; import nl.andrewlalis.crystalkeep.control.MainViewController; import nl.andrewlalis.crystalkeep.model.Cluster; import nl.andrewlalis.crystalkeep.model.Model; -import nl.andrewlalis.crystalkeep.model.Shard; import nl.andrewlalis.crystalkeep.model.serialization.ClusterLoader; -import nl.andrewlalis.crystalkeep.model.serialization.ClusterSerializer; import nl.andrewlalis.crystalkeep.model.shards.LoginCredentialsShard; import nl.andrewlalis.crystalkeep.model.shards.TextShard; -import java.io.ByteArrayInputStream; import java.io.IOException; import java.net.URL; import java.time.LocalDateTime; -import java.util.Arrays; public class CrystalKeep extends Application { public static void main(String[] args) { @@ -41,10 +37,10 @@ public class CrystalKeep extends Application { System.out.println("Loaded existing root cluster."); } catch (IOException e) { rootCluster = new Cluster("Root"); - rootCluster.addShard(new TextShard(rootCluster, "Example Shard", LocalDateTime.now(), "Hello world!")); - rootCluster.addShard(new LoginCredentialsShard(rootCluster, "Netflix", LocalDateTime.now(), "user", "secret password")); + rootCluster.addShard(new TextShard("Example Shard", LocalDateTime.now(), "Hello world!")); + rootCluster.addShard(new LoginCredentialsShard("Netflix", LocalDateTime.now(), "user", "secret password")); for (int i = 0; i < 100; i++) { - rootCluster.addShard(new TextShard(rootCluster, "test " + i, LocalDateTime.now(), "value: " + i)); + rootCluster.addShard(new TextShard("test " + i, LocalDateTime.now(), "value: " + i)); } clusterLoader.saveDefault(rootCluster); System.out.println("Saved root cluster on first load."); @@ -57,30 +53,4 @@ public class CrystalKeep extends Application { controller.init(model); System.out.println(rootCluster); } - - public static void test() throws IOException { - Cluster c = new Cluster("Test"); - Shard s = new TextShard(c, "sample", LocalDateTime.now(), "Hello world!"); - Shard s2 = new LoginCredentialsShard(c, "logs", LocalDateTime.now().plusHours(3), "andrew", "testing"); - c.addShard(s); - c.addShard(s2); - Cluster c2 = new Cluster("Test2"); - Shard s3 = new TextShard(c2, "another sample", LocalDateTime.now().plusMinutes(3), "Testing this stuff...."); - c2.addShard(s3); - Cluster parent = new Cluster("Parent"); - parent.addCluster(c); - parent.addCluster(c2); - - System.out.println(parent); - long start = System.currentTimeMillis(); - byte[] cBytes = ClusterSerializer.toBytes(parent); - long dur = System.currentTimeMillis() - start; - System.out.println("Duration: " + dur + " milliseconds"); - System.out.println(Arrays.toString(cBytes)); - - Cluster cLoaded = ClusterSerializer.clusterFromBytes(new ByteArrayInputStream(cBytes), null); - System.out.println(cLoaded); - System.out.println(Arrays.toString(ClusterSerializer.toBytes(cLoaded))); - System.out.println(Arrays.equals(cBytes, ClusterSerializer.toBytes(cLoaded))); - } } diff --git a/src/main/java/nl/andrewlalis/crystalkeep/model/Cluster.java b/src/main/java/nl/andrewlalis/crystalkeep/model/Cluster.java index cd9ed4e..72fa186 100644 --- a/src/main/java/nl/andrewlalis/crystalkeep/model/Cluster.java +++ b/src/main/java/nl/andrewlalis/crystalkeep/model/Cluster.java @@ -27,7 +27,6 @@ public class Cluster implements Comparable, CrystalItem { public void addShard(Shard shard) { this.shards.add(shard); - shard.setCluster(this); } public void addCluster(Cluster cluster) { diff --git a/src/main/java/nl/andrewlalis/crystalkeep/model/Shard.java b/src/main/java/nl/andrewlalis/crystalkeep/model/Shard.java index c5de7de..a73f159 100644 --- a/src/main/java/nl/andrewlalis/crystalkeep/model/Shard.java +++ b/src/main/java/nl/andrewlalis/crystalkeep/model/Shard.java @@ -19,15 +19,12 @@ import java.util.Objects; */ @Getter public abstract class Shard implements Comparable, CrystalItem { - @Setter - private Cluster cluster; @Setter private String name; private final LocalDateTime createdAt; private final ShardType type; - public Shard(Cluster cluster, String name, LocalDateTime createdAt, ShardType type) { - this.cluster = cluster; + public Shard(String name, LocalDateTime createdAt, ShardType type) { this.name = name; this.createdAt = createdAt; this.type = type; @@ -45,12 +42,12 @@ public abstract class Shard implements Comparable, CrystalItem { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Shard shard = (Shard) o; - return getCluster().equals(shard.getCluster()) && getName().equals(shard.getName()) && getType() == shard.getType(); + return getName().equals(shard.getName()) && getType() == shard.getType(); } @Override public int hashCode() { - return Objects.hash(getCluster(), getName(), getType()); + return Objects.hash(getName(), getType()); } @Override diff --git a/src/main/java/nl/andrewlalis/crystalkeep/model/serialization/ClusterLoader.java b/src/main/java/nl/andrewlalis/crystalkeep/model/serialization/ClusterLoader.java index 4f5e55f..d38d5e0 100644 --- a/src/main/java/nl/andrewlalis/crystalkeep/model/serialization/ClusterLoader.java +++ b/src/main/java/nl/andrewlalis/crystalkeep/model/serialization/ClusterLoader.java @@ -5,6 +5,7 @@ import nl.andrewlalis.crystalkeep.model.Cluster; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; @@ -14,12 +15,13 @@ public class ClusterLoader { public Cluster loadDefault() throws IOException { InputStream is = new FileInputStream(DEFAULT_CLUSTER.toFile()); - return ClusterSerializer.clusterFromBytes(is, null); + return ClusterSerializer.readCluster(is, null); } public void saveDefault(Cluster cluster) throws IOException { Files.createDirectories(CLUSTER_PATH); - byte[] bytes = ClusterSerializer.toBytes(cluster); - Files.write(DEFAULT_CLUSTER, bytes); + OutputStream os = Files.newOutputStream(DEFAULT_CLUSTER); + ClusterSerializer.writeCluster(cluster, os); + os.close(); } } diff --git a/src/main/java/nl/andrewlalis/crystalkeep/model/serialization/ClusterSerializer.java b/src/main/java/nl/andrewlalis/crystalkeep/model/serialization/ClusterSerializer.java index fc89090..de9e58d 100644 --- a/src/main/java/nl/andrewlalis/crystalkeep/model/serialization/ClusterSerializer.java +++ b/src/main/java/nl/andrewlalis/crystalkeep/model/serialization/ClusterSerializer.java @@ -6,9 +6,9 @@ import nl.andrewlalis.crystalkeep.model.shards.LoginCredentialsShard; import nl.andrewlalis.crystalkeep.model.shards.ShardType; import nl.andrewlalis.crystalkeep.model.shards.TextShard; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; @@ -33,31 +33,28 @@ public class ClusterSerializer { /** * Serializes a cluster to a byte array, including all shards and nested * clusters. - * TODO: Use output stream instead of byte array. * @param cluster The cluster to serialize. - * @return The byte array representing the cluster. + * @param os The output stream to write to. * @throws IOException If an error occurs while writing the cluster. */ - public static byte[] toBytes(Cluster cluster) throws IOException { - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - ByteUtils.writeLengthPrefixed(cluster.getName(), bos); + public static void writeCluster(Cluster cluster, OutputStream os) throws IOException { + ByteUtils.writeLengthPrefixed(cluster.getName(), os); - bos.write(ByteUtils.toBytes(cluster.getClusters().size())); + os.write(ByteUtils.toBytes(cluster.getClusters().size())); Cluster[] children = new Cluster[cluster.getClusters().size()]; cluster.getClusters().toArray(children); Arrays.sort(children); for (Cluster child : children) { - bos.write(toBytes(child)); + writeCluster(child, os); } - bos.write(ByteUtils.toBytes(cluster.getShards().size())); + os.write(ByteUtils.toBytes(cluster.getShards().size())); Shard[] shards = new Shard[cluster.getShards().size()]; cluster.getShards().toArray(shards); Arrays.sort(shards); for (Shard shard : shards) { - bos.write(toBytes(shard)); + writeShard(shard, os); } - return bos.toByteArray(); } /** @@ -67,16 +64,16 @@ public class ClusterSerializer { * @return The cluster that was read. * @throws IOException If data could not be read from the stream. */ - public static Cluster clusterFromBytes(InputStream is, Cluster parent) throws IOException { + public static Cluster readCluster(InputStream is, Cluster parent) throws IOException { String name = ByteUtils.readLengthPrefixedString(is); Cluster cluster = new Cluster(name, new HashSet<>(), new HashSet<>(), parent); int childCount = toInt(is.readNBytes(4)); for (int i = 0; i < childCount; i++) { - cluster.addCluster(clusterFromBytes(is, cluster)); + cluster.addCluster(readCluster(is, cluster)); } int shardCount = toInt(is.readNBytes(4)); for (int i = 0; i < shardCount; i++) { - cluster.addShard(shardFromBytes(is, cluster)); + cluster.addShard(readShard(is)); } return cluster; } @@ -86,29 +83,26 @@ public class ClusterSerializer { * standard shard information, then using a specific {@link ShardSerializer} * to get the bytes that represent the body of the shard. * @param shard The shard to serialize. - * @return A byte array representing the shard. + * @param os The output stream to write to. * @throws IOException If byte array stream could not be written to. */ @SuppressWarnings({"unchecked", "rawtypes"}) - public static byte[] toBytes(Shard shard) throws IOException { - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - ByteUtils.writeLengthPrefixed(shard.getName().getBytes(StandardCharsets.UTF_8), bos); - ByteUtils.writeLengthPrefixed(shard.getCreatedAt().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME).getBytes(StandardCharsets.UTF_8), bos); - bos.write(ByteUtils.toBytes(shard.getType().getValue())); + public static void writeShard(Shard shard, OutputStream os) throws IOException { + ByteUtils.writeLengthPrefixed(shard.getName().getBytes(StandardCharsets.UTF_8), os); + ByteUtils.writeLengthPrefixed(shard.getCreatedAt().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME).getBytes(StandardCharsets.UTF_8), os); + os.write(ByteUtils.toBytes(shard.getType().getValue())); ShardSerializer serializer = serializers.get(shard.getType()); - bos.write(serializer.serialize(shard)); - return bos.toByteArray(); + os.write(serializer.serialize(shard)); } /** * Deserializes a shard from a byte array that's being read by the given * input stream. * @param is The input stream to read from. - * @param cluster The cluster that the shard should belong to. * @return The shard that was deserialized. * @throws IOException If an error occurs while reading bytes. */ - public static Shard shardFromBytes(InputStream is, Cluster cluster) throws IOException { + public static Shard readShard(InputStream is) throws IOException { String name = ByteUtils.readLengthPrefixedString(is); LocalDateTime createdAt = LocalDateTime.parse(ByteUtils.readLengthPrefixedString(is), DateTimeFormatter.ISO_LOCAL_DATE_TIME); ShardType type = ShardType.valueOf(ByteUtils.toInt(is.readNBytes(4))); @@ -116,6 +110,6 @@ public class ClusterSerializer { if (serializer == null) { throw new IOException("Unsupported shard type."); } - return serializer.deserialize(is, cluster, name, createdAt); + return serializer.deserialize(is, name, createdAt); } } diff --git a/src/main/java/nl/andrewlalis/crystalkeep/model/serialization/ShardSerializer.java b/src/main/java/nl/andrewlalis/crystalkeep/model/serialization/ShardSerializer.java index 3ce2b85..b789351 100644 --- a/src/main/java/nl/andrewlalis/crystalkeep/model/serialization/ShardSerializer.java +++ b/src/main/java/nl/andrewlalis/crystalkeep/model/serialization/ShardSerializer.java @@ -1,9 +1,7 @@ package nl.andrewlalis.crystalkeep.model.serialization; -import nl.andrewlalis.crystalkeep.model.Cluster; import nl.andrewlalis.crystalkeep.model.Shard; -import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.time.LocalDateTime; @@ -11,5 +9,5 @@ import java.time.LocalDateTime; public interface ShardSerializer { byte[] serialize(T shard) throws IOException; - T deserialize(InputStream bis, Cluster cluster, String name, LocalDateTime createdAt) throws IOException; + T deserialize(InputStream is, String name, LocalDateTime createdAt) throws IOException; } diff --git a/src/main/java/nl/andrewlalis/crystalkeep/model/shards/LoginCredentialsShard.java b/src/main/java/nl/andrewlalis/crystalkeep/model/shards/LoginCredentialsShard.java index e433948..baa3720 100644 --- a/src/main/java/nl/andrewlalis/crystalkeep/model/shards/LoginCredentialsShard.java +++ b/src/main/java/nl/andrewlalis/crystalkeep/model/shards/LoginCredentialsShard.java @@ -2,7 +2,6 @@ package nl.andrewlalis.crystalkeep.model.shards; import lombok.Getter; import lombok.Setter; -import nl.andrewlalis.crystalkeep.model.Cluster; import nl.andrewlalis.crystalkeep.model.Shard; import nl.andrewlalis.crystalkeep.model.serialization.ByteUtils; import nl.andrewlalis.crystalkeep.model.serialization.ShardSerializer; @@ -17,8 +16,8 @@ public class LoginCredentialsShard extends Shard { private String username; private String password; - public LoginCredentialsShard(Cluster cluster, String name, LocalDateTime createdAt, String username, String password) { - super(cluster, name, createdAt, ShardType.LOGIN_CREDENTIALS); + public LoginCredentialsShard(String name, LocalDateTime createdAt, String username, String password) { + super(name, createdAt, ShardType.LOGIN_CREDENTIALS); this.username = username; this.password = password; } @@ -36,10 +35,10 @@ public class LoginCredentialsShard extends Shard { } @Override - public LoginCredentialsShard deserialize(InputStream is, Cluster cluster, String name, LocalDateTime createdAt) throws IOException { + public LoginCredentialsShard deserialize(InputStream is, String name, LocalDateTime createdAt) throws IOException { String username = ByteUtils.readLengthPrefixedString(is); String password = ByteUtils.readLengthPrefixedString(is); - return new LoginCredentialsShard(cluster, name, createdAt, username, password); + return new LoginCredentialsShard(name, createdAt, username, password); } } } diff --git a/src/main/java/nl/andrewlalis/crystalkeep/model/shards/TextShard.java b/src/main/java/nl/andrewlalis/crystalkeep/model/shards/TextShard.java index 6fade21..7ab6c08 100644 --- a/src/main/java/nl/andrewlalis/crystalkeep/model/shards/TextShard.java +++ b/src/main/java/nl/andrewlalis/crystalkeep/model/shards/TextShard.java @@ -14,8 +14,8 @@ import java.time.LocalDateTime; public class TextShard extends Shard { private String text; - public TextShard(Cluster cluster, String name, LocalDateTime createdAt, String text) { - super(cluster, name, createdAt, ShardType.TEXT); + public TextShard(String name, LocalDateTime createdAt, String text) { + super(name, createdAt, ShardType.TEXT); this.text = text; } @@ -31,9 +31,9 @@ public class TextShard extends Shard { } @Override - public TextShard deserialize(InputStream is, Cluster cluster, String name, LocalDateTime createdAt) throws IOException { + public TextShard deserialize(InputStream is, String name, LocalDateTime createdAt) throws IOException { String text = ByteUtils.readLengthPrefixedString(is); - return new TextShard(cluster, name, createdAt, text); + return new TextShard(name, createdAt, text); } } } diff --git a/src/test/java/nl/andrewlalis/crystalkeep/model/serialization/ClusterSerializerTest.java b/src/test/java/nl/andrewlalis/crystalkeep/model/serialization/ClusterSerializerTest.java new file mode 100644 index 0000000..67fcb49 --- /dev/null +++ b/src/test/java/nl/andrewlalis/crystalkeep/model/serialization/ClusterSerializerTest.java @@ -0,0 +1,54 @@ +package nl.andrewlalis.crystalkeep.model.serialization; + +import nl.andrewlalis.crystalkeep.model.Shard; +import nl.andrewlalis.crystalkeep.model.shards.LoginCredentialsShard; +import nl.andrewlalis.crystalkeep.model.shards.TextShard; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +public class ClusterSerializerTest { + private static List testShardIOData() { + return List.of( + new TextShard("a", LocalDateTime.now(), "Hello world!"), + new TextShard("Another", LocalDateTime.now(), "Testing"), + new TextShard("", LocalDateTime.now(), ""), + new LoginCredentialsShard("login", LocalDateTime.now(), "andrew", "password"), + new LoginCredentialsShard("test", LocalDateTime.now(), "", "") + ); + } + + @ParameterizedTest + @MethodSource("testShardIOData") + public void testShardIO(Shard s1) throws IOException { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ClusterSerializer.writeShard(s1, bos); + byte[] data = bos.toByteArray(); + assertNotEquals(0, data.length, "Serialized shard should never result in empty bytes."); + assertEquals( + s1.getName().length(), + ByteUtils.toInt(Arrays.copyOfRange(data, 0, 4)), + "Serialized shard name length does not match expected." + ); + assertEquals( + s1.getName(), + new String(Arrays.copyOfRange(data, 4, 4 + s1.getName().length())), + "First letter of shard name does not match expected." + ); + + Shard loadedS1 = ClusterSerializer.readShard(new ByteArrayInputStream(data)); + assertEquals(s1, loadedS1, "Loaded shard should equal original shard."); + bos.reset(); + ClusterSerializer.writeShard(loadedS1, bos); + byte[] data2 = bos.toByteArray(); + assertArrayEquals(data, data2, "Serialized data from a shard should not change."); + } +} diff --git a/src/test/java/nl/andrewlalis/crystalkeep/package-info.java b/src/test/java/nl/andrewlalis/crystalkeep/package-info.java new file mode 100644 index 0000000..da5f043 --- /dev/null +++ b/src/test/java/nl/andrewlalis/crystalkeep/package-info.java @@ -0,0 +1,4 @@ +/** + * This package contains all tests for CrystalKeep. + */ +package nl.andrewlalis.crystalkeep; \ No newline at end of file