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 @@ - +