From 23aee75962f8cf91ef711bfb115b423161ad3e99 Mon Sep 17 00:00:00 2001 From: Tobias Eidelpes Date: Tue, 13 Oct 2020 14:19:16 +0200 Subject: [PATCH] Initial commit --- .gitignore | 149 ++++++++++++++ .idea/.gitignore | 3 + .idea/.name | 1 + .idea/compiler.xml | 6 + .idea/misc.xml | 5 + README.md | 29 +++ build.gradle | 68 +++++++ gradle/wrapper/gradle-wrapper.properties | 5 + gradlew | 185 ++++++++++++++++++ gradlew.bat | 89 +++++++++ settings.gradle | 5 + src/main/java/dslab/ComponentFactory.java | 80 ++++++++ .../java/dslab/mailbox/IMailboxServer.java | 23 +++ .../java/dslab/mailbox/MailboxServer.java | 37 ++++ .../dslab/monitoring/IMonitoringServer.java | 33 ++++ .../dslab/monitoring/MonitoringServer.java | 48 +++++ .../java/dslab/transfer/ITransferServer.java | 22 +++ .../java/dslab/transfer/TransferServer.java | 38 ++++ src/main/java/dslab/util/Config.java | 79 ++++++++ src/main/resources/domains.properties | 4 + .../resources/mailbox-earth-planet.properties | 25 +++ .../resources/mailbox-univer-ze.properties | 25 +++ src/main/resources/monitoring.properties | 3 + src/main/resources/transfer-1.properties | 20 ++ src/main/resources/transfer-2.properties | 20 ++ .../resources/users-earth-planet.properties | 3 + src/main/resources/users-univer-ze.properties | 2 + src/test/java/dslab/CheckedConsumer.java | 9 + src/test/java/dslab/Constants.java | 14 ++ src/test/java/dslab/JunitSocketClient.java | 135 +++++++++++++ src/test/java/dslab/NullOutputStream.java | 38 ++++ src/test/java/dslab/Sockets.java | 84 ++++++++ src/test/java/dslab/StreamListener.java | 80 ++++++++ src/test/java/dslab/StringMatches.java | 30 +++ src/test/java/dslab/TestBase.java | 30 +++ src/test/java/dslab/TestInputStream.java | 99 ++++++++++ src/test/java/dslab/TestOutputStream.java | 150 ++++++++++++++ .../mailbox/MailboxServerProtocolTest.java | 108 ++++++++++ .../java/dslab/mailbox/MailboxServerTest.java | 75 +++++++ .../MonitoringServerProtocolTest.java | 92 +++++++++ .../monitoring/MonitoringServerTest.java | 60 ++++++ .../transfer/TransferServerProtocolTest.java | 71 +++++++ .../dslab/transfer/TransferServerTest.java | 60 ++++++ src/test/resources/.gitkeep | 0 44 files changed, 2142 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/.gitignore create mode 100644 .idea/.name create mode 100644 .idea/compiler.xml create mode 100644 .idea/misc.xml create mode 100644 README.md create mode 100644 build.gradle create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle create mode 100644 src/main/java/dslab/ComponentFactory.java create mode 100644 src/main/java/dslab/mailbox/IMailboxServer.java create mode 100644 src/main/java/dslab/mailbox/MailboxServer.java create mode 100644 src/main/java/dslab/monitoring/IMonitoringServer.java create mode 100644 src/main/java/dslab/monitoring/MonitoringServer.java create mode 100644 src/main/java/dslab/transfer/ITransferServer.java create mode 100644 src/main/java/dslab/transfer/TransferServer.java create mode 100644 src/main/java/dslab/util/Config.java create mode 100644 src/main/resources/domains.properties create mode 100644 src/main/resources/mailbox-earth-planet.properties create mode 100644 src/main/resources/mailbox-univer-ze.properties create mode 100644 src/main/resources/monitoring.properties create mode 100644 src/main/resources/transfer-1.properties create mode 100644 src/main/resources/transfer-2.properties create mode 100644 src/main/resources/users-earth-planet.properties create mode 100644 src/main/resources/users-univer-ze.properties create mode 100644 src/test/java/dslab/CheckedConsumer.java create mode 100644 src/test/java/dslab/Constants.java create mode 100644 src/test/java/dslab/JunitSocketClient.java create mode 100644 src/test/java/dslab/NullOutputStream.java create mode 100644 src/test/java/dslab/Sockets.java create mode 100644 src/test/java/dslab/StreamListener.java create mode 100644 src/test/java/dslab/StringMatches.java create mode 100644 src/test/java/dslab/TestBase.java create mode 100644 src/test/java/dslab/TestInputStream.java create mode 100644 src/test/java/dslab/TestOutputStream.java create mode 100644 src/test/java/dslab/mailbox/MailboxServerProtocolTest.java create mode 100644 src/test/java/dslab/mailbox/MailboxServerTest.java create mode 100644 src/test/java/dslab/monitoring/MonitoringServerProtocolTest.java create mode 100644 src/test/java/dslab/monitoring/MonitoringServerTest.java create mode 100644 src/test/java/dslab/transfer/TransferServerProtocolTest.java create mode 100644 src/test/java/dslab/transfer/TransferServerTest.java create mode 100644 src/test/resources/.gitkeep diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..01db763 --- /dev/null +++ b/.gitignore @@ -0,0 +1,149 @@ +# Created by .ignore support plugin (hsz.mobi) +### 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 + +### Gradle template +.gradle +**/build/ +!src/**/build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Cache of project +.gradletasknamecache + +# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 +# gradle/wrapper/gradle-wrapper.properties + +### 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* + +### 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 + +### Vim template +# Swap +[._]*.s[a-v][a-z] +!*.svg # comment out if you don't need vector files +[._]*.sw[a-p] +[._]s[a-rt-v][a-z] +[._]ss[a-gi-z] +[._]sw[a-p] + +# Session +Session.vim +Sessionx.vim + +# Temporary +.netrwhist +*~ +# Auto-generated tag files +tags +# Persistent undo +[._]*.un~ diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..5c06ea1 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +dslab20 \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..fb7f4a8 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..4663731 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..b6d72b4 --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +distributed systems lab +======================= + +Using gradle +------------ + +### Compile & Test + +Gradle is the build tool we are using. Here are some instructions: + +Compile the project using the gradle wrapper: + + ./gradlew assemble + +Compile and run the tests: + + ./gradlew build + +### Run the applications + +The gradle config config contains several tasks that start application components for you. +You can list them with + + ./gradlew tasks --all + +And search for 'Other tasks' starting with `run-`. For example, to run the monitoring server, execute: +(the `--console=plain` flag disables CLI features, like color output, that may break the console output when running a interactive application) + + ./gradlew --console=plain run-monitoring diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..e324d3d --- /dev/null +++ b/build.gradle @@ -0,0 +1,68 @@ +/* + * This file was generated by the Gradle 'init' task. + */ + +plugins { + id 'java-library' + id 'java' +} + +group = 'at.ac.tuwien.infosys.dslab' +version = '2020' + +java { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} + +dependencies { + implementation files('lib/commons-logging-1.2.jar') + implementation files('lib/hamcrest-core-1.3.jar') + implementation files('lib/junit-4.12.jar') + implementation files('lib/orvell-core-0.2.0.jar') +} + +// ======== run specifications + +tasks.withType(JavaCompile) { + options.encoding = 'UTF-8' +} + +def dslabRunTasks = [ + [name: 'run-monitoring', main: 'dslab.monitoring.MonitoringServer', args: 'monitoring', description: 'Run Monitoring Server'], + [name: 'run-transfer-1', main: 'dslab.transfer.TransferServer', args: 'transfer-1', description: 'Run Transfer Server #1'], + [name: 'run-transfer-2', main: 'dslab.transfer.TransferServer', args: 'transfer-2', description: 'Run Transfer Server #2'], + [name: 'run-mailbox-earth-planet', main: 'dslab.mailbox.MailboxServer', args: 'mailbox-earth-planet', description: 'Run Mailbox Server for earth.planet'], + [name: 'run-mailbox-univer-ze', main: 'dslab.mailbox.MailboxServer', args: 'mailbox-univer-ze', description: 'Run Mailbox Server for univer.ze'], + // assignment 2 + [name: 'run-ns-root', main: 'dslab.nameserver.Nameserver', args: 'ns-root', description: 'Run root nameserver'], + [name: 'run-ns-ze', main: 'dslab.nameserver.Nameserver', args: 'ns-ze', description: 'Run .ze nameserver'], + [name: 'run-ns-planet', main: 'dslab.nameserver.Nameserver', args: 'ns-planet', description: 'Run .planet nameserver'], + [name: 'run-ns-earth-planet', main: 'dslab.nameserver.Nameserver', args: 'ns-earth-planet', description: 'Run .earth.planet nameserver'], + [name: 'run-client-trillian', main: 'dslab.client.MessageClient', args: 'client-trillian', description: 'Run client for trillian'], + [name: 'run-client-arthur', main: 'dslab.client.MessageClient', args: 'client-arthur', description: 'Run client for arthur'], + [name: 'run-client-zaphod', main: 'dslab.client.MessageClient', args: 'client-zaphod', description: 'Run client for zaphod'], +] + +// dynamically create run tasks of this structure: +// +// task 'run-transfer-1'(dependsOn: 'classes', type: JavaExec) { +// main = 'dslab.transfer.TransferServer' +// args 'transfer-2' +// classpath = sourceSets.main.runtimeClasspath +// standardInput = System.in +// } + +for (t in dslabRunTasks) { + def taskName = t['name'] + + task "$taskName"(dependsOn: 'classes', type: JavaExec) { + main = t['main'] + args t['args'] + description = t['description'] + + classpath = sourceSets.main.runtimeClasspath + // https://discuss.gradle.org/t/why-doesnt-system-in-read-block-when-im-using-gradle/3308 + standardInput = System.in + } +} diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..12d38de --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..4f906e0 --- /dev/null +++ b/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..ac1b06f --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..64fca9d --- /dev/null +++ b/settings.gradle @@ -0,0 +1,5 @@ +/* + * This file was generated by the Gradle 'init' task. + */ + +rootProject.name = 'dslab20' diff --git a/src/main/java/dslab/ComponentFactory.java b/src/main/java/dslab/ComponentFactory.java new file mode 100644 index 0000000..90e0ace --- /dev/null +++ b/src/main/java/dslab/ComponentFactory.java @@ -0,0 +1,80 @@ +package dslab; + +import java.io.InputStream; +import java.io.PrintStream; + +import dslab.mailbox.IMailboxServer; +import dslab.mailbox.MailboxServer; +import dslab.monitoring.IMonitoringServer; +import dslab.monitoring.MonitoringServer; +import dslab.transfer.ITransferServer; +import dslab.transfer.TransferServer; +import dslab.util.Config; + +/** + * The component factory provides methods to create the core components of the application. You can edit the method body + * if the component instantiation requires additional logic. + * + * Do not change the existing method signatures! + */ +public final class ComponentFactory { + + private ComponentFactory() { + // static utility class + } + + /** + * Creates a new {@link IMonitoringServer} instance. + * + * @param componentId the component id + * @param in the input stream used for accepting cli commands + * @param out the output stream to print to + * @return a new MonitoringServer instance + */ + public static IMonitoringServer createMonitoringServer(String componentId, InputStream in, PrintStream out) + throws Exception { + /* + * TODO: Here you can modify the code (if necessary) to instantiate your components + */ + + Config config = new Config(componentId); + return new MonitoringServer(componentId, config, in, out); + } + + /** + * Creates a new {@link IMailboxServer} instance. + * + * @param componentId the component id + * @param in the input stream used for accepting cli commands + * @param out the output stream to print to + * @return a new MailboxServer instance + */ + public static IMailboxServer createMailboxServer(String componentId, InputStream in, PrintStream out) + throws Exception { + /* + * TODO: Here you can modify the code (if necessary) to instantiate your components + */ + + Config config = new Config(componentId); + return new MailboxServer(componentId, config, in, out); + } + + /** + * Creates a new {@link ITransferServer} instance. + * + * @param componentId the component id + * @param in the input stream used for accepting cli commands + * @param out the output stream to print to + * @return a new TransferServer instance + */ + public static ITransferServer createTransferServer(String componentId, InputStream in, PrintStream out) + throws Exception { + /* + * TODO: Here you can modify the code (if necessary) to instantiate your components + */ + + Config config = new Config(componentId); + return new TransferServer(componentId, config, in, out); + } + +} diff --git a/src/main/java/dslab/mailbox/IMailboxServer.java b/src/main/java/dslab/mailbox/IMailboxServer.java new file mode 100644 index 0000000..07d745f --- /dev/null +++ b/src/main/java/dslab/mailbox/IMailboxServer.java @@ -0,0 +1,23 @@ +package dslab.mailbox; + +/** + * The mailbox server receives mails via DMTP from transfer servers, and makes them available to users via the DMAP + * protocol. + * + * Do not change the existing method signatures! + */ +public interface IMailboxServer extends Runnable { + + /** + * Starts the server. + */ + @Override + void run(); + + /** + * CLI command to shut down the server. After this method, all resources should be closed, and the application + * should terminate. + */ + void shutdown(); + +} diff --git a/src/main/java/dslab/mailbox/MailboxServer.java b/src/main/java/dslab/mailbox/MailboxServer.java new file mode 100644 index 0000000..f422eda --- /dev/null +++ b/src/main/java/dslab/mailbox/MailboxServer.java @@ -0,0 +1,37 @@ +package dslab.mailbox; + +import java.io.InputStream; +import java.io.PrintStream; + +import dslab.ComponentFactory; +import dslab.util.Config; + +public class MailboxServer implements IMailboxServer, Runnable { + + /** + * Creates a new server instance. + * + * @param componentId the id of the component that corresponds to the Config resource + * @param config the component config + * @param in the input stream to read console input from + * @param out the output stream to write console output to + */ + public MailboxServer(String componentId, Config config, InputStream in, PrintStream out) { + // TODO + } + + @Override + public void run() { + // TODO + } + + @Override + public void shutdown() { + // TODO + } + + public static void main(String[] args) throws Exception { + IMailboxServer server = ComponentFactory.createMailboxServer(args[0], System.in, System.out); + server.run(); + } +} diff --git a/src/main/java/dslab/monitoring/IMonitoringServer.java b/src/main/java/dslab/monitoring/IMonitoringServer.java new file mode 100644 index 0000000..99c0ea6 --- /dev/null +++ b/src/main/java/dslab/monitoring/IMonitoringServer.java @@ -0,0 +1,33 @@ +package dslab.monitoring; + +/** + * The monitoring service accepts incoming monitoring packets via UDP. It provides CLI commands to access the + * information. + * + * Do not change the existing method signatures! + */ +public interface IMonitoringServer extends Runnable { + + /** + * Starts the server. + */ + @Override + void run(); + + /** + * CLI command to shut down the server. After this method, all resources should be closed, and the application + * should terminate. + */ + void shutdown(); + + /** + * CLI command to report usage statistics for transfer servers. + */ + void servers(); + + /** + * CLI command to report usage statistics for individual senders. + */ + void addresses(); + +} diff --git a/src/main/java/dslab/monitoring/MonitoringServer.java b/src/main/java/dslab/monitoring/MonitoringServer.java new file mode 100644 index 0000000..12bba90 --- /dev/null +++ b/src/main/java/dslab/monitoring/MonitoringServer.java @@ -0,0 +1,48 @@ +package dslab.monitoring; + +import java.io.InputStream; +import java.io.PrintStream; + +import dslab.ComponentFactory; +import dslab.util.Config; + +public class MonitoringServer implements IMonitoringServer { + + /** + * Creates a new server instance. + * + * @param componentId the id of the component that corresponds to the Config resource + * @param config the component config + * @param in the input stream to read console input from + * @param out the output stream to write console output to + */ + public MonitoringServer(String componentId, Config config, InputStream in, PrintStream out) { + // TODO + } + + @Override + public void run() { + // TODO + } + + @Override + public void addresses() { + // TODO + } + + @Override + public void servers() { + // TODO + } + + @Override + public void shutdown() { + // TODO + } + + public static void main(String[] args) throws Exception { + IMonitoringServer server = ComponentFactory.createMonitoringServer(args[0], System.in, System.out); + server.run(); + } + +} diff --git a/src/main/java/dslab/transfer/ITransferServer.java b/src/main/java/dslab/transfer/ITransferServer.java new file mode 100644 index 0000000..b6961d7 --- /dev/null +++ b/src/main/java/dslab/transfer/ITransferServer.java @@ -0,0 +1,22 @@ +package dslab.transfer; + +/** + * The transfer server is responsible for accepting mails sent by users, and forward them to mailbox servers via DMTP. + * It also reports usage statistics to the monitoring server. + * + * Do not change the existing method signatures! + */ +public interface ITransferServer extends Runnable { + + /** + * Starts the server. + */ + @Override + void run(); + + /** + * CLI command to shut down the server. After this method, all resources should be closed, and the application + * should terminate. + */ + void shutdown(); +} diff --git a/src/main/java/dslab/transfer/TransferServer.java b/src/main/java/dslab/transfer/TransferServer.java new file mode 100644 index 0000000..e4e9c89 --- /dev/null +++ b/src/main/java/dslab/transfer/TransferServer.java @@ -0,0 +1,38 @@ +package dslab.transfer; + +import java.io.InputStream; +import java.io.PrintStream; + +import dslab.ComponentFactory; +import dslab.util.Config; + +public class TransferServer implements ITransferServer, Runnable { + + /** + * Creates a new server instance. + * + * @param componentId the id of the component that corresponds to the Config resource + * @param config the component config + * @param in the input stream to read console input from + * @param out the output stream to write console output to + */ + public TransferServer(String componentId, Config config, InputStream in, PrintStream out) { + // TODO + } + + @Override + public void run() { + // TODO + } + + @Override + public void shutdown() { + // TODO + } + + public static void main(String[] args) throws Exception { + ITransferServer server = ComponentFactory.createTransferServer(args[0], System.in, System.out); + server.run(); + } + +} diff --git a/src/main/java/dslab/util/Config.java b/src/main/java/dslab/util/Config.java new file mode 100644 index 0000000..e0fa04d --- /dev/null +++ b/src/main/java/dslab/util/Config.java @@ -0,0 +1,79 @@ +package dslab.util; + +import java.util.HashMap; +import java.util.Map; +import java.util.ResourceBundle; +import java.util.Set; + +/** + * Reads the configuration from a {@code .properties} file. + */ +public final class Config { + + private final ResourceBundle bundle; + private Map properties = new HashMap<>(); + + /** + * Creates an instance of Config which reads configuration data form {@code .properties} file with given name found + * in classpath. + * + * @param name the name of the .properties file + */ + public Config(String name) { + if (name.endsWith(".properties")) { + this.bundle = ResourceBundle.getBundle(name.substring(0, name.length() - 11)); + } else { + this.bundle = ResourceBundle.getBundle(name); + } + } + + /** + * Returns the value as String for the given key. + * + * @param key the property's key + * @return String value of the property + * @see ResourceBundle#getString(String) + */ + public String getString(String key) { + if (properties.containsKey(key)) { + return properties.get(key).toString(); + } + return this.bundle.getString(key); + } + + /** + * Returns the value as {@code int} for the given key. + * + * @param key the property's key + * @return int value of the property + * @throws NumberFormatException if the String cannot be parsed to an Integer + */ + public int getInt(String key) { + return Integer.parseInt(getString(key)); + } + + public boolean containsKey(String key) { + return properties.containsKey(key) || bundle.containsKey(key); + } + + /** + * Sets the value for the given key. + * + * @param key the property's key + * @param value the value of the property + */ + public void setProperty(String key, Object value) { + properties.put(key, value); + } + + /** + * Returns all keys of this configuration. + * + * @return the keys + */ + public Set listKeys() { + Set keys = bundle.keySet(); + keys.addAll(properties.keySet()); + return keys; + } +} diff --git a/src/main/resources/domains.properties b/src/main/resources/domains.properties new file mode 100644 index 0000000..622fcaa --- /dev/null +++ b/src/main/resources/domains.properties @@ -0,0 +1,4 @@ +# = +# TODO: replace ports with the ports that your mailbox servers use +earth.planet=127.0.0.1:port_range+2 +univer.ze=127.0.0.1:port_range+4 diff --git a/src/main/resources/mailbox-earth-planet.properties b/src/main/resources/mailbox-earth-planet.properties new file mode 100644 index 0000000..acf1724 --- /dev/null +++ b/src/main/resources/mailbox-earth-planet.properties @@ -0,0 +1,25 @@ +# TCP port used for the DMTP server socket +# TODO: REPLACE with real value such as 16502 - considering the port range associated with your account - you have received after Lab 0 a port range (beginning_of_the_range, end_of_the_range) +dmtp.tcp.port=port_range+2 + +# TCP port used for the DMAP server socket +# TODO: REPLACE with real value such as 16503 - considering the port range associated with your account - you have received after Lab 0 a port range (beginning_of_the_range, end_of_the_range) +dmap.tcp.port=port_range+3 + +# mail domain managed by the mailbox server +domain=earth.planet + +# location of the users for this mailbox server +users.config=users-earth-planet.properties + +# ============================================= Required for Assignment 2 + +# name of the root nameserver's remote object +root_id=root-nameserver + +# RMI registry host +registry.host=localhost + +# RMI registry port +# TODO: REPLACE with real value such as 16509 - considering the port range associated with your account - you have received after Lab 0 a port range (beginning_of_the_range, end_of_the_range) +registry.port=port_range+9 diff --git a/src/main/resources/mailbox-univer-ze.properties b/src/main/resources/mailbox-univer-ze.properties new file mode 100644 index 0000000..f07c01d --- /dev/null +++ b/src/main/resources/mailbox-univer-ze.properties @@ -0,0 +1,25 @@ +# TCP port used for the DMTP server socket +# TODO: REPLACE with real value such as 16504 - considering the port range associated with your account - you have received after Lab 0 a port range (beginning_of_the_range, end_of_the_range) +dmtp.tcp.port=port_range+4 + +# TCP port used for the DMAP server socket +# TODO: REPLACE with real value such as 16505 - considering the port range associated with your account - you have received after Lab 0 a port range (beginning_of_the_range, end_of_the_range) +dmap.tcp.port=port_range+5 + +# mail domain managed by the mailbox server +domain=univer.ze + +# location of the users for this mailbox server +users.config=users-univer-ze.properties + +# ============================================= Required for Assignment 2 + +# name of the root nameserver's remote object +root_id=root-nameserver + +# RMI registry host +registry.host=localhost + +# RMI registry port +# TODO: REPLACE with real value such as 16509 - considering the port range associated with your account - you have received after Lab 0 a port range (beginning_of_the_range, end_of_the_range) +registry.port=port_range+9 diff --git a/src/main/resources/monitoring.properties b/src/main/resources/monitoring.properties new file mode 100644 index 0000000..8aff76c --- /dev/null +++ b/src/main/resources/monitoring.properties @@ -0,0 +1,3 @@ +# UDP port used for accepting monitoring packets +# TODO: REPLACE with real value such as 16508 - considering the port range associated with your account - you have received after Lab 0 a port range (beginning_of_the_range, end_of_the_range) +udp.port=port_range+8 diff --git a/src/main/resources/transfer-1.properties b/src/main/resources/transfer-1.properties new file mode 100644 index 0000000..0b87450 --- /dev/null +++ b/src/main/resources/transfer-1.properties @@ -0,0 +1,20 @@ +# TCP port used for the DMTP server socket +# TODO: REPLACE with real value such as 16500 - considering the port range associated with your account - you have received after Lab 0 a port range (beginning_of_the_range, end_of_the_range) +tcp.port=port_range + +# UDP socket address of the monitoring server +monitoring.host=127.0.0.1 +# TODO: REPLACE with the real value of the monitoring server port +monitoring.port=port_range+8 + +# ============================================= Required for Assignment 2 + +# name of the root nameserver's remote object +root_id=root-nameserver + +# RMI registry host +registry.host=localhost + +# RMI registry port +# TODO: REPLACE with real value such as 16509 - considering the port range associated with your account - you have received after Lab 0 a port range (beginning_of_the_range, end_of_the_range) +registry.port=port_range+9 diff --git a/src/main/resources/transfer-2.properties b/src/main/resources/transfer-2.properties new file mode 100644 index 0000000..11757ab --- /dev/null +++ b/src/main/resources/transfer-2.properties @@ -0,0 +1,20 @@ +# TCP port used for the DMTP server socket +# TODO: REPLACE with real value such as 16501 - considering the port range associated with your account - you have received after Lab 0 a port range (beginning_of_the_range, end_of_the_range) +tcp.port=port_range+1 + +# UDP socket address of the monitoring server +monitoring.host=127.0.0.1 +# TODO: REPLACE with the real value of the monitoring server port +monitoring.port=port_range+8 + +# ============================================= Required for Assignment 2 + +# name of the root nameserver's remote object +root_id=root-nameserver + +# RMI registry host +registry.host=localhost + +# RMI registry port +# TODO: REPLACE with real value such as 16509 - considering the port range associated with your account - you have received after Lab 0 a port range (beginning_of_the_range, end_of_the_range) +registry.port=port_range+9 diff --git a/src/main/resources/users-earth-planet.properties b/src/main/resources/users-earth-planet.properties new file mode 100644 index 0000000..1faf335 --- /dev/null +++ b/src/main/resources/users-earth-planet.properties @@ -0,0 +1,3 @@ +# = +trillian=12345 +arthur=23456 diff --git a/src/main/resources/users-univer-ze.properties b/src/main/resources/users-univer-ze.properties new file mode 100644 index 0000000..ae3c292 --- /dev/null +++ b/src/main/resources/users-univer-ze.properties @@ -0,0 +1,2 @@ +# = +zaphod=12345 diff --git a/src/test/java/dslab/CheckedConsumer.java b/src/test/java/dslab/CheckedConsumer.java new file mode 100644 index 0000000..c5b9c00 --- /dev/null +++ b/src/test/java/dslab/CheckedConsumer.java @@ -0,0 +1,9 @@ +package dslab; + +/** + * CheckedConsumer. + */ +@FunctionalInterface +public interface CheckedConsumer { + void accept(T socket) throws E; +} diff --git a/src/test/java/dslab/Constants.java b/src/test/java/dslab/Constants.java new file mode 100644 index 0000000..9c488d3 --- /dev/null +++ b/src/test/java/dslab/Constants.java @@ -0,0 +1,14 @@ +package dslab; + +public interface Constants { + + /** + * Default time (in milliseconds) to wait after starting a component to test. + */ + long COMPONENT_STARTUP_WAIT = 3000; + + /** + * Default time (in milliseconds) to wait after shutting down a component to test. + */ + long COMPONENT_TEARDOWN_WAIT = 3000; +} diff --git a/src/test/java/dslab/JunitSocketClient.java b/src/test/java/dslab/JunitSocketClient.java new file mode 100644 index 0000000..73419e6 --- /dev/null +++ b/src/test/java/dslab/JunitSocketClient.java @@ -0,0 +1,135 @@ +package dslab; + +import static org.hamcrest.CoreMatchers.containsString; + +import java.io.Closeable; +import java.io.IOException; +import java.io.PrintWriter; +import java.net.Socket; +import java.util.concurrent.TimeUnit; + +import org.hamcrest.Matcher; +import org.junit.Assert; +import org.junit.rules.ErrorCollector; + +public class JunitSocketClient implements Closeable { + + private ErrorCollector err; + + private Socket socket; + private PrintWriter writer; + + private StreamListener listener; + + /** + * Creates a new Socket that connects to localhost on the given port and holds the I/O resources. + * + * @param port the port to connect to + * @throws IOException if an I/O error occurred while connecting. + */ + public JunitSocketClient(int port) throws IOException { + this(new Socket("127.0.0.1", port)); + } + + public JunitSocketClient(Socket socket) throws IOException { + this.socket = socket; + this.writer = new PrintWriter(socket.getOutputStream()); + this.listener = new StreamListener(socket.getInputStream()); + + new Thread(listener).start(); + } + + /** + * Creates a new Socket that connects to localhost on the given port and holds the I/O resources. + * + * @param port the port to connect to + * @param err the error collector used to verify communication + * @throws IOException if an I/O error occurred while connecting. + */ + + public JunitSocketClient(int port, ErrorCollector err) throws IOException { + this(port); + this.err = err; + } + + public Socket getSocket() { + return socket; + } + + public PrintWriter getWriter() { + return writer; + } + + public void send(String message) { + writer.println(message); + writer.flush(); + } + + public String sendAndListen(String message) { + send(message); + return listen(); + } + + public String listen() { + return listen(1, TimeUnit.SECONDS); + } + + public String listen(long time, TimeUnit timeUnit) { + return listener.listen(time, timeUnit); + } + + public String read() throws IOException { + return listener.poll(1, TimeUnit.MINUTES); + } + + /** + * Reads a line from the input stream and verifies that it contains the given string. + * + * @param string the partial string to match + * @throws IOException on read errors + */ + public void verify(String string) throws IOException { + assertThat(read(), containsString(string)); + } + + /** + * Writes the given string to the output stream, and then behaves like {@link #verify(String)}. + * + * @param request the request to send + * @param response the expected response (partial string match) + * @throws IOException on I/O errors + */ + public void sendAndVerify(String request, String response) throws IOException { + assertThat(sendAndRead(request), containsString(response)); + } + + public String sendAndRead(String message) throws IOException { + send(message); + return read(); + } + + @Override + public void close() throws IOException { + closeQuietly(listener); + closeQuietly(writer); + closeQuietly(socket); + } + + private void assertThat(T actual, Matcher matcher) { + if (err != null) { + err.checkThat(actual, matcher); + } else { + Assert.assertThat(actual, matcher); + } + } + + private void closeQuietly(Closeable closeable) { + if (closeable != null) { + try { + closeable.close(); + } catch (IOException e) { + // ignore + } + } + } +} diff --git a/src/test/java/dslab/NullOutputStream.java b/src/test/java/dslab/NullOutputStream.java new file mode 100644 index 0000000..9353e42 --- /dev/null +++ b/src/test/java/dslab/NullOutputStream.java @@ -0,0 +1,38 @@ +package dslab; + +import java.io.OutputStream; + +/** + * This {@link OutputStream} has no destination (file/socket etc.) and all bytes written to it are ignored and lost. + */ +public final class NullOutputStream extends OutputStream { + public static final OutputStream INSTANCE = new NullOutputStream(); + + private NullOutputStream() { + } + + public static OutputStream getInstance() { + return INSTANCE; + } + + /** + * Discards the specified byte. + */ + @Override + public void write(int b) { + } + + /** + * Discards the specified byte array. + */ + @Override + public void write(byte[] b) { + } + + /** + * Discards the specified byte array. + */ + @Override + public void write(byte[] b, int off, int len) { + } +} diff --git a/src/test/java/dslab/Sockets.java b/src/test/java/dslab/Sockets.java new file mode 100644 index 0000000..cbb2632 --- /dev/null +++ b/src/test/java/dslab/Sockets.java @@ -0,0 +1,84 @@ +package dslab; + +import java.io.IOException; +import java.net.BindException; +import java.net.DatagramSocket; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketException; +import java.net.SocketTimeoutException; + +/** + * Util class for checking sockets. + */ +public final class Sockets { + + private Sockets() { + // util class + } + + /** + * Waits a given time for a TCP server socket to be opened at the given host and port by repeatedly (every 10 ms) + * trying to connect to the address. + * + * @param host the expected server socket host + * @param port the expected server socket port + * @param ms the time in milliseconds. + * @throws SocketTimeoutException if the timeout period was reached + */ + public static void waitForSocket(String host, int port, long ms) throws SocketTimeoutException { + long interval = 10; + + while (ms > 0) { + try (Socket ignored = new Socket(host, port)) { + return; + } catch (IOException e) { + ms -= interval; + + try { + Thread.sleep(interval); + } catch (InterruptedException e1) { + break; + } + } + } + + throw new SocketTimeoutException("Gave up waiting for socket " + host + ":" + port); + } + + /** + * Tries to open a DatagramSocket on the given port. If a BindException is thrown, it indicates that the socket was + * open before, and the method will return true. If the socket was opened successfully, the method returns false and + * immediately closes the socket. + * + * @param port the local port + * @return true if the socket was already open + * @throws SocketException if the socket could not be opened due to some other reason than a bind exception + */ + public static boolean isDatagramSocketOpen(int port) throws SocketException { + try (DatagramSocket socket = new DatagramSocket(port)) { + return false; + } catch (BindException e) { + return true; + } + } + + /** + * Tries to open a ServerSocket on the given port. If a BindException is thrown, it indicates that the socket was + * open before, and the method will return true. If the socket was opened successfully, the method returns false and + * immediately closes the socket. + * + * @param port the local port + * @return true if the socket was already open + * @throws IOException if the socket could not be opened due to some other reason than a bind exception + */ + public static boolean isServerSocketOpen(int port) throws IOException { + try (ServerSocket socket = new ServerSocket(port)) { + return false; + } catch (BindException e) { + return true; + } + } + + +} diff --git a/src/test/java/dslab/StreamListener.java b/src/test/java/dslab/StreamListener.java new file mode 100644 index 0000000..4468903 --- /dev/null +++ b/src/test/java/dslab/StreamListener.java @@ -0,0 +1,80 @@ +package dslab; + +import java.io.BufferedReader; +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.UncheckedIOException; +import java.net.SocketException; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +/** + * Runnable that reads from a Socket's InputStream line-by-line and writes it to a queue, until the socket is closed. + */ +public class StreamListener implements Runnable, Closeable { + + public static final String NULL_CHAR = new String(new byte[]{0x0}); + + private InputStream in; + private LinkedBlockingQueue queue; + + public StreamListener(InputStream in) { + this.in = in; + this.queue = new LinkedBlockingQueue<>(); + } + + private static String removeNullBytes(String str) { + return str.replace(NULL_CHAR, ""); + } + + public String poll(long timeout, TimeUnit timeUnit) { + try { + return queue.poll(timeout, timeUnit); + } catch (InterruptedException e) { + return null; + } + } + + public String listen(long timeout, TimeUnit timeUnit) { + StringBuilder str = new StringBuilder(128); + + String line; + while ((line = poll(timeout, timeUnit)) != null) { + str.append(removeNullBytes(line)).append("\n"); + } + + if (str.length() > 0) { + // remove trailing whitespace + int i = str.length() - 1; + if ('\n' == str.charAt(i)) { + str.deleteCharAt(i); + } + } + + return str.toString(); + } + + @Override + public void run() { + BufferedReader reader = new BufferedReader(new InputStreamReader(in)); + + String line; + try { + while ((line = reader.readLine()) != null) { + queue.offer(line); + } + } catch (SocketException e) { + // socket closed + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + @Override + public void close() throws IOException { + in.close(); + } + +} diff --git a/src/test/java/dslab/StringMatches.java b/src/test/java/dslab/StringMatches.java new file mode 100644 index 0000000..cc88958 --- /dev/null +++ b/src/test/java/dslab/StringMatches.java @@ -0,0 +1,30 @@ +package dslab; + +import org.hamcrest.Factory; +import org.hamcrest.Matcher; +import org.hamcrest.core.SubstringMatcher; + +/** + * String matcher that checks for regex matching. + */ +public class StringMatches extends SubstringMatcher { + + public StringMatches(String substring) { + super(substring); + } + + @Factory + public static Matcher matchesPattern(String pattern) { + return new StringMatches(pattern); + } + + @Override + protected boolean evalSubstringOf(String string) { + return string.matches(substring); + } + + @Override + protected String relationship() { + return "matching"; + } +} diff --git a/src/test/java/dslab/TestBase.java b/src/test/java/dslab/TestBase.java new file mode 100644 index 0000000..62be93b --- /dev/null +++ b/src/test/java/dslab/TestBase.java @@ -0,0 +1,30 @@ +package dslab; + +import java.util.concurrent.TimeUnit; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.rules.ErrorCollector; +import org.junit.rules.Timeout; + +/** + * Contains a generic setup for a unit test. + */ +public class TestBase { + + @Rule + public ErrorCollector err = new ErrorCollector(); + + @Rule + public Timeout timeout = new Timeout(30, TimeUnit.SECONDS); // fail tests that do not terminate after 30 seconds + + protected TestInputStream in; + protected TestOutputStream out; + + @Before + public void setUpBase() throws Exception { + in = new TestInputStream(); + out = new TestOutputStream(); + } + +} diff --git a/src/test/java/dslab/TestInputStream.java b/src/test/java/dslab/TestInputStream.java new file mode 100644 index 0000000..7d07cb9 --- /dev/null +++ b/src/test/java/dslab/TestInputStream.java @@ -0,0 +1,99 @@ +package dslab; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +/** + * Simulates reading lines from an {@link InputStream}. + * + * Internally, the lines read from the underlying {@link InputStream} are buffered and can be retrieved on demand for + * verification purposes. + */ +public class TestInputStream extends InputStream { + private volatile BlockingQueue lines = new LinkedBlockingQueue<>(); + private InputStream in; + + @Override + public synchronized int read() throws IOException { + if (in == null) { + if ((in = nextLine()) == null) { + return -1; + } + } else if (in.available() <= 0) { + try { + return -1; + } finally { + in = null; + } + } + return in != null ? in.read() : -1; + } + + /** + * Returns a copy of the lines available for reading. + * + * @return the available lines + * @throws IOException if the stream is closed + */ + public List getLines() throws IOException { + List copy = new ArrayList<>(); + do { + try { + copy.add(lines.take()); + } catch (InterruptedException e) { + throw new IOException(e); + } + } while (lines.size() > 0); + return copy; + } + + /** + * Adds the given line to the input queue. + * + * @param line the line to add + */ + public void addLine(String line) { + if (lines != null) { + lines.add(line); + } + } + + /** + * Prepares the next line available for reading from it. + *

+ * This method blocks until a line is available or the stream becomes closed. + * + * @return the {@link InputStream} holding the line + * @throws IOException if the stream is closed + */ + private InputStream nextLine() throws IOException { + try { + String line = null; + while (lines != null && line == null) { + line = lines.poll(500L, TimeUnit.MILLISECONDS); + } + if (line != null) { + return new ByteArrayInputStream((line.endsWith("\n") ? line : line + '\n').getBytes()); + } else { + return new ByteArrayInputStream("".getBytes()); + } + } catch (InterruptedException e) { + throw new IOException(e); + } + } + + @Override + public void close() throws IOException { + if (in != System.in) { + super.close(); + } + lines = null; + Thread.currentThread().interrupt(); + } +} diff --git a/src/test/java/dslab/TestOutputStream.java b/src/test/java/dslab/TestOutputStream.java new file mode 100644 index 0000000..d01b9be --- /dev/null +++ b/src/test/java/dslab/TestOutputStream.java @@ -0,0 +1,150 @@ +package dslab; + +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +/** + * Simulates writing lines to an {@link PrintStream}. + *

+ * Internally, the lines written to the underlying {@link PrintStream} are buffered and can be retrieved on demand for + * verification purposes. + */ +public class TestOutputStream extends PrintStream { + private final LinkedBlockingQueue lines = new LinkedBlockingQueue<>(); + private volatile StringBuilder line = new StringBuilder(); + private PrintStream delegate; + + /** + * Creates a new {@code TestOutputStream} instance writing to an {@link NullOutputStream}. + */ + public TestOutputStream() { + this(new PrintStream(NullOutputStream.getInstance())); + } + + /** + * Creates a new {@code TestOutputStream} instance writing to the provided {@link PrintStream}. + * + * @param delegate the stream to write to + */ + public TestOutputStream(PrintStream delegate) { + super(delegate); + this.delegate = delegate; + } + + @Override + public void close() { + if (delegate != System.out) { + super.close(); + } + } + + @Override + public void write(int b) { + delegate.write(b); + if (b == '\r') { + // Do nothing + } else if (b == '\n') { + addLine(); + } else { + line.append((char) b); + } + } + + public void write(byte b[], int off, int len) { + if (b == null) { + throw new NullPointerException(); + } else if ((off < 0) || (off > b.length) || (len < 0) || ((off + len) > b.length) || ((off + len) < 0)) { + throw new IndexOutOfBoundsException(); + } else if (len == 0) { + return; + } + for (int i = 0; i < len; i++) { + write(b[off + i]); + } + } + + /** + * Returns a copy of the lines written to the {@link PrintStream} so far. + * + * @return the written lines + */ + public List getLines() { + synchronized (lines) { + if (line.length() > 0) { + addLine(); + } + return new ArrayList<>(lines); + } + } + + /** + * Listens for stream output until no output has been received for one second. + * + * @return the aggregated output (joined by a newline) + * @throws InterruptedException if the polling was interrupted + */ + public String listen() throws InterruptedException { + return listen(1, TimeUnit.SECONDS); + } + + public String listen(long timeout, TimeUnit timeUnit) throws InterruptedException { + StringBuilder str = new StringBuilder(128); + + String line; + while ((line = poll(timeout, timeUnit)) != null) { + str.append(line).append("\n"); + } + + if (str.length() > 0) { + // remove trailing whitespace + int i = str.length() - 1; + if ('\n' == str.charAt(i)) { + str.deleteCharAt(i); + } + } + + return str.toString(); + } + + public String poll(long time, TimeUnit timeUnit) throws InterruptedException { + return lines.poll(time, timeUnit); + } + + /** + * Returns a copy of the lines written to the {@link PrintStream} so far and clears the buffer. + * + * @return the written lines + * @see #getLines() + * @see #clear() + */ + public List reset() { + synchronized (lines) { + List lines = getLines(); + clear(); + return lines; + } + } + + /** + * Clears the buffer holding the lines written to the {@link PrintStream} so far. + */ + private void clear() { + synchronized (lines) { + lines.clear(); + line = new StringBuilder(); + } + } + + /** + * Appends the current line to the buffer. + */ + private void addLine() { + synchronized (lines) { + lines.add(line.toString()); + line = new StringBuilder(); + } + } +} diff --git a/src/test/java/dslab/mailbox/MailboxServerProtocolTest.java b/src/test/java/dslab/mailbox/MailboxServerProtocolTest.java new file mode 100644 index 0000000..012ba78 --- /dev/null +++ b/src/test/java/dslab/mailbox/MailboxServerProtocolTest.java @@ -0,0 +1,108 @@ +package dslab.mailbox; + +import static org.hamcrest.CoreMatchers.containsString; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import dslab.ComponentFactory; +import dslab.Constants; +import dslab.JunitSocketClient; +import dslab.Sockets; +import dslab.TestBase; +import dslab.util.Config; + +public class MailboxServerProtocolTest extends TestBase { + + private static final Log LOG = LogFactory.getLog(MailboxServerProtocolTest.class); + + private String componentId = "mailbox-earth-planet"; + + private IMailboxServer component; + private int dmapServerPort; + private int dmtpServerPort; + + @Before + public void setUp() throws Exception { + component = ComponentFactory.createMailboxServer(componentId, in, out); + dmapServerPort = new Config(componentId).getInt("dmap.tcp.port"); + dmtpServerPort = new Config(componentId).getInt("dmtp.tcp.port"); + + new Thread(component).start(); + + LOG.info("Waiting for server sockets to appear"); + Sockets.waitForSocket("localhost", dmapServerPort, Constants.COMPONENT_STARTUP_WAIT); + Sockets.waitForSocket("localhost", dmtpServerPort, Constants.COMPONENT_STARTUP_WAIT); + } + + @After + public void tearDown() throws Exception { + in.addLine("shutdown"); // send "shutdown" command to command line + Thread.sleep(Constants.COMPONENT_TEARDOWN_WAIT); + } + + @Test(timeout = 15000) + public void loginAndLogout_withValidLogin() throws Exception { + try (JunitSocketClient client = new JunitSocketClient(dmapServerPort, err)) { + client.verify("ok DMAP"); + client.sendAndVerify("login trillian 12345", "ok"); + client.sendAndVerify("logout", "ok"); + client.sendAndVerify("quit", "ok bye"); + } + } + + @Test(timeout = 15000) + public void login_withInvalidLogin_returnsError() throws Exception { + try (JunitSocketClient client = new JunitSocketClient(dmapServerPort, err)) { + client.verify("ok DMAP"); + client.sendAndVerify("login trillian WRONGPW", "error"); + client.sendAndVerify("quit", "ok bye"); + } + } + + @Test(timeout = 15000) + public void acceptDmtpMessage_listDmapMessage() throws Exception { + + // accept a message via DMTP (to trillian) + try (JunitSocketClient client = new JunitSocketClient(dmtpServerPort, err)) { + client.verify("ok DMTP"); + client.sendAndVerify("begin", "ok"); + client.sendAndVerify("from arthur@earth.planet", "ok"); + client.sendAndVerify("to trillian@earth.planet", "ok 1"); + client.sendAndVerify("subject hello", "ok"); + client.sendAndVerify("data hello from junit", "ok"); + client.sendAndVerify("send", "ok"); + client.sendAndVerify("quit", "ok bye"); + } + + // list the message via DMAP list + try (JunitSocketClient client = new JunitSocketClient(dmapServerPort, err)) { + client.verify("ok DMAP"); + client.sendAndVerify("login trillian 12345", "ok"); + + client.send("list"); + String listResult = client.listen(); + err.checkThat(listResult, containsString("arthur@earth.planet hello")); + + client.sendAndVerify("logout", "ok"); + client.sendAndVerify("quit", "ok bye"); + } + } + + @Test(timeout = 15000) + public void dmtpMessage_withUnknownRecipient_returnsError() throws Exception { + + // accept a message via DMTP (to trillian) + try (JunitSocketClient client = new JunitSocketClient(dmtpServerPort, err)) { + client.verify("ok DMTP"); + client.sendAndVerify("begin", "ok"); + client.sendAndVerify("from arthur@earth.planet", "ok"); + client.sendAndVerify("to unknown@earth.planet", "error unknown"); + client.sendAndVerify("quit", "ok bye"); + } + } + +} diff --git a/src/test/java/dslab/mailbox/MailboxServerTest.java b/src/test/java/dslab/mailbox/MailboxServerTest.java new file mode 100644 index 0000000..a89700f --- /dev/null +++ b/src/test/java/dslab/mailbox/MailboxServerTest.java @@ -0,0 +1,75 @@ +package dslab.mailbox; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.junit.Assert.assertThat; + +import java.net.SocketTimeoutException; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.junit.Test; + +import dslab.ComponentFactory; +import dslab.Constants; +import dslab.Sockets; +import dslab.TestBase; +import dslab.monitoring.MonitoringServerTest; +import dslab.util.Config; + +/** + * MailboxServerTest. + */ +public class MailboxServerTest extends TestBase { + + private static final Log LOG = LogFactory.getLog(MonitoringServerTest.class); + + @Test + public void runAndShutdownTransferServer_createsAndStopsTcpSocketCorrectly() throws Exception { + String componentId = "mailbox-earth-planet"; + Config config = new Config(componentId); + + IMailboxServer component = ComponentFactory.createMailboxServer(componentId, in, out); + int dmtpPort = config.getInt("dmtp.tcp.port"); + int dmapPort = config.getInt("dmap.tcp.port"); + + assertThat(component, is(notNullValue())); + + Thread componentThread = new Thread(component); + LOG.info("Starting thread with component " + component); + componentThread.start(); + + try { + LOG.info("Waiting for DMTP socket to open on port " + dmtpPort); + Sockets.waitForSocket("localhost", dmtpPort, Constants.COMPONENT_STARTUP_WAIT); + } catch (SocketTimeoutException e) { + err.addError(new AssertionError("Expected a TCP server socket on port " + dmtpPort, e)); + } + + try { + LOG.info("Waiting for DMAP socket to open on port " + dmapPort); + Sockets.waitForSocket("localhost", dmapPort, Constants.COMPONENT_STARTUP_WAIT); + } catch (SocketTimeoutException e) { + err.addError(new AssertionError("Expected a TCP server socket on port " + dmapPort, e)); + } + + LOG.info("Shutting down component " + component); + in.addLine("shutdown"); // send "shutdown" command to command line + Thread.sleep(Constants.COMPONENT_TEARDOWN_WAIT); + + try { + LOG.info("Waiting for thread to stop for component " + component); + componentThread.join(); + } catch (InterruptedException e) { + err.addError(new AssertionError("Monitoring server was not terminated correctly")); + } + + + err.checkThat("Expected tcp socket on port " + dmtpPort + " to be closed after shutdown", + Sockets.isServerSocketOpen(dmtpPort), is(false)); + + err.checkThat("Expected tcp socket on port " + dmapPort + " to be closed after shutdown", + Sockets.isServerSocketOpen(dmapPort), is(false)); + } + +} diff --git a/src/test/java/dslab/monitoring/MonitoringServerProtocolTest.java b/src/test/java/dslab/monitoring/MonitoringServerProtocolTest.java new file mode 100644 index 0000000..a4ef39c --- /dev/null +++ b/src/test/java/dslab/monitoring/MonitoringServerProtocolTest.java @@ -0,0 +1,92 @@ +package dslab.monitoring; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.junit.Assert.assertThat; + +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetSocketAddress; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import dslab.ComponentFactory; +import dslab.Constants; +import dslab.TestBase; +import dslab.util.Config; + +/** + * Tests whether the UDP-based monitoring protocol is implemented correctly on the server side. + */ +public class MonitoringServerProtocolTest extends TestBase { + + private static final Log LOG = LogFactory.getLog(MonitoringServerProtocolTest.class); + + private String componentId = "monitoring"; + + private IMonitoringServer component; + private InetSocketAddress addr; + + @Before + public void setUp() throws Exception { + component = ComponentFactory.createMonitoringServer(componentId, in, out); + addr = new InetSocketAddress("127.0.0.1", new Config(componentId).getInt("udp.port")); + + new Thread(component).start(); + Thread.sleep(Constants.COMPONENT_STARTUP_WAIT); + } + + @After + public void tearDown() throws Exception { + in.addLine("shutdown"); + Thread.sleep(Constants.COMPONENT_TEARDOWN_WAIT); + } + + @Test(timeout = 15000) + public void addresses_returnsCorrectStatistics() throws Exception { + LOG.info("Sending three monitoring packets to monitoring socket"); + try (DatagramSocket socket = new DatagramSocket()) { + String str1 = "127.0.0.1:42 foo@example.com"; + String str2 = "127.0.0.1:43 foo@example.com"; + String str3 = "127.0.0.1:42 bar@example.com"; + + socket.send(new DatagramPacket(str1.getBytes(), str1.length(), addr)); + socket.send(new DatagramPacket(str2.getBytes(), str2.length(), addr)); + socket.send(new DatagramPacket(str3.getBytes(), str3.length(), addr)); + } + + Thread.sleep(2500); + in.addLine("addresses"); // send "addresses" command to command line + Thread.sleep(2500); + String output = String.join(",", out.getLines()); + assertThat(output, containsString("foo@example.com 2")); + assertThat(output, containsString("bar@example.com 1")); + } + + /* + * Assumes that run and shutdown works correctly. + */ + @Test(timeout = 15000) + public void servers_returnsCorrectStatistics() throws Exception { + LOG.info("Sending three monitoring packets to monitoring socket"); + try (DatagramSocket socket = new DatagramSocket()) { + String str1 = "127.0.0.1:42 foo@example.com"; + String str2 = "127.0.0.1:43 foo@example.com"; + String str3 = "127.0.0.1:42 bar@example.com"; + + socket.send(new DatagramPacket(str1.getBytes(), str1.length(), addr)); + socket.send(new DatagramPacket(str2.getBytes(), str2.length(), addr)); + socket.send(new DatagramPacket(str3.getBytes(), str3.length(), addr)); + } + + Thread.sleep(2500); + in.addLine("servers"); // send "addresses" command to command line + Thread.sleep(2500); + String output = String.join(",", out.getLines()); + assertThat(output, containsString("127.0.0.1:42 2")); + assertThat(output, containsString("127.0.0.1:43 1")); + } +} diff --git a/src/test/java/dslab/monitoring/MonitoringServerTest.java b/src/test/java/dslab/monitoring/MonitoringServerTest.java new file mode 100644 index 0000000..220511d --- /dev/null +++ b/src/test/java/dslab/monitoring/MonitoringServerTest.java @@ -0,0 +1,60 @@ +package dslab.monitoring; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.junit.Assert.assertThat; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.junit.Test; + +import dslab.ComponentFactory; +import dslab.Constants; +import dslab.Sockets; +import dslab.TestBase; +import dslab.util.Config; + +/** + * Tests the creation, running, and shutting down of the monitoring server. + */ +public class MonitoringServerTest extends TestBase { + + private static final Log LOG = LogFactory.getLog(MonitoringServerTest.class); + + @Test + public void runAndShutdownMonitoringServer_createsAndStopsUdpSocketCorrectly() throws Exception { + IMonitoringServer component = ComponentFactory.createMonitoringServer("monitoring", in, out); + int port = new Config("monitoring").getInt("udp.port"); + + assertThat(component, is(notNullValue())); + + Thread componentThread = new Thread(component); + LOG.info("Starting thread with component " + component); + componentThread.start(); + + Thread.sleep(Constants.COMPONENT_STARTUP_WAIT); // wait a bit for resources to be initialized + + try { + LOG.info("Trying to create socket on port " + port); + err.checkThat("Expected an open UDP socket on port " + port, Sockets.isDatagramSocketOpen(port), is(true)); + } catch (Exception e) { + // a different unexpected error occurred (unlikely) + err.addError(e); + } + + LOG.info("Shutting down component " + component); + in.addLine("shutdown"); // send "shutdown" command to command line + Thread.sleep(Constants.COMPONENT_TEARDOWN_WAIT); + + try { + LOG.info("Waiting for thread to stop for component " + component); + componentThread.join(); + } catch (InterruptedException e) { + err.addError(new AssertionError("Monitoring server was not terminated correctly")); + } + + err.checkThat("Expected datagram socket on port " + port + " to be closed after shutdown", + Sockets.isDatagramSocketOpen(port), is(false)); + } + +} diff --git a/src/test/java/dslab/transfer/TransferServerProtocolTest.java b/src/test/java/dslab/transfer/TransferServerProtocolTest.java new file mode 100644 index 0000000..ef2c139 --- /dev/null +++ b/src/test/java/dslab/transfer/TransferServerProtocolTest.java @@ -0,0 +1,71 @@ +package dslab.transfer; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import dslab.ComponentFactory; +import dslab.Constants; +import dslab.JunitSocketClient; +import dslab.Sockets; +import dslab.TestBase; +import dslab.util.Config; + +/** + * TransferServerProtocolTest. + */ +public class TransferServerProtocolTest extends TestBase { + + private static final Log LOG = LogFactory.getLog(TransferServerProtocolTest.class); + + private String componentId = "transfer-1"; + + private ITransferServer component; + private int serverPort; + + @Before + public void setUp() throws Exception { + component = ComponentFactory.createTransferServer(componentId, in, out); + serverPort = new Config(componentId).getInt("tcp.port"); + new Thread(component).start(); + + LOG.info("Waiting for server socket to appear"); + Sockets.waitForSocket("localhost", serverPort, Constants.COMPONENT_STARTUP_WAIT); + } + + @After + public void tearDown() throws Exception { + in.addLine("shutdown"); // send "shutdown" command to command line + Thread.sleep(Constants.COMPONENT_TEARDOWN_WAIT); + } + + @Test(timeout = 15000) + public void defaultDmtpInteractionTest() throws Exception { + try (JunitSocketClient client = new JunitSocketClient(serverPort, err)) { + client.verify("ok DMTP"); + client.sendAndVerify("begin", "ok"); + client.sendAndVerify("from trillian@earth.planet", "ok"); + client.sendAndVerify("to arthur@earth.planet", "ok 1"); + client.sendAndVerify("subject hello", "ok"); + client.sendAndVerify("data hello from junit", "ok"); + client.sendAndVerify("send", "ok"); + client.sendAndVerify("quit", "ok bye"); + } + } + + @Test(timeout = 15000) + public void sendWithoutRecipient_returnsErrorOnSend() throws Exception { + try (JunitSocketClient client = new JunitSocketClient(serverPort, err)) { + client.verify("ok DMTP"); + client.sendAndVerify("begin", "ok"); + client.sendAndVerify("from trillian@earth.planet", "ok"); + client.sendAndVerify("subject hello", "ok"); + client.sendAndVerify("data hello from junit", "ok"); + client.sendAndVerify("send", "error"); + client.sendAndVerify("quit", "ok bye"); + } + } + +} diff --git a/src/test/java/dslab/transfer/TransferServerTest.java b/src/test/java/dslab/transfer/TransferServerTest.java new file mode 100644 index 0000000..30d7be4 --- /dev/null +++ b/src/test/java/dslab/transfer/TransferServerTest.java @@ -0,0 +1,60 @@ +package dslab.transfer; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.junit.Assert.assertThat; + +import java.net.SocketTimeoutException; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.junit.Test; + +import dslab.ComponentFactory; +import dslab.Constants; +import dslab.Sockets; +import dslab.TestBase; +import dslab.monitoring.MonitoringServerTest; +import dslab.util.Config; + +/** + * TransferServerTest. + */ +public class TransferServerTest extends TestBase { + + private static final Log LOG = LogFactory.getLog(MonitoringServerTest.class); + + @Test + public void runAndShutdownTransferServer_createsAndStopsTcpSocketCorrectly() throws Exception { + ITransferServer component = ComponentFactory.createTransferServer("transfer-1", in, out); + int port = new Config("transfer-1").getInt("tcp.port"); + + assertThat(component, is(notNullValue())); + + Thread componentThread = new Thread(component); + LOG.info("Starting thread with component " + component); + componentThread.start(); + + try { + LOG.info("Waiting for socket to open on port " + port); + Sockets.waitForSocket("localhost", port, Constants.COMPONENT_STARTUP_WAIT); + } catch (SocketTimeoutException e) { + err.addError(new AssertionError("Expected a TCP server socket on port " + port, e)); + } + + LOG.info("Shutting down component " + component); + in.addLine("shutdown"); // send "shutdown" command to command line + Thread.sleep(Constants.COMPONENT_TEARDOWN_WAIT); + + try { + LOG.info("Waiting for thread to stop for component " + component); + componentThread.join(); + } catch (InterruptedException e) { + err.addError(new AssertionError("Monitoring server was not terminated correctly")); + } + + err.checkThat("Expected tcp socket on port " + port + " to be closed after shutdown", + Sockets.isServerSocketOpen(port), is(false)); + } + +} diff --git a/src/test/resources/.gitkeep b/src/test/resources/.gitkeep new file mode 100644 index 0000000..e69de29