UNPKG

@itwin/core-backend

Version:
546 lines • 32.5 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ import { Guid, Logger, LogLevel, OpenMode } from "@itwin/core-bentley"; import { GenericSchema, IModelHost, IModelJsFs, StandaloneDb } from "../../core-backend"; import { SchemaView, schemaViewFormatVersion, SchemaViewPrimitiveType } from "@itwin/ecschema-metadata"; import { expect } from "chai"; import * as path from "path"; import * as sinon from "sinon"; import { KnownTestLocations } from "../KnownTestLocations"; import { TestUtils } from "../TestUtils"; /** Categories of native log messages worth treating as a test failure. Native code routes through * `Logger`, so a C++ `warningv`/`errorv` surfaces here once the category level is raised. */ const monitoredNativeCategories = ["ECDb", "ECObjectsNative"]; /** Run `body` while capturing native warnings and errors from the monitored categories. * * The test harness initializes the logger with all categories off, so native warnings/errors are * dropped before they ever reach JS. To observe them we must (1) raise each monitored category to * Warning - which fires onLogLevelChanged, prompting the native side to re-read levels and start * emitting (Warning and anything more severe, i.e. Error too) - and (2) install sinks to collect * what arrives. Returns the captured logs so the caller can assert none were produced. */ async function captureNativeLogs(body) { const logs = []; const previousLevels = new Map(monitoredNativeCategories.map((category) => [category, Logger.getLevel(category)])); const warningStub = sinon.stub(Logger, "logWarning").callsFake((category, message) => { if (monitoredNativeCategories.includes(category)) logs.push({ level: "Warning", category, message }); }); const errorStub = sinon.stub(Logger, "logError").callsFake((category, messageOrError) => { if (monitoredNativeCategories.includes(category)) logs.push({ level: "Error", category, message: typeof messageOrError === "string" ? messageOrError : String(messageOrError) }); }); for (const category of monitoredNativeCategories) Logger.setLevel(category, LogLevel.Warning); try { await body(); } finally { warningStub.restore(); errorStub.restore(); for (const [category, previousLevel] of previousLevels) Logger.setLevel(category, previousLevel ?? LogLevel.None); } return logs; } /** SchemaB references BisCore and defines BElement deriving from a BisCore class. */ const schemaB = `<?xml version="1.0" encoding="UTF-8"?> <ECSchema schemaName="FragB" alias="fb" version="01.00.00" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.2"> <ECSchemaReference name="BisCore" version="01.00.00" alias="bis"/> <ECEntityClass typeName="BElement"> <BaseClass>bis:PhysicalElement</BaseClass> <ECProperty propertyName="BProp" typeName="string"/> </ECEntityClass> </ECSchema>`; /** SchemaA references FragB and derives AElement from FragB:BElement. * So loading FragA must transitively pull FragB (and, through it, BisCore). */ const schemaA = `<?xml version="1.0" encoding="UTF-8"?> <ECSchema schemaName="FragA" alias="fa" version="01.00.00" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.2"> <ECSchemaReference name="FragB" version="01.00.00" alias="fb"/> <ECEntityClass typeName="AElement"> <BaseClass>fb:BElement</BaseClass> <ECProperty propertyName="AProp" typeName="int"/> </ECEntityClass> </ECSchema>`; /** A third schema imported after the first load, to exercise cache invalidation. */ const schemaC = `<?xml version="1.0" encoding="UTF-8"?> <ECSchema schemaName="FragC" alias="fc" version="01.00.00" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.2"> <ECSchemaReference name="BisCore" version="01.00.00" alias="bis"/> <ECEntityClass typeName="CElement"> <BaseClass>bis:PhysicalElement</BaseClass> <ECProperty propertyName="CProp" typeName="string"/> </ECEntityClass> </ECSchema>`; /** * Tests the incremental ("husk") schema-view path: `getSchemaView({ schemas: [...] })` loads only * the requested schemas plus their references via `PRAGMA schema_view_fragment`, accumulating * across calls. These run against a real iModel - the blob is exercised end to end (native writer -> * TS reader/merge), which is where the fragment mechanism actually has to work. The blob format * itself is an internal C++-writer-to-TS-reader contract. */ describe("SchemaView fragment loading", () => { // Building an empty iModel and importing schemas into it dominates this suite's runtime, so each // seed below is created once in `before`. Each test then just needs a writable file of its own - // a raw file copy plus `StandaloneDb.openFile` - rather than paying for createEmpty + schema import // again, or for the checkpoint/vacuum overhead that `SnapshotDb.createFrom` adds on top of the copy. let fragSeedDb; let genericSeedDb; before(async () => { if (!IModelHost.isValid) await TestUtils.startBackend(); const fragSeedPath = path.join(KnownTestLocations.outputDir, `SchemaViewFragmentSeed-${Guid.createValue()}.bim`); fragSeedDb = StandaloneDb.createEmpty(fragSeedPath, { rootSubject: { name: "SchemaViewFragmentLoadingSeed" } }); await fragSeedDb.importSchemaStrings([schemaB, schemaA]); fragSeedDb.performCheckpoint(); // flush + truncate the WAL so the raw file copy below is consistent GenericSchema.registerSchema(); const genericSeedPath = path.join(KnownTestLocations.outputDir, `SchemaViewFragmentGenericSeed-${Guid.createValue()}.bim`); genericSeedDb = StandaloneDb.createEmpty(genericSeedPath, { rootSubject: { name: "SchemaViewFragmentGenericSeed" } }); await genericSeedDb.importSchemas([GenericSchema.schemaFilePath]); genericSeedDb.performCheckpoint(); }); after(() => { fragSeedDb.close(); genericSeedDb.close(); }); /** Clone a writable iModel from a seed's file via a raw file copy - no checkpoint/vacuum. Only * needed by the handful of tests that mutate the iModel (e.g. importing another schema); every * other test just opens a second read-only connection directly onto the seed's file below. */ function cloneSeed(seedDb, namePrefix) { const filePath = path.join(KnownTestLocations.outputDir, `${namePrefix}-${Guid.createValue()}.bim`); IModelJsFs.copySync(seedDb.pathName, filePath); return StandaloneDb.openFile(filePath, OpenMode.ReadWrite); } /** Get an iModel backed by the FragA/FragB (and BisCore) seed. Read-only by default - just another * connection onto the same file, no copy - since most tests only read a `SchemaView` off it. Pass * `writable: true` for the rare test that needs to mutate the iModel (e.g. import another schema), * which gets its own private copy so it can't affect the shared seed or other tests. */ async function createIModelWithFragSchemas(options) { if (options?.writable) return cloneSeed(fragSeedDb, "SchemaViewFragment"); return StandaloneDb.openFile(fragSeedDb.pathName, OpenMode.Readonly); } /** Every schema name in the iModel, straight from ECDbMeta - the same source the schema manifest * is built from - so tests never hardcode the schema inventory of an empty snapshot. */ async function queryAllSchemaNames(iModel) { const schemaNames = []; for await (const row of iModel.createQueryReader("SELECT Name FROM meta.ECSchemaDef")) schemaNames.push(row[0]); return schemaNames; } /** The sorted names of every schema present in a view. */ function getSortedViewSchemaNames(view) { return [...view.getSchemas()].map((schema) => schema.name).sort(); } it("loads a requested schema and its reference closure, leaving unrequested schemas absent", async () => { const iModel = await createIModelWithFragSchemas(); try { // Request only FragB. It references BisCore, so both must be present; FragA must not be. const view = await iModel.getSchemaView({ schemas: ["FragB"] }); expect(view.getSchema("FragB"), "FragB was requested").to.not.be.undefined; expect(view.getSchema("BisCore"), "BisCore is in FragB's closure").to.not.be.undefined; expect(view.getSchema("FragA"), "FragA was not requested and nothing pulled it in").to.be.undefined; expect(view.findClass("FragB:BElement")).to.not.be.undefined; expect(view.findClass("FragA:AElement")).to.be.undefined; } finally { iModel.close(); } }); it("pulls transitive dependencies when loading a dependent schema", async () => { const iModel = await createIModelWithFragSchemas(); try { // FragA -> FragB -> BisCore. Requesting FragA alone must make the whole chain resolvable. const view = await iModel.getSchemaView({ schemas: ["FragA"] }); expect(view.getSchema("FragA")).to.not.be.undefined; expect(view.getSchema("FragB")).to.not.be.undefined; expect(view.getSchema("BisCore")).to.not.be.undefined; const aElement = view.findClass("FragA:AElement"); expect(aElement).to.not.be.undefined; // Cross-schema base class resolves across the closure. expect(aElement.baseClass?.fullName).to.equal("FragB:BElement"); // And transitively up into BisCore. expect(aElement.is("BisCore:PhysicalElement")).to.be.true; } finally { iModel.close(); } }); it("accumulates schemas across calls into one view", async () => { const iModel = await createIModelWithFragSchemas(); try { const view1 = await iModel.getSchemaView({ schemas: ["FragB"] }); expect(view1.getSchema("FragA")).to.be.undefined; // A second call for FragA merges into the same accumulating view. const view2 = await iModel.getSchemaView({ schemas: ["FragA"] }); expect(view2, "subset view is a single accumulating instance").to.equal(view1); // Both schemas are now resolvable on the one view. expect(view2.findClass("FragA:AElement")).to.not.be.undefined; expect(view2.findClass("FragB:BElement")).to.not.be.undefined; } finally { iModel.close(); } }); it("is idempotent when re-requesting an already-loaded schema", async () => { const iModel = await createIModelWithFragSchemas(); try { const view1 = await iModel.getSchemaView({ schemas: ["FragA"] }); const aElement1 = view1.findClass("FragA:AElement"); expect(aElement1).to.not.be.undefined; // Re-requesting loads nothing new and returns the same instance with stable indices. const view2 = await iModel.getSchemaView({ schemas: ["FragA", "FragB"] }); expect(view2).to.equal(view1); expect(view2.findClass("FragA:AElement").idx).to.equal(aElement1.idx); } finally { iModel.close(); } }); it("ignores schema names the iModel does not contain", async () => { const iModel = await createIModelWithFragSchemas(); try { const view = await iModel.getSchemaView({ schemas: ["DoesNotExist"] }); expect(view.getSchema("DoesNotExist")).to.be.undefined; // No real schemas requested; the view is a valid, empty husk. expect(view.schemaCount).to.equal(0); } finally { iModel.close(); } }); it("serializes overlapping concurrent loads without double-merging", async () => { const iModel = await createIModelWithFragSchemas(); try { // Fire overlapping requests whose closures share BisCore. The merge queue must coalesce so no // schema is merged twice (which would surface as duplicate classes). const [v1, v2] = await Promise.all([ iModel.getSchemaView({ schemas: ["FragA"] }), iModel.getSchemaView({ schemas: ["FragB"] }), ]); expect(v2).to.equal(v1); // Exactly one BElement and one AElement - no duplicates from a double merge. let bElementCount = 0; let aElementCount = 0; for (const schema of v1.getSchemas()) { for (const cls of schema.getClasses()) { if (cls.fullName === "FragB:BElement") bElementCount++; if (cls.fullName === "FragA:AElement") aElementCount++; } } expect(bElementCount, "BElement merged exactly once").to.equal(1); expect(aElementCount, "AElement merged exactly once").to.equal(1); } finally { iModel.close(); } }); it("invalidates the subset husk after a schema import", async () => { const iModel = await createIModelWithFragSchemas({ writable: true }); try { const view1 = await iModel.getSchemaView({ schemas: ["FragB"] }); expect(view1.getSchema("FragB")).to.not.be.undefined; expect(view1.isOutdated).to.be.false; // Importing a schema calls clearCaches, which drops the husk and its loaded-set. await iModel.importSchemaStrings([schemaC]); expect(view1.isOutdated, "the old husk is marked outdated").to.be.true; // A fresh request rebuilds against the new schema state - a different instance that can now // load the newly imported schema. const view2 = await iModel.getSchemaView({ schemas: ["FragC"] }); expect(view2, "a new husk is built after invalidation").to.not.equal(view1); expect(view2.findClass("FragC:CElement")).to.not.be.undefined; } finally { iModel.close(); } }); it("the full view (no args) still contains everything the subset path can load", async () => { const iModel = await createIModelWithFragSchemas(); try { const full = await iModel.getSchemaView(); expect(full.findClass("FragA:AElement"), "full view has the domain class").to.not.be.undefined; expect(full.findClass("FragB:BElement")).to.not.be.undefined; } finally { iModel.close(); } }); it("an unfiltered call after a filtered one merges the remaining schemas into the same view and collapses to full mode", async () => { const iModel = await createIModelWithFragSchemas(); try { // Start incremental: only FragB (+ BisCore) is loaded. const subsetView = await iModel.getSchemaView({ schemas: ["FragB"] }); expect(subsetView.getSchema("FragA")).to.be.undefined; // The unfiltered call means "I need everything": it must merge the rest into the SAME instance. const fullView = await iModel.getSchemaView(); expect(fullView, "the husk accumulates; no new instance").to.equal(subsetView); expect(fullView.findClass("FragA:AElement"), "previously missing schema is now merged").to.not.be.undefined; expect(fullView.findClass("FragB:BElement"), "earlier schemas remain available").to.not.be.undefined; // The unfiltered merge must have collapsed the incremental bookkeeping into full mode: any // further request - filtered or not - is a synchronous no-op that issues no queries. const queryReaderSpy = sinon.spy(iModel, "createQueryReader"); try { const filteredAgain = await iModel.getSchemaView({ schemas: ["FragA"] }); const unfilteredAgain = await iModel.getSchemaView(); expect(filteredAgain).to.equal(fullView); expect(unfilteredAgain).to.equal(fullView); expect(queryReaderSpy.notCalled, "fully loaded view is served without any queries").to.be.true; } finally { queryReaderSpy.restore(); } } finally { iModel.close(); } }); it("filling the view schema-by-schema via filters collapses to full mode and matches a full load", async () => { const iModel = await createIModelWithFragSchemas(); try { // Request every schema the iModel contains, one filtered call at a time. The names come from // ECDbMeta (the manifest's own source), so nothing about the snapshot's schema inventory is // hardcoded - this also exercises requesting excluded schemas (e.g. CoreCustomAttributes) // directly, which must be tolerated and contribute nothing. const allSchemaNames = await queryAllSchemaNames(iModel); expect(allSchemaNames.length, "snapshot contains more schemas than the two imported ones").to.be.greaterThan(2); let accumulatingView; for (const schemaName of allSchemaNames) { const view = await iModel.getSchemaView({ schemas: [schemaName] }); if (accumulatingView === undefined) accumulatingView = view; else expect(view, "every filtered call returns the one accumulating instance").to.equal(accumulatingView); } if (accumulatingView === undefined) expect.fail("no schema view was obtained"); // Once every schema has been requested, the view must have collapsed to full mode: an // unfiltered request is a synchronous no-op on the same instance, with no queries issued. const queryReaderSpy = sinon.spy(iModel, "createQueryReader"); try { const fullRequest = await iModel.getSchemaView(); expect(fullRequest).to.equal(accumulatingView); expect(queryReaderSpy.notCalled, "collapsed view is served without any queries").to.be.true; } finally { queryReaderSpy.restore(); } // The fragment-filled view must be equivalent to a from-scratch full load of the same iModel: // same schemas present (both paths apply the same exclusion list in the native writer). const rebuiltFullView = await iModel.getSchemaView({ forceReload: true }); expect(rebuiltFullView).to.not.equal(accumulatingView); expect(getSortedViewSchemaNames(accumulatingView), "fragment fill and full load agree on the schema set") .to.deep.equal(getSortedViewSchemaNames(rebuiltFullView)); } finally { iModel.close(); } }); describe("forceReload", () => { it("discards an accumulated husk and rebuilds from scratch", async () => { const iModel = await createIModelWithFragSchemas(); try { // Accumulate FragA + FragB into one husk. const view1 = await iModel.getSchemaView({ schemas: ["FragA"] }); expect(view1.getSchema("FragB")).to.not.be.undefined; expect(view1.isOutdated).to.be.false; // forceReload with a narrower filter: the old husk is dropped and marked outdated, and the // rebuilt view contains only the newly requested closure - FragA is gone. const view2 = await iModel.getSchemaView({ schemas: ["FragB"], forceReload: true }); expect(view2).to.not.equal(view1); expect(view1.isOutdated, "the discarded husk is marked outdated").to.be.true; expect(view2.isOutdated).to.be.false; expect(view2.getSchema("FragB")).to.not.be.undefined; expect(view2.getSchema("FragA"), "rebuild starts from scratch; earlier schemas are not carried over").to.be.undefined; } finally { iModel.close(); } }); it("rebuilds a fully loaded view", async () => { const iModel = await createIModelWithFragSchemas(); try { const view1 = await iModel.getSchemaView(); const view2 = await iModel.getSchemaView({ forceReload: true }); expect(view2).to.not.equal(view1); expect(view1.isOutdated).to.be.true; expect(view2.isOutdated).to.be.false; // The rebuilt view is complete again. expect(getSortedViewSchemaNames(view2)).to.deep.equal(getSortedViewSchemaNames(view1)); } finally { iModel.close(); } }); it("is serialized behind an in-flight load", async () => { const iModel = await createIModelWithFragSchemas(); try { // Fire a load and a forceReload without awaiting in between. The reload must wait for the // first load to finish, then discard its result - never tearing down state mid-load. const [view1, view2] = await Promise.all([ iModel.getSchemaView({ schemas: ["FragA"] }), iModel.getSchemaView({ schemas: ["FragA"], forceReload: true }), ]); expect(view2).to.not.equal(view1); expect(view1.isOutdated, "the first load's view was discarded by the reload").to.be.true; expect(view2.isOutdated).to.be.false; expect(view2.findClass("FragA:AElement"), "the rebuilt view is fully usable").to.not.be.undefined; } finally { iModel.close(); } }); }); describe("PRAGMA schema_view_fragment version prefix", () => { // The frontend must pin the blob format version (frontend and backend can be version-skewed), // so it will issue the pragma with the `v<N>;` prefix. These tests prove the prefix contract // works end to end through ConcurrentQuery, not just in the native unit tests. /** Fetch one fragment blob directly via the pragma. Calls `next()` exactly once - PRAGMA * results must not be iterated, since ConcurrentQuery cannot paginate them. */ async function fetchFragmentBlob(iModel, argument) { const reader = iModel.createQueryReader(`PRAGMA schema_view_fragment('${argument}')`); const result = await reader.next(); expect(result.done, "pragma returned a row").to.be.false; const data = result.value.data; if (data === undefined || data === null) expect.fail("pragma returned no data column"); return { data, formatVersion: result.value.formatVersion }; } it("accepts a v1; prefix and returns the same blob as the unprefixed form", async () => { const iModel = await createIModelWithFragSchemas(); try { const nameList = (await queryAllSchemaNames(iModel)).join(","); const prefixed = await fetchFragmentBlob(iModel, `v${schemaViewFormatVersion};${nameList}`); expect(prefixed.formatVersion, "reported format version matches the requested one").to.equal(schemaViewFormatVersion); // Omitting the prefix means "latest", which today is the same version - identical blob bytes. const unprefixed = await fetchFragmentBlob(iModel, nameList); expect(unprefixed.formatVersion).to.equal(schemaViewFormatVersion); expect(Buffer.from(prefixed.data).equals(Buffer.from(unprefixed.data)), "explicit version and latest produce identical blobs").to.be.true; // The prefixed blob is a valid fragment end to end: it merges into a husk and resolves classes. const husk = SchemaView.createMergeable(); husk.mergeFragment(prefixed.data); expect(husk.findClass("FragA:AElement")).to.not.be.undefined; expect(husk.findClass("FragB:BElement")).to.not.be.undefined; } finally { iModel.close(); } }); it("rejects malformed and unsupported version prefixes", async () => { const iModel = await createIModelWithFragSchemas(); try { const nameList = (await queryAllSchemaNames(iModel)).join(","); const invalidArguments = [ `v99;${nameList}`, // unsupported version `v;${nameList}`, // malformed version token (no digits) `vx;${nameList}`, // malformed version token (non-digit) "v1;", // version present, empty name list ]; // Native rejects these at prepare time; ConcurrentQuery surfaces that as a rejected read. for (const argument of invalidArguments) { const reader = iModel.createQueryReader(`PRAGMA schema_view_fragment('${argument}')`); let rejected = false; try { await reader.next(); } catch { rejected = true; } expect(rejected, `'${argument}' is rejected`).to.be.true; } } finally { iModel.close(); } }); }); it("loads a real domain schema's reference closure, dropping references that are excluded", async () => { // Walk against real schemas. Generic references BisCore plus three schemas that are all on SchemaView's // exclusion list. BisCore in turn references four schemas that are ALL excluded. // So requesting Generic must yield a view containing exactly Generic + BisCore - the walk // pulls BisCore, and every excluded reference contributes no rows. const iModel = StandaloneDb.openFile(genericSeedDb.pathName, OpenMode.Readonly); try { // Capture native warnings and errors while the fragment blob is written + parsed. // assert nothing was logged as a safeguard against regressions. let schemaView; const nativeLogs = await captureNativeLogs(async () => { schemaView = await iModel.getSchemaView({ schemas: ["Generic"] }); }); expect(nativeLogs, `native warnings/errors during getSchemaView:\n${nativeLogs.map((l) => `${l.level} | ${l.category} | ${l.message}`).join("\n")}`).to.be.empty; expect(schemaView, "schema view was obtained").to.not.be.undefined; if (!schemaView) return; // The requested schema and its one non-excluded reference are present. expect(schemaView.getSchema("Generic"), "Generic was requested").to.not.be.undefined; expect(schemaView.getSchema("BisCore"), "BisCore is Generic's only non-excluded reference").to.not.be.undefined; // Every excluded reference in the closure contributes nothing to the view. for (const excluded of ["CoreCustomAttributes", "BisCustomAttributes", "ECDbMap", "ECDbSchemaPolicies"]) expect(schemaView.getSchema(excluded), `${excluded} is excluded from SchemaView`).to.be.undefined; // The view contains exactly the two non-excluded schemas - nothing leaked in from the closure. expect(schemaView.schemaCount, "view holds exactly Generic + BisCore").to.equal(2); // A cross-schema base-class reference resolves across the loaded closure. const physicalObject = schemaView.findClass("Generic:PhysicalObject"); expect(physicalObject, "Generic:PhysicalObject is loaded").to.not.be.undefined; expect(physicalObject.is("BisCore:PhysicalElement"), "resolves up into BisCore").to.be.true; // PhysicalObject's immediate base is bis:PhysicalElement, resolved across the schema boundary. const baseClass = physicalObject.baseClass; expect(baseClass, "PhysicalObject has a base class").to.not.be.undefined; expect(baseClass.fullName, "base class is bis:PhysicalElement").to.equal("BisCore:PhysicalElement"); // PhysicalObject defines no own properties, so walking its properties must surface only the // ones inherited from the BisCore ancestor chain (PhysicalElement -> ... -> Element). This // proves the cross-schema property inheritance walk is hydrated correctly. expect(physicalObject.getOwnProperties(), "PhysicalObject has no own properties").to.have.lengthOf(0); // Expected primitive type for a sampling of inherited primitive properties, one (or more) from // each ancestor. const expectedPrimitiveTypes = new Map([ ["FederationGuid", SchemaViewPrimitiveType.Binary], // Element, binary ["CodeValue", SchemaViewPrimitiveType.String], // Element, string ["UserLabel", SchemaViewPrimitiveType.String], // Element, string ["LastMod", SchemaViewPrimitiveType.DateTime], // Element, dateTime ["InSpatialIndex", SchemaViewPrimitiveType.Boolean], // GeometricElement3d, boolean ["Origin", SchemaViewPrimitiveType.Point3d], // GeometricElement3d, point3d ["Yaw", SchemaViewPrimitiveType.Double], // GeometricElement3d, double ["GeometryStream", SchemaViewPrimitiveType.Binary], // GeometricElement3d, binary ]); // Navigation properties contributed by the chain const expectedNavProperties = ["Model", "CodeSpec", "Category", "PhysicalMaterial"]; // First walk: getProperties() returns the full inherited set. Verify each expected property is // present and, for primitives, that its primitive type survived hydration intact. const propertiesByName = new Map(physicalObject.getProperties().map((p) => [p.name, p])); for (const [name, primitiveType] of expectedPrimitiveTypes) { const property = propertiesByName.get(name); expect(property, `inherited property "${name}" is present`).to.not.be.undefined; if (!property) continue; // narrows for the type checker; expect already failed if undefined if (!property.isPrimitive()) expect.fail(`inherited property "${name}" should be primitive`); expect(property.primitiveType, `"${name}" primitive type`).to.equal(primitiveType); } for (const name of expectedNavProperties) { const property = propertiesByName.get(name); expect(property, `inherited navigation property "${name}" is present`).to.not.be.undefined; if (!property) continue; expect(property.isNavigation(), `"${name}" is a navigation property`).to.be.true; } // Second walk: the single getProperty(name) accessor resolves the same inherited properties // (case-insensitively) and reports the same primitive types. for (const [name, primitiveType] of expectedPrimitiveTypes) { const property = physicalObject.getProperty(name); expect(property, `getProperty("${name}") resolves`).to.not.be.undefined; if (!property) continue; if (!property.isPrimitive()) expect.fail(`getProperty("${name}") should be primitive`); expect(property.primitiveType, `getProperty("${name}") primitive type`).to.equal(primitiveType); } } finally { iModel.close(); } }); }); //# sourceMappingURL=SchemaViewFragmentLoading.test.js.map