Skip to content

Commit 25e6ce7

Browse files
authored
test: cover parser pipeline (#67)
1 parent 66afb31 commit 25e6ce7

File tree

1 file changed

+156
-0
lines changed

1 file changed

+156
-0
lines changed
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import XCTest
2+
@testable import SwiftParser
3+
4+
final class ParserPipelineTests: XCTestCase {
5+
enum SimpleTokenElement: String, CaseIterable, CodeTokenElement {
6+
case number
7+
case plus
8+
case eof
9+
}
10+
11+
struct SimpleToken: CodeToken {
12+
typealias Element = SimpleTokenElement
13+
let element: SimpleTokenElement
14+
let text: String
15+
let range: Range<String.Index>
16+
}
17+
18+
struct NumberTokenBuilder: CodeTokenBuilder {
19+
typealias Token = SimpleTokenElement
20+
func build(from context: inout CodeTokenContext<Token>) -> Bool {
21+
guard context.consuming < context.source.endIndex,
22+
context.source[context.consuming].isNumber else { return false }
23+
var end = context.consuming
24+
while end < context.source.endIndex && context.source[end].isNumber {
25+
end = context.source.index(after: end)
26+
}
27+
let range = context.consuming..<end
28+
let token = SimpleToken(element: .number,
29+
text: String(context.source[range]),
30+
range: range)
31+
context.tokens.append(token)
32+
context.consuming = end
33+
return true
34+
}
35+
}
36+
37+
struct PlusTokenBuilder: CodeTokenBuilder {
38+
typealias Token = SimpleTokenElement
39+
func build(from context: inout CodeTokenContext<Token>) -> Bool {
40+
guard context.consuming < context.source.endIndex,
41+
context.source[context.consuming] == "+" else { return false }
42+
let start = context.consuming
43+
context.consuming = context.source.index(after: start)
44+
let token = SimpleToken(element: .plus, text: "+", range: start..<context.consuming)
45+
context.tokens.append(token)
46+
return true
47+
}
48+
}
49+
50+
struct WhitespaceTokenBuilder: CodeTokenBuilder {
51+
typealias Token = SimpleTokenElement
52+
func build(from context: inout CodeTokenContext<Token>) -> Bool {
53+
guard context.consuming < context.source.endIndex,
54+
context.source[context.consuming].isWhitespace else { return false }
55+
context.consuming = context.source.index(after: context.consuming)
56+
return true
57+
}
58+
}
59+
60+
enum SimpleNodeElement: String, CaseIterable, CodeNodeElement {
61+
case root
62+
case number
63+
}
64+
65+
struct NumberNodeBuilder: CodeNodeBuilder {
66+
typealias Node = SimpleNodeElement
67+
typealias Token = SimpleTokenElement
68+
func build(from context: inout CodeConstructContext<Node, Token>) -> Bool {
69+
guard context.consuming < context.tokens.count,
70+
let token = context.tokens[context.consuming] as? SimpleToken,
71+
token.element == .number else { return false }
72+
let node = CodeNode<Node>(element: .number)
73+
context.current.append(node)
74+
context.consuming += 1
75+
return true
76+
}
77+
}
78+
79+
struct SimpleLanguage: CodeLanguage {
80+
typealias Node = SimpleNodeElement
81+
typealias Token = SimpleTokenElement
82+
83+
var tokens: [any CodeTokenBuilder<Token>] {
84+
[WhitespaceTokenBuilder(), NumberTokenBuilder(), PlusTokenBuilder()]
85+
}
86+
var nodes: [any CodeNodeBuilder<Node, Token>] { [NumberNodeBuilder()] }
87+
88+
func root() -> CodeNode<Node> { CodeNode<Node>(element: .root) }
89+
func state() -> (any CodeConstructState<Node, Token>)? { nil }
90+
func state() -> (any CodeTokenState<Token>)? { nil }
91+
// rely on default eof implementation
92+
}
93+
94+
func testTokenizerProducesTokensAndErrors() {
95+
let tokenizer = CodeTokenizer(
96+
builders: [NumberTokenBuilder(), PlusTokenBuilder()],
97+
state: { nil },
98+
eof: { SimpleToken(element: .eof, text: "", range: $0) }
99+
)
100+
101+
let (tokens, errors) = tokenizer.tokenize("1+a")
102+
XCTAssertEqual(tokens.count, 3) // number, plus, eof
103+
XCTAssertEqual((tokens[0] as? SimpleToken)?.text, "1")
104+
XCTAssertEqual((tokens[1] as? SimpleToken)?.element, .plus)
105+
XCTAssertEqual((tokens[2] as? SimpleToken)?.element, .eof)
106+
XCTAssertEqual(errors.count, 1)
107+
}
108+
109+
func testConstructorBuildsNodesAndErrors() {
110+
let oneRange = "1".startIndex..<"1".endIndex
111+
let plusRange = "+".startIndex..<"+".endIndex
112+
let twoRange = "2".startIndex..<"2".endIndex
113+
let tokens: [any CodeToken<SimpleTokenElement>] = [
114+
SimpleToken(element: .number, text: "1", range: oneRange),
115+
SimpleToken(element: .plus, text: "+", range: plusRange),
116+
SimpleToken(element: .number, text: "2", range: twoRange)
117+
]
118+
let root = CodeNode<SimpleNodeElement>(element: .root)
119+
let constructor = CodeConstructor(builders: [NumberNodeBuilder()], state: { nil })
120+
let (parsed, errors) = constructor.parse(tokens, root: root)
121+
XCTAssertEqual(parsed.children.count, 2)
122+
XCTAssertEqual(errors.count, 1) // plus token unrecognized
123+
}
124+
125+
func testParserNormalizesAndParses() {
126+
let language = SimpleLanguage()
127+
let parser = CodeParser(language: language)
128+
let result = parser.parse("1\r\n2\r", language: language)
129+
XCTAssertEqual(result.root.children.count, 2)
130+
XCTAssertTrue(result.errors.isEmpty)
131+
XCTAssertEqual(result.tokens.count, 2)
132+
// ensure default eof returns nil
133+
XCTAssertNil(language.eof(at: "".startIndex..<"".endIndex))
134+
}
135+
136+
func testContextAndErrorInitialization() {
137+
let tokenContext = CodeTokenContext<SimpleTokenElement>(source: "1")
138+
XCTAssertEqual(tokenContext.source, "1")
139+
XCTAssertTrue(tokenContext.tokens.isEmpty)
140+
XCTAssertTrue(tokenContext.errors.isEmpty)
141+
142+
let root = CodeNode<SimpleNodeElement>(element: .root)
143+
let constructContext = CodeConstructContext<SimpleNodeElement, SimpleTokenElement>(
144+
current: root, tokens: []
145+
)
146+
XCTAssertTrue(constructContext.current === root)
147+
XCTAssertEqual(constructContext.tokens.count, 0)
148+
XCTAssertEqual(constructContext.consuming, 0)
149+
XCTAssertTrue(constructContext.errors.isEmpty)
150+
151+
let error = CodeError("msg", range: "a".startIndex..<"a".startIndex)
152+
XCTAssertEqual(error.message, "msg")
153+
XCTAssertNotNil(error.range)
154+
}
155+
}
156+

0 commit comments

Comments
 (0)