diff --git a/CLAUDE.md b/CLAUDE.md
index ac757a54..9abb2c5c 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -176,15 +176,15 @@ For complete navigation services architecture, Koin DI integration patterns, and
### InOut Elements
-**Minimum Requirement:** Every railway network must have at least 2 InOut elements (entry/exit points).
+**Minimum Requirement:** Every railway network must have at least 1 InOut element (entry/exit point).
**Rationale:**
-- Single InOut = dead-end (train enters but cannot exit)
-- Simulation requires trains to enter from one point and exit from another
+- With bidirectional train operation (PR #356), a single InOut can serve as both entry and exit
+- Train can enter, travel through the network, reverse direction, and exit through the same InOut
- XML validation enforces this constraint via XMLContextFactory
**Validation:**
-- Editor: GUI prevents saving contexts with < 2 InOuts (Issue #80)
+- Editor: GUI prevents saving contexts with < 1 InOuts (Issue #80)
- XML loading: XMLContextFactory validates during parse
- Test coverage: See InOutValidationTest (Issue #79)
diff --git a/docs/KOTLIN_STYLE_GUIDE.md b/docs/KOTLIN_STYLE_GUIDE.md
index b81dbbb4..5eb4f29a 100644
--- a/docs/KOTLIN_STYLE_GUIDE.md
+++ b/docs/KOTLIN_STYLE_GUIDE.md
@@ -2058,9 +2058,10 @@ dynamicInOut.lastTrain = train // Mutable operation (OK in simulation)
**Railway Domain Context:**
- **InOut elements** represent network boundaries (train spawn/despawn points)
-- **Minimum 2 InOuts required** per network (validated by XMLContextFactory)
+- **Minimum 1 InOut required** per network (validated by XMLContextFactory)
- **Entry points** (`isEntry == true`) - trains enter network here
- **Exit points** (`isEntry == false`) - trains leave network here
+- With bidirectional operation, a single InOut can serve as both entry and exit
**Why This Pattern:**
1. **Immutable editing context** - No simulation state during network design
@@ -2217,24 +2218,23 @@ class DefaultSimulationContext(
### Minimum InOut Requirement
-**Requirement:** Every railway network must have at least 2 InOut elements (entry/exit points).
+**Requirement:** Every railway network must have at least 1 InOut element (entry/exit point).
**Rationale:**
-- Single InOut = dead-end (train enters but cannot exit)
-- Simulation requires trains to enter from one point and exit from another
-- Networks with < 2 InOuts are topologically invalid for train simulation
+- With bidirectional train operation (PR #356), a single InOut can serve as both entry and exit
+- Train can enter, travel through the network, reverse direction, and exit through the same InOut
+- Networks with < 1 InOuts are invalid (no way for trains to enter/exit)
**Validation:**
-- **Editor**: GUI prevents saving contexts with < 2 InOuts (Issue #80)
+- **Editor**: GUI prevents saving contexts with < 1 InOuts (Issue #80)
- **XML loading**: XMLContextFactory validates during parse and throws `IllegalArgumentException`
- **Test coverage**: See `InOutValidationTest` (Issue #79)
-**Example Valid Network:**
+**Example Valid Network (single InOut):**
```xml
-
-
-
+
+
```
diff --git a/docs/STATIC_DYNAMIC_SEPARATION_ARCHITECTURE.md b/docs/STATIC_DYNAMIC_SEPARATION_ARCHITECTURE.md
index 93683a16..334e4695 100644
--- a/docs/STATIC_DYNAMIC_SEPARATION_ARCHITECTURE.md
+++ b/docs/STATIC_DYNAMIC_SEPARATION_ARCHITECTURE.md
@@ -321,7 +321,7 @@ dynamicInOut.lastTrain = train // Mutable operation (simulation state)
**Railway Domain Context:**
- **InOut elements** represent entry/exit points for trains (network boundaries)
-- **Minimum 2 InOuts required** per network (at least one entry, one exit)
+- **Minimum 1 InOut required** per network (with bidirectional operation, can serve as both entry and exit)
- **Entry points** (`isEntry == true`) - trains spawn here
- **Exit points** (`isEntry == false`) - trains despawn here
- **Examples:** "A" (entry), "B" (exit) in vyhybna.xml; "N-Lib-1" (north entry), "S-Vrs-2" (south exit) in praha-hlavni-nadrazi.xml
diff --git a/src/main/kotlin/cz/vutbr/fit/interlockSim/gui/ValidationUtils.kt b/src/main/kotlin/cz/vutbr/fit/interlockSim/gui/ValidationUtils.kt
index 376770f1..336d75e2 100644
--- a/src/main/kotlin/cz/vutbr/fit/interlockSim/gui/ValidationUtils.kt
+++ b/src/main/kotlin/cz/vutbr/fit/interlockSim/gui/ValidationUtils.kt
@@ -10,6 +10,7 @@
package cz.vutbr.fit.interlockSim.gui
import cz.vutbr.fit.interlockSim.context.ContextCreationException
+import cz.vutbr.fit.interlockSim.xml.XMLContextFactory.Companion.MIN_INOUT_ELEMENTS
import org.xml.sax.SAXException
import org.xml.sax.SAXParseException
import java.io.FileNotFoundException
@@ -109,15 +110,14 @@ object ValidationUtils {
*/
private fun parseErrorMessage(message: String): String =
when {
- message.contains("InOut") && message.contains("at least 2") -> {
+ message.contains("InOut") && message.contains("at least $MIN_INOUT_ELEMENTS") -> {
"""
- |Minimum 2 InOut elements required (found fewer).
+ |Minimum $MIN_INOUT_ELEMENTS InOut element required (found none).
|
- |InOut elements define entry/exit points for trains. At least 2 are required for simulation:
- |- One for trains to enter the network
- |- One for trains to exit the network
+ |InOut elements define entry/exit points for trains. At least $MIN_INOUT_ELEMENTS is required for simulation.
+ |With bidirectional train operation, a single InOut can serve as both entry and exit point.
|
- |Please add more InOut elements to your railway network.
+ |Please add an InOut element to your railway network.
""".trimMargin()
}
diff --git a/src/main/kotlin/cz/vutbr/fit/interlockSim/xml/XMLContextFactory.kt b/src/main/kotlin/cz/vutbr/fit/interlockSim/xml/XMLContextFactory.kt
index c17eb9cb..a94048ce 100644
--- a/src/main/kotlin/cz/vutbr/fit/interlockSim/xml/XMLContextFactory.kt
+++ b/src/main/kotlin/cz/vutbr/fit/interlockSim/xml/XMLContextFactory.kt
@@ -293,9 +293,9 @@ class XMLContextFactory : EditingContextFactory {
* Called when XML parsing completes. Validates the parsed railway network structure.
*
* **Validation Rules:**
- * - Minimum [MIN_INOUT_ELEMENTS] InOut elements required (entry and exit points)
+ * - Minimum [MIN_INOUT_ELEMENTS] InOut element required (entry/exit point)
* - InOut elements define where trains enter/exit the railway network
- * - Single InOut networks are invalid (dead-end, train cannot exit)
+ * - With bidirectional operation, a single InOut can serve as both entry and exit
* - Context must be initialized (non-null editingContext)
*
* This method is called automatically by the SAX parser after all XML elements
@@ -305,15 +305,14 @@ class XMLContextFactory : EditingContextFactory {
* ```xml
*
*
- *
- *
+ *
*
* ```
*
* **Example Invalid Network (throws exception):**
* ```xml
*
- *
+ *
*
* ```
*
@@ -325,9 +324,9 @@ class XMLContextFactory : EditingContextFactory {
* @see MIN_INOUT_ELEMENTS for validation threshold
*/
override fun endDocument() {
- // Strict validation: Railway networks must have at least 2 InOut elements (entry/exit points)
+ // Strict validation: Railway networks must have at least 1 InOut element (entry/exit point)
val ctx = editingContext ?: throw SAXException("Context not initialized")
-
+
// Only validate InOut count if not skipping structural validation
if (!skipStructuralValidation) {
// Access inouts via public method from BaseContext
@@ -430,13 +429,14 @@ class XMLContextFactory : EditingContextFactory {
/**
* Minimum number of InOut elements required in a railway network.
*
- * Railway networks must have at least 2 InOut elements (entry and exit points)
- * to allow trains to enter and exit the simulation. Single InOut networks
- * are invalid (dead-end configuration).
+ * Railway networks must have at least 1 InOut element (entry/exit point).
+ * With bidirectional train operation (Issue #356), a single InOut can serve
+ * as both entry and exit point.
*
* @since 2026-01 (Issue #76 validation, Issue #77 code quality)
+ * @since 2026-02 (Issue #341, PR #356 - reduced from 2 to 1 for bidirectional support)
*/
- private const val MIN_INOUT_ELEMENTS = 2
+ const val MIN_INOUT_ELEMENTS = 1
}
override fun createEmptyContext(): EditingContext = DefaultEditingContext(DEFAULT_GRID_SIZE, DEFAULT_GRID_SIZE)
@@ -445,16 +445,16 @@ class XMLContextFactory : EditingContextFactory {
* Creates an EditingContext by parsing an XML file conforming to data.xsd schema.
*
* **Validation Requirements:**
- * - Minimum 2 InOut elements required (entry and exit points)
+ * - Minimum 1 InOut element required (entry/exit point)
* - InOut elements define where trains enter/exit the railway network
- * - Single InOut networks are invalid (dead-end, train cannot exit)
+ * - With bidirectional operation, a single InOut can serve as both entry and exit
*
* @param file XML file containing railway network definition
* @return Parsed EditingContext with validated network structure
* @throws ContextCreationException if:
* - File not found
* - XML validation fails against schema
- * - InOut count < 2 (minimum requirement)
+ * - InOut count < 1 (minimum requirement)
* - Network structure is invalid
*/
@Throws(ContextCreationException::class)
@@ -493,15 +493,15 @@ class XMLContextFactory : EditingContextFactory {
* Creates an EditingContext by parsing an XML stream conforming to data.xsd schema.
*
* **Validation Requirements:**
- * - Minimum 2 InOut elements required (entry and exit points)
+ * - Minimum 1 InOut element required (entry/exit point)
* - InOut elements define where trains enter/exit the railway network
- * - Single InOut networks are invalid (dead-end, train cannot exit)
+ * - With bidirectional operation, a single InOut can serve as both entry and exit
*
* @param stream InputStream containing XML railway network definition
* @return Parsed EditingContext with validated network structure
* @throws ContextCreationException if:
* - XML validation fails against schema
- * - InOut count < 2 (minimum requirement)
+ * - InOut count < 1 (minimum requirement)
* - Network structure is invalid
*/
@Throws(ContextCreationException::class)
@@ -573,10 +573,10 @@ class XMLContextFactory : EditingContextFactory {
try {
val inputSource = InputSource(java.io.StringReader(xmlContent))
val handler = Handler(skipStructuralValidation = false)
-
+
// Try to validate with schema - this will throw on both parse and validation errors
validator.validate(SAXSource(inputSource), SAXResult(handler))
-
+
val context = handler.getContext()
if (context == null) {
return LenientParseResult(
@@ -604,22 +604,22 @@ class XMLContextFactory : EditingContextFactory {
// SAXException (not SAXParseException) = validation error
// Could be schema validation or structural validation (e.g., InOut count < 2)
// Try parsing without validation to see if we can get a context
-
+
val validationError = ContextCreationException(e)
-
+
// Second attempt: Parse without schema validation but WITH structural validation disabled
return try {
val inputSource = InputSource(java.io.StringReader(xmlContent))
val handler = Handler(skipStructuralValidation = true)
-
+
// Parse without schema validation - use SAX parser directly
val saxParserFactory = javax.xml.parsers.SAXParserFactory.newInstance()
saxParserFactory.isNamespaceAware = true // Keep namespace awareness
val saxParser = saxParserFactory.newSAXParser()
saxParser.parse(inputSource, handler)
-
+
val context = handler.getContext()
-
+
if (context != null) {
// Successfully parsed without validation - it's parseable with errors
LenientParseResult(
diff --git a/src/test/kotlin/cz/vutbr/fit/interlockSim/InvalidNetworkTest.kt b/src/test/kotlin/cz/vutbr/fit/interlockSim/InvalidNetworkTest.kt
index e80c580f..6756f89e 100644
--- a/src/test/kotlin/cz/vutbr/fit/interlockSim/InvalidNetworkTest.kt
+++ b/src/test/kotlin/cz/vutbr/fit/interlockSim/InvalidNetworkTest.kt
@@ -55,7 +55,7 @@ class InvalidNetworkTest : KoinTestBase() {
inner class MissingElementsTests {
/**
* Test: Network with no InOut elements must be rejected (strict validation)
- * Rationale: Railway networks require at least 2 InOut elements (entry and exit points)
+ * Rationale: Railway networks require at least 1 InOut element (entry/exit point)
*/
@Test
fun createContext_noInOutElements_throwsException() {
@@ -71,11 +71,11 @@ class InvalidNetworkTest : KoinTestBase() {
}
/**
- * Test: Network with single InOut must be rejected (strict validation)
- * Rationale: Railway networks require at least 2 InOut elements (entry and exit points)
+ * Test: Network with single InOut is now valid (with bidirectional operation)
+ * Rationale: With bidirectional train operation, a single InOut can serve as both entry and exit
*/
@Test
- fun createContext_singleInOutNoTracks_throwsException() {
+ fun createContext_singleInOutNoTracks_isValid() {
val networkXML = """
@@ -83,9 +83,10 @@ class InvalidNetworkTest : KoinTestBase() {
"""
val stream = ByteArrayInputStream(networkXML.toByteArray())
- // Strict validation: single InOut is insufficient
- assertThatBlock { editingContextFactory.createContext(stream) }
- .isFailure()
+ // With bidirectional operation, single InOut is now valid
+ val context = editingContextFactory.createContext(stream)
+ assertThat(context).isNotNull()
+ context.close()
}
/**
diff --git a/src/test/kotlin/cz/vutbr/fit/interlockSim/context/InOutValidationTest.kt b/src/test/kotlin/cz/vutbr/fit/interlockSim/context/InOutValidationTest.kt
index a15ce306..23923019 100644
--- a/src/test/kotlin/cz/vutbr/fit/interlockSim/context/InOutValidationTest.kt
+++ b/src/test/kotlin/cz/vutbr/fit/interlockSim/context/InOutValidationTest.kt
@@ -22,6 +22,7 @@ import assertk.assertions.isEqualTo
import assertk.assertions.isInstanceOf
import cz.vutbr.fit.interlockSim.testutil.TestContextBuilder
import cz.vutbr.fit.interlockSim.testutil.TestFixtures
+import cz.vutbr.fit.interlockSim.xml.XMLContextFactory
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
@@ -33,10 +34,10 @@ import org.koin.java.KoinJavaComponent.getKoin
* Test suite for InOut validation rules.
*
* **Requirements:**
- * - Minimum 2 InOut elements required (entry and exit points)
- * - Single InOut networks are invalid (dead-end)
+ * - Minimum 1 InOut element required (entry/exit point)
* - Networks with 0 InOuts are invalid
- * - Networks with 2+ InOuts are valid
+ * - Networks with 1+ InOuts are valid
+ * - With bidirectional operation, a single InOut can serve as both entry and exit
*
* **Coverage:**
* - XMLContextFactory validation during XML parsing
@@ -65,33 +66,33 @@ class InOutValidationTest {
/**
* Test: Context with 0 InOuts throws exception during XML loading.
*
- * Expected: ContextCreationException with message about minimum 2 InOuts.
+ * Expected: ContextCreationException with message about minimum 1 InOut.
*/
@Test
fun `XML with 0 InOuts throws ContextCreationException`() {
+ val min = XMLContextFactory.Companion.MIN_INOUT_ELEMENTS
assertFailure {
TestFixtures.loadInvalidInOutXml("zero-inouts.xml").use { stream ->
xmlFactory.createContext(stream)
}
}.isInstanceOf(ContextCreationException::class)
.transform { it.message ?: "" }
- .contains("at least 2 InOut")
+ .contains("Railway network must have at least $min InOut elements (entry and exit points).")
}
/**
- * Test: Context with 1 InOut throws exception during XML loading.
+ * Test: Context with 1 InOut passes validation.
*
- * Expected: ContextCreationException with message about minimum 2 InOuts.
+ * With bidirectional train operation, a single InOut is now valid.
+ * Expected: Context created successfully with 1 InOut.
*/
@Test
- fun `XML with 1 InOut throws ContextCreationException`() {
- assertFailure {
- TestFixtures.loadInvalidInOutXml("single-inout.xml").use { stream ->
- xmlFactory.createContext(stream)
+ fun `XML with 1 InOut passes validation`() {
+ TestFixtures.loadInvalidInOutXml("single-inout.xml").use { stream ->
+ xmlFactory.createContext(stream).use { context ->
+ assertThat(context.asEditingContext().getInOutsList()).hasSize(1)
}
- }.isInstanceOf(ContextCreationException::class)
- .transform { it.message ?: "" }
- .contains("at least 2 InOut")
+ }
}
/**
@@ -179,17 +180,17 @@ class InOutValidationTest {
/**
* Test: Error message contains helpful explanation.
*
- * Expected: Exception message explains why 2 InOuts are required.
+ * Expected: Exception message explains why 1 InOut is required.
*/
@Test
fun `Error message explains InOut requirement clearly`() {
assertFailure {
- TestFixtures.loadInvalidInOutXml("single-inout.xml").use { stream ->
+ TestFixtures.loadInvalidInOutXml("zero-inouts.xml").use { stream ->
xmlFactory.createContext(stream)
}
}.isInstanceOf(ContextCreationException::class)
.transform { it.message ?: "" }
- .contains("entry and exit points")
+ .contains("entry/exit point")
}
/**
diff --git a/src/test/kotlin/cz/vutbr/fit/interlockSim/gui/MenuBarTest.kt b/src/test/kotlin/cz/vutbr/fit/interlockSim/gui/MenuBarTest.kt
index 6974e479..3144f84a 100644
--- a/src/test/kotlin/cz/vutbr/fit/interlockSim/gui/MenuBarTest.kt
+++ b/src/test/kotlin/cz/vutbr/fit/interlockSim/gui/MenuBarTest.kt
@@ -38,7 +38,7 @@ class MenuBarTest : AbstractFrameTestBase() {
@BeforeEach
override fun setUp() {
super.setUp()
-
+
// Create Frame and MenuBar on EDT
runOnEDT {
frame = Frame()
@@ -85,13 +85,13 @@ class MenuBarTest : AbstractFrameTestBase() {
fun fileMenuHasSaveAction() {
runOnEDT {
val fileMenu = menuBar.getMenu(0) as JMenu
-
+
// Get menu items (excluding separators)
val menuItems =
(0 until fileMenu.itemCount)
.map { fileMenu.getItem(it) }
.filterIsInstance()
-
+
// Find Save action
val saveItem = menuItems.find { it.text == "Save as..." }
assertThat(saveItem!!.text).isEqualTo("Save as...")
@@ -104,13 +104,13 @@ class MenuBarTest : AbstractFrameTestBase() {
fun fileMenuHasExitAction() {
runOnEDT {
val fileMenu = menuBar.getMenu(0) as JMenu
-
+
// Get menu items (excluding separators)
val menuItems =
(0 until fileMenu.itemCount)
.map { fileMenu.getItem(it) }
.filterIsInstance()
-
+
// Find Exit action
val exitItem = menuItems.find { it.text == "Exit" }
assertThat(exitItem!!.text).isEqualTo("Exit")
@@ -123,12 +123,12 @@ class MenuBarTest : AbstractFrameTestBase() {
fun fileMenuHasSeparator() {
runOnEDT {
val fileMenu = menuBar.getMenu(0) as JMenu
-
+
// Count separators
val separatorCount =
(0 until fileMenu.itemCount)
.count { fileMenu.getItem(it) == null }
-
+
assertThat(separatorCount).isEqualTo(1)
}
}
@@ -139,13 +139,13 @@ class MenuBarTest : AbstractFrameTestBase() {
fun helpMenuHasUsageAction() {
runOnEDT {
val helpMenu = menuBar.getMenu(1) as JMenu
-
+
// Get menu items
val menuItems =
(0 until helpMenu.itemCount)
.map { helpMenu.getItem(it) }
.filterIsInstance()
-
+
// Find Usage action
val usageItem = menuItems.find { it.text == "Usage" }
assertThat(usageItem!!.text).isEqualTo("Usage")
@@ -158,13 +158,13 @@ class MenuBarTest : AbstractFrameTestBase() {
fun helpMenuHasAboutAction() {
runOnEDT {
val helpMenu = menuBar.getMenu(1) as JMenu
-
+
// Get menu items
val menuItems =
(0 until helpMenu.itemCount)
.map { helpMenu.getItem(it) }
.filterIsInstance()
-
+
// Find About action
val aboutItem = menuItems.find { it.text == "About" }
assertThat(aboutItem!!.text).isEqualTo("About")
@@ -177,13 +177,13 @@ class MenuBarTest : AbstractFrameTestBase() {
fun helpMenuHasExactlyTwoItems() {
runOnEDT {
val helpMenu = menuBar.getMenu(1) as JMenu
-
+
// Get menu items
val menuItems =
(0 until helpMenu.itemCount)
.map { helpMenu.getItem(it) }
.filterIsInstance()
-
+
assertThat(menuItems).hasSize(2)
}
}
@@ -207,14 +207,14 @@ class MenuBarTest : AbstractFrameTestBase() {
fun exitActionHasCorrectText() {
runOnEDT {
val fileMenu = menuBar.getMenu(0) as JMenu
-
+
// Get last item (Exit, after separator)
val menuItems =
(0 until fileMenu.itemCount)
.map { fileMenu.getItem(it) }
.filterIsInstance()
val exitItem = menuItems.last()
-
+
assertThat(exitItem.text).isEqualTo("Exit")
}
}
@@ -226,7 +226,7 @@ class MenuBarTest : AbstractFrameTestBase() {
runOnEDT {
val fileMenu = menuBar.getMenu(0) as JMenu
val helpMenu = menuBar.getMenu(1) as JMenu
-
+
// Verify File menu items are enabled
val fileItems =
(0 until fileMenu.itemCount)
@@ -235,7 +235,7 @@ class MenuBarTest : AbstractFrameTestBase() {
fileItems.forEach { item ->
assertThat(item.isEnabled).isEqualTo(true)
}
-
+
// Verify Help menu items are enabled
val helpItems =
(0 until helpMenu.itemCount)
diff --git a/src/test/kotlin/cz/vutbr/fit/interlockSim/gui/StatusBarTest.kt b/src/test/kotlin/cz/vutbr/fit/interlockSim/gui/StatusBarTest.kt
index d33b0dd4..6d587732 100644
--- a/src/test/kotlin/cz/vutbr/fit/interlockSim/gui/StatusBarTest.kt
+++ b/src/test/kotlin/cz/vutbr/fit/interlockSim/gui/StatusBarTest.kt
@@ -66,10 +66,10 @@ class StatusBarTest : KoinTestBase() {
fun registersStatusProducerMouseListener() {
// Create mock producer that is also a Component
val mockProducer = mockk(relaxed = true)
-
+
// Register producer
statusBar.registerProducer(mockProducer)
-
+
// Verify mouse motion listener was added
verify { mockProducer.addMouseMotionListener(any()) }
}
@@ -79,11 +79,11 @@ class StatusBarTest : KoinTestBase() {
fun unregistersStatusProducerMouseListener() {
// Create mock producer that is also a Component
val mockProducer = mockk(relaxed = true)
-
+
// Register then unregister producer
statusBar.registerProducer(mockProducer)
statusBar.unregisterProducer(mockProducer)
-
+
// Verify mouse motion listener was removed
verify { mockProducer.removeMouseMotionListener(any()) }
}
@@ -93,10 +93,10 @@ class StatusBarTest : KoinTestBase() {
fun updatesStatusOnMouseMoveFromProducer() {
// Create a real producer that can handle mouse events
val producer = TestStatusProducerImpl()
-
+
// Register producer
statusBar.registerProducer(producer)
-
+
// Simulate mouse move
val mouseEvent =
MouseEvent(
@@ -109,10 +109,10 @@ class StatusBarTest : KoinTestBase() {
0,
false
)
-
+
// Trigger mouse moved event
producer.fireMouseMove(mouseEvent)
-
+
// Verify status bar text was updated
assertThat(statusBar.text).isEqualTo("Test status: 10, 20")
}
@@ -128,10 +128,10 @@ class StatusBarTest : KoinTestBase() {
"old",
"New status message"
)
-
+
// Trigger property change
statusBar.propertyChange(event)
-
+
// Verify text was updated
assertThat(statusBar.text).isEqualTo("New status message")
}
@@ -147,10 +147,10 @@ class StatusBarTest : KoinTestBase() {
null,
12345
)
-
+
// Trigger property change
statusBar.propertyChange(event)
-
+
// Verify text was updated with toString()
assertThat(statusBar.text).isEqualTo("12345")
}
@@ -160,7 +160,7 @@ class StatusBarTest : KoinTestBase() {
fun handlesPropertyChangeWithNullValue() {
// Set initial text
statusBar.text = "Initial text"
-
+
// Create property change event with null new value
val event =
PropertyChangeEvent(
@@ -169,10 +169,10 @@ class StatusBarTest : KoinTestBase() {
"old",
null
)
-
+
// Trigger property change
statusBar.propertyChange(event)
-
+
// Verify text remains unchanged when new value is null
assertThat(statusBar.text).isEqualTo("Initial text")
}
diff --git a/src/test/kotlin/cz/vutbr/fit/interlockSim/gui/ValidationUtilsTest.kt b/src/test/kotlin/cz/vutbr/fit/interlockSim/gui/ValidationUtilsTest.kt
index ead0c500..03a77bf0 100644
--- a/src/test/kotlin/cz/vutbr/fit/interlockSim/gui/ValidationUtilsTest.kt
+++ b/src/test/kotlin/cz/vutbr/fit/interlockSim/gui/ValidationUtilsTest.kt
@@ -117,12 +117,12 @@ class ValidationUtilsTest {
@Test
@DisplayName("parseErrorMessage handles InOut validation error")
fun parseErrorMessageHandlesInOutValidation() {
- val saxException = SAXParseException("InOut must have at least 2 elements", null, null, 10, 5)
+ val saxException = SAXParseException("InOut must have at least 1 element", null, null, 10, 5)
val exception = ContextCreationException("Failed", saxException)
val result = ValidationUtils.fromException(exception)
- assertThat(result.errors[0].explanation).contains("Minimum 2 InOut elements required")
+ assertThat(result.errors[0].explanation).contains("Minimum 1 InOut element required")
assertThat(result.errors[0].explanation).contains("entry/exit points for trains")
}
diff --git a/src/test/kotlin/cz/vutbr/fit/interlockSim/testutil/TestContextBuilder.kt b/src/test/kotlin/cz/vutbr/fit/interlockSim/testutil/TestContextBuilder.kt
index 9ad4f8c7..b5f315f3 100644
--- a/src/test/kotlin/cz/vutbr/fit/interlockSim/testutil/TestContextBuilder.kt
+++ b/src/test/kotlin/cz/vutbr/fit/interlockSim/testutil/TestContextBuilder.kt
@@ -251,7 +251,7 @@ fun buildLinearTrackWithSemaphore(): DefaultSimulationContext {
/**
* Creates a minimal context with two InOut elements (entry and exit).
- * Updated to comply with strict validation requiring minimum 2 InOut elements.
+ * Two InOuts are used for conventional bidirectional operation scenarios.
*
* @return context with two InOut elements
*/
diff --git a/src/test/kotlin/cz/vutbr/fit/interlockSim/testutil/TestFixtures.kt b/src/test/kotlin/cz/vutbr/fit/interlockSim/testutil/TestFixtures.kt
index 9d5d2484..fb7e4239 100644
--- a/src/test/kotlin/cz/vutbr/fit/interlockSim/testutil/TestFixtures.kt
+++ b/src/test/kotlin/cz/vutbr/fit/interlockSim/testutil/TestFixtures.kt
@@ -247,14 +247,14 @@ object TestFixtures {
* Loads invalid InOut count fixtures (InOut validation tests).
*
* **Available fixtures:**
- * - "zero-inouts.xml" - Network with 0 InOut elements
- * - "single-inout.xml" - Network with 1 InOut element
+ * - "zero-inouts.xml" - Network with 0 InOut elements (INVALID)
+ * - "single-inout.xml" - Network with 1 InOut element (VALID with bidirectional operation)
*
- * **Validation rule:** Minimum 2 InOut elements required (entry and exit points).
- * Single InOut networks are dead-ends (train enters but cannot exit).
+ * **Validation rule:** Minimum 1 InOut element required (entry/exit point).
+ * With bidirectional train operation, a single InOut can serve as both entry and exit.
*
* @param fixtureName Fixture filename (e.g., "zero-inouts.xml", "single-inout.xml")
- * @return InputStream to invalid InOut fixture
+ * @return InputStream to InOut test fixture
* @throws IllegalStateException if fixture not found
* @see cz.vutbr.fit.interlockSim.xml.XMLContextFactory
*/
diff --git a/src/test/kotlin/cz/vutbr/fit/interlockSim/xml/XMLContextFactoryLenientTest.kt b/src/test/kotlin/cz/vutbr/fit/interlockSim/xml/XMLContextFactoryLenientTest.kt
index aa5b46b8..06422259 100644
--- a/src/test/kotlin/cz/vutbr/fit/interlockSim/xml/XMLContextFactoryLenientTest.kt
+++ b/src/test/kotlin/cz/vutbr/fit/interlockSim/xml/XMLContextFactoryLenientTest.kt
@@ -72,8 +72,8 @@ class XMLContextFactoryLenientTest : KoinTestBase() {
// Assert: Should be parseable but have validation errors
assertThat(result.isParseable).isTrue()
assertThat(result.context).isNotNull()
- assertThat(result.validationResult.isValid).isFalse()
- assertThat(result.validationResult.errors).transform { it.isNotEmpty() }.isTrue()
+ assertThat(result.validationResult.isValid).isTrue()
+ assertThat(result.validationResult.errors).transform { it.isEmpty() }.isTrue()
}
@Test
@@ -173,7 +173,6 @@ class XMLContextFactoryLenientTest : KoinTestBase() {
-
""".trimIndent())
@@ -192,7 +191,7 @@ class XMLContextFactoryLenientTest : KoinTestBase() {
// Arrange: Create a larger XML file with many elements
val largeFile = File.createTempFile("large", ".xml")
largeFile.deleteOnExit()
-
+
val content = buildString {
appendLine("""""")
appendLine("""""")
@@ -246,7 +245,7 @@ class XMLContextFactoryLenientTest : KoinTestBase() {
fun sequentialParsing_shouldWorkCorrectly() {
// Arrange: Get a valid fixture file
val fixtureFile = getFixtureFile("minimal-network.xml")
-
+
// Act: Parse the same file multiple times sequentially
val results = (1..5).map {
xmlContextFactory.createContextLenient(fixtureFile)
diff --git a/src/test/kotlin/cz/vutbr/fit/interlockSim/xml/XMLPolishTest.kt b/src/test/kotlin/cz/vutbr/fit/interlockSim/xml/XMLPolishTest.kt
index 866165dd..098fdb12 100644
--- a/src/test/kotlin/cz/vutbr/fit/interlockSim/xml/XMLPolishTest.kt
+++ b/src/test/kotlin/cz/vutbr/fit/interlockSim/xml/XMLPolishTest.kt
@@ -417,7 +417,7 @@ class XMLPolishTest : KoinTestBase() {
}
@Test
- fun `single InOut throws exception`() {
+ fun `exactly one InOuts is valid`() {
val xml = """
@@ -425,29 +425,12 @@ class XMLPolishTest : KoinTestBase() {
"""
val stream = ByteArrayInputStream(xml.toByteArray())
- assertThatBlock {
- editingContextFactory.createContext(stream)
- }.isFailure()
- .message()
- .isNotNull()
- }
-
- @Test
- fun `exactly two InOuts is valid`() {
- val xml = """
-
-
-
-
- """
- val stream = ByteArrayInputStream(xml.toByteArray())
-
val editingContext = editingContextFactory.createContext(stream) as EditingContext
val context = simulationContextFactory.createContext(editingContext)
assertThat(context).isNotNull()
val inOuts = context.getInOuts()
- assertThat(inOuts as Collection<*>).hasSize(2)
+ assertThat(inOuts as Collection<*>).hasSize(1)
}
@Test
diff --git a/src/test/resources/cz/vutbr/fit/interlockSim/xml/fixtures/single-inout.xml b/src/test/resources/cz/vutbr/fit/interlockSim/xml/fixtures/single-inout.xml
index 9df99175..d7cf7277 100644
--- a/src/test/resources/cz/vutbr/fit/interlockSim/xml/fixtures/single-inout.xml
+++ b/src/test/resources/cz/vutbr/fit/interlockSim/xml/fixtures/single-inout.xml
@@ -1,6 +1,6 @@
-
+