From 03f1a7389afeadf9aa2b1d62a38e5cb0a2a7e60d Mon Sep 17 00:00:00 2001 From: Ruben Beltran del Rio Date: Sat, 13 Dec 2025 01:17:52 +0100 Subject: Initial implementation --- Tests/WmapParserTests/ComponentParserTests.swift | 131 +++++++++ .../WmapParserTests/DependenciesParserTests.swift | 44 +++ Tests/WmapParserTests/EvolutionParserTests.swift | 48 ++++ Tests/WmapParserTests/GroupsParserTests.swift | 38 +++ Tests/WmapParserTests/InertiaParserTests.swift | 24 ++ Tests/WmapParserTests/NotesParserTests.swift | 42 +++ Tests/WmapParserTests/StagesParserTests.swift | 48 ++++ Tests/WmapParserTests/WmapParserTests.swift | 312 +++++++++++++++++++++ 8 files changed, 687 insertions(+) create mode 100644 Tests/WmapParserTests/ComponentParserTests.swift create mode 100644 Tests/WmapParserTests/DependenciesParserTests.swift create mode 100644 Tests/WmapParserTests/EvolutionParserTests.swift create mode 100644 Tests/WmapParserTests/GroupsParserTests.swift create mode 100644 Tests/WmapParserTests/InertiaParserTests.swift create mode 100644 Tests/WmapParserTests/NotesParserTests.swift create mode 100644 Tests/WmapParserTests/StagesParserTests.swift create mode 100644 Tests/WmapParserTests/WmapParserTests.swift (limited to 'Tests') diff --git a/Tests/WmapParserTests/ComponentParserTests.swift b/Tests/WmapParserTests/ComponentParserTests.swift new file mode 100644 index 0000000..52e347a --- /dev/null +++ b/Tests/WmapParserTests/ComponentParserTests.swift @@ -0,0 +1,131 @@ +import Testing + +@testable import WmapParser + +@Test func shouldParseABasicComponent() { + let source = "Dominoes (0.9, 0.5)" + let result = parse(source) + #expect(result.components.count == 1) + #expect( + result.components.first + == Component( + label: "Dominoes", coordinates: MapPoint(xCoordinate: 0.9, yCoordinate: 0.5), shape: .circle + ) + ) +} + +@Test func shouldParseAComponentWithAShape() { + let source = "Database (0.33, 0.81) [Square]" + let result = parse(source) + #expect(result.components.count == 1) + #expect( + result.components.first + == Component( + label: "Database", coordinates: MapPoint(xCoordinate: 0.33, yCoordinate: 0.81), + shape: .square) + ) +} + +@Test func shouldParseAComponentWithXShape() { + let source = "API (0.5, 0.6) [x]" + let result = parse(source) + #expect(result.components.count == 1) + #expect( + result.components.first + == Component( + label: "API", coordinates: MapPoint(xCoordinate: 0.5, yCoordinate: 0.6), shape: .xMark) + ) +} + +@Test func shouldParseAComponentWithTriangleShape() { + let source = "Service (0.4, 0.7) [Triangle]" + let result = parse(source) + #expect(result.components.count == 1) + #expect( + result.components.first + == Component( + label: "Service", coordinates: MapPoint(xCoordinate: 0.4, yCoordinate: 0.7), + shape: .triangle) + ) +} + +@Test func shouldParseAComponentWithCircleShape() { + let source = "Process (0.2, 0.9) [CIRCLE]" + let result = parse(source) + #expect(result.components.count == 1) + #expect( + result.components.first + == Component( + label: "Process", coordinates: MapPoint(xCoordinate: 0.2, yCoordinate: 0.9), shape: .circle) + ) +} + +@Test func shouldDefaultShapeToCircleIfInvalid() { + let source = "Figures (0.12, 0.711) [" + let result = parse(source) + #expect(result.components.count == 1) + #expect( + result.components.first + == Component( + label: "Figures", coordinates: MapPoint(xCoordinate: 0.12, yCoordinate: 0.711), + shape: .circle) + ) +} + +@Test func shouldHandleDecimalNumbers() { + let source = "Item (.5, .123)" + let result = parse(source) + #expect(result.components.count == 1) + #expect( + result.components.first + == Component( + label: "Item", coordinates: MapPoint(xCoordinate: 0.5, yCoordinate: 0.123), shape: .circle) + ) +} + +@Test func shouldHandleIntegersAsCoordinates() { + let source = "Item (1, 0)" + let result = parse(source) + #expect(result.components.count == 1) + #expect( + result.components.first + == Component( + label: "Item", coordinates: MapPoint(xCoordinate: 1.0, yCoordinate: 0.0), shape: .circle) + ) +} + +@Test func shouldPreserveCaseInComponentLabels() { + let source = "RubberGaskets (0.5, 0.5)" + let result = parse(source) + #expect(result.components.count == 1) + #expect( + result.components.first + == Component( + label: "RubberGaskets", coordinates: MapPoint(xCoordinate: 0.5, yCoordinate: 0.5), + shape: .circle) + ) +} + +@Test func shouldHandleComponentLabelsWithSpaces() { + let source = "Big Lawnmowers (0.5, 0.5)" + let result = parse(source) + #expect(result.components.count == 1) + #expect( + result.components.first + == Component( + label: "Big Lawnmowers", coordinates: MapPoint(xCoordinate: 0.5, yCoordinate: 0.5), + shape: .circle) + ) +} + +@Test func shouldHandleWhitespaceAroundTokens() { + let source = " Outer Space ( 0.9 , 0.5 ) [ Square ] " + let result = parse(source) + #expect(result.components.count == 1) + #expect( + result.components.first + == Component( + label: "Outer Space", coordinates: MapPoint(xCoordinate: 0.9, yCoordinate: 0.5), + shape: .square) + ) +} diff --git a/Tests/WmapParserTests/DependenciesParserTests.swift b/Tests/WmapParserTests/DependenciesParserTests.swift new file mode 100644 index 0000000..88e6e99 --- /dev/null +++ b/Tests/WmapParserTests/DependenciesParserTests.swift @@ -0,0 +1,44 @@ +import Testing + +@testable import WmapParser + +@Test func shouldParseUndirectedDependency() { + let source = "Smoke -- Fire" + let result = parse(source) + #expect(result.dependencies.count == 1) + #expect( + result.dependencies.first + == Dependency(fromComponent: "Smoke", toComponent: "Fire", isDirected: false) + ) +} + +@Test func shouldParseDirectedDependency() { + let source = "Cool Thing -> Different Cool Thing" + let result = parse(source) + #expect(result.dependencies.count == 1) + #expect( + result.dependencies.first + == Dependency( + fromComponent: "Cool Thing", toComponent: "Different Cool Thing", isDirected: true) + ) +} + +@Test func shouldPreserveCaseInDependencyLabels() { + let source = "SaRcAsM -> Seriousness." + let result = parse(source) + #expect(result.dependencies.count == 1) + #expect( + result.dependencies.first + == Dependency(fromComponent: "SaRcAsM", toComponent: "Seriousness.", isDirected: true) + ) +} + +@Test func shouldIgnoreWhitespaceInDependencies() { + let source = " loose --TIGHT" + let result = parse(source) + #expect(result.dependencies.count == 1) + #expect( + result.dependencies.first + == Dependency(fromComponent: "loose", toComponent: "TIGHT", isDirected: false) + ) +} diff --git a/Tests/WmapParserTests/EvolutionParserTests.swift b/Tests/WmapParserTests/EvolutionParserTests.swift new file mode 100644 index 0000000..e87bf9e --- /dev/null +++ b/Tests/WmapParserTests/EvolutionParserTests.swift @@ -0,0 +1,48 @@ +import Testing + +@testable import WmapParser + +@Test func shouldParsePositiveEvolution() { + let source = "[Evolution] Nanomachines + 0.5" + let result = parse(source) + #expect(result.evolutions.count == 1) + #expect( + result.evolutions.first == Evolution(component: "Nanomachines", value: 0.5) + ) +} + +@Test func shouldParseNegativeEvolution() { + let source = "[Evolution] FOXDIE - 0.3" + let result = parse(source) + #expect(result.evolutions.count == 1) + #expect( + result.evolutions.first == Evolution(component: "FOXDIE", value: -0.3) + ) +} + +@Test func shouldHandleCaseInsensitiveEvolutionKeyword() { + let source = "[Evolution] tamales + 0.73" + let result = parse(source) + #expect(result.evolutions.count == 1) + #expect( + result.evolutions.first == Evolution(component: "tamales", value: 0.73) + ) +} + +@Test func shouldPreserveCaseInEvolutionComponentLabel() { + let source = "[EVOLUTION] iMac - 0.2" + let result = parse(source) + #expect(result.evolutions.count == 1) + #expect( + result.evolutions.first == Evolution(component: "iMac", value: -0.2) + ) +} + +@Test func shouldHandleDecimalEvolutionValues() { + let source = "[Evolution] Numerals + .75" + let result = parse(source) + #expect(result.evolutions.count == 1) + #expect( + result.evolutions.first == Evolution(component: "Numerals", value: 0.75) + ) +} diff --git a/Tests/WmapParserTests/GroupsParserTests.swift b/Tests/WmapParserTests/GroupsParserTests.swift new file mode 100644 index 0000000..dced486 --- /dev/null +++ b/Tests/WmapParserTests/GroupsParserTests.swift @@ -0,0 +1,38 @@ +import Testing + +@testable import WmapParser + +@Test func shouldParseAGroupWithASingleComponent() { + let source = "[Group] Lonely" + let result = parse(source) + #expect(result.groups.count == 1) + #expect(result.groups.first?.components == ["Lonely"]) +} + +@Test func shouldParseAGroupWithMultipleComponents() { + let source = "[GrouP] Huey, Dewey, Louie" + let result = parse(source) + #expect(result.groups.count == 1) + #expect(result.groups.first?.components == ["Huey", "Dewey", "Louie"]) +} + +@Test func shouldHandleCaseInsensitiveGroupKeyword() { + let source = "[group] Salt, Pepper" + let result = parse(source) + #expect(result.groups.count == 1) + #expect(result.groups.first?.components == ["Salt", "Pepper"]) +} + +@Test func shouldPreserveCaseInGroupComponentLabel() { + let source = "[GROUP] XML, LaTeX" + let result = parse(source) + #expect(result.groups.count == 1) + #expect(result.groups.first?.components == ["XML", "LaTeX"]) +} + +@Test func shouldHandleWhitespaceAroundCommas() { + let source = "[Group] sky, is large, emperor" + let result = parse(source) + #expect(result.groups.count == 1) + #expect(result.groups.first?.components == ["sky", "is large", "emperor"]) +} diff --git a/Tests/WmapParserTests/InertiaParserTests.swift b/Tests/WmapParserTests/InertiaParserTests.swift new file mode 100644 index 0000000..92d9868 --- /dev/null +++ b/Tests/WmapParserTests/InertiaParserTests.swift @@ -0,0 +1,24 @@ +import Testing + +@testable import WmapParser + +@Test func shouldParseInertia() { + let source = "[Inertia] Print Media" + let result = parse(source) + #expect(result.inertias.count == 1) + #expect(result.inertias.first?.component == "Print Media") +} + +@Test func shouldHandleCaseInsensitiveInertiaKeyword() { + let source = "[inertia] Yes" + let result = parse(source) + #expect(result.inertias.count == 1) + #expect(result.inertias.first?.component == "Yes") +} + +@Test func shouldPreserveCaseInInertiaComponentLabel() { + let source = "[INERTIA] SpOnGeBoB" + let result = parse(source) + #expect(result.inertias.count == 1) + #expect(result.inertias.first?.component == "SpOnGeBoB") +} diff --git a/Tests/WmapParserTests/NotesParserTests.swift b/Tests/WmapParserTests/NotesParserTests.swift new file mode 100644 index 0000000..655bef6 --- /dev/null +++ b/Tests/WmapParserTests/NotesParserTests.swift @@ -0,0 +1,42 @@ +import Testing + +@testable import WmapParser + +@Test func shouldParseANote() { + let source = "[Note] (0.5, 0.5) This is a note" + let result = parse(source) + #expect(result.notes.count == 1) + #expect( + result.notes.first + == Note(text: "This is a note", coordinates: MapPoint(xCoordinate: 0.5, yCoordinate: 0.5)) + ) +} + +@Test func shouldHandleCaseInsensitiveNoteKeyword() { + let source = "[note] (0.1, 0.2) Text" + let result = parse(source) + #expect(result.notes.count == 1) + #expect( + result.notes.first + == Note(text: "Text", coordinates: MapPoint(xCoordinate: 0.1, yCoordinate: 0.2)) + ) +} + +@Test func shouldPreserveCaseInNoteText() { + let source = "[NOTE] (0.77, 0.19) Important NOTE" + let result = parse(source) + #expect(result.notes.count == 1) + #expect( + result.notes.first + == Note(text: "Important NOTE", coordinates: MapPoint(xCoordinate: 0.77, yCoordinate: 0.19)) + ) +} + +@Test func shouldHandleEmptyNoteText() { + let source = "[Note] (0.5, 0.5) " + let result = parse(source) + #expect(result.notes.count == 1) + #expect( + result.notes.first == Note(text: "", coordinates: MapPoint(xCoordinate: 0.5, yCoordinate: 0.5)) + ) +} diff --git a/Tests/WmapParserTests/StagesParserTests.swift b/Tests/WmapParserTests/StagesParserTests.swift new file mode 100644 index 0000000..50bab4b --- /dev/null +++ b/Tests/WmapParserTests/StagesParserTests.swift @@ -0,0 +1,48 @@ +import Testing + +@testable import WmapParser + +@Test func shouldParseStageI() { + let source = "[I] 0.25" + let result = parse(source) + #expect(result.stages.count == 1) + #expect( + result.stages.first == Stage(stage: .stageI, value: 0.25) + ) +} + +@Test func shouldParseStageII() { + let source = "[ii] 0.5" + let result = parse(source) + #expect(result.stages.count == 1) + #expect( + result.stages.first == Stage(stage: .stageII, value: 0.5) + ) +} + +@Test func shouldParseStageIII() { + let source = "[III] 0.75" + let result = parse(source) + #expect(result.stages.count == 1) + #expect( + result.stages.first == Stage(stage: .stageIII, value: 0.75) + ) +} + +@Test func shouldParseStageIV() { + let source = "[Iv] 1.0" + let result = parse(source) + #expect(result.stages.count == 1) + #expect( + result.stages.first == Stage(stage: .stageIV, value: 1.0) + ) +} + +@Test func shouldHandleDecimalValues() { + let source = "[i] .333" + let result = parse(source) + #expect(result.stages.count == 1) + #expect( + result.stages.first == Stage(stage: .stageI, value: 0.333) + ) +} diff --git a/Tests/WmapParserTests/WmapParserTests.swift b/Tests/WmapParserTests/WmapParserTests.swift new file mode 100644 index 0000000..b141dc9 --- /dev/null +++ b/Tests/WmapParserTests/WmapParserTests.swift @@ -0,0 +1,312 @@ +import Testing + +@testable import WmapParser + +@Test func shouldParseMultipleEntities() { + let source = """ + Water (0.9, 0.5) + Bucket (0.8, 0.4) [x] + Water -> Bucket + """ + let result = parse(source) + #expect(result.components.count == 2) + #expect( + result.components.first + == Component( + label: "Water", coordinates: MapPoint(xCoordinate: 0.9, yCoordinate: 0.5), shape: .circle) + ) + #expect( + result.components[1] + == Component( + label: "Bucket", coordinates: MapPoint(xCoordinate: 0.8, yCoordinate: 0.4), shape: .xMark) + ) + #expect(result.dependencies.count == 1) + #expect( + result.dependencies.first + == Dependency(fromComponent: "Water", toComponent: "Bucket", isDirected: true) + ) +} + +@Test func shouldHandleUnixLineEndings() { + let source = "Penguins (0.9, 0.5)\nI Use Arch BTW (0.8, 0.4) [x]" + let result = parse(source) + #expect(result.components.count == 2) + #expect( + result.components.first + == Component( + label: "Penguins", coordinates: MapPoint(xCoordinate: 0.9, yCoordinate: 0.5), shape: .circle + ) + ) + #expect( + result.components[1] + == Component( + label: "I Use Arch BTW", coordinates: MapPoint(xCoordinate: 0.8, yCoordinate: 0.4), + shape: .xMark) + ) +} + +@Test func shouldHandleWindowsLineEndings() { + let source = "Clippy (0.9, 0.5)\r\nSpace Cadet (0.8, 0.4) [x]" + let result = parse(source) + #expect(result.components.count == 2) + #expect( + result.components.first + == Component( + label: "Clippy", coordinates: MapPoint(xCoordinate: 0.9, yCoordinate: 0.5), shape: .circle) + ) + #expect( + result.components[1] + == Component( + label: "Space Cadet", coordinates: MapPoint(xCoordinate: 0.8, yCoordinate: 0.4), + shape: .xMark) + ) +} + +@Test func shouldHandleMacLineEndings() { + let source = "SE/30 (0.9, 0.5)\rPerforma (0.8, 0.4) [x]" + let result = parse(source) + #expect(result.components.count == 2) + #expect( + result.components.first + == Component( + label: "SE/30", coordinates: MapPoint(xCoordinate: 0.9, yCoordinate: 0.5), shape: .circle) + ) + #expect( + result.components[1] + == Component( + label: "Performa", coordinates: MapPoint(xCoordinate: 0.8, yCoordinate: 0.4), shape: .xMark) + ) +} + +@Test func shouldIgnoreEmptyLines() { + let source = """ + The Void (0.9, 0.5) + + The Abyss (0.8, 0.4) + """ + let result = parse(source) + #expect(result.components.count == 2) + #expect( + result.components.first + == Component( + label: "The Void", coordinates: MapPoint(xCoordinate: 0.9, yCoordinate: 0.5), shape: .circle + ) + ) + #expect( + result.components[1] + == Component( + label: "The Abyss", coordinates: MapPoint(xCoordinate: 0.8, yCoordinate: 0.4), + shape: .circle) + ) +} + +@Test func shouldHandleIncompleteNotes() { + let source = "[Note]" + let result = parse(source) + #expect(result.components.count == 0) +} + +@Test func shouldHandleIncompleteGroups() { + let source = "[Group]" + let result = parse(source) + #expect(result.components.count == 0) +} + +@Test func shouldHandleIncompleteNodesWithNoCoordinates() { + let source = "I am a node? (" + let result = parse(source) + #expect(result.components.count == 0) +} + +@Test func shouldHandleIncompleteNodesWithIncompleteCoordinates() { + let source = "I am a node? (12" + let result = parse(source) + #expect(result.components.count == 0) +} + +@Test func shouldHandleIncompleteNodesWithMissingYCoordinates() { + let source = "I am a node? (12," + let result = parse(source) + #expect(result.components.count == 0) +} + +@Test func shouldHandleIncompleteNodesWithUnclosedCoordinates() { + let source = "I am a node? (12,21" + let result = parse(source) + #expect(result.components.count == 0) +} + +@Test func shouldHandleIncompleteEvolutionWithoutValue() { + let source = "[Evolution] I accidentally a +" + let result = parse(source) + #expect(result.components.count == 0) +} + +@Test func shouldHandleIncompleteEvolutionWithoutComponent() { + let source = "[Evolution]+" + let result = parse(source) + #expect(result.components.count == 0) +} + +@Test func shouldHandleJustAParenthesis() { + let source = " (" + let result = parse(source) + #expect(result.components.count == 0) +} + +@Test func shouldIgnoreInvalidLines() { + let source = """ + Hello (0.9, 0.5) + Oh No + It's OK (0.8, 0.4) + [ijole] NO IT ISN'T + A-- + B-> + ( + Hello -> It's OK + [ + """ + let result = parse(source) + #expect(result.components.count == 2) + #expect( + result.components.first + == Component( + label: "Hello", coordinates: MapPoint(xCoordinate: 0.9, yCoordinate: 0.5), shape: .circle) + ) + #expect( + result.components[1] + == Component( + label: "It's OK", coordinates: MapPoint(xCoordinate: 0.8, yCoordinate: 0.4), shape: .circle) + ) + #expect(result.dependencies.count == 1) + #expect( + result.dependencies.first + == Dependency(fromComponent: "Hello", toComponent: "It's OK", isDirected: true) + ) +} + +// Edge Cases + +@Test func shouldHandleEmptyInput() { + let source = "" + let result = parse(source) + #expect(result.components.count == 0) +} + +@Test func shouldHandleOnlyWhitespace() { + let source = " \n \n " + let result = parse(source) + #expect(result.components.count == 0) +} + +@Test func onlyLineBreaks() { + let source = "\n" + let result = parse(source) + #expect(result.components.count == 0) +} + +@Test func shouldHandleOnlyInvalidLines() { + let source = "invalid\nstill invalid" + let result = parse(source) + #expect(result.components.count == 0) +} + +@Test func shouldHandleComplexComponentLabels() { + let source = "My Sü'per Complex Label 123 (0.5, 0.5)" + let result = parse(source) + #expect(result.components.count == 1) + #expect( + result.components.first + == Component( + label: "My Sü'per Complex Label 123", + coordinates: MapPoint(xCoordinate: 0.5, yCoordinate: 0.5), shape: .circle) + ) +} + +// Comprehensive Example +// swiftlint:disable:next function_body_length +@Test func shouldParseACompleteWardleyMap() { + let source = """ + [I] 0.25 + [II] 0.5 + [III] 0.75 + [IV] 1.0 + + Tea (0.9, 0.5) [Circle] + Cup (0.8, 0.4) + Kettle (0.7, 0.3) [Square] + Hot Water (0.6, 0.2) + + Tea -> Cup + Cup -> Kettle + Kettle -> Hot Water + + [Note] (0.5, 0.5) This is our tea supply chain + + [Group] Tea, Cup, Kettle + [Inertia] Kettle + [Evolution] Tea + 0.1 + """ + + let result = parse(source) + + // 4 stages + 4 components + 3 dependencies + 1 note + 1 group + 1 inertia + 1 evolution + #expect(result.stages.count == 4) + #expect(result.components.count == 4) + #expect(result.dependencies.count == 3) + #expect(result.notes.count == 1) + #expect(result.groups.count == 1) + #expect(result.inertias.count == 1) + #expect(result.evolutions.count == 1) + + #expect(result.stages.first == Stage(stage: .stageI, value: 0.25)) + #expect(result.stages[1] == Stage(stage: .stageII, value: 0.5)) + #expect(result.stages[2] == Stage(stage: .stageIII, value: 0.75)) + #expect(result.stages[3] == Stage(stage: .stageIV, value: 1.0)) + + #expect( + result.components.first + == Component( + label: "Tea", coordinates: MapPoint(xCoordinate: 0.9, yCoordinate: 0.5), shape: .circle) + ) + #expect( + result.components[1] + == Component( + label: "Cup", coordinates: MapPoint(xCoordinate: 0.8, yCoordinate: 0.4), shape: .circle) + ) + #expect( + result.components[2] + == Component( + label: "Kettle", coordinates: MapPoint(xCoordinate: 0.7, yCoordinate: 0.3), shape: .square) + ) + #expect( + result.components[3] + == Component( + label: "Hot Water", coordinates: MapPoint(xCoordinate: 0.6, yCoordinate: 0.2), + shape: .circle) + ) + + #expect( + result.dependencies.first + == Dependency(fromComponent: "Tea", toComponent: "Cup", isDirected: true) + ) + #expect( + result.dependencies[1] + == Dependency(fromComponent: "Cup", toComponent: "Kettle", isDirected: true) + ) + #expect( + result.dependencies[2] + == Dependency(fromComponent: "Kettle", toComponent: "Hot Water", isDirected: true) + ) + + #expect( + result.notes.first + == Note( + text: "This is our tea supply chain", + coordinates: MapPoint(xCoordinate: 0.5, yCoordinate: 0.5)) + ) + + #expect(result.groups.first?.components == ["Tea", "Cup", "Kettle"]) + #expect(result.inertias.first?.component == "Kettle") + #expect(result.evolutions.first == Evolution(component: "Tea", value: 0.1)) +} -- cgit