|
| 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