Skip to content

Commit 8b300b9

Browse files
bedaHovorkaclaude
andcommitted
Replace assert() with typed exception functions for better error handling
Converts assert() calls throughout the codebase to use typed exception functions (requireEditor, requireSimulation, requireValidArgument, requireValidState). This improves error messages by providing specific context and enables runtime validation even when assertions are disabled. Changes: - Replace assert() with requireEditor/requireSimulation in domain model and GUI - Add descriptive error messages for all validation failures - Improve debugging by identifying exact validation failure points - Standardize exception handling patterns across the codebase Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 376324c commit 8b300b9

21 files changed

Lines changed: 180 additions & 138 deletions

src/main/kotlin/cz/vutbr/fit/interlockSim/context/DefaultContext.kt

Lines changed: 42 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@ import cz.vutbr.fit.interlockSim.sim.InOutWorker
2828
import cz.vutbr.fit.interlockSim.sim.LoopProcess
2929
import cz.vutbr.fit.interlockSim.sim.ShuntingLoop
3030
import cz.vutbr.fit.interlockSim.exceptions.SimulationException
31+
import cz.vutbr.fit.interlockSim.exceptions.requireEditor
32+
import cz.vutbr.fit.interlockSim.exceptions.requireEditorNotNull
33+
import cz.vutbr.fit.interlockSim.exceptions.requireSimulation
34+
import cz.vutbr.fit.interlockSim.exceptions.requireSimulationNotNull
35+
import cz.vutbr.fit.interlockSim.exceptions.requireValidArgument
36+
import cz.vutbr.fit.interlockSim.exceptions.requireValidState
3137
import cz.vutbr.fit.interlockSim.objects.cells.anti
3238
import cz.vutbr.fit.interlockSim.objects.cells.conflict
3339
import cz.vutbr.fit.interlockSim.objects.cells.segmentFor
@@ -211,11 +217,11 @@ abstract class DefaultContext :
211217
val nodecell2: NodeCell = Util.assertNodeCell(getGrid().get(key2)!!)
212218

213219
for (s1: Segment in nodecell1.joins()) {
214-
assert(s1 != null) { nodecell1 }
220+
requireEditorNotNull(s1) { "Segment s1 cannot be null for nodecell $nodecell1" }
215221
val p1 = s1.transform(key1)
216222
if (used(p1)) continue
217223
for (s2: Segment in nodecell2.joins()) {
218-
assert(s2 != null) { nodecell2 }
224+
requireEditorNotNull(s2) { "Segment s2 cannot be null for nodecell $nodecell2" }
219225
if (s1 == s2) continue // stejne segmenty se nepropoji
220226
val p2 = s2.transform(key2)
221227
if (used(p2)) continue
@@ -250,7 +256,7 @@ abstract class DefaultContext :
250256
key2: Point,
251257
trackBlock: TrackBlock
252258
): Boolean {
253-
assert(key1 != null && key2 != null)
259+
requireEditor(key1 != null && key2 != null) { "Keys cannot be null in hardJoin" }
254260
if (key1.distance(key2) > SQRT2) {
255261
val blockEndFrom = s1.transform(key1)
256262
val blockEndTo = s2.transform(key2)
@@ -309,7 +315,9 @@ abstract class DefaultContext :
309315
@Suppress("UNCHECKED_CAST")
310316
val mapKeys: Set<Point> = mapToAdd.keySet() as Set<Point>
311317
linesKeys[trackBlock] = mapKeys
312-
assert(!extendedUnorientedGraph.contains(key1, key2))
318+
requireValidState(!extendedUnorientedGraph.contains(key1, key2)) {
319+
"Graph already contains edge between ($key1, $key2)"
320+
}
313321
extendedUnorientedGraph.put(key1, s1, key2, s2, trackBlock)
314322
return mapToAdd
315323
}
@@ -325,7 +333,9 @@ abstract class DefaultContext :
325333
key2: Point,
326334
trackBlock: TrackBlock
327335
): MutableMap<Point, TrackBlockPart>? {
328-
assert(bresenham != null && bresenham.isNotEmpty())
336+
requireValidArgument(bresenham != null && bresenham.isNotEmpty()) {
337+
"Bresenham path list cannot be null or empty"
338+
}
329339
val map: MutableMap<Point, TrackBlockPart> = LinkedHashMap()
330340
var from = key1
331341
var middle = bresenham[0]
@@ -358,7 +368,7 @@ abstract class DefaultContext :
358368
to: Point,
359369
block: TrackBlock
360370
): TrackBlockPart? {
361-
assert(block != null)
371+
requireEditorNotNull(block) { "TrackBlock cannot be null in createPart" }
362372
if (used(middle)) return null
363373
if (from == to || from == middle || middle == to) return null
364374

@@ -390,8 +400,12 @@ abstract class DefaultContext :
390400
p2: Point,
391401
points: MutableList<Point>
392402
): Boolean {
393-
assert(key1 != null && key2 != null && p1 != null && p2 != null)
394-
assert(key1 != p1 && key2 != p2 && key1 != p2 && key2 != p1)
403+
requireValidArgument(key1 != null && key2 != null && p1 != null && p2 != null) {
404+
"All points must be non-null in bresenham algorithm"
405+
}
406+
requireValidArgument(key1 != p1 && key2 != p2 && key1 != p2 && key2 != p1) {
407+
"Keys and intermediate points must be distinct in bresenham algorithm"
408+
}
395409

396410
// Make mutable copies since we need to modify them for the algorithm
397411
var p1Mut = p1
@@ -508,7 +522,7 @@ abstract class DefaultContext :
508522
key: Point,
509523
nodeCell: NodeCell
510524
) {
511-
assert(key != null)
525+
requireEditorNotNull(key) { "Cell key cannot be null in putCell" }
512526
// Validate coordinates are within grid bounds
513527
if (key.x < 0 || key.y < 0 || key.x >= railwayNetGrid.getCols() || key.y >= railwayNetGrid.getRows()) {
514528
throw ContextCreationException(
@@ -520,7 +534,7 @@ abstract class DefaultContext :
520534

521535
// vedlejsi Nody (sousedni bunky)
522536
for (s1: Segment in nodeCell.joins()) {
523-
assert(s1 != null) { nodeCell }
537+
requireEditorNotNull(s1) { "Segment s1 cannot be null for nodeCell $nodeCell" }
524538
val p = s1.transform(key)
525539
// Skip neighbor if it's outside grid bounds (boundary cells)
526540
if (p.x < 0 || p.y < 0 || p.x >= railwayNetGrid.getCols() || p.y >= railwayNetGrid.getRows()) {
@@ -533,7 +547,9 @@ abstract class DefaultContext :
533547
// vzit proti-segment
534548
val s2 = anti(s1)
535549
if (nodeCell2.joins().contains(s2)) {
536-
assert(s2.transform(p) == key)
550+
requireValidState(s2.transform(p) == key) {
551+
"Segment transformation inconsistency: s2.transform($p) != $key"
552+
}
537553
extendedUnorientedGraph.putIfNotExists(
538554
key,
539555
s1,
@@ -577,7 +593,7 @@ abstract class DefaultContext :
577593
* Remove a track line from the railway network
578594
*/
579595
override fun removeLine(line: TrackBlock) {
580-
assert(line != null)
596+
requireEditorNotNull(line) { "TrackBlock line cannot be null in removeLine" }
581597
extendedUnorientedGraph.remove(line)
582598
getGrid().keySet().removeAll(linesKeys.remove(line) ?: emptySet())
583599
changeSupport.firePropertyChange(
@@ -614,9 +630,11 @@ abstract class DefaultContext :
614630
): Segment? {
615631
// If track is not null, use it; otherwise use secondEndTrack
616632
if (track != null) return getSegment(separator, track)
617-
assert(separator != null)
618-
assert(secondEndTrack != null) { separator }
619-
assert(separator is OrientedPathSeparator) // Util
633+
requireValidArgument(separator != null) { "PathSeparator cannot be null" }
634+
requireValidArgument(secondEndTrack != null) { "secondEndTrack cannot be null for separator $separator" }
635+
requireValidArgument(separator is OrientedPathSeparator) {
636+
"PathSeparator must be OrientedPathSeparator, got ${separator.javaClass.simpleName}"
637+
}
620638
val segment = getSegment(separator, secondEndTrack!!)
621639
// Match Java 1:1: return null when segment doesn't exist
622640
return separator.getFollowingSegment(segment)
@@ -682,7 +700,9 @@ abstract class DefaultContext :
682700
current: TrackBlock?
683701
): Segment? {
684702
if (current != null) {
685-
assert(getGraph().get(location).contains(current)) { current }
703+
requireValidState(getGraph().get(location).contains(current)) {
704+
"Current track block $current not found in graph at location $location"
705+
}
686706
}
687707
return if (current == null) null else getGraph().extensionalObject(location, current)
688708
}
@@ -692,7 +712,7 @@ abstract class DefaultContext :
692712
*/
693713
private fun getLocation(node: NodeCell): Point {
694714
val location = getRailWayNetGrid().getLocation(node)
695-
assert(location != null) { this }
715+
requireValidState(location != null) { "Location not found for nodeCell $node in grid" }
696716
return location!!
697717
}
698718

@@ -724,7 +744,7 @@ abstract class DefaultContext :
724744
var trackBlock: TrackBlock? = null
725745
if (current != null) {
726746
trackBlock = current.getTrackBlock()
727-
assert(trackBlock != null)
747+
requireValidState(trackBlock != null) { "TrackBlock cannot be null for current track section" }
728748
val nextTrackSection = trackBlock?.getNextTrackSection(separator, current)
729749
if (nextTrackSection != null) {
730750
if (logger.isTraceEnabled()) {
@@ -785,7 +805,7 @@ abstract class DefaultContext :
785805
* Stop the simulation
786806
*/
787807
override fun stop() {
788-
assert(mainProcess != null)
808+
requireSimulationNotNull(mainProcess) { "Main process must be initialized before stopping simulation" }
789809
for (worker in workers.values) {
790810
worker.terminate()
791811
}
@@ -819,12 +839,12 @@ abstract class DefaultContext :
819839
next: Track?,
820840
previous: Track?
821841
): Boolean {
822-
assert(separator != null)
842+
requireSimulation(separator != null) { "Separator cannot be null in isSeparatorInDirection" }
823843
val segment = getSegment(separator, next, previous)
824844
if (segment == null && separator is InOut) return true
825-
assert(segment != null) { separator }
845+
requireSimulation(segment != null) { "Segment cannot be null for separator $separator" }
826846
val direction = separator.direction()
827-
assert(direction != null)
847+
requireSimulation(direction != null) { "Direction cannot be null for oriented separator" }
828848
val inDirection = segment === direction
829849
if (logger.isDebugEnabled()) {
830850
logger.debug(

src/main/kotlin/cz/vutbr/fit/interlockSim/context/DefaultRailWayNetGrid.kt

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
*/
1010
package cz.vutbr.fit.interlockSim.context
1111

12+
import cz.vutbr.fit.interlockSim.exceptions.requireValidState
1213
import cz.vutbr.fit.interlockSim.objects.cells.Cell
1314
import cz.vutbr.fit.interlockSim.objects.cells.TrackBlockPart
1415
import cz.vutbr.fit.interlockSim.util.Point
@@ -131,10 +132,14 @@ class DefaultRailWayNetGrid(
131132
@Synchronized
132133
fun containsKey(newPoint: Point): Boolean {
133134
if (getCells().containsKey(newPoint)) {
134-
assert(getReverseTable().containsValue(newPoint))
135+
requireValidState(getReverseTable().containsValue(newPoint)) {
136+
"Inconsistent grid state: point $newPoint in cells but not in reverse table"
137+
}
135138
return true
136139
}
137-
assert(!getReverseTable().containsValue(newPoint)) { newPoint }
140+
requireValidState(!getReverseTable().containsValue(newPoint)) {
141+
"Inconsistent grid state: point $newPoint in reverse table but not in cells: $newPoint"
142+
}
138143
return false
139144
}
140145

@@ -144,17 +149,21 @@ class DefaultRailWayNetGrid(
144149
*/
145150
@Synchronized
146151
fun remove(key: Point) {
147-
assert(key != null)
152+
requireValidState(key != null) { "Key cannot be null" }
148153
val removed: Cell? = getCells().remove(key)
149154
val remove2: Point? = getReverseTable().remove(removed)
150-
assert(key == remove2)
155+
requireValidState(key == remove2) {
156+
"Inconsistent grid state: expected key $key but got $remove2 from reverse table"
157+
}
151158
}
152159

153160
/**
154161
* @return true if grid is empty
155162
*/
156163
fun isEmpty(): Boolean {
157-
assert(getReverseTable().isEmpty() == getCells().isEmpty())
164+
requireValidState(getReverseTable().isEmpty() == getCells().isEmpty()) {
165+
"Inconsistent grid state: reverse table and cells have different empty states"
166+
}
158167
return getCells().isEmpty()
159168
}
160169

src/main/kotlin/cz/vutbr/fit/interlockSim/gui/RailwayNetGridCanvas.kt

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ import cz.vutbr.fit.interlockSim.context.Context
1313
import cz.vutbr.fit.interlockSim.context.EditingContext
1414
import cz.vutbr.fit.interlockSim.context.EditingContextFactory
1515
import cz.vutbr.fit.interlockSim.context.SimulationContext
16+
import cz.vutbr.fit.interlockSim.exceptions.requireEditor
17+
import cz.vutbr.fit.interlockSim.exceptions.requireEditorNotNull
18+
import cz.vutbr.fit.interlockSim.exceptions.requireValidState
1619
import cz.vutbr.fit.interlockSim.gui.gridcanvas.CellRenderer
1720
import cz.vutbr.fit.interlockSim.gui.gridcanvas.EditorCellRenderer
1821
import cz.vutbr.fit.interlockSim.gui.gridcanvas.GridCanvasEditingPopupMenu
@@ -75,7 +78,7 @@ class RailwayNetGridCanvas :
7578
MouseEvent.BUTTON1 -> leftMouseClicked(e)
7679
MouseEvent.BUTTON2 -> middleMouseClicked(e)
7780
MouseEvent.BUTTON3 -> rightMouseClicked(e)
78-
else -> assert(false) { "Unknown mouse button: ${e.button}" }
81+
else -> error("Unknown mouse button: ${e.button}")
7982
}
8083
}
8184

@@ -126,8 +129,7 @@ class RailwayNetGridCanvas :
126129
// Clear selection after creating a cell to prevent auto-joining
127130
selectedKey = null
128131
} catch (e1: Exception) {
129-
assert(false) { "Failed to create cell: $e1" }
130-
e1.printStackTrace()
132+
error("Failed to create cell: $e1")
131133
}
132134
}
133135
selectedKey != null -> {
@@ -206,7 +208,7 @@ class RailwayNetGridCanvas :
206208
state = State.SIMULATION
207209
changeListeners(editListener, simulationControlListener)
208210
}
209-
else -> assert(false) { "Unknown context type: ${newContext.javaClass}" }
211+
else -> error("Unknown context type: ${newContext.javaClass}")
210212
}
211213
changeContext(newContext)
212214
}
@@ -232,7 +234,7 @@ class RailwayNetGridCanvas :
232234

233235
// Update context and recalculate display
234236
private fun changeContext(cont: Context) {
235-
assert(cont != null) { "Context cannot be null" }
237+
requireEditorNotNull(cont) { "Context cannot be null" }
236238
if (context != null) {
237239
context!!.removePropertyChangeListener(this)
238240
}
@@ -246,7 +248,7 @@ class RailwayNetGridCanvas :
246248

247249
// Painting methods
248250
override fun paintComponent(g: Graphics) {
249-
assert(g is Graphics2D) { "Graphics context must be Graphics2D" }
251+
requireEditor(g is Graphics2D) { "Graphics context must be Graphics2D" }
250252
paint(g as Graphics2D)
251253
}
252254

@@ -261,7 +263,7 @@ class RailwayNetGridCanvas :
261263
for (entry in grid) {
262264
val key = entry.key
263265
val cell = entry.value
264-
assert(key != null && cell != null) { "Grid entry has null key or value: ($key, $cell)" }
266+
requireValidState(key != null && cell != null) { "Grid entry has null key or value: ($key, $cell)" }
265267

266268
val x = key.x * CELL_WIDTH
267269
val y = key.y * CELL_HEIGHT

src/main/kotlin/cz/vutbr/fit/interlockSim/gui/StatusBar.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ package cz.vutbr.fit.interlockSim.gui
1111

1212
import cz.vutbr.fit.interlockSim.PROGRAM_NAME
1313
import cz.vutbr.fit.interlockSim.context.ContextChangeListener
14+
import cz.vutbr.fit.interlockSim.exceptions.requireValidState
1415
import java.awt.Component
1516
import java.awt.Dimension
1617
import java.awt.event.MouseEvent
@@ -32,7 +33,7 @@ class StatusBar :
3233

3334
override fun mouseMoved(e: MouseEvent) {
3435
val source = e.source
35-
assert(source is StatusProducer) { "Source must be a StatusProducer" }
36+
requireValidState(source is StatusProducer) { "Source must be a StatusProducer" }
3637
val status = (source as StatusProducer).getStatus(e)
3738
if (status != null) {
3839
text = status
@@ -46,7 +47,7 @@ class StatusBar :
4647
}
4748

4849
private fun checkComponent(producer: StatusProducer): Component {
49-
assert(producer is Component) { "StatusProducer must be a Component" }
50+
requireValidState(producer is Component) { "StatusProducer must be a Component" }
5051
return producer as Component
5152
}
5253

src/main/kotlin/cz/vutbr/fit/interlockSim/gui/action/NodeCellAction.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,7 @@ class NodeCellAction(
5959
val f = getKoin().get<EditingContextFactory>()
6060
cell = f.createNew(context, cellClass, *args) as Cell
6161
} catch (e: Exception) {
62-
assert(false) { e }
63-
e.printStackTrace()
62+
error("Failed to create cell icon: $e")
6463
}
6564
val img = BufferedImage(iconSize, iconSize, BufferedImage.TYPE_INT_RGB)
6665
val g = img.createGraphics()

src/main/kotlin/cz/vutbr/fit/interlockSim/gui/gridcanvas/CellRenderer.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
*/
1010
package cz.vutbr.fit.interlockSim.gui.gridcanvas
1111

12+
import cz.vutbr.fit.interlockSim.exceptions.requireValidState
1213
import cz.vutbr.fit.interlockSim.objects.cells.Cell
1314
import cz.vutbr.fit.interlockSim.objects.cells.Cell.Segment
1415
import cz.vutbr.fit.interlockSim.objects.cells.InOut
@@ -94,7 +95,7 @@ abstract class CellRenderer(
9495
cell: OrientedPathSeparator
9596
) {
9697
val thetaObj = ANGLES[cell.getSpatialType()]
97-
assert(thetaObj != null) { "No angle defined for spatial type: ${cell.getSpatialType()}" }
98+
requireValidState(thetaObj != null) { "No angle defined for spatial type: ${cell.getSpatialType()}" }
9899

99100
val theta = if (cell.getOrientation()) thetaObj!! + Math.PI else thetaObj!!
100101
val xs = intArrayOf(cellWidth / 4, cellWidth / 4, 3 * cellWidth / 4)
@@ -112,14 +113,14 @@ abstract class CellRenderer(
112113
g: Graphics2D,
113114
cell: Cell
114115
) {
115-
assert(cell != null) { "Cell cannot be null" }
116+
requireValidState(cell != null) { "Cell cannot be null" }
116117

117118
try {
118119
// Use reflection to find and invoke the specific draw method for this cell type
119120
val method = javaClass.getMethod("draw", Graphics2D::class.java, cell.javaClass)
120121
method.invoke(this, g, cell)
121122
} catch (e: Exception) {
122-
assert(false) { "Failed to draw cell: $e" }
123+
error("Failed to draw cell: $e")
123124
}
124125
}
125126

0 commit comments

Comments
 (0)