/
Starolat
/
DeepDive
Обзор
Документация
Войти
/
Starolat
/
DeepDive
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
tests/binary-serializer.node-test.js
651 строка
20 KB
Starolat Sergei
feat: локальный критический путь (SYS-024) и календари Primavera в формате .dddb v2.7.0 (SYS-025)
18 июл 2026, 07:28
18 июл 2026, 07:28
2da8691
Код
Авторство
О чём код?
// @ts-check /** * @fileoverview Node.js standalone tests for BinarySerializer / BinaryDeserializer (Phase 1) * Run: node tests/binary-serializer.node-test.js * @version 0.1.0 */ import assert from "assert"; import fs from "fs"; import path from "path"; import { fileURLToPath } from "url"; // fzstd expects global.self in Node.js if (typeof global !== "undefined") { global.self = global; } import binarySerializer from "../js/services/BinarySerializer.js"; import binaryDeserializer from "../js/services/BinaryDeserializer.js"; import zipCompressionService from "../js/services/ZipCompressionService.js"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); let passed = 0; let failed = 0; async function test(name, fn) { try { await fn(); passed++; console.log(` ✓ ${name}`); } catch (err) { failed++; console.error(` ✗ ${name}`); console.error(` ${err.message}`); if (err.stack) { console.error( err.stack .split("\n") .slice(1, 3) .map((s) => ` ${s}`) .join("\n"), ); } } } console.log("\nBinarySerializer / BinaryDeserializer Tests\n"); // ============================================================================= // Helpers // ============================================================================= function createDatabaseIndexes() { const project = { projectId: 1, name: "Test Project", startDate: "2024-01-01", endDate: "2024-12-31", status: "active", dataDate: "2024-06-15", }; const wbsRoot = { wbsId: 1, projectId: 1, code: "1", name: "Root WBS", seqNum: 0, level: 0, scheduleType: "current", }; const wbsChild = { wbsId: 2, projectId: 1, parentId: 1, code: "1.1", name: "Child WBS", seqNum: 1, level: 1, scheduleType: "target", }; const codeType = { typeId: 100, name: "Location", maxLength: 20, isRequired: false, scope: "AS_Global", }; const codeValue = { valueId: 1000, typeId: 100, shortName: "ZONE_A", description: "Zone A (North)", color: "#FF6B6B", sortOrder: 1, }; const activity1 = { id: 1, scheduleType: "current", taskName: "First Activity", taskType: "TT_Task", startDate: "2024-03-01", endDate: "2024-03-15", actualStartDate: "2024-03-02", actualEndDate: "2024-03-14", duration: 15, totalFloat: 2, freeFloat: 1, wbsId: 2, projectId: 1, activityCode: "A001", cstrType: "CS_MEOA", cstrDate: "2024-03-20", cstrType2: "CS_MSOA", cstrDate2: "2024-03-05", totalManhours: 100, actualManhours: 40, remainingManhours: 60, }; const activity2 = { id: 2, scheduleType: "target", taskName: "Second Activity", taskType: "TT_LOE", startDate: "2024-04-01", endDate: "2024-04-10", actualStartDate: null, actualEndDate: null, duration: 10, totalFloat: 0, freeFloat: 0, wbsId: 2, projectId: 1, activityCode: "A002", totalManhours: 50, actualManhours: 50, remainingManhours: 0, }; const resource = { resourceId: 10, name: "Labor Resource", type: "labor", }; const assignment = { assignmentId: 100, activityId: 1, resourceId: 10, targetUnits: 120, actualUnits: 40, remainingUnits: 60, }; const relationship = { relationshipId: 500, successorId: 2, predecessorId: 1, type: "FS", lagDays: 1.5, }; const materialDelivery1 = { deliveryId: "mdl_1", materialCode: "M-001", materialName: "Cement", activityCode: "A001", activityId: 1, resourceId: 10, quantity: 100, unit: "т", deliveryStage: "object", deliveryDate: "2024-03-10", contractDate: "2024-03-05", forecastDate: "2024-03-08", sourceDocument: "ТН-123", status: "доставлено", supplier: "ООО Стройматериалы", matched: true, matchWarnings: [], }; const materialDelivery2 = { deliveryId: "mdl_2", materialCode: "M-002", materialName: "Unknown Material", activityCode: "A002", quantity: 50, unit: "м3", deliveryDate: "2024-04-05", matched: false, matchWarnings: ["RESOURCE_NOT_FOUND"], }; return { databaseId: "test-db", name: "Test Database", source: "api", createdAt: new Date().toISOString(), projectsById: new Map([[1, project]]), wbsById: new Map([ [1, wbsRoot], [2, wbsChild], ]), activitiesById: new Map([ [1, activity1], [2, activity2], ]), activitiesBySchedule: new Map([ ["current", new Map([[1, activity1]])], ["target", new Map([[2, activity2]])], ]), allActivityIds: new Int32Array([1, 2]), codeTypeById: new Map([[100, codeType]]), codeValueById: new Map([[1000, codeValue]]), codeValuesByType: new Map([[100, new Map([[1000, codeValue]])]]), activitiesByCodeValue: new Map([[1000, new Set([1])]]), activityCodesByType: new Map([[1, new Map([[100, 1000]])]]), resourcesById: new Map([[10, resource]]), assignmentsById: new Map([[100, assignment]]), assignmentsByActivity: new Map([[1, [assignment]]]), assignmentsByResource: new Map([[10, [assignment]]]), spread: { assignmentIds: new Int32Array([100, 100, 100]), weekIndices: new Int32Array([2800, 2801, 2802]), targetUnits: new Float64Array([40, 40, 40]), actualUnits: new Float64Array([10, 15, 15]), remainingUnits: new Float64Array([30, 25, 25]), }, spreadIndexByAssignment: new Map([[100, { start: 0, end: 3 }]]), spreadIndexByWeek: new Map([ [2800, { start: 0, end: 1 }], [2801, { start: 1, end: 2 }], [2802, { start: 2, end: 3 }], ]), weeks: [ { weekIndex: 2800, weekNumber: 10, year: 2024, weekStart: "2024-03-04", weekEnd: "2024-03-10", isPast: false, }, ], minWeekIndex: 2800, maxWeekIndex: 2802, relationships: [relationship], materialDeliveriesById: new Map([ ["mdl_1", materialDelivery1], ["mdl_2", materialDelivery2], ]), materialDeliveriesByActivity: new Map([[1, [materialDelivery1]]]), materialDeliveriesByResource: new Map([[10, [materialDelivery1]]]), materialDeliveriesByMaterialCode: new Map([ ["M-001", [materialDelivery1]], ["M-002", [materialDelivery2]], ]), materialDeliverySourcesById: new Map(), materialDeliveryQtyByWeek: new Map([[2810, 100]]), materialDeliveryQtyByMaterialWeek: new Map([ ["M-001", new Map([[2810, 100]])], ]), }; } // ============================================================================= // Tests // ============================================================================= await test("serialize returns valid .dddb ArrayBuffer", async () => { const indexes = createDatabaseIndexes(); const buffer = await binarySerializer.serialize(indexes); assert(buffer instanceof ArrayBuffer, "Expected ArrayBuffer"); assert(buffer.byteLength > 64, "Buffer too small"); const magic = new Uint8Array(buffer, 0, 4); assert.strictEqual( String.fromCharCode(...magic), "DDDB", "Magic should be DDDB", ); const version = new DataView(buffer).getUint32(4, true); assert.strictEqual(version, binarySerializer.getFormatVersion()); assert.strictEqual(version >> 16, 2, "Expected major format version 2"); }); await test("round-trip preserves activity count and fields", async () => { const indexes = createDatabaseIndexes(); const buffer = await binarySerializer.serialize(indexes); const parsed = await binaryDeserializer.deserialize(buffer); assert.strictEqual(parsed.activities.length, 2); const a1 = parsed.activities.find((a) => a.id === 1); assert(a1, "Activity 1 missing"); assert.strictEqual(a1.taskName, "First Activity"); assert.strictEqual(a1.scheduleType, "current"); assert.strictEqual(a1.startDate, "2024-03-01"); assert.strictEqual(a1.endDate, "2024-03-15"); assert.strictEqual(a1.duration, 15); assert.strictEqual(a1.totalFloat, 2); assert.strictEqual(a1.freeFloat, 1); assert.strictEqual(a1.taskType, "TT_Task"); assert.strictEqual(a1.startDate, "2024-03-01"); assert.strictEqual(a1.endDate, "2024-03-15"); assert.strictEqual(a1.actualStartDate, "2024-03-02"); assert.strictEqual(a1.actualEndDate, "2024-03-14"); assert.strictEqual(a1.cstrType, "CS_MEOA"); assert.strictEqual(a1.cstrDate, "2024-03-20"); assert.strictEqual(a1.cstrType2, "CS_MSOA"); assert.strictEqual(a1.cstrDate2, "2024-03-05"); assert.strictEqual(a1.wbsId, 2); assert.strictEqual(a1.projectId, 1); assert.strictEqual(a1.activityCode, "A001"); assert.strictEqual(a1.totalManhours, 100); assert.strictEqual(a1.actualManhours, 40); assert.strictEqual(a1.remainingManhours, 60); assert.strictEqual( "percentComplete" in a1, false, "percentComplete must not be serialized", ); const a2 = parsed.activities.find((a) => a.id === 2); assert(a2, "Activity 2 missing"); assert.strictEqual(a2.scheduleType, "target"); assert.strictEqual(a2.taskName, "Second Activity"); assert.strictEqual(a2.taskType, "TT_LOE"); assert.strictEqual(a2.actualStartDate, null); assert.strictEqual(a2.actualEndDate, null); assert.strictEqual(a2.cstrType, undefined); assert.strictEqual(a2.cstrDate, null); assert.strictEqual( "percentComplete" in a2, false, "percentComplete must not be serialized", ); }); await test("round-trip preserves metadata (projects, wbs, codes)", async () => { const indexes = createDatabaseIndexes(); const buffer = await binarySerializer.serialize(indexes); const parsed = await binaryDeserializer.deserialize(buffer); assert.strictEqual(parsed.projects.length, 1); const project = parsed.projects[0]; assert.strictEqual(project.projectId, "1"); assert.strictEqual(project.name, "Test Project"); assert.strictEqual(project.startDate, "2024-01-01"); assert.strictEqual(project.status, "active"); assert.strictEqual(parsed.wbs.length, 2); const root = parsed.wbs.find((w) => w.wbsId === 1); assert(root); assert.strictEqual(root.scheduleType, "current"); const child = parsed.wbs.find((w) => w.wbsId === 2); assert(child); assert.strictEqual(child.parentId, 1); assert.strictEqual(child.code, "1.1"); assert.strictEqual(child.scheduleType, "target"); assert.strictEqual(parsed.codeTypes.length, 1); assert.strictEqual(parsed.codeTypes[0].name, "Location"); assert.strictEqual(parsed.codeValues.length, 1); const cv = parsed.codeValues[0]; assert.strictEqual(cv.shortName, "ZONE_A"); assert.strictEqual(cv.description, "Zone A (North)"); assert.strictEqual(cv.color, "#FF6B6B"); }); await test("round-trip preserves resources, assignments and code assignments", async () => { const indexes = createDatabaseIndexes(); const buffer = await binarySerializer.serialize(indexes); const parsed = await binaryDeserializer.deserialize(buffer); assert.strictEqual(parsed.resources.length, 1); assert.strictEqual(parsed.resources[0].name, "Labor Resource"); assert.strictEqual(parsed.resources[0].type, "labor"); assert.strictEqual(parsed.resourceAssignments.length, 1); const ra = parsed.resourceAssignments[0]; assert.strictEqual(ra.assignmentId, 100); assert.strictEqual(ra.activityId, 1); assert.strictEqual(ra.resourceId, 10); assert.strictEqual(ra.targetUnits, 120); assert.strictEqual(ra.actualUnits, 40); assert.strictEqual(ra.remainingUnits, 60); assert.strictEqual(parsed.codeAssignments.length, 1); const ca = parsed.codeAssignments[0]; assert.strictEqual(ca.activityId, 1); assert.strictEqual(ca.codeTypeId, 100); assert.strictEqual(ca.codeValueId, 1000); }); await test("round-trip preserves spread data", async () => { const indexes = createDatabaseIndexes(); const buffer = await binarySerializer.serialize(indexes); const parsed = await binaryDeserializer.deserialize(buffer); assert.strictEqual(parsed.spread.assignmentIds.length, 3); assert.deepStrictEqual( Array.from(parsed.spread.assignmentIds), [100, 100, 100], ); assert.deepStrictEqual( Array.from(parsed.spread.weekIndices), [2800, 2801, 2802], ); assert.deepStrictEqual(Array.from(parsed.spread.actualUnits), [10, 15, 15]); }); await test("round-trip preserves relationships", async () => { const indexes = createDatabaseIndexes(); const buffer = await binarySerializer.serialize(indexes); const parsed = await binaryDeserializer.deserialize(buffer); assert.strictEqual(parsed.relationships.length, 1); const rel = parsed.relationships[0]; assert.strictEqual(rel.relationshipId, 500); assert.strictEqual(rel.successorId, 2); assert.strictEqual(rel.predecessorId, 1); assert.strictEqual(rel.type, "FS"); assert.strictEqual(rel.lagDays, 1.5); }); await test("round-trip preserves material deliveries", async () => { const indexes = createDatabaseIndexes(); const buffer = await binarySerializer.serialize(indexes); const parsed = await binaryDeserializer.deserialize(buffer); assert.strictEqual(parsed.materialDeliveries.length, 2); const d1 = parsed.materialDeliveries.find((d) => d.deliveryId === "mdl_1"); assert(d1, "Delivery mdl_1 missing"); assert.strictEqual(d1.materialCode, "M-001"); assert.strictEqual(d1.materialName, "Cement"); assert.strictEqual(d1.activityCode, "A001"); assert.strictEqual(d1.activityId, 1); assert.strictEqual(d1.resourceId, 10); assert.strictEqual(d1.quantity, 100); assert.strictEqual(d1.unit, "т"); assert.strictEqual(d1.deliveryStage, "object"); assert.strictEqual(d1.deliveryDate, "2024-03-10"); assert.strictEqual(d1.contractDate, "2024-03-05"); assert.strictEqual(d1.forecastDate, "2024-03-08"); assert.strictEqual(d1.sourceDocument, "ТН-123"); assert.strictEqual(d1.status, "доставлено"); assert.strictEqual(d1.supplier, "ООО Стройматериалы"); assert.strictEqual(d1.matched, true); assert.deepStrictEqual(d1.matchWarnings, []); const d2 = parsed.materialDeliveries.find((d) => d.deliveryId === "mdl_2"); assert(d2, "Delivery mdl_2 missing"); assert.strictEqual(d2.materialCode, "M-002"); assert.strictEqual(d2.activityCode, "A002"); assert.strictEqual(d2.quantity, 50); assert.strictEqual(d2.unit, "м3"); assert.strictEqual(d2.deliveryDate, "2024-04-05"); assert.strictEqual(d2.matched, false); assert.deepStrictEqual(d2.matchWarnings, ["RESOURCE_NOT_FOUND"]); assert.strictEqual(d2.activityId, undefined); assert.strictEqual(d2.resourceId, undefined); }); await test("round-trip works with empty relationships", async () => { const indexes = createDatabaseIndexes(); indexes.relationships = []; const buffer = await binarySerializer.serialize(indexes); const parsed = await binaryDeserializer.deserialize(buffer); assert.strictEqual(parsed.relationships.length, 0); }); await test("isDDDB and peekFormatVersion work on uncompressed buffer", async () => { const indexes = createDatabaseIndexes(); const buffer = await binarySerializer.serialize(indexes); assert(binaryDeserializer.isDDDB(buffer)); assert.strictEqual( binaryDeserializer.peekFormatVersion(buffer), binarySerializer.getFormatVersion(), ); }); await test("round-trip works through JSZip compression", async () => { const indexes = createDatabaseIndexes(); const uncompressed = await binarySerializer.serialize(indexes); const compressed = await zipCompressionService.compress( new Uint8Array(uncompressed), ); // Verify zip magic assert.strictEqual(compressed[0], 0x50); assert.strictEqual(compressed[1], 0x4b); assert.strictEqual(compressed[2], 0x03); assert.strictEqual(compressed[3], 0x04); assert(compressed.byteLength < uncompressed.byteLength); const parsed = await binaryDeserializer.deserialize( compressed.buffer.slice( compressed.byteOffset, compressed.byteOffset + compressed.byteLength, ), ); assert.strictEqual(parsed.activities.length, 2); assert.strictEqual(parsed.projects.length, 1); assert.strictEqual(parsed.wbs.length, 2); }); await test("legacy v2.2.0 file (Python converter era) is readable by BinaryDeserializer", async () => { const fixturePath = path.join( __dirname, "fixtures", "demo_legacy_v220.dddb.zst", ); const compressed = new Uint8Array(fs.readFileSync(fixturePath)); const parsed = await binaryDeserializer.deserialize( compressed.buffer.slice( compressed.byteOffset, compressed.byteOffset + compressed.byteLength, ), ); assert.strictEqual(parsed.header.magic, "DDDB"); assert.strictEqual(parsed.header.version, 0x00020200); assert.strictEqual(parsed.activities.length, 3); assert.strictEqual(parsed.wbs.length, 2); assert.strictEqual(parsed.codeTypes.length, 2); assert.strictEqual(parsed.resources.length, 3); assert.strictEqual(parsed.relationships.length, 1); // Check activity fields are reasonable const a1 = parsed.activities[0]; assert.strictEqual(typeof a1.id, "number"); assert.strictEqual(typeof a1.taskName, "string"); assert.strictEqual(a1.startDate, "2024-01-01"); }); await test("deserialize rejects invalid magic", async () => { const bad = new ArrayBuffer(64); new Uint8Array(bad).fill(0); await assert.rejects( () => binaryDeserializer.deserialize(bad), /Unknown file format/, ); }); await test("round-trip handles null strings and dates", async () => { const indexes = createDatabaseIndexes(); const a1 = indexes.activitiesById.get(1); assert(a1); a1.taskName = undefined; a1.activityCode = undefined; a1.startDate = null; a1.endDate = undefined; const buffer = await binarySerializer.serialize(indexes); const parsed = await binaryDeserializer.deserialize(buffer); const pa = parsed.activities.find((a) => a.id === 1); assert(pa); assert.strictEqual(pa.taskName, ""); assert.strictEqual(pa.activityCode, undefined); assert.strictEqual(pa.startDate, null); assert.strictEqual(pa.endDate, null); }); await test("round-trip handles empty database", async () => { const empty = createDatabaseIndexes(); empty.projectsById = new Map(); empty.wbsById = new Map(); empty.activitiesById = new Map(); empty.activitiesBySchedule = new Map([ ["current", new Map()], ["target", new Map()], ]); empty.allActivityIds = new Int32Array(0); empty.codeTypeById = new Map(); empty.codeValueById = new Map(); empty.codeValuesByType = new Map(); empty.activitiesByCodeValue = new Map(); empty.activityCodesByType = new Map(); empty.resourcesById = new Map(); empty.assignmentsById = new Map(); empty.assignmentsByActivity = new Map(); empty.assignmentsByResource = new Map(); empty.spread = { assignmentIds: new Int32Array(0), weekIndices: new Int32Array(0), targetUnits: new Float64Array(0), actualUnits: new Float64Array(0), remainingUnits: new Float64Array(0), }; empty.relationships = []; empty.materialDeliveriesById = new Map(); empty.materialDeliveriesByActivity = new Map(); empty.materialDeliveriesByResource = new Map(); empty.materialDeliveriesByMaterialCode = new Map(); empty.materialDeliveryQtyByWeek = new Map(); empty.materialDeliveryQtyByMaterialWeek = new Map(); const buffer = await binarySerializer.serialize(empty); const parsed = await binaryDeserializer.deserialize(buffer); assert.strictEqual(parsed.projects.length, 0); assert.strictEqual(parsed.activities.length, 0); assert.strictEqual(parsed.resources.length, 0); assert.strictEqual(parsed.spread.assignmentIds.length, 0); }); await test("round-trip handles all resource types", async () => { const indexes = createDatabaseIndexes(); indexes.resourcesById.set(11, { resourceId: 11, name: "Material", type: "material", }); indexes.resourcesById.set(12, { resourceId: 12, name: "Equipment", type: "nonlabor", }); const buffer = await binarySerializer.serialize(indexes); const parsed = await binaryDeserializer.deserialize(buffer); const material = parsed.resources.find((r) => r.resourceId === 11); const equipment = parsed.resources.find((r) => r.resourceId === 12); assert(material); assert(equipment); assert.strictEqual(material.type, "material"); assert.strictEqual(equipment.type, "nonlabor"); }); // ============================================================================= // Summary // ============================================================================= console.log(`\n${passed} passed, ${failed} failed`); process.exit(failed > 0 ? 1 : 0);