Skip to content

Commit cba2bf4

Browse files
reidspencerclaude
andcommitted
Add BASTLoader tests for import functionality
Tests verify: - Loading BAST imports and populating contents - Error handling for missing BAST files - Multiple imports with namespace isolation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 93f3dc0 commit cba2bf4

1 file changed

Lines changed: 212 additions & 0 deletions

File tree

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
/*
2+
* Copyright 2019-2026 Ossum, Inc.
3+
*
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
package com.ossuminc.riddl.bast
8+
9+
import com.ossuminc.riddl.language.AST.*
10+
import com.ossuminc.riddl.language.At
11+
import com.ossuminc.riddl.language.parsing.{RiddlParserInput, TopLevelParser}
12+
import com.ossuminc.riddl.passes.{Pass, PassInput}
13+
import com.ossuminc.riddl.utils.{pc, ec, Await, URL}
14+
import org.scalatest.TestData
15+
import org.scalatest.wordspec.AnyWordSpec
16+
17+
import java.nio.file.{Files, Path}
18+
import scala.concurrent.duration.*
19+
20+
class BASTLoaderTest extends AnyWordSpec {
21+
22+
"BASTLoader" should {
23+
"load a BAST import and populate contents" in { (td: TestData) =>
24+
// Step 1: Create a simple RIDDL source with a type definition
25+
val sourceRiddl = """domain ImportedLib is {
26+
| type MyImportedType is String
27+
| briefly "A library domain"
28+
|}
29+
|""".stripMargin
30+
31+
// Step 2: Parse the source RIDDL to get a Root
32+
val sourceInput = RiddlParserInput(sourceRiddl, "test-source")
33+
val sourceParseResult = TopLevelParser.parseInput(sourceInput, withVerboseFailures = true)
34+
35+
sourceParseResult match {
36+
case Left(messages) =>
37+
fail(s"Source parse failed: ${messages.format}")
38+
39+
case Right(sourceRoot: Root) =>
40+
// Step 3: Write the Root to a BAST file using the Pass framework
41+
val passInput = PassInput(sourceRoot)
42+
val writerResult = Pass.runThesePasses(passInput, Seq(BASTWriter.creator()))
43+
val output = writerResult.outputOf[BASTOutput](BASTWriter.name).get
44+
val bastBytes = output.bytes
45+
46+
val tempDir = Files.createTempDirectory("bast-loader-test")
47+
val bastFile = tempDir.resolve("imported.bast")
48+
Files.write(bastFile, bastBytes)
49+
50+
try {
51+
// Step 4: Create a RIDDL file that imports the BAST
52+
val riddlContent = s"""import "${bastFile.toAbsolutePath}" as imported
53+
|
54+
|domain TestDomain is {
55+
| briefly "A test domain"
56+
|}
57+
|""".stripMargin
58+
59+
// Step 5: Parse the RIDDL file
60+
val rpi = RiddlParserInput(riddlContent, "test-import")
61+
val parseResult = TopLevelParser.parseInput(rpi, withVerboseFailures = true)
62+
63+
parseResult match {
64+
case Left(messages) =>
65+
fail(s"Parse failed: ${messages.format}")
66+
67+
case Right(parsedRoot: Root) =>
68+
// Step 6: Verify we have a BASTImport node
69+
val imports = BASTLoader.getImports(parsedRoot)
70+
assert(imports.size == 1, s"Expected 1 import, got ${imports.size}")
71+
val bastImport = imports.head
72+
assert(bastImport.namespace.value == "imported",
73+
s"Expected namespace 'imported', got '${bastImport.namespace.value}'")
74+
assert(bastImport.contents.isEmpty, "BASTImport contents should be empty before loading")
75+
76+
// Step 7: Load the BAST imports
77+
val baseURL = URL.fromCwdPath(".")
78+
val loadResult = BASTLoader.loadImports(parsedRoot, baseURL)
79+
80+
assert(loadResult.failedCount == 0,
81+
s"Expected 0 failed imports, got ${loadResult.failedCount}: ${loadResult.messages.map(_.format).mkString("; ")}")
82+
assert(loadResult.loadedCount == 1, s"Expected 1 loaded import, got ${loadResult.loadedCount}")
83+
84+
// Step 8: Verify the contents were populated
85+
assert(bastImport.contents.nonEmpty, "BASTImport contents should not be empty after loading")
86+
87+
// Step 9: Verify we can look up the imported domain
88+
val foundDomain = BASTLoader.lookupInNamespace(parsedRoot, "imported", "ImportedLib")
89+
assert(foundDomain.isDefined, "Should find ImportedLib in imported namespace")
90+
assert(foundDomain.get.isInstanceOf[Domain], "Found definition should be a Domain")
91+
}
92+
} finally {
93+
// Cleanup
94+
Files.deleteIfExists(bastFile)
95+
Files.deleteIfExists(tempDir)
96+
}
97+
}
98+
}
99+
100+
"report error for missing BAST file" in { (td: TestData) =>
101+
// Create a RIDDL file that imports a non-existent BAST
102+
val riddlContent = """import "nonexistent.bast" as missing
103+
|
104+
|domain TestDomain is {
105+
| briefly "A test domain"
106+
|}
107+
|""".stripMargin
108+
109+
val rpi = RiddlParserInput(riddlContent, "test-missing")
110+
val parseResult = TopLevelParser.parseInput(rpi, withVerboseFailures = true)
111+
112+
parseResult match {
113+
case Left(messages) =>
114+
fail(s"Parse failed: ${messages.format}")
115+
116+
case Right(parsedRoot: Root) =>
117+
val baseURL = URL.fromCwdPath(".")
118+
val loadResult = BASTLoader.loadImports(parsedRoot, baseURL)
119+
120+
assert(loadResult.failedCount == 1, s"Expected 1 failed import, got ${loadResult.failedCount}")
121+
assert(loadResult.loadedCount == 0, s"Expected 0 loaded imports, got ${loadResult.loadedCount}")
122+
assert(loadResult.messages.nonEmpty, "Should have error messages")
123+
}
124+
}
125+
126+
"handle multiple imports" in { (td: TestData) =>
127+
// Create two RIDDL sources with different definitions
128+
val source1Riddl = """domain UtilsDomain is {
129+
| type TypeA is String
130+
| briefly "Utils domain"
131+
|}
132+
|""".stripMargin
133+
134+
val source2Riddl = """domain ModelsDomain is {
135+
| type TypeB is Number
136+
| briefly "Models domain"
137+
|}
138+
|""".stripMargin
139+
140+
// Parse both sources
141+
val input1 = RiddlParserInput(source1Riddl, "test-utils")
142+
val result1 = TopLevelParser.parseInput(input1, withVerboseFailures = true)
143+
144+
val input2 = RiddlParserInput(source2Riddl, "test-models")
145+
val result2 = TopLevelParser.parseInput(input2, withVerboseFailures = true)
146+
147+
(result1, result2) match {
148+
case (Right(root1: Root), Right(root2: Root)) =>
149+
// Write both to BAST files
150+
val passInput1 = PassInput(root1)
151+
val writerResult1 = Pass.runThesePasses(passInput1, Seq(BASTWriter.creator()))
152+
val output1 = writerResult1.outputOf[BASTOutput](BASTWriter.name).get
153+
154+
val passInput2 = PassInput(root2)
155+
val writerResult2 = Pass.runThesePasses(passInput2, Seq(BASTWriter.creator()))
156+
val output2 = writerResult2.outputOf[BASTOutput](BASTWriter.name).get
157+
158+
val tempDir = Files.createTempDirectory("bast-loader-test-multi")
159+
val bastFile1 = tempDir.resolve("utils.bast")
160+
val bastFile2 = tempDir.resolve("models.bast")
161+
Files.write(bastFile1, output1.bytes)
162+
Files.write(bastFile2, output2.bytes)
163+
164+
try {
165+
val riddlContent = s"""import "${bastFile1.toAbsolutePath}" as utils
166+
|import "${bastFile2.toAbsolutePath}" as models
167+
|
168+
|domain TestDomain is {
169+
| briefly "A test domain"
170+
|}
171+
|""".stripMargin
172+
173+
val rpi = RiddlParserInput(riddlContent, "test-multi-import")
174+
val parseResult = TopLevelParser.parseInput(rpi, withVerboseFailures = true)
175+
176+
parseResult match {
177+
case Left(messages) =>
178+
fail(s"Parse failed: ${messages.format}")
179+
180+
case Right(parsedRoot: Root) =>
181+
val imports = BASTLoader.getImports(parsedRoot)
182+
assert(imports.size == 2, s"Expected 2 imports, got ${imports.size}")
183+
184+
val baseURL = URL.fromCwdPath(".")
185+
val loadResult = BASTLoader.loadImports(parsedRoot, baseURL)
186+
187+
assert(loadResult.failedCount == 0,
188+
s"Expected 0 failed imports: ${loadResult.messages.map(_.format).mkString("; ")}")
189+
assert(loadResult.loadedCount == 2, s"Expected 2 loaded imports, got ${loadResult.loadedCount}")
190+
191+
// Verify namespace isolation
192+
val utilsDomain = BASTLoader.lookupInNamespace(parsedRoot, "utils", "UtilsDomain")
193+
val modelsDomain = BASTLoader.lookupInNamespace(parsedRoot, "models", "ModelsDomain")
194+
assert(utilsDomain.isDefined, "Should find UtilsDomain in utils namespace")
195+
assert(modelsDomain.isDefined, "Should find ModelsDomain in models namespace")
196+
197+
// Verify domains aren't in wrong namespace
198+
val wrongLookup = BASTLoader.lookupInNamespace(parsedRoot, "utils", "ModelsDomain")
199+
assert(wrongLookup.isEmpty, "ModelsDomain should not be in utils namespace")
200+
}
201+
} finally {
202+
Files.deleteIfExists(bastFile1)
203+
Files.deleteIfExists(bastFile2)
204+
Files.deleteIfExists(tempDir)
205+
}
206+
207+
case _ =>
208+
fail("Failed to parse source RIDDL files")
209+
}
210+
}
211+
}
212+
}

0 commit comments

Comments
 (0)