diff --git a/gradle.properties b/gradle.properties index 3bef4c2..e8074f8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -application.version=1.5 \ No newline at end of file +application.version=1.6 diff --git a/src/main/kotlin/bgp/BGP.kt b/src/main/kotlin/bgp/BGP.kt index 8bcddfa..428d0e0 100644 --- a/src/main/kotlin/bgp/BGP.kt +++ b/src/main/kotlin/bgp/BGP.kt @@ -64,12 +64,14 @@ abstract class BaseBGP(val mrai: Time, routingTable: RoutingTable): Pr } /** - * Announces [node] as the destination. + * Makes [node] advertise a destination and sets [defaultRoute] as the default route to reach that destination. + * The default route is immediately exported if it becomes the selected route. + * + * @param node the node to advertise destination + * @param defaultRoute the default route to reach the destination */ - override fun start(node: Node) { - val selfRoute = BGPRoute.self() - routingTable.update(node, selfRoute) - export(node) + override fun advertise(node: Node, defaultRoute: BGPRoute) { + process(node, node, defaultRoute) } /** diff --git a/src/main/kotlin/bgp/policies/interdomain/InterdomainExtenders.kt b/src/main/kotlin/bgp/policies/interdomain/InterdomainExtenders.kt index 15642fb..77c74da 100644 --- a/src/main/kotlin/bgp/policies/interdomain/InterdomainExtenders.kt +++ b/src/main/kotlin/bgp/policies/interdomain/InterdomainExtenders.kt @@ -10,18 +10,12 @@ import core.routing.Node * @author David Fialho */ -const val LOCAL_PREF_PEERPLUS: Int = 500000 -const val LOCAL_PREF_PEERSTAR: 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 || route.localPref == LOCAL_PREF_PEERSTAR -> BGPRoute.invalid() + route.localPref <= peerLocalPreference || route.localPref == peerstarLocalPreference -> BGPRoute.invalid() else -> customerRoute(asPath = route.asPath.append(sender)) } } @@ -33,7 +27,7 @@ object PeerExtender: Extender { override fun extend(route: BGPRoute, sender: Node): BGPRoute { return when { - route.localPref <= LOCAL_PREF_PEER || route.localPref == LOCAL_PREF_PEERSTAR -> BGPRoute.invalid() + route.localPref <= peerLocalPreference || route.localPref == peerstarLocalPreference -> BGPRoute.invalid() else -> peerRoute(asPath = route.asPath.append(sender)) } } @@ -57,7 +51,7 @@ object PeerplusExtender: Extender { override fun extend(route: BGPRoute, sender: Node): BGPRoute { return when { - route.localPref <= LOCAL_PREF_PEER || route.localPref == LOCAL_PREF_PEERSTAR -> BGPRoute.invalid() + route.localPref <= peerLocalPreference || route.localPref == peerstarLocalPreference -> BGPRoute.invalid() else -> peerplusRoute(asPath = route.asPath.append(sender)) } } @@ -69,7 +63,7 @@ object PeerstarExtender: Extender { override fun extend(route: BGPRoute, sender: Node): BGPRoute { return when { - route.localPref <= LOCAL_PREF_PEER || route.localPref == LOCAL_PREF_PEERSTAR -> BGPRoute.invalid() + route.localPref <= peerLocalPreference || route.localPref == peerstarLocalPreference -> BGPRoute.invalid() else -> peerstarRoute(asPath = route.asPath.append(sender)) } } diff --git a/src/main/kotlin/bgp/policies/interdomain/Routes.kt b/src/main/kotlin/bgp/policies/interdomain/Routes.kt index db6c805..50c96ab 100644 --- a/src/main/kotlin/bgp/policies/interdomain/Routes.kt +++ b/src/main/kotlin/bgp/policies/interdomain/Routes.kt @@ -12,32 +12,41 @@ import core.routing.emptyPath * This file contains methods to construct interdomain routes. */ + +// LOCAL-PREFs for each interdomain route +val peerplusLocalPreference: Int = 500000 +val peerstarLocalPreference: Int = 400000 +val customerLocalPreference: Int = 300000 +val peerLocalPreference: Int = 200000 +val providerLocalPreference: Int = 100000 + + /** * Returns a peer+ route. */ fun peerplusRoute(siblingHops: Int = 0, asPath: Path = emptyPath()) - = BGPRoute.with(localPref = LOCAL_PREF_PEERPLUS - siblingHops, asPath = asPath) + = BGPRoute.with(localPref = peerplusLocalPreference - siblingHops, asPath = asPath) /** * Returns a peer* route. */ fun peerstarRoute(siblingHops: Int = 0, asPath: Path = emptyPath()) - = BGPRoute.with(localPref = LOCAL_PREF_PEERSTAR - siblingHops, asPath = asPath) + = BGPRoute.with(localPref = peerstarLocalPreference - siblingHops, asPath = asPath) /** * Returns a customer route. */ fun customerRoute(siblingHops: Int = 0, asPath: Path = emptyPath()) - = BGPRoute.with(localPref = LOCAL_PREF_CUSTOMER - siblingHops, asPath = asPath) + = BGPRoute.with(localPref = customerLocalPreference - siblingHops, asPath = asPath) /** * Returns a peer route. */ fun peerRoute(siblingHops: Int = 0, asPath: Path = emptyPath()) - = BGPRoute.with(localPref = LOCAL_PREF_PEER - siblingHops, asPath = asPath) + = BGPRoute.with(localPref = peerLocalPreference - siblingHops, asPath = asPath) /** * Returns a provider route. */ fun providerRoute(siblingHops: Int = 0, asPath: Path = emptyPath()) - = BGPRoute.with(localPref = LOCAL_PREF_PROVIDER - siblingHops, asPath = asPath) + = BGPRoute.with(localPref = providerLocalPreference - siblingHops, asPath = asPath) diff --git a/src/main/kotlin/core/routing/Extender.kt b/src/main/kotlin/core/routing/Extender.kt index 391cffc..a643872 100644 --- a/src/main/kotlin/core/routing/Extender.kt +++ b/src/main/kotlin/core/routing/Extender.kt @@ -9,7 +9,7 @@ package core.routing * 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 + * TODO @doc - improve the documentation for extender */ interface Extender { diff --git a/src/main/kotlin/core/routing/Node.kt b/src/main/kotlin/core/routing/Node.kt index 9f4a7b7..8ccd95f 100644 --- a/src/main/kotlin/core/routing/Node.kt +++ b/src/main/kotlin/core/routing/Node.kt @@ -1,5 +1,6 @@ package core.routing +import core.simulator.Advertiser import core.simulator.notifications.BasicNotifier import core.simulator.notifications.MessageReceivedNotification import core.simulator.notifications.MessageSentNotification @@ -20,7 +21,7 @@ typealias NodeID = Int * * @property id The ID of the node. This ID uniquely identifies it inside a topology */ -class Node(val id: NodeID, val protocol: Protocol) { +class Node(override val id: NodeID, val protocol: Protocol) : Advertiser { /** * Collection containing the in-neighbors of this node. @@ -39,10 +40,13 @@ class Node(val id: NodeID, val protocol: Protocol) { } /** - * Starts the protocol deployed by this node. + * This node advertises a destination according to the specification of the deployed protocol. + * It sets a default route for the destination. This route maybe sent to neighbors. + * + * @param defaultRoute the default route to set for the destination */ - fun start() { - protocol.start(this) + override fun advertise(defaultRoute: R) { + protocol.advertise(this, defaultRoute) } /** @@ -82,7 +86,7 @@ class Node(val id: NodeID, val protocol: Protocol) { /** * Resets the node state. */ - fun reset() { + override fun reset() { protocol.reset() inNeighbors.forEach { it.exporter.reset() } } diff --git a/src/main/kotlin/core/routing/Protocol.kt b/src/main/kotlin/core/routing/Protocol.kt index a03a998..21de854 100644 --- a/src/main/kotlin/core/routing/Protocol.kt +++ b/src/main/kotlin/core/routing/Protocol.kt @@ -21,9 +21,12 @@ interface Protocol { fun addInNeighbor(neighbor: Neighbor) /** - * Starts this protocol. + * Makes [node] advertise a destination and sets [defaultRoute] as the default route to reach that destination. + * + * @param node the node to advertise destination + * @param defaultRoute the default route to reach the destination */ - fun start(node: Node) + fun advertise(node: Node, defaultRoute: R) /** * Processes an incoming routing message. diff --git a/src/main/kotlin/core/simulator/AdvertiseEvent.kt b/src/main/kotlin/core/simulator/AdvertiseEvent.kt new file mode 100644 index 0000000..120b782 --- /dev/null +++ b/src/main/kotlin/core/simulator/AdvertiseEvent.kt @@ -0,0 +1,18 @@ +package core.simulator + +import core.routing.Route + +/** + * Created on 09-11-2017 + * + * @author David Fialho + */ +class AdvertiseEvent(private val advertiser: Advertiser, private val route: R): Event { + + /** + * Processes this event. + */ + override fun processIt() { + advertiser.advertise(route) + } +} \ No newline at end of file diff --git a/src/main/kotlin/core/simulator/Advertisement.kt b/src/main/kotlin/core/simulator/Advertisement.kt new file mode 100644 index 0000000..df45125 --- /dev/null +++ b/src/main/kotlin/core/simulator/Advertisement.kt @@ -0,0 +1,12 @@ +package core.simulator + +import core.routing.Route + +/** + * Created on 08-11-2017 + * + * @author David Fialho + * + * An advertisement is a data class that specifies the advertiser and the time at which it will/did take place. + */ +data class Advertisement(val advertiser: Advertiser, val route: R, val time: Time = 0) \ No newline at end of file diff --git a/src/main/kotlin/core/simulator/Advertiser.kt b/src/main/kotlin/core/simulator/Advertiser.kt new file mode 100644 index 0000000..bb4eba8 --- /dev/null +++ b/src/main/kotlin/core/simulator/Advertiser.kt @@ -0,0 +1,31 @@ +package core.simulator + +import core.routing.NodeID +import core.routing.Route + +/** + * Created on 08-11-2017 + * + * @author David Fialho + * + * An advertiser is some entity that can advertise destinations. + */ +interface Advertiser { + + /** + * Each advertiser is associated with a unique ID. + */ + val id: NodeID + + /** + * Advertises some destination. + * + * @param defaultRoute the default route for the destination. + */ + fun advertise(defaultRoute: R) + + /** + * Resets the state of the advertiser. This may be required before advertising. + */ + fun reset() +} \ No newline at end of file diff --git a/src/main/kotlin/core/simulator/Engine.kt b/src/main/kotlin/core/simulator/Engine.kt index 65080c6..dfa6fe0 100644 --- a/src/main/kotlin/core/simulator/Engine.kt +++ b/src/main/kotlin/core/simulator/Engine.kt @@ -1,6 +1,6 @@ package core.simulator -import core.routing.Node +import core.routing.Route import core.routing.Topology import core.simulator.notifications.BasicNotifier import core.simulator.notifications.EndNotification @@ -36,27 +36,60 @@ object Engine { } /** - * Runs the simulation for the given destination. + * Runs a simulation with a single advertisement. + * + * The scheduler is cleared before running the simulation. + * * 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 + * @param topology the topology used for the simulation + * @param advertisement the single advertisement to start off 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 { + fun simulate(topology: Topology, advertisement: Advertisement, + threshold: Time = Int.MAX_VALUE): Boolean { + return simulate(topology, listOf(advertisement), threshold) + } + + /** + * Runs a simulation with one or multiple advertisements. + * + * The scheduler is cleared before running the simulation. + * + * 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 advertisements a list containing all the advertisements to occur in the simulation + * @param threshold a threshold value for the simulation + * @return true if the simulation terminated before the specified threshold or false if otherwise + * @throws IllegalArgumentException if the advertisement plan is empty + */ + @Throws(IllegalArgumentException::class) + fun simulate(topology: Topology<*>, advertisements: List>, + threshold: Time = Int.MAX_VALUE): Boolean { + + if (advertisements.isEmpty()) { + throw IllegalArgumentException("a simulation requires at least one advertisement") + } // 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() + // Schedule advertisements specified in the strategy + for (advertisement in advertisements) { + scheduler.schedule(advertisement) + } + // Flag that will indicate whether or not the simulation finished before the threshold was reached var terminatedBeforeThreshold = true + while (scheduler.hasEvents()) { val event = scheduler.nextEvent() @@ -72,6 +105,7 @@ object Engine { event.processIt() } + // Notify listeners the simulation ended BasicNotifier.notifyEnd(EndNotification(topology)) return terminatedBeforeThreshold @@ -92,6 +126,14 @@ object Engine { } + +/** + * Schedules an advertisement event. + */ +private fun Scheduler.schedule(advertisement: Advertisement) { + schedule(AdvertiseEvent(advertisement.advertiser, advertisement.route), advertisement.time) +} + /** * Cleaner way to access the simulation time. */ diff --git a/src/main/kotlin/core/simulator/RandomDelayGenerator.kt b/src/main/kotlin/core/simulator/RandomDelayGenerator.kt index a0684ce..99f0815 100644 --- a/src/main/kotlin/core/simulator/RandomDelayGenerator.kt +++ b/src/main/kotlin/core/simulator/RandomDelayGenerator.kt @@ -1,7 +1,6 @@ package core.simulator -import java.lang.System -import java.util.Random +import java.util.* /** * Created on 22-07-2017 @@ -31,14 +30,17 @@ private constructor(override val min: Time, override val max: Time, seed: Long): * @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 IllegalArgumentException 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") + if (min < 0) { + throw IllegalArgumentException("minimum must be a non-negative value, but was $min") + } + + if (max < min) { + throw IllegalArgumentException("maximum delay can not be lower than minimum ($max < $min)") } return RandomDelayGenerator(min, max, seed) diff --git a/src/main/kotlin/io/AdvertisementInfo.kt b/src/main/kotlin/io/AdvertisementInfo.kt new file mode 100644 index 0000000..2b62c8b --- /dev/null +++ b/src/main/kotlin/io/AdvertisementInfo.kt @@ -0,0 +1,12 @@ +package io + +import core.routing.NodeID +import core.routing.Route +import core.simulator.Time + +/** + * Created on 14-11-2017 + * + * @author David Fialho + */ +data class AdvertisementInfo(val advertiserID: NodeID, val defaultRoute: R, val time: Time) \ No newline at end of file diff --git a/src/main/kotlin/io/ExtenderParseFunctions.kt b/src/main/kotlin/io/ExtenderParseFunctions.kt deleted file mode 100644 index 9e260db..0000000 --- a/src/main/kotlin/io/ExtenderParseFunctions.kt +++ /dev/null @@ -1,49 +0,0 @@ -package io - -import bgp.BGPRoute -import bgp.policies.interdomain.* -import core.routing.Extender - -/** - * Created on 31-08-2017 - * - * @author David Fialho - * - * This file contains functions to parse extenders from labels. - */ - -/** - * Parses an Interdomain Extender. The supported labels are: - * - * R+ - parsed as a PeerplusExtender - * R* - parsed as a PeerstarExtender - * C - parsed as a CustomerExtender - * R - parsed as a PeerExtender - * P - parsed as a ProviderExtender - * S - parsed as a SiblingExtender - * - * This function is NOT case sensitive! - * - * @param label the label of the extender - * @param lineNumber the number of the line in which the label was found (used for the parse exception message only) - * @return the extender parsed from the label - * @throws ParseException if the label is not recognized - */ -@Throws(ParseException::class) -fun parseInterdomainExtender(label: String, lineNumber: Int): Extender { - - return when (label.toLowerCase()) { - "r+" -> PeerplusExtender - "r*" -> PeerstarExtender - "c" -> CustomerExtender - "r" -> PeerExtender - "p" -> ProviderExtender - "s" -> SiblingExtender - else -> throw ParseException("Extender label `$label` was not recognized: " + - "must be either R+, R*, C, R, P, or S", lineNumber) - } -} - -fun parseInterdomainExtender(label: String): Extender { - return parseInterdomainExtender(label, lineNumber = 0) -} \ No newline at end of file diff --git a/src/main/kotlin/io/InterdomainAdvertisementReader.kt b/src/main/kotlin/io/InterdomainAdvertisementReader.kt new file mode 100644 index 0000000..f82bea9 --- /dev/null +++ b/src/main/kotlin/io/InterdomainAdvertisementReader.kt @@ -0,0 +1,92 @@ +package io + +import bgp.BGPRoute +import core.routing.pathOf +import utils.toNonNegativeInt +import java.io.File +import java.io.FileReader +import java.io.IOException +import java.io.Reader + + +private val DEFAULT_ADVERTISING_TIME = 0 +private val DEFAULT_DEFAULT_ROUTE = BGPRoute.self() + + +/** + * Created on 14-11-2017 + * + * @author David Fialho + */ +class InterdomainAdvertisementReader(reader: Reader): AutoCloseable { + + constructor(file: File): this(FileReader(file)) + + private class Handler(val advertisements: MutableList>): KeyValueParser.Handler { + + /** + * Invoked when a new entry is parsed. + * + * @param entry the parsed entry + * @param currentLine line number where the node was parsed + */ + override fun onEntry(entry: KeyValueParser.Entry, currentLine: Int) { + + if (entry.values.size > 2) { + throw ParseException("only 2 values are expected for an advertiser, " + + "but ${entry.values.size} were given", currentLine) + } + + // The key corresponds to the advertiser ID + val advertiserID = try { + entry.key.toNonNegativeInt() + } catch (e: NumberFormatException) { + throw ParseException("advertising node ID must be a non-negative integer value, " + + "but was '${entry.key}'", currentLine) + } + + // The first value is the advertising time - this value is NOT mandatory + // The KeyValueParser ensure that there is at least one value always, even if it is blank + val timeValue = entry.values[0] + val time = if (timeValue.isBlank()) DEFAULT_ADVERTISING_TIME else try { + timeValue.toNonNegativeInt() + } catch (e: NumberFormatException) { + throw ParseException("advertising time must be a non-negative integer value, " + + "but was '$timeValue'", currentLine) + } + + // The second value is a cost label for the default route's local preference - this value is NOT mandatory + val defaultRoute = if (entry.values.size == 1 || entry.values[1].isBlank()) { + DEFAULT_DEFAULT_ROUTE + } else { + BGPRoute.with(parseInterdomainCost(entry.values[1], currentLine), pathOf()) + } + + advertisements.add(AdvertisementInfo(advertiserID, defaultRoute, time)) + } + + } + + private val parser = KeyValueParser(reader) + + /** + * Reads a map, mapping advertiser IDs to pairs of default routes and advertising times. + * + * @throws ParseException if the format of the input is invalid + * @throws IOException if an IO error occurs + * @return a list of advertisements read from the stream + */ + @Throws(ParseException::class, IOException::class) + fun read(): List> { + val advertisements = ArrayList>() + parser.parse(Handler(advertisements)) + return advertisements + } + + /** + * Closes the underlying stream. + */ + override fun close() { + parser.close() + } +} diff --git a/src/main/kotlin/io/InterdomainConvertingFunctions.kt b/src/main/kotlin/io/InterdomainConvertingFunctions.kt new file mode 100644 index 0000000..974dffd --- /dev/null +++ b/src/main/kotlin/io/InterdomainConvertingFunctions.kt @@ -0,0 +1,133 @@ +package io + +import bgp.BGPRoute +import bgp.policies.interdomain.* +import core.routing.Extender + +/** + * Created on 16-11-2017 + * + * @author David Fialho + * + * This file contains a set of functions and extension methods to convert labels into extenders, or costs, or routes, + * a functions and extension methods to convert in the other direction: from extenders, costs, or routes, to labels. + */ + +// +// Convert from string labels to classes +// + +/** + * Parses String as an InterdomainExtender and returns the result. + * This method is case sensitive. + * + * Valid string labels and corresponding extenders: + * R+ -> PeerplusExtender + * R* -> PeerstarExtender + * C -> CustomerExtender + * R -> PeerExtender + * P -> ProviderExtender + * S -> SiblingExtender + * + * @return the extender corresponding to the string + * @throws InvalidLabelException if the string does not match any valid extension label + */ +@Throws(InvalidLabelException::class) +fun String.toInterdomainExtender(): Extender = when (this) { + + "R+" -> PeerplusExtender + "R*" -> PeerstarExtender + "C" -> CustomerExtender + "R" -> PeerExtender + "P" -> ProviderExtender + "S" -> SiblingExtender + else -> throw InvalidLabelException("extender label '$this' was not recognized, " + + "it but must be one of R+, R*, C, R, P, and S") +} + +/** + * Parses String as an interdomain local preference value and returns the result. + * This method is case sensitive. + * + * Valid string labels and corresponding local preference values: + * r+ -> peer+ route local preference + * r* -> peer* route local preference + * c -> customer route local preference + * r -> peer route local preference + * p -> provider route local preference + * + * @return the local preference corresponding to the cost label + * @throws InvalidLabelException if the string does not match any valid cost label + */ +@Throws(InvalidLabelException::class) +fun String.toInterdomainLocalPreference(): Int = when (this) { + + "r+" -> peerplusLocalPreference + "r*" -> peerstarLocalPreference + "c" -> customerLocalPreference + "r" -> peerLocalPreference + "p" -> providerLocalPreference + else -> throw InvalidLabelException("cost label '$this' was not recognized, " + + "it but must be one of r+, r*, c, r, and p") +} + +/** + * Tries to convert [label] to an Interdomain extender. If that fails, then it throws a ParseException. + * @see #toInterdomainExtender() for more details about how the label is parsed. + * + * @param label the label to parse + * @param lineNumber the number of the line in which the label was found (used for the parse exception message only) + * @return the extender corresponding to [label] + * @throws ParseException if the label is not recognized + */ +@Throws(ParseException::class) +fun parseInterdomainExtender(label: String, lineNumber: Int = 0): Extender { + + return try { + label.toInterdomainExtender() + } catch (e: InvalidLabelException) { + throw ParseException(e.message ?: "", lineNumber) + } +} + + +// This function is required because it is passed as parameter, which requires a function with this specific signature +@Suppress("NOTHING_TO_INLINE") +@Throws(ParseException::class) +inline fun parseInterdomainExtender(label: String): Extender = parseInterdomainExtender(label, lineNumber = 0) + + +/** + * Tries to convert [label] to an Interdomain local preference value. If that fails, then it throws a ParseException. + * @see #toInterdomainLocalPreference() for more details about how the label is parsed. + * + * @param label the label to parse + * @param lineNumber the number of the line in which the label was found (used for the parse exception message only) + * @return the local preference value corresponding to [label] + * @throws ParseException if the label is not recognized + */ +@Suppress("NOTHING_TO_INLINE") +@Throws(ParseException::class) +inline fun parseInterdomainCost(label: String, lineNumber: Int): Int { + + return try { + label.toInterdomainLocalPreference() + } catch (e: InvalidLabelException) { + throw ParseException(e.message ?: "", lineNumber) + } +} + +// +// Convert from classes to string objects +// + +fun Int.toInterdomainLabel(): String = when (this) { + peerplusLocalPreference -> "r+" + peerstarLocalPreference -> "r*" + customerLocalPreference -> "c" + peerLocalPreference -> "r" + providerLocalPreference -> "p" + BGPRoute.invalid().localPref -> BGPRoute.invalid().toString() + BGPRoute.self().localPref -> BGPRoute.self().toString() + else -> this.toString() +} \ No newline at end of file diff --git a/src/main/kotlin/io/InterdomainTopologyReader.kt b/src/main/kotlin/io/InterdomainTopologyReader.kt index a46a539..00c23dd 100644 --- a/src/main/kotlin/io/InterdomainTopologyReader.kt +++ b/src/main/kotlin/io/InterdomainTopologyReader.kt @@ -2,7 +2,7 @@ package io import bgp.* import core.routing.* -import io.TopologyParser.Handler +import core.simulator.Time import utils.toNonNegativeInt import java.io.* @@ -11,16 +11,88 @@ import java.io.* * * @author David Fialho */ -class InterdomainTopologyReader(reader: Reader): TopologyReader, Closeable, Handler { +class InterdomainTopologyReader(reader: Reader, private val forcedMRAI: Time? = null) + : TopologyReader, Closeable { /** * Provides option to create a reader with a file object. */ @Throws(FileNotFoundException::class) - constructor(file: File): this(FileReader(file)) + constructor(file: File, forcedMRAI: Time? = null): this(FileReader(file), forcedMRAI) + + private val parser = TopologyParser(reader) + + private inner class InterdomainHandler(val builder: TopologyBuilder) + : TopologyParser.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 + */ + 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] + + // Use the "forced MRAI" value if set. Otherwise, use the value specified in the topology file. + val mrai = forcedMRAI ?: try { + values[1].toNonNegativeInt() + } catch (e: NumberFormatException) { + throw ParseException("MRAI must be a non-negative integer number, but was ${values[1]}", currentLine) + } + + // TODO @refactor - make this case sensitive + val protocol = when (protocolLabel.toLowerCase()) { + "bgp" -> BGP(mrai) + "ssbgp" -> SSBGP(mrai) + "issbgp" -> ISSBGP(mrai) + "ssbgp2" -> SSBGP2(mrai) + "issbgp2" -> ISSBGP2(mrai) + else -> throw ParseException( + "protocol label `$protocolLabel` was not recognized: supported labels are BGP, " + + "SSBGP, ISSBGP, SSBGP2, and ISSBGP2", currentLine) + } + + try { + builder.addNode(id, protocol) + + } catch (e: ElementExistsException) { + throw ParseException(e.message!!, currentLine) + } + } - private val builder = TopologyBuilder() - private val parser = TopologyParser(reader, this) + /** + * 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 extender label value", currentLine) + } + + val extender = parseInterdomainExtender(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) + } + } + } /** * Returns a Topology object that is represented in the input source. @@ -33,75 +105,11 @@ class InterdomainTopologyReader(reader: Reader): TopologyReader, Close */ @Throws(IOException::class, ParseException::class) override fun read(): Topology { - parser.parse() + val builder = TopologyBuilder() + parser.parse(InterdomainHandler(builder)) 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 = try { - values[1].toNonNegativeInt() - } catch (e: NumberFormatException) { - throw ParseException("Failed to parse `${values[1]}`: must be a non-negative integer number", currentLine) - } - - val protocol = when (protocolLabel.toLowerCase()) { - "bgp" -> BGP(mrai) - "ssbgp" -> SSBGP(mrai) - "issbgp" -> ISSBGP(mrai) - "ssbgp2" -> SSBGP2(mrai) - "issbgp2" -> ISSBGP2(mrai) - else -> throw ParseException( - "Protocol label `$protocolLabel` was not recognized: supported labels are BGP, " + - "SSBGP, ISSBGP, SSBGP2, and ISSBGP2", 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 = parseInterdomainExtender(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) - } - } - /** * Closes the stream and releases any system resources associated with it. */ diff --git a/src/main/kotlin/io/InvalidLabelException.kt b/src/main/kotlin/io/InvalidLabelException.kt new file mode 100644 index 0000000..6a7320a --- /dev/null +++ b/src/main/kotlin/io/InvalidLabelException.kt @@ -0,0 +1,10 @@ +package io + +/** + * Created on 16-11-2017 + * + * @author David Fialho + * + * Thrown to indicate that a string label is not valid in that context. + */ +class InvalidLabelException(message: String): Exception(message) \ No newline at end of file diff --git a/src/main/kotlin/io/KeyValueParser.kt b/src/main/kotlin/io/KeyValueParser.kt new file mode 100644 index 0000000..533bcd7 --- /dev/null +++ b/src/main/kotlin/io/KeyValueParser.kt @@ -0,0 +1,108 @@ +package io + +import java.io.BufferedReader +import java.io.Closeable +import java.io.IOException +import java.io.Reader + +/** + * Created on 29-08-2017 + * + * @author David Fialho + * + * A parser for key-values formatted streams. + * + * A key-values formatted stream have multiple entries (one in each line) with the form: + * + * key = value1 | value2 | ... | valueN + * + * The key is separated from the values with an equals sign '='. + * A key can be associated with multiple values. Each one separated by a '|' character. + * + * Each parser is associated with an handler. This handler is notified every time a new entry is parsed from the stream. + * The handler is then responsible for parsing the key and values, ensuring they are valid according to the required + * specifications. + * + * Entries are parsed in the same order they are described in the stream. Thus, the handler is guaranteed to be + * notified of new entries in that exact same order. + */ +class KeyValueParser(reader: Reader): Closeable { + + /** + * Handlers are notified once a new key-value entry is parsed. + * + * Subclasses should use this method to parse the key and values, ensuring they are valid according to their + * unique specifications. + */ + interface Handler { + + /** + * Invoked when a new entry is parsed. + * + * @param entry the parsed entry + * @param currentLine line number where the node was parsed + */ + fun onEntry(entry: Entry, currentLine: Int) + + } + + data class Entry(val key: String, val values: List) + + /** + * The underlying reader used to read the stream. + */ + private val reader = BufferedReader(reader) + + /** + * Parses the stream, invoking the handler every time a new entry is parsed. + * + * @param handler the handler that is notified when a new entry is parsed + * @throws IOException If an I/O error occurs + * @throws ParseException if the format of the stream is not valid + */ + @Throws(IOException::class, ParseException::class) + fun parse(handler: KeyValueParser.Handler) { + + // Read the first line - throw error if empty + var line: String? = reader.readLine() ?: throw ParseException("file is empty", lineNumber = 1) + + var currentLine = 1 + while (line != null) { + + // Ignore blank lines + if (!line.isBlank()) { + val entry = parseEntry(line, currentLine) + handler.onEntry(entry, currentLine) + } + + line = reader.readLine() + currentLine++ + } + } + + private fun parseEntry(line: String, currentLine: Int): Entry { + + // 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("=", limit = 2) + + if (keyAndValues.size < 2) { + throw ParseException("line $currentLine$ is missing an equal sign '=' to " + + "distinguish between key and values", currentLine) + } + + val key = keyAndValues[0].trim() + val values = keyAndValues[1].split("|").map { it.trim() }.toList() + + return Entry(key, values) + } + + /** + * Closes the stream and releases any system resources associated with it. + */ + override fun close() { + reader.close() + } +} \ No newline at end of file diff --git a/src/main/kotlin/io/KeyValueWriter.kt b/src/main/kotlin/io/KeyValueWriter.kt new file mode 100644 index 0000000..bbcc4ce --- /dev/null +++ b/src/main/kotlin/io/KeyValueWriter.kt @@ -0,0 +1,48 @@ +package io + +import java.io.BufferedWriter +import java.io.File +import java.io.FileWriter + +/** + * Created on 09-11-2017 + * + * @author David Fialho + * + * Writer that specializes in writing key/value pairs. + * Each key/value pair is written to a new line. + */ +class KeyValueWriter(private val writer: BufferedWriter): AutoCloseable { + + /** + * Helper constructor to create a KeyValueWriter from a File instance. + */ + constructor(file: File): this(BufferedWriter(FileWriter(file))) + + /** + * Writes a new key value pair. + */ + fun write(key: Any, value: Any) { + writer.apply { + write("$key = $value") + newLine() + } + } + + /** + * Writes a new key value pair. + */ + fun write(pair: Pair) { + writer.apply { + write("${pair.first} = ${pair.second}") + newLine() + } + } + + /** + * Closes the underlying buffered writer. + */ + override fun close() { + writer.close() + } +} \ No newline at end of file diff --git a/src/main/kotlin/io/Metadata.kt b/src/main/kotlin/io/Metadata.kt deleted file mode 100644 index d66af4a..0000000 --- a/src/main/kotlin/io/Metadata.kt +++ /dev/null @@ -1,56 +0,0 @@ -package io - -import core.simulator.Time -import java.io.File -import java.io.FileWriter -import java.io.Writer -import java.time.Instant - -/** - * Created on 27-10-2017 - * - * @author David Fialho - * - * Data class to hold metadata information. - * It also provides methods to output the information to a file or a stream. - */ -data class Metadata( - val version: String, - val startInstant: Instant, - val finishInstant: Instant, - val topologyFilename: String, - val stubsFilename: String?, - val destinationID: Int, - val minDelay: Time, - val maxDelay: Time, - val threshold: Time -) { - - /** - * Prints metadata information using the specified writer. - */ - fun print(writer: Writer) { - - writer.apply { - write("version = $version\n") - write("start datetime = $startInstant\n") - write("finish datetime = $finishInstant\n") - write("topology file = $topologyFilename\n") - if (stubsFilename != null) write("stubs file = $stubsFilename\n") - write("destination = $destinationID\n") - write("min delay = $minDelay\n") - write("max delay = $maxDelay\n") - write("threshold = $threshold\n") - } - } - - /** - * Prints metadata information to the specified file. - */ - fun print(outputFile: File) { - FileWriter(outputFile).use { - print(it) - } - } - -} diff --git a/src/main/kotlin/io/NodeDataReporter.kt b/src/main/kotlin/io/NodeDataReporter.kt index 7485202..a8f9378 100644 --- a/src/main/kotlin/io/NodeDataReporter.kt +++ b/src/main/kotlin/io/NodeDataReporter.kt @@ -53,7 +53,7 @@ class NodeDataReporter(private val outputFile: File): Reporter { it.printRecord( simulation, nodeID, - selectedRoute.localPref, + selectedRoute.localPref.toInterdomainLabel(), selectedRoute.asPath.nextHop()?.id, selectedRoute.asPath.size, data.terminationTimes[nodeID] diff --git a/src/main/kotlin/io/Prettifiers.kt b/src/main/kotlin/io/Prettifiers.kt new file mode 100644 index 0000000..18cf1ea --- /dev/null +++ b/src/main/kotlin/io/Prettifiers.kt @@ -0,0 +1,37 @@ +package io + +import bgp.BGPRoute +import core.routing.Node +import core.routing.Path +import core.routing.Route + +/** + * Created on 16-11-2017 + * + * @author David Fialho + * + * This files contains extension methods to prettify some classes that are output to the user. + */ + +/** + * Returns the node's ID as a string. + */ +fun Node.pretty(): String = id.toString() + +/** + * Returns the IDs of the nodes in the path separated by a comma. + */ +fun Path.pretty(): String = joinToString(transform = {it.pretty()}) + +/** + * Converts the local preference of a BGP route to the corresponding interdomain label and the AS path to a path. + * These are put inside parenthesis and separated by a comma. + */ +fun BGPRoute.pretty(): String { + + if (this === BGPRoute.invalid() || this === BGPRoute.self()) { + return toString() + } + + return "(${localPref.toInterdomainLabel()}, ${asPath.pretty()})" +} \ No newline at end of file diff --git a/src/main/kotlin/io/StubParser.kt b/src/main/kotlin/io/StubParser.kt index a5a47cf..c43848b 100644 --- a/src/main/kotlin/io/StubParser.kt +++ b/src/main/kotlin/io/StubParser.kt @@ -11,20 +11,14 @@ import java.io.* */ class StubParser(reader: Reader): Closeable { - companion object { - - /** - * Creates a stub parser with a file reader to parse a file. - * - * @throws FileNotFoundException if the file does not exist, is a directory rather than a regular file, or for - * some other reason cannot be opened for reading. - */ - @Throws(FileNotFoundException::class) - fun useFile(stubFile: File): StubParser { - return StubParser(FileReader(stubFile)) - } - - } + /** + * Creates a stub parser with a file reader to parse a file. + * + * @throws FileNotFoundException if the file does not exist, is a directory rather than a regular file, or for + * some other reason cannot be opened for reading. + */ + @Throws(FileNotFoundException::class) + constructor(stubFile: File): this(FileReader(stubFile)) /** * Interface for an handler that is called when a new stub item is parsed. diff --git a/src/main/kotlin/io/TopologyParser.kt b/src/main/kotlin/io/TopologyParser.kt index d79b08a..7280f4a 100644 --- a/src/main/kotlin/io/TopologyParser.kt +++ b/src/main/kotlin/io/TopologyParser.kt @@ -1,7 +1,7 @@ package io import core.routing.NodeID -import java.io.BufferedReader +import utils.toNonNegativeInt import java.io.Closeable import java.io.IOException import java.io.Reader @@ -11,7 +11,7 @@ import java.io.Reader * * @author David Fialho */ -class TopologyParser(reader: Reader, private val handler: TopologyParser.Handler): Closeable { +class TopologyParser(reader: Reader): Closeable { /** * Handlers are notified once a new topology item (a node or a link) is parsed. @@ -40,106 +40,83 @@ class TopologyParser(reader: Reader, private val handler: TopologyParser.Handler } - /** - * 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) + private class KeyValueHandler(val handler: TopologyParser.Handler): KeyValueParser.Handler { - 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 + /** + * Invoked when a new entry is parsed. + * + * @param entry the parsed entry + * @param currentLine line number where the node was parsed + */ + override fun onEntry(entry: KeyValueParser.Entry, currentLine: Int) { - // Split the key from the values - val keyAndValues = line.split("=") + val values = entry.values - if (keyAndValues.size != 2) { - throw ParseException("Line $currentLine$ contains multiple equal signs(=): only one is permitted per line", - currentLine) - } + when (entry.key.toLowerCase()) { + "node" -> { - val key = keyAndValues[0].trim().toLowerCase() - val values = keyAndValues[1].split("|").map { it.trim().toLowerCase() }.toList() + // The first value is the node ID - this value is mandatory + if (values.isEmpty() || (values.size == 1 && values[0].isEmpty())) { + throw ParseException("node entry is missing the ID value", currentLine) + } - when (key) { - "node" -> { + val nodeID = parseNodeID(values[0], currentLine) - // 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) + // The remaining values should be parsed by the Topology Reader according to + // its required specifications + handler.onNodeItem(nodeID, values.subList(1, values.lastIndex + 1), currentLine) } + "link" -> { - // Node ID is the first value - val nodeID = parseNodeID(values[0], currentLine) + // The first two values are the tail and head nodes of the link + if (values.size < 2 || values[0].isBlank() || values[1].isBlank()) { + throw ParseException("link entry is missing required values: tail node ID and/or head node ID", + currentLine) + } - handler.onNodeItem(nodeID, values.subList(1, values.lastIndex + 1), currentLine) - } - "link" -> { + val tailID = parseNodeID(values[0], currentLine) + val headID = parseNodeID(values[1], currentLine) - // 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) + handler.onLinkItem(tailID, headID, values.subList(2, values.lastIndex + 1), currentLine) + } + else -> { + throw ParseException("invalid key `${entry.key}`: supported keys are 'node' or 'link'", 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 { + private fun parseNodeID(value: String, currentLine: Int): Int { - try { - val intValue = value.toInt() - if (intValue < 0) { - throw NumberFormatException() + try { + return value.toNonNegativeInt() + } catch (e: NumberFormatException) { + throw ParseException("a node ID must be a non-negative value, but was `$value`", currentLine) } + } + } - return intValue + /** + * The topology parser is based on a key-value parser. + * It uses this parser to handle identifying entries. + */ + private val parser = KeyValueParser(reader) - } catch (e: NumberFormatException) { - throw ParseException("Failed to parse node ID from value `$value`: must be a non-negative " + - "integer value", currentLine) - } + /** + * Parses the stream invoking the handler once a new node or link is parsed. + * + * @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(handler: TopologyParser.Handler) { + parser.parse(KeyValueHandler(handler)) } /** * Closes the stream and releases any system resources associated with it. */ override fun close() { - reader.close() + parser.close() } } diff --git a/src/main/kotlin/io/TopologyReaderHandler.kt b/src/main/kotlin/io/TopologyReaderHandler.kt deleted file mode 100644 index c519720..0000000 --- a/src/main/kotlin/io/TopologyReaderHandler.kt +++ /dev/null @@ -1,52 +0,0 @@ -package io - -import bgp.BGPRoute -import core.routing.Route -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/simulation/AdvertiserDB.kt b/src/main/kotlin/simulation/AdvertiserDB.kt new file mode 100644 index 0000000..6fb8559 --- /dev/null +++ b/src/main/kotlin/simulation/AdvertiserDB.kt @@ -0,0 +1,141 @@ +package simulation + +import core.routing.* +import core.simulator.Advertiser +import io.ParseException +import io.StubParser +import java.io.File +import java.io.IOException + +/** + * Created on 13-11-2017 + * + * @author David Fialho + * + * The Advertiser DB is an abstraction to obtain advertising nodes from both the topology (if they + * are included in it) and a database of stub nodes. The stub database is optional. If no database + * is specified, advertising node will only be taken from the topology. + * + * @property topology the topology to get advertisers from + * @property stubsFile the file specifying stubs to use as advertisers + * @param stubsProtocol the protocol to assign to stubs + * @param parseExtender the function used to parse extender labels to actual extenders + */ +class AdvertiserDB( + val topology: Topology, + val stubsFile: File?, + private val stubsProtocol: Protocol, + private val parseExtender: (String) -> Extender +) { + + /** + * Handler class used to create stubs found in a stubs file while it is parsed. + */ + private class StubsCreator( + private val stubIDs: List, + private val topology: Topology, + private val stubsProtocol: Protocol, + private val parseExtender: (String) -> Extender + ): StubParser.Handler { + + /** + * Maps IDs to stubs + */ + private val stubsMap = HashMap>() + + /** + * Returns stubs found in the stubs file. + */ + val stubs: Collection> + get() = stubsMap.values + + /** + * Invoked when a new stub item is found. + * + * @param id the ID of the stub + * @param inNeighbor the ID of the stub's in-neighbor + * @param label the label of the extender associated with the link between the + * neighbor and the stub + * @param currentLine line number where the stub link was parsed + */ + override fun onStubLink(id: NodeID, inNeighbor: NodeID, label: String, currentLine: Int) { + + // Ignore all stubs that in the stubIDs list + if (id !in stubIDs) { + return + } + + val neighbor = topology[inNeighbor] + + if (neighbor == null) { + throw ParseException("neighbor '$inNeighbor' of stub '$id' was not found in " + + "the topology", currentLine) + } + + // May throw a ParseException + // Do this before putting any stub in stubs + val extender = parseExtender(label) + + val stub = stubsMap.getOrPut(id) { Node(id, stubsProtocol) } + stub.addInNeighbor(neighbor, extender) + } + + } + + /** + * Retrieves advertisers with the IDs specified in [advertiserIDs], from the topology or the + * stubs database. For each ID It will always check the topology first. Therefore, if the + * topology contains a node a specified ID and the stubs database includes a stub with the same + * ID, then the node from the topology will be used. + * + * @param advertiserIDs the IDs of the advertisers to retrieve + * @throws InitializationException if it can not get all advertisers + * @throws IOException if an IO error occurs + * @throws ParseException if a neighbor of a stub is not included in the topology or if the + * one of the extender labels is invalid + */ + @Throws(InitializationException::class, IOException::class, ParseException::class) + fun get(advertiserIDs: List): List> { + + val advertisers = ArrayList>() // holds stubs found + val missingAdvertisers = ArrayList() // stubs not found on the topology + + // Start looking for stubs in the topology + // The topology is in memory and therefore it is usually faster to search than reading a + // file. If all stubs are found in the topology, then we avid having to access + // the filesystem + for (id in advertiserIDs) { + val advertiser = topology[id] + + if (advertiser != null) { + advertisers.add(advertiser) + } else { + missingAdvertisers.add(id) + } + } + + // Look in stubs file only for advertisers that were not found in the topology + if (!missingAdvertisers.isEmpty() && stubsFile != null) { + val stubsCreator = StubsCreator(missingAdvertisers, + topology, stubsProtocol, parseExtender) + + StubParser(stubsFile).use { + it.parse(stubsCreator) + } + + advertisers.addAll(stubsCreator.stubs) + } + + // Check if all advertisers were obtained + if (advertiserIDs.size != advertisers.size) { + val idsFound = advertisers.map { it.id }.toSet() + val idsNotFound = advertiserIDs.filter { it in idsFound } + + throw InitializationException("the following advertisers " + + "'${idsNotFound.joinToString(limit = 5)}' were not found") + } + + return advertisers + } + +} \ No newline at end of file diff --git a/src/main/kotlin/simulation/BGPAdvertisementInitializer.kt b/src/main/kotlin/simulation/BGPAdvertisementInitializer.kt new file mode 100644 index 0000000..4047d88 --- /dev/null +++ b/src/main/kotlin/simulation/BGPAdvertisementInitializer.kt @@ -0,0 +1,249 @@ +package simulation + +import bgp.BGP +import bgp.BGPRoute +import core.routing.NodeID +import core.routing.Topology +import core.simulator.Advertisement +import core.simulator.RandomDelayGenerator +import core.simulator.Time +import io.InterdomainAdvertisementReader +import io.InterdomainTopologyReader +import io.ParseException +import io.parseInterdomainExtender +import ui.Application +import java.io.File +import java.io.IOException + +/** + * Created on 09-11-2017 + * + * @author David Fialho + */ +sealed class BGPAdvertisementInitializer( + // Mandatory + val topologyFile: File, + + // Optional (with defaults) + var repetitions: Int = DEFAULT_REPETITIONS, + var minDelay: Time = DEFAULT_MINDELAY, + var maxDelay: Time = DEFAULT_MAXDELAY, + var threshold: Time = DEFAULT_THRESHOLD, + var reportDirectory: File = DEFAULT_REPORT_DIRECTORY, + var reportNodes: Boolean = false, + var outputMetadata: Boolean = false, + var outputTrace: Boolean = false, + + // Optional (without defaults) + var seed: Long? = null, + var stubsFile: File? = null, + var forcedMRAI: Time? = null + +): Initializer { + + companion object { + + val DEFAULT_REPETITIONS = 1 + val DEFAULT_THRESHOLD = 1_000_000 + val DEFAULT_MINDELAY = 1 + val DEFAULT_MAXDELAY = 1 + val DEFAULT_REPORT_DIRECTORY = File(System.getProperty("user.dir")) // current working directory + + fun with(topologyFile: File, advertiserIDs: Set): BGPAdvertisementInitializer = + BGPAdvertisementInitializer.UsingDefaultSet(topologyFile, advertiserIDs) + + fun with(topologyFile: File, advertisementsFile: File): BGPAdvertisementInitializer = + BGPAdvertisementInitializer.UsingFile(topologyFile, advertisementsFile) + } + + /** + * This is the base output name for the report files. The base output name does no include the extension. + * Subclasses should provide a name depending on their specifications. + */ + abstract val outputName: String + + /** + * Initializes a simulation. It sets up the executions to run and the runner to run them. + */ + override fun initialize(application: Application, metadata: Metadata): Pair, Execution> { + + // If no seed is set, then a new seed is generated, based on the current time, for each new initialization + val seed = seed ?: System.currentTimeMillis() + + // Append extensions according to the file type + val basicReportFile = File(reportDirectory, outputName.plus(".basic.csv")) + val nodesReportFile = File(reportDirectory, outputName.plus(".nodes.csv")) + val metadataFile = File(reportDirectory, outputName.plus(".meta.txt")) + val traceFile = File(reportDirectory, outputName.plus(".trace.txt")) + + // Setup the message delay generator + val messageDelayGenerator = try { + RandomDelayGenerator.with(minDelay, maxDelay, seed) + } catch (e: IllegalArgumentException) { + throw InitializationException(e.message) + } + + // Load the topology + val topology: Topology = application.loadTopology(topologyFile) { + InterdomainTopologyReader(topologyFile, forcedMRAI).use { + it.read() + } + } + + val advertisements = application.setupAdvertisements { + // Subclasses determine how advertisements are configured, see subclasses at the bottom of this file + initAdvertisements(application, topology) + } + + val runner = RepetitionRunner( + application, + topology, + advertisements, + threshold, + repetitions, + messageDelayGenerator, + metadataFile = if(outputMetadata) metadataFile else null // null tells the runner not to print metadata + ) + + val execution = SimpleAdvertisementExecution().apply { + dataCollectors.add(BasicDataCollector(basicReportFile)) + + if (reportNodes) { + dataCollectors.add(NodeDataCollector(nodesReportFile)) + } + + if (outputTrace) { + dataCollectors.add(TraceReporter(traceFile)) + } + } + + metadata["Topology file"] = topologyFile.name + stubsFile?.apply { + metadata["Stubs file"] = name + } + metadata["Advertiser(s)"] = advertisements.map { it.advertiser.id }.joinToString() + metadata["Minimum Delay"] = minDelay.toString() + metadata["Maximum Delay"] = maxDelay.toString() + metadata["Threshold"] = threshold.toString() + forcedMRAI?.apply { + metadata["MRAI"] = forcedMRAI.toString() + } + + return Pair(runner, execution) + } + + /** + * Subclasses should use this method to initialize advertisements to occur in the simulation. The way these are + * defined is dependent on the implementation. + * + * @return list of initialized advertisements to occur in the simulation + */ + protected abstract fun initAdvertisements(application: Application, topology: Topology) + : List> + + // ----------------------------------------------------------------------------------------------------------------- + // + // Subclasses + // + // ----------------------------------------------------------------------------------------------------------------- + + /** + * Initialization based on a set of pre-defined advertiser IDs. Each ID is mapped to an advertiser. An advertiser + * can be obtained from the topology or from a stubs file. + * + * It generates a single advertisement for each advertiser, with a default route corresponding to the self BGP + * route, and an advertising time of 0. + */ + private class UsingDefaultSet(topologyFile: File, val advertiserIDs: Set) + : BGPAdvertisementInitializer(topologyFile) { + + init { + // Verify that at least 1 advertiser ID is provided in the constructor + if (advertiserIDs.isEmpty()) { + throw IllegalArgumentException("initializer requires at least 1 advertiser") + } + } + + /** + * The output name (excluding the extension) corresponds to the topology filename and the IDs of the + * advertisers. For instance, if the topology file name is `topology.topo` and the advertiser IDs are 10 and + * 12, then the output file name will be `topology_10-12`. + */ + override val outputName: String = topologyFile.nameWithoutExtension + "_${advertiserIDs.joinToString("-")}" + + /** + * A single advertisement is created for each advertiser specified in the ID set. + * + * @throws InitializationException if the advertisers can not be found in the topology or stubs file + * @throws ParseException if the stubs file format is invalid + * @throws IOException if an IO error occurs + * @return list of initialized advertisements to occur in the simulation + */ + override fun initAdvertisements(application: Application, topology: Topology) + : List> { + + // Find all the advertisers from the specified IDs + val advertisers = application.readStubsFile(stubsFile) { + AdvertiserDB(topology, stubsFile, BGP(), ::parseInterdomainExtender) + .get(advertiserIDs.toList()) + } + + // In this mode, nodes set the self BGP route as the default route + // Use the advertisements file to configure different routes + return advertisers.map { Advertisement(it, BGPRoute.self()) }.toList() + } + } + + /** + * Initialization based on an advertisements file. This file describes the set of advertisements to occur in each + * simulation execution. + * + * The advertiser IDs described in the advertisements file are mapped to actual advertisers. An advertiser + * can be obtained from the topology or from a stubs file. + */ + private class UsingFile(topologyFile: File, val advertisementsFile: File) + : BGPAdvertisementInitializer(topologyFile) { + + /** + * The output name (excluding the extension) corresponds to the topology filename appended with the + * advertisements file name. + * + * For instance, if the topology file name is `topology.topo` and the advertisements file name is + * `advertisements.adv`, then the output base name is `topology-advertisements` + */ + override val outputName: String = + "${topologyFile.nameWithoutExtension}-${advertisementsFile.nameWithoutExtension}" + + /** + * Advertisements are obtained from an advertisements file + * + * @return list of initialized advertisements to occur in the simulation + * @throws InitializationException if the advertisers can not be found in the topology or stubs file + * @throws ParseException if the advertisements file format or the stubs file format are invalid + * @throws IOException if an IO error occurs + */ + @Throws(InitializationException::class, ParseException::class, IOException::class) + override fun initAdvertisements(application: Application, topology: Topology) + : List> { + + val advertisingInfo = application.readAdvertisementsFile(advertisementsFile) { + InterdomainAdvertisementReader(advertisementsFile).use { + it.read() + } + } + + // Find all the advertisers based on the IDs included in the advertisements file + val advertisers = application.readStubsFile(stubsFile) { + AdvertiserDB(topology, stubsFile, BGP(), ::parseInterdomainExtender) + .get(advertisingInfo.map { it.advertiserID }) + } + + val advertisersByID = advertisers.associateBy { it.id } + + return advertisingInfo.map { + val advertiser = advertisersByID[it.advertiserID] ?: throw IllegalStateException("can not happen") + Advertisement(advertiser, it.defaultRoute, it.time) + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/simulation/BasicDataCollector.kt b/src/main/kotlin/simulation/BasicDataCollector.kt index 502417a..4233c35 100644 --- a/src/main/kotlin/simulation/BasicDataCollector.kt +++ b/src/main/kotlin/simulation/BasicDataCollector.kt @@ -73,17 +73,6 @@ class BasicDataCollector(private val reporter: BasicReporter) : DataCollector, BasicNotifier.removeEndListener(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. * @@ -146,9 +135,16 @@ class BasicDataCollector(private val reporter: BasicReporter) : DataCollector, */ override fun notify(notification: EndNotification) { + // Count the nodes that were left disconnected when the simulation ended data.disconnectedCount = notification.topology.nodes .filterNot { it.protocol.selectedRoute.isValid() } .count() + + // 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()) } // endregion diff --git a/src/main/kotlin/simulation/DataCollector.kt b/src/main/kotlin/simulation/DataCollector.kt index bccb56b..2720557 100644 --- a/src/main/kotlin/simulation/DataCollector.kt +++ b/src/main/kotlin/simulation/DataCollector.kt @@ -38,11 +38,6 @@ interface DataCollector { */ 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. */ diff --git a/src/main/kotlin/simulation/DataCollectorGroup.kt b/src/main/kotlin/simulation/DataCollectorGroup.kt index 2618621..f1af1b5 100644 --- a/src/main/kotlin/simulation/DataCollectorGroup.kt +++ b/src/main/kotlin/simulation/DataCollectorGroup.kt @@ -33,13 +33,6 @@ class DataCollectorGroup: DataCollector { 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. */ diff --git a/src/main/kotlin/simulation/Execution.kt b/src/main/kotlin/simulation/Execution.kt index b1b73f9..05f332e 100644 --- a/src/main/kotlin/simulation/Execution.kt +++ b/src/main/kotlin/simulation/Execution.kt @@ -1,7 +1,8 @@ package simulation -import core.routing.Node +import core.routing.Route import core.routing.Topology +import core.simulator.Advertisement import core.simulator.Time /** @@ -9,11 +10,18 @@ import core.simulator.Time * * @author David Fialho */ -interface Execution { +interface Execution { /** - * Performs a single simulation execution with the specified topology and destination. + * Performs a single simulation execution with the specified topology and a single + * advertisement. */ - fun execute(topology: Topology<*>, destination: Node<*>, threshold: Time) + fun execute(topology: Topology, advertisement: Advertisement, threshold: Time) + + /** + * Performs a single simulation execution with the specified topology having multiple + * advertisements. + */ + fun execute(topology: Topology, advertisements: List>, threshold: Time) } \ No newline at end of file diff --git a/src/main/kotlin/simulation/InitializationException.kt b/src/main/kotlin/simulation/InitializationException.kt new file mode 100644 index 0000000..0ffcd3c --- /dev/null +++ b/src/main/kotlin/simulation/InitializationException.kt @@ -0,0 +1,10 @@ +package simulation + +/** + * Created on 13-11-2017 + * + * @author David Fialho + * + * Thrown by an initializer when an error occurs during the initialization. + */ +class InitializationException(message: String?): Exception(message) \ No newline at end of file diff --git a/src/main/kotlin/simulation/Initializer.kt b/src/main/kotlin/simulation/Initializer.kt new file mode 100644 index 0000000..49479e5 --- /dev/null +++ b/src/main/kotlin/simulation/Initializer.kt @@ -0,0 +1,22 @@ +package simulation + +import core.routing.Route +import ui.Application + +/** + * Created on 09-11-2017 + * + * @author David Fialho + * + * An initializer is responsible for setting up the simulator and getting it ready to run. To do so, + * it may require some parameters that should be provided in the constructor. + * + * TODO @doc - improve the initializer's documentation + */ +interface Initializer { + + /** + * Initializes a runner and execution based on some predefined parameters. + */ + fun initialize(application: Application, metadata: Metadata): Pair, Execution> +} \ No newline at end of file diff --git a/src/main/kotlin/simulation/Metadata.kt b/src/main/kotlin/simulation/Metadata.kt new file mode 100644 index 0000000..e2b54f7 --- /dev/null +++ b/src/main/kotlin/simulation/Metadata.kt @@ -0,0 +1,51 @@ +package simulation + +/** + * Created on 09-11-2017 + * + * @author David Fialho + * + * Container holding all key and value pairs that constitute the simulation run metadata. + */ +class Metadata(version: String): Iterable> { + + private val data = LinkedHashMap() + + init { + data["Version"] = version + } + + operator fun set(key: String, value: Any) { + data[key] = value + } + + operator fun get(key: String): Any? = data[key] + + // Iterator based on the map's iterator. It converts entries to pairs of key values + private class MetadataIterator(private val iterator: MutableIterator>): + Iterator> { + + /** + * Returns `true` if the iteration has more elements. + */ + override fun hasNext(): Boolean { + return iterator.hasNext() + } + + /** + * Returns the next element in the iteration. + */ + override fun next(): Pair { + val (key, value) = iterator.next() + return Pair(key, value) + } + + } + + /** + * Returns an iterator over the elements of this object. + */ + override fun iterator(): Iterator> { + return MetadataIterator(data.iterator()) + } +} diff --git a/src/main/kotlin/simulation/NodeDataCollector.kt b/src/main/kotlin/simulation/NodeDataCollector.kt index cc7810f..2c13d71 100644 --- a/src/main/kotlin/simulation/NodeDataCollector.kt +++ b/src/main/kotlin/simulation/NodeDataCollector.kt @@ -43,13 +43,6 @@ class NodeDataCollector(private val reporter: NodeDataReporter) : BGPNotifier.removeExportListener(this) } - /** - * Processes the data after all raw data has been collected. It should be called after an execution. - */ - override fun processData() { - // nothing to do here - } - /** * Reports the currently collected data. * diff --git a/src/main/kotlin/simulation/RepetitionRunner.kt b/src/main/kotlin/simulation/RepetitionRunner.kt index 3b6c418..be4ece3 100644 --- a/src/main/kotlin/simulation/RepetitionRunner.kt +++ b/src/main/kotlin/simulation/RepetitionRunner.kt @@ -1,14 +1,12 @@ package simulation -import core.routing.Node -import core.routing.NodeID import core.routing.Route import core.routing.Topology +import core.simulator.Advertisement import core.simulator.DelayGenerator import core.simulator.Engine import core.simulator.Time -import io.Metadata -import io.TopologyReaderHandler +import io.KeyValueWriter import ui.Application import java.io.File import java.time.Instant @@ -19,38 +17,28 @@ import java.time.Instant * @author David Fialho */ class RepetitionRunner( - private val topologyFile: File, - private val topologyReader: TopologyReaderHandler, - private val destinationID: NodeID, + private val application: Application, + private val topology: Topology, + private val advertisements: List>, + private val threshold: Time, private val repetitions: Int, private val messageDelayGenerator: DelayGenerator, - private val stubDB: StubDB?, - private val threshold: Time, - private val metadataFile: File + private val metadataFile: File? -): Runner { +): 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. + * The engine's 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 + * @param execution the execution that will be executed in each run + * @param metadata a metadata instance that may already contain some meta values */ - override fun run(execution: Execution, application: Application) { + override fun run(execution: Execution, metadata: Metadata) { val startInstant = Instant.now() - - val topology: Topology = application.loadTopology(topologyFile, topologyReader) { - topologyReader.read() - } - - val destination: Node = application.findDestination(destinationID) { - topology[destinationID] ?: stubDB?.getStub(destinationID, topology) - } - Engine.messageDelayGenerator = messageDelayGenerator application.run { @@ -58,13 +46,15 @@ class RepetitionRunner( try { repeat(times = repetitions) { repetition -> - application.execute(repetition + 1, destination, messageDelayGenerator.seed) { - execution.execute(topology, destination, threshold) + application.execute(repetition + 1, advertisements, messageDelayGenerator.seed) { + execution.execute(topology, advertisements, threshold) } // Cleanup for next execution topology.reset() - destination.reset() + // TODO @refactor - put stubs in the topology itself to avoid having this + // reset() method in the advertiser interface + advertisements.forEach { it.advertiser.reset() } Engine.messageDelayGenerator.generateNewSeed() } @@ -74,18 +64,19 @@ class RepetitionRunner( } } - // Output metadata - Metadata( - Engine.version(), - startInstant, - finishInstant = Instant.now(), - topologyFilename = topologyFile.name, - stubsFilename = stubDB?.stubsFile?.name, - destinationID = destinationID, - minDelay = messageDelayGenerator.min, - maxDelay = messageDelayGenerator.max, - threshold = threshold - ).print(metadataFile) + metadata["Start Time"] = startInstant + metadata["Finish Time"] = Instant.now() + + if (metadataFile != null) { + application.writeMetadata(metadataFile) { + + KeyValueWriter(metadataFile).use { + for ((key, value) in metadata) { + it.write(key, value) + } + } + } + } } } \ No newline at end of file diff --git a/src/main/kotlin/simulation/Runner.kt b/src/main/kotlin/simulation/Runner.kt index 657d911..7e03b27 100644 --- a/src/main/kotlin/simulation/Runner.kt +++ b/src/main/kotlin/simulation/Runner.kt @@ -1,17 +1,19 @@ package simulation -import ui.Application +import core.routing.Route /** * Created on 29-08-2017 * * @author David Fialho + * + * TODO @doc - add documentation for Runner */ -interface Runner { +interface Runner { /** * Runs the specified execution. */ - fun run(execution: Execution, application: Application) + fun run(execution: Execution, metadata: Metadata) } \ No newline at end of file diff --git a/src/main/kotlin/simulation/SimpleAdvertisementExecution.kt b/src/main/kotlin/simulation/SimpleAdvertisementExecution.kt index e04980c..d575e9a 100644 --- a/src/main/kotlin/simulation/SimpleAdvertisementExecution.kt +++ b/src/main/kotlin/simulation/SimpleAdvertisementExecution.kt @@ -1,7 +1,8 @@ package simulation -import core.routing.Node +import core.routing.Route import core.routing.Topology +import core.simulator.Advertisement import core.simulator.Engine import core.simulator.Time import java.io.IOException @@ -11,29 +12,41 @@ import java.io.IOException * * @author David Fialho */ -class SimpleAdvertisementExecution: Execution { +class SimpleAdvertisementExecution: 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. + * 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<*>, threshold: Time) { + override fun execute(topology: Topology, advertisement: Advertisement, threshold: Time) { + execute(topology, listOf(advertisement), threshold) + } + + /** + * 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, advertisements: List>, + threshold: Time) { dataCollectors.clear() val data = dataCollectors.collect { - Engine.simulate(topology, destination, threshold) + Engine.simulate(topology, advertisements, threshold) } - data.processData() data.report() } - } \ No newline at end of file diff --git a/src/main/kotlin/simulation/StubDB.kt b/src/main/kotlin/simulation/StubDB.kt deleted file mode 100644 index da63496..0000000 --- a/src/main/kotlin/simulation/StubDB.kt +++ /dev/null @@ -1,87 +0,0 @@ -package simulation - -import core.routing.* -import io.ParseException -import io.StubParser -import java.io.File -import java.util.* -import kotlin.collections.ArrayList - -/** - * Created on 31-08-2017 - * - * @author David Fialho - * - * @param stubProtocol the protocol to assign to the stub - * @property parseExtender function to convert a string label into an extender - */ -class StubDB( - val stubsFile: File, - private val stubProtocol: Protocol, - private val parseExtender: (String) -> Extender - -): StubParser.Handler { - - private var topology: Topology? = null - private var stubID: NodeID = -1 - - /** - * Stores the links of the stub that is to be obtained. - */ - private val stubLinks = ArrayList, Extender>>() - - /** - * Obtains the stub node with id [stubID]. - * - * @param stubID the ID of the stub to get - * @param topology the topology missing the stubs in this database - * @return the stub node initialized with its in-neighbors - * @throws ParseException if the input file defined a neighbor not included in the topology or if it includes a - * label that is not recognized. - */ - @Throws(ParseException::class) - fun getStub(stubID: NodeID, topology: Topology): Node? { - - StubParser.useFile(stubsFile).use { - this.stubID = stubID - this.topology = topology - it.parse(this) - } - - // Return empty optional if the stub was not found - if (stubLinks.isEmpty()) return null - - // Create stub node - val stub = Node(stubID, stubProtocol) - stubLinks.forEach { (neighbor, extender) -> stub.addInNeighbor(neighbor, extender) } - - // Stub links are no longer required - stubLinks.clear() - - return stub - } - - /** - * Invoked when a new stub item is found. - * - * @param id the ID of the stub - * @param inNeighbor the ID of the stub's in-neighbor - * @param label the label of the extender associated with the link between the neighbor and the stub - * @param currentLine line number where the stub link was parsed - * @throws ParseException if the neighbor was not included in the topology or if it includes a - * label that is not recognized. - */ - @Throws(ParseException::class) - override fun onStubLink(id: NodeID, inNeighbor: NodeID, label: String, currentLine: Int) { - - // Consider only the stub that we want to obtain - if (id != stubID) return - - val node = topology?.get(inNeighbor) ?: - throw ParseException("Neighbor ID `$inNeighbor` was not found in the topology", currentLine) - val extender = parseExtender(label) - - // Add neighbor and extender to list of stub links corresponding to the stub we want to get - stubLinks.add(Pair(node, extender)) - } -} \ No newline at end of file diff --git a/src/main/kotlin/simulation/TraceReporter.kt b/src/main/kotlin/simulation/TraceReporter.kt new file mode 100644 index 0000000..408a9f9 --- /dev/null +++ b/src/main/kotlin/simulation/TraceReporter.kt @@ -0,0 +1,189 @@ +package simulation + +import bgp.notifications.* +import core.simulator.notifications.BasicNotifier +import core.simulator.notifications.StartListener +import core.simulator.notifications.StartNotification +import io.pretty +import java.io.BufferedWriter +import java.io.Closeable +import java.io.File +import java.io.FileWriter + +/** + * Created on 15-11-2017 + * + * @author David Fialho + * + * TODO @doc + * TODO @optimization - try different methods of writing that may speedup the simulation process + */ +class TraceReporter(outputFile: File): DataCollector, StartListener, + LearnListener, ExportListener, SelectListener, DetectListener, Closeable { + + private val baseOutputFile = outputFile + + /** + * The reporter outputs the trace of each simulation to its own file. + * This variable stores the output file for the current simulation. It is updated every time a new simulation + * starts. + */ + private var simulationWriter: BufferedWriter? = null + private var simulationNumber = 0 + + /** + * Stores the size for the "Node" column. This depends on the size of longest node ID. + * By default, it is set to fit the word "Node" included in the header. + */ + private var nodeColumnSize = 4 + + /** + * Adds the collector as a listener for notifications the collector needs to listen to collect data. + */ + override fun register() { + BasicNotifier.addStartListener(this) + BGPNotifier.addLearnListener(this) + BGPNotifier.addExportListener(this) + BGPNotifier.addSelectListener(this) + BGPNotifier.addDetectListener(this) + } + + /** + * Removes the collector from all notifiers + */ + override fun unregister() { + BasicNotifier.removeStartListener(this) + BGPNotifier.removeLearnListener(this) + BGPNotifier.removeExportListener(this) + BGPNotifier.removeSelectListener(this) + BGPNotifier.removeDetectListener(this) + + // Unregister must always be called before discarding the trace reporter + // Thus, it is a go way to ensure that the current writer is closed to + close() + } + + /** + * Closes the underlying writer. + */ + override fun close() { + simulationWriter?.close() + simulationWriter = null + } + + /** + * Reports the currently collected data. + */ + override fun report() { + // nothing to do + // reporting is done during the execution + } + + /** + * Clears all collected data. + */ + override fun clear() { + // nothing to do + } + + /** + * Invoked to notify the listener of a new start notification. + */ + override fun notify(notification: StartNotification) { + // Keeping track of the number of simulations is important to ensure that the tracing output from a new + // simulation does not overwrite the previous one + simulationNumber++ + + // The trace output of each simulation is written to its own file + val simulationOutputFile = File(baseOutputFile.parent, baseOutputFile.nameWithoutExtension + + "$simulationNumber.${baseOutputFile.extension}") + + // Close writer used for a previous simulation and create a new one for the new simulation + simulationWriter?.close() + simulationWriter = BufferedWriter(FileWriter(simulationOutputFile)) + + // Look for all node IDs to determine which one has the longest ID number + val maxIDSize = notification.topology.nodes.asSequence().map { it.id }.max() ?: 0 + + // Node column size corresponds to longest between the word "Node" and the longest node ID + nodeColumnSize = maxOf(4, maxIDSize) + + // Write headers + simulationWriter?.apply { + write("${align("Time")}| Event | ${align("Node", nodeColumnSize)} | Routing Information\n") + } + } + + /** + * Invoked to notify the listener of a new learn notification. + */ + override fun notify(notification: LearnNotification) { + simulationWriter?.apply { + notification.apply { + write("${align(time)}| LEARN | ${align(node.pretty(), nodeColumnSize)} | ${route.pretty()} " + + "via ${neighbor.pretty()}\n") + } + } + } + + /** + * Invoked to notify the listener of a new export notification. + */ + override fun notify(notification: ExportNotification) { + simulationWriter?.apply { + notification.apply { + write("${align(time)}| EXPORT | ${align(node.pretty(), nodeColumnSize)} | ${route.pretty()}\n") + } + } + } + + /** + * Invoked to notify the listener of a new learn notification. + */ + override fun notify(notification: SelectNotification) { + simulationWriter?.apply { + notification.apply { + write("${align(time)}| SELECT | ${align(node.pretty(), nodeColumnSize)} | " + + "${selectedRoute.pretty()} over ${previousRoute.pretty()}\n") + } + } + } + + /** + * Invoked to notify the listener of a new detect notification. + */ + override fun notify(notification: DetectNotification) { + simulationWriter?.apply { + notification.apply { + write("${align(time)}| DETECT | ${align(node.pretty(), nodeColumnSize)} |\n") + } + } + } + + // + // Helper functions to help align the information shown in the messages + // + + private fun align(value: Any, length: Int = 7): String { + + val builder = StringBuilder(length) + + val text = value.toString() + val remainder = length - text.length + val padding = remainder / 2 + + // Add padding to the left + for (i in 1..(padding)) + builder.append(' ') + + // Add the text at the center + builder.append(text) + + // Add padding to the right + for (i in 1..(padding + remainder % 2)) + builder.append(' ') + + return builder.toString() + } + +} \ No newline at end of file diff --git a/src/main/kotlin/ui/Application.kt b/src/main/kotlin/ui/Application.kt index e28e71d..3848ae9 100644 --- a/src/main/kotlin/ui/Application.kt +++ b/src/main/kotlin/ui/Application.kt @@ -1,10 +1,8 @@ package ui -import core.routing.Node -import core.routing.NodeID import core.routing.Route import core.routing.Topology -import io.TopologyReaderHandler +import core.simulator.Advertisement import java.io.File /** @@ -19,28 +17,59 @@ interface Application { /** * 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. + * @param topologyFile the file from which the topology will be loaded + * @param block the code block to load the topology */ - fun loadTopology(topologyFile: File, topologyReader: TopologyReaderHandler, - loadBlock: () -> Topology): Topology + fun loadTopology(topologyFile: File, block: () -> Topology): Topology - fun findDestination(destinationID: NodeID, block: () -> Node?): Node + /** + * Invoked when setting up the advertisements to occur in the simulation. This may imply accessing the filesystem, + * which may throw some IO error. + * + * @param block the block of code to setup advertisements + * @return a list containing the advertisements already setup + */ + fun setupAdvertisements(block: () -> List>): List> + + /** + * Invoked when reading the stubs file. It returns whatever the [block] returns. + * + * @param file the stubs file that is going to be read, null indicates the file was not read. + * @param block the block of code to read stubs file + * @return whatever the [block] returns. + */ + fun readStubsFile(file: File?, block: () -> T): T + + /** + * Invoked when reading the advertisements file. It returns whatever the [block] returns. + * + * @param file the advertisements file that is going to be read + * @param block the block of code to read stubs file + * @return whatever the [block] returns. + */ + fun readAdvertisementsFile(file: File, block: () -> T): T /** * 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 + * @param executionID the identifier of the execution + * @param advertisements the advertisements programmed to occur in the simulation + * @param seed the seed of the message delay generator used for the execution + * @param block the code block that performs one execution */ - fun execute(executionID: Int, destination: Node, seed: Long, executeBlock: () -> Unit) + fun execute(executionID: Int, advertisements: List>, seed: Long, + block: () -> Unit) /** * Invoked during a run. */ fun run(runBlock: () -> Unit) + /** + * Invoked while metadata is being written to disk. + * + * @param file the file where the metadata is going to be written to + */ + fun writeMetadata(file: File, block: () -> Unit) + } \ No newline at end of file diff --git a/src/main/kotlin/ui/DummyApplication.kt b/src/main/kotlin/ui/DummyApplication.kt deleted file mode 100644 index 39ad735..0000000 --- a/src/main/kotlin/ui/DummyApplication.kt +++ /dev/null @@ -1,28 +0,0 @@ -package ui - -import core.routing.Node -import core.routing.NodeID -import core.routing.Route -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 index e9e1c3f..1619683 100644 --- a/src/main/kotlin/ui/cli/CLIApplication.kt +++ b/src/main/kotlin/ui/cli/CLIApplication.kt @@ -1,11 +1,12 @@ package ui.cli -import core.routing.Node -import core.routing.NodeID import core.routing.Route import core.routing.Topology +import core.simulator.Advertisement +import core.simulator.Engine import io.ParseException -import io.TopologyReaderHandler +import simulation.InitializationException +import simulation.Metadata import ui.Application import java.io.File import java.io.IOException @@ -25,18 +26,25 @@ object CLIApplication: Application { override fun launch(args: Array) { try { - val (runner, execution) = InputArgumentsParser().parse(args) - runner.run(execution, this) + val initializer = InputArgumentsParser().parse(args) + val metadata = Metadata(version = Engine.version()) + val (runner, execution) = initializer.initialize(this, metadata) + runner.run(execution, metadata) } catch (e: InputArgumentsException) { - console.error("Input arguments are invalid.") - console.error("Cause: ${e.message ?: "No information available."}") + console.error("Input arguments are invalid") + console.error("Cause: ${e.message ?: "No information available"}") console.info("Try the '-h' option to see more information") exitProcess(1) - } catch (e: Exception){ + } catch (e: InitializationException) { + console.error("Initialization failed") + console.error("Cause: ${e.message ?: "No information available"}") + exitProcess(1) + + } catch (e: Exception) { console.error("Program was interrupted due to unexpected error: ${e.javaClass.simpleName}") - console.error("Cause: ${e.message ?: "No information available."}") + console.error("Cause: ${e.message ?: "No information available"}") exitProcess(1) } } @@ -45,29 +53,30 @@ object CLIApplication: Application { * 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. + * @param block the code block to load the topology. */ - override fun loadTopology(topologyFile: File, topologyReader: TopologyReaderHandler, - loadBlock: () -> Topology): Topology { + override fun loadTopology(topologyFile: File, + block: () -> Topology): Topology { try { console.info("Topology file: ${topologyFile.path}.") console.info("Loading topology... ", inline = true) val (duration, topology) = timer { - loadBlock() + block() } console.print("loaded in $duration seconds") return topology } catch (exception: ParseException) { + console.print() // must print a new line here console.error("Failed to load topology due to parse error.") console.error("Cause: ${exception.message ?: "No information available"}") exitProcess(1) } catch (exception: IOException) { + console.print() // must print a new line here console.error("Failed to load topology due to IO error.") console.error("Cause: ${exception.message ?: "No information available"}") exitProcess(2) @@ -76,48 +85,98 @@ object CLIApplication: Application { } /** - * Invoked when trying to find the destination node based on the ID. + * Invoked when setting up the advertisements to occur in the simulation. This may imply accessing the filesystem, + * which may throw some IO error. + * + * @param block the block of code to setup advertisements + * @return a list containing the advertisements already setup + */ + override fun setupAdvertisements(block: () -> List>): List> { + + try { + console.info("Setting up advertisements... ") + val advertisements = block() + console.info("Advertising nodes: ${advertisements.map { it.advertiser.id }.joinToString()}") + + return advertisements + + } catch (exception: InitializationException) { + console.print() // must print a new line here + console.error("Failed to initialize the simulation") + console.error("Cause: ${exception.message ?: "no information available"}") + exitProcess(3) + } + } + + /** + * Invoked when reading the stubs file. It returns whatever the [block] returns. * - * @param destinationID the destination ID - * @param block the block of code to find the destination + * @param file the stubs file that is going to be read + * @param block the block of code to read stubs file + * @return whatever the [block] returns. */ - override fun findDestination(destinationID: NodeID, block: () -> Node?): Node { + override fun readStubsFile(file: File?, block: () -> T): T { - val destination= try { + return if (file == null) { + // File is not going to be read block() + } else { + handleReadingFiles(file, block, name = "stubs") + } + } + + /** + * Invoked when reading the advertisements file. It returns whatever the [block] returns. + * + * @param file the advertisements file that is going to be read + * @param block the block of code to read stubs file + * @return whatever the [block] returns. + */ + override fun readAdvertisementsFile(file: File, block: () -> T): T = + handleReadingFiles(file, block, name = "advertisements") + + /** + * Handles errors when reading input files and shows a time it took to read the file. + */ + private fun handleReadingFiles(file: File, block: () -> T, name: String): T { + + try { + console.info("Reading $name file '${file.name}'... ", inline = true) + val (duration, value) = timer { + block() + } + console.print("done in $duration seconds") + + return value } catch (exception: ParseException) { - console.error("Failed to parse stubs file.") - console.error("Cause: ${exception.message ?: "No information available"}") + console.print() // must print a new line here + console.error("Failed to parse $name file '${file.name}'") + console.error("Cause: ${exception.message ?: "no information available"}") exitProcess(1) } catch (exception: IOException) { - console.error("Failed to read stubs file due to IO error.") - console.error("Cause: ${exception.message ?: "No information available"}") - exitProcess(2) - } - - if (destination == null) { - console.error("Destination `$destinationID` was not found.") - exitProcess(3) + console.print() // must print a new line here + console.error("Failed to access $name file '${file.name}' due to an IO error") + console.error("Cause: ${exception.message ?: "no information available"}") + exitProcess(1) } - - 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 + * @param executionID the identifier of the execution + * @param advertisements the advertisements that will occur during the execution + * @param seed the seed of the message delay generator used for the execution + * @param block the code block that performs one execution */ - override fun execute(executionID: Int, destination: Node, seed: Long, executeBlock: () -> Unit) { + override fun execute(executionID: Int, advertisements: List>, + seed: Long, block: () -> Unit) { - console.info("Executing $executionID (destination=${destination.id} and seed=$seed)... ", inline = true) + console.info("Executing $executionID (seed=$seed)... ", inline = true) val (duration, _) = timer { - executeBlock() + block() } console.print("finished in $duration seconds") } @@ -132,7 +191,7 @@ object CLIApplication: Application { val (duration, _) = timer { runBlock() } - console.info("Finished run in $duration in seconds") + console.info("Finished run in $duration seconds") } catch (exception: IOException) { console.error("Failed to report results due to an IO error.") @@ -142,8 +201,32 @@ object CLIApplication: Application { } + /** + * Invoked while metadata is being written to disk. + * + * @param file the file where the metadata is going to be written to + */ + override fun writeMetadata(file: File, block: () -> Unit) { + + try { + console.info("Writing metadata... ", inline = true) + val (duration, _) = timer { + block() + } + console.print("done in $duration seconds") + + } catch (exception: IOException) { + console.print() // must print a new line here + console.error("Failed to metadata due to an IO error.") + console.error("Cause: ${exception.message ?: "No information available"}") + exitProcess(4) + } + } + } + +// TODO @refactor - move timer to a utils file private fun timer(block: () -> R): Pair { val start = Instant.now() diff --git a/src/main/kotlin/ui/cli/Console.kt b/src/main/kotlin/ui/cli/Console.kt index 7dca42b..d8be2f7 100644 --- a/src/main/kotlin/ui/cli/Console.kt +++ b/src/main/kotlin/ui/cli/Console.kt @@ -19,7 +19,7 @@ class Console { print("ERROR", message, inline) } - fun print(message: String) { + fun print(message: String = "") { println(message) } diff --git a/src/main/kotlin/ui/cli/InputArgumentsParser.kt b/src/main/kotlin/ui/cli/InputArgumentsParser.kt index 4bea762..1d7dc99 100644 --- a/src/main/kotlin/ui/cli/InputArgumentsParser.kt +++ b/src/main/kotlin/ui/cli/InputArgumentsParser.kt @@ -1,12 +1,12 @@ package ui.cli -import bgp.BGP +import bgp.BGPRoute +import core.routing.NodeID import core.simulator.Engine -import core.simulator.RandomDelayGenerator -import io.InterdomainTopologyReaderHandler -import io.parseInterdomainExtender import org.apache.commons.cli.* -import simulation.* +import simulation.BGPAdvertisementInitializer +import simulation.Initializer +import utils.toNonNegativeInt import java.io.File import java.util.* import kotlin.system.exitProcess @@ -19,23 +19,29 @@ import kotlin.system.exitProcess */ class InputArgumentsParser { - private val MAIN_COMMAND = "ssbgp-simulator" - - // Information Options - private val HELP = "help" - private val VERSION = "version" - - // Execution 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 STUBS = "stubs" - private val NODE_REPORT = "reportnodes" + companion object { + private val MAIN_COMMAND = "ssbgp-simulator" + + // Information Options + private val HELP = "help" + private val VERSION = "version" + + // Execution Options + private val TOPOLOGY_FILE = "topology" + private val ADVERTISER = "advertiser" + 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 STUBS = "stubs" + private val NODE_REPORT = "reportnodes" + private val ADVERTISE_FILE = "advertise" + private val METADATA = "metadata" + private val TRACE = "trace" + private val MRAI = "mrai" + } private val options = Options() @@ -65,10 +71,10 @@ class InputArgumentsParser { .longOpt(TOPOLOGY_FILE) .build()) addOption(Option.builder("d") - .desc("ID of destination node") - .hasArg(true) - .argName("destination") - .longOpt(DESTINATION) + .desc("ID(s) of node(s) advertising a destination") + .hasArgs() + .argName("advertisers") + .longOpt(ADVERTISER) .build()) addOption(Option.builder("c") .desc("Number of executions to run [default: 1]") @@ -120,12 +126,34 @@ class InputArgumentsParser { .hasArg(false) .longOpt(NODE_REPORT) .build()) + addOption(Option.builder("D") + .desc("File with advertisements") + .hasArg(true) + .argName("advertise-file") + .longOpt(ADVERTISE_FILE) + .build()) + addOption(Option.builder("meta") + .desc("Output metadata file") + .hasArg(false) + .longOpt(METADATA) + .build()) + addOption(Option.builder("tr") + .desc("Output a trace with the simulation events to a file") + .hasArg(false) + .longOpt(TRACE) + .build()) + addOption(Option.builder("mrai") + .desc("Force the MRAI value for all nodes") + .hasArg(true) + .argName("") + .longOpt(MRAI) + .build()) } } @Throws(InputArgumentsException::class) - fun parse(args: Array): Pair { + fun parse(args: Array): Initializer { val commandLine = try { DefaultParser().parse(options, args) @@ -149,59 +177,55 @@ class InputArgumentsParser { commandLine.let { + // + // Validate options used + // + + if (it.hasOption(ADVERTISER) && it.hasOption(ADVERTISE_FILE)) { + throw InputArgumentsException("options -d/--$ADVERTISER and -D/--$ADVERTISE_FILE are mutually exclusive") + } else if (!it.hasOption(ADVERTISER) && !it.hasOption(ADVERTISE_FILE)) { + throw InputArgumentsException("one option of -d/--$ADVERTISER and -D/--$ADVERTISE_FILE is required") + } + + // + // Parse option values + // + val topologyFile = getFile(it, option = TOPOLOGY_FILE).get() - val destination = getNonNegativeInteger(it, option = DESTINATION) + val advertisers = getManyNonNegativeIntegers(it, option = ADVERTISER, default = emptyList()) + val advertisementsFile = getFile(it, option = ADVERTISE_FILE, default = Optional.empty()) 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 stubsFile = getFile(it, option = STUBS, default = Optional.empty()) - + val reportNodes = commandLine.hasOption(NODE_REPORT) val minDelay = getPositiveInteger(it, option = MIN_DELAY, default = 1) val maxDelay = getPositiveInteger(it, option = MAX_DELAY, default = 1) + val outputMetadata = commandLine.hasOption(METADATA) + val outputTrace = commandLine.hasOption(TRACE) - 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 outputName = topologyFile.nameWithoutExtension - - val basicReportFile = File(reportDirectory, outputName.plus("_$destination.basic.csv")) - val nodesReportFile = File(reportDirectory, outputName.plus("_$destination.nodes.csv")) - val metadataFile = File(reportDirectory, outputName.plus("_$destination.meta.txt")) - - val topologyReader = InterdomainTopologyReaderHandler(topologyFile) - val messageDelayGenerator = RandomDelayGenerator.with(minDelay, maxDelay, seed) - - val stubDB = if (stubsFile.isPresent) { - StubDB(stubsFile.get(), BGP(), ::parseInterdomainExtender) + // Select the initialization based on whether the user specified a set of advertisers or a file + val initializer = if (it.hasOption(ADVERTISER)) { + BGPAdvertisementInitializer.with(topologyFile, advertisers.toSet()) } else { - null + BGPAdvertisementInitializer.with(topologyFile, advertisementsFile.get()) } - val runner = RepetitionRunner( - topologyFile, - topologyReader, - destination, - repetitions, - messageDelayGenerator, - stubDB, - threshold, - metadataFile - ) - - val execution = SimpleAdvertisementExecution().apply { - dataCollectors.add(BasicDataCollector(basicReportFile)) - - if (commandLine.hasOption(NODE_REPORT)) { - dataCollectors.add(NodeDataCollector(nodesReportFile)) - } + return initializer.apply { + this.repetitions = repetitions + this.reportDirectory = reportDirectory + this.threshold = threshold + this.minDelay = minDelay + this.maxDelay = maxDelay + this.reportNodes = reportNodes + this.outputMetadata = outputMetadata + this.outputTrace = outputTrace + this.stubsFile = stubsFile.orElseGet { null } + this.seed = seed + if (it.hasOption(MRAI)) + this.forcedMRAI = getNonNegativeInteger(it, MRAI) } - - return Pair(runner, execution) } } @@ -236,6 +260,23 @@ class InputArgumentsParser { return directory } + @Throws(InputArgumentsException::class) + private fun getManyNonNegativeIntegers(commandLine: CommandLine, option: String, + default: List? = null): List { + verifyOption(commandLine, option, default) + + val values = commandLine.getOptionValues(option) + + try { + @Suppress("USELESS_ELVIS") + // Although the IDE does not recognize it, 'values' can actually be null if the option set. The + // documentation for getOptionValues() indicates that it returns null if the option is not set. + return values?.map { it.toNonNegativeInt() } ?: default!! // never null at this point!! + } catch (numberError: NumberFormatException) { + throw InputArgumentsException("values for '--$option' must be non-negative integer values") + } + } + @Throws(InputArgumentsException::class) private fun getNonNegativeInteger(commandLine: CommandLine, option: String, default: Int? = null): Int { verifyOption(commandLine, option, default) @@ -243,18 +284,11 @@ class InputArgumentsParser { val value = commandLine.getOptionValue(option) try { - val intValue = value?.toInt() ?: default!! // See note below + return value?.toNonNegativeInt() ?: 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'") + throw InputArgumentsException("value for '--$option' must be a non-negative integer value") } } diff --git a/src/main/kotlin/utils/HelperFunctions.kt b/src/main/kotlin/utils/HelperFunctions.kt index ce5b6e6..f0b530c 100644 --- a/src/main/kotlin/utils/HelperFunctions.kt +++ b/src/main/kotlin/utils/HelperFunctions.kt @@ -19,7 +19,7 @@ fun String.toNonNegativeInt(): Int { val value = this.toInt() if (value < 0) { - throw NumberFormatException() + throw NumberFormatException("For input string \"$this\"") } return value diff --git a/src/test/kotlin/bgp/BGPTests.kt b/src/test/kotlin/bgp/BGPTests.kt index 1f340cd..6fcb1e6 100644 --- a/src/test/kotlin/bgp/BGPTests.kt +++ b/src/test/kotlin/bgp/BGPTests.kt @@ -2,8 +2,8 @@ package bgp import com.nhaarman.mockito_kotlin.* import core.routing.* -import core.simulator.Time import core.simulator.Engine +import core.simulator.Time import org.hamcrest.MatcherAssert.assertThat import org.jetbrains.spek.api.Spek import org.jetbrains.spek.api.dsl.context @@ -107,7 +107,7 @@ object BGPTests : Spek({ beforeEachTest { // Keep the protocol state clean after each test protocol.reset() - protocol.routingTable.update(neighbor, BGPRoute.with(10, pathOf(BGPNode(0), neighbor))) + protocol.process(node, neighbor, BGPRoute.with(10, pathOf(BGPNode(0), neighbor))) } `when`("node imports an invalid route") { @@ -547,9 +547,9 @@ object BGPTests : Spek({ verify(node, times(1)).send(exportedRoute) } - it("does NOT start a new MRAI timer") { + it("does start a new MRAI timer") { assertThat(protocol.mraiTimer.expired, - Is(true)) + Is(false)) } } } diff --git a/src/test/kotlin/bgp/SSBGP2WithInterdomainRoutingTests.kt b/src/test/kotlin/bgp/SSBGP2WithInterdomainRoutingTests.kt index f3f7d64..a7b8031 100644 --- a/src/test/kotlin/bgp/SSBGP2WithInterdomainRoutingTests.kt +++ b/src/test/kotlin/bgp/SSBGP2WithInterdomainRoutingTests.kt @@ -46,7 +46,7 @@ object SSBGP2WithInterdomainRoutingTests: Spek({ on("simulating with node 0 as the destination") { - val terminated = Engine.simulate(topology, node[0], threshold = 1000) + val terminated = simulate(topology, node[0], threshold = 1000) it("terminates") { assertThat(terminated, Is(true)) @@ -106,7 +106,7 @@ object SSBGP2WithInterdomainRoutingTests: Spek({ on("simulating with node 0 as the destination") { - val terminated = Engine.simulate(topology, node[0], threshold = 1000) + val terminated = simulate(topology, node[0], threshold = 1000) it("does NOT terminate") { assertThat(terminated, Is(false)) diff --git a/src/test/kotlin/bgp/SSBGP2WithShortestPathRoutingTests.kt b/src/test/kotlin/bgp/SSBGP2WithShortestPathRoutingTests.kt index a0ed805..742345e 100644 --- a/src/test/kotlin/bgp/SSBGP2WithShortestPathRoutingTests.kt +++ b/src/test/kotlin/bgp/SSBGP2WithShortestPathRoutingTests.kt @@ -3,7 +3,6 @@ package bgp import core.routing.pathOf import core.simulator.Engine import org.hamcrest.MatcherAssert.assertThat -import org.hamcrest.Matchers.`is` as Is import org.hamcrest.Matchers.nullValue import org.jetbrains.spek.api.Spek import org.jetbrains.spek.api.dsl.given @@ -11,6 +10,7 @@ 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 01-09-2017 @@ -40,7 +40,7 @@ object SSBGP2WithShortestPathRoutingTests: Spek({ on("simulating with node 1 as the destination") { - val terminated = Engine.simulate(topology, node1, threshold = 1000) + val terminated = simulate(topology, node1, threshold = 1000) it("terminated") { assertThat(terminated, Is(true)) @@ -65,7 +65,7 @@ object SSBGP2WithShortestPathRoutingTests: Spek({ on("simulating with node 2 as the destination") { - val terminated = Engine.simulate(topology, node2, threshold = 1000) + val terminated = simulate(topology, node2, threshold = 1000) it("terminated") { assertThat(terminated, Is(true)) @@ -115,7 +115,7 @@ object SSBGP2WithShortestPathRoutingTests: Spek({ on("simulating with node 0 as the destination") { - val terminated = Engine.simulate(topology, node[0], threshold = 1000) + val terminated = simulate(topology, node[0], threshold = 1000) it("terminates") { assertThat(terminated, Is(true)) @@ -176,7 +176,7 @@ object SSBGP2WithShortestPathRoutingTests: Spek({ on("simulating with node 0 as the destination") { - val terminated = Engine.simulate(topology, node[0], threshold = 1000) + val terminated = simulate(topology, node[0], threshold = 1000) it("terminates") { assertThat(terminated, Is(true)) @@ -235,7 +235,7 @@ object SSBGP2WithShortestPathRoutingTests: Spek({ on("simulating with node 0 as the destination") { - val terminated = Engine.simulate(topology, node[0], threshold = 1000) + val terminated = simulate(topology, node[0], threshold = 1000) it("terminates") { assertThat(terminated, Is(true)) diff --git a/src/test/kotlin/core/simulator/BGPWithInterdomainRoutingTests.kt b/src/test/kotlin/core/simulator/BGPWithInterdomainRoutingTests.kt index 26eaedf..b844fc8 100644 --- a/src/test/kotlin/core/simulator/BGPWithInterdomainRoutingTests.kt +++ b/src/test/kotlin/core/simulator/BGPWithInterdomainRoutingTests.kt @@ -6,13 +6,13 @@ 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 +import org.hamcrest.Matchers.`is` as Is /** * Created on 26-07-2017. @@ -32,7 +32,7 @@ object BGPWithInterdomainRoutingTests : Spek({ afterEachTest { Engine.scheduler.reset() - topology.nodes.forEach { it.protocol.reset() } + topology.nodes.forEach { it.reset() } } val node = topology.nodes.sortedBy { it.id } @@ -40,7 +40,7 @@ object BGPWithInterdomainRoutingTests : Spek({ on("simulating with node 0 as the destination") { - val terminated = Engine.simulate(topology, node[0], threshold = 1000) + val terminated = simulate(topology, node[0], threshold = 1000) it("terminated") { assertThat(terminated, Is(true)) @@ -59,7 +59,7 @@ object BGPWithInterdomainRoutingTests : Spek({ on("simulating with node 1 as the destination") { - val terminated = Engine.simulate(topology, node[1], threshold = 1000) + val terminated = simulate(topology, node[1], threshold = 1000) it("terminated") { assertThat(terminated, Is(true)) @@ -77,7 +77,7 @@ object BGPWithInterdomainRoutingTests : Spek({ } } - given("square topology") { + given("multiple relationships topology") { val topology = bgpTopology { node { 0 deploying BGP() } @@ -93,7 +93,7 @@ object BGPWithInterdomainRoutingTests : Spek({ afterEachTest { Engine.scheduler.reset() - topology.nodes.forEach { it.protocol.reset() } + topology.nodes.forEach { it.reset() } } val node = topology.nodes.sortedBy { it.id } @@ -101,7 +101,7 @@ object BGPWithInterdomainRoutingTests : Spek({ on("simulating with node 0 as the destination") { - val terminated = Engine.simulate(topology, node[0], threshold = 1000) + val terminated = simulate(topology, node[0], threshold = 1000) it("terminated") { assertThat(terminated, Is(true)) @@ -124,6 +124,114 @@ object BGPWithInterdomainRoutingTests : Spek({ } } + given("square topology") { + + val topology = bgpTopology { + node { 0 deploying BGP() } + node { 1 deploying BGP() } + node { 2 deploying BGP() } + node { 3 deploying BGP() } + + customerLink { 3 to 1 } + customerLink { 2 to 0 } + customerLink { 2 to 3 } + customerLink { 3 to 2 } + peerplusLink { 0 to 3 } + } + + afterEachTest { + Engine.scheduler.reset() + topology.nodes.forEach { it.reset() } + } + + val node = topology.nodes.sortedBy { it.id } + val protocol = node.map { it.protocol as BGP } + + on("simulating with nodes 0 and 1 advertising peer+ routes at 0 and 100") { + + val advertisements = listOf( + Advertisement(node[0], peerplusRoute(), time = 0), + Advertisement(node[1], peerplusRoute(), time = 100) + ) + + val terminated = Engine.simulate(topology, advertisements, threshold = 1000) + + it("terminated") { + assertThat(terminated, Is(true)) + } + + it("finishes with node 0 selecting peer+ route via himself") { + assertThat(protocol[0].routingTable.getSelectedRoute(), + Is(peerplusRoute())) + assertThat(protocol[0].routingTable.getSelectedNeighbor(), + Is(node[0])) + } + + it("finishes with node 1 selecting peer+ route via himself") { + assertThat(protocol[1].routingTable.getSelectedRoute(), + Is(peerplusRoute())) + assertThat(protocol[1].routingTable.getSelectedNeighbor(), + Is(node[1])) + } + + it("finishes with node 2 selecting customer route via node 0") { + assertThat(protocol[2].routingTable.getSelectedRoute(), + Is(customerRoute(asPath = pathOf(0)))) + assertThat(protocol[2].routingTable.getSelectedNeighbor(), + Is(node[0])) + } + + it("finishes with node 3 selecting customer route via node 1") { + assertThat(protocol[3].routingTable.getSelectedRoute(), + Is(customerRoute(asPath = pathOf(1)))) + assertThat(protocol[3].routingTable.getSelectedNeighbor(), + Is(node[1])) + } + } + + on("simulating with nodes 0 and 1 advertising customer routes at 0 and 100") { + + val advertisements = listOf( + Advertisement(node[0], customerRoute(), time = 0), + Advertisement(node[1], customerRoute(), time = 100) + ) + + val terminated = Engine.simulate(topology, advertisements, threshold = 1000) + + it("terminated") { + assertThat(terminated, Is(true)) + } + + it("finishes with node 0 selecting peer+ route via node 3") { + assertThat(protocol[0].routingTable.getSelectedRoute(), + Is(peerplusRoute(asPath = pathOf(1, 3)))) + assertThat(protocol[0].routingTable.getSelectedNeighbor(), + Is(node[3])) + } + + it("finishes with node 1 selecting customer route via himself") { + assertThat(protocol[1].routingTable.getSelectedRoute(), + Is(customerRoute())) + assertThat(protocol[1].routingTable.getSelectedNeighbor(), + Is(node[1])) + } + + it("finishes with node 2 selecting customer route via node 3") { + assertThat(protocol[2].routingTable.getSelectedRoute(), + Is(customerRoute(asPath = pathOf(1, 3)))) + assertThat(protocol[2].routingTable.getSelectedNeighbor(), + Is(node[3])) + } + + it("finishes with node 3 selecting customer route via node 1") { + assertThat(protocol[3].routingTable.getSelectedRoute(), + Is(customerRoute(asPath = pathOf(1)))) + assertThat(protocol[3].routingTable.getSelectedNeighbor(), + Is(node[1])) + } + } + } + given("loop topology with customer to destination and peer+ around the cycle") { val topology = bgpTopology { @@ -142,19 +250,72 @@ object BGPWithInterdomainRoutingTests : Spek({ afterEachTest { Engine.scheduler.reset() - topology.nodes.forEach { it.protocol.reset() } + topology.nodes.forEach { it.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) + val terminated = simulate(topology, node[0], threshold = 1000) it("does NOT terminate") { assertThat(terminated, Is(false)) } } + + + on("simulating with nodes 1, 2, and 3 advertising customer routes at time 0") { + + val advertisements = listOf( + Advertisement(node[1], customerRoute()), + Advertisement(node[2], customerRoute()), + Advertisement(node[3], customerRoute()) + ) + + val terminated = Engine.simulate(topology, advertisements, threshold = 1000) + + it("does not terminate") { + assertThat(terminated, Is(false)) + } + } + + on("simulating with nodes 1, 2, and 3 advertising customer routes at times 0, 100, and 200") { + + val advertisements = listOf( + Advertisement(node[1], customerRoute(), time = 0), + Advertisement(node[2], customerRoute(), time = 100), + Advertisement(node[3], customerRoute(), time = 200) + ) + + val terminated = Engine.simulate(topology, advertisements, threshold = 1000) + + it("does not terminate") { + assertThat(terminated, Is(true)) + } + + it("finishes with node 1 selecting a customer route via himself") { + assertThat(protocol[1].routingTable.getSelectedRoute(), + Is(customerRoute())) + assertThat(protocol[1].routingTable.getSelectedNeighbor(), + Is(node[1])) + } + + it("finishes with node 2 selecting peer+ route via node 3") { + assertThat(protocol[2].routingTable.getSelectedRoute(), + Is(peerplusRoute(asPath = pathOf(1, 3)))) + assertThat(protocol[2].routingTable.getSelectedNeighbor(), + Is(node[3])) + } + + it("finishes with node 3 selecting peer+ route via node 1") { + assertThat(protocol[3].routingTable.getSelectedRoute(), + Is(peerplusRoute(asPath = pathOf(1)))) + assertThat(protocol[3].routingTable.getSelectedNeighbor(), + Is(node[1])) + } + } } given("topology without cycles and with siblings") { @@ -173,7 +334,7 @@ object BGPWithInterdomainRoutingTests : Spek({ afterEachTest { Engine.scheduler.reset() - topology.nodes.forEach { it.protocol.reset() } + topology.nodes.forEach { it.reset() } } val node = topology.nodes.sortedBy { it.id } @@ -181,7 +342,7 @@ object BGPWithInterdomainRoutingTests : Spek({ on("simulating with node 0 as the destination") { - val terminated = Engine.simulate(topology, node[0], threshold = 1000) + val terminated = simulate(topology, node[0], threshold = 1000) it("terminates") { assertThat(terminated, Is(true)) @@ -221,14 +382,14 @@ object BGPWithInterdomainRoutingTests : Spek({ afterEachTest { Engine.scheduler.reset() - topology.nodes.forEach { it.protocol.reset() } + topology.nodes.forEach { it.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) + val terminated = simulate(topology, node[0], threshold = 1000) it("does NOT terminate") { assertThat(terminated, Is(false)) diff --git a/src/test/kotlin/core/simulator/BGPWithShortestPathRoutingTests.kt b/src/test/kotlin/core/simulator/BGPWithShortestPathRoutingTests.kt index ca4264d..7bfdf90 100644 --- a/src/test/kotlin/core/simulator/BGPWithShortestPathRoutingTests.kt +++ b/src/test/kotlin/core/simulator/BGPWithShortestPathRoutingTests.kt @@ -4,13 +4,13 @@ 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.* +import org.hamcrest.Matchers.`is` as Is /** * Created on 26-07-2017. @@ -40,7 +40,7 @@ object BGPWithShortestPathRoutingTests : Spek({ on("simulating with node 1 as the destination") { - val terminated = Engine.simulate(topology, node1, threshold = 1000) + val terminated = simulate(topology, node1, threshold = 1000) it("terminated") { assertThat(terminated, Is(true)) @@ -69,7 +69,7 @@ object BGPWithShortestPathRoutingTests : Spek({ on("simulating with node 2 as the destination") { - val terminated = Engine.simulate(topology, node2, threshold = 1000) + val terminated = simulate(topology, node2, threshold = 1000) it("terminated") { assertThat(terminated, Is(true)) @@ -116,7 +116,7 @@ object BGPWithShortestPathRoutingTests : Spek({ 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) + val terminated = simulate(topology, node1, threshold = 1000) it("terminated") { assertThat(terminated, Is(true)) @@ -171,7 +171,7 @@ object BGPWithShortestPathRoutingTests : Spek({ on("simulating with node 0 as the destination") { - val terminated = Engine.simulate(topology, node[0], threshold = 1000) + val terminated = simulate(topology, node[0], threshold = 1000) it("terminated") { assertThat(terminated, Is(true)) @@ -215,7 +215,7 @@ object BGPWithShortestPathRoutingTests : Spek({ on("simulating with node 1 as the destination") { - val terminated = Engine.simulate(topology, node[1], threshold = 1000) + val terminated = simulate(topology, node[1], threshold = 1000) it("terminated") { assertThat(terminated, Is(true)) @@ -282,7 +282,7 @@ object BGPWithShortestPathRoutingTests : Spek({ on("simulating with node 0 as the destination") { - val terminated = Engine.simulate(topology, node[0], threshold = 1000) + val terminated = simulate(topology, node[0], threshold = 1000) it("does not terminate") { assertThat(terminated, Is(false)) diff --git a/src/test/kotlin/core/simulator/ISSBGPWithShortestPathRoutingTests.kt b/src/test/kotlin/core/simulator/ISSBGPWithShortestPathRoutingTests.kt index 76f0f78..bc6423e 100644 --- a/src/test/kotlin/core/simulator/ISSBGPWithShortestPathRoutingTests.kt +++ b/src/test/kotlin/core/simulator/ISSBGPWithShortestPathRoutingTests.kt @@ -44,7 +44,7 @@ object ISSBGPWithShortestPathRoutingTests : Spek({ on("simulating with node 0 as the destination") { - val terminated = Engine.simulate(topology, node[0], threshold = 1000) + val terminated = simulate(topology, node[0], threshold = 1000) it("terminates") { assertThat(terminated, Is(true)) diff --git a/src/test/kotlin/core/simulator/NotificationTests.kt b/src/test/kotlin/core/simulator/NotificationTests.kt index c940726..386cb09 100644 --- a/src/test/kotlin/core/simulator/NotificationTests.kt +++ b/src/test/kotlin/core/simulator/NotificationTests.kt @@ -32,7 +32,7 @@ object NotificationsTests: Spek({ val node = topology.nodes.sortedBy { it.id } val collector = collectBGPNotifications { - Engine.simulate(topology, node[0], threshold = 1000) + simulate(topology, node[0], threshold = 1000) } it("issues start notification once") { @@ -59,16 +59,16 @@ object NotificationsTests: Spek({ assertThat(collector.importNotifications.size, Is(1)) } - it("issues learn notification once") { - assertThat(collector.learnNotifications.size, Is(1)) + it("issues learn notification twice") { + assertThat(collector.learnNotifications.size, Is(2)) } it("never issues detect notification") { assertThat(collector.detectNotifications.size, Is(0)) } - it("issues select notification once") { - assertThat(collector.selectNotifications.size, Is(1)) + it("issues select notification twice") { + assertThat(collector.selectNotifications.size, Is(2)) } it("issues export notification 2 times") { @@ -97,7 +97,7 @@ object NotificationsTests: Spek({ val node = topology.nodes.sortedBy { it.id } val collector = collectBGPNotifications { - Engine.simulate(topology, node[0], threshold = 1000) + simulate(topology, node[0], threshold = 1000) } it("never issues threshold reached notification") { @@ -116,16 +116,16 @@ object NotificationsTests: Spek({ assertThat(collector.importNotifications.size, Is(8)) } - it("issues learn notification 8 times") { - assertThat(collector.learnNotifications.size, Is(8)) + it("issues learn notification 9 times") { + assertThat(collector.learnNotifications.size, Is(9)) } 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 select notification 7 times") { + assertThat(collector.selectNotifications.size, Is(7)) } it("issues export notification 7 times") { diff --git a/src/test/kotlin/core/simulator/SSBGPWithInterdomainRoutingTests.kt b/src/test/kotlin/core/simulator/SSBGPWithInterdomainRoutingTests.kt index 8133bab..a534d17 100644 --- a/src/test/kotlin/core/simulator/SSBGPWithInterdomainRoutingTests.kt +++ b/src/test/kotlin/core/simulator/SSBGPWithInterdomainRoutingTests.kt @@ -18,6 +18,10 @@ import org.hamcrest.Matchers.`is` as Is */ object SSBGPWithInterdomainRoutingTests : Spek({ + beforeEachTest { + Engine.resetToDefaults() + } + given("loop topology with customer to destination and peer+ around the cycle") { val topology = bgpTopology { @@ -44,7 +48,7 @@ object SSBGPWithInterdomainRoutingTests : Spek({ on("simulating with node 0 as the destination") { - val terminated = Engine.simulate(topology, node[0], threshold = 1000) + val terminated = simulate(topology, node[0], threshold = 1000) it("terminates") { assertThat(terminated, @@ -108,7 +112,7 @@ object SSBGPWithInterdomainRoutingTests : Spek({ on("simulating with node 0 as the destination") { - val terminated = Engine.simulate(topology, node[0], threshold = 1000) + val terminated = simulate(topology, node[0], threshold = 1000) it("terminates") { assertThat(terminated, Is(true)) diff --git a/src/test/kotlin/core/simulator/SSBGPWithShortestPathRoutingTests.kt b/src/test/kotlin/core/simulator/SSBGPWithShortestPathRoutingTests.kt index 1786fd4..85a05ef 100644 --- a/src/test/kotlin/core/simulator/SSBGPWithShortestPathRoutingTests.kt +++ b/src/test/kotlin/core/simulator/SSBGPWithShortestPathRoutingTests.kt @@ -41,7 +41,7 @@ object SSBGPWithShortestPathRoutingTests : Spek({ on("simulating with node 1 as the destination") { - val terminated = Engine.simulate(topology, node1, threshold = 1000) + val terminated = simulate(topology, node1, threshold = 1000) it("terminated") { assertThat(terminated, Is(true)) @@ -66,7 +66,7 @@ object SSBGPWithShortestPathRoutingTests : Spek({ on("simulating with node 2 as the destination") { - val terminated = Engine.simulate(topology, node2, threshold = 1000) + val terminated = simulate(topology, node2, threshold = 1000) it("terminated") { assertThat(terminated, Is(true)) @@ -116,7 +116,7 @@ object SSBGPWithShortestPathRoutingTests : Spek({ on("simulating with node 0 as the destination") { - val terminated = Engine.simulate(topology, node[0], threshold = 1000) + val terminated = simulate(topology, node[0], threshold = 1000) it("terminates") { assertThat(terminated, Is(true)) @@ -177,7 +177,7 @@ object SSBGPWithShortestPathRoutingTests : Spek({ on("simulating with node 0 as the destination") { - val terminated = Engine.simulate(topology, node[0], threshold = 1000) + val terminated = simulate(topology, node[0], threshold = 1000) it("terminates") { assertThat(terminated, Is(true)) diff --git a/src/test/kotlin/io/InterdomainAdvertisementReaderTest.kt b/src/test/kotlin/io/InterdomainAdvertisementReaderTest.kt new file mode 100644 index 0000000..43765ae --- /dev/null +++ b/src/test/kotlin/io/InterdomainAdvertisementReaderTest.kt @@ -0,0 +1,137 @@ +package io + +import bgp.BGPRoute +import bgp.policies.interdomain.* +import core.routing.Route +import core.simulator.Advertisement +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 org.junit.jupiter.api.Assertions.assertThrows +import testing.node +import testing.then +import java.io.StringReader +import org.hamcrest.Matchers.`is` as Is + +object InterdomainAdvertisementReaderTest: Spek({ + + infix fun String.isParsedTo(advertisement: Advertisement): Pair> = + Pair(this, advertisement) + + context("an interdomain advertisements file with a single entry") { + + listOf( + "10 = 0 | c" isParsedTo Advertisement(node(10), customerRoute(), time = 0), + "11 = 0 | c" isParsedTo Advertisement(node(11), customerRoute(), time = 0), + "10 = 15 | c" isParsedTo Advertisement(node(10), customerRoute(), time = 15), + "10 = 15 | r" isParsedTo Advertisement(node(10), peerRoute(), time = 15), + "10 = 15 | p" isParsedTo Advertisement(node(10), providerRoute(), time = 15), + "10 = 15 | r+" isParsedTo Advertisement(node(10), peerplusRoute(), time = 15), + "10 = 15 | r*" isParsedTo Advertisement(node(10), peerstarRoute(), time = 15), + "10 = | c" isParsedTo Advertisement(node(10), customerRoute(), time = 0), + "10 = 15 | " isParsedTo Advertisement(node(10), BGPRoute.self(), time = 15), + "10 = | " isParsedTo Advertisement(node(10), BGPRoute.self(), time = 0), + "10 = " isParsedTo Advertisement(node(10), BGPRoute.self(), time = 0), + "10 = 15" isParsedTo Advertisement(node(10), BGPRoute.self(), time = 15) + + ).forEach { (line, advertisement) -> + + given("entry is `$line`") { + + val advertisements = InterdomainAdvertisementReader(StringReader(line)).use { + it.read() + } + + then("it reads 1 advertisement") { + assertThat(advertisements.size, Is(1)) + } + + val info = advertisements[0] + + then("the advertiser has ID '${advertisement.advertiser.id}'") { + assertThat(info.advertiserID, Is(advertisement.advertiser.id)) + } + + then("the default route is '${advertisement.route}'") { + assertThat(info.defaultRoute, Is(advertisement.route)) + } + + then("the advertising time is '${advertisement.time}'") { + assertThat(info.time, Is(advertisement.time)) + } + } + } + + listOf( + "a = 0 | c", + "10 = 0 | c | 10", + "10 = 0 | b ", // 'b' is not a valid interdomain cost label + "10 = -1 | c ", // advertise time must be non-negative + "10 = a | c ", + "10 = 0 | c | " + + ).forEach { line -> + + given("invalid entry is `$line`") { + + var exception: ParseException? = null + + it("throws a ParseException") { + InterdomainAdvertisementReader(StringReader(line)).use { + exception = assertThrows(ParseException::class.java) { + it.read() + } + } + } + + it("indicates the error is in line 1") { + assertThat(exception?.lineNumber, Is(1)) + } + } + } + } + + + given("file with entries `10 = 0 | c` and `10 = 1 | r`") { + + val fileContent = lines( + "10 = 0 | c", + "10 = 1 | r" + ) + + val advertisements = InterdomainAdvertisementReader(StringReader(fileContent)).use { + it.read() + } + + it("reads 2 advertisements") { + assertThat(advertisements.size, Is(2)) + } + + it("reads one advertisement with advertiser ID 10") { + assertThat(advertisements[0].advertiserID, Is(10)) + } + + it("reads one advertisement with a customer route") { + assertThat(advertisements[0].defaultRoute, Is(customerRoute())) + } + + it("reads one advertisement with advertising time 0") { + assertThat(advertisements[0].time, Is(0)) + } + + it("reads another advertisement with advertiser ID 10") { + assertThat(advertisements[1].advertiserID, Is(10)) + } + + it("reads another advertisement with a peer route") { + assertThat(advertisements[1].defaultRoute, Is(peerRoute())) + } + + it("reads another advertisement with advertising time 1") { + assertThat(advertisements[1].time, Is(1)) + } + } + +}) \ No newline at end of file diff --git a/src/test/kotlin/io/InterdomainTopologyReaderTest.kt b/src/test/kotlin/io/InterdomainTopologyReaderTest.kt index 466d775..baaeee0 100644 --- a/src/test/kotlin/io/InterdomainTopologyReaderTest.kt +++ b/src/test/kotlin/io/InterdomainTopologyReaderTest.kt @@ -12,6 +12,7 @@ import org.jetbrains.spek.api.dsl.it import org.junit.jupiter.api.Assertions.assertThrows import testing.`when` import testing.bgp.BGPNode +import testing.then import java.io.StringReader import org.hamcrest.Matchers.`is` as Is @@ -142,6 +143,23 @@ object InterdomainTopologyReaderTest: Spek({ } } } + + `when`("a fixed MRAI value is set") { + + val content = lines( + "node = 1 | BGP | 5000", + "node = 2 | BGP | 1234" + ) + val topology = InterdomainTopologyReader(StringReader(content), forcedMRAI = 10).use { + it.read() + } + + then("all nodes are initialized with that MRAI value") { + topology.nodes.asSequence() + .map { it.protocol as BGP } + .forEach { assertThat(it.mrai, Is(10)) } + } + } } }) \ No newline at end of file diff --git a/src/test/kotlin/io/KeyValueParserTest.kt b/src/test/kotlin/io/KeyValueParserTest.kt new file mode 100644 index 0000000..ff515ca --- /dev/null +++ b/src/test/kotlin/io/KeyValueParserTest.kt @@ -0,0 +1,209 @@ +package io + +import com.nhaarman.mockito_kotlin.any +import com.nhaarman.mockito_kotlin.mock +import com.nhaarman.mockito_kotlin.times +import com.nhaarman.mockito_kotlin.verify +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 KeyValueParserTest: Spek({ + + given("an empty file") { + + val fileContent = "" + val handler: KeyValueParser.Handler = mock() + + on("parsing the file") { + + var exception: ParseException? = null + + it("throws a ParseException") { + KeyValueParser(StringReader(fileContent)).use { + exception = assertThrows(ParseException::class.java) { + it.parse(handler) + } + } + } + + it("indicates the error is in line 1") { + assertThat(exception?.lineNumber, Is(1)) + } + } + } + + listOf( + "node = 10" isParsedTo Entry(key = "node", values = listOf("10")), + "node = 10 | 11" isParsedTo Entry(key = "node", values = listOf("10", "11")), + "node = 10 | 11 - 9" isParsedTo Entry(key = "node", values = listOf("10", "11 - 9")), + "node = 10 | 11|abc|1a " isParsedTo Entry(key = "node", values = listOf("10", "11", "abc", "1a")), + "node = 10 | " isParsedTo Entry(key = "node", values = listOf("10", "")), + "node = 10 | | 11" isParsedTo Entry(key = "node", values = listOf("10", "", "11")), + "node = " isParsedTo Entry(key = "node", values = listOf("")), + "node = | | |" isParsedTo Entry(key = "node", values = listOf("", "", "", "")), + "node = | a | |" isParsedTo Entry(key = "node", values = listOf("", "a", "", "")), + "node = a=b" isParsedTo Entry(key = "node", values = listOf("a=b")), + "node = a = b" isParsedTo Entry(key = "node", values = listOf("a = b")), + "node = a | a=b" isParsedTo Entry(key = "node", values = listOf("a", "a=b")), + "node 1 = a" isParsedTo Entry(key = "node 1", values = listOf("a")), + "node ; a = a" isParsedTo Entry(key = "node ; a", values = listOf("a")), + "node | a = a" isParsedTo Entry(key = "node | a", values = listOf("a")), + "node | a = a | b | c" isParsedTo Entry(key = "node | a", values = listOf("a", "b", "c")) + ).forEach { (line, entry) -> + + given("file with single line `$line`") { + + val handler: KeyValueParser.Handler = mock() + + on("parsing the file") { + + KeyValueParser(StringReader(line)).use { + it.parse(handler) + } + + it("parsed a single entry") { + verify(handler, times(1)).onEntry(any(), any()) + } + + it("parsed an entry in line 1 with key '${entry.key}' and values ${entry.values}") { + verify(handler, times(1)).onEntry(entry, 1) + } + } + } + } + + given("file with lines `node = 10`, `link = 10 | 11`") { + + val fileContent = lines( + "node = 10", + "link = 10 | 11" + ) + val handler: KeyValueParser.Handler = mock() + + on("parsing the file") { + + KeyValueParser(StringReader(fileContent)).use { + it.parse(handler) + } + + it("parsed a 2 entries") { + verify(handler, times(2)).onEntry(any(), any()) + } + + it("parsed an entry in line 1 with key 'node' and values [10]") { + verify(handler, times(1)).onEntry(Entry("node", listOf("10")), 1) + } + + it("parsed an entry in line 2 with key 'link' and values [10, 11]") { + verify(handler, times(1)).onEntry(Entry("link", listOf("10", "11")), 2) + } + } + } + + given("file with lines `node = 10`, `link = 10 | 11`, ``") { + + val fileContent = lines( + "node = 10", + "link = 10 | 11", + "" + ) + val handler: KeyValueParser.Handler = mock() + + on("parsing the file") { + + KeyValueParser(StringReader(fileContent)).use { + it.parse(handler) + } + + it("parsed a 2 entries") { + verify(handler, times(2)).onEntry(any(), any()) + } + + it("parsed an entry in line 1 with key 'node' and values [10]") { + verify(handler, times(1)).onEntry(Entry("node", listOf("10")), 1) + } + + it("parsed an entry in line 2 with key 'link' and values [10]") { + verify(handler, times(1)).onEntry(Entry("link", listOf("10", "11")), 2) + } + } + } + + given("file with lines `node = 10`, ``, `link = 10 | 11`") { + + val fileContent = lines( + "node = 10", + "", + "link = 10 | 11" + ) + val handler: KeyValueParser.Handler = mock() + + on("parsing the file") { + + KeyValueParser(StringReader(fileContent)).use { + it.parse(handler) + } + + it("parsed a 2 entries") { + verify(handler, times(2)).onEntry(any(), any()) + } + + it("parsed an entry in line 1 with key 'node' and values [10]") { + verify(handler, times(1)).onEntry(Entry("node", listOf("10")), 1) + } + + it("parsed an entry in line 3 with key 'link' and values [10, 11]") { + verify(handler, times(1)).onEntry(Entry("link", listOf("10", "11")), 3) + } + } + } + + listOf( + "node", + "node is very good", + "node | a | b", + "node : a ", + "node - a " + ).forEach { line -> + + given("file with invalid line `$line`") { + + val handler: KeyValueParser.Handler = mock() + + on("parsing the file") { + + var exception: ParseException? = null + + it("throws a ParseException") { + KeyValueParser(StringReader(line)).use { + exception = assertThrows(ParseException::class.java) { + it.parse(handler) + } + } + } + + it("indicates the error is in line 1") { + assertThat(exception?.lineNumber, Is(1)) + } + } + } + } + +}) + +typealias Entry = KeyValueParser.Entry + + +infix fun String.isParsedTo(entry: Entry): Pair { + return Pair(this, entry) +} + + +fun lines(vararg lines: String): String = lines.joinToString("\n") diff --git a/src/test/kotlin/io/TopologyParserTest.kt b/src/test/kotlin/io/TopologyParserTest.kt index 468b2a3..ec265fb 100644 --- a/src/test/kotlin/io/TopologyParserTest.kt +++ b/src/test/kotlin/io/TopologyParserTest.kt @@ -22,9 +22,9 @@ object TopologyParserTest: Spek({ var exception: ParseException? = null it("throws a ParseException") { - TopologyParser(StringReader(fileContent), handler).use { + TopologyParser(StringReader(fileContent)).use { exception = assertThrows(ParseException::class.java) { - it.parse() + it.parse(handler) } } } @@ -42,8 +42,8 @@ object TopologyParserTest: Spek({ on("parsing the file") { - TopologyParser(StringReader(fileContent), handler).use { - it.parse() + TopologyParser(StringReader(fileContent)).use { + it.parse(handler) } it("parsed a single node") { @@ -67,8 +67,8 @@ object TopologyParserTest: Spek({ on("parsing the file") { - TopologyParser(StringReader(fileContent), handler).use { - it.parse() + TopologyParser(StringReader(fileContent)).use { + it.parse(handler) } it("parsed a single node") { @@ -92,8 +92,8 @@ object TopologyParserTest: Spek({ on("parsing the file") { - TopologyParser(StringReader(fileContent), handler).use { - it.parse() + TopologyParser(StringReader(fileContent)).use { + it.parse(handler) } it("parsed a single node") { @@ -117,8 +117,8 @@ object TopologyParserTest: Spek({ on("parsing the file") { - TopologyParser(StringReader(fileContent), handler).use { - it.parse() + TopologyParser(StringReader(fileContent)).use { + it.parse(handler) } it("parsed a single node") { @@ -142,8 +142,8 @@ object TopologyParserTest: Spek({ on("parsing the file") { - TopologyParser(StringReader(fileContent), handler).use { - it.parse() + TopologyParser(StringReader(fileContent)).use { + it.parse(handler) } it("parsed a single link") { @@ -167,8 +167,8 @@ object TopologyParserTest: Spek({ on("parsing the file") { - TopologyParser(StringReader(fileContent), handler).use { - it.parse() + TopologyParser(StringReader(fileContent)).use { + it.parse(handler) } it("parsed a single link") { @@ -197,8 +197,8 @@ object TopologyParserTest: Spek({ on("parsing the file") { - TopologyParser(StringReader(fileContent), handler).use { - it.parse() + TopologyParser(StringReader(fileContent)).use { + it.parse(handler) } it("parsed in line 1 a node with ID 10 and no values") { @@ -222,8 +222,8 @@ object TopologyParserTest: Spek({ on("parsing the file") { - TopologyParser(StringReader(fileContent), handler).use { - it.parse() + TopologyParser(StringReader(fileContent)).use { + it.parse(handler) } it("parsed in line 1 a node with ID 10 and no values") { @@ -247,8 +247,8 @@ object TopologyParserTest: Spek({ on("parsing the file") { - TopologyParser(StringReader(fileContent), handler).use { - it.parse() + TopologyParser(StringReader(fileContent)).use { + it.parse(handler) } it("parsed in line 1 a node with ID 10 and no values") { @@ -261,7 +261,7 @@ object TopologyParserTest: Spek({ } } - val incorrectLines = listOf( + val invalidLines = listOf( "node = a", "node = 10a", "element = 10", @@ -270,16 +270,24 @@ object TopologyParserTest: Spek({ "node = ", "node", "node == 10", - "node = 10 | 11 = 19", + "node = 10a", + "node = 10 = 19", "link = 10", "link = 10 | a", "link = a | 10", - "link = a | b" + "link = a | b", + "link = 10 | 11a", + "link = 10a | 11", + "link = 0 | ", + "link = 0 | | C", + "link = | 1 | C", + "link = | | C", + "link = | " ) - incorrectLines.forEach { line -> + invalidLines.forEach { line -> - given("file with incorrect line `$line`") { + given("file with invalid line `$line`") { val handler: TopologyParser.Handler = mock() @@ -288,9 +296,9 @@ object TopologyParserTest: Spek({ var exception: ParseException? = null it("throws a ParseException") { - TopologyParser(StringReader(line), handler).use { + TopologyParser(StringReader(line)).use { exception = assertThrows(ParseException::class.java) { - it.parse() + it.parse(handler) } } } diff --git a/src/test/kotlin/testing/Fakes.kt b/src/test/kotlin/testing/Fakes.kt index d76cdef..88c0481 100644 --- a/src/test/kotlin/testing/Fakes.kt +++ b/src/test/kotlin/testing/Fakes.kt @@ -32,25 +32,25 @@ fun fakeCompare(route1: Route, route2: Route): Int { object FakeProtocol: Protocol { override val inNeighbors: Collection> - get() = TODO("not implemented yet") + get() = throw UnsupportedOperationException() override val selectedRoute: Route - get() = TODO("not implemented yet") + get() = throw UnsupportedOperationException() override fun addInNeighbor(neighbor: Neighbor) { - TODO("not implemented yet") + throw UnsupportedOperationException() } - override fun start(node: Node) { - TODO("not implemented yet") + override fun advertise(node: Node, defaultRoute: Route) { + throw UnsupportedOperationException() } override fun process(message: Message) { - TODO("not implemented yet") + throw UnsupportedOperationException() } override fun reset() { - TODO("not implemented yet") + throw UnsupportedOperationException() } } diff --git a/src/test/kotlin/testing/Simulating.kt b/src/test/kotlin/testing/Simulating.kt new file mode 100644 index 0000000..45432d7 --- /dev/null +++ b/src/test/kotlin/testing/Simulating.kt @@ -0,0 +1,18 @@ +package testing + +import bgp.BGPRoute +import core.routing.Topology +import core.simulator.Advertisement +import core.simulator.Advertiser +import core.simulator.Engine +import core.simulator.Time + +/** + * Created on 09-11-2017 + * + * @author David Fialho + */ + +fun simulate(topology: Topology, advertiser: Advertiser, threshold: Time = Int.MAX_VALUE): Boolean { + return Engine.simulate(topology, Advertisement(advertiser, BGPRoute.self()), threshold) +} \ No newline at end of file