diff --git a/.gitignore b/.gitignore index 398fec5..ac231d3 100644 --- a/.gitignore +++ b/.gitignore @@ -40,5 +40,6 @@ gradle-app.setting ### JetBrains template .idea/ +out/ diff --git a/build.gradle b/build.gradle index c5166de..670aad2 100644 --- a/build.gradle +++ b/build.gradle @@ -1,30 +1,88 @@ group 'ssbgp' -version '1.0-SNAPSHOT' +version '1.0' buildscript { - ext.kotlin_version = '1.1.3-2' + ext.kotlin_version = '1.1.4-2' + ext.dokka_version = '0.9.15' + ext.junit_version = '1.0.0-M4' + ext.junit5_version = '5.0.0-M4' + ext.spek_version = '1.1.2' repositories { mavenCentral() + jcenter() } dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath "org.jetbrains.dokka:dokka-gradle-plugin:$dokka_version" + classpath "org.junit.platform:junit-platform-gradle-plugin:$junit_version" } } apply plugin: 'kotlin' +apply plugin: 'org.jetbrains.dokka' +apply plugin: 'org.junit.platform.gradle.plugin' + +junitPlatform { + filters { + engines { + include 'spek' + } + } +} repositories { mavenCentral() + maven { + url "https://oss.sonatype.org/content/repositories/snapshots" + } } dependencies { + testCompile "org.junit.jupiter:junit-jupiter-api:$junit5_version" + testCompile "org.jetbrains.spek:spek-api:$spek_version" + testCompile "org.hamcrest:hamcrest-all:1.3" + testCompile "org.mockito:mockito-core:2.8.9" + testCompile "com.nhaarman:mockito-kotlin-kt1.1:1.5.0" + + testRuntime "org.junit.jupiter:junit-jupiter-engine:$junit5_version" + testRuntime "org.jetbrains.spek:spek-junit-platform-engine:$spek_version" + testRuntime "org.junit.platform:junit-platform-launcher:$junit_version" + compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" + compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" + compile "com.github.inamik.text.tables:inamik-text-tables:1.0-SNAPSHOT" + compile 'org.apache.commons:commons-csv:1.4' + compile group: 'commons-cli', name: 'commons-cli', version: '1.4' } compileKotlin { kotlinOptions.jvmTarget = "1.8" } + compileTestKotlin { kotlinOptions.jvmTarget = "1.8" +} + +dokka { + outputFormat = 'html' + outputDirectory = "$buildDir/javadoc" +} + +task enabledMockingFinalClasses << { + def mockMakerFile = new File("$projectDir/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker") + mockMakerFile.parentFile.mkdirs() + mockMakerFile.createNewFile() + mockMakerFile.write("mock-maker-inline") +} + +jar { + manifest { + attributes 'Main-Class': 'main.MainKt' + } + + // This line of code recursively collects and copies all of a project's files + // and adds them to the JAR itself. One can extend this task, to skip certain + // files or particular types at will + from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } } } \ No newline at end of file diff --git a/src/main/kotlin/bgp/BGP.kt b/src/main/kotlin/bgp/BGP.kt new file mode 100644 index 0000000..55f4746 --- /dev/null +++ b/src/main/kotlin/bgp/BGP.kt @@ -0,0 +1,194 @@ +package bgp + +import bgp.notifications.* +import core.routing.* +import core.simulator.Time +import core.simulator.Timer + +/** + * Created on 21-07-2017 + * + * @author David Fialho + */ +abstract class BaseBGP(val mrai: Time, routingTable: RoutingTable): Protocol { + + /** + * This data structure contains all neighbors that the protocol needs to send routes to when a new + * route is selected. + */ + protected val neighbors = ArrayList>() + + /** + * Collection of all the in-neighbors added to the protocol. + */ + override val inNeighbors: Collection> + get() = neighbors + + /** + * Routing table containing the candidate routes. + * Uses a route selector to perform the route selection. + * The route selector wraps the provided routing table if one is provided. Otherwise, it wraps a new routing table. + */ + val routingTable = RouteSelector.wrap(routingTable, ::bgpRouteCompare) + + var mraiTimer = Timer.disabled() + protected set + + /** + * Flag that indicates if a new route was selected as a result of processing a new incoming message. This flag is + * always set to false when a new message arrives and should only be set to true if a new route is selected when + * the message is being processed. + */ + protected var wasSelectedRouteUpdated: Boolean = false + + /** + * Adds a new in-neighbor for the protocol to export selected routes to. + * + * It does not check if the neighbor was already added to the protocol. Thus, the same neighbor can be added + * twice, which means that it will be notified twice every time a new route is selected. + */ + override fun addInNeighbor(neighbor: Neighbor) { + neighbors.add(neighbor) + } + + /** + * Announces [node] as the destination. + */ + override fun start(node: Node) { + val selfRoute = BGPRoute.self() + routingTable.update(node, selfRoute) + export(node) + } + + /** + * Processes a BGP message received by a node. + * May update the routing table and the selected route/neighbor. + * + * @param message the message to be processed + */ + override fun process(message: Message) { + + val importedRoute = import(message.sender, message.route, message.extender) + BGPNotifier.notifyImport(ImportNotification(message.receiver, importedRoute, message.sender)) + + process(message.receiver, message.sender, importedRoute) + } + + /** + * Processes a BGP route imported by a node. + * May update the routing table and the selected route/neighbor. + * + * @param node the node that imported the route + * @param neighbor the neighbor that exported the route + * @param importedRoute the route imported by [node] + */ + fun process(node: Node, neighbor: Node, importedRoute: BGPRoute) { + + // Store the route the node was selecting before processing this message + val previousSelectedRoute = routingTable.getSelectedRoute() + + val learnedRoute = learn(node, neighbor, importedRoute) + BGPNotifier.notifyLearn(LearnNotification(node, learnedRoute, neighbor)) + + val updated = routingTable.update(neighbor, learnedRoute) + + // Set updated flag to true if 'updated' is true or keep its current state + wasSelectedRouteUpdated = wasSelectedRouteUpdated || updated + + if (wasSelectedRouteUpdated) { + + val selectedRoute = routingTable.getSelectedRoute() + BGPNotifier.notifySelect( + SelectNotification(node, selectedRoute, previousSelectedRoute)) + + export(node) + wasSelectedRouteUpdated = false + } + } + + /** + * Implements the process of importing a route. + * Returns the result of extending the given route with the given extender. + * + * @param sender the node the sent the route + * @param route the route received by the node (route obtained directly from the message) + * @param extender the extender used to import the route (extender included in the message) + */ + protected fun import(sender: Node, route: BGPRoute, extender: Extender): BGPRoute { + return extender.extend(route, sender) + } + + /** + * Implements the process of learning a route. + * + * @param node the node processing the route + * @param sender the out-neighbor that sent the route + * @param route the route imported by the node (route obtained after applying the extender) + * @return the imported route if the route's AS-PATH does not include the node learning the route or it returns + * an invalid if the route's AS-PATH includes the learning node. Note that it may also return an invalid route if + * the imported route is invalid. + */ + protected fun learn(node: Node, sender: Node, route: BGPRoute): BGPRoute { + + if (node in route.asPath) { + // Notify the implementations that a loop was detected + onLoopDetected(node, sender, route) + + return BGPRoute.invalid() + } else { + return route + } + } + + /** + * Implements the process of exporting a route. It exports the currently selected by the node. + * + * @param node the node exporting the node + */ + protected fun export(node: Node, restartMRAITimer: Boolean = true) { + + if (!mraiTimer.expired) { + // The MRAI timer is still running: no route is exported while the MRAI timer is running + return + } + + val selectedRoute = routingTable.getSelectedRoute() + + // Export the route currently selected + node.send(selectedRoute) + BGPNotifier.notifyExport(ExportNotification(node, selectedRoute)) + + if (restartMRAITimer && mrai > 0) { + // Restart the MRAI timer + mraiTimer = Timer.enabled(mrai) { + export(node, restartMRAITimer = false) + } // when the timer expires + mraiTimer.start() + } + } + + /** + * Resets the state of the protocol as if it was just created. + */ + override fun reset() { + routingTable.clear() + wasSelectedRouteUpdated = false + mraiTimer.cancel() + mraiTimer = Timer.disabled() + } + + /** + * Called by the protocol when it detects a routing loop. + */ + abstract protected fun onLoopDetected(node: Node, sender: Node, route: BGPRoute) + +} + +/** + * BGP: when a loop is detected it does nothing extra. + */ +class BGP(mrai: Time = 0, routingTable: RoutingTable = RoutingTable.empty(BGPRoute.invalid())) + : BaseBGP(mrai, routingTable) { + + override fun onLoopDetected(node: Node, sender: Node, route: BGPRoute) = Unit +} diff --git a/src/main/kotlin/bgp/BGPRoute.kt b/src/main/kotlin/bgp/BGPRoute.kt new file mode 100644 index 0000000..9ff7455 --- /dev/null +++ b/src/main/kotlin/bgp/BGPRoute.kt @@ -0,0 +1,101 @@ +package bgp + +import core.routing.Path +import core.routing.Route +import core.routing.emptyPath + +/** + * Created on 20-07-2017 + * + * @author David Fialho + * + * A BGP route is composed of two attributes: the LOCAL-PREF and the AS-PATH. The LOCAL-PREF is assigned locally by + * each node and indicates the degree of preference that node assigns to each route. The AS-PATH contains the + * sequence of nodes traversed by the route from the original advertiser to the current node holding the route. + * + * BGP routes are always immutable instances! + */ +sealed class BGPRoute : Route { + + abstract val localPref: Int + abstract val asPath: Path + + companion object Factory { + + /** + * Returns a BGP route with the specified LOCAL-PREF and AS-PATH. + */ + fun with(localPref: Int, asPath: Path): BGPRoute = ValidBGPRoute(localPref, asPath) + + /** + * Returns an invalid BGP route. + */ + fun invalid(): BGPRoute = InvalidBGPRoute + + /** + * Returns a self BGP route. A self BGP route is the BGP route with the highest preference possible. + */ + fun self(): BGPRoute = SelfBGPRoute + + } + + /** + * An implementation for a valid BGP route. + */ + private data class ValidBGPRoute(override val localPref: Int, override val asPath: Path) : BGPRoute() { + override fun isValid(): Boolean = true + } + + /** + * An implementation for a invalid BGP route. + */ + private object InvalidBGPRoute : BGPRoute() { + override val localPref: Int = Int.MIN_VALUE + override val asPath: Path = emptyPath() + override fun isValid(): Boolean = false + override fun toString(): String = "•" + } + + /** + * An implementation for a self BGP route. A self BGP route is the BGP route with the highest preference possible. + */ + private object SelfBGPRoute : BGPRoute() { + override val localPref: Int = Int.MAX_VALUE + override val asPath: Path = emptyPath() + override fun isValid(): Boolean = true + override fun toString(): String = "◦" + } + + override fun toString(): String { + return "BGPRoute(localPref=$localPref, asPath=$asPath)" + } +} + +/** + * Compare function for BGP routes. It compares the preference of two BGP routes. + * + * The preference of a BGP route is determined based on the following attributes: + * + * 1. the LOCAL-PREF + * 2. the length of the AS-PATH + * 3. the ID of the next-hop node + * + * @return positive value if route1 is preferred to route 2; zero if they have the same preference; and negative + * value if route2 is preferred to route1 + */ +fun bgpRouteCompare(route1: BGPRoute, route2: BGPRoute): Int { + + var difference = route1.localPref.compareTo(route2.localPref) + if (difference == 0) { + difference = route2.asPath.size.compareTo(route1.asPath.size) + if (difference == 0) { + + val nextHop1 = route1.asPath.nextHop() ?: return 0 + val nextHop2 = route2.asPath.nextHop() ?: return 0 + + difference = nextHop2.id.compareTo(nextHop1.id) + } + } + + return difference +} diff --git a/src/main/kotlin/bgp/SSBGP.kt b/src/main/kotlin/bgp/SSBGP.kt new file mode 100644 index 0000000..69ed21a --- /dev/null +++ b/src/main/kotlin/bgp/SSBGP.kt @@ -0,0 +1,100 @@ +package bgp + +import bgp.notifications.BGPNotifier +import bgp.notifications.DetectNotification +import core.routing.Node +import core.routing.RoutingTable +import core.simulator.Time + +/** + * Base class for the SS-BGP like protocols. Implements the deactivation of neighbors and leaves the detection + * condition to the subclasses. + */ +abstract class BaseSSBGP(mrai: Time = 0, routingTable: RoutingTable): BaseBGP(mrai, routingTable) { + + /** + * Invoked the the BaseBGP right after a routing loop is detected. + * + * An SS-BGP like protocol checks if the routing loop is recurrent and if so it deactivates the neighbor that + * sent the route. + */ + final override fun onLoopDetected(node: Node, sender: Node, route: BGPRoute) { + + // Ignore route learned from a disabled neighbor + if (!routingTable.table.isEnabled(sender)) { + return + } + + // Since a loop routing was detected, the new route via the sender node is surely invalid + + // Set the route via the sender as invalid + // This will force the selector to select the alternative route + val updated = routingTable.update(sender, BGPRoute.invalid()) + wasSelectedRouteUpdated = wasSelectedRouteUpdated || updated + + val alternativeRoute = routingTable.getSelectedRoute() + if (isRecurrent(node, route, alternativeRoute)) { + disableNeighbor(sender) + BGPNotifier.notifyDetect(DetectNotification(node, route, alternativeRoute, sender)) + } + } + + /** + * Checks if the routing loop detected is recurrent. + * Subclasses must implement this method to define the detection condition. + */ + abstract fun isRecurrent(node: Node, learnedRoute: BGPRoute, alternativeRoute: BGPRoute): Boolean + + /** + * Enables the specified neighbor. + * + * May update the `wasSelectedRouteUpdated` property. + * + * @param neighbor the neighbor to enable + */ + fun enableNeighbor(neighbor: Node) { + val updated = routingTable.enable(neighbor) + wasSelectedRouteUpdated = wasSelectedRouteUpdated || updated + } + + /** + * Disables the specified neighbor. + * + * May update the `wasSelectedRouteUpdated` property. + * + * @param neighbor the neighbor to disable + */ + fun disableNeighbor(neighbor: Node) { + val updated = routingTable.disable(neighbor) + wasSelectedRouteUpdated = wasSelectedRouteUpdated || updated + } + + override fun reset() { + super.reset() + } +} + +/** + * SS-BGP Protocol: when a loop is detected it tries to detect if the loop is recurrent using the WEAK detection + * condition. If it determines the loop is recurrent, it disables the neighbor that exported the route. + */ +class SSBGP(mrai: Time = 0, routingTable: RoutingTable = RoutingTable.empty(BGPRoute.invalid())) + : BaseSSBGP(mrai, routingTable) { + + override fun isRecurrent(node: Node, learnedRoute: BGPRoute, alternativeRoute: BGPRoute): Boolean { + return learnedRoute.localPref > alternativeRoute.localPref + } +} + +/** + * ISS-BGP: when a loop is detected it tries to detect if the loop is recurrent using the STRONG detection + * condition. If it determines the loop is recurrent, it disables the neighbor that exported the route. + */ +class ISSBGP(mrai: Time = 0, routingTable: RoutingTable = RoutingTable.empty(BGPRoute.invalid())) + : BaseSSBGP(mrai, routingTable) { + + override fun isRecurrent(node: Node, learnedRoute: BGPRoute, alternativeRoute: BGPRoute): Boolean { + return learnedRoute.localPref > alternativeRoute.localPref && + alternativeRoute.asPath == learnedRoute.asPath.subPathBefore(node) + } +} \ No newline at end of file diff --git a/src/main/kotlin/bgp/notifications/BGPNotifier.kt b/src/main/kotlin/bgp/notifications/BGPNotifier.kt new file mode 100644 index 0000000..f60ff66 --- /dev/null +++ b/src/main/kotlin/bgp/notifications/BGPNotifier.kt @@ -0,0 +1,209 @@ +package bgp.notifications + +import core.simulator.notifications.Notifier + +/** + * Created on 26-07-2017 + * + * @author David Fialho + */ +object BGPNotifier: Notifier { + + //region Lists containing the registered listeners + + private val importListeners = mutableListOf() + private val learnListeners = mutableListOf() + private val detectListeners = mutableListOf() + private val selectListeners = mutableListOf() + private val exportListeners = mutableListOf() + private val reEnableListeners = mutableListOf() + + //endregion + + //region Import notification + + /** + * Registers a new import listener. + * + * @param listener import listener to register. + */ + fun addImportListener(listener: ImportListener) { + importListeners.add(listener) + } + + /** + * Unregisters a new import listener. + * + * @param listener import listener to unregister. + */ + fun removeImportListener(listener: ImportListener) { + importListeners.remove(listener) + } + + /** + * Sends a import notification to each import listener. + * + * @param notification the import notification to send to each registered listener. + */ + fun notifyImport(notification: ImportNotification) { + importListeners.forEach { it.notify(notification) } + } + + //endregion + + //region Learn notification + + /** + * Registers a new learn listener. + * + * @param listener learn listener to register. + */ + fun addLearnListener(listener: LearnListener) { + learnListeners.add(listener) + } + + /** + * Unregisters a new learn listener. + * + * @param listener learn listener to unregister. + */ + fun removeLearnListener(listener: LearnListener) { + learnListeners.remove(listener) + } + + /** + * Sends a learn notification to each learn listener. + * + * @param notification the learn notification to send to each registered listener. + */ + fun notifyLearn(notification: LearnNotification) { + learnListeners.forEach { it.notify(notification) } + } + + //endregion + + //region Detect notification + + /** + * Registers a new detect listener. + * + * @param listener detect listener to register. + */ + fun addDetectListener(listener: DetectListener) { + detectListeners.add(listener) + } + + /** + * Unregisters a new detect listener. + * + * @param listener detect listener to unregister. + */ + fun removeDetectListener(listener: DetectListener) { + detectListeners.remove(listener) + } + + /** + * Sends a detect notification to each detect listener. + * + * @param notification the detect notification to send to each registered listener. + */ + fun notifyDetect(notification: DetectNotification) { + detectListeners.forEach { it.notify(notification) } + } + + //endregion + + //region Select notification + + /** + * Registers a new select listener. + * + * @param listener select listener to register. + */ + fun addSelectListener(listener: SelectListener) { + selectListeners.add(listener) + } + + /** + * Unregisters a new select listener. + * + * @param listener select listener to unregister. + */ + fun removeSelectListener(listener: SelectListener) { + selectListeners.remove(listener) + } + + /** + * Sends a select notification to each select listener. + * + * @param notification the select notification to send to each registered listener. + */ + fun notifySelect(notification: SelectNotification) { + selectListeners.forEach { it.notify(notification) } + } + + //endregion + + //region Export notification + + /** + * Registers a new export listener. + * + * @param listener export listener to register. + */ + fun addExportListener(listener: ExportListener) { + exportListeners.add(listener) + } + + /** + * Unregisters a new export listener. + * + * @param listener export listener to unregister. + */ + fun removeExportListener(listener: ExportListener) { + exportListeners.remove(listener) + } + + /** + * Sends a export notification to each export listener. + * + * @param notification the export notification to send to each registered listener. + */ + fun notifyExport(notification: ExportNotification) { + exportListeners.forEach { it.notify(notification) } + } + + //endregion + + //region Re-enable notification + + /** + * Registers a new re-enable listener. + * + * @param listener re-enable listener to register. + */ + fun addReEnableListener(listener: ReEnableListener) { + reEnableListeners.add(listener) + } + + /** + * Unregisters a new re-enable listener. + * + * @param listener re-enable listener to unregister. + */ + fun removeReEnableListener(listener: ReEnableListener) { + reEnableListeners.remove(listener) + } + + /** + * Sends a re-enable notification to each re-enable listener. + * + * @param notification the re-enable notification to send to each registered listener. + */ + fun notifyReEnable(notification: ReEnableNotification) { + reEnableListeners.forEach { it.notify(notification) } + } + + //endregion + +} \ No newline at end of file diff --git a/src/main/kotlin/bgp/notifications/DetectListener.kt b/src/main/kotlin/bgp/notifications/DetectListener.kt new file mode 100644 index 0000000..661b6b3 --- /dev/null +++ b/src/main/kotlin/bgp/notifications/DetectListener.kt @@ -0,0 +1,16 @@ +package bgp.notifications + +/** + * Created on 25-07-2017. + * + * @author David Fialho + * + * An DetectListener listens for DetectNotifications. + */ +interface DetectListener { + + /** + * Invoked to notify the listener of a new detect notification. + */ + fun notify(notification: DetectNotification) +} \ No newline at end of file diff --git a/src/main/kotlin/bgp/notifications/DetectNotification.kt b/src/main/kotlin/bgp/notifications/DetectNotification.kt new file mode 100644 index 0000000..79c3135 --- /dev/null +++ b/src/main/kotlin/bgp/notifications/DetectNotification.kt @@ -0,0 +1,20 @@ +package bgp.notifications + +import bgp.BGPRoute +import core.routing.Node +import core.simulator.notifications.Notification + +/** + * Created on 26-07-2017 + * + * @author David Fialho + * + * Notification sent when a node detects a recurrent routing loop. This is valid only for SS-BGP or ISS-BGP. + * + * @property node the node that detected a recurrent routing loop. + * @property learnedRoute the route that was learned + * @property alternativeRoute the alternative route + * @property neighbor the neighbor from which the route was learned + */ +data class DetectNotification(val node: Node, val learnedRoute: BGPRoute, val alternativeRoute: BGPRoute, + val neighbor: Node) : Notification() \ No newline at end of file diff --git a/src/main/kotlin/bgp/notifications/ExportListener.kt b/src/main/kotlin/bgp/notifications/ExportListener.kt new file mode 100644 index 0000000..b85027a --- /dev/null +++ b/src/main/kotlin/bgp/notifications/ExportListener.kt @@ -0,0 +1,16 @@ +package bgp.notifications + +/** + * Created on 25-07-2017. + * + * @author David Fialho + * + * An ExportListener listens for ExportNotifications. + */ +interface ExportListener { + + /** + * Invoked to notify the listener of a new export notification. + */ + fun notify(notification: ExportNotification) +} \ No newline at end of file diff --git a/src/main/kotlin/bgp/notifications/ExportNotification.kt b/src/main/kotlin/bgp/notifications/ExportNotification.kt new file mode 100644 index 0000000..a5bdbb4 --- /dev/null +++ b/src/main/kotlin/bgp/notifications/ExportNotification.kt @@ -0,0 +1,20 @@ +package bgp.notifications + +import bgp.BGPRoute +import core.routing.Node +import core.simulator.notifications.Notification + +/** + * Created on 26-07-2017 + * + * @author David Fialho + * + * Notification sent when a node exports a route received to its in-neighbors. + * This notification is sent to indicate that a node exported a route. It does not specify to which neighbors the + * route was exported and it is sent only once. To be notified of each routing message that is sent use the + * MessageSentNotification. + * + * @property node the node that exported a route + * @property route the route that was exported + */ +data class ExportNotification(val node: Node, val route: BGPRoute) : Notification() \ No newline at end of file diff --git a/src/main/kotlin/bgp/notifications/ImportListener.kt b/src/main/kotlin/bgp/notifications/ImportListener.kt new file mode 100644 index 0000000..370d334 --- /dev/null +++ b/src/main/kotlin/bgp/notifications/ImportListener.kt @@ -0,0 +1,16 @@ +package bgp.notifications + +/** + * Created on 25-07-2017. + * + * @author David Fialho + * + * An ImportListener listens for ImportNotifications. + */ +interface ImportListener { + + /** + * Invoked to notify the listener of a new import notification. + */ + fun notify(notification: ImportNotification) +} \ No newline at end of file diff --git a/src/main/kotlin/bgp/notifications/ImportNotification.kt b/src/main/kotlin/bgp/notifications/ImportNotification.kt new file mode 100644 index 0000000..58f8f81 --- /dev/null +++ b/src/main/kotlin/bgp/notifications/ImportNotification.kt @@ -0,0 +1,19 @@ +package bgp.notifications + +import bgp.BGPRoute +import core.routing.Node +import core.simulator.notifications.Notification + +/** + * Created on 26-07-2017 + * + * @author David Fialho + * + * Notification sent when a node imports a route received from one of its out-neighbors. + * + * @property node the node that imported a route + * @property route the route that was imported + * @property neighbor the neighbor from which the route was imported + */ +data class ImportNotification +(val node: Node, val route: BGPRoute, val neighbor: Node) : Notification() \ No newline at end of file diff --git a/src/main/kotlin/bgp/notifications/LearnListener.kt b/src/main/kotlin/bgp/notifications/LearnListener.kt new file mode 100644 index 0000000..18f7f97 --- /dev/null +++ b/src/main/kotlin/bgp/notifications/LearnListener.kt @@ -0,0 +1,16 @@ +package bgp.notifications + +/** + * Created on 25-07-2017. + * + * @author David Fialho + * + * An LearnListener listens for LearnNotifications. + */ +interface LearnListener { + + /** + * Invoked to notify the listener of a new learn notification. + */ + fun notify(notification: LearnNotification) +} \ No newline at end of file diff --git a/src/main/kotlin/bgp/notifications/LearnNotification.kt b/src/main/kotlin/bgp/notifications/LearnNotification.kt new file mode 100644 index 0000000..bd10463 --- /dev/null +++ b/src/main/kotlin/bgp/notifications/LearnNotification.kt @@ -0,0 +1,19 @@ +package bgp.notifications + +import bgp.BGPRoute +import core.routing.Node +import core.simulator.notifications.Notification + +/** + * Created on 26-07-2017 + * + * @author David Fialho + * + * Notification sent when a node learns a route from one of its out-neighbors. + * + * @property node the node that learned a route + * @property route the route that was learned + * @property neighbor the neighbor from which the route was learned + */ +data class LearnNotification +(val node: Node, val route: BGPRoute, val neighbor: Node) : Notification() \ No newline at end of file diff --git a/src/main/kotlin/bgp/notifications/ReEnableListenerListener.kt b/src/main/kotlin/bgp/notifications/ReEnableListenerListener.kt new file mode 100644 index 0000000..2b0361d --- /dev/null +++ b/src/main/kotlin/bgp/notifications/ReEnableListenerListener.kt @@ -0,0 +1,16 @@ +package bgp.notifications + +/** + * Created on 25-07-2017. + * + * @author David Fialho + * + * An ReEnableListener listens for ReEnableNotifications. + */ +interface ReEnableListener { + + /** + * Invoked to notify the listener of a new re-enable notification. + */ + fun notify(notification: ReEnableNotification) +} \ No newline at end of file diff --git a/src/main/kotlin/bgp/notifications/ReEnableNotification.kt b/src/main/kotlin/bgp/notifications/ReEnableNotification.kt new file mode 100644 index 0000000..60de108 --- /dev/null +++ b/src/main/kotlin/bgp/notifications/ReEnableNotification.kt @@ -0,0 +1,18 @@ +package bgp.notifications + +import bgp.BGPRoute +import core.routing.Node +import core.simulator.notifications.Notification + +/** + * Created on 26-07-2017 + * + * @author David Fialho + * + * Notification sent when a node re-enables its disabled neighbors. + * + * @property node the node that detected a recurrent routing loop + * @property reEnabledNeighbors the neighbors of 'node' that were re-enabled + */ +data class ReEnableNotification(val node: Node, + val reEnabledNeighbors: Collection>) : Notification() \ No newline at end of file diff --git a/src/main/kotlin/bgp/notifications/SelectListener.kt b/src/main/kotlin/bgp/notifications/SelectListener.kt new file mode 100644 index 0000000..b3de499 --- /dev/null +++ b/src/main/kotlin/bgp/notifications/SelectListener.kt @@ -0,0 +1,16 @@ +package bgp.notifications + +/** + * Created on 25-07-2017. + * + * @author David Fialho + * + * An SelectListener listens for SelectNotifications. + */ +interface SelectListener { + + /** + * Invoked to notify the listener of a new learn notification. + */ + fun notify(notification: SelectNotification) +} \ No newline at end of file diff --git a/src/main/kotlin/bgp/notifications/SelectNotification.kt b/src/main/kotlin/bgp/notifications/SelectNotification.kt new file mode 100644 index 0000000..f271577 --- /dev/null +++ b/src/main/kotlin/bgp/notifications/SelectNotification.kt @@ -0,0 +1,19 @@ +package bgp.notifications + +import bgp.BGPRoute +import core.routing.Node +import core.simulator.notifications.Notification + +/** + * Created on 26-07-2017 + * + * @author David Fialho + * + * Notification sent when a node selects a new route. + * + * @property node the node that selected a new route + * @property selectedRoute the newly route selected + * @property previousRoute the route being selected before + */ +data class SelectNotification +(val node: Node, val selectedRoute: BGPRoute, val previousRoute: BGPRoute) : Notification() \ No newline at end of file diff --git a/src/main/kotlin/bgp/policies/interdomain/InterdomainExtenders.kt b/src/main/kotlin/bgp/policies/interdomain/InterdomainExtenders.kt new file mode 100644 index 0000000..1fff59c --- /dev/null +++ b/src/main/kotlin/bgp/policies/interdomain/InterdomainExtenders.kt @@ -0,0 +1,78 @@ +package bgp.policies.interdomain + +import bgp.BGPRoute +import core.routing.Extender +import core.routing.Node + +/** + * Created on 26-07-2017 + * + * @author David Fialho + */ + +const val LOCAL_PREF_PEERPLUS: Int = 400000 +const val LOCAL_PREF_CUSTOMER: Int = 300000 +const val LOCAL_PREF_PEER: Int = 200000 +const val LOCAL_PREF_PROVIDER: Int = 100000 + +object CustomerExtender : Extender { + + override fun extend(route: BGPRoute, sender: Node): BGPRoute { + + return when { + route.localPref <= LOCAL_PREF_PEER -> BGPRoute.invalid() + else -> customerRoute(asPath = route.asPath.append(sender)) + } + } + +} + +object PeerExtender : Extender { + + override fun extend(route: BGPRoute, sender: Node): BGPRoute { + + return when { + route.localPref <= LOCAL_PREF_PEER -> BGPRoute.invalid() + else -> peerRoute(asPath = route.asPath.append(sender)) + } + } + +} + +object ProviderExtender : Extender { + + override fun extend(route: BGPRoute, sender: Node): BGPRoute { + + return when { + !route.isValid() -> BGPRoute.invalid() + else -> providerRoute(asPath = route.asPath.append(sender)) + } + } + +} + +object PeerplusExtender : Extender { + + override fun extend(route: BGPRoute, sender: Node): BGPRoute { + + return when { + route.localPref <= LOCAL_PREF_PEER -> BGPRoute.invalid() + else -> peerplusRoute(asPath = route.asPath.append(sender)) + } + } + +} + +object SiblingExtender : Extender { + + override fun extend(route: BGPRoute, sender: Node): BGPRoute { + + return when { + !route.isValid() -> BGPRoute.invalid() + route === BGPRoute.self() -> customerRoute(siblingHops = 1, asPath = route.asPath.append(sender)) + else -> BGPRoute.with(localPref = route.localPref - 1, + asPath = route.asPath.append(sender)) + } + } + +} \ No newline at end of file diff --git a/src/main/kotlin/bgp/policies/interdomain/Routes.kt b/src/main/kotlin/bgp/policies/interdomain/Routes.kt new file mode 100644 index 0000000..5fe804d --- /dev/null +++ b/src/main/kotlin/bgp/policies/interdomain/Routes.kt @@ -0,0 +1,37 @@ +package bgp.policies.interdomain + +import bgp.BGPRoute +import core.routing.Path +import core.routing.emptyPath + +/** + * Created on 26-07-2017 + * + * @author David Fialho + * + * This file contains methods to construct interdomain routes. + */ + +/** + * Returns a peer+ route. + */ +fun peerplusRoute(siblingHops: Int = 0, asPath: Path = emptyPath()) + = BGPRoute.with(localPref = LOCAL_PREF_PEERPLUS - siblingHops, asPath = asPath) + +/** + * Returns a customer route. + */ +fun customerRoute(siblingHops: Int = 0, asPath: Path = emptyPath()) + = BGPRoute.with(localPref = LOCAL_PREF_CUSTOMER - siblingHops, asPath = asPath) + +/** + * Returns a peer route. + */ +fun peerRoute(siblingHops: Int = 0, asPath: Path = emptyPath()) + = BGPRoute.with(localPref = LOCAL_PREF_PEER - siblingHops, asPath = asPath) + +/** + * Returns a provider route. + */ +fun providerRoute(siblingHops: Int = 0, asPath: Path = emptyPath()) + = BGPRoute.with(localPref = LOCAL_PREF_PROVIDER - siblingHops, asPath = asPath) diff --git a/src/main/kotlin/bgp/policies/shortestpath/ShortestPathExtender.kt b/src/main/kotlin/bgp/policies/shortestpath/ShortestPathExtender.kt new file mode 100644 index 0000000..c42e546 --- /dev/null +++ b/src/main/kotlin/bgp/policies/shortestpath/ShortestPathExtender.kt @@ -0,0 +1,28 @@ +package bgp.policies.shortestpath + +import bgp.BGPRoute +import core.routing.Extender +import core.routing.Node +import core.routing.pathOf + +/** + * Created on 24-07-2017. + * + * @author David Fialho + */ +class ShortestPathExtender(val cost: Int) : Extender { + + /** + * Extends the route by adding the cost of the extender to the LOCAL-PREF of the incoming route. + */ + override fun extend(route: BGPRoute, sender: Node): BGPRoute { + + return when { + !route.isValid() -> BGPRoute.invalid() + BGPRoute.self() == route -> BGPRoute.with(localPref = cost, asPath = pathOf(sender)) + else -> BGPRoute.with(localPref = route.localPref + cost, asPath = route.asPath.append(sender)) + } + + } + +} \ No newline at end of file diff --git a/src/main/kotlin/core/routing/Extender.kt b/src/main/kotlin/core/routing/Extender.kt new file mode 100644 index 0000000..391cffc --- /dev/null +++ b/src/main/kotlin/core/routing/Extender.kt @@ -0,0 +1,26 @@ +package core.routing + +/** + * Created on 19-07-2017 + * + * @author David Fialho + * + * An extender is a transformation function. It is always associated with a link between two neighboring nodes. + * This function describes how each route elected by the head node of the link is transformed at the tail node. + * It describes both the export bgp.policies of the head node and the import bgp.policies of the tail node. + * + * TODO improve the documentation for extender + */ +interface Extender { + + /** + * Takes a route and returns a new route with the attributes defined according to the implementation of the + * extender function. + * + * @param route the route to be extended + * @param sender the node that sends the route + * @return the extended route + */ + fun extend(route: R, sender: Node): R + +} \ No newline at end of file diff --git a/src/main/kotlin/core/routing/Link.kt b/src/main/kotlin/core/routing/Link.kt new file mode 100644 index 0000000..4983b9b --- /dev/null +++ b/src/main/kotlin/core/routing/Link.kt @@ -0,0 +1,11 @@ +package core.routing + + +/** + * Data class to represent an uni-directional link in a topology. + * + * @property tail the node at the tail of the link + * @property head the node at the head of the link + * @property extender the extender used to map routes exported by the head node to the tail node + */ +data class Link(val tail: Node, val head: Node, val extender: Extender) \ No newline at end of file diff --git a/src/main/kotlin/core/routing/Message.kt b/src/main/kotlin/core/routing/Message.kt new file mode 100644 index 0000000..7d84614 --- /dev/null +++ b/src/main/kotlin/core/routing/Message.kt @@ -0,0 +1,16 @@ +package core.routing + +/** + * Created on 21-07-2017 + * + * @author David Fialho + * + * Data class representing a routing message. + * + * @property sender the node that sent the message + * @property receiver the node that will receive the messsage + * @property route the route to be sent + * @property extender the extender that will be used to map the route sent by the [sender] to the route learned at + * the [receiver] + */ +data class Message(val sender: Node, val receiver: Node, val route: R, val extender: Extender) \ No newline at end of file diff --git a/src/main/kotlin/core/routing/Neighbor.kt b/src/main/kotlin/core/routing/Neighbor.kt new file mode 100644 index 0000000..cc70184 --- /dev/null +++ b/src/main/kotlin/core/routing/Neighbor.kt @@ -0,0 +1,12 @@ +package core.routing + +import core.simulator.Exporter + +/** + * Data class containing all the attributes that define a neighbor. + * + * @property node the neighbor node + * @property extender the extender used to map routes exported to this neighbor + * @property exporter the exporter used to export routes to this neighbor + */ +data class Neighbor(val node: Node, val extender: Extender, val exporter: Exporter = Exporter()) \ No newline at end of file diff --git a/src/main/kotlin/core/routing/Node.kt b/src/main/kotlin/core/routing/Node.kt new file mode 100644 index 0000000..9f4a7b7 --- /dev/null +++ b/src/main/kotlin/core/routing/Node.kt @@ -0,0 +1,115 @@ +package core.routing + +import core.simulator.notifications.BasicNotifier +import core.simulator.notifications.MessageReceivedNotification +import core.simulator.notifications.MessageSentNotification + +/** + * Alias used for node IDs + * This makes it easy to change the ID type to long if an Int is too small + */ +typealias NodeID = Int + +/** + * Created on 19-07-17 + * + * @author David Fialho + * + * A node is the fundamental component of a topology @see Topology. A node is some entity that is able to speak with + * other nodes using a common protocol. + * + * @property id The ID of the node. This ID uniquely identifies it inside a topology + */ +class Node(val id: NodeID, val protocol: Protocol) { + + /** + * Collection containing the in-neighbors of this node. + */ + val inNeighbors: Collection> + get() = protocol.inNeighbors + + /** + * Sets a new in-neighbor for this node. + * + * @param neighbor the in-neighbor node + * @param extender the extender used to map routes from this node to the in-neighbor + */ + fun addInNeighbor(neighbor: Node, extender: Extender) { + protocol.addInNeighbor(Neighbor(neighbor, extender)) + } + + /** + * Starts the protocol deployed by this node. + */ + fun start() { + protocol.start(this) + } + + /** + * Sends a message containing the route [route] to all in-neighbors of this node. + * + * @param route the route to be sent + */ + fun send(route: R) { + + for (neighbor in inNeighbors) { + send(route, neighbor) + } + } + + /** + * Sends a message containing the route [route] to the specified neighbor. + * + * @param route the route to be sent + */ + fun send(route: R, neighbor: Neighbor) { + val message = Message(this, neighbor.node, route, neighbor.extender) + + neighbor.exporter.export(message) + BasicNotifier.notifyMessageSent(MessageSentNotification(message)) + } + + /** + * Receives a message from an out-neighbor of this node. + * This method should be invoked when a new message is expected to arrive to this node and be processed by it. + */ + fun receive(message: Message) { + + BasicNotifier.notifyMessageReceived(MessageReceivedNotification(message)) + protocol.process(message) + } + + /** + * Resets the node state. + */ + fun reset() { + protocol.reset() + inNeighbors.forEach { it.exporter.reset() } + } + + /** + * Two nodes are considered equal if they have exactly the same ID. + * Subclasses of node should not override this method. + */ + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Node<*> + + if (id != other.id) return false + + return true + } + + /** + * Follows the equals/hashCode contract. + */ + override fun hashCode(): Int { + return id + } + + override fun toString(): String { + return "Node($id)" + } +} \ No newline at end of file diff --git a/src/main/kotlin/core/routing/Path.kt b/src/main/kotlin/core/routing/Path.kt new file mode 100644 index 0000000..d102335 --- /dev/null +++ b/src/main/kotlin/core/routing/Path.kt @@ -0,0 +1,108 @@ +package core.routing + +/** + * Created on 21-07-2017 + * + * @author David Fialho + * + * A Path is a sequence of nodes that form a path in a network. Nodes appended to a path are kept in order. + * Nodes can be repeated in a path. That is, a path may include two or more nodes with the same ID. + * + * Path instances are immutable! + * + * @property size expresses the number of nodes in the path + */ +class Path internal constructor(private val nodes: List>) : Iterable> { + + val size: Int = nodes.size + + /** + * Returns a new path instance containing the nodes in the same order as this path and with the given node + * appended to it. + */ + fun append(node: Node<*>): Path { + val nodesCopy = ArrayList(nodes) + nodesCopy.add(node) + + return Path(nodesCopy) + } + + /** + * Returns the next-hop node of the path. + */ + fun nextHop(): Node<*>? { + return nodes.lastOrNull() + } + + /** + * Checks if this path contains the given node. + */ + operator fun contains(node: Node<*>) = node in nodes + + /** + * Returns a path instance containing exactly the same nodes as this path and exactly in the same order. + */ + fun copy(): Path = Path(nodes) + + /** + * Returns a path corresponding to the sub-path from the beginning of the path until the fir node equal to the + * specified node. + */ + fun subPathBefore(node: Node<*>): Path { + + val nodeIndex = nodes.indexOf(node) + return if (nodeIndex >= 0) Path(nodes.subList(0, nodeIndex)) else this + } + + /** + * Returns an iterator over the nodes of the path. The iterator starts at first node in the path. + */ + override fun iterator(): Iterator> { + return nodes.iterator() + } + + /** + * Two paths are considered equal if they have the exact same nodes in the exact same order. + */ + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other?.javaClass != javaClass) return false + + other as Path + + if (nodes != other.nodes) return false + + return true + } + + override fun hashCode(): Int { + return nodes.hashCode() + } + + override fun toString(): String { + return "$nodes" + } + +} + +/** + * Returns a path with no nodes. + */ +fun emptyPath(): Path { + return Path(emptyList()) +} + +/** + * Returns a path containing the given nodes in the same order as they are given. + */ +fun pathOf(vararg nodes: Node<*>): Path { + return Path(listOf(*nodes)) +} + +/** + * Returns an empty path. + */ +@Suppress("NOTHING_TO_INLINE") +inline fun pathOf(): Path { + return emptyPath() +} \ No newline at end of file diff --git a/src/main/kotlin/core/routing/Protocol.kt b/src/main/kotlin/core/routing/Protocol.kt new file mode 100644 index 0000000..852717d --- /dev/null +++ b/src/main/kotlin/core/routing/Protocol.kt @@ -0,0 +1,36 @@ +package core.routing + +/** + * This is the basic interface for a protocol implementation. + */ +interface Protocol { + + /** + * Collection of all the in-neighbors added to the protocol. + */ + val inNeighbors: Collection> + + /** + * Adds a new in-neighbor for the protocol to consider. + */ + fun addInNeighbor(neighbor: Neighbor) + + /** + * Starts this protocol. + */ + fun start(node: Node) + + /** + * Processes an incoming routing message. + * + * This method is invoked by the node using this protocol when it receives a new routing message that must be + * processed. + */ + fun process(message: Message) + + /** + * Resets the state of the protocol to its initial state. + */ + fun reset() + +} \ No newline at end of file diff --git a/src/main/kotlin/core/routing/Route.kt b/src/main/kotlin/core/routing/Route.kt new file mode 100644 index 0000000..5b97498 --- /dev/null +++ b/src/main/kotlin/core/routing/Route.kt @@ -0,0 +1,24 @@ +package core.routing + +/** + * Created on 19-07-2017 + * + * @author David Fialho + * + * Routes are pieces of routing information that associate a set of attributes with a destination. Usually, nodes + * exchange routes with their neighbors in other to provide connectivity to one another. + * + * Routes may be invalid. An invalid route indicates that there is no electable route to the destination via some + * neighbor, which means that an invalid route should NEVER be elected by a node. Routes can be checked for validity + * through the isInvalid() method included in the Route interface. + * + * Implementations of the Route interface should be always immutable! + */ +interface Route { + + /** + * Returns true if a route is valid or false if otherwise. + */ + fun isValid(): Boolean + +} \ No newline at end of file diff --git a/src/main/kotlin/core/routing/RouteSelector.kt b/src/main/kotlin/core/routing/RouteSelector.kt new file mode 100644 index 0000000..3b2b110 --- /dev/null +++ b/src/main/kotlin/core/routing/RouteSelector.kt @@ -0,0 +1,226 @@ +package core.routing + +/** + * Created on 21-07-2017 + * + * @author David Fialho + * + * A route selector is responsible for selecting the best route in a routing table. It works similar to a cache: to + * update the routing table the update() method of the selector should be used. This allows the selector to adjust + * the selected route/neighbor in the most efficient way possible. + * + * DO NOT update the routing table outside of the selector. Doing so will prevent the selector from working correctly + * since it is not informed of the changes performed to the table. + * + * The constructor takes the routing table holding the routes, a forceSelect flag, and a compare method. If the force + * reselect flag is set to true it will force the selector to reselect the route/neighbor based on the initial routes of + * the given table. By default, the flag is set to true. This flag should be set to false if and only if you are + * sure the table contains only invalid routes. The compare method should take to routes and compare their + * preferences. It should as a common compare method which returns a positive value if the left route has higher + * preference than the right route, 0 if they have the same preference and a negative value if the left route has a + * lower preference than the right route. + * + * @param table the table to select routes from + * @param forceReselect if set to true the selector will perform a reselect operation in the initializer + * @param compare the method used by the selector to compare the routes + */ +class RouteSelector private constructor +(val table: RoutingTable, private val compare: (R, R) -> Int, forceReselect: Boolean = true) { + + companion object Factory { + + /** + * Returns a RouteSelector wrapping a newly created routing table. + * + * @param invalid the invalid route + * @param compare the compare method used to compare route preferences + */ + fun wrapNewTable(invalid: R, compare: (R, R) -> Int): RouteSelector { + return RouteSelector( + table = RoutingTable.empty(invalid), + compare = compare, + forceReselect = false) + } + + /** + * Returns a RouteSelector wrapping an existing routing table. + * + * @param table the table to be wrapped by the selector + * @param compare the compare method used to compare route preferences + */ + fun wrap(table: RoutingTable, compare: (R, R) -> Int): RouteSelector { + return RouteSelector(table, compare) + } + + } + + // Stores the currently selected route + private var selectedRoute: R = table.invalidRoute + // Stores the currently selected neighbor + private var selectedNeighbor: Node? = null + + /** + * Keeps record of the neighbors that are disabled. + */ + private val mutableDisabledNeighbors = HashSet>() + val disabledNeighbors: Collection> get() = mutableDisabledNeighbors + + init { + if (forceReselect) { + reselect() + } + } + + /** + * Returns the currently selected route + */ + fun getSelectedRoute(): R = selectedRoute + + /** + * Returns the currently selected neighbor. + */ + fun getSelectedNeighbor(): Node? = selectedNeighbor + + /** + * This method should always be used to update the routing table when a selector is being used. + * + * Updates the routing table, setting the given route as the candidate route via the given neighbor. + * The selected route/neighbor may also be updated if the given route/neighbor forces a reselection. + * + * @return true if the selected route/neighbor was updated or false if otherwise + */ + fun update(neighbor: Node, route: R): Boolean { + + table[neighbor] = route + + return if (table.isEnabled(neighbor) && compare(route, selectedRoute) > 0) { + updateSelectedTo(route, neighbor) + true + + } else if (neighbor == selectedNeighbor && compare(route, selectedRoute) != 0) { + reselect() + true + + } else { + // do nothing + false + } + } + + /** + * Disables a neighbor. Routes learned from a disabled neighbor are still stored in the routing table, but the + * selector will never select a candidate route associated with that neighbor. + * + * @return true if the selected route/neighbor was updated or false if otherwise + */ + fun disable(neighbor: Node): Boolean { + + table.setEnabled(neighbor, false) + mutableDisabledNeighbors.add(neighbor) + + // Do not need to check if the node was added to the disabled neighbors set: + // if it wasn't then the neighbor was already disabled and surely is not the selected neighbor + + if (neighbor == selectedNeighbor) { + reselect() + return true + } + + return false + } + + /** + * Enables a neighbor that was disabled. If the neighbor was not disabled than nothing changes. + * + * @return true if the selected route/neighbor was updated or false if otherwise + */ + fun enable(neighbor: Node): Boolean { + + table.setEnabled(neighbor, true) + + // Checking if the neighbor was really removed from the disabled set prevents making a table lookup + // if the node was not disabled + + if (mutableDisabledNeighbors.remove(neighbor)) { + + + val route = table[neighbor] + + if (compare(route, selectedRoute) > 0) { + updateSelectedTo(route, neighbor) + return true + } + } + + return false + } + + /** + * Enables all neighbors that are currently disabled. + * + * @return true if the selected route/neighbor was updated or false if otherwise + */ + fun enableAll(): Boolean { + + var selectedRouteAmongDisabled = table.invalidRoute + var selectedNeighborAmongDisabled: Node? = null + + for (neighbor in mutableDisabledNeighbors) { + val route = table.setEnabled(neighbor, true) + + if (compare(route, selectedRouteAmongDisabled) > 0) { + selectedRouteAmongDisabled = route + selectedNeighborAmongDisabled = neighbor + } + } + + // If we are enabling all neighbors that this set can be cleared + mutableDisabledNeighbors.clear() + + if (compare(selectedRouteAmongDisabled, selectedRoute) > 0) { + selectedRoute = selectedRouteAmongDisabled + selectedNeighbor = selectedNeighborAmongDisabled + return true + } else { + return false + } + } + + /** + * Forces the selector to reselect the route/neighbor based on the current candidates routes available in the + * routing table. + */ + fun reselect() { + + selectedRoute = table.invalidRoute + selectedNeighbor = null + + table.forEach { neighbor, route, enabled -> if (enabled && compare(route, + selectedRoute) > 0) { + selectedRoute = route + selectedNeighbor = neighbor + } + } + } + + /** + * Clears the wrapped routing table. + */ + fun clear() { + selectedRoute = table.invalidRoute + selectedNeighbor = null + table.clear() + mutableDisabledNeighbors.clear() + } + + @Suppress("NOTHING_TO_INLINE") + inline private fun updateSelectedTo(route: R, neighbor: Node?) { + selectedRoute = route + selectedNeighbor = neighbor + } + + override fun toString(): String { + return "RouteSelector(table=$table)" + } + +} \ No newline at end of file diff --git a/src/main/kotlin/core/routing/RoutingTable.kt b/src/main/kotlin/core/routing/RoutingTable.kt new file mode 100644 index 0000000..d65436b --- /dev/null +++ b/src/main/kotlin/core/routing/RoutingTable.kt @@ -0,0 +1,123 @@ +package core.routing + +/** + * Created on 21-07-2017 + * + * @author David Fialho + * + * The routing table stores a candidate route for each defined out-neighbor. + * For each neighbor it holds a flag indicating if the neighbor is enabled or not. If the neighbor is set as disabled + * the route associated with that neighbor should not ever be selected. + * + * It does not perform any route selection! For that use the RouteSelector. + */ +class RoutingTable +private constructor(val invalidRoute: R, private val routes: MutableMap, EntryData> = HashMap()) { + + /** + * Returns the number of entries in the table. + */ + val size: Int get() = routes.size + + /** + * Contains the data stored in each entry. + */ + data class EntryData(var route: R, var enabled: Boolean = true) + + /** + * Represents an entry in the routing table. + */ + data class Entry(val neighbor: Node, val route: R, val enabled: Boolean = true) + + companion object Factory { + + /** + * Returns a routing table with no entries. + */ + fun empty(invalid: R) = RoutingTable(invalid) + + /** + * Returns a routing table containing the specified entries. + */ + fun of(invalid: R, vararg entries: Entry): RoutingTable { + + val routes = HashMap, EntryData>(entries.size) + for ((neighbor, route, enabled) in entries) { + routes.put(neighbor, EntryData(route, enabled)) + } + + return RoutingTable(invalid, routes) + } + + } + + /** + * Returns the candidate route via a neighbor. + */ + operator fun get(neighbor: Node): R { + return routes[neighbor]?.route ?: invalidRoute + } + + /** + * Sets the candidate route via a neighbor. + * If the given node is not defined as a neighbor, then the table is not modified. + */ + operator fun set(neighbor: Node, route: R) { + + val entry = routes[neighbor] + + if (entry == null) { + routes[neighbor] = EntryData(route) + } else { + entry.route = route + } + + } + + /** + * Clears all entries from the table. + */ + fun clear() { + routes.clear() + } + + /** + * Sets the enable/disable flag for the given neighbor. + * + * @return the route via the specified neighbor or invalid route if the table contains no entry for that neighbor + */ + fun setEnabled(neighbor: Node, enabled: Boolean): R { + val entry = routes[neighbor] + + return if (entry == null) { + routes[neighbor] = EntryData(invalidRoute, enabled = false) + invalidRoute + + } else { + entry.enabled = enabled + entry.route + } + } + + /** + * Checks if a neighbor is enabled or not. If the table contains no entry for the specified neighbor it indicates + * the neighbor is enabled. + */ + fun isEnabled(neighbor: Node): Boolean { + return routes[neighbor]?.enabled ?: return true + } + + /** + * Provides way to iterate over each entry in the table. + */ + inline internal fun forEach(operation: (Node, R, Boolean) -> Unit) { + for ((neighbor, entry) in routes) { + operation(neighbor, entry.route, entry.enabled) + } + } + + override fun toString(): String { + return "RoutingTable(routes=$routes)" + } + +} \ No newline at end of file diff --git a/src/main/kotlin/core/routing/Topology.kt b/src/main/kotlin/core/routing/Topology.kt new file mode 100644 index 0000000..6c4ffe5 --- /dev/null +++ b/src/main/kotlin/core/routing/Topology.kt @@ -0,0 +1,73 @@ +package core.routing + +/** + * Created on 16-07-2017. + * + * @author David Fialho + * + * Topology is an high-level abstraction of a network composed of nodes and their interconnections. + * This interface defines the interface that is common to all topologies. + * + * Notice the Topology interface does not define any methods to add/remove nodes or links. Topology implementations + * should be immutable! That is, there should be no way to add or remove nodes from the topology and the same should + * be true for links. A topology should be built using a topology builder @see TopologyBuilder. + * + * Each node in a topology is uniquely identified by its ID. Therefore, the topology interface provides methods to + * access topology nodes using their IDs. + * + * This interface takes a generic type N that extends from Node. N is the type of nodes that the topology holds. + * + * @property size the number of nodes in the topology + */ +class Topology(private val idToNode: Map>) { + + /** + * Number of nodes in the topology. + */ + val size: Int = idToNode.size + + /** + * Number of links in the topology. + */ + val linkCount: Int + get() = idToNode.flatMap { it.value.inNeighbors }.count() + + /** + * Collection containing all nodes in the topology in no particular order. + */ + val nodes: Collection> = idToNode.values + + /** + * Collection containing all links in the topology in no particular order. + */ + val links: Collection> + get() { + + val links = ArrayList>() + + for (node in nodes) { + for ((neighbor, extender, _) in node.inNeighbors) { + links.add(Link(neighbor, node, extender)) + } + } + + return links + } + + + /** + * Returns the node associated with the given ID. + + * @param id the ID of the node to get from the network. + * @return the node associated with the given ID or null if the topology does not contain a node with such an ID. + */ + operator fun get(id: Int): Node? = idToNode[id] + + /** + * Resets the topology state. + */ + fun reset() { + nodes.forEach { it.reset() } + } + +} diff --git a/src/main/kotlin/core/routing/TopologyBuilder.kt b/src/main/kotlin/core/routing/TopologyBuilder.kt new file mode 100644 index 0000000..676e8af --- /dev/null +++ b/src/main/kotlin/core/routing/TopologyBuilder.kt @@ -0,0 +1,74 @@ +package core.routing + +/** + * Thrown by the topology builder when trying to add an element to the builder that was already added before. + */ +class ElementExistsException(message: String): Exception(message) + + +/** + * Thrown by the topology builder when an element is required but it was not yet added to the builder. + */ +class ElementNotFoundException(message: String): Exception(message) + + +/** + * Created on 21-07-2017 + * + * @author David Fialho + */ +class TopologyBuilder { + + private val nodes = HashMap>() + private val links = HashSet>() + + /** + * Adds a new node with the specified ID to the builder. If a node with the same ID was already added to the + * builder then it does not add any new node and throws an ElementExistsException. + * + * @param id the ID to identify the new node + * @param protocol the protocol deployed by the new node + * @throws ElementExistsException if a node with the specified ID was already added to the builder + */ + @Throws(ElementExistsException::class) + fun addNode(id: NodeID, protocol: Protocol) { + + if (nodes.putIfAbsent(id, Node(id, protocol)) != null) { + throw ElementExistsException("Node with ID `$id` was added twice to the topology builder") + } + } + + /** + * Establishes a new link connecting the node identified by the [from] ID to the node identified by the [to] ID. + * + * It initializes the link with the specified extender. This extender will be used to map the routes exported by + * the [to] node and learned by the [from] node. + * + * @param from the Id of the node at the tail of the link + * @param to the protocol deployed by the new node + * @param extender the protocol deployed by the new node + * @throws ElementExistsException if a node with the specified ID was already added to the builder + * @throws ElementNotFoundException if builder is missing the node with ID [from] and/or [to] + */ + @Throws(ElementExistsException::class, ElementNotFoundException::class) + fun link(from: NodeID, to: NodeID, extender: Extender) { + + val tail = nodes[from] ?: throw ElementNotFoundException("Node with ID `$from` was not yet added the builder") + val head = nodes[to] ?: throw ElementNotFoundException("Node with ID `$to` was not yet added the builder") + + if (!links.add(Link(tail, head, extender))) { + throw ElementExistsException("Link from `$from` to `$to` was already added to the topology builder") + } + + head.addInNeighbor(tail, extender) + } + + /** + * Returns a Topology containing the nodes and links defined in the builder at the time this method is + * called. + */ + fun build(): Topology { + return Topology(nodes) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/core/simulator/DelayGenerator.kt b/src/main/kotlin/core/simulator/DelayGenerator.kt new file mode 100644 index 0000000..b9f0aa9 --- /dev/null +++ b/src/main/kotlin/core/simulator/DelayGenerator.kt @@ -0,0 +1,34 @@ +package core.simulator + +/** + * Created on 22-07-2017 + * + * @author David Fialho + */ +interface DelayGenerator { + + /** + * Th seed used to generate the delays. + */ + val seed: Long + + /** + * Generates the next delay value and returns it. + */ + fun nextDelay(): Time + + /** + * Resets the delay generator. + * + * The general contract of reset is that it alters the state of the delay generator object so as to be in + * exactly the same state as if it had just been created and set to use the specified seed. Thus, future calls to + * nextDelay() should return the same values as if the generator was just created. + */ + fun reset() + + /** + * Generates a new seed for the delay generator and sets the new seed as the generator's seed. + */ + fun generateNewSeed() + +} \ No newline at end of file diff --git a/src/main/kotlin/core/simulator/Engine.kt b/src/main/kotlin/core/simulator/Engine.kt new file mode 100644 index 0000000..e02cfd3 --- /dev/null +++ b/src/main/kotlin/core/simulator/Engine.kt @@ -0,0 +1,85 @@ +package core.simulator + +import core.routing.Node +import core.routing.Topology +import core.simulator.notifications.BasicNotifier +import core.simulator.notifications.EndNotification +import core.simulator.notifications.StartNotification +import core.simulator.notifications.ThresholdReachedNotification + +/** + * Created on 23-07-2017 + * + * @author David Fialho + */ +object Engine { + + /** + * This variable holds the scheduler that is being used in the simulations. + */ + var scheduler = Scheduler + + /** + * This is the delay generator used to generate the delays for the messages. + * By default, it uses a ZeroDelayGenerator + * This should be changed to a different generator to obtain different behavior. + */ + var messageDelayGenerator: DelayGenerator = ZeroDelayGenerator + + /** + * Resets the engine to the defaults. + */ + fun resetToDefaults() { + scheduler = Scheduler + messageDelayGenerator = ZeroDelayGenerator + } + + /** + * Runs the simulation for the given destination. + * The threshold value determines the number of units of time the simulation should have terminated on. If this + * threshold is reached the simulation is interrupted immediately. If no threshold is specified then the + * simulator will run 'forever'. + * + * @param topology the topology used for the simulation + * @param destination the destination used for the simulation + * @param threshold a threshold value for the simulation + * @return true if the simulation terminated before the specified threshold or false if otherwise. + */ + fun simulate(topology: Topology<*>, destination: Node<*>, threshold: Time = Int.MAX_VALUE): Boolean { + + // Ensure the scheduler is completely clean before starting the simulation + scheduler.reset() + + BasicNotifier.notifyStart(StartNotification(messageDelayGenerator.seed, topology)) + + // The simulation execution starts when the protocol of the destination is started + destination.start() + + var terminatedBeforeThreshold = true + while (scheduler.hasEvents()) { + val event = scheduler.nextEvent() + + // Check if the threshold was reached: + // This verification needs to be performed after obtaining the next event because the scheduler's time is + // updated when performing that action + if (currentTime() >= threshold) { + BasicNotifier.notifyThresholdReached(ThresholdReachedNotification(threshold)) + terminatedBeforeThreshold = false + break + } + + event.processIt() + } + + BasicNotifier.notifyEnd(EndNotification()) + + return terminatedBeforeThreshold + } + +} + +/** + * Cleaner way to access the simulation time. + */ +@Suppress("NOTHING_TO_INLINE") +inline fun currentTime(): Time = Engine.scheduler.time \ No newline at end of file diff --git a/src/main/kotlin/core/simulator/Event.kt b/src/main/kotlin/core/simulator/Event.kt new file mode 100644 index 0000000..65d638f --- /dev/null +++ b/src/main/kotlin/core/simulator/Event.kt @@ -0,0 +1,14 @@ +package core.simulator + +/** + * Created on 22-07-2017 + * + * @author David Fialho + */ +interface Event { + + /** + * Processes this event. + */ + fun processIt() +} \ No newline at end of file diff --git a/src/main/kotlin/core/simulator/ExportEvent.kt b/src/main/kotlin/core/simulator/ExportEvent.kt new file mode 100644 index 0000000..5a517dd --- /dev/null +++ b/src/main/kotlin/core/simulator/ExportEvent.kt @@ -0,0 +1,21 @@ +package core.simulator + +import core.routing.Route +import core.routing.Message + + +/** + * Created on 22-07-2017 + * + * @author David Fialho + */ +class ExportEvent(private val message: Message) : Event { + + /** + * Sends the message to the receiver node. + */ + override fun processIt() { + message.receiver.receive(message) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/core/simulator/Exporter.kt b/src/main/kotlin/core/simulator/Exporter.kt new file mode 100644 index 0000000..6512a91 --- /dev/null +++ b/src/main/kotlin/core/simulator/Exporter.kt @@ -0,0 +1,42 @@ +package core.simulator + +import core.routing.Message +import core.routing.Route + +/** + * Created on 22-07-2017 + * + * @author David Fialho + */ +class Exporter { + + /** + * Stores the timestamp of the time at which the last message exported using this exporter was delivered to its + * destination. + */ + private var lastDeliverTime = 0 + + /** + * It issues an export event with the specified message. + * + * @return the deliver time of the exported message + */ + fun export(message: Message): Time { + + val delay = Engine.messageDelayGenerator.nextDelay() + val deliverTime = maxOf(Scheduler.time + delay, lastDeliverTime) + 1 + + Scheduler.schedule(ExportEvent(message), deliverTime) + lastDeliverTime = deliverTime + + return deliverTime + } + + /** + * Resets the exporter to its initial state. + */ + fun reset() { + lastDeliverTime = 0 + } + +} \ No newline at end of file diff --git a/src/main/kotlin/core/simulator/RandomDelayGenerator.kt b/src/main/kotlin/core/simulator/RandomDelayGenerator.kt new file mode 100644 index 0000000..5d1db81 --- /dev/null +++ b/src/main/kotlin/core/simulator/RandomDelayGenerator.kt @@ -0,0 +1,64 @@ +package core.simulator + +import java.lang.System +import java.util.Random + +/** + * Created on 22-07-2017 + * + * @author David Fialho + * + * Generates delays randomly in a specified interval. + * + * @param min the minimum delay value the generator will generate + * @param max the maximum delay value the generator will generate + * @param seed the seed used to generate the random delays + */ +class RandomDelayGenerator +private constructor(val min: Time, val max: Time, seed: Long): DelayGenerator { + + /** + * Initial seed used for the generator. When reset() is called this seed is reused. + */ + override var seed = seed + private set + + companion object Factories { + + /** + * Returns a RandomDelayGenerator with the specified configurations. + * + * @param min the minimum delay value the generator will generate. Must be higher than 0 + * @param max the maximum delay value the generator will generate. Must be higher than or equal to 'min' + * @param seed the seed used to generate the random delays. If none is provided it uses the system current time + * @throws IllegalStateException if 'max' is lower than 'min' or if 'min' is lower than 0. + */ + @Throws(IllegalStateException::class) + fun with(min: Time, max: Time, seed: Long = System.currentTimeMillis()): RandomDelayGenerator { + + if (min < 0 || max < min) { + throw IllegalArgumentException("Maximum delay can not be lower than minimum and minimum must be a " + + "non-negative value") + } + + return RandomDelayGenerator(min, max, seed) + } + } + + /** + * Random value generator used to generate the delays + */ + private var random = Random(seed) + + override fun nextDelay(): Time = random.nextInt(max - min + 1) + min + + override fun reset() = random.setSeed(seed) + + /** + * Generates a new seed for the delay generator and sets the new seed as the generator's seed. + */ + override fun generateNewSeed() { + seed = random.nextInt().toLong() + random = Random(seed) + } +} \ No newline at end of file diff --git a/src/main/kotlin/core/simulator/Scheduler.kt b/src/main/kotlin/core/simulator/Scheduler.kt new file mode 100644 index 0000000..c0c5116 --- /dev/null +++ b/src/main/kotlin/core/simulator/Scheduler.kt @@ -0,0 +1,77 @@ +package core.simulator + +import java.util.* + +/** + * Created on 22-07-2017 + * + * @author David Fialho + */ +object Scheduler { + + /** + * A scheduled event associates a timestamp with an event. The timestamp is used by the scheduler to determine + * the order in which the event occur. + */ + private class ScheduledEvent(val time: Time, val event: Event) : Comparable { + override operator fun compareTo(other: ScheduledEvent): Int = time.compareTo(other.time) + } + + private val events = PriorityQueue() + + /** + * Always indicates the current time. It updates every time a new event is taken from the scheduler. + */ + var time: Time = 0 + private set + + /** + * Schedules an event to occur in the specified timestamp. + * + * @throws IllegalArgumentException if the specified timestamp is before the current time of the scheduler. + */ + @Throws(IllegalArgumentException::class) + fun schedule(event: Event, timestamp: Time) { + + if (timestamp < time) { + throw IllegalArgumentException("Scheduling time '$timestamp' is lower than the current time '$time'") + } + + events.add(ScheduledEvent(timestamp, event)) + } + + /** + * Schedules an event to occur 'interval' units of time from the current time. + */ + fun scheduleFromNow(event: Event, interval: Time) { + schedule(event, time + interval) + } + + /** + * Returns true if the scheduler still has events in the queue or false if otherwise. + */ + fun hasEvents(): Boolean = !events.isEmpty() + + /** + * Returns the next event in the queue. It ay update the current time. + * + * @throws NoSuchElementException if the scheduler has no more events in the queue. + */ + @Throws(NoSuchElementException::class) + fun nextEvent(): Event { + + val scheduledEvent = events.poll() ?: throw NoSuchElementException("Scheduler has no more events in the queue") + + time = scheduledEvent.time + return scheduledEvent.event + } + + /** + * Resets the scheduler to the initial state: with no events in the queue and time set to 0. + */ + fun reset() { + events.clear() + time = 0 + } + +} \ No newline at end of file diff --git a/src/main/kotlin/core/simulator/Time.kt b/src/main/kotlin/core/simulator/Time.kt new file mode 100644 index 0000000..c25c326 --- /dev/null +++ b/src/main/kotlin/core/simulator/Time.kt @@ -0,0 +1,8 @@ +package core.simulator + +/** + * Created on 22-07-2017 + * + * @author David Fialho + */ +typealias Time = Int \ No newline at end of file diff --git a/src/main/kotlin/core/simulator/Timer.kt b/src/main/kotlin/core/simulator/Timer.kt new file mode 100644 index 0000000..5724281 --- /dev/null +++ b/src/main/kotlin/core/simulator/Timer.kt @@ -0,0 +1,120 @@ +package core.simulator + +/** + * Created on 22-07-2017 + * + * @author David Fialho + * + * A time starts in expired mode + * The timer performs the specified action action whe it expires. + */ +interface Timer { + + // Flag indicating if the time has expired or not + val expired: Boolean + + /** + * Starts the timer if the timer has not expired. The timer is set to 'not expired' after calling start(). + * + * @throws IllegalStateException if the timer has not expired yet + */ + @Throws(IllegalStateException::class) + fun start() + + /** + * Cancels the timer if the timer as not expired yet. + */ + fun cancel() + + /** + * Should be called when the timer expires. + */ + fun onExpired() + + companion object Factory { + + /** + * Returns an enabled timer with the specified duration and action. + */ + fun enabled(duration: Time, action: () -> Unit): Timer = EnabledTimer(duration, action) + + /** + * Returns a disabled timer. + */ + fun disabled(): Timer = DisabledTimer + + } + + /** + * Timer implementation that represented an enabled timer. That is, it is a timer that actually works. Sew the + * DisabledTimer below to understand what it means to say a timer is enabled/disabled. + */ + private class EnabledTimer(val duration: Time, private val action: () -> Unit) : Timer { + + // At first the timer is set as expired to indicate that is available to be started + override var expired = true + private set + + // Flags used to indicate if a timer was canceled + private var canceled = false + + /** + * Starts the timer. Started timer will expire 'duration' units of time from now. + */ + @Throws(IllegalStateException::class) + override fun start() { + + if (!expired) throw IllegalStateException("Can not start an expired timer") + + Engine.scheduler.scheduleFromNow(TimerExpiredEvent(this), duration) + expired = false + } + + /** + * Avoids the action of the time being performed when the timer expires. If the timer has already expired + * then it does nothing. + */ + override fun cancel() { + if (!expired) canceled = true + } + + /** + * Should be called when the timer expires. It calls the action and sets the timer as 'expired'. + */ + override fun onExpired() { + + expired = true + if (!canceled) action() + canceled = false + } + + } + + /** + * A disabled timer is a timer that does not work. That is, calling start does not start any timer. + * Providing this timer implementation to an object is the same thing as saying that the timer used by that + * object is disabled. + */ + private object DisabledTimer : Timer { + + // A disabled timer is never expired + override val expired: Boolean = true + + /** + * Does not start anything. + */ + override fun start() = Unit + + /** + * Does nothing because a disabled timer itself does nothing. + */ + override fun cancel() = Unit + + /** + * Does nothing, because this should never be called. + */ + override fun onExpired() = Unit + + } + +} \ No newline at end of file diff --git a/src/main/kotlin/core/simulator/TimerExpiredEvent.kt b/src/main/kotlin/core/simulator/TimerExpiredEvent.kt new file mode 100644 index 0000000..3ba5d78 --- /dev/null +++ b/src/main/kotlin/core/simulator/TimerExpiredEvent.kt @@ -0,0 +1,17 @@ +package core.simulator + +/** + * Created on 22-07-2017 + * + * @author David Fialho + */ +class TimerExpiredEvent(private val timer: Timer) : Event { + + /** + * Calls the onExpired() method of the timer that expired. + */ + override fun processIt() { + timer.onExpired() + } + +} \ No newline at end of file diff --git a/src/main/kotlin/core/simulator/ZeroDelayGenerator.kt b/src/main/kotlin/core/simulator/ZeroDelayGenerator.kt new file mode 100644 index 0000000..339eb1d --- /dev/null +++ b/src/main/kotlin/core/simulator/ZeroDelayGenerator.kt @@ -0,0 +1,20 @@ +package core.simulator + +/** + * Created on 22-07-2017 + * + * @author David Fialho + * + * Generates constant delay values of zero! The method nextDelay() always returns 0. + */ +object ZeroDelayGenerator : DelayGenerator { + + override val seed = 0L + + override fun nextDelay(): Time = 0 + + override fun reset() = Unit + + override fun generateNewSeed() = Unit + +} \ No newline at end of file diff --git a/src/main/kotlin/core/simulator/notifications/BasicNotifier.kt b/src/main/kotlin/core/simulator/notifications/BasicNotifier.kt new file mode 100644 index 0000000..6fa0581 --- /dev/null +++ b/src/main/kotlin/core/simulator/notifications/BasicNotifier.kt @@ -0,0 +1,175 @@ +package core.simulator.notifications + +/** + * Created on 25-07-2017. + * + * @author David Fialho + */ +object BasicNotifier: Notifier { + + //region Lists containing the registered listeners + + private val startListeners = mutableListOf() + private val endListeners = mutableListOf() + private val thresholdReachedListeners = mutableListOf() + private val messageSentListeners = mutableListOf() + private val messageReceivedListeners = mutableListOf() + + //endregion + + //region Start notification + + /** + * Registers a new start listener. + * + * @param listener start listener to register. + */ + fun addStartListener(listener: StartListener) { + startListeners.add(listener) + } + + /** + * Unregisters a new start listener. + * + * @param listener start listener to unregister. + */ + fun removeStartListener(listener: StartListener) { + startListeners.remove(listener) + } + + /** + * Sends a start notification to each start listener. + * + * @param notification the start notification to send to each registered listener. + */ + fun notifyStart(notification: StartNotification) { + startListeners.forEach { it.notify(notification) } + } + + //endregion + + //region End notification + + /** + * Registers a new end listener. + * + * @param listener end listener to register. + */ + fun addEndListener(listener: EndListener) { + endListeners.add(listener) + } + + /** + * Unregisters a new end listener. + * + * @param listener end listener to unregister. + */ + fun removeEndListener(listener: EndListener) { + endListeners.remove(listener) + } + + /** + * Sends a end notification to each end listener. + * + * @param notification the end notification to send to each registered listener. + */ + fun notifyEnd(notification: EndNotification) { + endListeners.forEach { it.notify(notification) } + } + + //endregion + + //region Threshold reached notification + + /** + * Registers a new threshold reached listener. + * + * @param listener threshold reached listener to register. + */ + fun addThresholdReachedListener(listener: ThresholdReachedListener) { + thresholdReachedListeners.add(listener) + } + + /** + * Unregisters a new threshold reached listener. + * + * @param listener threshold reached listener to unregister. + */ + fun removeThresholdReachedListener(listener: ThresholdReachedListener) { + thresholdReachedListeners.remove(listener) + } + + /** + * Sends a threshold reached notification to each threshold reached listener. + * + * @param notification the threshold reached notification to send to each registered listener. + */ + fun notifyThresholdReached(notification: ThresholdReachedNotification) { + thresholdReachedListeners.forEach { it.notify(notification) } + } + + //endregion + + //region Message sent notification + + /** + * Registers a new message sent listener. + * + * @param listener message sent listener to register. + */ + fun addMessageSentListener(listener: MessageSentListener) { + messageSentListeners.add(listener) + } + + /** + * Unregisters a new message sent listener. + * + * @param listener message sent listener to unregister. + */ + fun removeMessageSentListener(listener: MessageSentListener) { + messageSentListeners.remove(listener) + } + + /** + * Sends a message sent notification to each message sent listener. + * + * @param notification the message sent notification to send to each registered listener. + */ + fun notifyMessageSent(notification: MessageSentNotification) { + messageSentListeners.forEach { it.notify(notification) } + } + + //endregion + + //region Message received notification + + /** + * Registers a new message received listener. + * + * @param listener message received listener to register. + */ + fun addMessageReceivedListener(listener: MessageReceivedListener) { + messageReceivedListeners.add(listener) + } + + /** + * Unregisters a new message received listener. + * + * @param listener message received listener to unregister. + */ + fun removeMessageReceivedListener(listener: MessageReceivedListener) { + messageReceivedListeners.remove(listener) + } + + /** + * Sends a message received notification to each message received listener. + * + * @param notification the message received notification to send to each registered listener. + */ + fun notifyMessageReceived(notification: MessageReceivedNotification) { + messageReceivedListeners.forEach { it.notify(notification) } + } + + //endregion + +} \ No newline at end of file diff --git a/src/main/kotlin/core/simulator/notifications/EndListener.kt b/src/main/kotlin/core/simulator/notifications/EndListener.kt new file mode 100644 index 0000000..65dec69 --- /dev/null +++ b/src/main/kotlin/core/simulator/notifications/EndListener.kt @@ -0,0 +1,16 @@ +package core.simulator.notifications + +/** + * Created on 25-07-2017. + * + * @author David Fialho + * + * An EndListener listens for EndNotifications. + */ +interface EndListener { + + /** + * Invoked to notify the listener of a new end notification. + */ + fun notify(notification: EndNotification) +} \ No newline at end of file diff --git a/src/main/kotlin/core/simulator/notifications/EndNotification.kt b/src/main/kotlin/core/simulator/notifications/EndNotification.kt new file mode 100644 index 0000000..ff479ab --- /dev/null +++ b/src/main/kotlin/core/simulator/notifications/EndNotification.kt @@ -0,0 +1,9 @@ +package core.simulator.notifications + + +/** + * Created on 25-07-2017. + * + * @author David Fialho + */ +class EndNotification : Notification() \ No newline at end of file diff --git a/src/main/kotlin/core/simulator/notifications/MessageReceivedListener.kt b/src/main/kotlin/core/simulator/notifications/MessageReceivedListener.kt new file mode 100644 index 0000000..8bd97d7 --- /dev/null +++ b/src/main/kotlin/core/simulator/notifications/MessageReceivedListener.kt @@ -0,0 +1,15 @@ +package core.simulator.notifications + +/** + * Created on 29-08-2017 + * + * @author David Fialho + */ +interface MessageReceivedListener: NotificationListener { + + /** + * Invoked to notify the listener of a new message received notification. + */ + fun notify(notification: MessageReceivedNotification) + +} \ No newline at end of file diff --git a/src/main/kotlin/core/simulator/notifications/MessageReceivedNotification.kt b/src/main/kotlin/core/simulator/notifications/MessageReceivedNotification.kt new file mode 100644 index 0000000..d2239a1 --- /dev/null +++ b/src/main/kotlin/core/simulator/notifications/MessageReceivedNotification.kt @@ -0,0 +1,14 @@ +package core.simulator.notifications + +import core.routing.Message + +/** + * Created on 29-08-2017 + * + * @author David Fialho + * + * Notification sent when a node receives a message. + * + * @property message the message received + */ +data class MessageReceivedNotification(val message: Message<*>) : Notification() \ No newline at end of file diff --git a/src/main/kotlin/core/simulator/notifications/MessageSentListener.kt b/src/main/kotlin/core/simulator/notifications/MessageSentListener.kt new file mode 100644 index 0000000..5ba6aff --- /dev/null +++ b/src/main/kotlin/core/simulator/notifications/MessageSentListener.kt @@ -0,0 +1,15 @@ +package core.simulator.notifications + +/** + * Created on 29-08-2017 + * + * @author David Fialho + */ +interface MessageSentListener: NotificationListener { + + /** + * Invoked to notify the listener of a new message sent notification. + */ + fun notify(notification: MessageSentNotification) + +} \ No newline at end of file diff --git a/src/main/kotlin/core/simulator/notifications/MessageSentNotification.kt b/src/main/kotlin/core/simulator/notifications/MessageSentNotification.kt new file mode 100644 index 0000000..35c105a --- /dev/null +++ b/src/main/kotlin/core/simulator/notifications/MessageSentNotification.kt @@ -0,0 +1,14 @@ +package core.simulator.notifications + +import core.routing.Message + +/** + * Created on 29-08-2017 + * + * @author David Fialho + * + * Notification sent when a node sends a message. + * + * @property message the message sent + */ +data class MessageSentNotification(val message: Message<*>) : Notification() \ No newline at end of file diff --git a/src/main/kotlin/core/simulator/notifications/Notification.kt b/src/main/kotlin/core/simulator/notifications/Notification.kt new file mode 100644 index 0000000..ac802ec --- /dev/null +++ b/src/main/kotlin/core/simulator/notifications/Notification.kt @@ -0,0 +1,13 @@ +package core.simulator.notifications + +import core.simulator.Time +import core.simulator.currentTime + +/** + * Created on 25-07-2017. + * + * @author David Fialho + * + * @property time the time at which the notification was sent. + */ +abstract class Notification(val time: Time = currentTime()) \ No newline at end of file diff --git a/src/main/kotlin/core/simulator/notifications/NotificationListener.kt b/src/main/kotlin/core/simulator/notifications/NotificationListener.kt new file mode 100644 index 0000000..ff0da99 --- /dev/null +++ b/src/main/kotlin/core/simulator/notifications/NotificationListener.kt @@ -0,0 +1,8 @@ +package core.simulator.notifications + +/** + * Created on 25-07-2017. + * + * @author David Fialho + */ +interface NotificationListener \ No newline at end of file diff --git a/src/main/kotlin/core/simulator/notifications/Notifier.kt b/src/main/kotlin/core/simulator/notifications/Notifier.kt new file mode 100644 index 0000000..2683fd8 --- /dev/null +++ b/src/main/kotlin/core/simulator/notifications/Notifier.kt @@ -0,0 +1,10 @@ +package core.simulator.notifications + +/** + * Created on 26-07-2017 + * + * @author David Fialho + * + * Tag interface for all notifiers. + */ +interface Notifier \ No newline at end of file diff --git a/src/main/kotlin/core/simulator/notifications/StartListener.kt b/src/main/kotlin/core/simulator/notifications/StartListener.kt new file mode 100644 index 0000000..8d78db8 --- /dev/null +++ b/src/main/kotlin/core/simulator/notifications/StartListener.kt @@ -0,0 +1,17 @@ +package core.simulator.notifications + +/** + * Created on 25-07-2017. + * + * @author David Fialho + * + * An StartListener listens for StartNotifications. + */ +interface StartListener : NotificationListener { + + /** + * Invoked to notify the listener of a new start notification. + */ + fun notify(notification: StartNotification) + +} \ No newline at end of file diff --git a/src/main/kotlin/core/simulator/notifications/StartNotification.kt b/src/main/kotlin/core/simulator/notifications/StartNotification.kt new file mode 100644 index 0000000..6fda864 --- /dev/null +++ b/src/main/kotlin/core/simulator/notifications/StartNotification.kt @@ -0,0 +1,15 @@ +package core.simulator.notifications + +import core.routing.Topology + +/** + * Created on 25-07-2017. + * + * @author David Fialho + * + * Notification sent before the simulation starts. + * + * @property seed the initial seed used to generate the communication delays + * @property topology the topology used fot the simulation that is about to start + */ +data class StartNotification(val seed: Long, val topology: Topology<*>) : Notification() \ No newline at end of file diff --git a/src/main/kotlin/core/simulator/notifications/ThresholdReachedListener.kt b/src/main/kotlin/core/simulator/notifications/ThresholdReachedListener.kt new file mode 100644 index 0000000..2f72cc5 --- /dev/null +++ b/src/main/kotlin/core/simulator/notifications/ThresholdReachedListener.kt @@ -0,0 +1,16 @@ +package core.simulator.notifications + +/** + * Created on 25-07-2017. + * + * @author David Fialho + * + * An core.simulator.notifications.ThresholdReachedListener listens for ThresholdReachedNotifications. + */ +interface ThresholdReachedListener { + + /** + * Invoked to notify the listener of a new threshold reached notification. + */ + fun notify(notification: ThresholdReachedNotification) +} \ No newline at end of file diff --git a/src/main/kotlin/core/simulator/notifications/ThresholdReachedNotification.kt b/src/main/kotlin/core/simulator/notifications/ThresholdReachedNotification.kt new file mode 100644 index 0000000..e608f5d --- /dev/null +++ b/src/main/kotlin/core/simulator/notifications/ThresholdReachedNotification.kt @@ -0,0 +1,10 @@ +package core.simulator.notifications + +import core.simulator.Time + +/** + * Created on 25-07-2017. + * + * @author David Fialho + */ +class ThresholdReachedNotification(val threshold: Time) : Notification() \ No newline at end of file diff --git a/src/main/kotlin/io/BasicReporter.kt b/src/main/kotlin/io/BasicReporter.kt new file mode 100644 index 0000000..6e7bd69 --- /dev/null +++ b/src/main/kotlin/io/BasicReporter.kt @@ -0,0 +1,76 @@ +package io + +import simulation.BasicDataSet +import java.io.File +import java.io.FileWriter +import java.io.IOException +import java.io.Writer + + +/** + * Created on 29-08-2017 + * + * @author David Fialho + * + * Reporter used to reporter data from a basic data set. + */ +class BasicReporter(private val outputFile: File): Reporter { + + /** + * Flag to indicate if the headers were already printed. + * Helps to ensure the headers are printed only once despite the report() method being called multiple times. + */ + private var wereHeadersPrinted = false + + /** + * Stores the simulation number, Incremented each time the report() method is called. + */ + private var simulation = 1 + + /** + * Reports a data set. + * + * @throws IOException If an I/O error occurs + */ + @Throws(IOException::class) + override fun report(data: BasicDataSet) { + + CSVPrinter(outputFile).use { + + if (!wereHeadersPrinted) { + it.printRecord( + "Simulation", + "Delay Seed", + "Termination Time (Total)", + "Termination Time (Avg.)", + "Message Count", + "Detection Count", + "Terminated" + ) + + wereHeadersPrinted = true + } + + it.printRecord( + simulation, + data.delaySeed, + data.totalTerminationTime, + data.avgTerminationTime, + data.messageCount, + data.detectionCount, + if (data.terminated) "Yes" else "No" + ) + + simulation++ + } + } + + /** + * Resets the reporter to its initial state. + */ + override fun reset() { + wereHeadersPrinted = false + simulation = 1 + } + +} \ No newline at end of file diff --git a/src/main/kotlin/io/CSVPrinter.kt b/src/main/kotlin/io/CSVPrinter.kt new file mode 100644 index 0000000..1950b15 --- /dev/null +++ b/src/main/kotlin/io/CSVPrinter.kt @@ -0,0 +1,36 @@ +package io + +import org.apache.commons.csv.CSVFormat +import org.apache.commons.csv.CSVPrinter +import java.io.File +import java.io.FileWriter +import java.io.IOException +import java.io.Writer + +/** + * Created on 30-08-2017 + * + * @author David Fialho + * + * This file contains helper methods to work with a CSV printer + */ + +/** + * Creates a printer that will print values to the specified writer following some format. + * + * @throws IOException if the optional header cannot be printed. + */ +@Throws(IOException::class) +fun CSVPrinter(writer: Writer): CSVPrinter { + return CSVPrinter(writer, CSVFormat.EXCEL.withDelimiter(';')) +} + +/** + * Creates a printer that will print values to the specified file following some format. + * + * @throws IOException if the optional header cannot be printed. + */ +@Throws(IOException::class) +fun CSVPrinter(file: File): CSVPrinter { + return CSVPrinter(FileWriter(file, true)) +} diff --git a/src/main/kotlin/io/InterdomainTopologyReader.kt b/src/main/kotlin/io/InterdomainTopologyReader.kt new file mode 100644 index 0000000..abd19f8 --- /dev/null +++ b/src/main/kotlin/io/InterdomainTopologyReader.kt @@ -0,0 +1,137 @@ +package io + +import bgp.BGP +import bgp.BGPRoute +import bgp.ISSBGP +import bgp.SSBGP +import bgp.policies.interdomain.* +import core.routing.* +import io.TopologyParser.Handler +import java.io.* + +/** + * Created on 29-08-2017 + * + * @author David Fialho + */ +class InterdomainTopologyReader(reader: Reader): TopologyReader, Closeable, Handler { + + /** + * Provides option to create a reader with a file object. + */ + @Throws(FileNotFoundException::class) + constructor(file: File): this(FileReader(file)) + + private val builder = TopologyBuilder() + private val parser = TopologyParser(reader, this) + + /** + * Returns a Topology object that is represented in the input source. + * + * The topology object uses a BGP like protocol and the extenders assigned to the links are defined in the + * interdomain routing policies. + * + * @throws IOException If an I/O error occurs + * @throws ParseException if a topology object can not be created due to incorrect representation + */ + @Throws(IOException::class, ParseException::class) + override fun read(): Topology { + parser.parse() + return builder.build() + } + + /** + * Invoked when reading the stream when a new node item is read. + * + * @param id the ID of the node parse + * @param values sequence of values associated with the node + * @param currentLine line number where the node was parsed + */ + override fun onNodeItem(id: NodeID, values: List, currentLine: Int) { + + if (values.size < 2) { + throw ParseException("Node is missing required values: Protocol and/or MRAI", currentLine) + } + + val protocolLabel = values[0] + val mrai = parseNonNegativeInteger(values[1], currentLine) + + val protocol = when (protocolLabel.toLowerCase()) { + "bgp" -> BGP(mrai) + "ssbgp" -> SSBGP(mrai) + "issbgp" -> ISSBGP(mrai) + else -> throw ParseException("Protocol label `$protocolLabel` was not recognized: supported labels are BGP, " + + "SSBGP, and ISSBGP", currentLine) + } + + try { + builder.addNode(id, protocol) + + } catch (e: ElementExistsException) { + throw ParseException(e.message!!, currentLine) + } + } + + /** + * Invoked when reading the stream when a new link item is read. + * + * @param tail the ID of the tail node + * @param head the ID of the head node + * @param values sequence of values associated with the link item + * @param currentLine line number where the node was parsed + */ + override fun onLinkItem(tail: NodeID, head: NodeID, values: List, currentLine: Int) { + + if (values.isEmpty()) { + throw ParseException("Link is missing required values: extender label", currentLine) + } + + val extender = parseExtender(values[0], currentLine) + + try { + builder.link(tail, head, extender) + + } catch (e: ElementNotFoundException) { + throw ParseException(e.message!!, currentLine) + } catch (e: ElementExistsException) { + throw ParseException(e.message!!, currentLine) + } + } + + @Throws(ParseException::class) + private fun parseNonNegativeInteger(value: String, currentLine: Int): Int { + + try { + val intValue = value.toInt() + if (intValue < 0) { + throw NumberFormatException() + } + + return intValue + + } catch (e: NumberFormatException) { + throw ParseException("Failed to parse value `$value`: must be a non-negative integer value", currentLine) + } + } + + @Throws(ParseException::class) + private fun parseExtender(label: String, currentLine: Int): Extender { + + return when (label) { + "r+" -> PeerplusExtender + "c" -> CustomerExtender + "r" -> PeerExtender + "p" -> ProviderExtender + "s" -> SiblingExtender + else -> throw ParseException("Extender label `$label` was not recognized: " + + "must be either R+, C, R, P, or S", currentLine) + } + } + + /** + * Closes the stream and releases any system resources associated with it. + */ + override fun close() { + parser.close() + } +} \ No newline at end of file diff --git a/src/main/kotlin/io/ParseException.kt b/src/main/kotlin/io/ParseException.kt new file mode 100644 index 0000000..6963dae --- /dev/null +++ b/src/main/kotlin/io/ParseException.kt @@ -0,0 +1,15 @@ +package io + +/** + * Created on 27-07-2017 + * + * @author David Fialho + * + * Generic parse exception thrown when a parse error occurs while parsing a formatted file. + */ +open class ParseException(message: String, val lineNumber: Int = 0) : Exception(message) { + + override val message: String? + get() = "${super.message} (in line $lineNumber)" + +} diff --git a/src/main/kotlin/io/Reporter.kt b/src/main/kotlin/io/Reporter.kt new file mode 100644 index 0000000..c68215e --- /dev/null +++ b/src/main/kotlin/io/Reporter.kt @@ -0,0 +1,22 @@ +package io + +import simulation.DataSet + +/** + * Created on 29-08-2017 + * + * @author David Fialho + */ +interface Reporter { + + /** + * Reports a data set. + */ + fun report(data: S) + + /** + * Resets the reporter to its initial state. + */ + fun reset() + +} \ No newline at end of file diff --git a/src/main/kotlin/io/TopologyParser.kt b/src/main/kotlin/io/TopologyParser.kt new file mode 100644 index 0000000..d79b08a --- /dev/null +++ b/src/main/kotlin/io/TopologyParser.kt @@ -0,0 +1,145 @@ +package io + +import core.routing.NodeID +import java.io.BufferedReader +import java.io.Closeable +import java.io.IOException +import java.io.Reader + +/** + * Created on 29-08-2017 + * + * @author David Fialho + */ +class TopologyParser(reader: Reader, private val handler: TopologyParser.Handler): Closeable { + + /** + * Handlers are notified once a new topology item (a node or a link) is parsed. + * Subclasses should implement how these items should be handled. + */ + interface Handler { + + /** + * Invoked when reading the stream when a new node item is read. + * + * @param id the ID of the node parse + * @param values sequence of values associated with the node + * @param currentLine line number where the node was parsed + */ + fun onNodeItem(id: NodeID, values: List, currentLine: Int) + + /** + * Invoked when reading the stream when a new link item is read. + * + * @param tail the ID of the tail node + * @param head the ID of the head node + * @param values sequence of values associated with the link + * @param currentLine line number where the node was parsed + */ + fun onLinkItem(tail: NodeID, head: NodeID, values: List, currentLine: Int) + + } + + /** + * The underlying reader used to read the stream. + */ + private val reader = BufferedReader(reader) + + /** + * Returns a Topology object that is represented in the input source. + * + * @throws IOException If an I/O error occurs + * @throws ParseException if a topology object can not be created due to incorrect representation + */ + @Throws(IOException::class, ParseException::class) + fun parse() { + + // Read the first line - throw error if empty + var line: String? = reader.readLine() ?: throw ParseException("Topology file is empty", lineNumber = 1) + + var currentLine = 1 + while (line != null) { + + // Do not parse blank lines + if (!line.isBlank()) { + parseLine(line, currentLine) + } + + line = reader.readLine() + currentLine++ + } + } + + private fun parseLine(line: String, currentLine: Int) { + + // Each line must have a key separated from its values with an equal sign + // e.g. node = 1 + + // Split the key from the values + val keyAndValues = line.split("=") + + if (keyAndValues.size != 2) { + throw ParseException("Line $currentLine$ contains multiple equal signs(=): only one is permitted per line", + currentLine) + } + + val key = keyAndValues[0].trim().toLowerCase() + val values = keyAndValues[1].split("|").map { it.trim().toLowerCase() }.toList() + + when (key) { + "node" -> { + + // Check there is at least one value: the node ID + if (values.isEmpty() || (values.size == 1 && values[0].isEmpty())) { + throw ParseException("Line with `node` key is missing a value: must have at least an ID value", + currentLine) + } + + // Node ID is the first value + val nodeID = parseNodeID(values[0], currentLine) + + handler.onNodeItem(nodeID, values.subList(1, values.lastIndex + 1), currentLine) + } + "link" -> { + + // Check there is at least one value: the node ID + if (values.size < 2) { + throw ParseException("Line with `link` is missing a value: must have at least two ID values", + currentLine) + } + + // Node ID is the first value + val tailID = parseNodeID(values[0], currentLine) + val headID = parseNodeID(values[1], currentLine) + + handler.onLinkItem(tailID, headID, values.subList(2, values.lastIndex + 1), currentLine) + } + else -> { + throw ParseException("Invalid key `$key`: keys must be either `node` or `link`", currentLine) + } + } + } + + private fun parseNodeID(value: String, currentLine: Int): Int { + + try { + val intValue = value.toInt() + if (intValue < 0) { + throw NumberFormatException() + } + + return intValue + + } catch (e: NumberFormatException) { + throw ParseException("Failed to parse node ID from value `$value`: must be a non-negative " + + "integer value", currentLine) + } + } + + /** + * Closes the stream and releases any system resources associated with it. + */ + override fun close() { + reader.close() + } +} diff --git a/src/main/kotlin/io/TopologyReader.kt b/src/main/kotlin/io/TopologyReader.kt new file mode 100644 index 0000000..65776da --- /dev/null +++ b/src/main/kotlin/io/TopologyReader.kt @@ -0,0 +1,13 @@ +package io + +import core.routing.Topology + +/** + * Created on 29-08-2017 + * + * @author David Fialho + */ +interface TopologyReader { + + fun read(): Topology<*> +} \ No newline at end of file diff --git a/src/main/kotlin/io/TopologyReaderHandler.kt b/src/main/kotlin/io/TopologyReaderHandler.kt new file mode 100644 index 0000000..2f12d35 --- /dev/null +++ b/src/main/kotlin/io/TopologyReaderHandler.kt @@ -0,0 +1,50 @@ +package io + +import core.routing.Topology +import java.io.File +import java.io.FileReader +import java.io.IOException +import java.io.Reader + +/** + * Created on 29-08-2017 + * + * @author David Fialho + */ +sealed class TopologyReaderHandler { + + /** + * Reads the topology file associated with the handler on a new topology reader and then closes it down correctly + * whether an exception is thrown or not. + * + * @return the topology read from the file. + * @throws IOException - If an I/O error occurs + * @throws ParseException - if a topology object can not be created due to incorrect representation + */ + @Throws(IOException::class, ParseException::class) + abstract fun read(): Topology<*> + +} + +/** + * Handler for InterdomainTopologyReader. + */ +class InterdomainTopologyReaderHandler(private val reader: Reader): TopologyReaderHandler() { + + constructor(topologyFile: File): this(FileReader(topologyFile)) + + /** + * Reads the topology file associated with the handler on a new topology reader and then closes it down correctly + * whether an exception is thrown or not. + * + * @throws IOException - If an I/O error occurs + * @throws ParseException - if a topology object can not be created due to incorrect representation + */ + @Throws(IOException::class, ParseException::class) + override fun read(): Topology<*> { + + InterdomainTopologyReader(reader).use { + return it.read() + } + } +} diff --git a/src/main/kotlin/main/Main.kt b/src/main/kotlin/main/Main.kt new file mode 100644 index 0000000..b02f9b7 --- /dev/null +++ b/src/main/kotlin/main/Main.kt @@ -0,0 +1,12 @@ +package main + +import ui.cli.CLIApplication + +/** + * Created on 30-08-2017 + * + * @author David Fialho + */ +fun main(args: Array) { + CLIApplication.launch(args) +} \ No newline at end of file diff --git a/src/main/kotlin/simulation/BasicDataCollector.kt b/src/main/kotlin/simulation/BasicDataCollector.kt new file mode 100644 index 0000000..7009446 --- /dev/null +++ b/src/main/kotlin/simulation/BasicDataCollector.kt @@ -0,0 +1,142 @@ +package simulation + +import bgp.notifications.* +import core.routing.NodeID +import core.simulator.Time +import core.simulator.notifications.* +import io.BasicReporter +import java.io.File +import java.io.IOException + +/** + * Created on 29-08-2017 + * + * @author David Fialho + * + * The BasicDataCollector collects the most `basic` information from a simulation execution. + * The collected data is stored in a BasicDataSet and reported using a BasicReporter. + * + * @param reporter the reporter used to report the final data. + */ +class BasicDataCollector(private val reporter: BasicReporter): DataCollector, + StartListener, + MessageSentListener, + ExportListener, + DetectListener, + ThresholdReachedListener { + + /** + * Creates a Basic Reporter that will output results to the specified output file. + */ + constructor(outputFile: File): this(BasicReporter(outputFile)) + + /** + * Stores the final data to be reported. + */ + val data = BasicDataSet() + + /** + * Stores the number of nodes contained in the topology that was simulated. + */ + private var nodeCount = 0 + + /** + * Keeps record of the termination times of each node. The termination time of a node corresponds to the time at + * which the node exported its last route. + * + * This information will be used to compute the average termination time. + */ + private val terminationTimes = HashMap() + + /** + * Adds the collector as a listener for notifications the collector needs to listen to collect data. + */ + override fun register() { + BasicNotifier.addStartListener(this) + BasicNotifier.addMessageSentListener(this) + BGPNotifier.addExportListener(this) + BGPNotifier.addDetectListener(this) + BasicNotifier.addThresholdReachedListener(this) + } + + /** + * Removes the collector from all notifiers + */ + override fun unregister() { + BasicNotifier.removeStartListener(this) + BasicNotifier.removeMessageSentListener(this) + BGPNotifier.removeExportListener(this) + BGPNotifier.removeDetectListener(this) + BasicNotifier.removeThresholdReachedListener(this) + } + + /** + * Processes the data after all raw data has been collected. It should be called after an execution. + */ + override fun processData() { + // If a node never exports a route then it will not be included in [terminationTimes]. In that case the + // termination time of that node is 0 + + // The average termination time corresponds to the mean of the termination times of all nodes + data.avgTerminationTime = terminationTimes.values.sum().div(nodeCount.toDouble()) + } + + /** + * Reports the currently collected data. + * + * @throws IOException If an I/O error occurs + */ + @Throws(IOException::class) + override fun report() { + reporter.report(data) + } + + /** + * Clears all collected data. + */ + override fun clear() { + data.clear() + terminationTimes.clear() + } + + // region Notify methods used to collect data + + /** + * Invoked to notify the listener of a new start notification. + */ + override fun notify(notification: StartNotification) { + data.delaySeed = notification.seed + nodeCount = notification.topology.size + } + + /** + * Invoked to notify the listener of a new message sent notification. + */ + override fun notify(notification: MessageSentNotification) { + data.messageCount++ + data.totalTerminationTime = notification.time + } + + /** + * Invoked to notify the listener of a new export notification. + */ + override fun notify(notification: ExportNotification) { + terminationTimes[notification.node.id] = notification.time + } + + /** + * Invoked to notify the listener of a new detect notification. + */ + override fun notify(notification: DetectNotification) { + data.detectionCount++ + } + + /** + * Invoked to notify the listener of a new threshold reached notification. + */ + override fun notify(notification: ThresholdReachedNotification) { + data.terminated = false + } + + // endregion +} \ No newline at end of file diff --git a/src/main/kotlin/simulation/BasicDataSet.kt b/src/main/kotlin/simulation/BasicDataSet.kt new file mode 100644 index 0000000..1055176 --- /dev/null +++ b/src/main/kotlin/simulation/BasicDataSet.kt @@ -0,0 +1,40 @@ +package simulation + +import core.simulator.Time + +/** + * Created on 29-08-2017 + * + * @author David Fialho + * + * The BasicDataSet contains data collected by a basic collector that will be output by a basic reporter. + * + * @property delaySeed the seed used to generate the message delays + * @property messageCount the total number o messages sent during the simulation + * @property totalTerminationTime the time at which the last node exported its last route + * @property avgTerminationTime the average of the termination times of all nodes + * @property detectionCount the number of detections recorded during the simulation. + * @property terminated flag indicating if the simulation terminated or not + */ +data class BasicDataSet( + var delaySeed: Long = 0L, + var messageCount: Int = 0, + var totalTerminationTime: Time = 0, + var avgTerminationTime: Double = 0.0, + var detectionCount: Int = 0, + var terminated: Boolean = true + +): DataSet { + + /** + * Clears all data from the data set. + */ + override fun clear() { + delaySeed = 0L + messageCount = 0 + totalTerminationTime = 0 + avgTerminationTime = 0.0 + detectionCount = 0 + terminated = true + } +} \ No newline at end of file diff --git a/src/main/kotlin/simulation/DataCollector.kt b/src/main/kotlin/simulation/DataCollector.kt new file mode 100644 index 0000000..bccb56b --- /dev/null +++ b/src/main/kotlin/simulation/DataCollector.kt @@ -0,0 +1,56 @@ +package simulation + +/** + * Created on 29-08-2017 + * + * @author David Fialho + * + * A data collector is responsible for collecting data during an execution. + * + * Any method can be used to collect that. One of the most important methods is by using the notifications issued by + * the engine during the simulation. To do this look into the notifier classes tagged with the Notifier interfaced. + */ +interface DataCollector { + + /** + * Helper method to perform the collection of data. It handles registering and un-registering the collector with + * the necessary notifiers. + */ + fun collect(body: () -> Unit): DataCollector { + + register() + try { + body() + } finally { + unregister() + } + + return this + } + + /** + * Adds the collector as a listener for notifications the collector needs to listen to collect data. + */ + fun register() + + /** + * Removes the collector from all notifiers + */ + fun unregister() + + /** + * Processes the data after all raw data has been collected. It should be called after an execution. + */ + fun processData() + + /** + * Reports the currently collected data. + */ + fun report() + + /** + * Clears all collected data. + */ + fun clear() + +} \ No newline at end of file diff --git a/src/main/kotlin/simulation/DataCollectorGroup.kt b/src/main/kotlin/simulation/DataCollectorGroup.kt new file mode 100644 index 0000000..2618621 --- /dev/null +++ b/src/main/kotlin/simulation/DataCollectorGroup.kt @@ -0,0 +1,57 @@ +package simulation + +/** + * Created on 29-08-2017 + * + * @author David Fialho + * + * The data collector group handles multiple collectors. It provides the same interface as a data collector + * implementation. Therefore, it can be used as any other data collector. + */ +class DataCollectorGroup: DataCollector { + + private val collectors = mutableListOf() + + /** + * Adds a new collector to the group. + */ + fun add(collector: DataCollector) { + collectors.add(collector) + } + + /** + * Registers all collectors in the group. + */ + override fun register() { + collectors.forEach { it.register() } + } + + /** + * Unregisters all collectors in the group. + */ + override fun unregister() { + collectors.forEach { it.unregister() } + } + + /** + * Processes the data after all raw data has been collected. It should be called after an execution. + */ + override fun processData() { + collectors.forEach { it.processData() } + } + + /** + * Reports collected data from all collectors in the group. + */ + override fun report() { + collectors.forEach { it.report() } + } + + /** + * Clears all data from all collectors in the group. + */ + override fun clear() { + collectors.forEach { it.clear() } + } + +} \ No newline at end of file diff --git a/src/main/kotlin/simulation/DataSet.kt b/src/main/kotlin/simulation/DataSet.kt new file mode 100644 index 0000000..2bd7cf5 --- /dev/null +++ b/src/main/kotlin/simulation/DataSet.kt @@ -0,0 +1,14 @@ +package simulation + +/** + * Created on 29-08-2017 + * + * @author David Fialho + */ +interface DataSet { + + /** + * Clears all data from the data set. + */ + fun clear() +} \ No newline at end of file diff --git a/src/main/kotlin/simulation/Execution.kt b/src/main/kotlin/simulation/Execution.kt new file mode 100644 index 0000000..994fde4 --- /dev/null +++ b/src/main/kotlin/simulation/Execution.kt @@ -0,0 +1,18 @@ +package simulation + +import core.routing.Node +import core.routing.Topology + +/** + * Created on 29-08-2017 + * + * @author David Fialho + */ +interface Execution { + + /** + * Performs a single simulation execution with the specified topology and destination. + */ + fun execute(topology: Topology<*>, destination: Node<*>) + +} \ No newline at end of file diff --git a/src/main/kotlin/simulation/RepetitionRunner.kt b/src/main/kotlin/simulation/RepetitionRunner.kt new file mode 100644 index 0000000..adedb16 --- /dev/null +++ b/src/main/kotlin/simulation/RepetitionRunner.kt @@ -0,0 +1,67 @@ +package simulation + +import core.routing.Node +import core.routing.NodeID +import core.routing.Topology +import core.simulator.DelayGenerator +import core.simulator.Engine +import io.TopologyReaderHandler +import ui.Application +import java.io.File + +/** + * Created on 29-08-2017 + * + * @author David Fialho + */ +class RepetitionRunner( + private val topologyFile: File, + private val topologyReader: TopologyReaderHandler, + private val destination: NodeID, + private val repetitions: Int, + private val messageDelayGenerator: DelayGenerator + +): Runner { + + /** + * Runs the specified execution the number of times specified in the [repetitions] property. + * + * The engine configurations may be modified during the run. At the end of this method the engine is always + * reverted to its defaults. + * + * @param execution the execution that will be executed in each run + * @param application the application running that wants to monitor progress and handle errors + */ + override fun run(execution: Execution, application: Application) { + + val topology: Topology<*> = application.loadTopology(topologyFile, topologyReader) { + topologyReader.read() + } + + val destination: Node<*> = application.findDestination(destination) { + topology[destination] + } + + Engine.messageDelayGenerator = messageDelayGenerator + + application.run { + + try { + repeat(times = repetitions) { repetition -> + + application.execute(repetition + 1, destination, messageDelayGenerator.seed) { + execution.execute(topology, destination) + } + + // Cleanup for next execution + topology.reset() + Engine.messageDelayGenerator.generateNewSeed() + } + + } finally { + // Make sure that the engine is always reverted to the defaults after running + Engine.resetToDefaults() + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/simulation/Runner.kt b/src/main/kotlin/simulation/Runner.kt new file mode 100644 index 0000000..657d911 --- /dev/null +++ b/src/main/kotlin/simulation/Runner.kt @@ -0,0 +1,17 @@ +package simulation + +import ui.Application + +/** + * Created on 29-08-2017 + * + * @author David Fialho + */ +interface Runner { + + /** + * Runs the specified execution. + */ + fun run(execution: Execution, application: Application) + +} \ No newline at end of file diff --git a/src/main/kotlin/simulation/SimpleAdvertisementExecution.kt b/src/main/kotlin/simulation/SimpleAdvertisementExecution.kt new file mode 100644 index 0000000..d67f6ea --- /dev/null +++ b/src/main/kotlin/simulation/SimpleAdvertisementExecution.kt @@ -0,0 +1,39 @@ +package simulation + +import core.routing.Node +import core.routing.Topology +import core.simulator.Engine +import core.simulator.Time +import java.io.IOException + +/** + * Created on 29-08-2017 + * + * @author David Fialho + */ +class SimpleAdvertisementExecution(val threshold: Time): Execution { + + val dataCollectors = DataCollectorGroup() + + /** + * Executes a simulation, collects data from it, and reports the results. + * + * To collect data, before calling this method the data collectors to be used must be specified, by adding each + * collector to the data collector group of this execution. + * + * @throws IOException If an I/O error occurs + */ + @Throws(IOException::class) + override fun execute(topology: Topology<*>, destination: Node<*>) { + + dataCollectors.clear() + + val data = dataCollectors.collect { + Engine.simulate(topology, destination, threshold) + } + + data.processData() + data.report() + } + +} \ No newline at end of file diff --git a/src/main/kotlin/ui/Application.kt b/src/main/kotlin/ui/Application.kt new file mode 100644 index 0000000..5eaa072 --- /dev/null +++ b/src/main/kotlin/ui/Application.kt @@ -0,0 +1,45 @@ +package ui + +import core.routing.Node +import core.routing.NodeID +import core.routing.Topology +import io.TopologyReaderHandler +import java.io.File + +/** + * Created on 30-08-2017 + * + * @author David Fialho + */ +interface Application { + + fun launch(args: Array) + + /** + * Invoked while loading the topology. + * + * @param topologyFile the file from which the topology will be loaded + * @param topologyReader the reader used to load the topology into memory + * @param loadBlock the code block to load the topology. + */ + fun loadTopology(topologyFile: File, topologyReader: TopologyReaderHandler, + loadBlock: () -> Topology<*>): Topology<*> + + fun findDestination(destinationID: NodeID, block: () -> Node<*>?): Node<*> + + /** + * Invoked while executing each execution. + * + * @param executionID the identifier of the execution + * @param destination the destination used in the execution + * @param seed the seed of the message delay generator used for the execution + * @param executeBlock the code block that performs one execution + */ + fun execute(executionID: Int, destination: Node<*>, seed: Long, executeBlock: () -> Unit) + + /** + * Invoked during a run. + */ + fun run(runBlock: () -> Unit) + +} \ No newline at end of file diff --git a/src/main/kotlin/ui/DummyApplication.kt b/src/main/kotlin/ui/DummyApplication.kt new file mode 100644 index 0000000..cb37eba --- /dev/null +++ b/src/main/kotlin/ui/DummyApplication.kt @@ -0,0 +1,27 @@ +package ui + +import core.routing.Node +import core.routing.NodeID +import core.routing.Topology +import io.TopologyReaderHandler +import java.io.File + +/** + * Created on 30-08-2017 + * + * @author David Fialho + */ +object DummyApplication: Application { + + override fun launch(args: Array) = Unit + + override fun loadTopology(topologyFile: File, topologyReader: TopologyReaderHandler, + loadBlock: () -> Topology<*>): Topology<*> = loadBlock() + + override fun findDestination(destinationID: NodeID, block: () -> Node<*>?): Node<*> = block()!! + + override fun execute(executionID: Int, destination: Node<*>, seed: Long, executeBlock: () -> Unit) = Unit + + override fun run(runBlock: () -> Unit) = Unit + +} diff --git a/src/main/kotlin/ui/cli/CLIApplication.kt b/src/main/kotlin/ui/cli/CLIApplication.kt new file mode 100644 index 0000000..baf0070 --- /dev/null +++ b/src/main/kotlin/ui/cli/CLIApplication.kt @@ -0,0 +1,135 @@ +package ui.cli + +import core.routing.Node +import core.routing.NodeID +import core.routing.Topology +import io.ParseException +import io.TopologyReaderHandler +import ui.Application +import java.io.File +import java.io.IOException +import java.time.Duration +import java.time.Instant +import kotlin.system.exitProcess + +/** + * Created on 30-08-2017 + * + * @author David Fialho + */ +object CLIApplication: Application { + + private val console = Console() + + override fun launch(args: Array) { + + try { + val (runner, execution) = InputArgumentsParser().parse(args) + runner.run(execution, this) + + } catch (e: InputArgumentsException) { + console.error("Input arguments are invalid.\n${e.message}.") + console.error("Cause: ${e.message ?: "No information available"}") + + } catch (e: Exception){ + console.error("Program was interrupted due to unexpected error.") + console.error("Cause: ${e.message ?: "No information available"}") + } + } + + /** + * Invoked while loading the topology. + * + * @param topologyFile the file from which the topology will be loaded + * @param topologyReader the reader used to load the topology into memory + * @param loadBlock the code block to load the topology. + */ + override fun loadTopology(topologyFile: File, topologyReader: TopologyReaderHandler, + loadBlock: () -> Topology<*>): Topology<*> { + + try { + console.info("Topology file: ${topologyFile.path}.") + console.info("Loading topology... ", inline = true) + + val (duration, topology) = timer { + loadBlock() + } + + console.print("loaded in $duration seconds") + return topology + + } catch (exception: ParseException) { + console.error("Failed to load topology due to parse error.") + console.error("Cause: ${exception.message ?: "No information available"}") + exitProcess(1) + + } catch (exception: IOException) { + console.error("Failed to load topology due to IO error.") + console.error("Cause: ${exception.message ?: "No information available"}") + exitProcess(2) + } + + } + + /** + * Invoked when trying to find the destination node based on the ID. + * + * @param destinationID the destination ID + * @param block the block of code to find the destination + */ + override fun findDestination(destinationID: NodeID, block: () -> Node<*>?): Node<*> { + val destination: Node<*>? = block() + + if (destination == null) { + console.error("Destination `$destinationID` was not found.") + exitProcess(3) + } + + return destination + } + + /** + * Invoked while executing each execution. + * + * @param executionID the identifier of the execution + * @param destination the destination used in the execution + * @param seed the seed of the message delay generator used for the execution + * @param executeBlock the code block that performs one execution + */ + override fun execute(executionID: Int, destination: Node<*>, seed: Long, executeBlock: () -> Unit) { + + console.info("Executing $executionID (destination=${destination.id} and seed=$seed)... ", inline = true) + val (duration, _) = timer { + executeBlock() + } + console.print("finished in $duration seconds") + } + + /** + * Invoked during a run. + */ + override fun run(runBlock: () -> Unit) { + + try { + console.info("Running...") + runBlock() + console.info("Finished run") + + } catch (exception: IOException) { + console.error("Failed to report results due to an IO error.") + console.error("Cause: ${exception.message ?: "No information available"}") + exitProcess(4) + } + + } + +} + +private fun timer(block: () -> R): Pair { + + val start = Instant.now() + val value = block() + val end = Instant.now() + + return Pair(Duration.between(start, end).toMillis().div(1000.0), value) +} \ No newline at end of file diff --git a/src/main/kotlin/ui/cli/Console.kt b/src/main/kotlin/ui/cli/Console.kt new file mode 100644 index 0000000..7dca42b --- /dev/null +++ b/src/main/kotlin/ui/cli/Console.kt @@ -0,0 +1,31 @@ +package ui.cli + +/** + * Created on 30-08-2017 + * + * @author David Fialho + */ +class Console { + + fun info(message: String, inline: Boolean = false) { + print("INFO", message, inline) + } + + fun warning(message: String, inline: Boolean = false) { + print("WARNING", message, inline) + } + + fun error(message: String, inline: Boolean = false) { + print("ERROR", message, inline) + } + + fun print(message: String) { + println(message) + } + + private fun print(level: String, message: String, inline: Boolean) { + + val formattedMessage = String.format("%s: %s", level, message) + if (inline) kotlin.io.print(formattedMessage) else println(formattedMessage) + } +} \ No newline at end of file diff --git a/src/main/kotlin/ui/cli/InputArgumentsException.kt b/src/main/kotlin/ui/cli/InputArgumentsException.kt new file mode 100644 index 0000000..49da89b --- /dev/null +++ b/src/main/kotlin/ui/cli/InputArgumentsException.kt @@ -0,0 +1,8 @@ +package ui.cli + +/** + * Created on 30-08-2017 + * + * @author David Fialho + */ +class InputArgumentsException(message: String) : Exception(message) diff --git a/src/main/kotlin/ui/cli/InputArgumentsParser.kt b/src/main/kotlin/ui/cli/InputArgumentsParser.kt new file mode 100644 index 0000000..3fc6260 --- /dev/null +++ b/src/main/kotlin/ui/cli/InputArgumentsParser.kt @@ -0,0 +1,176 @@ +package ui.cli + +import core.simulator.RandomDelayGenerator +import io.InterdomainTopologyReaderHandler +import org.apache.commons.cli.CommandLine +import org.apache.commons.cli.DefaultParser +import org.apache.commons.cli.Options +import simulation.* +import java.io.File + +/** + * Created on 30-08-2017 + * + * @author David Fialho + */ +class InputArgumentsParser { + + //region Options + + private val TOPOLOGY_FILE = "topology" + private val DESTINATION = "destination" + private val REPETITIONS = "repetitions" + private val REPORT_DIRECTORY = "output" + private val MIN_DELAY = "mindelay" + private val MAX_DELAY = "maxdelay" + private val THRESHOLD = "threshold" + private val SEED = "seed" + + private val options = Options() + + init { + // setup the command options + options.addOption("t", TOPOLOGY_FILE, true, "path to topology file") + options.addOption("d", DESTINATION, true, "ID of the destination") + options.addOption("c", REPETITIONS, true, "number of repetitions") + options.addOption("o", REPORT_DIRECTORY, true, "directory where to output results") + options.addOption(MIN_DELAY, true, "minimum message delay (inclusive)") + options.addOption(MAX_DELAY, true, "maximum message delay (inclusive)") + options.addOption("th", THRESHOLD, true, "threshold value") + options.addOption(SEED, true, "first seed used for generate message delays") + } + + @Throws(InputArgumentsException::class) + fun parse(args: Array): Pair { + + DefaultParser().parse(options, args).let { + + val topologyFile = getFile(it, option = TOPOLOGY_FILE) + val destination = getNonNegativeInteger(it, option = DESTINATION) + val repetitions = getPositiveInteger(it, option = REPETITIONS, default = 1) + val reportDirectory = getDirectory(it, option = REPORT_DIRECTORY, default = File(System.getProperty("user.dir"))) + val threshold = getPositiveInteger(it, option = THRESHOLD, default = 1_000_000) + val seed = getLong(it, option = SEED, default = System.currentTimeMillis()) + + val minDelay = getPositiveInteger(it, option = MIN_DELAY, default = 1) + val maxDelay = getPositiveInteger(it, option = MAX_DELAY, default = 1) + + if (maxDelay < minDelay) { + throw InputArgumentsException("Maximum delay must equal to or greater than the minimum delay: " + + "min=$minDelay > max=$maxDelay") + } + + // If the topology filename is `topology.nf` and the destination is 10 the report filename + // is `topology_10.basic.csv` + val reportFile = File(reportDirectory, topologyFile.nameWithoutExtension.plus("_$destination.basic.csv")) + val topologyReader = InterdomainTopologyReaderHandler(topologyFile) + val messageDelayGenerator = RandomDelayGenerator.with(minDelay, maxDelay, seed) + + val runner = RepetitionRunner(topologyFile, topologyReader, destination, repetitions, messageDelayGenerator) + val execution = SimpleAdvertisementExecution(threshold).apply { + dataCollectors.add(BasicDataCollector(reportFile)) + } + + return Pair(runner, execution) + } + } + + @Throws(InputArgumentsException::class) + private fun getFile(commandLine: CommandLine, option: String, default: File? = null): File { + verifyOption(commandLine, option, default) + + val value = commandLine.getOptionValue(option) + val file = if (value != null) File(value) else default!! // See note below + + // Note: the verifyOption method would throw exception if the option was ot defined and default was null + + if (!file.isFile) { + throw InputArgumentsException("The file specified for `$option` does not exist: ${file.path}") + } + + return file + } + + @Throws(InputArgumentsException::class) + private fun getDirectory(commandLine: CommandLine, option: String, default: File? = null): File { + verifyOption(commandLine, option, default) + + val value = commandLine.getOptionValue(option) + val directory = if (value != null) File(value) else default!! // See note below + + // Note: the verifyOption method would throw exception if the option was ot defined and default was null + + if (!directory.isDirectory) { + throw InputArgumentsException("The directory specified for `$option` does not exist: ${directory.path}") + } + + return directory + } + + @Throws(InputArgumentsException::class) + private fun getNonNegativeInteger(commandLine: CommandLine, option: String, default: Int? = null): Int { + verifyOption(commandLine, option, default) + + val value = commandLine.getOptionValue(option) + + try { + val intValue = value?.toInt() ?: default!! // See note below + // Note: the verifyOption method would throw exception if the option was ot defined and default was null + + if (intValue < 0) { + // Handle error in th + throw NumberFormatException() + } + + return intValue + + } catch (numberError: NumberFormatException) { + throw InputArgumentsException("Parameter '$option' must be a non-negative integer value: was '$value'") + } + } + + @Throws(InputArgumentsException::class) + private fun getPositiveInteger(commandLine: CommandLine, option: String, default: Int? = null): Int { + verifyOption(commandLine, option, default) + + val value = commandLine.getOptionValue(option) + + try { + val intValue = value?.toInt() ?: default!! // See note below + // Note: the verifyOption method would throw exception if the option was ot defined and default was null + + if (intValue <= 0) { + // Handle error in th + throw NumberFormatException() + } + + return intValue + + } catch (numberError: NumberFormatException) { + throw InputArgumentsException("Parameter '$option' must be a positive integer value: was '$value'") + } + } + + @Throws(InputArgumentsException::class) + private fun getLong(commandLine: CommandLine, option: String, default: Long? = null): Long { + verifyOption(commandLine, option, default) + + val value = commandLine.getOptionValue(option) + + try { + return value?.toLong() ?: default!! // See note below + // Note: the verifyOption method would throw exception if the option was ot defined and default was null + + } catch (numberError: NumberFormatException) { + throw InputArgumentsException("Parameter '$option' must be a positive long value: was '$value'") + } + } + + @Throws(InputArgumentsException::class) + private fun verifyOption(commandLine: CommandLine, option: String, default: Any?) { + + if (!commandLine.hasOption(option) && default == null) { + throw InputArgumentsException("The parameter '$option' is missing and it is mandatory") + } + } +} diff --git a/src/main/kotlin/utils/BGPNotificationCollector.kt b/src/main/kotlin/utils/BGPNotificationCollector.kt new file mode 100644 index 0000000..05d5e1a --- /dev/null +++ b/src/main/kotlin/utils/BGPNotificationCollector.kt @@ -0,0 +1,100 @@ +package utils + +import bgp.notifications.* +import core.simulator.notifications.Notification + +/** + * Created on 26-07-2017 + * + * @author David Fialho + */ +class BGPNotificationCollector(val withOutput: Boolean): NotificationCollector(), + ImportListener, LearnListener, DetectListener, SelectListener, ExportListener, + ReEnableListener { + + //region Lists containing all notifications + + val importNotifications = ArrayList() + val learnNotifications = ArrayList() + val detectNotifications = ArrayList() + val selectNotifications = ArrayList() + val exportNotifications = ArrayList() + val reEnableNotifications = ArrayList() + + //endregion + + //region Register/Unregister methods + + override fun register() { + super.register() + BGPNotifier.addImportListener(this) + BGPNotifier.addLearnListener(this) + BGPNotifier.addDetectListener(this) + BGPNotifier.addSelectListener(this) + BGPNotifier.addExportListener(this) + BGPNotifier.addReEnableListener(this) + + } + + override fun unregister() { + super.unregister() + BGPNotifier.removeImportListener(this) + BGPNotifier.removeLearnListener(this) + BGPNotifier.removeDetectListener(this) + BGPNotifier.removeSelectListener(this) + BGPNotifier.removeExportListener(this) + BGPNotifier.removeReEnableListener(this) + } + + //endregion + + //region Notify methods + + override fun notify(notification: ImportNotification) { + importNotifications.add(notification) + print(notification) + } + + override fun notify(notification: LearnNotification) { + learnNotifications.add(notification) + print(notification) + } + + override fun notify(notification: DetectNotification) { + detectNotifications.add(notification) + print(notification) + } + + override fun notify(notification: SelectNotification) { + selectNotifications.add(notification) + print(notification) + } + + override fun notify(notification: ExportNotification) { + exportNotifications.add(notification) + print(notification) + } + + override fun notify(notification: ReEnableNotification) { + reEnableNotifications.add(notification) + print(notification) + } + + private fun print(notification: Notification) { + if (withOutput) { + println("time=${notification.time}: $notification") + } + } + + //endregion + +} + +fun collectBGPNotifications(withOutput: Boolean = false, body: () -> Unit): BGPNotificationCollector { + + val collector = BGPNotificationCollector(withOutput) + collector.register() + body() + collector.unregister() + return collector +} \ No newline at end of file diff --git a/src/main/kotlin/utils/NotificationCollector.kt b/src/main/kotlin/utils/NotificationCollector.kt new file mode 100644 index 0000000..1757ebc --- /dev/null +++ b/src/main/kotlin/utils/NotificationCollector.kt @@ -0,0 +1,66 @@ +package utils + +import core.simulator.notifications.* + +/** + * Created on 26-07-2017 + * + * @author David Fialho + * + * The NotificationCollector collects all notifications send by the notifier. + */ +open class NotificationCollector: StartListener, EndListener, ThresholdReachedListener, + MessageSentListener, MessageReceivedListener { + + val startNotifications = ArrayList() + val endNotifications = ArrayList() + val thresholdReachedNotifications = ArrayList() + val messageSentNotifications = ArrayList() + val messageReceivedNotifications = ArrayList() + + open fun register() { + BasicNotifier.addStartListener(this) + BasicNotifier.addEndListener(this) + BasicNotifier.addThresholdReachedListener(this) + BasicNotifier.addMessageSentListener(this) + BasicNotifier.addMessageReceivedListener(this) + } + + open fun unregister() { + BasicNotifier.removeStartListener(this) + BasicNotifier.removeEndListener(this) + BasicNotifier.removeThresholdReachedListener(this) + BasicNotifier.removeMessageSentListener(this) + BasicNotifier.removeMessageReceivedListener(this) + } + + final override fun notify(notification: StartNotification) { + startNotifications.add(notification) + } + + final override fun notify(notification: EndNotification) { + endNotifications.add(notification) + } + + final override fun notify(notification: ThresholdReachedNotification) { + thresholdReachedNotifications.add(notification) + } + + final override fun notify(notification: MessageSentNotification) { + messageSentNotifications.add(notification) + } + + final override fun notify(notification: MessageReceivedNotification) { + messageReceivedNotifications.add(notification) + } + +} + +fun collectBasicNotifications(body: () -> Unit): NotificationCollector { + + val collector = NotificationCollector() + collector.register() + body() + collector.unregister() + return collector +} \ No newline at end of file diff --git a/src/test/kotlin/bgp/BGPRouteTests.kt b/src/test/kotlin/bgp/BGPRouteTests.kt new file mode 100644 index 0000000..45b4e00 --- /dev/null +++ b/src/test/kotlin/bgp/BGPRouteTests.kt @@ -0,0 +1,94 @@ +package bgp + +import core.routing.emptyPath +import core.routing.pathOf +import org.jetbrains.spek.api.Spek +import org.jetbrains.spek.api.dsl.given +import org.jetbrains.spek.api.dsl.it +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.* +import testing.bgp.BGPNode + +/** + * Created on 21-07-2017 + + * @author David Fialho + */ +object BGPRouteTests : Spek({ + + given("route1 has higher LOCAL-PREF than route2") { + + it("returns positive value if length of the AS-PATH of route1 is shorter than route2's") { + val route1 = BGPRoute.with(localPref = 10, asPath = pathOf(BGPNode(id = 1))) + val route2 = BGPRoute.with(localPref = 5, asPath = pathOf(BGPNode(id = 1), BGPNode(id = 2))) + + assertThat(bgpRouteCompare(route1, route2), greaterThan(0)) + } + + it("returns positive value if length of the AS-PATH of route1 is equal than route2's") { + val route1 = BGPRoute.with(localPref = 10, asPath = pathOf(BGPNode(id = 1))) + val route2 = BGPRoute.with(localPref = 5, asPath = pathOf(BGPNode(id = 1))) + + assertThat(bgpRouteCompare(route1, route2), greaterThan(0)) + } + + it("returns positive value if length of the AS-PATH of route1 is longer than route2's") { + val route1 = BGPRoute.with(localPref = 10, asPath = pathOf(BGPNode(id = 1))) + val route2 = BGPRoute.with(localPref = 5, asPath = pathOf(BGPNode(id = 1), BGPNode(id = 2))) + + assertThat(bgpRouteCompare(route1, route2), greaterThan(0)) + } + + } + + given("route1 has same LOCAL-PREF as route2 and they have different AS-PATH lengths") { + + it("returns positive value if length of the AS-PATH of route1 is shorter than route2's") { + val route1 = BGPRoute.with(localPref = 10, asPath = pathOf(BGPNode(id = 1))) + val route2 = BGPRoute.with(localPref = 10, asPath = pathOf(BGPNode(id = 1), BGPNode(id = 2))) + + assertThat(bgpRouteCompare(route1, route2), greaterThan(0)) + } + + it("returns negative value if length of the AS-PATH of route1 is longer than route2's") { + val route1 = BGPRoute.with(localPref = 10, asPath = pathOf(BGPNode(id = 1), BGPNode(id = 2))) + val route2 = BGPRoute.with(localPref = 10, asPath = pathOf(BGPNode(id = 1))) + + assertThat(bgpRouteCompare(route1, route2), lessThan(0)) + } + + } + + given("routes 1 and 2 have the same LOCAL-PREF the same AS-PATH length") { + + it("returns positive value if the ID of the next-hop node of route1 is lower than route2's") { + val route1 = BGPRoute.with(localPref = 10, asPath = pathOf(BGPNode(id = 1), BGPNode(id = 2))) + val route2 = BGPRoute.with(localPref = 10, asPath = pathOf(BGPNode(id = 2), BGPNode(id = 3))) + + assertThat(bgpRouteCompare(route1, route2), greaterThan(0)) + } + + it("returns negative value if the ID of the next-hop node of route1 is higher than route2's") { + val route1 = BGPRoute.with(localPref = 10, asPath = pathOf(BGPNode(id = 2), BGPNode(id = 3))) + val route2 = BGPRoute.with(localPref = 10, asPath = pathOf(BGPNode(id = 1), BGPNode(id = 2))) + + assertThat(bgpRouteCompare(route1, route2), lessThan(0)) + } + + it("returns zero if the ID of the next-hop node of route1 is equal to route2's") { + val route1 = BGPRoute.with(localPref = 10, asPath = pathOf(BGPNode(id = 2), BGPNode(id = 1))) + val route2 = BGPRoute.with(localPref = 10, asPath = pathOf(BGPNode(id = 3), BGPNode(id = 1))) + + assertThat(bgpRouteCompare(route1, route2), equalTo(0)) + } + + it("returns zero if both AS-PATH are empty") { + val route1 = BGPRoute.with(localPref = 10, asPath = emptyPath()) + val route2 = BGPRoute.with(localPref = 10, asPath = emptyPath()) + + assertThat(bgpRouteCompare(route1, route2), equalTo(0)) + } + + } + +}) diff --git a/src/test/kotlin/bgp/BGPTests.kt b/src/test/kotlin/bgp/BGPTests.kt new file mode 100644 index 0000000..1f340cd --- /dev/null +++ b/src/test/kotlin/bgp/BGPTests.kt @@ -0,0 +1,558 @@ +package bgp + +import com.nhaarman.mockito_kotlin.* +import core.routing.* +import core.simulator.Time +import core.simulator.Engine +import org.hamcrest.MatcherAssert.assertThat +import org.jetbrains.spek.api.Spek +import org.jetbrains.spek.api.dsl.context +import org.jetbrains.spek.api.dsl.given +import org.jetbrains.spek.api.dsl.it +import testing.`when` +import testing.bgp.BGPNode +import org.hamcrest.Matchers.`is` as Is + +/** + * Created on 26-07-2017 + * + * @author David Fialho + */ +object BGPTests : Spek({ + + context("node with ID 1 imports a new route from neighbor with ID 2 with MRAI disabled") { + + val protocol = BGP(routingTable = spy(RoutingTable.empty(BGPRoute.invalid()))) + val node = spy(Node(1, protocol)) + val neighbor = BGPNode(id = 2) + + // Make sure the scheduler is kept clean for the next tests + afterGroup { Engine.scheduler.reset() } + + // Reset spies + afterEachTest { + reset(node) + reset(protocol.routingTable.table) + } + + given("a node with empty protocol state and with MRAI timer disabled") { + + beforeEachTest { + // Keep the protocol state clean after each test + protocol.reset() + } + + `when`("node imports an invalid route") { + + protocol.process(node, neighbor, importedRoute = BGPRoute.invalid()) + + it("does not export any route") { + verify(node, never()).send(any()) + } + + it("stores an invalid route via neighbor 2") { + @Suppress("ReplaceGetOrSet") + verify(protocol.routingTable.table, times(1)).set(neighbor, BGPRoute.invalid()) + } + + it("does not select any route") { + assertThat(protocol.routingTable.getSelectedRoute(), + Is(BGPRoute.invalid())) + } + } + + `when`("node imports route(10, [0, 2])") { + + val importedRoute = BGPRoute.with(localPref = 10, asPath = pathOf(BGPNode(0), neighbor)) + protocol.process(node, neighbor, importedRoute) + + it("exports the newly imported route") { + verify(node, times(1)).send(importedRoute) + } + + it("stores imported route as candidate route via neighbor 2") { + @Suppress("ReplaceGetOrSet") + verify(protocol.routingTable.table, times(1)).set(neighbor, importedRoute) + } + + it("selects the newly imported route") { + assertThat(protocol.routingTable.getSelectedRoute(), + Is(importedRoute)) + } + } + + `when`("node imports route(10, [0, 1, 2])") { + + val importedRoute = BGPRoute.with(localPref = 10, asPath = pathOf(BGPNode(0), node, neighbor)) + protocol.process(node, neighbor, importedRoute) + + it("does not export any route") { + verify(node, never()).send(any()) + } + + it("stores an invalid route via neighbor 2") { + @Suppress("ReplaceGetOrSet") + verify(protocol.routingTable.table, times(1)).set(neighbor, BGPRoute.invalid()) + } + + it("does not select any route") { + assertThat(protocol.routingTable.getSelectedRoute(), + Is(BGPRoute.invalid())) + } + } + } + + given("node selects route(10, [0, 2]) via node 2 and with no alternative") { + + beforeEachTest { + // Keep the protocol state clean after each test + protocol.reset() + protocol.routingTable.update(neighbor, BGPRoute.with(10, pathOf(BGPNode(0), neighbor))) + } + + `when`("node imports an invalid route") { + + protocol.process(node, neighbor, importedRoute = BGPRoute.invalid()) + + it("exports an invalid route") { + verify(node, times(1)).send(BGPRoute.invalid()) + } + + it("stores an invalid route via neighbor 2") { + @Suppress("ReplaceGetOrSet") + verify(protocol.routingTable.table, times(1)).set(neighbor, BGPRoute.invalid()) + } + + it("does not select any route") { + assertThat(protocol.routingTable.getSelectedRoute(), + Is(BGPRoute.invalid())) + } + } + + `when`("node imports route(20, [0, 2])") { + + val importedRoute = BGPRoute.with(localPref = 20, asPath = pathOf(BGPNode(0), neighbor)) + protocol.process(node, neighbor, importedRoute) + + it("exports the newly imported route") { + verify(node, times(1)).send(importedRoute) + } + + it("stores imported route as candidate route via neighbor 2") { + @Suppress("ReplaceGetOrSet") + verify(protocol.routingTable.table, times(1)).set(neighbor, importedRoute) + } + + it("selects the newly imported route") { + assertThat(protocol.routingTable.getSelectedRoute(), + Is(importedRoute)) + } + } + + `when`("node imports route(5, [0, 2])") { + + val importedRoute = BGPRoute.with(localPref = 5, asPath = pathOf(BGPNode(0), neighbor)) + protocol.process(node, neighbor, importedRoute) + + it("exports the newly imported route") { + verify(node, times(1)).send(importedRoute) + } + + it("stores imported route as candidate route via neighbor 2") { + @Suppress("ReplaceGetOrSet") + verify(protocol.routingTable.table, times(1)).set(neighbor, importedRoute) + } + + it("selects the newly imported route") { + assertThat(protocol.routingTable.getSelectedRoute(), + Is(importedRoute)) + } + } + + `when`("node imports route(20, [0, 1, 2])") { + + val importedRoute = BGPRoute.with(localPref = 20, asPath = pathOf(BGPNode(0), node, neighbor)) + protocol.process(node, neighbor, importedRoute) + + it("exports an invalid route") { + verify(node, times(1)).send(BGPRoute.invalid()) + } + + it("stores an invalid route via neighbor 2") { + @Suppress("ReplaceGetOrSet") + verify(protocol.routingTable.table, times(1)).set(neighbor, BGPRoute.invalid()) + } + + it("does not select any route") { + assertThat(protocol.routingTable.getSelectedRoute(), + Is(BGPRoute.invalid())) + } + } + } + + given("node selects route(10, [0, 3]) via node 3 and with no alternative") { + + beforeEachTest { + // Keep the protocol state clean after each test + protocol.reset() + val neighbor3 = BGPNode(3) + protocol.routingTable.update(neighbor3, BGPRoute.with(10, pathOf(BGPNode(0), neighbor3))) + } + + `when`("node imports an invalid route") { + + protocol.process(node, neighbor, importedRoute = BGPRoute.invalid()) + + it("does not export any route") { + verify(node, never()).send(any()) + } + + it("stores an invalid route via neighbor 2") { + @Suppress("ReplaceGetOrSet") + verify(protocol.routingTable.table, times(1)).set(neighbor, BGPRoute.invalid()) + } + + it("keeps selecting route(10, , [0, 3])") { + assertThat(protocol.routingTable.getSelectedRoute(), + Is(BGPRoute.with(10, pathOf(BGPNode(0), BGPNode(3))))) + } + } + + `when`("node imports route(20, [0, 2])") { + + val importedRoute = BGPRoute.with(localPref = 20, asPath = pathOf(BGPNode(0), neighbor)) + protocol.process(node, neighbor, importedRoute) + + it("exports the newly imported route") { + verify(node, times(1)).send(importedRoute) + } + + it("stores imported route as candidate route via neighbor 2") { + @Suppress("ReplaceGetOrSet") + verify(protocol.routingTable.table, times(1)).set(neighbor, importedRoute) + } + + it("selects the newly imported route") { + assertThat(protocol.routingTable.getSelectedRoute(), + Is(importedRoute)) + } + } + + `when`("node imports route(5, [0, 2])") { + + val importedRoute = BGPRoute.with(localPref = 5, asPath = pathOf(BGPNode(0), neighbor)) + protocol.process(node, neighbor, importedRoute) + + it("does not export any route") { + verify(node, never()).send(any()) + } + + it("stores imported route as candidate route via neighbor 2") { + @Suppress("ReplaceGetOrSet") + verify(protocol.routingTable.table, times(1)).set(neighbor, importedRoute) + } + + it("keeps selecting route(10, , [0, 3])") { + assertThat(protocol.routingTable.getSelectedRoute(), + Is(BGPRoute.with(10, pathOf(BGPNode(0), BGPNode(3))))) + } + } + + `when`("node imports route(20, [0, 1, 2])") { + + val importedRoute = BGPRoute.with(localPref = 20, asPath = pathOf(BGPNode(0), node, neighbor)) + protocol.process(node, neighbor, importedRoute) + + it("does not export any route") { + verify(node, never()).send(any()) + } + + it("stores an invalid route via neighbor 2") { + @Suppress("ReplaceGetOrSet") + verify(protocol.routingTable.table, times(1)).set(neighbor, BGPRoute.invalid()) + } + + it("keeps selecting route(10, , [0, 3])") { + assertThat(protocol.routingTable.getSelectedRoute(), + Is(BGPRoute.with(10, pathOf(BGPNode(0), BGPNode(3))))) + } + } + } + + given("node selects route(10, [0, 2]) via node 2 and alternative route(5, [0, 3]) via node 3") { + + val neighbor3 = BGPNode(3) + + beforeEachTest { + protocol.reset() + + // Alternative route + protocol.routingTable.update(neighbor3, BGPRoute.with(5, pathOf(BGPNode(0), neighbor3))) + // Selected route + protocol.routingTable.update(neighbor, BGPRoute.with(10, pathOf(BGPNode(0), neighbor))) + } + + `when`("node imports an invalid route") { + + protocol.process(node, neighbor, importedRoute = BGPRoute.invalid()) + + it("exports route(5, [0, 3])") { + verify(node, times(1)).send(BGPRoute.with(5, pathOf(BGPNode(0), neighbor3))) + } + + it("stores an invalid route via neighbor 2") { + @Suppress("ReplaceGetOrSet") + verify(protocol.routingTable.table, times(1)).set(neighbor, BGPRoute.invalid()) + } + + it("selects route route(5, [0, 3])") { + assertThat(protocol.routingTable.getSelectedRoute(), + Is(BGPRoute.with(5, pathOf(BGPNode(0), neighbor3)))) + } + } + + `when`("node imports route(20, [0, 2])") { + + val importedRoute = BGPRoute.with(localPref = 20, asPath = pathOf(BGPNode(0), neighbor)) + protocol.process(node, neighbor, importedRoute) + + it("exports the newly imported route") { + verify(node, times(1)).send(importedRoute) + } + + it("stores imported route as candidate route via neighbor 2") { + @Suppress("ReplaceGetOrSet") + verify(protocol.routingTable.table, times(1)).set(neighbor, importedRoute) + } + + it("selects the newly imported route") { + assertThat(protocol.routingTable.getSelectedRoute(), + Is(importedRoute)) + } + } + + `when`("node imports route(7, [0, 2])") { + + val importedRoute = BGPRoute.with(localPref = 7, asPath = pathOf(BGPNode(0), neighbor)) + protocol.process(node, neighbor, importedRoute) + + it("exports the newly imported route") { + verify(node, times(1)).send(importedRoute) + } + + it("stores imported route as candidate route via neighbor 2") { + @Suppress("ReplaceGetOrSet") + verify(protocol.routingTable.table, times(1)).set(neighbor, importedRoute) + } + + it("selects the newly imported route") { + assertThat(protocol.routingTable.getSelectedRoute(), + Is(importedRoute)) + } + } + + `when`("node imports route(20, [0, 1, 2])") { + + val importedRoute = BGPRoute.with(localPref = 20, asPath = pathOf(BGPNode(0), node, neighbor)) + protocol.process(node, neighbor, importedRoute) + + it("exports route(5, [0, 3])") { + verify(node, times(1)).send(BGPRoute.with(5, pathOf(BGPNode(0), neighbor3))) + } + + it("stores an invalid route via neighbor 2") { + @Suppress("ReplaceGetOrSet") + verify(protocol.routingTable.table, times(1)).set(neighbor, BGPRoute.invalid()) + } + + it("selects route route(5, [0, 3])") { + assertThat(protocol.routingTable.getSelectedRoute(), + Is(BGPRoute.with(5, pathOf(BGPNode(0), neighbor3)))) + } + } + } + + given("node selects route(10, [0, 3]) via node 3 and alternative route(5, [0, 2]) via node 2") { + + val neighbor3 = BGPNode(3) + + beforeEachTest { + protocol.reset() + + // Selected route + protocol.routingTable.update(neighbor3, BGPRoute.with(10, pathOf(BGPNode(0), neighbor3))) + // Alternative route + protocol.routingTable.update(neighbor, BGPRoute.with(5, pathOf(BGPNode(0), neighbor))) + } + + `when`("node imports an invalid route") { + + protocol.process(node, neighbor, importedRoute = BGPRoute.invalid()) + + it("does not export any route") { + verify(node, never()).send(any()) + } + + it("stores an invalid route via neighbor 2") { + @Suppress("ReplaceGetOrSet") + verify(protocol.routingTable.table, times(1)).set(neighbor, BGPRoute.invalid()) + } + + it("selects route route(10, [0, 3])") { + assertThat(protocol.routingTable.getSelectedRoute(), + Is(BGPRoute.with(10, pathOf(BGPNode(0), neighbor3)))) + } + } + + `when`("node imports route(20, [0, 2])") { + + val importedRoute = BGPRoute.with(localPref = 20, asPath = pathOf(BGPNode(0), neighbor)) + protocol.process(node, neighbor, importedRoute) + + it("exports the newly imported route") { + verify(node, times(1)).send(importedRoute) + } + + it("stores imported route as candidate route via neighbor 2") { + @Suppress("ReplaceGetOrSet") + verify(protocol.routingTable.table, times(1)).set(neighbor, importedRoute) + } + + it("selects the newly imported route") { + assertThat(protocol.routingTable.getSelectedRoute(), + Is(importedRoute)) + } + } + + `when`("node imports route(7, [0, 2])") { + + val importedRoute = BGPRoute.with(localPref = 7, asPath = pathOf(BGPNode(0), neighbor)) + protocol.process(node, neighbor, importedRoute) + + it("does not export any route") { + verify(node, never()).send(any()) + } + + it("stores imported route as candidate route via neighbor 2") { + @Suppress("ReplaceGetOrSet") + verify(protocol.routingTable.table, times(1)).set(neighbor, importedRoute) + } + + it("selects route route(10, [0, 3])") { + assertThat(protocol.routingTable.getSelectedRoute(), + Is(BGPRoute.with(10, pathOf(BGPNode(0), neighbor3)))) + } + } + + `when`("node imports route(20, [0, 1, 2])") { + + val importedRoute = BGPRoute.with(localPref = 20, asPath = pathOf(BGPNode(0), node, neighbor)) + protocol.process(node, neighbor, importedRoute) + + it("does not export any route") { + verify(node, never()).send(any()) + } + + it("stores an invalid route via neighbor 2") { + @Suppress("ReplaceGetOrSet") + verify(protocol.routingTable.table, times(1)).set(neighbor, BGPRoute.invalid()) + } + + it("selects route route(10, [0, 3])") { + assertThat(protocol.routingTable.getSelectedRoute(), + Is(BGPRoute.with(10, pathOf(BGPNode(0), neighbor3)))) + } + } + } + + } + + fun BGP(mrai: Time = 0) = BGP(mrai, routingTable = spy(RoutingTable.empty(BGPRoute.invalid()))) + fun SpyBGPNode(protocol: Protocol) = spy(Node(1, protocol)) + + context("node exports a new route with MRAI timer enabled") { + + val neighbor = BGPNode(2) + val exportedRoute = BGPRoute.with(localPref = 10, asPath = pathOf(BGPNode(0), neighbor)) + + // Make sure the scheduler is kept clean for the next tests + afterGroup { Engine.scheduler.reset() } + + given("node never exported a route") { + + val protocol = BGP(mrai = 10) + val node = SpyBGPNode(protocol) + + protocol.process(node, neighbor, exportedRoute) + + it("sends the route to neighbors immediately") { + verify(node, times(1)).send(exportedRoute) + } + + it("starts a new MRAI timer") { + assertThat(protocol.mraiTimer.expired, + Is(false)) + } + } + + given("the MRAI timer has expired after previous export") { + + val protocol = BGP(mrai = 10) + val node = SpyBGPNode(protocol) + + // Node exports some route that starts the MRAI timer + protocol.process(node, neighbor, BGPRoute.with(localPref = 10, asPath = emptyPath())) + // MRAI Timer expires + protocol.mraiTimer.onExpired() + + // Ensure the node call count is clean + reset(node) + + // Node has a new route to export + protocol.process(node, neighbor, exportedRoute) + + it("sends the route to neighbors immediately") { + verify(node, times(1)).send(exportedRoute) + } + + it("starts a new MRAI timer") { + assertThat(protocol.mraiTimer.expired, + Is(false)) + } + } + + given("the MRAI timer is running") { + + val protocol = BGP(mrai = 10) + val node = SpyBGPNode(protocol) + + // Node exports some route that starts the MRAI timer + protocol.process(node, neighbor, BGPRoute.with(localPref = 10, asPath = emptyPath())) + + // Ensure the node call count is clean + reset(node) + + // Node has a new route to export + protocol.process(node, neighbor, exportedRoute) + + it("does NOT send route to neighbors") { + verify(node, never()).send(any()) + } + + `when`("the MRAI timer expires") { + + protocol.mraiTimer.onExpired() + + it("sends the newly exported route") { + verify(node, times(1)).send(exportedRoute) + } + + it("does NOT start a new MRAI timer") { + assertThat(protocol.mraiTimer.expired, + Is(true)) + } + } + } + } + +}) diff --git a/src/test/kotlin/bgp/SSBGPTests.kt b/src/test/kotlin/bgp/SSBGPTests.kt new file mode 100644 index 0000000..427f114 --- /dev/null +++ b/src/test/kotlin/bgp/SSBGPTests.kt @@ -0,0 +1,352 @@ +package bgp + +import com.nhaarman.mockito_kotlin.* +import core.routing.Node +import core.routing.Protocol +import core.routing.RoutingTable +import core.routing.pathOf +import core.simulator.Time +import core.simulator.Engine +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.`is` as Is +import org.jetbrains.spek.api.Spek +import org.jetbrains.spek.api.dsl.context +import org.jetbrains.spek.api.dsl.given +import org.jetbrains.spek.api.dsl.it +import testing.`when` +import testing.bgp.BGPNode + +/** + * Created on 28-08-2017. + * + * @author David Fialho + */ +object SSBGPTests : Spek({ + + fun SSBGP(mrai: Time = 0) = SSBGP(mrai, routingTable = spy(RoutingTable.empty(BGPRoute.invalid()))) + fun SpyBGPNode(protocol: Protocol) = spy(Node(1, protocol)) + + context("node with ID 1 imports a new route from neighbor with ID 2 with MRAI disabled") { + + val neighbor = BGPNode(id = 2) + + // Make sure the scheduler is kept clean for the next tests + afterGroup { Engine.scheduler.reset() } + + given("a node with empty protocol state and with MRAI timer disabled") { + + val protocol = SSBGP() + val node = SpyBGPNode(protocol) + beforeEachTest { protocol.reset() } + + `when`("node imports looping route(10, [0, 1, 2])") { + + protocol.process(node, neighbor, importedRoute = BGPRoute.with(10, pathOf(BGPNode(0), node, neighbor))) + + it("stores an invalid route via neighbor 2") { + @Suppress("ReplaceGetOrSet") + verify(protocol.routingTable.table, atLeastOnce()).set(neighbor, BGPRoute.invalid()) + } + + it("does not select any route") { + assertThat(protocol.routingTable.getSelectedRoute(), + Is(BGPRoute.invalid())) + } + + it("disables neighbor 2") { + assertThat(protocol.routingTable.table.isEnabled(neighbor), + Is(false)) + } + } + } + + given("node selects route(10, [0, 2]) via node 2 and with no alternative") { + + val protocol = SSBGP() + val node = SpyBGPNode(protocol) + + beforeEachTest { + protocol.reset() + protocol.routingTable.update(neighbor, BGPRoute.with(10, pathOf(BGPNode(0), neighbor))) + reset(protocol.routingTable.table) + } + + `when`("node imports looping route(10, [0, 1, 2])") { + + protocol.process(node, neighbor, importedRoute = BGPRoute.with(10, pathOf(BGPNode(0), node, neighbor))) + + it("stores an invalid route via neighbor 2") { + @Suppress("ReplaceGetOrSet") + verify(protocol.routingTable.table, atLeastOnce()).set(neighbor, BGPRoute.invalid()) + } + + it("does not select any route") { + assertThat(protocol.routingTable.getSelectedRoute(), + Is(BGPRoute.invalid())) + } + + it("disables neighbor 2") { + assertThat(protocol.routingTable.table.isEnabled(neighbor), + Is(false)) + } + } + + `when`("node imports looping route(20, [0, 1, 2])") { + + protocol.process(node, neighbor, importedRoute = BGPRoute.with(20, pathOf(BGPNode(0), node, neighbor))) + + it("stores an invalid route via neighbor 2") { + @Suppress("ReplaceGetOrSet") + verify(protocol.routingTable.table, atLeastOnce()).set(neighbor, BGPRoute.invalid()) + } + + it("does not select any route") { + assertThat(protocol.routingTable.getSelectedRoute(), + Is(BGPRoute.invalid())) + } + + it("disables neighbor 2") { + assertThat(protocol.routingTable.table.isEnabled(neighbor), + Is(false)) + } + } + } + + given("node selects route(10, [0, 3]) via node 3 and with no alternative") { + + val protocol = SSBGP() + val node = SpyBGPNode(protocol) + val neighbor3 = BGPNode(3) + + beforeEachTest { + protocol.reset() + protocol.routingTable.update(neighbor3, BGPRoute.with(10, pathOf(BGPNode(0), neighbor3))) + reset(protocol.routingTable.table) + } + + `when`("node imports looping route(20, [0, 1, 2])") { + + protocol.process(node, neighbor, importedRoute = BGPRoute.with(20, pathOf(BGPNode(0), node, neighbor))) + + it("stores an invalid route via neighbor 2") { + @Suppress("ReplaceGetOrSet") + verify(protocol.routingTable.table, atLeastOnce()).set(neighbor, BGPRoute.invalid()) + } + + it("selects route(10, [0, 3])") { + assertThat(protocol.routingTable.getSelectedRoute(), + Is(BGPRoute.with(10, pathOf(BGPNode(0), neighbor3)))) + } + + it("disables neighbor 2") { + assertThat(protocol.routingTable.table.isEnabled(neighbor), + Is(false)) + } + } + + `when`("node imports looping route(10, [0, 1, 2])") { + + protocol.process(node, neighbor, importedRoute = BGPRoute.with(10, pathOf(BGPNode(0), node, neighbor))) + + it("stores an invalid route via neighbor 2") { + @Suppress("ReplaceGetOrSet") + verify(protocol.routingTable.table, atLeastOnce()).set(neighbor, BGPRoute.invalid()) + } + + it("selects route(10, [0, 3])") { + assertThat(protocol.routingTable.getSelectedRoute(), + Is(BGPRoute.with(10, pathOf(BGPNode(0), neighbor3)))) + } + + it("does NOT disable neighbor 2") { + assertThat(protocol.routingTable.table.isEnabled(neighbor), + Is(true)) + } + } + + `when`("node imports looping route(5, [0, 1, 2])") { + + protocol.process(node, neighbor, importedRoute = BGPRoute.with(5, pathOf(BGPNode(0), node, neighbor))) + + it("stores an invalid route via neighbor 2") { + @Suppress("ReplaceGetOrSet") + verify(protocol.routingTable.table, atLeastOnce()).set(neighbor, BGPRoute.invalid()) + } + + it("selects route(10, [0, 3])") { + assertThat(protocol.routingTable.getSelectedRoute(), + Is(BGPRoute.with(10, pathOf(BGPNode(0), neighbor3)))) + } + + it("does NOT disable neighbor 2") { + assertThat(protocol.routingTable.table.isEnabled(neighbor), + Is(true)) + } + } + + `when`("node imports looping route(20, [0, 4, 1, 2])") { + + val importedRoute = BGPRoute.with(20, pathOf(BGPNode(0), BGPNode(4), node, neighbor)) + protocol.process(node, neighbor, importedRoute) + + it("stores an invalid route via neighbor 2") { + @Suppress("ReplaceGetOrSet") + verify(protocol.routingTable.table, atLeastOnce()).set(neighbor, BGPRoute.invalid()) + } + + it("selects route(10, [0, 3])") { + assertThat(protocol.routingTable.getSelectedRoute(), + Is(BGPRoute.with(10, pathOf(BGPNode(0), neighbor3)))) + } + + it("disables neighbor 2") { + assertThat(protocol.routingTable.table.isEnabled(neighbor), + Is(false)) + } + } + } + + given("node selects route(10, [0, 3]) via node 3, with no alternative, and neighbor 2 is disabled") { + + val protocol = SSBGP() + val node = SpyBGPNode(protocol) + val neighbor3 = BGPNode(3) + + beforeEachTest { + protocol.reset() + protocol.routingTable.update(neighbor3, BGPRoute.with(10, pathOf(BGPNode(0), neighbor3))) + protocol.disableNeighbor(neighbor) + reset(protocol.routingTable.table) + } + + `when`("node imports route(5, [0, 2])") { + + val importedRoute = BGPRoute.with(5, pathOf(BGPNode(0), neighbor)) + protocol.process(node, neighbor, importedRoute) + + it("stores route(5, [0, 2] as candidate route via neighbor 2") { + @Suppress("ReplaceGetOrSet") + verify(protocol.routingTable.table, atLeastOnce()).set(neighbor, importedRoute) + } + + it("selects route(10, [0, 3])") { + assertThat(protocol.routingTable.getSelectedRoute(), + Is(BGPRoute.with(10, pathOf(BGPNode(0), neighbor3)))) + } + + it("neighbor 2 is still disabled") { + assertThat(protocol.routingTable.table.isEnabled(neighbor), + Is(false)) + } + } + + `when`("node imports route(20, [0, 2])") { + + val importedRoute = BGPRoute.with(20, pathOf(BGPNode(0), neighbor)) + protocol.process(node, neighbor, importedRoute) + + it("stores route(20, [0, 2] as candidate route via neighbor 2") { + @Suppress("ReplaceGetOrSet") + verify(protocol.routingTable.table, atLeastOnce()).set(neighbor, importedRoute) + } + + it("selects route(10, [0, 3])") { + assertThat(protocol.routingTable.getSelectedRoute(), + Is(BGPRoute.with(10, pathOf(BGPNode(0), neighbor3)))) + } + + it("neighbor 2 is still disabled") { + assertThat(protocol.routingTable.table.isEnabled(neighbor), + Is(false)) + } + } + } + } + + context("node with ID 1 disables/enables neighbor with ID 2") { + + val neighbor2 = BGPNode(id = 2) + val neighbor3 = BGPNode(id = 3) + + // Make sure the scheduler is kept clean for the next tests + afterGroup { Engine.scheduler.reset() } + + given("node selects route (10, [0, 3]) via node 3 and with alternative route (5, [0, 2]) via node 2") { + + val protocol = SSBGP() + + protocol.routingTable.update(neighbor3, BGPRoute.with(10, pathOf(BGPNode(0), neighbor3))) + protocol.routingTable.update(neighbor2, BGPRoute.with(5, pathOf(BGPNode(0), neighbor2))) + + `when`("it disables neighbor 2") { + + protocol.disableNeighbor(neighbor2) + + it("selects route (10, [0, 3])") { + assertThat(protocol.routingTable.getSelectedRoute(), + Is(BGPRoute.with(localPref = 10, asPath = pathOf(BGPNode(0), neighbor3)))) + } + + it("still indicates neighbor 2 is disabled") { + assertThat(protocol.routingTable.table.isEnabled(neighbor2), + Is(false)) + } + } + + `when`("it enables back neighbor 2") { + + protocol.enableNeighbor(neighbor2) + + it("selects route (10, [0, 3])") { + assertThat(protocol.routingTable.getSelectedRoute(), + Is(BGPRoute.with(localPref = 10, asPath = pathOf(BGPNode(0), neighbor3)))) + } + + it("enabled neighbor 2") { + assertThat(protocol.routingTable.table.isEnabled(neighbor2), + Is(true)) + } + } + } + + given("node selects route (10, [0, 2]) via node 2 and with alternative route (5, [0, 3]) via node 3") { + + val protocol = SSBGP() + + protocol.routingTable.update(neighbor2, BGPRoute.with(10, pathOf(BGPNode(0), neighbor2))) + protocol.routingTable.update(neighbor3, BGPRoute.with(5, pathOf(BGPNode(0), neighbor3))) + + protocol.disableNeighbor(neighbor2) + + `when`("it disables neighbor 2") { + + protocol.disableNeighbor(neighbor2) + + it("selects route (5, [0, 3])") { + assertThat(protocol.routingTable.getSelectedRoute(), + Is(BGPRoute.with(localPref = 5, asPath = pathOf(BGPNode(0), neighbor3)))) + } + + it("still indicates neighbor 2 is disabled") { + assertThat(protocol.routingTable.table.isEnabled(neighbor2), + Is(false)) + } + } + + `when`("it enables back neighbor 2") { + + protocol.enableNeighbor(neighbor2) + + it("selects route (10, [0, 2])") { + assertThat(protocol.routingTable.getSelectedRoute(), + Is(BGPRoute.with(localPref = 10, asPath = pathOf(BGPNode(0), neighbor2)))) + } + + it("enabled neighbor 2") { + assertThat(protocol.routingTable.table.isEnabled(neighbor2), + Is(true)) + } + } + } + } +}) \ No newline at end of file diff --git a/src/test/kotlin/bgp/policies/interdomain/InterdomainCompareRoutesTests.kt b/src/test/kotlin/bgp/policies/interdomain/InterdomainCompareRoutesTests.kt new file mode 100644 index 0000000..fa29a18 --- /dev/null +++ b/src/test/kotlin/bgp/policies/interdomain/InterdomainCompareRoutesTests.kt @@ -0,0 +1,103 @@ +package bgp.policies.interdomain + +import bgp.bgpRouteCompare +import org.jetbrains.spek.api.Spek +import org.jetbrains.spek.api.dsl.it +import org.jetbrains.spek.api.dsl.on +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.* +import org.jetbrains.spek.api.dsl.context +import org.hamcrest.Matchers.`is` as Is + + +/** + * Created on 26-07-2017 + * + * @author David Fialho + */ +object InterdomainCompareRoutesTests: Spek({ + + context("AS-PATHs are always empty") { + + on("comparing two customer routes") { + + it("returns they have the same preference") { + assertThat(bgpRouteCompare(customerRoute(), customerRoute()), + Is(equalTo(0))) + } + } + + on("comparing a customer route and a peer+ route") { + + it("returns peer+ route has higher preference") { + assertThat(bgpRouteCompare(peerplusRoute(), customerRoute()), + Is(greaterThan(0))) + } + } + + on("comparing a customer route and a peer route") { + + it("returns customer route has higher preference") { + assertThat(bgpRouteCompare(customerRoute(), peerRoute()), + Is(greaterThan(0))) + } + } + + on("comparing a customer route and a provider route") { + + it("returns customer route has higher preference") { + assertThat(bgpRouteCompare(customerRoute(), providerRoute()), + Is(greaterThan(0))) + } + } + + on("comparing a customer route with 0 sibling hops with customer route with 1 sibling hop") { + + it("returns route with 0 sibling hops has higher preference") { + assertThat(bgpRouteCompare(customerRoute(siblingHops = 0), customerRoute(siblingHops = 1)), + Is(greaterThan(0))) + } + } + + on("comparing a customer route with 0 sibling hops with peer+ route with 10 sibling hops") { + + it("returns peer+ route has higher preference") { + assertThat(bgpRouteCompare(peerplusRoute(siblingHops = 10), customerRoute(siblingHops = 0)), + Is(greaterThan(0))) + } + } + + on("comparing a customer route with 10 sibling hops with peer route with 0 sibling hops") { + + it("returns peer+ route has higher preference") { + assertThat(bgpRouteCompare(customerRoute(siblingHops = 10), peerRoute(siblingHops = 0)), + Is(greaterThan(0))) + } + } + + on("comparing a peer+ route with 0 sibling hops with peer+ route with 1 sibling hop") { + + it("returns route with 0 sibling hops has higher preference") { + assertThat(bgpRouteCompare(peerplusRoute(siblingHops = 0), peerplusRoute(siblingHops = 1)), + Is(greaterThan(0))) + } + } + + on("comparing a peer route with 0 sibling hops with peer route with 1 sibling hop") { + + it("returns route with 0 sibling hops has higher preference") { + assertThat(bgpRouteCompare(peerRoute(siblingHops = 0), peerRoute(siblingHops = 1)), + Is(greaterThan(0))) + } + } + + on("comparing a provider route with 0 sibling hops with provider route with 1 sibling hop") { + + it("returns route with 0 sibling hops has higher preference") { + assertThat(bgpRouteCompare(providerRoute(siblingHops = 0), providerRoute(siblingHops = 1)), + Is(greaterThan(0))) + } + } + } + +}) \ No newline at end of file diff --git a/src/test/kotlin/bgp/policies/interdomain/InterdomainExtendersTests.kt b/src/test/kotlin/bgp/policies/interdomain/InterdomainExtendersTests.kt new file mode 100644 index 0000000..0f0d2a0 --- /dev/null +++ b/src/test/kotlin/bgp/policies/interdomain/InterdomainExtendersTests.kt @@ -0,0 +1,699 @@ +package bgp.policies.interdomain + +import bgp.BGPRoute +import core.routing.emptyPath +import core.routing.pathOf +import org.hamcrest.MatcherAssert.assertThat +import org.jetbrains.spek.api.Spek +import org.jetbrains.spek.api.dsl.given +import org.jetbrains.spek.api.dsl.it +import org.jetbrains.spek.api.dsl.on +import testing.bgp.BGPNode +import org.hamcrest.Matchers.`is` as Is + +/** + * Created on 26-07-2017 + + * @author David Fialho + */ +object InterdomainExtendersTests : Spek({ + + given("a customer extender") { + + on("extending a customer route with an empty AS-PATH") { + + val route = customerRoute(asPath = emptyPath()) + val sender = BGPNode(id = 1) + + val extendedRoute = CustomerExtender.extend(route, sender) + + it("returns a customer route with AS-PATH containing the sender") { + assertThat(extendedRoute, + Is(customerRoute(asPath = pathOf(sender)))) + } + } + + on("extending a customer route with an AS-PATH containing node 2") { + + val route = customerRoute(asPath = pathOf(BGPNode(id = 2))) + val sender = BGPNode(id = 1) + + val extendedRoute = CustomerExtender.extend(route, sender) + + it("returns a customer route with AS-PATH containing node 2 and the sender") { + assertThat(extendedRoute, + Is(customerRoute(asPath = pathOf(BGPNode(2), sender)))) + } + } + + on("extending a peer+ route") { + + val route = peerplusRoute(asPath = emptyPath()) + val sender = BGPNode(id = 1) + + val extendedRoute = CustomerExtender.extend(route, sender) + + it("returns a customer route") { + assertThat(extendedRoute, + Is(customerRoute(asPath = pathOf(sender)))) + } + } + + on("extending a peer route") { + + val route = peerRoute(asPath = emptyPath()) + val sender = BGPNode(id = 1) + + val extendedRoute = CustomerExtender.extend(route, sender) + + it("returns an invalid route") { + assertThat(extendedRoute, + Is(BGPRoute.invalid())) + } + } + + on("extending a provider route") { + + val route = providerRoute(asPath = emptyPath()) + val sender = BGPNode(id = 1) + + val extendedRoute = CustomerExtender.extend(route, sender) + + it("returns an invalid route") { + assertThat(extendedRoute, + Is(BGPRoute.invalid())) + } + } + + on("extending a self route") { + + val route = BGPRoute.self() + val sender = BGPNode(id = 1) + + val extendedRoute = CustomerExtender.extend(route, sender) + + it("returns a customer route with the AS-PATH containing the sender") { + assertThat(extendedRoute, + Is(customerRoute(asPath = pathOf(sender)))) + } + } + + on("extending a invalid route") { + + val route = BGPRoute.invalid() + val sender = BGPNode(id = 1) + + val extendedRoute = CustomerExtender.extend(route, sender) + + it("returns an invalid route") { + assertThat(extendedRoute, + Is(BGPRoute.invalid())) + } + } + + on("extending a customer route with 1 sibling hop") { + + val route = customerRoute(siblingHops = 1, asPath = emptyPath()) + val sender = BGPNode(id = 1) + + val extendedRoute = CustomerExtender.extend(route, sender) + + it("returns a customer route with 0 sibling hops") { + assertThat(extendedRoute, + Is(customerRoute(siblingHops = 0, asPath = pathOf(sender)))) + } + } + + on("extending a peer+ route with 1 sibling hop") { + + val route = peerplusRoute(siblingHops = 1, asPath = emptyPath()) + val sender = BGPNode(id = 1) + + val extendedRoute = CustomerExtender.extend(route, sender) + + it("returns a customer route with 0 sibling hops") { + assertThat(extendedRoute, + Is(customerRoute(siblingHops = 0, asPath = pathOf(sender)))) + } + } + + on("extending a peer route with 1 sibling hop") { + + val route = peerRoute(siblingHops = 1, asPath = emptyPath()) + val sender = BGPNode(id = 1) + + val extendedRoute = CustomerExtender.extend(route, sender) + + it("returns an invalid route") { + assertThat(extendedRoute, + Is(BGPRoute.invalid())) + } + } + + on("extending a provider route with 1 sibling hop") { + + val route = providerRoute(siblingHops = 1, asPath = emptyPath()) + val sender = BGPNode(id = 1) + + val extendedRoute = CustomerExtender.extend(route, sender) + + it("returns an invalid route") { + assertThat(extendedRoute, + Is(BGPRoute.invalid())) + } + } + } + + given("a peer extender") { + + on("extending a customer route with an empty AS-PATH") { + + val route = customerRoute(asPath = emptyPath()) + val sender = BGPNode(id = 1) + + val extendedRoute = PeerExtender.extend(route, sender) + + it("returns a peer route with AS-PATH containing the sender") { + assertThat(extendedRoute, + Is(peerRoute(asPath = pathOf(sender)))) + } + } + + on("extending a customer route with an AS-PATH containing node 2") { + + val route = customerRoute(asPath = pathOf(BGPNode(id = 2))) + val sender = BGPNode(id = 1) + + val extendedRoute = PeerExtender.extend(route, sender) + + it("returns a peer route with AS-PATH containing node 2 and the sender") { + assertThat(extendedRoute, + Is(peerRoute(asPath = pathOf(BGPNode(2), sender)))) + } + } + + on("extending a peer+ route") { + + val route = peerplusRoute(asPath = emptyPath()) + val sender = BGPNode(id = 1) + + val extendedRoute = PeerExtender.extend(route, sender) + + it("returns a peer route") { + assertThat(extendedRoute, + Is(peerRoute(asPath = pathOf(sender)))) + } + } + + on("extending a peer route") { + + val route = peerRoute(asPath = emptyPath()) + val sender = BGPNode(id = 1) + + val extendedRoute = PeerExtender.extend(route, sender) + + it("returns an invalid route") { + assertThat(extendedRoute, + Is(BGPRoute.invalid())) + } + } + + on("extending a provider route") { + + val route = providerRoute(asPath = emptyPath()) + val sender = BGPNode(id = 1) + + val extendedRoute = PeerExtender.extend(route, sender) + + it("returns an invalid route") { + assertThat(extendedRoute, + Is(BGPRoute.invalid())) + } + } + + on("extending a self route") { + + val route = BGPRoute.self() + val sender = BGPNode(id = 1) + + val extendedRoute = PeerExtender.extend(route, sender) + + it("returns a peer route with the AS-PATH containing the sender") { + assertThat(extendedRoute, + Is(peerRoute(asPath = pathOf(sender)))) + } + } + + on("extending a invalid route") { + + val route = BGPRoute.invalid() + val sender = BGPNode(id = 1) + + val extendedRoute = PeerExtender.extend(route, sender) + + it("returns an invalid route") { + assertThat(extendedRoute, + Is(BGPRoute.invalid())) + } + } + + on("extending a customer route with 1 sibling hop") { + + val route = customerRoute(siblingHops = 1, asPath = emptyPath()) + val sender = BGPNode(id = 1) + + val extendedRoute = PeerExtender.extend(route, sender) + + it("returns a peer route with 0 sibling hops") { + assertThat(extendedRoute, + Is(peerRoute(siblingHops = 0, asPath = pathOf(sender)))) + } + } + + on("extending a peer+ route with 1 sibling hop") { + + val route = peerplusRoute(siblingHops = 1, asPath = emptyPath()) + val sender = BGPNode(id = 1) + + val extendedRoute = PeerExtender.extend(route, sender) + + it("returns a peer route with 0 sibling hops") { + assertThat(extendedRoute, + Is(peerRoute(siblingHops = 0, asPath = pathOf(sender)))) + } + } + + on("extending a peer route with 1 sibling hop") { + + val route = peerRoute(siblingHops = 1, asPath = emptyPath()) + val sender = BGPNode(id = 1) + + val extendedRoute = PeerExtender.extend(route, sender) + + it("returns an invalid route") { + assertThat(extendedRoute, + Is(BGPRoute.invalid())) + } + } + + on("extending a provider route with 1 sibling hop") { + + val route = providerRoute(siblingHops = 1, asPath = emptyPath()) + val sender = BGPNode(id = 1) + + val extendedRoute = PeerExtender.extend(route, sender) + + it("returns an invalid route") { + assertThat(extendedRoute, + Is(BGPRoute.invalid())) + } + } + } + + given("a provider extender") { + + on("extending a customer route with an empty AS-PATH") { + + val route = customerRoute(asPath = emptyPath()) + val sender = BGPNode(id = 1) + + val extendedRoute = ProviderExtender.extend(route, sender) + + it("returns a provider route with AS-PATH containing the sender") { + assertThat(extendedRoute, + Is(providerRoute(asPath = pathOf(sender)))) + } + } + + on("extending a customer route with an AS-PATH containing node 2") { + + val route = customerRoute(asPath = pathOf(BGPNode(id = 2))) + val sender = BGPNode(id = 1) + + val extendedRoute = ProviderExtender.extend(route, sender) + + it("returns a provider route with AS-PATH containing node 2 and the sender") { + assertThat(extendedRoute, + Is(providerRoute(asPath = pathOf(BGPNode(2), sender)))) + } + } + + on("extending a peer+ route") { + + val route = peerplusRoute(asPath = emptyPath()) + val sender = BGPNode(id = 1) + + val extendedRoute = ProviderExtender.extend(route, sender) + + it("returns a provider route") { + assertThat(extendedRoute, + Is(providerRoute(asPath = pathOf(sender)))) + } + } + + on("extending a peer route") { + + val route = peerRoute(asPath = emptyPath()) + val sender = BGPNode(id = 1) + + val extendedRoute = ProviderExtender.extend(route, sender) + + it("returns provider route") { + assertThat(extendedRoute, + Is(providerRoute(asPath = pathOf(sender)))) + } + } + + on("extending a provider route") { + + val route = providerRoute(asPath = emptyPath()) + val sender = BGPNode(id = 1) + + val extendedRoute = ProviderExtender.extend(route, sender) + + it("returns provider route") { + assertThat(extendedRoute, + Is(providerRoute(asPath = pathOf(sender)))) + } + } + + on("extending a self route") { + + val route = BGPRoute.self() + val sender = BGPNode(id = 1) + + val extendedRoute = ProviderExtender.extend(route, sender) + + it("returns a provider route with the AS-PATH containing the sender") { + assertThat(extendedRoute, + Is(providerRoute(asPath = pathOf(sender)))) + } + } + + on("extending a invalid route") { + + val route = BGPRoute.invalid() + val sender = BGPNode(id = 1) + + val extendedRoute = ProviderExtender.extend(route, sender) + + it("returns an invalid route") { + assertThat(extendedRoute, + Is(BGPRoute.invalid())) + } + } + + on("extending a customer route with 1 sibling hop") { + + val route = customerRoute(siblingHops = 1, asPath = emptyPath()) + val sender = BGPNode(id = 1) + + val extendedRoute = ProviderExtender.extend(route, sender) + + it("returns a provider route with 0 sibling hops") { + assertThat(extendedRoute, + Is(providerRoute(siblingHops = 0, asPath = pathOf(sender)))) + } + } + + on("extending a peer+ route with 1 sibling hop") { + + val route = peerplusRoute(siblingHops = 1, asPath = emptyPath()) + val sender = BGPNode(id = 1) + + val extendedRoute = ProviderExtender.extend(route, sender) + + it("returns a provider route with 0 sibling hops") { + assertThat(extendedRoute, + Is(providerRoute(siblingHops = 0, asPath = pathOf(sender)))) + } + } + + on("extending a peer route with 1 sibling hop") { + + val route = peerRoute(siblingHops = 1, asPath = emptyPath()) + val sender = BGPNode(id = 1) + + val extendedRoute = ProviderExtender.extend(route, sender) + + it("returns a provider route with 0 sibling hops") { + assertThat(extendedRoute, + Is(providerRoute(siblingHops = 0, asPath = pathOf(sender)))) + } + } + + on("extending a provider route with 1 sibling hop") { + + val route = providerRoute(siblingHops = 1, asPath = emptyPath()) + val sender = BGPNode(id = 1) + + val extendedRoute = ProviderExtender.extend(route, sender) + + it("returns a provider route with 0 sibling hops") { + assertThat(extendedRoute, + Is(providerRoute(siblingHops = 0, asPath = pathOf(sender)))) + } + } + } + + given("a peer+ extender") { + + on("extending a customer route with an empty AS-PATH") { + + val route = customerRoute(asPath = emptyPath()) + val sender = BGPNode(id = 1) + + val extendedRoute = PeerplusExtender.extend(route, sender) + + it("returns a peer+ route with AS-PATH containing the sender") { + assertThat(extendedRoute, + Is(peerplusRoute(asPath = pathOf(sender)))) + } + } + + on("extending a customer route with an AS-PATH containing node 2") { + + val route = customerRoute(asPath = pathOf(BGPNode(id = 2))) + val sender = BGPNode(id = 1) + + val extendedRoute = PeerplusExtender.extend(route, sender) + + it("returns a peer+ route with AS-PATH containing node 2 and the sender") { + assertThat(extendedRoute, + Is(peerplusRoute(asPath = pathOf(BGPNode(2), sender)))) + } + } + + on("extending a peer+ route") { + + val route = peerplusRoute(asPath = emptyPath()) + val sender = BGPNode(id = 1) + + val extendedRoute = PeerplusExtender.extend(route, sender) + + it("returns a peer+ route") { + assertThat(extendedRoute, + Is(peerplusRoute(asPath = pathOf(sender)))) + } + } + + on("extending a peer route") { + + val route = peerRoute(asPath = emptyPath()) + val sender = BGPNode(id = 1) + + val extendedRoute = PeerplusExtender.extend(route, sender) + + it("returns an invalid route") { + assertThat(extendedRoute, + Is(BGPRoute.invalid())) + } + } + + on("extending a provider route") { + + val route = providerRoute(asPath = emptyPath()) + val sender = BGPNode(id = 1) + + val extendedRoute = PeerplusExtender.extend(route, sender) + + it("returns an invalid route") { + assertThat(extendedRoute, + Is(BGPRoute.invalid())) + } + } + + on("extending a self route") { + + val route = BGPRoute.self() + val sender = BGPNode(id = 1) + + val extendedRoute = PeerplusExtender.extend(route, sender) + + it("returns a peer+ route with the AS-PATH containing the sender") { + assertThat(extendedRoute, + Is(peerplusRoute(asPath = pathOf(sender)))) + } + } + + on("extending an invalid route") { + + val route = BGPRoute.invalid() + val sender = BGPNode(id = 1) + + val extendedRoute = PeerplusExtender.extend(route, sender) + + it("returns an invalid route") { + assertThat(extendedRoute, + Is(BGPRoute.invalid())) + } + } + + on("extending a customer route with 1 sibling hop") { + + val route = customerRoute(siblingHops = 1, asPath = emptyPath()) + val sender = BGPNode(id = 1) + + val extendedRoute = PeerplusExtender.extend(route, sender) + + it("returns a peer+ route with 0 sibling hops") { + assertThat(extendedRoute, + Is(peerplusRoute(siblingHops = 0, asPath = pathOf(sender)))) + } + } + + on("extending a peer+ route with 1 sibling hop") { + + val route = peerplusRoute(siblingHops = 1, asPath = emptyPath()) + val sender = BGPNode(id = 1) + + val extendedRoute = PeerplusExtender.extend(route, sender) + + it("returns a peer+ route with 0 sibling hops") { + assertThat(extendedRoute, + Is(peerplusRoute(siblingHops = 0, asPath = pathOf(sender)))) + } + } + + on("extending a peer route with 1 sibling hop") { + + val route = peerRoute(siblingHops = 1, asPath = emptyPath()) + val sender = BGPNode(id = 1) + + val extendedRoute = PeerplusExtender.extend(route, sender) + + it("returns an invalid route") { + assertThat(extendedRoute, + Is(BGPRoute.invalid())) + } + } + + on("extending a provider route with 1 sibling hop") { + + val route = providerRoute(siblingHops = 1, asPath = emptyPath()) + val sender = BGPNode(id = 1) + + val extendedRoute = PeerplusExtender.extend(route, sender) + + it("returns an invalid route") { + assertThat(extendedRoute, + Is(BGPRoute.invalid())) + } + } + } + + given("a sibling extender") { + + on("extending a customer route with 0 sibling hops and with an empty AS-PATH") { + + val route = customerRoute(asPath = emptyPath()) + val sender = BGPNode(id = 1) + + val extendedRoute = SiblingExtender.extend(route, sender) + + it("returns a customer route with 1 sibling hop and with AS-PATH containing the sender") { + assertThat(extendedRoute, + Is(customerRoute(siblingHops = 1, asPath = pathOf(sender)))) + } + } + + on("extending a customer route with 0 sibling hops and with an AS-PATH containing node 2") { + + val route = customerRoute(asPath = pathOf(BGPNode(id = 2))) + val sender = BGPNode(id = 1) + + val extendedRoute = SiblingExtender.extend(route, sender) + + it("returns a customer route with 1 sibling hop and with AS-PATH containing node 2 and the sender") { + assertThat(extendedRoute, + Is(customerRoute(siblingHops = 1, asPath = pathOf(BGPNode(2), sender)))) + } + } + + on("extending a self route") { + + val route = BGPRoute.self() + val sender = BGPNode(id = 1) + + val extendedRoute = SiblingExtender.extend(route, sender) + + it("returns a customer route with 1 sibling hop and with AS-PATH containing the sender") { + assertThat(extendedRoute, + Is(customerRoute(siblingHops = 1, asPath = pathOf(sender)))) + } + } + + on("extending an invalid route") { + + val route = BGPRoute.invalid() + val sender = BGPNode(id = 1) + + val extendedRoute = SiblingExtender.extend(route, sender) + + it("returns an invalid route") { + assertThat(extendedRoute, + Is(BGPRoute.invalid())) + } + } + + on("extending a peer+ route") { + + val route = peerplusRoute(asPath = emptyPath()) + val sender = BGPNode(id = 1) + + val extendedRoute = SiblingExtender.extend(route, sender) + + it("returns a peer+ route with 1 sibling hop and with AS-PATH containing the sender") { + assertThat(extendedRoute, + Is(peerplusRoute(siblingHops = 1, asPath = pathOf(sender)))) + } + } + + on("extending a peer route") { + + val route = peerRoute(asPath = emptyPath()) + val sender = BGPNode(id = 1) + + val extendedRoute = SiblingExtender.extend(route, sender) + + it("returns a peer route with 1 sibling hop and with AS-PATH containing the sender") { + assertThat(extendedRoute, + Is(peerRoute(siblingHops = 1, asPath = pathOf(sender)))) + } + } + + on("extending a provider route") { + + val route = providerRoute(asPath = emptyPath()) + val sender = BGPNode(id = 1) + + val extendedRoute = SiblingExtender.extend(route, sender) + + it("returns a provider route with 1 sibling hop and with AS-PATH containing the sender") { + assertThat(extendedRoute, + Is(providerRoute(siblingHops = 1, asPath = pathOf(sender)))) + } + } + } + +}) diff --git a/src/test/kotlin/bgp/policies/shortestpath/ShortestPathExtenderTest.kt b/src/test/kotlin/bgp/policies/shortestpath/ShortestPathExtenderTest.kt new file mode 100644 index 0000000..676dafc --- /dev/null +++ b/src/test/kotlin/bgp/policies/shortestpath/ShortestPathExtenderTest.kt @@ -0,0 +1,115 @@ +package bgp.policies.shortestpath + +import bgp.BGPRoute +import core.routing.emptyPath +import core.routing.pathOf +import org.jetbrains.spek.api.Spek +import org.jetbrains.spek.api.dsl.given +import org.jetbrains.spek.api.dsl.it +import org.jetbrains.spek.api.dsl.on +import org.hamcrest.Matchers.* +import org.hamcrest.MatcherAssert.assertThat +import testing.bgp.BGPNode + +/** + * Created on 24-07-2017. + + * @author David Fialho + */ +object ShortestPathExtenderTest : Spek({ + + given("a route with LOCAL-PREF = 5 and an empty AS-PATH sent by node 1234") { + + val route = BGPRoute.with(localPref = 5, asPath = emptyPath()) + val sender = BGPNode(id = 1234) + + on("extending through a link of cost 5") { + + val extendedRoute = ShortestPathExtender(cost = 5).extend(route, sender) + + it("returns a route with LOCAL-PREF = 10") { + assertThat(extendedRoute.localPref, `is`(10)) + } + + it("returns a route with an AS-PATH = [1234]") { + assertThat(extendedRoute.asPath, `is`(pathOf(sender))) + } + + } + + on("extending through a link of cost 15") { + + val extendedRoute = ShortestPathExtender(cost = 15).extend(route, sender) + + it("returns a route with LOCAL-PREF = 20") { + assertThat(extendedRoute.localPref, `is`(20)) + } + + it("returns a route with an AS-PATH = [1234]") { + assertThat(extendedRoute.asPath, `is`(pathOf(sender))) + } + + } + + } + + given("a route with LOCAL-PREF = 5 and an AS-PATH = [1, 2] sent by node 1234") { + + val route = BGPRoute.with(localPref = 5, asPath = pathOf(BGPNode(id = 1), BGPNode(id = 2))) + val sender = BGPNode(id = 1234) + + on("extending through a link of cost 15") { + + val extendedRoute = ShortestPathExtender(cost = 15).extend(route, sender) + + it("returns a route with LOCAL-PREF = 20") { + assertThat(extendedRoute.localPref, `is`(20)) + } + + it("returns a route with an AS-PATH = [1, 2, 1234]") { + assertThat(extendedRoute.asPath, `is`(pathOf(BGPNode(1), BGPNode(2), sender))) + } + + } + + } + + given("an invalid route sent by node 1234") { + + val route = BGPRoute.invalid() + val sender = BGPNode(id = 1234) + + on("extending through a link of cost 15") { + + val extendedRoute = ShortestPathExtender(cost = 15).extend(route, sender) + + it("returns an invalid route") { + assertThat(extendedRoute.isValid(), `is`(false)) + } + + } + + } + + given("a self route sent by node 1234 ") { + + val route = BGPRoute.self() + val sender = BGPNode(id = 1234) + + on("extending through a link of cost 5") { + + val extendedRoute = ShortestPathExtender(cost = 5).extend(route, sender) + + it("returns a route with LOCAL-PREF = 5") { + assertThat(extendedRoute.localPref, `is`(5)) + } + + it("returns a route with AS-PATH = [1234]") { + assertThat(extendedRoute.asPath, `is`(pathOf(sender))) + } + + } + + } + +}) \ No newline at end of file diff --git a/src/test/kotlin/core/routing/PathTests.kt b/src/test/kotlin/core/routing/PathTests.kt new file mode 100644 index 0000000..ff9718d --- /dev/null +++ b/src/test/kotlin/core/routing/PathTests.kt @@ -0,0 +1,302 @@ +package core.routing + +import testing.node +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.`is` as Is +import org.hamcrest.Matchers.* +import org.jetbrains.spek.api.Spek +import org.jetbrains.spek.api.dsl.context +import org.jetbrains.spek.api.dsl.given +import org.jetbrains.spek.api.dsl.it +import org.jetbrains.spek.api.dsl.on + + +/** + * Created on 21-07-2017 + + * @author David Fialho + */ +object PathTests : Spek({ + + given("a path with nodes 1 and 2") { + + val path = pathOf(node(1), node(2)) + + it("contains node 1") { + assertThat(node(1) in path, + Is(true)) + } + + it("contains node 2") { + assertThat(node(2) in path, + Is(true)) + } + + it("does not contain node 3") { + assertThat(node(3) in path, + Is(false)) + } + + } + + given("an empty path") { + + val emptyPath = emptyPath() + + it("has size 0") { + assertThat(emptyPath.size, equalTo(0)) + } + + on("appending a new node") { + + val appendedPath = emptyPath.append(node(1)) + + it("returns a path of size 1") { + assertThat(appendedPath.size, equalTo(1)) + } + + it("returns a path containing that new node") { + assertThat(node(1) in appendedPath, + Is(true)) + } + + it("keeps the original path empty") { + assertThat(emptyPath.size, equalTo(0)) + } + + } + + } + + given("a path with 1 node") { + + val path = pathOf(node(1)) + + it("has size 1") { + assertThat(path.size, equalTo(1)) + } + + on("appending a new node") { + + val appendedPath = path.append(node(2)) + + it("returns a path of size 2") { + assertThat(appendedPath.size, equalTo(2)) + } + + it("returns a path containing that previous node and the new node") { + assertThat(node(1) in appendedPath, + Is(true)) + assertThat(node(2) in appendedPath, + Is(true)) + } + + it("keeps the original path with size 1") { + assertThat(path.size, equalTo(1)) + } + } + + on("copying the node") { + + val pathCopy = path.copy() + + it("returns a new path instance") { + assertThat(path !== pathCopy, + Is(true)) + } + + it("returns a path equal to the initial path") { + assertThat(pathCopy, equalTo(path)) + } + + it("returns a path equal to the initial path") { + assertThat(pathCopy, equalTo(path)) + } + } + + } + + given("two paths with the same size and the same nodes in the exact same order") { + + val path1 = pathOf(node(1), node(2)) + val path2 = pathOf(node(1), node(2)) + + it("is true that the two are equal") { + assertThat(path1, equalTo(path2)) + } + + } + + given("two paths with different sizes and matching nodes") { + + val path1 = pathOf(node(1), node(2), node(3)) + val path2 = pathOf(node(1), node(2)) + + it("is true that the two are different") { + assertThat(path1, not(equalTo(path2))) + } + + } + + given("two paths with the same size but different nodes") { + + val path1 = pathOf(node(1), node(2)) + val path2 = pathOf(node(2), node(3)) + + it("is true that the two are different") { + assertThat(path1, not(equalTo(path2))) + } + + } + + given("two paths with the same size and the same nodes in different order") { + + val path1 = pathOf(node(1), node(2)) + val path2 = pathOf(node(2), node(1)) + + it("is true that the two are different") { + assertThat(path1, not(equalTo(path2))) + } + + } + + context("obtaining the sub-path before some node") { + + given("path is empty") { + + val path = emptyPath() + + on("obtaining sub-path before some node") { + + val subPath = path.subPathBefore(node(1)) + + it("returns empty path") { + assertThat(subPath, + Is(emptyPath())) + } + } + } + + given("path contains only node 1") { + + val path = pathOf(node(1)) + + on("obtaining sub-path before node 1") { + + val subPath = path.subPathBefore(node(1)) + + it("returns empty path") { + assertThat(subPath, + Is(emptyPath())) + } + } + + on("obtaining sub-path before node 2") { + + val subPath = path.subPathBefore(node(2)) + + it("returns original path with node 1") { + assertThat(subPath, + Is(path)) + } + } + } + + given("path with nodes 1, 2, 3, 4, and 5") { + + val path = pathOf(node(1), node(2), node(3), node(4), node(5)) + + on("obtaining sub-path before node 1") { + + val subPath = path.subPathBefore(node(1)) + + it("returns empty path") { + assertThat(subPath, + Is(emptyPath())) + } + } + + on("obtaining sub-path before node 2") { + + val subPath = path.subPathBefore(node(2)) + + it("returns path with node 1") { + assertThat(subPath, + Is(pathOf(node(1)))) + } + } + + on("obtaining sub-path before node 3") { + + val subPath = path.subPathBefore(node(3)) + + it("returns path with nodes 1 and 2") { + assertThat(subPath, + Is(pathOf(node(1), node(2)))) + } + } + + on("obtaining sub-path before node 4") { + + val subPath = path.subPathBefore(node(4)) + + it("returns path with nodes 1, 2, and 3") { + assertThat(subPath, + Is(pathOf(node(1), node(2), node(3)))) + } + } + + on("obtaining sub-path before node 5") { + + val subPath = path.subPathBefore(node(5)) + + it("returns path with nodes 1, 2, 3, and 4") { + assertThat(subPath, + Is(pathOf(node(1), node(2), node(3), node(4)))) + } + } + + on("obtaining sub-path before node 6") { + + val subPath = path.subPathBefore(node(6)) + + it("returns original path with nodes 1, 2, 3, 4, and 5") { + assertThat(subPath, + Is(pathOf(node(1), node(2), node(3), node(4), node(5)))) + } + } + } + + given("path contains nodes 1 and 1") { + + val path = pathOf(node(1), node(1)) + + on("obtaining sub-path before node 1") { + + val subPath = path.subPathBefore(node(1)) + + it("returns empty path") { + assertThat(subPath, + Is(emptyPath())) + } + } + } + + given("path contains nodes 2, 1, 3, 1") { + + val path = pathOf(node(2), node(1), node(3), node(1)) + + on("obtaining sub-path before node 1") { + + val subPath = path.subPathBefore(node(1)) + + it("returns path with node 2") { + assertThat(subPath, + Is(pathOf(node(2)))) + } + } + } + } + + +}) \ No newline at end of file diff --git a/src/test/kotlin/core/routing/RouteSelectorTests.kt b/src/test/kotlin/core/routing/RouteSelectorTests.kt new file mode 100644 index 0000000..1db3ddf --- /dev/null +++ b/src/test/kotlin/core/routing/RouteSelectorTests.kt @@ -0,0 +1,472 @@ +package core.routing + +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.* +import org.hamcrest.Matchers.`is` as Is +import org.jetbrains.spek.api.Spek +import org.jetbrains.spek.api.dsl.context +import org.jetbrains.spek.api.dsl.given +import org.jetbrains.spek.api.dsl.it +import org.jetbrains.spek.api.dsl.on +import testing.* + +/** + * Created on 21-07-2017 + + * @author David Fialho + */ +object RouteSelectorTests : Spek({ + + //region Helper methods + + /** + * Returns a route selector for testing based on fake nodes and fake routes. + */ + fun routeSelector(table: RoutingTable): RouteSelector { + return RouteSelector.wrap(table, ::fakeCompare) + } + + //endregion + + given("a route selector using a table with no entries") { + + val selector = routeSelector(RoutingTable.empty(invalidRoute())) + + it("selects an invalid route") { + assertThat(selector.getSelectedRoute(), Is(invalidRoute())) + } + + it("selects a null neighbor") { + assertThat(selector.getSelectedNeighbor(), Is(nullValue())) + } + + on("updating the route of some neighbor with ID 1 to route with preference 10") { + + val updated = selector.update(node(1), route(preference = 10)) + + it("selects route with preference 10") { + assertThat(selector.getSelectedRoute(), Is(route(preference = 10))) + } + + it("selects neighbor 1") { + assertThat(selector.getSelectedNeighbor(), Is(node(1))) + } + + it("indicates the selected route/neighbor was updated") { + assertThat(updated, Is(true)) + } + } + } + + given("a route selector using a table containing a route with preference 10 via a neighbor with ID 1") { + + val selector = routeSelector(RoutingTable.of(invalidRoute(), + route(preference = 10) via node(1) + )) + + it("selects route with preference 10") { + assertThat(selector.getSelectedRoute(), Is(route(preference = 10))) + } + + it("selects neighbor with ID 1") { + assertThat(selector.getSelectedNeighbor(), Is(node(1))) + } + + on("updating the route neighbor with ID 1 to route with preference 15") { + + val updated = selector.update(node(1), route(preference = 15)) + + it("selects route with preference 15") { + assertThat(selector.getSelectedRoute(), Is(route(preference = 15))) + } + + it("selects neighbor with ID 1") { + assertThat(selector.getSelectedNeighbor(), Is(node(1))) + } + + it("indicates the selected route/neighbor was updated") { + assertThat(updated, Is(true)) + } + } + + on("updating the route of neighbor with ID 1 to invalid route") { + + val updated = selector.update(node(1), invalidRoute()) + + it("selects route invalid route") { + assertThat(selector.getSelectedRoute(), Is(invalidRoute())) + } + + it("selects null neighbor") { + assertThat(selector.getSelectedNeighbor(), Is(nullValue())) + } + + it("indicates the selected route/neighbor was updated") { + assertThat(updated, Is(true)) + } + } + + on("updating the route of neighbor with ID 1 to invalid route again") { + + val updated = selector.update(node(1), invalidRoute()) + + it("selects route invalid route") { + assertThat(selector.getSelectedRoute(), Is(invalidRoute())) + } + + it("selects null neighbor") { + assertThat(selector.getSelectedNeighbor(), Is(nullValue())) + } + + it("indicates the selected route/neighbor was NOT updated") { + assertThat(updated, Is(false)) + } + } + + } + + given("a route selector using a table containing invalid routes via neighbors 1 and 2") { + + val selector = routeSelector(RoutingTable.of(invalidRoute(), + invalidRoute() via node(1), + invalidRoute() via node(2) + )) + + it("selects an invalid route") { + assertThat(selector.getSelectedRoute(), Is(invalidRoute())) + } + + it("selects a neighbor that is null") { + assertThat(selector.getSelectedNeighbor(), Is(nullValue())) + } + + on("updating the route of neighbor 1 to route with preference 10") { + + val updated = selector.update(node(1), route(preference = 10)) + + it("selects route with preference 10") { + assertThat(selector.getSelectedRoute(), Is(route(preference = 10))) + } + + it("selects neighbor 1") { + assertThat(selector.getSelectedNeighbor(), Is(node(1))) + } + + it("indicates the selected route/neighbor was updated") { + assertThat(updated, Is(true)) + } + + } + + on("updating the route of neighbor 1 to route with preference 5") { + + val updated = selector.update(node(1), route(preference = 5)) + + it("selects route with preference 5") { + assertThat(selector.getSelectedRoute(), Is(route(preference = 5))) + } + + it("selects neighbor 1") { + assertThat(selector.getSelectedNeighbor(), Is(node(1))) + } + + it("indicates the selected route/neighbor was updated") { + assertThat(updated, Is(true)) + } + + } + + on("updating the route of neighbor 2 to route with preference 15") { + + val updated = selector.update(node(2), route(preference = 15)) + + it("selects route with preference 15") { + assertThat(selector.getSelectedRoute(), Is(route(preference = 15))) + } + + it("selects neighbor 2") { + assertThat(selector.getSelectedNeighbor(), Is(node(2))) + } + + it("indicates the selected route/neighbor was updated") { + assertThat(updated, Is(true)) + } + + } + + on("updating the route of neighbor 2 to route with preference 1") { + + val updated = selector.update(node(2), route(preference = 1)) + + it("selects route with preference 5") { + assertThat(selector.getSelectedRoute(), Is(route(preference = 5))) + } + + it("selects neighbor 1") { + assertThat(selector.getSelectedNeighbor(), Is(node(1))) + } + + it("indicates the selected route/neighbor was updated") { + assertThat(updated, Is(true)) + } + + } + + on("updating the route of neighbor 2 to route with preference 3") { + + val updated = selector.update(node(2), route(preference = 3)) + + it("selects route with preference 5") { + assertThat(selector.getSelectedRoute(), Is(route(preference = 5))) + } + + it("selects neighbor 1") { + assertThat(selector.getSelectedNeighbor(), Is(node(1))) + } + + it("indicates the selected route/neighbor was NOT updated") { + assertThat(updated, Is(false)) + } + + } + } + + given("a route selector using a table containing valid routes via neighbors 1 and 2 and selecting route via 1") { + + val selector = routeSelector(RoutingTable.of(invalidRoute(), + route(preference = 10) via node(1), + route(preference = 5) via node(2) + )) + + on("disabling neighbor 1") { + + val updated = selector.disable(node(1)) + + it("indicates the selected route/neighbor was updated") { + assertThat(updated, Is(true)) + } + + it("selects route via neighbor 2") { + assertThat(selector.getSelectedRoute(), Is(route(preference = 5))) + } + + it("selects neighbor 2") { + assertThat(selector.getSelectedNeighbor(), Is(node(2))) + } + + } + + on("updating route via neighbor 1 to a route with preference 15") { + + val updated = selector.update(node(1), route(preference = 15)) + + it("indicates the selected route/neighbor was NOT updated") { + assertThat(updated, Is(false)) + } + + it("selects route via neighbor 2") { + assertThat(selector.getSelectedRoute(), Is(route(preference = 5))) + } + + it("selects neighbor 2") { + assertThat(selector.getSelectedNeighbor(), Is(node(2))) + } + + } + + on("enabling neighbor 1") { + + val updated = selector.enable(node(1)) + + it("indicates the selected route/neighbor was updated") { + assertThat(updated, Is(true)) + } + + it("selects route with preference 15") { + assertThat(selector.getSelectedRoute(), Is(route(preference = 15))) + } + + it("selects neighbor 1") { + assertThat(selector.getSelectedNeighbor(), Is(node(1))) + } + + } + + on("disabling neighbor 2") { + + val updated = selector.disable(node(2)) + + it("indicates the selected route/neighbor was NOT updated") { + assertThat(updated, Is(false)) + } + + it("selects route with preference 15") { + assertThat(selector.getSelectedRoute(), Is(route(preference = 15))) + } + + it("selects neighbor 1") { + assertThat(selector.getSelectedNeighbor(), Is(node(1))) + } + + } + + on("enabling neighbor 2") { + + val updated = selector.enable(node(2)) + + it("indicates the selected route/neighbor was NOT updated") { + assertThat(updated, Is(false)) + } + + it("selects route with preference 15") { + assertThat(selector.getSelectedRoute(), Is(route(preference = 15))) + } + + it("selects neighbor 1") { + assertThat(selector.getSelectedNeighbor(), Is(node(1))) + } + + } + + on("disabling neighbors 1 and 2") { + + selector.disable(node(1)) + selector.disable(node(2)) + + it("selects invalid route") { + assertThat(selector.getSelectedRoute(), Is(invalidRoute())) + } + + it("selects null neighbor") { + assertThat(selector.getSelectedNeighbor(), Is(nullValue())) + } + + } + + } + + given("a route selector wrapping a table with valid routes via neighbors 1 and 2 is cleared") { + + val selector = routeSelector(RoutingTable.of(invalidRoute(), + route(preference = 10) via node(1), + route(preference = 5) via node(2) + )) + + on("clearing selector") { + + selector.clear() + + it("selects an invalid route") { + assertThat(selector.getSelectedRoute(), Is(invalidRoute())) + } + + it("selects a null neighbor") { + assertThat(selector.getSelectedNeighbor(), Is(nullValue())) + } + } + + on("updating neighbor 2 to route with preference 8") { + + selector.update(node(2), route(preference = 8)) + + it("selects a route with preference 8") { + assertThat(selector.getSelectedRoute(), Is(route(preference = 8))) + } + + it("selects neighbor 2") { + assertThat(selector.getSelectedNeighbor(), Is(node(2))) + } + } + + on("updating neighbor 2 to route with preference 15") { + + selector.update(node(2), route(preference = 15)) + + it("selects a route with preference 15") { + assertThat(selector.getSelectedRoute(), Is(route(preference = 15))) + } + + it("selects neighbor 2") { + assertThat(selector.getSelectedNeighbor(), Is(node(2))) + } + } + + on("updating neighbor 2 to route with preference 5") { + // this will for the selector to reselect: if the table was not cleared it will select route(10) via node 1 + + selector.update(node(2), route(preference = 5)) + + it("selects a route with preference 5") { + assertThat(selector.getSelectedRoute(), Is(route(preference = 5))) + } + + it("selects neighbor 2") { + assertThat(selector.getSelectedNeighbor(), Is(node(2))) + } + } + } + + context("a route selector wrapping a table with 4 entries and neighbors 1 and 3 are disabled") { + + given("all nodes have valid routes and the most preferred route is via node 3") { + + val selector = routeSelector(RoutingTable.of(invalidRoute(), + route(preference = 15) via node(1), + route(preference = 5) via node(2), + route(preference = 30) via node(3), + route(preference = 10) via node(4) + )) + + selector.disable(node(1)) + selector.disable(node(3)) + + on("enabling all neighbors") { + + val updated = selector.enableAll() + + it("selects a route with preference 30") { + assertThat(selector.getSelectedRoute(), Is(route(preference = 30))) + } + + it("selects neighbor 3") { + assertThat(selector.getSelectedNeighbor(), Is(node(3))) + } + + it("indicates the selected route/neighbor was updated") { + assertThat(updated, Is(true)) + } + } + } + + given("all nodes have valid routes and the most preferred route is via node 2") { + + val selector = routeSelector(RoutingTable.of(invalidRoute(), + route(preference = 15) via node(1), + route(preference = 50) via node(2), + route(preference = 30) via node(3), + route(preference = 10) via node(4) + )) + + selector.disable(node(1)) + selector.disable(node(3)) + + on("enabling all neighbors") { + + val updated = selector.enableAll() + + it("selects a route with preference 50") { + assertThat(selector.getSelectedRoute(), Is(route(preference = 50))) + } + + it("selects neighbor 2") { + assertThat(selector.getSelectedNeighbor(), Is(node(2))) + } + + it("indicates the selected route/neighbor was NOT updated") { + assertThat(updated, Is(false)) + } + } + } + } + + }) \ No newline at end of file diff --git a/src/test/kotlin/core/routing/RoutingTableTest.kt b/src/test/kotlin/core/routing/RoutingTableTest.kt new file mode 100644 index 0000000..681f35a --- /dev/null +++ b/src/test/kotlin/core/routing/RoutingTableTest.kt @@ -0,0 +1,199 @@ +package core.routing + +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.`is` as Is +import org.jetbrains.spek.api.Spek +import org.jetbrains.spek.api.dsl.given +import org.jetbrains.spek.api.dsl.it +import org.jetbrains.spek.api.dsl.on +import testing.invalidRoute +import testing.via +import testing.route +import testing.node + +/** + * Created on 21-07-2017 + + * @author David Fialho + */ +object RoutingTableTest: Spek({ + + given("an empty routing table") { + + val table = RoutingTable.empty(invalidRoute()) + + on("getting the route for any neighbor") { + + val neighborRoute = table[node(1)] + + it("returns an invalid route") { + assertThat(neighborRoute, Is(invalidRoute())) + } + } + + on("disabling neighbor with ID 1") { + + val neighbor = node(1) + table.setEnabled(neighbor, false) + + it("indicates neighbor 1 is disabled") { + assertThat(table.isEnabled(neighbor), + Is(false)) + } + } + + } + + given("a routing table with invalid route via neighbor with ID 1") { + + val table = RoutingTable.of(invalidRoute(), + invalidRoute() via node(1) + ) + + on("getting the route for neighbor 1") { + + val neighborRoute = table[node(1)] + + it("returns an invalid route") { + assertThat(neighborRoute, Is(invalidRoute())) + } + } + + on("setting route with preference 10 for neighbor 1") { + + table[node(1)] = route(preference = 10) + + it("returns a route with preference 10 when getting the route for neighbor 1") { + assertThat(table[node(1)], Is(route(preference = 10))) + } + } + + } + + given("a routing table with two invalid routes via neighbors 1 and 2") { + + val table = RoutingTable.of(invalidRoute(), + invalidRoute() via node(1), + invalidRoute() via node(2) + ) + + on("setting route with preference 10 for neighbor 1") { + + table[node(1)] = route(preference = 10) + + it("returns a valid route with preference 10 when getting the route for neighbor 1") { + assertThat(table[node(1)], Is(route(preference = 10))) + } + + it("returns an invalid route when getting the route for neighbor 2") { + assertThat(table[node(2)], Is(invalidRoute())) + } + } + + on("setting a route with preference 10 for neighbor not yet included in the table") { + + table[node(5)] = route(preference = 10) + + it("returns the route with preference 10") { + // returning an invalid route indicates the neighbor was not added to the table + assertThat(table[node(5)], Is(route(preference = 10))) + } + } + + on("clearing the table") { + table.clear() + + it("returns an invalid route when getting the route for neighbor 1") { + assertThat(table[node(1)], Is(invalidRoute())) + } + + it("returns an invalid route when getting the route for neighbor 2") { + assertThat(table[node(2)], Is(invalidRoute())) + } + } + + on("setting route with preference 10 to neighbor 1") { + + table[node(1)] = route(preference = 10) + + it("returns a valid route with preference 10 when getting the route for neighbor 1") { + // this indicates the neighbors were not removed from the table when the table was cleared + assertThat(table[node(1)], Is(route(preference = 10))) + } + } + + } + + given("routing table containing valid routes via 4 neighbors: only nodes 1 and 3 are disabled") { + + val table = RoutingTable.of(invalidRoute(), + route(preference = 10) via node(1), + route(preference = 5) via node(2), + route(preference = 15) via node(3), + route(preference = 30) via node(4) + ) + + table.setEnabled(node(1), false) + table.setEnabled(node(3), false) + + on("enabling node 1") { + + val route = table.setEnabled(node(1), true) + + it("enabled node 1") { + assertThat(table.isEnabled(node(1)), Is(true)) + } + + it("kept node 2 enabled") { + assertThat(table.isEnabled(node(2)), Is(true)) + } + + it("kept node 3 disabled") { + assertThat(table.isEnabled(node(3)), Is(false)) + } + + it("kept node 4 enabled") { + assertThat(table.isEnabled(node(4)), Is(true)) + } + + it("returns the candidate route via node 1") { + assertThat(route, Is(route(preference = 10))) + } + } + + on("enabling node 3") { + + val route = table.setEnabled(node(3), true) + + it("kept node 1 enabled") { + assertThat(table.isEnabled(node(1)), Is(true)) + } + + it("kept node 2 enabled") { + assertThat(table.isEnabled(node(2)), Is(true)) + } + + it("enabled node 3") { + assertThat(table.isEnabled(node(3)), Is(true)) + } + + it("kept node 4 enabled") { + assertThat(table.isEnabled(node(4)), Is(true)) + } + + it("returns the candidate route via node 3") { + assertThat(route, Is(route(preference = 15))) + } + } + + on("enabling node 10") { + + val route = table.setEnabled(node(10), true) + + it("returns invalid route") { + assertThat(route, Is(invalidRoute())) + } + } + } + +}) \ No newline at end of file diff --git a/src/test/kotlin/core/simulator/BGPWithInterdomainRoutingTests.kt b/src/test/kotlin/core/simulator/BGPWithInterdomainRoutingTests.kt new file mode 100644 index 0000000..26eaedf --- /dev/null +++ b/src/test/kotlin/core/simulator/BGPWithInterdomainRoutingTests.kt @@ -0,0 +1,239 @@ +package core.simulator + +import bgp.BGP +import bgp.BGPRoute +import bgp.policies.interdomain.customerRoute +import bgp.policies.interdomain.peerplusRoute +import bgp.policies.interdomain.providerRoute +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.`is` as Is +import org.jetbrains.spek.api.Spek +import org.jetbrains.spek.api.dsl.given +import org.jetbrains.spek.api.dsl.it +import org.jetbrains.spek.api.dsl.on +import testing.* +import testing.bgp.pathOf + +/** + * Created on 26-07-2017. + * + * @author David Fialho + */ +object BGPWithInterdomainRoutingTests : Spek({ + + given("topology with a single customer link from 1 to 0") { + + val topology = bgpTopology { + node { 0 deploying BGP() } + node { 1 deploying BGP() } + + customerLink { 1 to 0 } + } + + afterEachTest { + Engine.scheduler.reset() + topology.nodes.forEach { it.protocol.reset() } + } + + val node = topology.nodes.sortedBy { it.id } + val protocol = node.map { it.protocol as BGP } + + on("simulating with node 0 as the destination") { + + val terminated = Engine.simulate(topology, node[0], threshold = 1000) + + it("terminated") { + assertThat(terminated, Is(true)) + } + + it("finishes with node 0 selecting self route") { + assertThat(protocol[0].routingTable.getSelectedRoute(), + Is(BGPRoute.self())) + } + + it("finishes with node 1 selecting customer via node 0") { + assertThat(protocol[1].routingTable.getSelectedRoute(), + Is(customerRoute(asPath = pathOf(0)))) + } + } + + on("simulating with node 1 as the destination") { + + val terminated = Engine.simulate(topology, node[1], threshold = 1000) + + it("terminated") { + assertThat(terminated, Is(true)) + } + + it("finishes with node 0 selecting an invalid route") { + assertThat(protocol[0].routingTable.getSelectedRoute(), + Is(BGPRoute.invalid())) + } + + it("finishes with node 1 selecting self route") { + assertThat(protocol[1].routingTable.getSelectedRoute(), + Is(BGPRoute.self())) + } + } + } + + given("square topology") { + + val topology = bgpTopology { + node { 0 deploying BGP() } + node { 1 deploying BGP() } + node { 2 deploying BGP() } + node { 3 deploying BGP() } + + customerLink { 1 to 0 } + peerLink { 1 to 2 } + peerplusLink { 2 to 1 } + providerLink { 3 to 2 } + } + + afterEachTest { + Engine.scheduler.reset() + topology.nodes.forEach { it.protocol.reset() } + } + + val node = topology.nodes.sortedBy { it.id } + val protocol = node.map { it.protocol as BGP } + + on("simulating with node 0 as the destination") { + + val terminated = Engine.simulate(topology, node[0], threshold = 1000) + + it("terminated") { + assertThat(terminated, Is(true)) + } + + it("finishes with node 1 selecting customer route via node 0") { + assertThat(protocol[1].routingTable.getSelectedRoute(), + Is(customerRoute(asPath = pathOf(0)))) + } + + it("finishes with node 2 selecting peer+ route via node 1") { + assertThat(protocol[2].routingTable.getSelectedRoute(), + Is(peerplusRoute(asPath = pathOf(0, 1)))) + } + + it("finishes with node 3 selecting provider route via node 2") { + assertThat(protocol[3].routingTable.getSelectedRoute(), + Is(providerRoute(asPath = pathOf(0, 1, 2)))) + } + } + } + + given("loop topology with customer to destination and peer+ around the cycle") { + + val topology = bgpTopology { + node { 0 deploying BGP() } + node { 1 deploying BGP() } + node { 2 deploying BGP() } + node { 3 deploying BGP() } + + customerLink { 1 to 0 } + customerLink { 2 to 0 } + customerLink { 3 to 0 } + peerplusLink { 1 to 2 } + peerplusLink { 2 to 3 } + peerplusLink { 3 to 1 } + } + + afterEachTest { + Engine.scheduler.reset() + topology.nodes.forEach { it.protocol.reset() } + } + + val node = topology.nodes.sortedBy { it.id } + + on("simulating with node 0 as the destination") { + + val terminated = Engine.simulate(topology, node[0], threshold = 1000) + + it("does NOT terminate") { + assertThat(terminated, Is(false)) + } + } + } + + given("topology without cycles and with siblings") { + + val topology = bgpTopology { + node { 0 deploying BGP() } + node { 1 deploying BGP() } + node { 2 deploying BGP() } + node { 3 deploying BGP() } + + siblingLink { 1 to 0 } + siblingLink { 2 to 1 } + peerplusLink { 3 to 1 } + customerLink { 3 to 2 } + } + + afterEachTest { + Engine.scheduler.reset() + topology.nodes.forEach { it.protocol.reset() } + } + + val node = topology.nodes.sortedBy { it.id } + val protocol = node.map { it.protocol as BGP } + + on("simulating with node 0 as the destination") { + + val terminated = Engine.simulate(topology, node[0], threshold = 1000) + + it("terminates") { + assertThat(terminated, Is(true)) + } + + it("finishes with node 1 selecting customer route with 1 sibling hop via node 0") { + assertThat(protocol[1].routingTable.getSelectedRoute(), + Is(customerRoute(siblingHops = 1, asPath = pathOf(0)))) + } + + it("finishes with node 2 selecting customer route with 2 sibling hops via node 1") { + assertThat(protocol[2].routingTable.getSelectedRoute(), + Is(customerRoute(siblingHops = 2, asPath = pathOf(0, 1)))) + } + + it("finishes with node 3 selecting peer+ route with 0 sibling hops via node 1") { + assertThat(protocol[3].routingTable.getSelectedRoute(), + Is(peerplusRoute(siblingHops = 0, asPath = pathOf(0, 1)))) + } + } + } + + given("topology with non-absorbent cycle and with siblings") { + + val topology = bgpTopology { + node { 0 deploying BGP() } + node { 1 deploying BGP() } + node { 2 deploying BGP() } + node { 3 deploying BGP() } + + siblingLink { 1 to 0 } + siblingLink { 2 to 0 } + customerLink { 1 to 2 } + customerLink { 2 to 3 } + siblingLink { 3 to 1 } + } + + afterEachTest { + Engine.scheduler.reset() + topology.nodes.forEach { it.protocol.reset() } + } + + val node = topology.nodes.sortedBy { it.id } + + on("simulating with node 0 as the destination") { + + val terminated = Engine.simulate(topology, node[0], threshold = 1000) + + it("does NOT terminate") { + assertThat(terminated, Is(false)) + } + } + } + +}) \ No newline at end of file diff --git a/src/test/kotlin/core/simulator/BGPWithShortestPathRoutingTests.kt b/src/test/kotlin/core/simulator/BGPWithShortestPathRoutingTests.kt new file mode 100644 index 0000000..ca4264d --- /dev/null +++ b/src/test/kotlin/core/simulator/BGPWithShortestPathRoutingTests.kt @@ -0,0 +1,293 @@ +package core.simulator + +import bgp.BGP +import bgp.BGPRoute +import core.routing.pathOf +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.`is` as Is +import org.hamcrest.Matchers.* +import org.jetbrains.spek.api.Spek +import org.jetbrains.spek.api.dsl.given +import org.jetbrains.spek.api.dsl.it +import org.jetbrains.spek.api.dsl.on +import testing.* + +/** + * Created on 26-07-2017. + * + * @author David Fialho + */ +object BGPWithShortestPathRoutingTests : Spek({ + + given("topology with a single link from 2 to 1 with cost 10") { + + val topology = bgpTopology { + node { 1 deploying BGP() } + node { 2 deploying BGP() } + + link { 2 to 1 withCost 10 } + } + + afterEachTest { + Engine.scheduler.reset() + topology.nodes.forEach { it.protocol.reset() } + } + + val node1 = topology[1]!! + val node2 = topology[2]!! + val protocol1 = node1.protocol as BGP + val protocol2 = node2.protocol as BGP + + on("simulating with node 1 as the destination") { + + val terminated = Engine.simulate(topology, node1, threshold = 1000) + + it("terminated") { + assertThat(terminated, Is(true)) + } + + it("finishes with node 1 selecting self route") { + assertThat(protocol1.routingTable.getSelectedRoute(), + Is(BGPRoute.self())) + } + + it("finishes with node 1 selecting route via himself") { + assertThat(protocol1.routingTable.getSelectedNeighbor(), + Is(node1)) + } + + it("finishes with node 2 selecting route with LOCAL-PREF=10 and AS-PATH=[1]") { + assertThat(protocol2.routingTable.getSelectedRoute(), + Is(BGPRoute.with(10, pathOf(node1)))) + } + + it("finishes with node 2 selecting route via node 1") { + assertThat(protocol2.routingTable.getSelectedNeighbor(), + Is(node1)) + } + } + + on("simulating with node 2 as the destination") { + + val terminated = Engine.simulate(topology, node2, threshold = 1000) + + it("terminated") { + assertThat(terminated, Is(true)) + } + + it("finishes with node 1 selecting an invalid route") { + assertThat(protocol1.routingTable.getSelectedRoute(), Is(BGPRoute.invalid())) + } + + it("finishes with node 1 selecting null neighbor") { + assertThat(protocol1.routingTable.getSelectedNeighbor(), Is(nullValue())) + } + + it("finishes with node 2 selecting self route") { + assertThat(protocol2.routingTable.getSelectedRoute(), Is(BGPRoute.self())) + } + + it("finishes with node 2 selecting route via himself") { + assertThat(protocol2.routingTable.getSelectedNeighbor(), Is(node2)) + } + } + } + + given("topology with 2 nodes with a link in each direction") { + + val topology = bgpTopology { + node { 1 deploying BGP() } + node { 2 deploying BGP() } + + link { 2 to 1 withCost 10 } + link { 1 to 2 withCost 10 } + } + + afterEachTest { + Engine.scheduler.reset() + topology.nodes.forEach { it.protocol.reset() } + } + + val node1 = topology[1]!! + val node2 = topology[2]!! + val protocol1 = node1.protocol as BGP + val protocol2 = node2.protocol as BGP + + on("simulating with node 1 as the destination") { + // Make sure that node 1 always elects the self route + + val terminated = Engine.simulate(topology, node1, threshold = 1000) + + it("terminated") { + assertThat(terminated, Is(true)) + } + + it("finishes with node 1 selecting self route") { + assertThat(protocol1.routingTable.getSelectedRoute(), + Is(BGPRoute.self())) + } + + it("finishes with node 1 selecting route via himself") { + assertThat(protocol1.routingTable.getSelectedNeighbor(), + Is(node1)) + } + + it("finishes with node 2 selecting route with LOCAL-PREF=10 and AS-PATH=[1]") { + assertThat(protocol2.routingTable.getSelectedRoute(), + Is(BGPRoute.with(10, pathOf(node1)))) + } + + it("finishes with node 2 selecting route via node 1") { + assertThat(protocol2.routingTable.getSelectedNeighbor(), + Is(node1)) + } + } + } + + given("topology with 5 nodes forming a pyramid") { + + val topology = bgpTopology { + node { 0 deploying BGP() } + node { 1 deploying BGP() } + node { 2 deploying BGP() } + node { 3 deploying BGP() } + node { 4 deploying BGP() } + + link { 2 to 0 withCost 0 } + link { 2 to 3 withCost -10 } + link { 3 to 1 withCost 0 } + link { 3 to 2 withCost 1 } + link { 4 to 2 withCost 10 } + link { 4 to 3 withCost 1 } + } + + afterEachTest { + Engine.scheduler.reset() + topology.nodes.forEach { it.protocol.reset() } + } + + val node = topology.nodes.sortedBy { it.id } + val protocol = node.map { it.protocol as BGP } + + on("simulating with node 0 as the destination") { + + val terminated = Engine.simulate(topology, node[0], threshold = 1000) + + it("terminated") { + assertThat(terminated, Is(true)) + } + + it("finishes with node 0 selecting route via himself") { + assertThat(protocol[0].routingTable.getSelectedRoute(), + Is(BGPRoute.self())) + assertThat(protocol[0].routingTable.getSelectedNeighbor(), + Is(node[0])) + } + + it("finishes with node 1 without a route") { + assertThat(protocol[1].routingTable.getSelectedRoute(), + Is(BGPRoute.invalid())) + assertThat(protocol[1].routingTable.getSelectedNeighbor(), + Is(nullValue())) + } + + it("finishes with node 2 selecting route with cost 0 via node 0") { + assertThat(protocol[2].routingTable.getSelectedRoute(), + Is(BGPRoute.with(0, pathOf(node[0])))) + assertThat(protocol[2].routingTable.getSelectedNeighbor(), + Is(node[0])) + } + + it("finishes with node 3 selecting route with cost 1 via node 2") { + assertThat(protocol[3].routingTable.getSelectedRoute(), + Is(BGPRoute.with(1, pathOf(node[0], node[2])))) + assertThat(protocol[3].routingTable.getSelectedNeighbor(), + Is(node[2])) + } + + it("finishes with node 4 selecting route with cost 10 via node 2") { + assertThat(protocol[4].routingTable.getSelectedRoute(), + Is(BGPRoute.with(10, pathOf(node[0], node[2])))) + assertThat(protocol[4].routingTable.getSelectedNeighbor(), + Is(node[2])) + } + } + + on("simulating with node 1 as the destination") { + + val terminated = Engine.simulate(topology, node[1], threshold = 1000) + + it("terminated") { + assertThat(terminated, Is(true)) + } + + it("finishes with node 0 without a route") { + assertThat(protocol[0].routingTable.getSelectedRoute(), + Is(BGPRoute.invalid())) + assertThat(protocol[0].routingTable.getSelectedNeighbor(), + Is(nullValue())) + } + + it("finishes with node 1 selecting route via himself") { + assertThat(protocol[1].routingTable.getSelectedRoute(), + Is(BGPRoute.self())) + assertThat(protocol[1].routingTable.getSelectedNeighbor(), + Is(node[1])) + } + it("finishes with node 2 selecting route with cost -10 via node 3") { + assertThat(protocol[2].routingTable.getSelectedRoute(), + Is(BGPRoute.with(-10, pathOf(node[1], node[3])))) + assertThat(protocol[2].routingTable.getSelectedNeighbor(), + Is(node[3])) + } + + it("finishes with node 3 selecting route with cost 0 via node 1") { + assertThat(protocol[3].routingTable.getSelectedRoute(), + Is(BGPRoute.with(0, pathOf(node[1])))) + assertThat(protocol[3].routingTable.getSelectedNeighbor(), + Is(node[1])) + } + + it("finishes with node 4 selecting route with cost 0 via node 3") { + assertThat(protocol[4].routingTable.getSelectedRoute(), + Is(BGPRoute.with(1, pathOf(node[1], node[3])))) + assertThat(protocol[4].routingTable.getSelectedNeighbor(), + Is(node[3])) + } + } + } + + given("topology with 4 where three form a cycle and all three have a link for node 0") { + + val topology = bgpTopology { + node { 0 deploying BGP() } + node { 1 deploying BGP() } + node { 2 deploying BGP() } + node { 3 deploying BGP() } + + link { 1 to 0 withCost 0 } + link { 2 to 0 withCost 0 } + link { 3 to 0 withCost 0 } + link { 1 to 2 withCost 1 } + link { 2 to 3 withCost -1 } + link { 3 to 1 withCost 2 } + } + + afterEachTest { + Engine.scheduler.reset() + topology.nodes.forEach { it.protocol.reset() } + } + + val node = topology.nodes.sortedBy { it.id } + + on("simulating with node 0 as the destination") { + + val terminated = Engine.simulate(topology, node[0], threshold = 1000) + + it("does not terminate") { + assertThat(terminated, Is(false)) + } + } + } + +}) \ No newline at end of file diff --git a/src/test/kotlin/core/simulator/ExporterTests.kt b/src/test/kotlin/core/simulator/ExporterTests.kt new file mode 100644 index 0000000..09d4f17 --- /dev/null +++ b/src/test/kotlin/core/simulator/ExporterTests.kt @@ -0,0 +1,58 @@ +package core.simulator + +import core.routing.Message +import core.routing.Route +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.greaterThan +import org.jetbrains.spek.api.Spek +import org.jetbrains.spek.api.dsl.given +import org.jetbrains.spek.api.dsl.it +import org.jetbrains.spek.api.dsl.on +import testing.invalidRoute +import testing.node +import testing.someExtender + +/** + * Created on 22-07-2017 + * + * @author David Fialho + */ +object ExporterTests : Spek({ + + /** + * Returns a message. + */ + fun message(): Message { + return Message(node(1), node(2), invalidRoute(), someExtender()) + } + + given("an exporter using a random delay generator") { + + // Change message delay generator to random generator + Engine.messageDelayGenerator = RandomDelayGenerator.with(min = 1, max = 10, seed = 10L) + + afterGroup { + Engine.resetToDefaults() + } + + val exporter = Exporter() + + on("exporting 100 messages") { + + it("keeps the deliver time of each message higher than the previous one") { + + var previousDeliverTime = 0 + var deliverTime = exporter.export(message()) + + for (i in 1..99) { + assertThat(deliverTime, greaterThan(previousDeliverTime)) + + previousDeliverTime = deliverTime + deliverTime = exporter.export(message()) + } + } + } + } + +}) + diff --git a/src/test/kotlin/core/simulator/ISSBGPWithShortestPathRoutingTests.kt b/src/test/kotlin/core/simulator/ISSBGPWithShortestPathRoutingTests.kt new file mode 100644 index 0000000..76f0f78 --- /dev/null +++ b/src/test/kotlin/core/simulator/ISSBGPWithShortestPathRoutingTests.kt @@ -0,0 +1,82 @@ +package core.simulator + +import bgp.BGPRoute +import bgp.ISSBGP +import org.hamcrest.MatcherAssert.assertThat +import org.jetbrains.spek.api.Spek +import org.jetbrains.spek.api.dsl.given +import org.jetbrains.spek.api.dsl.it +import org.jetbrains.spek.api.dsl.on +import testing.* +import testing.bgp.pathOf +import org.hamcrest.Matchers.`is` as Is + +/** + * Created on 26-07-2017. + * + * @author David Fialho + */ +object ISSBGPWithShortestPathRoutingTests : Spek({ + + given("topology with non-absorbent cycle") { + + val topology = bgpTopology { + node { 0 deploying ISSBGP() } + node { 1 deploying ISSBGP() } + node { 2 deploying ISSBGP() } + node { 3 deploying ISSBGP() } + + link { 1 to 0 withCost 0 } + link { 2 to 0 withCost 0 } + link { 3 to 0 withCost 0 } + link { 1 to 2 withCost 1 } + link { 2 to 3 withCost -1 } + link { 3 to 1 withCost 2 } + } + + afterEachTest { + Engine.scheduler.reset() + topology.nodes.forEach { it.protocol.reset() } + } + + val node = topology.nodes.sortedBy { it.id } + val protocol = node.map { it.protocol as ISSBGP } + + on("simulating with node 0 as the destination") { + + val terminated = Engine.simulate(topology, node[0], threshold = 1000) + + it("terminates") { + assertThat(terminated, Is(true)) + } + + it("finishes with node 1 selecting route with cost 0 via node 0") { + assertThat(protocol[1].routingTable.getSelectedRoute(), + Is(BGPRoute.with(localPref = 0, asPath = pathOf(0)))) + } + + it("finishes with node 2 selecting route with cost 0 via node 0") { + assertThat(protocol[2].routingTable.getSelectedRoute(), + Is(BGPRoute.with(localPref = 0, asPath = pathOf(0)))) + } + + it("finishes with node 3 selecting route with cost 2 via node 1") { + assertThat(protocol[3].routingTable.getSelectedRoute(), + Is(BGPRoute.with(localPref = 2, asPath = pathOf(0, 1)))) + } + + it("finishes with link from 1 to 2 disabled") { + assertThat(protocol[1].routingTable.table.isEnabled(node[2]), Is(false)) + } + + it("finishes with link from 2 to 3 disabled") { + assertThat(protocol[2].routingTable.table.isEnabled(node[3]), Is(false)) + } + + it("finishes with link from 3 to 1 enabled") { + assertThat(protocol[3].routingTable.table.isEnabled(node[1]), Is(true)) + } + } + } + +}) \ No newline at end of file diff --git a/src/test/kotlin/core/simulator/NotificationTests.kt b/src/test/kotlin/core/simulator/NotificationTests.kt new file mode 100644 index 0000000..cb8b1a9 --- /dev/null +++ b/src/test/kotlin/core/simulator/NotificationTests.kt @@ -0,0 +1,136 @@ +package core.simulator + +import bgp.BGP +import bgp.SSBGP +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.`is` as Is +import org.jetbrains.spek.api.Spek +import org.jetbrains.spek.api.dsl.context +import org.jetbrains.spek.api.dsl.it +import org.jetbrains.spek.api.dsl.on +import testing.* +import utils.collectBGPNotifications + +/** + * Created on 26-07-2017. + * + * @author David Fialho + */ +object NotificationsTests: Spek({ + + context("engine") { + + on("simulating topology with only one link") { + + val topology = bgpTopology { + node { 0 deploying BGP() } + node { 1 deploying BGP() } + + link { 1 to 0 withCost 10 } + } + + val node = topology.nodes.sortedBy { it.id } + + val collector = collectBGPNotifications { + Engine.simulate(topology, node[0], threshold = 1000) + } + + it("issues start notification once") { + assertThat(collector.startNotifications.size, Is(1)) + } + + it("issues end notification once") { + assertThat(collector.endNotifications.size, Is(1)) + } + + it("never issues threshold reached notification") { + assertThat(collector.thresholdReachedNotifications.size, Is(0)) + } + + it("issues message sent notification once") { + assertThat(collector.messageSentNotifications.size, Is(1)) + } + + it("issues message received notification once") { + assertThat(collector.messageReceivedNotifications.size, Is(1)) + } + + it("issues import notification once") { + assertThat(collector.importNotifications.size, Is(1)) + } + + it("issues learn notification once") { + assertThat(collector.learnNotifications.size, Is(1)) + } + + it("never issues detect notification") { + assertThat(collector.detectNotifications.size, Is(0)) + } + + it("issues select notification once") { + assertThat(collector.selectNotifications.size, Is(1)) + } + + it("issues export notification 2 times") { + // Although node 1 has no in-neighbors two export notifications are sent + // This is because the export notification indicates that node exports a route not that it actually + // sent any route to any neighbor + // Note that the sent notification coutn is only one indicating that only 1 message was sent from + // node 0 to node 1 + assertThat(collector.exportNotifications.size, Is(2)) + } + } + + on("simulating topology with non-absorbent cycle with two nodes") { + + val topology = bgpTopology { + node { 0 deploying SSBGP() } + node { 1 deploying SSBGP() } + node { 2 deploying SSBGP() } + + link { 1 to 0 withCost 0 } + link { 2 to 0 withCost 0 } + link { 1 to 2 withCost 1 } + link { 2 to 1 withCost 1 } + } + + val node = topology.nodes.sortedBy { it.id } + + val collector = collectBGPNotifications { + Engine.simulate(topology, node[0], threshold = 1000) + } + + it("never issues threshold reached notification") { + assertThat(collector.thresholdReachedNotifications.size, Is(0)) + } + + it("issues message sent notification once") { + assertThat(collector.messageSentNotifications.size, Is(8)) + } + + it("issues message received notification once") { + assertThat(collector.messageReceivedNotifications.size, Is(8)) + } + + it("issues import notification 8 times") { + assertThat(collector.importNotifications.size, Is(8)) + } + + it("issues learn notification 8 times") { + assertThat(collector.learnNotifications.size, Is(8)) + } + + it("issues detect notification 2 times") { + assertThat(collector.detectNotifications.size, Is(2)) + } + + it("issues select notification 6 times") { + assertThat(collector.selectNotifications.size, Is(6)) + } + + it("issues export notification 7 times") { + assertThat(collector.exportNotifications.size, Is(7)) + } + } + } +}) diff --git a/src/test/kotlin/core/simulator/RandomDelayGeneratorTests.kt b/src/test/kotlin/core/simulator/RandomDelayGeneratorTests.kt new file mode 100644 index 0000000..7976147 --- /dev/null +++ b/src/test/kotlin/core/simulator/RandomDelayGeneratorTests.kt @@ -0,0 +1,63 @@ +package core.simulator + +import org.jetbrains.spek.api.Spek +import org.jetbrains.spek.api.dsl.context +import org.jetbrains.spek.api.dsl.given +import org.jetbrains.spek.api.dsl.it +import org.jetbrains.spek.api.dsl.on +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.* + +/** + * Created on 22-07-2017 + + * @author David Fialho + */ +object RandomDelayGeneratorTests : Spek({ + + context("a random delay generator that has already generated 3 delays") { + + val generator = RandomDelayGenerator.with(min = 0, max = 10, seed = 10L) + val delays = listOf(generator.nextDelay(), generator.nextDelay(), generator.nextDelay()) + + given ("reset is called") { + + generator.reset() + + on("calling nextDelay 3 times") { + + val delaysAfterReset = listOf(generator.nextDelay(), generator.nextDelay(), generator.nextDelay()) + + it("generates the same 3 delays as before in the exact same order") { + assertThat(delaysAfterReset, `is`(delays)) + } + } + } + + } + + given("a random delay generator with a minimum of 1 and a maximum of 5") { + + val generator = RandomDelayGenerator.with(min = 1, max = 5, seed = 10L) + + on ("generating 100 delays values") { + + val delays = (1..100).map { generator.nextDelay() }.toList() + + it("generates all values between 1 and 5 (inclusive)") { + delays.forEach { assertThat(it, allOf(greaterThanOrEqualTo(1), lessThanOrEqualTo(5))) } + } + + it("generates some delays of 5") { + assertThat(delays, hasItem(5)) + } + + it("generates some delays of 1") { + assertThat(delays, hasItem(1)) + } + + } + + } + +}) diff --git a/src/test/kotlin/core/simulator/SSBGPWithInterdomainRoutingTests.kt b/src/test/kotlin/core/simulator/SSBGPWithInterdomainRoutingTests.kt new file mode 100644 index 0000000..8133bab --- /dev/null +++ b/src/test/kotlin/core/simulator/SSBGPWithInterdomainRoutingTests.kt @@ -0,0 +1,149 @@ +package core.simulator + +import bgp.SSBGP +import bgp.policies.interdomain.customerRoute +import org.hamcrest.MatcherAssert.assertThat +import org.jetbrains.spek.api.Spek +import org.jetbrains.spek.api.dsl.given +import org.jetbrains.spek.api.dsl.it +import org.jetbrains.spek.api.dsl.on +import testing.* +import testing.bgp.pathOf +import org.hamcrest.Matchers.`is` as Is + +/** + * Created on 26-07-2017 + * + * @author David Fialho + */ +object SSBGPWithInterdomainRoutingTests : Spek({ + + given("loop topology with customer to destination and peer+ around the cycle") { + + val topology = bgpTopology { + node { 0 deploying SSBGP() } + node { 1 deploying SSBGP() } + node { 2 deploying SSBGP() } + node { 3 deploying SSBGP() } + + customerLink { 1 to 0 } + customerLink { 2 to 0 } + customerLink { 3 to 0 } + peerplusLink { 1 to 2 } + peerplusLink { 2 to 3 } + peerplusLink { 3 to 1 } + } + + val node = topology.nodes.sortedBy { it.id } + val protocol = node.map { it.protocol as SSBGP } + + afterEachTest { + Engine.scheduler.reset() + topology.nodes.forEach { it.protocol.reset() } + } + + on("simulating with node 0 as the destination") { + + val terminated = Engine.simulate(topology, node[0], threshold = 1000) + + it("terminates") { + assertThat(terminated, + Is(true)) + } + + it("finishes with node 1 selecting customer route via node 0") { + assertThat(protocol[1].routingTable.getSelectedRoute(), + Is(customerRoute(asPath = pathOf(0)))) + } + + it("finishes with node 2 selecting customer route via node 0") { + assertThat(protocol[2].routingTable.getSelectedRoute(), + Is(customerRoute(asPath = pathOf(0)))) + } + + it("finishes with node 3 selecting customer route via node 0") { + assertThat(protocol[3].routingTable.getSelectedRoute(), + Is(customerRoute(asPath = pathOf(0)))) + } + + it("finishes with node 1 disabling neighbor 2") { + assertThat(protocol[1].routingTable.table.isEnabled(node[2]), + Is(false)) + } + + it("finishes with node 2 disabling neighbor 3") { + assertThat(protocol[2].routingTable.table.isEnabled(node[3]), + Is(false)) + } + + it("finishes with node 3 disabling neighbor 1") { + assertThat(protocol[3].routingTable.table.isEnabled(node[1]), + Is(false)) + } + } + } + + given("topology with non-absorbent cycle and with siblings") { + + val topology = bgpTopology { + node { 0 deploying SSBGP() } + node { 1 deploying SSBGP() } + node { 2 deploying SSBGP() } + node { 3 deploying SSBGP() } + + siblingLink { 1 to 0 } + siblingLink { 2 to 0 } + customerLink { 1 to 2 } + customerLink { 2 to 3 } + siblingLink { 3 to 1 } + } + + val node = topology.nodes.sortedBy { it.id } + val protocol = node.map { it.protocol as SSBGP } + + afterEachTest { + Engine.scheduler.reset() + topology.nodes.forEach { it.protocol.reset() } + } + + on("simulating with node 0 as the destination") { + + val terminated = Engine.simulate(topology, node[0], threshold = 1000) + + it("terminates") { + assertThat(terminated, Is(true)) + } + + it("finishes with node 1 selecting customer route with 1 sibling hop via node 0") { + assertThat(protocol[1].routingTable.getSelectedRoute(), + Is(customerRoute(siblingHops = 1, asPath = pathOf(0)))) + } + + it("finishes with node 2 selecting customer route with 1 sibling hop via node 0") { + assertThat(protocol[2].routingTable.getSelectedRoute(), + Is(customerRoute(siblingHops = 1, asPath = pathOf(0)))) + } + + it("finishes with node 3 selecting customer route with 2 sibling hops via node 1") { + assertThat(protocol[3].routingTable.getSelectedRoute(), + Is(customerRoute(siblingHops = 2, asPath = pathOf(0, 1)))) + } + + it("finishes with node 1 disabling neighbor 2") { + assertThat(protocol[1].routingTable.table.isEnabled(node[2]), + Is(false)) + } + + it("finishes with node 2 disabling neighbor 3") { + assertThat(protocol[2].routingTable.table.isEnabled(node[3]), + Is(false)) + } + + it("finishes with node 3 NOT disabling neighbor 1") { + assertThat(protocol[3].routingTable.table.isEnabled(node[1]), + Is(true)) + } + } + } + +}) \ No newline at end of file diff --git a/src/test/kotlin/core/simulator/SSBGPWithShortestPathRoutingTests.kt b/src/test/kotlin/core/simulator/SSBGPWithShortestPathRoutingTests.kt new file mode 100644 index 0000000..1786fd4 --- /dev/null +++ b/src/test/kotlin/core/simulator/SSBGPWithShortestPathRoutingTests.kt @@ -0,0 +1,215 @@ +package core.simulator + +import bgp.BGPRoute +import bgp.SSBGP +import core.routing.pathOf +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.* +import org.jetbrains.spek.api.Spek +import org.jetbrains.spek.api.dsl.given +import org.jetbrains.spek.api.dsl.it +import org.jetbrains.spek.api.dsl.on +import testing.* +import testing.bgp.pathOf +import org.hamcrest.Matchers.`is` as Is + +/** + * Created on 26-07-2017. + * + * @author David Fialho + */ +object SSBGPWithShortestPathRoutingTests : Spek({ + + given("topology with a single link from 2 to 1 with cost 10") { + + val topology = bgpTopology { + node { 1 deploying SSBGP() } + node { 2 deploying SSBGP() } + + link { 2 to 1 withCost 10 } + } + + afterEachTest { + Engine.scheduler.reset() + topology.nodes.forEach { it.protocol.reset() } + } + + val node1 = topology[1]!! + val node2 = topology[2]!! + val protocol1 = node1.protocol as SSBGP + val protocol2 = node2.protocol as SSBGP + + on("simulating with node 1 as the destination") { + + val terminated = Engine.simulate(topology, node1, threshold = 1000) + + it("terminated") { + assertThat(terminated, Is(true)) + } + + it("finishes with node 1 selecting self route") { + assertThat(protocol1.routingTable.getSelectedRoute(), Is(BGPRoute.self())) + } + + it("finishes with node 1 selecting route via himself") { + assertThat(protocol1.routingTable.getSelectedNeighbor(), Is(node1)) + } + + it("finishes with node 2 selecting route with LOCAL-PREF=10 and AS-PATH=[1]") { + assertThat(protocol2.routingTable.getSelectedRoute(), Is(BGPRoute.with(10, pathOf(node1)))) + } + + it("finishes with node 2 selecting route via node 1") { + assertThat(protocol2.routingTable.getSelectedNeighbor(), Is(node1)) + } + } + + on("simulating with node 2 as the destination") { + + val terminated = Engine.simulate(topology, node2, threshold = 1000) + + it("terminated") { + assertThat(terminated, Is(true)) + } + + it("finishes with node 1 selecting an invalid route") { + assertThat(protocol1.routingTable.getSelectedRoute(), Is(BGPRoute.invalid())) + } + + it("finishes with node 1 selecting null neighbor") { + assertThat(protocol1.routingTable.getSelectedNeighbor(), Is(nullValue())) + } + + it("finishes with node 2 selecting self route") { + assertThat(protocol2.routingTable.getSelectedRoute(), Is(BGPRoute.self())) + } + + it("finishes with node 2 selecting route via himself") { + assertThat(protocol2.routingTable.getSelectedNeighbor(), Is(node2)) + } + } + } + + given("topology with 4 where three form a cycle and all three have a link for node 0") { + + val topology = bgpTopology { + node { 0 deploying SSBGP() } + node { 1 deploying SSBGP() } + node { 2 deploying SSBGP() } + node { 3 deploying SSBGP() } + + link { 1 to 0 withCost 0 } + link { 2 to 0 withCost 0 } + link { 3 to 0 withCost 0 } + link { 1 to 2 withCost 1 } + link { 2 to 3 withCost -1 } + link { 3 to 1 withCost 2 } + } + + afterEachTest { + Engine.scheduler.reset() + topology.nodes.forEach { it.protocol.reset() } + } + + val node = topology.nodes.sortedBy { it.id } + val protocol = node.map { it.protocol as SSBGP } + + on("simulating with node 0 as the destination") { + + val terminated = Engine.simulate(topology, node[0], threshold = 1000) + + it("terminates") { + assertThat(terminated, Is(true)) + } + + it("finishes with node 1 selecting route with cost 0 via node 0") { + assertThat(protocol[1].routingTable.getSelectedRoute(), + Is(BGPRoute.with(localPref = 0, asPath = pathOf(0)))) + } + + it("finishes with node 2 selecting route with cost 0 via node 0") { + assertThat(protocol[2].routingTable.getSelectedRoute(), + Is(BGPRoute.with(localPref = 0, asPath = pathOf(0)))) + } + + it("finishes with node 3 selecting route with cost 2 via node 1") { + assertThat(protocol[3].routingTable.getSelectedRoute(), + Is(BGPRoute.with(localPref = 2, asPath = pathOf(0, 1)))) + } + + it("finishes with link from 1 to 2 disabled") { + assertThat(protocol[1].routingTable.table.isEnabled(node[2]), Is(false)) + } + + it("finishes with link from 2 to 3 disabled") { + assertThat(protocol[2].routingTable.table.isEnabled(node[3]), Is(false)) + } + + it("finishes with link from 3 to 1 enabled") { + assertThat(protocol[3].routingTable.table.isEnabled(node[1]), Is(true)) + } + } + } + + given("topology with absorbent cycle") { + + val topology = bgpTopology { + node { 0 deploying SSBGP() } + node { 1 deploying SSBGP() } + node { 2 deploying SSBGP() } + node { 3 deploying SSBGP() } + + link { 1 to 0 withCost 0 } + link { 2 to 0 withCost 0 } + link { 3 to 0 withCost 0 } + link { 1 to 2 withCost -3 } + link { 2 to 3 withCost 1 } + link { 3 to 1 withCost 2 } + } + + afterEachTest { + Engine.scheduler.reset() + topology.nodes.forEach { it.protocol.reset() } + } + + val node = topology.nodes.sortedBy { it.id } + val protocol = node.map { it.protocol as SSBGP } + + on("simulating with node 0 as the destination") { + + val terminated = Engine.simulate(topology, node[0], threshold = 1000) + + it("terminates") { + assertThat(terminated, Is(true)) + } + + it("finishes with node 1 selecting route with cost 0 via node 0") { + assertThat(protocol[1].routingTable.getSelectedRoute(), + Is(BGPRoute.with(localPref = 0, asPath = pathOf(0)))) + } + + it("finishes with node 2 selecting route with cost 3 via node 3") { + assertThat(protocol[2].routingTable.getSelectedRoute(), + Is(BGPRoute.with(localPref = 3, asPath = pathOf(0, 1, 3)))) + } + + it("finishes with node 3 selecting route with cost 2 via node 1") { + assertThat(protocol[3].routingTable.getSelectedRoute(), + Is(BGPRoute.with(localPref = 2, asPath = pathOf(0, 1)))) + } + + it("finishes with link from 1 to 2 enabled") { + assertThat(protocol[1].routingTable.table.isEnabled(node[2]), Is(true)) + } + + it("finishes with link from 2 to 3 enabled") { + assertThat(protocol[2].routingTable.table.isEnabled(node[3]), Is(true)) + } + + it("finishes with link from 3 to 1 enabled") { + assertThat(protocol[3].routingTable.table.isEnabled(node[1]), Is(true)) + } + } + } + +}) \ No newline at end of file diff --git a/src/test/kotlin/core/simulator/SchedulerTests.kt b/src/test/kotlin/core/simulator/SchedulerTests.kt new file mode 100644 index 0000000..b561bcd --- /dev/null +++ b/src/test/kotlin/core/simulator/SchedulerTests.kt @@ -0,0 +1,161 @@ +package core.simulator + +import org.jetbrains.spek.api.Spek +import org.jetbrains.spek.api.dsl.given +import org.jetbrains.spek.api.dsl.it +import org.jetbrains.spek.api.dsl.on +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.* +import org.junit.jupiter.api.Assertions.assertThrows + +/** + * Created on 22-07-2017 + + * @author David Fialho + */ +object SchedulerTests : Spek({ + + given("an empty scheduler that has never had an event") { + + beforeGroup { + Scheduler.reset() + } + + it("has no events") { + assertThat(Scheduler.hasEvents(), `is`(false)) + } + + it("throws an exception when trying to get the next event") { + assertThrows(NoSuchElementException::class.java) { -> + Scheduler.nextEvent() + } + } + + it("has time of 0") { + assertThat(Scheduler.time, equalTo(0)) + } + + on("scheduling a new event to occur at time 10") { + + Scheduler.schedule(event(id = 0), timestamp = 10) + + it("has events") { + assertThat(Scheduler.hasEvents(), `is`(true)) + } + + it("it returns the scheduled event when trying to get the next event") { + assertThat(Scheduler.nextEvent(), `is`(event(id = 0))) + } + + it("has time of 10") { + assertThat(Scheduler.time, `is`(10)) + } + + it("no longer has events in the queue") { + assertThat(Scheduler.hasEvents(), `is`(false)) + } + + } + + on("scheduling an event at time 15") { + + Scheduler.schedule(event(id = 1), timestamp = 15) + } + + on("and scheduling another event at time 20") { + + Scheduler.schedule(event(id = 2), timestamp = 20) + + it("has events") { + assertThat(Scheduler.hasEvents(), `is`(true)) + } + + it("has time of 10") { + assertThat(Scheduler.time, `is`(10)) + } + } + + on("trying to get the next event") { + + it("returns the event scheduled at time 15") { + assertThat(Scheduler.nextEvent(), `is`(event(id = 1))) + } + + it("still has events") { + assertThat(Scheduler.hasEvents(), `is`(true)) + } + + it("has time of 15") { + assertThat(Scheduler.time, `is`(15)) + } + + } + + on("scheduling another event at time 10") { + + it("throws an IllegalArgumentException") { + assertThrows(IllegalArgumentException::class.java) { -> + Scheduler.schedule(event(id = 3), timestamp = 10) + } + } + } + + on("scheduling another event at time 15: equal to the current time") { + + it("does not throw any exception") { + Scheduler.schedule(event(id = 4), timestamp = 15) + } + } + } + + + + given("a scheduler with time 10 and containing an event to occur at time 20") { + + beforeGroup { + Scheduler.reset() + } + + Scheduler.schedule(event(id = 0), timestamp = 10) + Scheduler.nextEvent() // remove the event with timestamp 10, which updates the Scheduler time to 10 + Scheduler.schedule(event(id = 1), timestamp = 20) + + on("scheduling an event at time 15 and trying to get the next event") { + + Scheduler.schedule(event(id = 2), timestamp = 15) + + it("returns the event scheduled to time 15") { + assertThat(Scheduler.nextEvent(), `is`(event(id = 2))) + } + + it("has time 15") { + assertThat(Scheduler.time, `is`(15)) + } + } + + } + +}) + +//region Fake implementation of an Event + +/** + * Fake implementation of an event used for testing the Scheduler. + * + * It defines an ID property that can be used to identify each event in the tests. It is specially useful in + * assertion error reports. + * + * Notice this is a data class, which means the equals method will consider the ID to determine the equality. + */ +data class FakeEvent(val id: Int) : Event { + override fun processIt() { + throw UnsupportedOperationException("Does not need to implement this for the tests") + } +} + +/** + * Returns an event with the specified ID. + */ +fun event(id: Int = 0): Event = FakeEvent(id) + +//endregion diff --git a/src/test/kotlin/io/InterdomainTopologyReaderTest.kt b/src/test/kotlin/io/InterdomainTopologyReaderTest.kt new file mode 100644 index 0000000..466d775 --- /dev/null +++ b/src/test/kotlin/io/InterdomainTopologyReaderTest.kt @@ -0,0 +1,147 @@ +package io + +import bgp.* +import bgp.policies.interdomain.* +import core.routing.* +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.contains +import org.hamcrest.Matchers.instanceOf +import org.jetbrains.spek.api.Spek +import org.jetbrains.spek.api.dsl.given +import org.jetbrains.spek.api.dsl.it +import org.junit.jupiter.api.Assertions.assertThrows +import testing.`when` +import testing.bgp.BGPNode +import java.io.StringReader +import org.hamcrest.Matchers.`is` as Is + +object InterdomainTopologyReaderTest: Spek({ + + given("an interdomain topology file") { + + data class ExpectedNode(val id: NodeID, val protocol: Protocol, val mrai: Int) + val correctNodeLines = listOf( + Pair("node = 10 | BGP | 5000", ExpectedNode(id = 10, protocol = BGP(), mrai = 5000)), + Pair("node = 10 | BGP | 0", ExpectedNode(id = 10, protocol = BGP(), mrai = 0)), + // Extra values are ignored + Pair("node = 10 | BGP | 5000 | 35", ExpectedNode(id = 10, protocol = BGP(), mrai = 5000)), + Pair("node = 10 | SSBGP | 5000", ExpectedNode(id = 10, protocol = SSBGP(), mrai = 5000)), + Pair("node = 10 | ISSBGP | 5000", ExpectedNode(id = 10, protocol = ISSBGP(), mrai = 5000)) + ) + + correctNodeLines.forEach { (line, expected) -> + + `when`("topology is read from file with a single node `$line`") { + + var nullableTopology: Topology? = null + + InterdomainTopologyReader(StringReader(line)).use { + nullableTopology = it.read() + } + + val topology = nullableTopology!! + + it("has size 1") { + assertThat(topology.size, Is(1)) + } + + it("has a node with ID ${expected.id}") { + assertThat(BGPNode(expected.id) in topology.nodes, Is(true)) + } + + val node = topology[expected.id]!! + + it("has node that deploys ${expected.protocol}") { + assertThat(node.protocol, + Is(instanceOf(expected.protocol::class.java))) + } + + it("has node with MRAI ${expected.mrai}") { + assertThat((node.protocol as BaseBGP).mrai, + Is(expected.mrai)) + } + } + } + + data class ExpectedLink(val tail: NodeID, val head: NodeID, val extender: Extender) + val correctLinkLines = listOf( + Pair("link = 10 | 11 | C", ExpectedLink(tail = 10, head = 11, extender = CustomerExtender)), + // Extra values are ignored + Pair("link = 10 | 11 | C | 35", ExpectedLink(tail = 10, head = 11, extender = CustomerExtender)), + Pair("link = 10 | 11 | R+", ExpectedLink(tail = 10, head = 11, extender = PeerplusExtender)), + Pair("link = 10 | 11 | R", ExpectedLink(tail = 10, head = 11, extender = PeerExtender)), + Pair("link = 10 | 11 | P", ExpectedLink(tail = 10, head = 11, extender = ProviderExtender)), + Pair("link = 10 | 11 | S", ExpectedLink(tail = 10, head = 11, extender = SiblingExtender)) + ) + + fun lines(vararg lines: String): String = lines.joinToString("\n") + + correctLinkLines.forEach { (line, expected) -> + + `when`("topology is read from file with `node = ${expected.tail}`, `node = ${expected.head}`, `$line`") { + + val content = lines( + "node = ${expected.tail} | BGP | 10", + "node = ${expected.head} | BGP | 10", + line + ) + var nullableTopology: Topology? = null + + InterdomainTopologyReader(StringReader(content)).use { + nullableTopology = it.read() + } + + val topology = nullableTopology!! + + it("has size 2") { + assertThat(topology.size, Is(2)) + } + + it("has a node with ID ${expected.tail}") { + assertThat(BGPNode(expected.tail) in topology.nodes, Is(true)) + } + + it("has a node with ID ${expected.head}") { + assertThat(BGPNode(expected.head) in topology.nodes, Is(true)) + } + + val tail = topology[expected.tail]!! + val head = topology[expected.head]!! + + it("has link from node ${expected.tail} to node ${expected.head} of type ${expected.extender}") { + assertThat(topology.links, contains(Link(tail, head, expected.extender))) + } + } + } + + val incorrectLines = listOf( + "node = 10 | proto | 5000", + "node = 10 | BGP | -1", + "node = 10 | BGP | ", + "node = 10 | BGP", + "link = 10 | 11 |", + "link = 10 | 11 | abc" + ) + + incorrectLines.forEach { line -> + + `when`("topology is read from file with incorrect line `$line`") { + + var exception: ParseException? = null + + it("throws a ParseException") { + InterdomainTopologyReader(StringReader(line)).use { + exception = assertThrows(ParseException::class.java) { + it.read() + } + } + } + + it("indicates the error is in line 1") { + assertThat(exception?.lineNumber, Is(1)) + } + } + } + } + +}) \ No newline at end of file diff --git a/src/test/kotlin/io/TopologyParserTest.kt b/src/test/kotlin/io/TopologyParserTest.kt new file mode 100644 index 0000000..468b2a3 --- /dev/null +++ b/src/test/kotlin/io/TopologyParserTest.kt @@ -0,0 +1,305 @@ +package io + +import com.nhaarman.mockito_kotlin.* +import org.hamcrest.MatcherAssert.assertThat +import org.jetbrains.spek.api.Spek +import org.jetbrains.spek.api.dsl.given +import org.jetbrains.spek.api.dsl.it +import org.jetbrains.spek.api.dsl.on +import org.junit.jupiter.api.Assertions.assertThrows +import java.io.StringReader +import org.hamcrest.Matchers.`is` as Is + +object TopologyParserTest: Spek({ + + given("an empty file") { + + val fileContent = "" + val handler: TopologyParser.Handler = mock() + + on("parsing the file") { + + var exception: ParseException? = null + + it("throws a ParseException") { + TopologyParser(StringReader(fileContent), handler).use { + exception = assertThrows(ParseException::class.java) { + it.parse() + } + } + } + + it("indicates the error is in line 1") { + assertThat(exception?.lineNumber, Is(1)) + } + } + } + + given("file with single line `node = 10`") { + + val fileContent = "node = 10" + val handler: TopologyParser.Handler = mock() + + on("parsing the file") { + + TopologyParser(StringReader(fileContent), handler).use { + it.parse() + } + + it("parsed a single node") { + verify(handler, times(1)).onNodeItem(any(), any(), any()) + } + + it("did NOT parse any link") { + verify(handler, never()).onLinkItem(any(), any(), any(), any()) + } + + it("parsed node with ID 10 and no values in line 1") { + verify(handler, times(1)).onNodeItem(10, emptyList(), 1) + } + } + } + + given("file with single line with multiple values `node = 10 | 11`") { + + val fileContent = "node = 10 | 11" + val handler: TopologyParser.Handler = mock() + + on("parsing the file") { + + TopologyParser(StringReader(fileContent), handler).use { + it.parse() + } + + it("parsed a single node") { + verify(handler, times(1)).onNodeItem(any(), any(), any()) + } + + it("did NOT parse any link") { + verify(handler, never()).onLinkItem(any(), any(), any(), any()) + } + + it("parsed node with ID 10 and values [11] in line 1") { + verify(handler, times(1)).onNodeItem(10, listOf("11"), 1) + } + } + } + + given("file with single line with multiple values `node = 10 | 11 - 19`") { + + val fileContent = "node = 10 | 11 - 19" + val handler: TopologyParser.Handler = mock() + + on("parsing the file") { + + TopologyParser(StringReader(fileContent), handler).use { + it.parse() + } + + it("parsed a single node") { + verify(handler, times(1)).onNodeItem(any(), any(), any()) + } + + it("did NOT parse any link") { + verify(handler, never()).onLinkItem(any(), any(), any(), any()) + } + + it("parsed node with values [11 - 19] in line 1") { + verify(handler, times(1)).onNodeItem(10, listOf("11 - 19"), 1) + } + } + } + + given("file with single line with multiple values `node = 10 | 11 | abc | 1a`") { + + val fileContent = "node = 10 | 11 | abc | 1a" + val handler: TopologyParser.Handler = mock() + + on("parsing the file") { + + TopologyParser(StringReader(fileContent), handler).use { + it.parse() + } + + it("parsed a single node") { + verify(handler, times(1)).onNodeItem(any(), any(), any()) + } + + it("did NOT parse any link") { + verify(handler, never()).onLinkItem(any(), any(), any(), any()) + } + + it("parsed node with ID 10 and values [11, abc, 1a] in line 1") { + verify(handler, times(1)).onNodeItem(10, listOf("11", "abc", "1a"), 1) + } + } + } + + given("file with single line `link = 10 | 11`") { + + val fileContent = "link = 10 | 11" + val handler: TopologyParser.Handler = mock() + + on("parsing the file") { + + TopologyParser(StringReader(fileContent), handler).use { + it.parse() + } + + it("parsed a single link") { + verify(handler, times(1)).onLinkItem(any(), any(), any(), any()) + } + + it("did NOT parse any node") { + verify(handler, never()).onNodeItem(any(), any(), any()) + } + + it("parsed in line 1 a link from node 10 to node 11 with no values") { + verify(handler, times(1)).onLinkItem(10, 11, emptyList(), 1) + } + } + } + + given("file with single line `link = 10 | 11 | abc`") { + + val fileContent = "link = 10 | 11 | abc" + val handler: TopologyParser.Handler = mock() + + on("parsing the file") { + + TopologyParser(StringReader(fileContent), handler).use { + it.parse() + } + + it("parsed a single link") { + verify(handler, times(1)).onLinkItem(any(), any(), any(), any()) + } + + it("did NOT parse any node") { + verify(handler, never()).onNodeItem(any(), any(), any()) + } + + it("parsed in line 1 a link from node 10 to node 11 with values [abc]") { + verify(handler, times(1)).onLinkItem(10, 11, listOf("abc"), 1) + } + } + } + + fun lines(vararg lines: String): String = lines.joinToString("\n") + + given("file with lines `node = 10`, `link = 10 | 11`") { + + val fileContent = lines( + "node = 10", + "link = 10 | 11" + ) + val handler: TopologyParser.Handler = mock() + + on("parsing the file") { + + TopologyParser(StringReader(fileContent), handler).use { + it.parse() + } + + it("parsed in line 1 a node with ID 10 and no values") { + verify(handler, times(1)).onNodeItem(10, emptyList(), 1) + } + + it("parsed in line 2 a link from node 10 to node 11 with no values") { + verify(handler, times(1)).onLinkItem(10, 11, emptyList(), 2) + } + } + } + + given("file with lines `node = 10`, `link = 10 | 11`, ``") { + + val fileContent = lines( + "node = 10", + "link = 10 | 11", + "" + ) + val handler: TopologyParser.Handler = mock() + + on("parsing the file") { + + TopologyParser(StringReader(fileContent), handler).use { + it.parse() + } + + it("parsed in line 1 a node with ID 10 and no values") { + verify(handler, times(1)).onNodeItem(10, emptyList(), 1) + } + + it("parsed in line 2 a link from node 10 to node 11 with no values") { + verify(handler, times(1)).onLinkItem(10, 11, emptyList(), 2) + } + } + } + + given("file with lines `node = 10`, ``, `link = 10 | 11`") { + + val fileContent = lines( + "node = 10", + "", + "link = 10 | 11" + ) + val handler: TopologyParser.Handler = mock() + + on("parsing the file") { + + TopologyParser(StringReader(fileContent), handler).use { + it.parse() + } + + it("parsed in line 1 a node with ID 10 and no values") { + verify(handler, times(1)).onNodeItem(10, emptyList(), 1) + } + + it("parsed in line 3 a link from node 10 to node 11 with no values") { + verify(handler, times(1)).onLinkItem(10, 11, emptyList(), 3) + } + } + } + + val incorrectLines = listOf( + "node = a", + "node = 10a", + "element = 10", + "node = 10 = 11", + "node 10", + "node = ", + "node", + "node == 10", + "node = 10 | 11 = 19", + "link = 10", + "link = 10 | a", + "link = a | 10", + "link = a | b" + ) + + incorrectLines.forEach { line -> + + given("file with incorrect line `$line`") { + + val handler: TopologyParser.Handler = mock() + + on("parsing the file") { + + var exception: ParseException? = null + + it("throws a ParseException") { + TopologyParser(StringReader(line), handler).use { + exception = assertThrows(ParseException::class.java) { + it.parse() + } + } + } + + it("indicates the error is in line 1") { + assertThat(exception?.lineNumber, Is(1)) + } + } + } + } + +}) \ No newline at end of file diff --git a/src/test/kotlin/testing/Fakes.kt b/src/test/kotlin/testing/Fakes.kt new file mode 100644 index 0000000..4a12c3e --- /dev/null +++ b/src/test/kotlin/testing/Fakes.kt @@ -0,0 +1,91 @@ +package testing + +import core.routing.* + +sealed class FakeRoute: Route { + + abstract val preference: Int + + internal data class ValidFakeRoute(override val preference: Int): FakeRoute() { + override fun isValid() = true + } + + internal object InvalidFakeRoute: FakeRoute() { + override val preference = Int.MIN_VALUE + override fun isValid() = false + } +} + +/** + * A compare method for fake routes. + * + * Not surprisingly, it considers routes with higher preference value to be preferred to routes with lower + * preference values. + */ +fun fakeCompare(route1: Route, route2: Route): Int { + route1 as FakeRoute + route2 as FakeRoute + + return route1.preference.compareTo(route2.preference) +} + +object FakeProtocol: Protocol { + + override val inNeighbors: Collection> + get() = TODO("not implemented yet") + + override fun addInNeighbor(neighbor: Neighbor) { + TODO("not implemented yet") + } + + override fun start(node: Node) { + TODO("not implemented yet") + } + + override fun process(message: Message) { + TODO("not implemented yet") + } + + override fun reset() { + TODO("not implemented yet") + } +} + +/** + * Created on 25-07-2017 + * + * @author David Fialho + * + * Fake extender used for testing purposes. + */ +object FakeExtender: Extender { + override fun extend(route: Route, sender: Node): Route = invalidRoute() +} + +//region Factory methods + +/** + * Returns a node with the specified ID using a fake protocol. + */ +fun node(id: NodeID): Node { + return Node(id, FakeProtocol) +} + +/** + * Returns a valid route with the given preference value. + */ +fun route(preference: Int): Route = FakeRoute.ValidFakeRoute(preference) + +/** + * Returns an invalid route. + */ +fun invalidRoute(): Route = FakeRoute.InvalidFakeRoute + +/** + * Returns an extender when it is needed one but it is not important which one. + */ +fun someExtender(): Extender { + return FakeExtender +} + +//endregion \ No newline at end of file diff --git a/src/test/kotlin/testing/RoutingTableExtensions.kt b/src/test/kotlin/testing/RoutingTableExtensions.kt new file mode 100644 index 0000000..bf18730 --- /dev/null +++ b/src/test/kotlin/testing/RoutingTableExtensions.kt @@ -0,0 +1,13 @@ +package testing + +import core.routing.Node +import core.routing.Route +import core.routing.RoutingTable + +/** + * Allows us to write something like 'route(1) via node(1)'. + * The neighbors defined here are all enabled. + */ +infix fun R.via(neighbor: Node): RoutingTable.Entry { + return RoutingTable.Entry(neighbor, this, enabled = true) +} \ No newline at end of file diff --git a/src/test/kotlin/testing/SpekExtensions.kt b/src/test/kotlin/testing/SpekExtensions.kt new file mode 100644 index 0000000..eb37e59 --- /dev/null +++ b/src/test/kotlin/testing/SpekExtensions.kt @@ -0,0 +1,45 @@ +package testing + +import org.jetbrains.spek.api.dsl.ActionBody +import org.jetbrains.spek.api.dsl.SpecBody +import org.jetbrains.spek.api.dsl.TestBody +import org.jetbrains.spek.api.dsl.TestContainer + +/** + * Created on 01-08-2017 + * + * @author David Fialho + * + * This file contains methods used to extend the Spek testing framework. + * Most of this methods are wrappers around the common methods such as 'given', 'on', and 'it' that + * replace the name with something more appropriate for some tests. + */ + +//region Group methods + +/** + * Creates a [group][SpecBody.group]. + */ +fun SpecBody.`when`(description: String, body: ActionBody.() -> Unit) { + action("when $description", body = body) +} + +//endregion + +//region Test methods + +/** + * Creates a [test][SpecBody.test]. + */ +fun TestContainer.then(description: String, body: TestBody.() -> Unit) { + test("then $description", body = body) +} + +/** + * Creates a [test][SpecBody.test]. + */ +fun TestContainer.its(description: String, body: TestBody.() -> Unit) { + test("its $description", body = body) +} + +//endregion \ No newline at end of file diff --git a/src/test/kotlin/testing/TopologyExtensions.kt b/src/test/kotlin/testing/TopologyExtensions.kt new file mode 100644 index 0000000..ad3e873 --- /dev/null +++ b/src/test/kotlin/testing/TopologyExtensions.kt @@ -0,0 +1,106 @@ +package testing + +import bgp.BGPRoute +import bgp.BaseBGP +import bgp.policies.interdomain.* +import bgp.policies.shortestpath.ShortestPathExtender +import core.routing.* + +object DummyBGPExtender : Extender { + override fun extend(route: BGPRoute, sender: Node): BGPRoute = route +} + +/** + * Entry point to start creating a topology that uses a BGP like protocol. + */ +fun bgpTopology(body: TopologyBuilder.() -> Unit): Topology { + + val builder = TopologyBuilder() + body(builder) + return builder.build() +} + +/** + * Entry point to declare a node in the topology. + */ +fun TopologyBuilder.node(nodePair: () -> Pair) { + + val pair = nodePair() + this.addNode(id = pair.first, protocol = pair.second) +} + +/** + * Set that a node is deploying the specified protocol. + */ +infix fun Int.deploying(protocol: BaseBGP): Pair { + return Pair(this, protocol) +} + +/** + * Entry point to declare a link in the topology. + */ +fun TopologyBuilder.link(createLink: () -> TemporaryLink) { + + val link = createLink() + this.link(link.tail, link.head, link.extender) +} + +/** + * Data structure characterizing a link. + */ +data class TemporaryLink(val tail: NodeID, val head: NodeID, var extender: Extender = DummyBGPExtender) + +/** + * Connector used to indicate the head of the link. + */ +infix fun Int.to(head: Int) = TemporaryLink(tail = this, head = head) + +/** + * Connector used to specify the extender of the link + */ +infix fun TemporaryLink.using(extender: Extender): TemporaryLink { + this.extender = extender + return this +} + +//region Shortest Path Routing + +/** + * Connector used to specify the cost of a link. + * Can only be used for shortest path routing. + */ +infix fun TemporaryLink.withCost(cost: Int): TemporaryLink { + this.extender = ShortestPathExtender(cost) + return this +} + +//endregion + +//region Interdomain Routing + +infix fun TopologyBuilder.peerplusLink(createLink: () -> TemporaryLink) { + val link = createLink() + this.link(link.tail, link.head, PeerplusExtender) +} + +infix fun TopologyBuilder.customerLink(createLink: () -> TemporaryLink) { + val link = createLink() + this.link(link.tail, link.head, CustomerExtender) +} + +infix fun TopologyBuilder.peerLink(createLink: () -> TemporaryLink) { + val link = createLink() + this.link(link.tail, link.head, PeerExtender) +} + +infix fun TopologyBuilder.providerLink(createLink: () -> TemporaryLink) { + val link = createLink() + this.link(link.tail, link.head, ProviderExtender) +} + +infix fun TopologyBuilder.siblingLink(createLink: () -> TemporaryLink) { + val link = createLink() + this.link(link.tail, link.head, SiblingExtender) +} + +//endregion \ No newline at end of file diff --git a/src/test/kotlin/testing/bgp/Fakes.kt b/src/test/kotlin/testing/bgp/Fakes.kt new file mode 100644 index 0000000..17d3783 --- /dev/null +++ b/src/test/kotlin/testing/bgp/Fakes.kt @@ -0,0 +1,41 @@ +package testing.bgp + +import bgp.BGP +import bgp.BGPRoute +import core.routing.Path +import core.routing.Node +import core.routing.NodeID +import core.routing.pathOf + +/** + * Created on 26-07-2017. + * + * @author David Fialho + * + * This file contains helper functions for tests with BGP. + */ + +/** + * Creates a node with the specified ID and deploying the BGP protocol. + */ +fun BGPNode(id: NodeID): Node { + return Node(id, protocol = BGP()) +} + +/** + * Shorter way to create a valid BGP route. + */ +fun route(localPref: Int, asPath: Path) = BGPRoute.with(localPref, asPath) + +/** + * Shorter way to create an invalid BGP route. + */ +fun invalid() = BGPRoute.invalid() + +/** + * Returns a path of nodes deploying the BGP protocol with the specified IDs and in the specified order. + */ +fun pathOf(vararg ids: Int): Path { + val nodes = ids.map { BGPNode(it) }.toTypedArray() + return pathOf(*nodes) +} diff --git a/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 0000000..ca6ee9c --- /dev/null +++ b/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline \ No newline at end of file