Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .github/workflows/core-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
name: VisuStructCore tests

on:
push:
branches: [main]
pull_request:

jobs:
swift-test:
runs-on: macos-15
defaults:
run:
working-directory: VisuStruct-iOS/VisuStructCore
steps:
- uses: actions/checkout@v4
- name: Swift version
run: swift --version
- name: Build and test
run: swift test
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ extension VisuStructCodeGenerator {
}
result = result.trimmingCharacters(in: CharacterSet(charactersIn: "_"))
if result.isEmpty { return "Main" }
// Das Trimmen oben entfernt auch den `_` vor einer führenden Ziffer.
if let first = result.unicodeScalars.first, CharacterSet.decimalDigits.contains(first) {
result = "_" + result
}
let reserved: Set<String> = [
"abstract", "assert", "boolean", "break", "byte", "case", "catch", "char", "class", "const",
"continue", "default", "do", "double", "else", "enum", "extends", "final", "finally",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import XCTest
import VisuStructCore

final class CodeGeneratorTests: XCTestCase {
/// Die mitgelieferten `.java`-Dateien sind der erwartete Export der zugehörigen Diagramme.
func testJavaClassFileMatchesExamples() throws {
for name in ["Collatz", "Fibonacci"] {
let document = try ExampleFiles.document(named: "\(name).visustruct")
let generated = VisuStructCodeGenerator.generateJavaClassFile(document: document, className: name)
XCTAssertEqual(generated, try ExampleFiles.text(named: "\(name).java"), name)
}
}

func testJavaClassFileWithInputImportsScannerOnce() throws {
let document = try ExampleFiles.document(named: "2026-04-23-NumberEven.visustruct")
let generated = VisuStructCodeGenerator.generateJavaClassFile(document: document, className: "NumberEven")
XCTAssertTrue(generated.hasPrefix("import java.util.Scanner;\n\npublic class NumberEven {\n"))
XCTAssertEqual(generated.components(separatedBy: "new Scanner(System.in)").count - 1, 1)
XCTAssertTrue(generated.contains("if (number % 2 == 0) {"))
}

func testCollatzInPythonAndJavaScript() throws {
let document = try ExampleFiles.document(named: "Collatz.visustruct")

let python = VisuStructCodeGenerator.generate(document: document, target: .python)
XCTAssertTrue(python.contains("while number != 1:"), python)
XCTAssertTrue(python.contains("print(number)"), python)
XCTAssertFalse(python.contains("{"), python)

let javaScript = VisuStructCodeGenerator.generate(document: document, target: .javascript)
XCTAssertTrue(javaScript.hasPrefix("\"use strict\";"), javaScript)
XCTAssertTrue(javaScript.contains("while (number != 1) {"), javaScript)
XCTAssertTrue(javaScript.contains("console.log(number);"), javaScript)
}

func testSanitizedJavaClassName() {
XCTAssertEqual(VisuStructCodeGenerator.sanitizedJavaClassName("Fibonacci"), "Fibonacci")
let sanitized = VisuStructCodeGenerator.sanitizedJavaClassName("2 Summe bilden!")
XCTAssertFalse(sanitized.isEmpty)
XCTAssertFalse(sanitized.contains(" "))
XCTAssertFalse(sanitized.first?.isNumber ?? true)
}

func testEmptyDocumentProducesNonEmptyPlaceholder() {
for target in [VisuStructCodeGenerator.Target.java, .python, .javascript] {
let code = VisuStructCodeGenerator.generate(document: VisuStructDocument(), target: target)
XCTAssertFalse(code.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty, target.rawValue)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import XCTest
import VisuStructCore

/// Jede Beispieldatei muss sich laden, verlustfrei speichern und in alle Zielsprachen übersetzen lassen.
final class ExampleFilesTests: XCTestCase {
func testExampleDirectoryContainsDiagrams() throws {
XCTAssertFalse(try ExampleFiles.allDiagramURLs().isEmpty, "Keine Beispieldateien gefunden in \(ExampleFiles.directory.path)")
}

func testAllExamplesDecodeAndRoundTrip() throws {
for url in try ExampleFiles.allDiagramURLs() {
let name = url.lastPathComponent
let decoded = try VisuStructXMLCodec.decode(url: url)
XCTAssertFalse(decoded.elements.isEmpty, "\(name): keine Elemente")

let reencoded = VisuStructXMLCodec.encode(decoded)
let decodedAgain = try VisuStructXMLCodec.decode(data: reencoded)
XCTAssertEqual(decodedAgain, decoded, "\(name): Speichern und erneutes Laden verändert das Diagramm")
}
}

func testAllExamplesGenerateCodeForEveryTarget() throws {
let targets: [VisuStructCodeGenerator.Target] = [.java, .python, .javascript]
for url in try ExampleFiles.allDiagramURLs() {
let name = url.lastPathComponent
let document = try VisuStructXMLCodec.decode(url: url)
for target in targets {
let code = VisuStructCodeGenerator.generate(document: document, target: target)
XCTAssertFalse(
code.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,
"\(name): leerer Code für \(target.rawValue)"
)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import XCTest
import VisuStructCore

final class SimulationEngineTests: XCTestCase {
func testCollatzExampleOutputsSequence() throws {
let state = runSimulation(try ExampleFiles.document(named: "Collatz.visustruct"))
XCTAssertEqual(
state.outputLines,
["58", "29", "88", "44", "22", "11", "34", "17", "52", "26", "13", "40", "20", "10", "5", "16", "8", "4", "2", "1"]
)
XCTAssertEqual(state.variables["number"], .int(1))
}

func testFibonacciExampleOutputsFirstTenNumbers() throws {
let state = runSimulation(try ExampleFiles.document(named: "Fibonacci.visustruct"))
XCTAssertEqual(state.outputLines, ["1", "1", "2", "3", "5", "8", "13", "21", "34", "55"])
}

func testNumberEvenExampleUsesInput() throws {
let document = try ExampleFiles.document(named: "2026-04-23-NumberEven.visustruct")
XCTAssertEqual(runSimulation(document, inputs: ["4"]).outputLines, ["number is even"])
XCTAssertEqual(runSimulation(document, inputs: ["7"]).outputLines, ["number is odd"])
}

func testInvalidIntegerInputIsRejectedAndCanBeRetried() {
let document = VisuStructDocument(elements: [
VisuStructElement(type: .anweisung, textLines: ["input: int n"]),
VisuStructElement(type: .anweisung, textLines: ["print: n * 2"]),
])
var engine = VisuStructSimulationEngine(document: document)
engine.step()
XCTAssertEqual(engine.state.inputRequest?.variableName, "n")

engine.provideInput("abc")
XCTAssertNotNil(engine.state.inputError)
XCTAssertNotNil(engine.state.inputRequest)

engine.provideInput("21")
XCTAssertNil(engine.state.inputRequest)
engine.step()
XCTAssertEqual(engine.state.outputLines, ["42"])
}

func testCountingLoops() {
let forLoop = VisuStructDocument(elements: [
VisuStructElement(type: .anweisung, textLines: ["int sum = 0"]),
VisuStructElement(
type: .forSchleife,
textLines: ["int i = 1", "i <= 4", "i++"],
loopBody: [VisuStructElement(type: .anweisung, textLines: ["sum += i"])]
),
])
XCTAssertEqual(runSimulation(forLoop).variables["sum"], .int(10))

let doUntil = VisuStructDocument(elements: [
VisuStructElement(type: .anweisung, textLines: ["int k = 0"]),
VisuStructElement(
type: .doUntilSchleife,
textLines: ["k < 3"],
loopBody: [VisuStructElement(type: .anweisung, textLines: ["k++"])]
),
])
XCTAssertEqual(runSimulation(doUntil).variables["k"], .int(3))
}

func testSwitchFallsBackToDefaultCase() {
func document(day: Int) -> VisuStructDocument {
VisuStructDocument(elements: [
VisuStructElement(type: .anweisung, textLines: ["int day = \(day)"]),
VisuStructElement(
type: .fallauswahl,
textLines: ["day"],
cases: [
VisuStructCase(name: "1", elements: [VisuStructElement(type: .anweisung, textLines: ["print: \"Montag\""])]),
VisuStructCase(name: "2", elements: [VisuStructElement(type: .anweisung, textLines: ["print: \"Dienstag\""])]),
VisuStructCase(name: "default", elements: [VisuStructElement(type: .anweisung, textLines: ["print: \"anderer Tag\""])]),
]
),
])
}
XCTAssertEqual(runSimulation(document(day: 2)).outputLines, ["Dienstag"])
XCTAssertEqual(runSimulation(document(day: 5)).outputLines, ["anderer Tag"])
}

func testBooleanOperatorsInConditions() {
func document(_ condition: String) -> VisuStructDocument {
VisuStructDocument(elements: [
VisuStructElement(type: .anweisung, textLines: ["int a = 5"]),
VisuStructElement(
type: .verzweigung,
textLines: [condition],
cases: [
VisuStructCase(name: "true", elements: [VisuStructElement(type: .anweisung, textLines: ["print: \"ja\""])]),
VisuStructCase(name: "false", elements: [VisuStructElement(type: .anweisung, textLines: ["print: \"nein\""])]),
]
),
])
}
XCTAssertEqual(runSimulation(document("a > 3 && a < 10")).outputLines, ["ja"])
XCTAssertEqual(runSimulation(document("a < 3 || a == 5")).outputLines, ["ja"])
XCTAssertEqual(runSimulation(document("a != 5")).outputLines, ["nein"])
}

func testDivisionByZeroReportsError() {
let document = VisuStructDocument(elements: [
VisuStructElement(type: .anweisung, textLines: ["int x = 1 / 0"]),
])
var engine = VisuStructSimulationEngine(document: document)
engine.step()
XCTAssertEqual(engine.state.message, "Division by zero.")
XCTAssertNil(engine.state.variables["x"])
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import Foundation
import XCTest
import VisuStructCore

/// Beispieldiagramme im Repo-Ordner `VisuStruct-Dateien` (relativ zu dieser Datei aufgelöst).
enum ExampleFiles {
static let directory: URL = URL(fileURLWithPath: #filePath)
.deletingLastPathComponent() // VisuStructCoreTests
.deletingLastPathComponent() // Tests
.deletingLastPathComponent() // VisuStructCore
.deletingLastPathComponent() // VisuStruct-iOS
.deletingLastPathComponent() // Repo-Wurzel
.appendingPathComponent("VisuStruct-Dateien", isDirectory: true)

/// Alle `.visustruct`-Dateien, rekursiv, sortiert.
static func allDiagramURLs() throws -> [URL] {
guard let enumerator = FileManager.default.enumerator(at: directory, includingPropertiesForKeys: nil) else {
return []
}
return enumerator
.compactMap { $0 as? URL }
.filter { $0.pathExtension == "visustruct" }
.sorted { $0.path < $1.path }
}

static func document(named name: String) throws -> VisuStructDocument {
try VisuStructXMLCodec.decode(url: directory.appendingPathComponent(name))
}

static func text(named name: String) throws -> String {
try String(contentsOf: directory.appendingPathComponent(name), encoding: .utf8)
}
}

/// Lässt die Simulation bis zum Ende laufen und beantwortet `input:`-Anfragen der Reihe nach.
func runSimulation(
_ document: VisuStructDocument,
inputs: [String] = [],
maxSteps: Int = 10_000,
file: StaticString = #filePath,
line: UInt = #line
) -> VisuStructSimulationState {
var engine = VisuStructSimulationEngine(document: document)
var remainingInputs = inputs[...]
var stepsTaken = 0
while stepsTaken < maxSteps {
if engine.state.inputRequest != nil {
guard let next = remainingInputs.popFirst() else {
XCTFail("Simulation wartet auf Eingabe, aber es sind keine Eingaben mehr übrig.", file: file, line: line)
return engine.state
}
engine.provideInput(next)
continue
}
guard engine.canStep else { break }
engine.step()
stepsTaken += 1
}
XCTAssertLessThan(stepsTaken, maxSteps, "Simulation endet nicht.", file: file, line: line)
return engine.state
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ import XCTest
import VisuStructCore

final class VisuStructXMLCodecTests: XCTestCase {
/// Texte stehen wie im Java-Editor als `;`-getrennte Unicode-Codepoints im XML.
func testDecodesLegacyStruktogrammeditorXML() throws {
let xml = """
<?xml version="1.0" encoding="UTF-8"?>
<struktogramm caption="Beispiel">
<struktogramm caption="66;101;105;115;112;105;101;108;">
<strelem typ="0" zx="0" zy="0">
<text>x = 1</text>
<text>120;32;61;32;49;</text>
</strelem>
</struktogramm>
"""
Expand Down
Loading