diff --git a/.github/workflows/gradle-java21.yml b/.github/workflows/gradle-java21.yml index 8699af966..cb7fa60c7 100644 --- a/.github/workflows/gradle-java21.yml +++ b/.github/workflows/gradle-java21.yml @@ -123,7 +123,7 @@ jobs: echo "Starting smoke test (shunting loop, 300 simulated seconds)..." echo "Timeout: 8 minutes. Under normal CI conditions: ~4-5 minutes." - if java -Dlogback.configurationFile=.github/workflows/logback-ci.xml -jar -ea build/libs/interlockSim.jar example shuntingLoop 300; then + if java -Dlogback.configurationFile=.github/workflows/logback-ci.xml -jar build/libs/interlockSim.jar example shuntingLoop 300; then echo "✓ Smoke test completed successfully" exit 0 else diff --git a/CLAUDE.md b/CLAUDE.md index e99b68021..430e6fcd5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -148,25 +148,24 @@ After building with `./gradlew shadowJar`, run from the project root: **Simulation mode:** ```bash -java -ea -jar build/libs/interlockSim.jar sim [xmlFile] +java -jar build/libs/interlockSim.jar sim [xmlFile] ``` **Editor mode:** ```bash -java -ea -jar build/libs/interlockSim.jar edit [xmlFile] +java -jar build/libs/interlockSim.jar edit [xmlFile] ``` **Built-in examples:** ```bash -java -ea -jar build/libs/interlockSim.jar example [exampleName] [endTime] +java -jar build/libs/interlockSim.jar example [exampleName] [endTime] ``` To list available examples, run: ```bash -java -ea -jar build/libs/interlockSim.jar example +java -jar build/libs/interlockSim.jar example ``` -**Note:** Enable assertions with `-ea` flag. For memory-constrained environments, add `-Xmx300m`. ## Docker Setup (Recommended) @@ -217,12 +216,12 @@ xhost -local:docker **Run simulation example:** ```bash -docker compose run app java -ea -jar interlockSim.jar example shuntingLoop 60 +docker compose run app java -jar interlockSim.jar example shuntingLoop 60 ``` **Run simulation with custom XML:** ```bash -docker compose run -v $(pwd)/myfile.xml:/app/myfile.xml app java -ea -jar interlockSim.jar sim myfile.xml +docker compose run -v $(pwd)/myfile.xml:/app/myfile.xml app java -jar interlockSim.jar sim myfile.xml ``` **Build thesis PDF:** @@ -837,12 +836,12 @@ Standard log levels (most to least verbose): **Runtime system property override:** ```bash -java -Dlogback.level=DEBUG -ea -cp "build/main:lib/compile/*" cz.vutbr.fit.interlockSim.Main example shuntingLoop 300 +java -Dlogback.level=DEBUG -cp "build/main:lib/compile/*" cz.vutbr.fit.interlockSim.Main example shuntingLoop 300 ``` **Docker environment variable:** ```bash -docker compose run -e ROOT_LOG_LEVEL=DEBUG app java -ea -jar interlockSim.jar example shuntingLoop 60 +docker compose run -e ROOT_LOG_LEVEL=DEBUG app java -jar interlockSim.jar example shuntingLoop 60 ``` ### Pre-configured Loggers @@ -1027,11 +1026,9 @@ None. All critical bugs identified by SonarQube have been fixed. **Severity:** Medium (production concern) **Files:** Multiple simulation classes -**Description:** Critical validation logic uses Java `assert` statements, which are disabled without the `-ea` flag. **Impact:** Invalid states may not be detected when running without assertions enabled. -**Workaround:** Always run the application with `-ea` flag: `java -ea -jar interlockSim.jar ...` **Recommendation:** Convert critical assertions to explicit validation with exceptions. diff --git a/Dockerfile b/Dockerfile index 1c62da595..386c80f86 100644 --- a/Dockerfile +++ b/Dockerfile @@ -107,5 +107,5 @@ RUN cp /app/interlockSim.jar /artifacts/ ENV DISPLAY=:0 # Default command: run editor GUI -# Users can override with: docker compose run app java -ea -jar interlockSim.jar sim file.xml +# Users can override with: docker compose run app java -jar interlockSim.jar sim file.xml CMD ["java", "-ea", "-jar", "interlockSim.jar", "edit"] diff --git a/README.md b/README.md index aa0e56ed7..4780c532f 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ docker compose build docker compose up app # Run simulation example -docker compose run app java -ea -jar interlockSim.jar example shuntingLoop 60 +docker compose run app java -jar interlockSim.jar example shuntingLoop 60 # Build thesis PDF docker compose up text @@ -146,7 +146,7 @@ Open the track editor to design railway layouts: Or manually (after building): ```bash -java -ea -jar build/libs/interlockSim.jar edit [xmlFile] +java -jar build/libs/interlockSim.jar edit [xmlFile] ``` ![InterlockSim Editor](text/img/Screenshot%20at%202026-01-03%2009-09-58.png) @@ -158,7 +158,7 @@ java -ea -jar build/libs/interlockSim.jar edit [xmlFile] Run a simulation from an XML configuration file: ```bash -java -ea -jar build/libs/interlockSim.jar sim [xmlFile] +java -jar build/libs/interlockSim.jar sim [xmlFile] ``` ### 3. Built-in Examples @@ -167,23 +167,23 @@ Run pre-configured simulation scenarios: ```bash # List all available examples -java -ea -jar build/libs/interlockSim.jar example +java -jar build/libs/interlockSim.jar example # Run shunting loop example for 300 time units -java -ea -jar build/libs/interlockSim.jar example shuntingLoop 300 +java -jar build/libs/interlockSim.jar example shuntingLoop 300 ``` **Quick example:** ```bash # Build and run shunting yard simulation (5 minutes model time) ./gradlew clean build -java -ea -jar build/libs/interlockSim.jar example shuntingLoop 300 +java -jar build/libs/interlockSim.jar example shuntingLoop 300 ``` ### Command-Line Synopsis ``` -java -ea -jar build/libs/interlockSim.jar (sim|edit|example) [arguments] +java -jar build/libs/interlockSim.jar (sim|edit|example) [arguments] ``` **Modes:** @@ -191,7 +191,7 @@ java -ea -jar build/libs/interlockSim.jar (sim|edit|example) [arguments] - `edit [file.xml]` - Open editor (optionally load file) - `example [name] [endTime]` - Run built-in example -**Note:** Always use `-ea` to enable assertions. For memory-constrained environments, add `-Xmx300`. +**Note:** For memory-constrained environments, add `-Xmx300`. --- @@ -301,12 +301,12 @@ xhost -local:docker **Run simulation example:** ```bash -docker compose run app java -ea -jar interlockSim.jar example shuntingLoop 60 +docker compose run app java -jar interlockSim.jar example shuntingLoop 60 ``` **Run simulation with custom XML:** ```bash -docker compose run -v $(pwd)/myfile.xml:/app/myfile.xml app java -ea -jar interlockSim.jar sim myfile.xml +docker compose run -v $(pwd)/myfile.xml:/app/myfile.xml app java -jar interlockSim.jar sim myfile.xml ``` **Build thesis PDF:** @@ -387,13 +387,13 @@ Edit `src/main/resources/logback.xml`: **Method 2: System property (runtime override)** ```bash -java -Dlogback.level=DEBUG -ea -cp "build/main:lib/compile/*" cz.vutbr.fit.interlockSim.Main example shuntingLoop 300 +java -Dlogback.level=DEBUG -cp "build/main:lib/compile/*" cz.vutbr.fit.interlockSim.Main example shuntingLoop 300 ``` **Method 3: Environment variable (Docker)** ```bash -docker compose run -e ROOT_LOG_LEVEL=DEBUG app java -ea -jar interlockSim.jar example shuntingLoop 60 +docker compose run -e ROOT_LOG_LEVEL=DEBUG app java -jar interlockSim.jar example shuntingLoop 60 ``` ### Pre-configured Loggers diff --git a/build.gradle.kts b/build.gradle.kts index 658e57ff4..243bb008e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -119,11 +119,6 @@ application { mainClass.set("cz.vutbr.fit.interlockSim.Main") } -// Enable assertions for all run tasks (including default 'run' task) -tasks.withType { - jvmArgs("-ea") -} - // Configure compilation tasks tasks.compileJava { // CRITICAL: Use ISO-8859-1 encoding (legacy requirement from Ant build) @@ -180,8 +175,6 @@ tasks.test { excludeTags("integration-test") } - // Enable assertions (matching Ant's -ea flag) - jvmArgs("-ea") // PARALLEL EXECUTION ENABLED (per user preference) // Tests run concurrently for faster execution (~30-60 sec vs 1-2 min sequential) @@ -235,8 +228,6 @@ val integrationTest by tasks.registering(Test::class) { includeTags("integration-test") } - // Enable assertions - jvmArgs("-ea") // Integration tests may be slower, use serial execution by default maxParallelForks = 1 @@ -417,8 +408,6 @@ val runSim by tasks.registering(JavaExec::class) { mainClass.set(application.mainClass.get()) args = listOf("example", "shuntingLoop", "60") - // Enable assertions (matching Ant) - jvmArgs("-ea") } /** @@ -434,8 +423,6 @@ val runEditor by tasks.registering(JavaExec::class) { mainClass.set(application.mainClass.get()) args = listOf("edit") - // Enable assertions - jvmArgs("-ea") } /** @@ -455,7 +442,6 @@ val runExample by tasks.registering(JavaExec::class) { val endTime = project.findProperty("endTime") as String? ?: "60" args = listOf("example", exampleName, endTime) - jvmArgs("-ea") } /** @@ -470,8 +456,6 @@ val runSimFromXml by tasks.registering(JavaExec::class) { classpath = sourceSets.main.get().runtimeClasspath mainClass.set(application.mainClass.get()) - // Enable assertions - jvmArgs("-ea") // Validate and set XML file path at execution time (not configuration time) doFirst { diff --git a/docker-compose.yml b/docker-compose.yml index af94c0011..a4e604359 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,10 +20,10 @@ # docker-compose up app # # # Run simulation with example: -# docker-compose run app java -ea -jar interlockSim.jar example shuntingLoop 60 +# docker-compose run app java -jar interlockSim.jar example shuntingLoop 60 # # # Run simulation with custom XML: -# docker-compose run -v $(pwd)/myfile.xml:/app/myfile.xml app java -ea -jar interlockSim.jar sim myfile.xml +# docker-compose run -v $(pwd)/myfile.xml:/app/myfile.xml app java -jar interlockSim.jar sim myfile.xml # # # Note: Requires GITHUB_TOKEN for GitHub Packages access # # Export GITHUB_TOKEN before building or create .env file diff --git a/src/main/kotlin/cz/vutbr/fit/interlockSim/Main.kt b/src/main/kotlin/cz/vutbr/fit/interlockSim/Main.kt index b51b1ac4c..1b6d856aa 100644 --- a/src/main/kotlin/cz/vutbr/fit/interlockSim/Main.kt +++ b/src/main/kotlin/cz/vutbr/fit/interlockSim/Main.kt @@ -20,7 +20,7 @@ import cz.vutbr.fit.interlockSim.context.SimulationContext.ReportType import cz.vutbr.fit.interlockSim.context.SimulationContextFactory import cz.vutbr.fit.interlockSim.gui.Frame import cz.vutbr.fit.interlockSim.sim.ShuntingLoop -import cz.vutbr.fit.interlockSim.sim.SimulationException +import cz.vutbr.fit.interlockSim.exceptions.SimulationException import cz.vutbr.fit.interlockSim.util.Util import cz.vutbr.fit.interlockSim.xml.XMLContextFactory import io.github.oshai.kotlinlogging.KotlinLogging diff --git a/src/main/kotlin/cz/vutbr/fit/interlockSim/context/DefaultContext.kt b/src/main/kotlin/cz/vutbr/fit/interlockSim/context/DefaultContext.kt index 7539a7b99..5f062c1b6 100644 --- a/src/main/kotlin/cz/vutbr/fit/interlockSim/context/DefaultContext.kt +++ b/src/main/kotlin/cz/vutbr/fit/interlockSim/context/DefaultContext.kt @@ -28,7 +28,7 @@ import cz.vutbr.fit.interlockSim.sim.Generator import cz.vutbr.fit.interlockSim.sim.InOutWorker import cz.vutbr.fit.interlockSim.sim.LoopProcess import cz.vutbr.fit.interlockSim.sim.ShuntingLoop -import cz.vutbr.fit.interlockSim.sim.SimulationException +import cz.vutbr.fit.interlockSim.exceptions.SimulationException import cz.vutbr.fit.interlockSim.util.ExtendedUnorientedGraph import cz.vutbr.fit.interlockSim.util.HashMapGraph import cz.vutbr.fit.interlockSim.util.Point diff --git a/src/main/kotlin/cz/vutbr/fit/interlockSim/context/SimulationContext.kt b/src/main/kotlin/cz/vutbr/fit/interlockSim/context/SimulationContext.kt index 984e2dc15..a3a8bee8e 100644 --- a/src/main/kotlin/cz/vutbr/fit/interlockSim/context/SimulationContext.kt +++ b/src/main/kotlin/cz/vutbr/fit/interlockSim/context/SimulationContext.kt @@ -19,7 +19,7 @@ import cz.vutbr.fit.interlockSim.objects.tracks.Track import cz.vutbr.fit.interlockSim.objects.tracks.TrackBlock import cz.vutbr.fit.interlockSim.objects.tracks.TrackSection import cz.vutbr.fit.interlockSim.sim.InOutWorker -import cz.vutbr.fit.interlockSim.sim.SimulationException +import cz.vutbr.fit.interlockSim.exceptions.SimulationException import java.util.Collection import java.util.EnumSet diff --git a/src/main/kotlin/cz/vutbr/fit/interlockSim/exceptions/EditorException.kt b/src/main/kotlin/cz/vutbr/fit/interlockSim/exceptions/EditorException.kt new file mode 100644 index 000000000..aa252e946 --- /dev/null +++ b/src/main/kotlin/cz/vutbr/fit/interlockSim/exceptions/EditorException.kt @@ -0,0 +1,81 @@ +/* Brno University of Technology + * Faculty of Information Technology + * + * BSc Thesis 2006/2007 + * + * Railway Interlocking Simulator + * + * Bedrich Hovorka + */ +package cz.vutbr.fit.interlockSim.exceptions + +/** + * Exception thrown during editor operations, validation, or user mistakes. + * Examples: wrong move, bad element placing, invalid configuration. + * + * @property severity Severity level of the exception + * @property obj Optional object associated with the exception + */ +class EditorException( + val severity: Severity, + message: String, + cause: Throwable?, + private val obj: Any? +) : Exception(message, cause) { + + /** + * Create EditorException with default FATAL severity + */ + constructor() : this(Severity.FATAL, "", null, null) + + /** + * Create EditorException with default FATAL severity + * @param obj Object associated with the exception + */ + constructor(obj: Any?) : this(Severity.FATAL, "", null, obj) + + /** + * Create EditorException with default FATAL severity + * @param message Error message + */ + constructor(message: String) : this(Severity.FATAL, message, null, null) + + /** + * Create EditorException with specified severity + * @param severity Severity level + * @param message Error message + */ + constructor(severity: Severity, message: String) : this(severity, message, null, null) + + /** + * Create EditorException with specified severity and object + * @param severity Severity level + * @param message Error message + * @param obj Object associated with the exception + */ + constructor(severity: Severity, message: String, obj: Any?) : this(severity, message, null, obj) + + /** + * Create EditorException with default FATAL severity and cause + * @param cause Underlying cause + */ + constructor(cause: Throwable?) : this(Severity.FATAL, "", cause, null) + + /** + * Create EditorException with specified severity and cause + * @param severity Severity level + * @param cause Underlying cause + * @param obj Object associated with the exception + */ + constructor(severity: Severity, cause: Throwable?, obj: Any?) : this(severity, "", cause, obj) + + /** + * @return object getter + */ + fun getObject(): Any? = obj + + override fun toString(): String { + val msg = message?.takeIf { it.isNotEmpty() } ?: "" + return "${this::class.simpleName}[$severity]: $msg" + } +} diff --git a/src/main/kotlin/cz/vutbr/fit/interlockSim/sim/PathSeparatorChangeException.kt b/src/main/kotlin/cz/vutbr/fit/interlockSim/exceptions/PathSeparatorChangeException.kt similarity index 95% rename from src/main/kotlin/cz/vutbr/fit/interlockSim/sim/PathSeparatorChangeException.kt rename to src/main/kotlin/cz/vutbr/fit/interlockSim/exceptions/PathSeparatorChangeException.kt index 932dde0f5..3806262d3 100644 --- a/src/main/kotlin/cz/vutbr/fit/interlockSim/sim/PathSeparatorChangeException.kt +++ b/src/main/kotlin/cz/vutbr/fit/interlockSim/exceptions/PathSeparatorChangeException.kt @@ -7,7 +7,7 @@ * * Bedrich Hovorka */ -package cz.vutbr.fit.interlockSim.sim +package cz.vutbr.fit.interlockSim.exceptions import cz.vutbr.fit.interlockSim.objects.paths.PathSeparator diff --git a/src/main/kotlin/cz/vutbr/fit/interlockSim/exceptions/RequireFunctions.kt b/src/main/kotlin/cz/vutbr/fit/interlockSim/exceptions/RequireFunctions.kt new file mode 100644 index 000000000..9599d9190 --- /dev/null +++ b/src/main/kotlin/cz/vutbr/fit/interlockSim/exceptions/RequireFunctions.kt @@ -0,0 +1,159 @@ +/* Brno University of Technology + * Faculty of Information Technology + * + * BSc Thesis 2006/2007 + * + * Railway Interlocking Simulator + * + * Bedrich Hovorka + */ +package cz.vutbr.fit.interlockSim.exceptions + +/** + * Utility functions for validation and requirement checking. + * These functions replace assert() calls and if-throw boilerplate. + * They throw appropriate exceptions with meaningful messages. + */ + +/** + * Requires that the given value is true for simulation logic. + * Throws SimulationException if the value is false. + * + * @param value The boolean value to check + * @param lazyMessage A function producing the error message (only evaluated if value is false) + * @throws SimulationException if value is false + */ +inline fun requireSimulation(value: Boolean, lazyMessage: () -> String = { "Simulation requirement failed" }) { + if (!value) { + throw SimulationException(lazyMessage()) + } +} + +/** + * Requires that the given value is true for simulation logic. + * Throws SimulationException with specified severity if the value is false. + * + * @param value The boolean value to check + * @param severity Severity level of the exception + * @param lazyMessage A function producing the error message (only evaluated if value is false) + * @throws SimulationException if value is false + */ +inline fun requireSimulation( + value: Boolean, + severity: Severity, + lazyMessage: () -> String = { "Simulation requirement failed" } +) { + if (!value) { + throw SimulationException(severity, lazyMessage(), null, null) + } +} + +/** + * Requires that the given value is not null for simulation logic. + * Throws SimulationException if the value is null. + * + * @param value The value to check + * @param lazyMessage A function producing the error message (only evaluated if value is null) + * @return The non-null value + * @throws SimulationException if value is null + */ +inline fun requireSimulationNotNull( + value: T?, + lazyMessage: () -> String = { "Required value was null" } +): T { + if (value == null) { + throw SimulationException(lazyMessage()) + } + return value +} + +/** + * Requires that the given state is valid for simulation logic. + * Throws SimulationException if the value is false. + * + * @param value The boolean value to check + * @param lazyMessage A function producing the error message (only evaluated if value is false) + * @throws SimulationException if value is false + */ +inline fun requireSimulationState(value: Boolean, lazyMessage: () -> String = { "Invalid simulation state" }) { + if (!value) { + throw SimulationException(lazyMessage()) + } +} + +/** + * Requires that the given value is true for editor operations. + * Throws EditorException if the value is false. + * + * @param value The boolean value to check + * @param lazyMessage A function producing the error message (only evaluated if value is false) + * @throws EditorException if value is false + */ +inline fun requireEditor(value: Boolean, lazyMessage: () -> String = { "Editor requirement failed" }) { + if (!value) { + throw EditorException(lazyMessage()) + } +} + +/** + * Requires that the given value is true for editor operations. + * Throws EditorException with specified severity if the value is false. + * + * @param value The boolean value to check + * @param severity Severity level of the exception + * @param lazyMessage A function producing the error message (only evaluated if value is false) + * @throws EditorException if value is false + */ +inline fun requireEditor( + value: Boolean, + severity: Severity, + lazyMessage: () -> String = { "Editor requirement failed" } +) { + if (!value) { + throw EditorException(severity, lazyMessage()) + } +} + +/** + * Requires that the given value is not null for editor operations. + * Throws EditorException if the value is null. + * + * @param value The value to check + * @param lazyMessage A function producing the error message (only evaluated if value is null) + * @return The non-null value + * @throws EditorException if value is null + */ +inline fun requireEditorNotNull(value: T?, lazyMessage: () -> String = { "Required value was null" }): T { + if (value == null) { + throw EditorException(lazyMessage()) + } + return value +} + +/** + * Requires that the given argument is valid. + * Throws IllegalArgumentException if the value is false. + * + * @param value The boolean value to check + * @param lazyMessage A function producing the error message (only evaluated if value is false) + * @throws IllegalArgumentException if value is false + */ +inline fun requireValidArgument(value: Boolean, lazyMessage: () -> String = { "Invalid argument" }) { + if (!value) { + throw IllegalArgumentException(lazyMessage()) + } +} + +/** + * Requires that the given state is valid. + * Throws IllegalStateException if the value is false. + * + * @param value The boolean value to check + * @param lazyMessage A function producing the error message (only evaluated if value is false) + * @throws IllegalStateException if value is false + */ +inline fun requireValidState(value: Boolean, lazyMessage: () -> String = { "Invalid state" }) { + if (!value) { + throw IllegalStateException(lazyMessage()) + } +} diff --git a/src/main/kotlin/cz/vutbr/fit/interlockSim/exceptions/Severity.kt b/src/main/kotlin/cz/vutbr/fit/interlockSim/exceptions/Severity.kt new file mode 100644 index 000000000..eb5e83a1b --- /dev/null +++ b/src/main/kotlin/cz/vutbr/fit/interlockSim/exceptions/Severity.kt @@ -0,0 +1,35 @@ +/* Brno University of Technology + * Faculty of Information Technology + * + * BSc Thesis 2006/2007 + * + * Railway Interlocking Simulator + * + * Bedrich Hovorka + */ +package cz.vutbr.fit.interlockSim.exceptions + +/** + * Severity level for exceptions in the interlocking simulator + * + * Defines how critical an exception is and how the application should respond: + * - FATAL: Must end operation immediately (simulation or editor) + * - ERROR: Can be recovered by user decision, but should be handled ASAP + * - WARN: Can be fixed by user decision later, non-critical + */ +enum class Severity { + /** + * Fatal error - must terminate operation immediately + */ + FATAL, + + /** + * Error - can be recovered by user decision but requires immediate attention + */ + ERROR, + + /** + * Warning - can be fixed by user decision later + */ + WARN +} diff --git a/src/main/kotlin/cz/vutbr/fit/interlockSim/exceptions/SimulationException.kt b/src/main/kotlin/cz/vutbr/fit/interlockSim/exceptions/SimulationException.kt new file mode 100644 index 000000000..bdb6816fa --- /dev/null +++ b/src/main/kotlin/cz/vutbr/fit/interlockSim/exceptions/SimulationException.kt @@ -0,0 +1,86 @@ +/* Brno University of Technology + * Faculty of Information Technology + * + * BSc Thesis 2006/2007 + * + * Railway Interlocking Simulator + * + * Bedrich Hovorka + */ +package cz.vutbr.fit.interlockSim.exceptions + +import jDisco.Process + +/** + * Exception thrown during simulation - at start, between start and end of simulation. + * + * @property severity Severity level of the exception + */ +open class SimulationException( + val severity: Severity, + message: String, + cause: Throwable?, + private val obj: Any? +) : Exception(message, cause) { + private val time: Double = Process.time() + + /** + * Create SimulationException with default FATAL severity + */ + constructor() : this(Severity.FATAL, "", null, null) + + /** + * Create SimulationException with default FATAL severity + * @param obj Object associated with the exception + */ + constructor(obj: Any?) : this(Severity.FATAL, "", null, obj) + + /** + * Create SimulationException with default FATAL severity + * @param message Error message + */ + constructor(message: String) : this(Severity.FATAL, message, null, null) + + /** + * Create SimulationException with default FATAL severity + * @param message Error message + * @param obj Object associated with the exception + */ + constructor(message: String, obj: Any?) : this(Severity.FATAL, message, null, obj) + + /** + * Create SimulationException with default FATAL severity + * @param cause Underlying cause + */ + constructor(cause: Throwable?) : this(Severity.FATAL, "", cause, null) + + /** + * Create SimulationException with default FATAL severity + * @param cause Underlying cause + * @param obj Object associated with the exception + */ + constructor(cause: Throwable?, obj: Any?) : this(Severity.FATAL, "", cause, obj) + + /** + * Create SimulationException with default FATAL severity + * @param message Error message + * @param cause Underlying cause + * @param obj Object associated with the exception + */ + constructor(message: String, cause: Throwable?, obj: Any?) : this(Severity.FATAL, message, cause, obj) + + /** + * @return object getter + */ + open fun getObject(): Any? = obj + + /** + * @return model time of exception + */ + fun getTime(): Double = time + + override fun toString(): String { + val msg = message?.takeIf { it.isNotEmpty() } ?: "" + return "${this::class.simpleName}[$severity]: $msg at time $time" + } +} diff --git a/src/main/kotlin/cz/vutbr/fit/interlockSim/sim/TrackOperationException.kt b/src/main/kotlin/cz/vutbr/fit/interlockSim/exceptions/TrackOperationException.kt similarity index 94% rename from src/main/kotlin/cz/vutbr/fit/interlockSim/sim/TrackOperationException.kt rename to src/main/kotlin/cz/vutbr/fit/interlockSim/exceptions/TrackOperationException.kt index f540e80e8..ca08c7189 100644 --- a/src/main/kotlin/cz/vutbr/fit/interlockSim/sim/TrackOperationException.kt +++ b/src/main/kotlin/cz/vutbr/fit/interlockSim/exceptions/TrackOperationException.kt @@ -7,7 +7,7 @@ * * Bedrich Hovorka */ -package cz.vutbr.fit.interlockSim.sim +package cz.vutbr.fit.interlockSim.exceptions import cz.vutbr.fit.interlockSim.objects.tracks.Track diff --git a/src/main/kotlin/cz/vutbr/fit/interlockSim/objects/cells/InOut.kt b/src/main/kotlin/cz/vutbr/fit/interlockSim/objects/cells/InOut.kt index 6dbeee091..ec417c2cd 100644 --- a/src/main/kotlin/cz/vutbr/fit/interlockSim/objects/cells/InOut.kt +++ b/src/main/kotlin/cz/vutbr/fit/interlockSim/objects/cells/InOut.kt @@ -9,9 +9,10 @@ */ package cz.vutbr.fit.interlockSim.objects.cells +import cz.vutbr.fit.interlockSim.exceptions.requireSimulation import cz.vutbr.fit.interlockSim.objects.cells.RailSemaphore.Signal import cz.vutbr.fit.interlockSim.objects.paths.PathElement -import cz.vutbr.fit.interlockSim.sim.PathSeparatorChangeException +import cz.vutbr.fit.interlockSim.exceptions.PathSeparatorChangeException import java.util.EnumSet import java.util.Set @@ -31,7 +32,9 @@ class InOut( init { this.name = name this.inSemaphore = RailSemaphore(!orientation, spatialType) - assert(inSemaphore.direction() == Cell.Segment.anti(direction())) + requireSimulation(inSemaphore.direction() == Cell.Segment.anti(direction())) { + "In semaphore direction must be anti-parallel to InOut direction" + } this.outSemaphore = RailSemaphore.getConstantInstance(orientation, spatialType, Signal.FREE) setName(name) } @@ -40,7 +43,7 @@ class InOut( override fun getFollowingSegment(from: Cell.Segment?): Cell.Segment? { if (from == null) return direction() - assert(from === direction()) { "$from ${direction()}" } + requireSimulation(from === direction()) { "Invalid segment: $from, expected: ${direction()}" } return null } diff --git a/src/main/kotlin/cz/vutbr/fit/interlockSim/objects/cells/OrientedNodeCell.kt b/src/main/kotlin/cz/vutbr/fit/interlockSim/objects/cells/OrientedNodeCell.kt index 034f18ea6..3bc7e6e63 100644 --- a/src/main/kotlin/cz/vutbr/fit/interlockSim/objects/cells/OrientedNodeCell.kt +++ b/src/main/kotlin/cz/vutbr/fit/interlockSim/objects/cells/OrientedNodeCell.kt @@ -9,6 +9,7 @@ */ package cz.vutbr.fit.interlockSim.objects.cells +import cz.vutbr.fit.interlockSim.exceptions.requireSimulationNotNull import cz.vutbr.fit.interlockSim.objects.paths.OrientedPathSeparator import java.util.EnumSet import java.util.Set @@ -28,7 +29,7 @@ abstract class OrientedNodeCell protected constructor( override fun direction(): Cell.Segment { val st = getSpatialType() - assert(st != null) { this } + requireSimulationNotNull(st) { "Spatial type must not be null for: $this" } return st.segments[if (getOrientation()) 1 else 0] } } diff --git a/src/main/kotlin/cz/vutbr/fit/interlockSim/objects/cells/RailSemaphore.kt b/src/main/kotlin/cz/vutbr/fit/interlockSim/objects/cells/RailSemaphore.kt index b2501950f..b20c50567 100644 --- a/src/main/kotlin/cz/vutbr/fit/interlockSim/objects/cells/RailSemaphore.kt +++ b/src/main/kotlin/cz/vutbr/fit/interlockSim/objects/cells/RailSemaphore.kt @@ -9,8 +9,9 @@ */ package cz.vutbr.fit.interlockSim.objects.cells +import cz.vutbr.fit.interlockSim.exceptions.requireSimulation import cz.vutbr.fit.interlockSim.objects.paths.PathElement -import cz.vutbr.fit.interlockSim.sim.PathSeparatorChangeException +import cz.vutbr.fit.interlockSim.exceptions.PathSeparatorChangeException import io.github.oshai.kotlinlogging.KotlinLogging import java.util.Set @@ -87,7 +88,9 @@ open class RailSemaphore( */ @JvmStatic fun forSpeed(speed: Double): Signal { - assert(speed >= S30.allowedSpeed() || speed == 0.0) + requireSimulation(speed >= S30.allowedSpeed() || speed == 0.0) { + "Speed must be at least S30 allowed speed or 0.0: $speed" + } for (s in values) { if (s.allowedSpeed() > speed) return if (s.ordinal == 0) STOP else values[s.ordinal - 1] } diff --git a/src/main/kotlin/cz/vutbr/fit/interlockSim/objects/cells/RailSwitch.kt b/src/main/kotlin/cz/vutbr/fit/interlockSim/objects/cells/RailSwitch.kt index e22e56bdb..d7c96eb9d 100644 --- a/src/main/kotlin/cz/vutbr/fit/interlockSim/objects/cells/RailSwitch.kt +++ b/src/main/kotlin/cz/vutbr/fit/interlockSim/objects/cells/RailSwitch.kt @@ -9,7 +9,9 @@ */ package cz.vutbr.fit.interlockSim.objects.cells -import cz.vutbr.fit.interlockSim.sim.PathSeparatorChangeException +import cz.vutbr.fit.interlockSim.exceptions.requireSimulation +import cz.vutbr.fit.interlockSim.exceptions.requireSimulationNotNull +import cz.vutbr.fit.interlockSim.exceptions.PathSeparatorChangeException import cz.vutbr.fit.interlockSim.util.EnumUnorientedGraph import io.github.oshai.kotlinlogging.KotlinLogging import java.beans.PropertyChangeListener @@ -177,7 +179,7 @@ class RailSwitch : NodeCell { "(safety SI-5: switch cannot toggle during train movement)" ) } - assert(conf != null) + requireSimulationNotNull(conf) { "Switch configuration must not be null" } val oldConf = conf conf = if (conf == Conf.MAIN) Conf.BRANCH else Conf.MAIN logger.info { @@ -197,7 +199,7 @@ class RailSwitch : NodeCell { override fun allowedSpeed(): Double { val double1 = speeds.get(getConf()) - assert(double1 != null) { speeds } + requireSimulationNotNull(double1) { "Speed for configuration must not be null: speeds=$speeds" } return double1!!.toDouble() } @@ -366,7 +368,7 @@ class RailSwitch : NodeCell { val merging = st.segments[if (t.getMergingPosition()) 0 else 1] // je v proti-directionu val mainDir = Cell.Segment.anti(merging) val set = branches.get(t, st) - assert(set!!.size == 1) { set } + requireSimulation(set!!.size == 1) { "Branch set must have exactly 1 element: $set" } val branch = set.iterator().next() confs.put(merging, branch, Conf.BRANCH) confs.put(merging, mainDir, Conf.MAIN) diff --git a/src/main/kotlin/cz/vutbr/fit/interlockSim/objects/cells/TrackBlockPart.kt b/src/main/kotlin/cz/vutbr/fit/interlockSim/objects/cells/TrackBlockPart.kt index 6da9d7234..908d0b82b 100644 --- a/src/main/kotlin/cz/vutbr/fit/interlockSim/objects/cells/TrackBlockPart.kt +++ b/src/main/kotlin/cz/vutbr/fit/interlockSim/objects/cells/TrackBlockPart.kt @@ -9,6 +9,7 @@ */ package cz.vutbr.fit.interlockSim.objects.cells // jinde? +import cz.vutbr.fit.interlockSim.exceptions.requireValidArgument import cz.vutbr.fit.interlockSim.objects.tracks.TrackBlock import java.util.Set @@ -21,7 +22,7 @@ class TrackBlockPart( private val segments: Array ) : AbstractCell() { init { - assert(segments.isNotEmpty()) + requireValidArgument(segments.isNotEmpty()) { "Segments array must not be empty" } } /** diff --git a/src/main/kotlin/cz/vutbr/fit/interlockSim/objects/paths/AbstractPath.kt b/src/main/kotlin/cz/vutbr/fit/interlockSim/objects/paths/AbstractPath.kt index 04d0b0332..360703426 100644 --- a/src/main/kotlin/cz/vutbr/fit/interlockSim/objects/paths/AbstractPath.kt +++ b/src/main/kotlin/cz/vutbr/fit/interlockSim/objects/paths/AbstractPath.kt @@ -11,6 +11,9 @@ package cz.vutbr.fit.interlockSim.objects.paths import cz.vutbr.fit.interlockSim.context.SimulationContext import cz.vutbr.fit.interlockSim.context.SimulationContext.ReportType +import cz.vutbr.fit.interlockSim.exceptions.requireSimulation +import cz.vutbr.fit.interlockSim.exceptions.requireSimulationNotNull +import cz.vutbr.fit.interlockSim.exceptions.requireValidArgument import cz.vutbr.fit.interlockSim.objects.cells.Cell.Segment import cz.vutbr.fit.interlockSim.objects.cells.InOut import cz.vutbr.fit.interlockSim.objects.cells.RailSemaphore @@ -18,8 +21,9 @@ import cz.vutbr.fit.interlockSim.objects.cells.RailSwitch import cz.vutbr.fit.interlockSim.objects.tracks.AbstractTrack import cz.vutbr.fit.interlockSim.objects.tracks.SimpleTrack import cz.vutbr.fit.interlockSim.objects.tracks.Track -import cz.vutbr.fit.interlockSim.sim.PathSeparatorChangeException -import cz.vutbr.fit.interlockSim.sim.TrackOperationException +import cz.vutbr.fit.interlockSim.exceptions.PathSeparatorChangeException +import cz.vutbr.fit.interlockSim.exceptions.SimulationException +import cz.vutbr.fit.interlockSim.exceptions.TrackOperationException import cz.vutbr.fit.interlockSim.util.Util import io.github.oshai.kotlinlogging.KotlinLogging import java.lang.reflect.InvocationTargetException @@ -54,8 +58,8 @@ abstract class AbstractPath protected constructor( Collections.addAll(nameList, CANCEL_PATH_SETUP, SET_UP_PATH, IS_FREE_FROM, IS_SET_UP_PATH) fillMethodMap(trackMethods, Track::class.java, nameList as List) } catch (e: Exception) { - assert(false) { e } - e.printStackTrace() + // Unexpected error during method map initialization + throw SimulationException("Failed to initialize track methods", e, null) } } @@ -176,7 +180,9 @@ abstract class AbstractPath protected constructor( logger.debug { "Method invocation returned false for method: $methodName" } return false } - assert(invoke == null || java.lang.Boolean.TRUE == invoke) { invoke } + requireSimulation(invoke == null || java.lang.Boolean.TRUE == invoke) { + "Track method invocation returned unexpected result: $invoke" + } previous = nextTrack } if (method == trackMethods[SET_UP_PATH]) { @@ -199,10 +205,10 @@ abstract class AbstractPath protected constructor( previous: Track?, next: Track ): Boolean { - assert(methodName != null && next != null) + requireValidArgument(methodName != null && next != null) { "Method name and next track must not be null" } val from = context.getSegment(separator, previous, next) val to = context.getSegment(separator, next, previous) - assert(!Segment.conflict(from, to)) { "$from $to" } + requireSimulation(!Segment.conflict(from, to)) { "Segment conflict: from=$from, to=$to" } // NOTE: from and to CAN be null - this is intentional and matches Java behavior // getSegment() returns null when no segment exists (e.g., InOut.getFollowingSegment) @@ -229,7 +235,9 @@ abstract class AbstractPath protected constructor( } val following = separator.getFollowingSegment(from) logger.debug { "SET_UP_PATH: getFollowingSegment($from) returned $following, expected $to" } - assert(following === to) { "Separator $separator: getFollowingSegment($from) returned $following but expected $to" } + requireSimulation(following === to) { + "Separator $separator: getFollowingSegment($from) returned $following but expected $to" + } } else if (methodName == IS_FREE_FROM) { // Java: //EMPTY // Intentionally empty - segments can be null, no action needed @@ -264,14 +272,16 @@ abstract class AbstractPath protected constructor( } } context.report("", this, ReportType.PATH_SETTING) - assert(maxSpeed(sep) >= PathElement.MINIMAL_MAX_SPEED) { maxSpeed(sep) } + requireSimulation(maxSpeed(sep) >= PathElement.MINIMAL_MAX_SPEED) { + "Max speed must be at least MINIMAL_MAX_SPEED, got: ${maxSpeed(sep)}" + } } @Suppress("RETURN_TYPE_MISMATCH_ON_OVERRIDE", "UNCHECKED_CAST") protected fun getIterator(sep: PathSeparator): Iterator { if (!isEnd(sep)) throw IllegalArgumentException("Is not end of abstrPath") if (sep == getFirst()) return iterator() as Iterator - assert(sep == getLast()) + requireSimulation(sep == getLast()) { "Separator must be either first or last" } return (descendingIterator() as Iterator?)!! } @@ -284,10 +294,10 @@ abstract class AbstractPath protected constructor( val pathIt = path.iterator() while (thisIt.hasNext()) { - assert(pathIt.hasNext()) + requireSimulation(pathIt.hasNext()) { "Path iterator ended prematurely" } val thisNext = thisIt.next() val pathNext = pathIt.next() - assert(thisNext != null) + requireSimulationNotNull(thisNext) { "Path element must not be null" } if (thisNext != pathNext) return false } diff --git a/src/main/kotlin/cz/vutbr/fit/interlockSim/objects/paths/PathSeparator.kt b/src/main/kotlin/cz/vutbr/fit/interlockSim/objects/paths/PathSeparator.kt index 4ac21823a..4fc8ac625 100644 --- a/src/main/kotlin/cz/vutbr/fit/interlockSim/objects/paths/PathSeparator.kt +++ b/src/main/kotlin/cz/vutbr/fit/interlockSim/objects/paths/PathSeparator.kt @@ -10,7 +10,7 @@ package cz.vutbr.fit.interlockSim.objects.paths import cz.vutbr.fit.interlockSim.objects.cells.Cell -import cz.vutbr.fit.interlockSim.sim.PathSeparatorChangeException +import cz.vutbr.fit.interlockSim.exceptions.PathSeparatorChangeException import java.util.Set /** diff --git a/src/main/kotlin/cz/vutbr/fit/interlockSim/objects/tracks/SimpleTrack.kt b/src/main/kotlin/cz/vutbr/fit/interlockSim/objects/tracks/SimpleTrack.kt index 6815dc3a6..329aeea8f 100644 --- a/src/main/kotlin/cz/vutbr/fit/interlockSim/objects/tracks/SimpleTrack.kt +++ b/src/main/kotlin/cz/vutbr/fit/interlockSim/objects/tracks/SimpleTrack.kt @@ -9,9 +9,11 @@ */ package cz.vutbr.fit.interlockSim.objects.tracks +import cz.vutbr.fit.interlockSim.exceptions.requireSimulation +import cz.vutbr.fit.interlockSim.exceptions.requireSimulationNotNull import cz.vutbr.fit.interlockSim.objects.paths.PathElement import cz.vutbr.fit.interlockSim.objects.paths.PathSeparator -import cz.vutbr.fit.interlockSim.sim.TrackOperationException +import cz.vutbr.fit.interlockSim.exceptions.TrackOperationException import io.github.oshai.kotlinlogging.KotlinLogging import jDisco.Process import java.util.IdentityHashMap @@ -61,7 +63,7 @@ abstract class SimpleTrack( "${Process.time()} CONFLICT: Block $this collision! Existing occupant=${`in`}, occupant=$occupant" } } - assert(`in` == null) // tak to zavani srazkou (posuny neimplementovany) + requireSimulation(`in` == null) { "Track occupant collision - must be null on entry (shunting not implemented)" } assertGoodStateChange(TrackFacility.State.RESERVED, TrackFacility.State.OCCUPIED) `in` = occupant from = null @@ -71,7 +73,7 @@ abstract class SimpleTrack( logger.info { "${Process.time()} Block $this EXIT: occupant=$occupant, state=OCCUPIED->FREE" } - assert(`in` === occupant) + requireSimulation(`in` === occupant) { "Track occupant mismatch on leave" } assertGoodStateChange(TrackFacility.State.OCCUPIED, TrackFacility.State.FREE) `in` = null } @@ -96,12 +98,12 @@ abstract class SimpleTrack( } override fun isSetUpPath(sep: PathSeparator): Boolean { - assert(sep != null) + requireSimulationNotNull(sep) { "Path separator must not be null" } val isSetUp: Boolean if (state == TrackFacility.State.RESERVED) { isSetUp = sep === from } else { - assert(from == null) + requireSimulation(from == null) { "From separator must be null when state is not RESERVED" } isSetUp = false } logger.debug { "Track $this isSetUpPath check: sep=$sep, state=$state, from=$from, result=$isSetUp" } @@ -147,7 +149,7 @@ abstract class SimpleTrack( ) { // mozna nekdy jina vyjimka... val stateChange = stateChange(from, to) - assert(stateChange) { errorStateMessage(from) } + requireSimulation(stateChange) { errorStateMessage(from) } } override fun length(): Double = length @@ -158,13 +160,13 @@ abstract class SimpleTrack( override fun getTrackOccupant(): TrackOccupant { // Note: Track must be occupied when this method is called, but field is nullable for initialization - // This violates the interface contract if called when track is free - assert checks this - assert(`in` != null) { "Track occupant should not be null - must call when track is OCCUPIED" } + // This violates the interface contract if called when track is free - requireSimulation checks this + requireSimulationNotNull(`in`) { "Track occupant should not be null - must call when track is OCCUPIED" } return `in`!! } override fun maxSpeed(from: PathSeparator?): Double { - assert(isEnd(from!!)) + requireSimulation(isEnd(from!!)) { "Path separator must be an end of this track" } return speeds[from]!! } } diff --git a/src/main/kotlin/cz/vutbr/fit/interlockSim/objects/tracks/Track.kt b/src/main/kotlin/cz/vutbr/fit/interlockSim/objects/tracks/Track.kt index 9d41bbf15..5e4ff9efe 100644 --- a/src/main/kotlin/cz/vutbr/fit/interlockSim/objects/tracks/Track.kt +++ b/src/main/kotlin/cz/vutbr/fit/interlockSim/objects/tracks/Track.kt @@ -11,7 +11,7 @@ package cz.vutbr.fit.interlockSim.objects.tracks import cz.vutbr.fit.interlockSim.objects.paths.PathElement import cz.vutbr.fit.interlockSim.objects.paths.PathSeparator -import cz.vutbr.fit.interlockSim.sim.TrackOperationException +import cz.vutbr.fit.interlockSim.exceptions.TrackOperationException /** * "jakykoliv (z ruznych pohledu) usek koleji mezi pathseparatory (cast cesty)" diff --git a/src/main/kotlin/cz/vutbr/fit/interlockSim/sim/InOutWorker.kt b/src/main/kotlin/cz/vutbr/fit/interlockSim/sim/InOutWorker.kt index 3a7c83347..b59c19f78 100644 --- a/src/main/kotlin/cz/vutbr/fit/interlockSim/sim/InOutWorker.kt +++ b/src/main/kotlin/cz/vutbr/fit/interlockSim/sim/InOutWorker.kt @@ -11,6 +11,7 @@ package cz.vutbr.fit.interlockSim.sim import cz.vutbr.fit.interlockSim.context.SimulationContext import cz.vutbr.fit.interlockSim.context.SimulationContext.ReportType +import cz.vutbr.fit.interlockSim.exceptions.TrackOperationException import cz.vutbr.fit.interlockSim.objects.cells.InOut import cz.vutbr.fit.interlockSim.objects.paths.Path import cz.vutbr.fit.interlockSim.objects.tracks.TrackSection diff --git a/src/main/kotlin/cz/vutbr/fit/interlockSim/sim/ShuntingLoop.kt b/src/main/kotlin/cz/vutbr/fit/interlockSim/sim/ShuntingLoop.kt index 739ed6cf3..9173ef308 100644 --- a/src/main/kotlin/cz/vutbr/fit/interlockSim/sim/ShuntingLoop.kt +++ b/src/main/kotlin/cz/vutbr/fit/interlockSim/sim/ShuntingLoop.kt @@ -12,6 +12,7 @@ package cz.vutbr.fit.interlockSim.sim import cz.vutbr.fit.interlockSim.context.RailwayNetGrid import cz.vutbr.fit.interlockSim.context.SimulationContext import cz.vutbr.fit.interlockSim.context.SimulationContext.ReportType +import cz.vutbr.fit.interlockSim.exceptions.TrackOperationException import cz.vutbr.fit.interlockSim.objects.cells.Cell import cz.vutbr.fit.interlockSim.objects.cells.Cell.Segment import cz.vutbr.fit.interlockSim.objects.cells.InOut diff --git a/src/main/kotlin/cz/vutbr/fit/interlockSim/sim/SimulationException.kt b/src/main/kotlin/cz/vutbr/fit/interlockSim/sim/SimulationException.kt deleted file mode 100644 index ee9a5d6e9..000000000 --- a/src/main/kotlin/cz/vutbr/fit/interlockSim/sim/SimulationException.kt +++ /dev/null @@ -1,78 +0,0 @@ -/* Brno University of Technology - * Faculty of Information Technology - * - * BSc Thesis 2006/2007 - * - * Railway Interlocking Simulator - * - * Bedrich Hovorka - */ -package cz.vutbr.fit.interlockSim.sim - -import jDisco.Process - -/** - * - */ -open class SimulationException : Exception { - private val obj: Any? - private val time: Double - - /** - * - */ - constructor() : this(null as Any?) - - /** - * @param object - * - */ - constructor(obj: Any?) : this("", obj) - - /** - * @param message - */ - constructor(message: String) : this(message, null as Any?) - - /** - * @param message - * @param object - */ - constructor(message: String, obj: Any?) : this(message, null as Throwable?, obj) - - /** - * @param cause - */ - constructor(cause: Throwable?) : this(cause, null as Any?) - - /** - * @param cause - * @param object - */ - constructor(cause: Throwable?, obj: Any?) : this("", cause, obj) - - /** - * @param message - * @param cause - * @param object - */ - constructor(message: String, cause: Throwable?, obj: Any?) : super(message, cause) { - this.obj = obj - this.time = Process.time() - } - - /** - * @return object getter - */ - open fun getObject(): Any? = obj - - /** - * @return model time of exception - */ - fun getTime(): Double = time - - override fun toString(): String { - val msg = message?.takeIf { it.isNotEmpty() } ?: "" - return "${this::class.simpleName}: $msg at time $time" - } -} diff --git a/src/main/kotlin/cz/vutbr/fit/interlockSim/sim/Train.kt b/src/main/kotlin/cz/vutbr/fit/interlockSim/sim/Train.kt index 3fb160f68..4ed1af52a 100644 --- a/src/main/kotlin/cz/vutbr/fit/interlockSim/sim/Train.kt +++ b/src/main/kotlin/cz/vutbr/fit/interlockSim/sim/Train.kt @@ -11,6 +11,8 @@ package cz.vutbr.fit.interlockSim.sim import cz.vutbr.fit.interlockSim.context.SimulationContext import cz.vutbr.fit.interlockSim.context.SimulationContext.ReportType +import cz.vutbr.fit.interlockSim.exceptions.requireSimulation +import cz.vutbr.fit.interlockSim.exceptions.requireSimulationNotNull import cz.vutbr.fit.interlockSim.objects.cells.InOut import cz.vutbr.fit.interlockSim.objects.cells.RailSemaphore import cz.vutbr.fit.interlockSim.objects.cells.RailSemaphore.Signal @@ -87,7 +89,7 @@ class Train : final override fun actions() { var where: PathSeparator = timetable.getIn() - assert(where != null) + requireSimulationNotNull(where) { "PathSeparator from timetable.getIn() must not be null" } // out se muze rovnat in => bude vyreseno "prepojenim lokomotivy" while (true) { @@ -102,7 +104,9 @@ class Train : separatorAction(where, current, next) onNext = true - assert(position.isActive && pv.isActive) + requireSimulation(position.isActive && pv.isActive) { + "Position and velocity integration must be active" + } waitUntil { // dtmin - horni odhad zmeny pri poslednim kroku numericke metody behem dobrzdovani k uzlu position.state + dtMin >= nextLength @@ -111,7 +115,7 @@ class Train : position.state -= nextLength totalLenghtOfPreviousBlocks += nextLength where = next!!.getSecondEnd(where) - assert(where != null) + requireSimulationNotNull(where) { "PathSeparator from getSecondEnd() must not be null" } current = next onNext = false } @@ -173,8 +177,10 @@ class Train : next: TrackSection? ) { // isSeparatorInDirection accepts nullable Track parameters - assert(context.isSeparatorInDirection(separator as OrientedPathSeparator, next, current)) { semaphore } - assert(semaphore.getSignal() != null) + requireSimulation(context.isSeparatorInDirection(separator as OrientedPathSeparator, next, current)) { + "Separator must be in direction, semaphore: $semaphore" + } + requireSimulationNotNull(semaphore.getSignal()) { "Semaphore signal must not be null" } logger.info { "${jDisco.Process.time()} SENSOR: Train $number detected at semaphore " + "${if (semaphore.getName() != null) semaphore.getName() else semaphore.hashCode()}, " + @@ -185,7 +191,7 @@ class Train : // EXTENSION stanice if (semaphore.getSignal() == RailSemaphore.Signal.STOP) { - assert(getVelocity() >= 0) + requireSimulation(getVelocity() >= 0) { "Velocity must be non-negative when approaching semaphore" } logger.debug { "Train $number approaching semaphore with STOP signal, halting" } fireStop() context.report(semaphore.getSignal().toString(), this@Train, ReportType.TRAIN_EVENTS) @@ -247,7 +253,7 @@ class Train : } private fun fireStop() { - assert(getVelocity() >= 0) + requireSimulation(getVelocity() >= 0) { "Velocity must be non-negative when stopping" } front.stop() tail.stop() this@Train.stop() @@ -271,9 +277,9 @@ class Train : semaphore: RailSemaphore, path: Path? ) { - assert(path != null) + requireSimulationNotNull(path) { "Path must not be null in accelerate method" } val thisSignal: Signal = semaphore.getSignal() - assert(thisSignal.isAllowing()) { thisSignal } + requireSimulation(thisSignal.isAllowing()) { "Signal must be allowing: $thisSignal" } @Suppress("RECEIVER_NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS") val nextSemaphore: RailSemaphore = path!!.getLastPathSemaphore() val nextSignal: Signal = nextSemaphore.getSignal() @@ -313,7 +319,7 @@ class Train : val semaphore: RailSemaphore = where semaphoreAction(semaphore, semaphore, current, next) } else if (where == timetable.getIn() && next != null) { - assert(getAcceleration() != null) + requireSimulationNotNull(getAcceleration()) { "Acceleration must not be null at timetable entry" } semaphoreAction((where as InOut).getInSemaphore(), where, current, next) } else { @Suppress("RECEIVER_NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS") @@ -321,7 +327,9 @@ class Train : @Suppress("RECEIVER_NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS") pathToSemaphore?.removeFirst() } - assert(pathToSemaphore?.getFirst() == where) { pathToSemaphore ?: "null" } + requireSimulation(pathToSemaphore?.getFirst() == where) { + "Path to semaphore first element must match current position: ${pathToSemaphore ?: "null"}" + } if (next != null) next.enter(this@Train) } } @@ -358,7 +366,7 @@ class Train : Math.abs(front.getTotalDistance() - tail.getTotalDistance() - getLength()) <= maxAbsError override fun report(reportObj: StringBuilder): StringBuilder { - assert(reportObj != null) + requireSimulationNotNull(reportObj) { "Report object must not be null" } reportObj.append(front.getTotalDistance()).append(' ').append(tail.getTotalDistance()) return reportObj.append(' ').append(getLength()) } @@ -409,7 +417,7 @@ class Train : } override fun iteration() { - assert(currentCondition != null) + requireSimulationNotNull(currentCondition) { "Current condition must not be null during iteration" } accelerate = true logger.trace { "Train $number motor iteration: target speed $targetSpeed, " + @@ -433,7 +441,7 @@ class Train : speed: Double, test: AccelerationStopTest ) { - assert(speed >= 0) + requireSimulation(speed >= 0) { "Speed must be non-negative: $speed" } targetSpeed = speed currentCondition = AccelerationStopCondition(test) cancelAccelerating() @@ -469,7 +477,7 @@ class Train : } context.report("in on warning $normalSpeed", this@Train, ReportType._DEBUG) - assert(getVelocity() >= 0) + requireSimulation(getVelocity() >= 0) { "Velocity must be non-negative in onWarning" } privateAccelerateTo(normalSpeed, AccelerationStopTest.TO_HALF_SPEED) } @@ -526,20 +534,15 @@ class Train : * @param timetable */ constructor(context: SimulationContext?, timetable: Timetable?) { - if (context == null) { - throw NullPointerException("context must not be null") - } - if (timetable == null) { - throw NullPointerException("timetable must not be null") - } - this.context = context - this.timetable = timetable - this.length = timetable.getLength() + this.context = requireSimulationNotNull(context) { "context must not be null" } + val validatedTimetable = requireSimulationNotNull(timetable) { "timetable must not be null" } + this.timetable = validatedTimetable + this.length = validatedTimetable.getLength() number = ++count trainPrefix = "Train #$number" - logger.debug { - "Train $number created: from ${timetable.getIn().getName()} to ${timetable.getOut().getName()}, length $length" - } + val inName = validatedTimetable.getIn().getName() + val outName = validatedTimetable.getOut().getName() + logger.debug { "Train $number created: from $inName to $outName, length $length" } } override fun distanceToSemaphore(): Double = diff --git a/src/test/kotlin/cz/vutbr/fit/interlockSim/ExampleLoadingTest.kt b/src/test/kotlin/cz/vutbr/fit/interlockSim/ExampleLoadingTest.kt index 3aaaefc89..17369f52b 100644 --- a/src/test/kotlin/cz/vutbr/fit/interlockSim/ExampleLoadingTest.kt +++ b/src/test/kotlin/cz/vutbr/fit/interlockSim/ExampleLoadingTest.kt @@ -201,6 +201,7 @@ class ExampleLoadingTest { val unknownExampleName = "nonExistentExample" // Act & Assert + // TODO: Replace try-catch with assertFailure - https://github.com/bedaHovorka/interlockSim/issues/44 try { mainClass.getMethod(unknownExampleName, SimulationContextFactory::class.java, Array::class.java) throw AssertionError("Should have thrown NoSuchMethodException for unknown example") @@ -239,6 +240,7 @@ class ExampleLoadingTest { val insufficientArgs = arrayOf("example") // Missing end time // Act & Assert + // TODO: Replace try-catch with assertFailure - https://github.com/bedaHovorka/interlockSim/issues/45 try { val method = mainClass.getMethod("shuntingLoop", SimulationContextFactory::class.java, Array::class.java) method.invoke(main, factory, insufficientArgs) @@ -263,6 +265,7 @@ class ExampleLoadingTest { val invalidArgs = arrayOf("example", "shuntingLoop", "notANumber") // Act & Assert + // TODO: Replace try-catch with assertFailure - https://github.com/bedaHovorka/interlockSim/issues/46 try { val method = mainClass.getMethod("shuntingLoop", SimulationContextFactory::class.java, Array::class.java) method.invoke(main, factory, invalidArgs) @@ -320,6 +323,7 @@ class ExampleLoadingTest { val args = arrayOf("example") // Insufficient args will cause ContextCreationException // Act & Assert + // TODO: Replace try-catch with assertFailure - https://github.com/bedaHovorka/interlockSim/issues/47 try { val method = mainClass.getMethod("shuntingLoop", SimulationContextFactory::class.java, Array::class.java) method.invoke(main, factory, args) @@ -339,6 +343,7 @@ class ExampleLoadingTest { val mainClass = Main::class.java // Act & Assert + // TODO: Replace try-catch with assertFailure - https://github.com/bedaHovorka/interlockSim/issues/48 try { mainClass.getMethod("invalidExample", SimulationContextFactory::class.java, Array::class.java) throw AssertionError("Should have thrown NoSuchMethodException") diff --git a/src/test/kotlin/cz/vutbr/fit/interlockSim/RaceConditionTest.kt b/src/test/kotlin/cz/vutbr/fit/interlockSim/RaceConditionTest.kt index c467e6997..671da885a 100644 --- a/src/test/kotlin/cz/vutbr/fit/interlockSim/RaceConditionTest.kt +++ b/src/test/kotlin/cz/vutbr/fit/interlockSim/RaceConditionTest.kt @@ -17,7 +17,7 @@ import assertk.assertions.isNotNull import assertk.assertions.isTrue import cz.vutbr.fit.interlockSim.objects.cells.RailSwitch import cz.vutbr.fit.interlockSim.objects.tracks.SimpleTrackBlock -import cz.vutbr.fit.interlockSim.sim.TrackOperationException +import cz.vutbr.fit.interlockSim.exceptions.TrackOperationException import cz.vutbr.fit.interlockSim.testutil.MockNodeCell import cz.vutbr.fit.interlockSim.testutil.MockSimulationContext import cz.vutbr.fit.interlockSim.testutil.MockTrackOccupant @@ -449,6 +449,8 @@ class RaceConditionTest { successCount.incrementAndGet() } catch (e: AssertionError) { // Expected - only one should succeed in entering + } catch (e: cz.vutbr.fit.interlockSim.exceptions.SimulationException) { + // Expected - collision detection throws SimulationException } catch (e: Exception) { exceptions.add(e) } finally { diff --git a/src/test/kotlin/cz/vutbr/fit/interlockSim/context/ContextInitializationTest.kt b/src/test/kotlin/cz/vutbr/fit/interlockSim/context/ContextInitializationTest.kt index 889a43dc7..af68b2f3e 100644 --- a/src/test/kotlin/cz/vutbr/fit/interlockSim/context/ContextInitializationTest.kt +++ b/src/test/kotlin/cz/vutbr/fit/interlockSim/context/ContextInitializationTest.kt @@ -10,6 +10,7 @@ */ package cz.vutbr.fit.interlockSim.context +import assertk.assertFailure import assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.isGreaterThan @@ -209,17 +210,9 @@ class ContextInitializationTest { val xmlFile = File("nonexistent/path/does-not-exist.xml") // Act & Assert - val exception = - try { - XMLContextFactory.getInstance().createContext(xmlFile) - null - } catch (e: Exception) { - e - } - - assertThat(exception) - .withMessage("Should throw exception for nonexistent file") - .isNotNull() + assertk.assertFailure { + XMLContextFactory.getInstance().createContext(xmlFile) + }.isInstanceOf(Exception::class) } /** @@ -235,17 +228,9 @@ class ContextInitializationTest { val xmlFile = File("src/test/resources/cz/vutbr/fit/interlockSim/xml/fixtures/invalid-malformed-xml.xml") // Act & Assert - val exception = - try { - XMLContextFactory.getInstance().createContext(xmlFile) - null - } catch (e: Exception) { - e - } - - assertThat(exception) - .withMessage("Should throw exception for malformed XML") - .isNotNull() + assertk.assertFailure { + XMLContextFactory.getInstance().createContext(xmlFile) + }.isInstanceOf(Exception::class) } } diff --git a/src/test/kotlin/cz/vutbr/fit/interlockSim/objects/cells/CellTest.kt b/src/test/kotlin/cz/vutbr/fit/interlockSim/objects/cells/CellTest.kt index baffa3f62..0cbf91732 100644 --- a/src/test/kotlin/cz/vutbr/fit/interlockSim/objects/cells/CellTest.kt +++ b/src/test/kotlin/cz/vutbr/fit/interlockSim/objects/cells/CellTest.kt @@ -77,6 +77,8 @@ class CellTest { .withMessage("direction for class ${clazz.simpleName} and $t") .isSameInstanceAs(Segment.anti(sem2.direction())) } catch (e: IllegalArgumentException) { + // This try-catch is intentional - it handles valid cases where certain + // switch types are unsupported. This is not an assertion test. val message = e.message if (message != null && message == RailSwitch.UNSUPORTED_SWITCH_TYPES_MESSAGE) { continue diff --git a/src/test/kotlin/cz/vutbr/fit/interlockSim/objects/tracks/SimpleTrackEnterLeaveTest.kt b/src/test/kotlin/cz/vutbr/fit/interlockSim/objects/tracks/SimpleTrackEnterLeaveTest.kt index 69f1fe320..4d5f9ae9d 100644 --- a/src/test/kotlin/cz/vutbr/fit/interlockSim/objects/tracks/SimpleTrackEnterLeaveTest.kt +++ b/src/test/kotlin/cz/vutbr/fit/interlockSim/objects/tracks/SimpleTrackEnterLeaveTest.kt @@ -9,8 +9,10 @@ */ package cz.vutbr.fit.interlockSim.objects.tracks +import assertk.assertFailure import assertk.assertThat import assertk.assertions.isEqualTo +import assertk.assertions.isInstanceOf import assertk.assertions.isNotEqualTo import assertk.assertions.isSameInstanceAs import cz.vutbr.fit.interlockSim.testutil.MockNodeCell @@ -207,13 +209,9 @@ class SimpleTrackEnterLeaveTest { track.leave(train1) // Assert: Occupancy is cleared (getTrackOccupant fails on FREE track) - try { + assertk.assertFailure { track.getTrackOccupant() - throw AssertionError("Should have thrown AssertionError - track is FREE, not OCCUPIED") - } catch (e: AssertionError) { - if (e.message?.startsWith("Should have thrown") == true) throw e - // Expected - assertion failed as designed (occupant should be null when FREE) - } + }.isInstanceOf(cz.vutbr.fit.interlockSim.exceptions.SimulationException::class) } @Test @@ -312,15 +310,11 @@ class SimpleTrackEnterLeaveTest { .isEqualTo(TrackFacility.State.OCCUPIED) // Act & Assert: Attempt to enter with second train should fail - // SimpleTrack.enter() has assertion: assert(`in` == null) + // SimpleTrack.enter() throws exception: `in` == null // This is safety property SI-1 (collision prevention) - try { + assertk.assertFailure { track.enter(train2) - throw AssertionError("Should have thrown AssertionError - collision prevention failed") - } catch (e: AssertionError) { - if (e.message?.startsWith("Should have thrown") == true) throw e - // Expected - assertion failed as designed (prevent multiple occupants) - } + }.isInstanceOf(cz.vutbr.fit.interlockSim.exceptions.SimulationException::class) // Verify first train still occupies track assertThat(track.getTrackOccupant()) @@ -339,14 +333,10 @@ class SimpleTrackEnterLeaveTest { .isEqualTo(TrackFacility.State.OCCUPIED) // Act & Assert: Only the occupying train can leave - // SimpleTrack.leave() has assertion: assert(`in` === occupant) - try { + // SimpleTrack.leave() throws exception: `in` === occupant + assertk.assertFailure { track.leave(train2) // Wrong train attempts to leave - throw AssertionError("Should have thrown AssertionError - identity check failed") - } catch (e: AssertionError) { - if (e.message?.startsWith("Should have thrown") == true) throw e - // Expected - assertion failed as designed (only occupant can leave) - } + }.isInstanceOf(cz.vutbr.fit.interlockSim.exceptions.SimulationException::class) // Verify track still occupied with train1 assertThat(track.getState()) diff --git a/src/test/kotlin/cz/vutbr/fit/interlockSim/objects/tracks/SimpleTrackStateTest.kt b/src/test/kotlin/cz/vutbr/fit/interlockSim/objects/tracks/SimpleTrackStateTest.kt index ad03f0562..9b0fdb8ef 100644 --- a/src/test/kotlin/cz/vutbr/fit/interlockSim/objects/tracks/SimpleTrackStateTest.kt +++ b/src/test/kotlin/cz/vutbr/fit/interlockSim/objects/tracks/SimpleTrackStateTest.kt @@ -9,10 +9,12 @@ */ package cz.vutbr.fit.interlockSim.objects.tracks +import assertk.assertFailure import assertk.assertThat import assertk.assertions.isEqualTo +import assertk.assertions.isInstanceOf import assertk.assertions.isSameInstanceAs -import cz.vutbr.fit.interlockSim.sim.TrackOperationException +import cz.vutbr.fit.interlockSim.exceptions.TrackOperationException import cz.vutbr.fit.interlockSim.testutil.MockNodeCell import cz.vutbr.fit.interlockSim.testutil.MockTrackOccupant import org.junit.jupiter.api.BeforeEach @@ -152,12 +154,9 @@ class SimpleTrackStateTest { track.setUpPath(end1) // Act & Assert - should throw TrackOperationException - try { + assertk.assertFailure { track.setUpPath(end2) // Try to reserve from different end - throw AssertionError("Should have thrown TrackOperationException") - } catch (e: TrackOperationException) { - // Expected - } + }.isInstanceOf(TrackOperationException::class) } @Test @@ -168,12 +167,9 @@ class SimpleTrackStateTest { track.enter(mockOccupant) // Act & Assert - should throw TrackOperationException - try { + assertk.assertFailure { track.setUpPath(end2) // Try to reserve occupied track - throw AssertionError("Should have thrown TrackOperationException") - } catch (e: TrackOperationException) { - // Expected - } + }.isInstanceOf(TrackOperationException::class) } @Test @@ -182,14 +178,10 @@ class SimpleTrackStateTest { val track = SimpleTrackBlock(end1, end2, 100.0, 80.0) // Track is still in FREE state - never reserved - // Act & Assert - should throw AssertionError due to assert in enter() - try { + // Act & Assert - should throw SimulationException due to validation in enter() + assertk.assertFailure { track.enter(mockOccupant) // Try to enter without reservation - throw AssertionError("Should have thrown AssertionError") - } catch (e: AssertionError) { - if (e.message?.startsWith("Should have thrown") == true) throw e - // Expected - assertion failed as desired - } + }.isInstanceOf(cz.vutbr.fit.interlockSim.exceptions.SimulationException::class) } @Test @@ -198,14 +190,10 @@ class SimpleTrackStateTest { val track = SimpleTrackBlock(end1, end2, 100.0, 80.0) // Track is FREE - // Act & Assert - should throw AssertionError due to assert in leave() - try { + // Act & Assert - should throw SimulationException due to validation in leave() + assertk.assertFailure { track.leave(mockOccupant) // Try to leave when not occupied - throw AssertionError("Should have thrown AssertionError") - } catch (e: AssertionError) { - if (e.message?.startsWith("Should have thrown") == true) throw e - // Expected - assertion failed as desired - } + }.isInstanceOf(cz.vutbr.fit.interlockSim.exceptions.SimulationException::class) } } @@ -271,15 +259,11 @@ class SimpleTrackStateTest { val track = SimpleTrackBlock(end1, end2, 100.0, 80.0) // Act & Assert - Track is FREE, occupant should be null - // Note: getTrackOccupant() asserts that track must be OCCUPIED + // Note: getTrackOccupant() throws exception that track must be OCCUPIED // This test verifies that calling it on FREE track fails - try { + assertk.assertFailure { track.getTrackOccupant() - throw AssertionError("Should have thrown AssertionError") - } catch (e: AssertionError) { - if (e.message?.startsWith("Should have thrown") == true) throw e - // Expected - assertion failed as desired - } + }.isInstanceOf(cz.vutbr.fit.interlockSim.exceptions.SimulationException::class) } @Test diff --git a/src/test/kotlin/cz/vutbr/fit/interlockSim/sim/SimulationExceptionTest.kt b/src/test/kotlin/cz/vutbr/fit/interlockSim/sim/SimulationExceptionTest.kt index 1b7e21d9c..852d2adaf 100644 --- a/src/test/kotlin/cz/vutbr/fit/interlockSim/sim/SimulationExceptionTest.kt +++ b/src/test/kotlin/cz/vutbr/fit/interlockSim/sim/SimulationExceptionTest.kt @@ -16,6 +16,9 @@ import assertk.assertions.isEqualTo import assertk.assertions.isInstanceOf import assertk.assertions.isNotNull import assertk.assertions.startsWith +import cz.vutbr.fit.interlockSim.exceptions.PathSeparatorChangeException +import cz.vutbr.fit.interlockSim.exceptions.SimulationException +import cz.vutbr.fit.interlockSim.exceptions.TrackOperationException import cz.vutbr.fit.interlockSim.objects.paths.PathSeparator import cz.vutbr.fit.interlockSim.objects.tracks.SimpleTrack import cz.vutbr.fit.interlockSim.testutil.MockSimulationContext @@ -168,7 +171,9 @@ class SimulationExceptionTest { val result = exception.toString() // Assert - assertThat(result).startsWith("SimulationException:") + assertThat(result).startsWith("SimulationException[FATAL]:") + assertThat(result).contains(testMessage) + assertThat(result).contains("at time") } @Test @@ -708,8 +713,8 @@ class SimulationExceptionTest { val trackException: SimulationException = TrackOperationException(mockTrack) // Act & Assert - assertThat(simException.javaClass.simpleName).isEqualTo("SimulationException") - assertThat(trackException.javaClass.simpleName).isEqualTo("TrackOperationException") + assertThat(simException::class.java.simpleName).isEqualTo("SimulationException") + assertThat(trackException::class.java.simpleName).isEqualTo("TrackOperationException") } } } diff --git a/src/test/kotlin/cz/vutbr/fit/interlockSim/sim/TrainTest.kt b/src/test/kotlin/cz/vutbr/fit/interlockSim/sim/TrainTest.kt index 7fb523ba4..e4bb1f58b 100644 --- a/src/test/kotlin/cz/vutbr/fit/interlockSim/sim/TrainTest.kt +++ b/src/test/kotlin/cz/vutbr/fit/interlockSim/sim/TrainTest.kt @@ -68,14 +68,14 @@ class TrainTest { // Act & Assert assertThatThrownBy { Train(null as cz.vutbr.fit.interlockSim.context.SimulationContext?, timetable) } - .isInstanceOf(NullPointerException::class) + .isInstanceOf(cz.vutbr.fit.interlockSim.exceptions.SimulationException::class) } @Test fun constructor_nullTimetable_throwsException() { // Act & Assert assertThatThrownBy { Train(mockContext, null as Timetable?) } - .isInstanceOf(NullPointerException::class) + .isInstanceOf(cz.vutbr.fit.interlockSim.exceptions.SimulationException::class) } @Test