110 lines
11 KiB
JavaScript
110 lines
11 KiB
JavaScript
import { createHash } from 'node:crypto'
|
|
import { isDeepStrictEqual } from 'node:util'
|
|
import { readFile } from 'node:fs/promises'
|
|
import { resolve } from 'node:path'
|
|
|
|
const root = resolve(new URL('..', import.meta.url).pathname)
|
|
const runtimePath = '.cache/freecad/reference-desktop.json'
|
|
const reportPath = 'config/freecad-property-fileincluded-inventory.json'
|
|
const fail = (message) => { throw new Error(`FreeCAD PropertyFileIncluded inventory check failed: ${message}`) }
|
|
const [runtimeContent, reportContent] = await Promise.all([
|
|
readFile(resolve(root, runtimePath)),
|
|
readFile(resolve(root, reportPath)),
|
|
])
|
|
const runtime = JSON.parse(runtimeContent)
|
|
const report = JSON.parse(reportContent)
|
|
|
|
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.propertyType !== 'App::PropertyFileIncluded' || report.classification !== 'opaque-fcstd-proxy' || report.baseline?.freecadVersion !== '1.1.1' || report.baseline?.commit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') fail('report boundary is invalid')
|
|
if (report.recordCount !== 20 || report.objectTypeCount !== 15 || report.records?.length !== 20) fail('runtime record or host count changed')
|
|
if (report.provenance?.runtime?.path !== runtimePath || report.provenance.runtime.bytes !== runtimeContent.length || report.provenance.runtime.sha256 !== createHash('sha256').update(runtimeContent).digest('hex')) fail('runtime provenance is stale')
|
|
for (const locked of report.provenance?.sources ?? []) {
|
|
const content = await readFile(resolve(root, locked.path))
|
|
if (content.length !== locked.bytes || createHash('sha256').update(content).digest('hex') !== locked.sha256) fail(`source provenance is stale for ${locked.path}`)
|
|
}
|
|
|
|
const normalizeStatus = (status) => status.map((entry) => entry === 27 ? 'PropOutput' : String(entry))
|
|
const nativeMatches = []
|
|
for (const objectType of runtime.runtimeObjects?.types ?? []) for (const property of objectType.properties ?? []) {
|
|
if (property.typeId !== 'App::PropertyFileIncluded') continue
|
|
nativeMatches.push({
|
|
objectTypeId: objectType.typeId,
|
|
objectAvailable: objectType.available === true,
|
|
probeStatus: objectType.probeStatus,
|
|
propertyName: property.name,
|
|
group: property.group,
|
|
status: property.status,
|
|
normalizedStatus: normalizeStatus(property.status),
|
|
defaultRaw: property.default,
|
|
})
|
|
}
|
|
const identity = ({ objectTypeId, objectAvailable, probeStatus, propertyName, group, status, normalizedStatus, defaultRaw }) => ({ objectTypeId, objectAvailable, probeStatus, propertyName, group, status, normalizedStatus, defaultRaw })
|
|
const sortRecords = (records) => records.map(identity).sort((left, right) => `${left.objectTypeId}.${left.propertyName}`.localeCompare(`${right.objectTypeId}.${right.propertyName}`))
|
|
if (nativeMatches.length !== 20 || !isDeepStrictEqual(sortRecords(nativeMatches), sortRecords(report.records))) fail('report diverges from the locked runtime oracle')
|
|
|
|
const expectedKeys = [
|
|
'App::DocumentObjectFileIncluded.File',
|
|
'App::VRMLObject.VrmlFile',
|
|
'Image::ImagePlane.ImageFile',
|
|
'Robot::RobotObject.RobotKinematicFile',
|
|
'Robot::RobotObject.RobotVrmlFile',
|
|
'Sketcher::SketchObjectSF.SketchFlatFile',
|
|
'TechDraw::DrawComplexSection.PatIncluded',
|
|
'TechDraw::DrawComplexSection.SvgIncluded',
|
|
'TechDraw::DrawComplexSectionPython.PatIncluded',
|
|
'TechDraw::DrawComplexSectionPython.SvgIncluded',
|
|
'TechDraw::DrawGeomHatch.PatIncluded',
|
|
'TechDraw::DrawHatch.SvgIncluded',
|
|
'TechDraw::DrawSVGTemplate.PageResult',
|
|
'TechDraw::DrawTileWeld.SymbolIncluded',
|
|
'TechDraw::DrawTileWeldPython.SymbolIncluded',
|
|
'TechDraw::DrawViewImage.ImageIncluded',
|
|
'TechDraw::DrawViewSection.PatIncluded',
|
|
'TechDraw::DrawViewSection.SvgIncluded',
|
|
'TechDraw::DrawViewSectionPython.PatIncluded',
|
|
'TechDraw::DrawViewSectionPython.SvgIncluded',
|
|
]
|
|
if (report.records.map(({ objectTypeId, propertyName }) => `${objectTypeId}.${propertyName}`).join('|') !== expectedKeys.join('|')) fail('host/property inventory changed')
|
|
if (report.records.some((record) => record.objectAvailable !== true || record.probeStatus !== 'available' || record.valueModel?.kind !== 'included-file-resource' || record.valueModel.representation !== 'read-only document-transient file backed by embedded FCStd bytes' || record.valueModel.pathIsRuntimeOnly !== true || record.valueModel.bytesAreAuthoritative !== true || record.inputs?.sourceFileMustExist !== true || record.inputs.emptyStringBehavior !== 'accepted-no-op; does not clear an existing value' || record.inputs.sameTransientPathBehavior !== 'rejected' || record.editor !== 'Gui::PropertyEditor::PropertyTransientFileItem' || record.applicability?.requiredObjectTypeId !== record.objectTypeId || record.applicability.propertyWriteRequiresShape !== false)) fail('per-record value, input, editor or applicability contract is incomplete')
|
|
if (report.records.filter(({ valueModel }) => valueModel.writableByStatus).length !== 11 || report.records.filter(({ status }) => isDeepStrictEqual(status, ['ReadOnly'])).length !== 8 || report.records.filter(({ status }) => isDeepStrictEqual(status, [27])).length !== 1 || report.records.filter(({ defaultKind }) => defaultKind === 'empty').length !== 8 || report.records.filter(({ defaultKind }) => defaultKind === 'document-transient-copy').length !== 12) fail('status/default partition changed')
|
|
|
|
const dependencyPairs = report.records.filter(({ dependency }) => dependency !== null).map(({ objectTypeId, propertyName, dependency }) => `${objectTypeId}.${propertyName}<-${dependency.sourceProperty}`)
|
|
const expectedDependencyPairs = [
|
|
'TechDraw::DrawComplexSection.PatIncluded<-FileGeomPattern',
|
|
'TechDraw::DrawComplexSection.SvgIncluded<-FileHatchPattern',
|
|
'TechDraw::DrawComplexSectionPython.PatIncluded<-FileGeomPattern',
|
|
'TechDraw::DrawComplexSectionPython.SvgIncluded<-FileHatchPattern',
|
|
'TechDraw::DrawGeomHatch.PatIncluded<-FilePattern',
|
|
'TechDraw::DrawHatch.SvgIncluded<-HatchPattern',
|
|
'TechDraw::DrawSVGTemplate.PageResult<-Template',
|
|
'TechDraw::DrawTileWeld.SymbolIncluded<-SymbolFile',
|
|
'TechDraw::DrawTileWeldPython.SymbolIncluded<-SymbolFile',
|
|
'TechDraw::DrawViewImage.ImageIncluded<-ImageFile',
|
|
'TechDraw::DrawViewSection.PatIncluded<-FileGeomPattern',
|
|
'TechDraw::DrawViewSection.SvgIncluded<-FileHatchPattern',
|
|
'TechDraw::DrawViewSectionPython.PatIncluded<-FileGeomPattern',
|
|
'TechDraw::DrawViewSectionPython.SvgIncluded<-FileHatchPattern',
|
|
]
|
|
if (!isDeepStrictEqual(dependencyPairs, expectedDependencyPairs) || report.records.filter(({ dependency }) => dependency === null).length !== 6 || report.records.some(({ dependency }) => dependency && dependency.relation !== 'host-onChanged-copies-source-bytes-into-included-property')) fail('derived-source dependency inventory changed')
|
|
|
|
const setter = report.setterSemantics
|
|
if (setter?.externalSource !== 'copy bytes into the document transient directory' || setter.writableTransientSource !== 'rename into the selected transient destination' || setter.replacement !== 'delete the previous transient copy unless undo owns it' || setter.destinationPermissions !== 'read-only' || setter.archiveNameCollision !== 'append a positive integer before the extension' || setter.undoRedo !== 'Copy/Paste move or copy transient files while preserving per-property ownership') fail('setter and ownership semantics are incomplete')
|
|
const storage = report.storage
|
|
if (storage?.xmlElement !== 'FileIncluded' || storage.archiveAttribute !== 'file' || storage.archiveResource !== 'separate FCStd ZIP entry containing byte-identical file data' || storage.forceXmlAttribute !== 'data' || storage.forceXmlResource !== 'binary data embedded in the XML stream' || storage.emptyEncoding !== '<FileIncluded file=""/>' || storage.restoredValue !== 'document transient path; not a portable semantic path' || storage.browserBoundary !== 'project resource identity plus bytes; never expose or persist an ambient host path') fail('FCStd or browser storage boundary is incomplete')
|
|
|
|
const sources = new Map(await Promise.all((report.provenance?.sources ?? []).map(async ({ path }) => [path, await readFile(resolve(root, path), 'utf8')])))
|
|
const propertyHeader = sources.get('.cache/freecad/FreeCAD/src/App/PropertyFile.h') ?? ''
|
|
const propertySource = sources.get('.cache/freecad/FreeCAD/src/App/PropertyFile.cpp') ?? ''
|
|
const editorHeader = sources.get('.cache/freecad/FreeCAD/src/Gui/propertyeditor/PropertyItem.h') ?? ''
|
|
const editorSource = sources.get('.cache/freecad/FreeCAD/src/Gui/propertyeditor/PropertyItem.cpp') ?? ''
|
|
if (!propertyHeader.includes('class AppExport PropertyFileIncluded: public Property') || !propertyHeader.includes('return "Gui::PropertyEditor::PropertyTransientFileItem"') || !propertyHeader.includes("it's not allowed to write the file") || !propertySource.includes('TYPESYSTEM_SOURCE(App::PropertyFileIncluded, App::Property)') || !propertySource.includes('Not possible to set the same file!') || !propertySource.includes('str << "File " << file.filePath() << " does not exist."') || !propertySource.includes('PyTuple_Check(value)') || !propertySource.includes('PyDict_Check(value)') || !propertySource.includes('writer.isForceXML()') || !propertySource.includes('<FileIncluded data=') || !propertySource.includes('<FileIncluded file=') || !propertySource.includes('writer.addFile(file.fileName().c_str(), this)') || !propertySource.includes('reader.addFile(file.c_str(), this)') || !propertySource.includes('reader.readBinFile(_cValue.c_str())') || !propertySource.includes('PropertyFileIncluded::SaveDocFile()') || !propertySource.includes('PropertyFileIncluded::Copy()') || !propertySource.includes('PropertyFileIncluded::Paste(') || !editorHeader.includes('class GuiExport PropertyTransientFileItem: public PropertyItem') || !editorSource.includes('PROPERTYITEM_SOURCE(Gui::PropertyEditor::PropertyTransientFileItem)') || !editorSource.includes('Gui::FileChooser')) fail('locked native setter, FCStd, ownership or editor source changed')
|
|
for (const [path, snippets] of [
|
|
['.cache/freecad/FreeCAD/src/Mod/TechDraw/App/DrawViewSection.cpp', ['if (prop == &FileHatchPattern)', 'replaceSvgIncluded(FileHatchPattern.getValue())', 'if (prop == &FileGeomPattern)', 'replacePatIncluded(FileGeomPattern.getValue())']],
|
|
['.cache/freecad/FreeCAD/src/Mod/TechDraw/App/DrawGeomHatch.cpp', ['if (prop == &FilePattern)', 'PatIncluded.setValue(newHatchFileName.c_str())']],
|
|
['.cache/freecad/FreeCAD/src/Mod/TechDraw/App/DrawHatch.cpp', ['if (prop == &HatchPattern)', 'SvgIncluded.setValue(newHatchFileName.c_str())']],
|
|
['.cache/freecad/FreeCAD/src/Mod/TechDraw/App/DrawSVGTemplate.cpp', ['if (prop == &Template && !isRestoring())', 'PageResult.setValue(newTemplateFileName.c_str())']],
|
|
['.cache/freecad/FreeCAD/src/Mod/TechDraw/App/DrawTileWeld.cpp', ['if (prop == &SymbolFile)', 'SymbolIncluded.setValue(newSymbolFile.c_str())']],
|
|
['.cache/freecad/FreeCAD/src/Mod/TechDraw/App/DrawViewImage.cpp', ['if (prop == &ImageFile)', 'ImageIncluded.setValue(newImageFile.c_str())']],
|
|
]) for (const snippet of snippets) if (!(sources.get(path) ?? '').includes(snippet)) fail(`locked derived-source contract changed in ${path}`)
|
|
|
|
console.log(JSON.stringify({ status: 'freecad-property-fileincluded-inventory-pass', propertyType: report.propertyType, recordCount: report.recordCount, objectTypeCount: report.objectTypeCount, writableByStatus: report.records.filter(({ valueModel }) => valueModel.writableByStatus).length, readOnlyRecords: 8, outputRecords: 1, directRecords: 6, derivedRecords: 14, storage: report.storage, classification: report.classification }, null, 2))
|