From e43a4deaefd273bbda7d51def822f5aa1bf0ca35 Mon Sep 17 00:00:00 2001 From: Dongyu Zhao Date: Thu, 7 Aug 2025 14:35:59 +0800 Subject: [PATCH] test: cover parser pipeline --- .../ParserPipelineTests.swift | 156 ++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 Tests/SwiftParserTests/ParserPipelineTests.swift diff --git a/Tests/SwiftParserTests/ParserPipelineTests.swift b/Tests/SwiftParserTests/ParserPipelineTests.swift new file mode 100644 index 0000000..b232ea4 --- /dev/null +++ b/Tests/SwiftParserTests/ParserPipelineTests.swift @@ -0,0 +1,156 @@ +import XCTest +@testable import SwiftParser + +final class ParserPipelineTests: XCTestCase { + enum SimpleTokenElement: String, CaseIterable, CodeTokenElement { + case number + case plus + case eof + } + + struct SimpleToken: CodeToken { + typealias Element = SimpleTokenElement + let element: SimpleTokenElement + let text: String + let range: Range + } + + struct NumberTokenBuilder: CodeTokenBuilder { + typealias Token = SimpleTokenElement + func build(from context: inout CodeTokenContext) -> Bool { + guard context.consuming < context.source.endIndex, + context.source[context.consuming].isNumber else { return false } + var end = context.consuming + while end < context.source.endIndex && context.source[end].isNumber { + end = context.source.index(after: end) + } + let range = context.consuming..) -> Bool { + guard context.consuming < context.source.endIndex, + context.source[context.consuming] == "+" else { return false } + let start = context.consuming + context.consuming = context.source.index(after: start) + let token = SimpleToken(element: .plus, text: "+", range: start..) -> Bool { + guard context.consuming < context.source.endIndex, + context.source[context.consuming].isWhitespace else { return false } + context.consuming = context.source.index(after: context.consuming) + return true + } + } + + enum SimpleNodeElement: String, CaseIterable, CodeNodeElement { + case root + case number + } + + struct NumberNodeBuilder: CodeNodeBuilder { + typealias Node = SimpleNodeElement + typealias Token = SimpleTokenElement + func build(from context: inout CodeConstructContext) -> Bool { + guard context.consuming < context.tokens.count, + let token = context.tokens[context.consuming] as? SimpleToken, + token.element == .number else { return false } + let node = CodeNode(element: .number) + context.current.append(node) + context.consuming += 1 + return true + } + } + + struct SimpleLanguage: CodeLanguage { + typealias Node = SimpleNodeElement + typealias Token = SimpleTokenElement + + var tokens: [any CodeTokenBuilder] { + [WhitespaceTokenBuilder(), NumberTokenBuilder(), PlusTokenBuilder()] + } + var nodes: [any CodeNodeBuilder] { [NumberNodeBuilder()] } + + func root() -> CodeNode { CodeNode(element: .root) } + func state() -> (any CodeConstructState)? { nil } + func state() -> (any CodeTokenState)? { nil } + // rely on default eof implementation + } + + func testTokenizerProducesTokensAndErrors() { + let tokenizer = CodeTokenizer( + builders: [NumberTokenBuilder(), PlusTokenBuilder()], + state: { nil }, + eof: { SimpleToken(element: .eof, text: "", range: $0) } + ) + + let (tokens, errors) = tokenizer.tokenize("1+a") + XCTAssertEqual(tokens.count, 3) // number, plus, eof + XCTAssertEqual((tokens[0] as? SimpleToken)?.text, "1") + XCTAssertEqual((tokens[1] as? SimpleToken)?.element, .plus) + XCTAssertEqual((tokens[2] as? SimpleToken)?.element, .eof) + XCTAssertEqual(errors.count, 1) + } + + func testConstructorBuildsNodesAndErrors() { + let oneRange = "1".startIndex..<"1".endIndex + let plusRange = "+".startIndex..<"+".endIndex + let twoRange = "2".startIndex..<"2".endIndex + let tokens: [any CodeToken] = [ + SimpleToken(element: .number, text: "1", range: oneRange), + SimpleToken(element: .plus, text: "+", range: plusRange), + SimpleToken(element: .number, text: "2", range: twoRange) + ] + let root = CodeNode(element: .root) + let constructor = CodeConstructor(builders: [NumberNodeBuilder()], state: { nil }) + let (parsed, errors) = constructor.parse(tokens, root: root) + XCTAssertEqual(parsed.children.count, 2) + XCTAssertEqual(errors.count, 1) // plus token unrecognized + } + + func testParserNormalizesAndParses() { + let language = SimpleLanguage() + let parser = CodeParser(language: language) + let result = parser.parse("1\r\n2\r", language: language) + XCTAssertEqual(result.root.children.count, 2) + XCTAssertTrue(result.errors.isEmpty) + XCTAssertEqual(result.tokens.count, 2) + // ensure default eof returns nil + XCTAssertNil(language.eof(at: "".startIndex..<"".endIndex)) + } + + func testContextAndErrorInitialization() { + let tokenContext = CodeTokenContext(source: "1") + XCTAssertEqual(tokenContext.source, "1") + XCTAssertTrue(tokenContext.tokens.isEmpty) + XCTAssertTrue(tokenContext.errors.isEmpty) + + let root = CodeNode(element: .root) + let constructContext = CodeConstructContext( + current: root, tokens: [] + ) + XCTAssertTrue(constructContext.current === root) + XCTAssertEqual(constructContext.tokens.count, 0) + XCTAssertEqual(constructContext.consuming, 0) + XCTAssertTrue(constructContext.errors.isEmpty) + + let error = CodeError("msg", range: "a".startIndex..<"a".startIndex) + XCTAssertEqual(error.message, "msg") + XCTAssertNotNil(error.range) + } +} +