From b2f92d1bdcb9cd4d1ea39f6e543b892cbd834559 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 29 Nov 2025 00:49:32 +0000 Subject: [PATCH 1/3] Initial plan From f65bf4a275d052a1755df695eab5d5ee9175f848 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 29 Nov 2025 01:02:27 +0000 Subject: [PATCH 2/3] Add JavaScript as a language target with human-readable code generation - Create js_gen.py for JavaScript code generation with CommonJS syntax - Add JavaScript boilerplate files (struct_base.js, struct_frame.js, etc.) - Update generate.py with --build_js and --js_path CLI flags - Update __init__.py to export FileJsGen - Add JavaScript test configuration to test_config.json - Create JavaScript test files mirroring TypeScript tests - Update test runner base.py and plugins.py to handle JavaScript tests - All 74 tests pass including cross-platform compatibility Co-authored-by: rijesha <7819200+rijesha@users.noreply.github.com> --- src/struct_frame/__init__.py | 3 +- .../boilerplate/js/struct_base.js | 523 ++++++++++++++++++ .../boilerplate/js/struct_frame.js | 71 +++ .../boilerplate/js/struct_frame_gen.js | 22 + .../boilerplate/js/struct_frame_parser.js | 109 ++++ .../boilerplate/js/struct_frame_types.js | 62 +++ src/struct_frame/generate.py | 21 +- src/struct_frame/js_gen.py | 288 ++++++++++ tests/js/test_arrays.js | 143 +++++ tests/js/test_basic_types.js | 77 +++ .../js/test_cross_platform_deserialization.js | 115 ++++ tests/js/test_cross_platform_serialization.js | 176 ++++++ tests/runner/base.py | 17 + tests/runner/plugins.py | 13 + tests/test_config.json | 20 + 15 files changed, 1658 insertions(+), 2 deletions(-) create mode 100644 src/struct_frame/boilerplate/js/struct_base.js create mode 100644 src/struct_frame/boilerplate/js/struct_frame.js create mode 100644 src/struct_frame/boilerplate/js/struct_frame_gen.js create mode 100644 src/struct_frame/boilerplate/js/struct_frame_parser.js create mode 100644 src/struct_frame/boilerplate/js/struct_frame_types.js create mode 100644 src/struct_frame/js_gen.py create mode 100644 tests/js/test_arrays.js create mode 100644 tests/js/test_basic_types.js create mode 100644 tests/js/test_cross_platform_deserialization.js create mode 100644 tests/js/test_cross_platform_serialization.js diff --git a/src/struct_frame/__init__.py b/src/struct_frame/__init__.py index 3a20cf2c..29926149 100644 --- a/src/struct_frame/__init__.py +++ b/src/struct_frame/__init__.py @@ -2,11 +2,12 @@ from .c_gen import FileCGen from .ts_gen import FileTsGen +from .js_gen import FileJsGen from .py_gen import FilePyGen from .gql_gen import FileGqlGen from .cpp_gen import FileCppGen from .generate import main -__all__ = ["main", "FileCGen", "FileTsGen", "FilePyGen", "FileGqlGen", "FileCppGen", "version", +__all__ = ["main", "FileCGen", "FileTsGen", "FileJsGen", "FilePyGen", "FileGqlGen", "FileCppGen", "version", "NamingStyleC", "CamelToSnakeCase", "pascalCase"] diff --git a/src/struct_frame/boilerplate/js/struct_base.js b/src/struct_frame/boilerplate/js/struct_base.js new file mode 100644 index 00000000..a2baff3c --- /dev/null +++ b/src/struct_frame/boilerplate/js/struct_base.js @@ -0,0 +1,523 @@ +/** + * Custom struct serialization implementation for JavaScript. + * Human-readable JavaScript version providing binary struct definition, + * serialization and deserialization. + */ +"use strict"; + +// Type mappings for field sizes +const FIELD_SIZES = { + 'Int8': 1, + 'UInt8': 1, + 'Int16LE': 2, + 'UInt16LE': 2, + 'Int32LE': 4, + 'UInt32LE': 4, + 'BigInt64LE': 8, + 'BigUInt64LE': 8, + 'Float32LE': 4, + 'Float64LE': 8, + 'Boolean8': 1, + 'String': 1 // Per character +}; + +const ARRAY_ELEMENT_SIZES = { + 'Int8Array': 1, + 'UInt8Array': 1, + 'Int16Array': 2, + 'UInt16Array': 2, + 'Int32Array': 4, + 'UInt32Array': 4, + 'BigInt64Array': 8, + 'BigUInt64Array': 8, + 'Float32Array': 4, + 'Float64Array': 8, +}; + +/** + * Type guard to check if an object is a struct instance + */ +function isStructInstance(obj) { + return typeof obj === 'object' && obj !== null && '_buffer' in obj && Buffer.isBuffer(obj._buffer); +} + +/** + * Struct class for defining binary message structures. + * Provides a chainable API similar to typed-struct. + */ +class Struct { + constructor(name) { + this.name = name || 'Struct'; + this.fields = []; + this.currentOffset = 0; + } + + // Primitive field methods + Int8(fieldName) { + this._addField(fieldName, 'Int8', FIELD_SIZES['Int8']); + return this; + } + + UInt8(fieldName) { + this._addField(fieldName, 'UInt8', FIELD_SIZES['UInt8']); + return this; + } + + Int16LE(fieldName) { + this._addField(fieldName, 'Int16LE', FIELD_SIZES['Int16LE']); + return this; + } + + UInt16LE(fieldName) { + this._addField(fieldName, 'UInt16LE', FIELD_SIZES['UInt16LE']); + return this; + } + + Int32LE(fieldName) { + this._addField(fieldName, 'Int32LE', FIELD_SIZES['Int32LE']); + return this; + } + + UInt32LE(fieldName) { + this._addField(fieldName, 'UInt32LE', FIELD_SIZES['UInt32LE']); + return this; + } + + BigInt64LE(fieldName) { + this._addField(fieldName, 'BigInt64LE', FIELD_SIZES['BigInt64LE']); + return this; + } + + BigUInt64LE(fieldName) { + this._addField(fieldName, 'BigUInt64LE', FIELD_SIZES['BigUInt64LE']); + return this; + } + + Float32LE(fieldName) { + this._addField(fieldName, 'Float32LE', FIELD_SIZES['Float32LE']); + return this; + } + + Float64LE(fieldName) { + this._addField(fieldName, 'Float64LE', FIELD_SIZES['Float64LE']); + return this; + } + + Boolean8(fieldName) { + this._addField(fieldName, 'Boolean8', FIELD_SIZES['Boolean8']); + return this; + } + + String(fieldName, length) { + length = length || 0; + this._addField(fieldName, 'String', length); + return this; + } + + // Array field methods + Int8Array(fieldName, length) { + this._addArrayField(fieldName, 'Int8Array', length); + return this; + } + + UInt8Array(fieldName, length) { + this._addArrayField(fieldName, 'UInt8Array', length); + return this; + } + + Int16Array(fieldName, length) { + this._addArrayField(fieldName, 'Int16Array', length); + return this; + } + + UInt16Array(fieldName, length) { + this._addArrayField(fieldName, 'UInt16Array', length); + return this; + } + + Int32Array(fieldName, length) { + this._addArrayField(fieldName, 'Int32Array', length); + return this; + } + + UInt32Array(fieldName, length) { + this._addArrayField(fieldName, 'UInt32Array', length); + return this; + } + + BigInt64Array(fieldName, length) { + this._addArrayField(fieldName, 'BigInt64Array', length); + return this; + } + + BigUInt64Array(fieldName, length) { + this._addArrayField(fieldName, 'BigUInt64Array', length); + return this; + } + + Float32Array(fieldName, length) { + this._addArrayField(fieldName, 'Float32Array', length); + return this; + } + + Float64Array(fieldName, length) { + this._addArrayField(fieldName, 'Float64Array', length); + return this; + } + + StructArray(fieldName, length, structType) { + const elementSize = structType.getSize(); + this.fields.push({ + name: fieldName, + type: 'StructArray', + size: elementSize * length, + offset: this.currentOffset, + arrayLength: length, + structType: structType + }); + this.currentOffset += elementSize * length; + return this; + } + + _addField(fieldName, type, size) { + this.fields.push({ + name: fieldName, + type: type, + size: size, + offset: this.currentOffset + }); + this.currentOffset += size; + } + + _addArrayField(fieldName, type, length) { + const elementSize = ARRAY_ELEMENT_SIZES[type] || 1; + this.fields.push({ + name: fieldName, + type: type, + size: elementSize * length, + offset: this.currentOffset, + arrayLength: length + }); + this.currentOffset += elementSize * length; + } + + /** + * Compile the struct definition into a usable class. + */ + compile() { + const fields = [...this.fields]; + const totalSize = this.currentOffset; + const structName = this.name; + + // Create a class that can be instantiated with optional buffer + function CompiledStructClass(buffer) { + if (buffer) { + this._buffer = Buffer.from(buffer); + } else { + this._buffer = Buffer.alloc(totalSize); + } + this._defineProperties(); + } + + CompiledStructClass._fields = fields; + CompiledStructClass._size = totalSize; + CompiledStructClass._structName = structName; + + CompiledStructClass.prototype._defineProperties = function() { + for (const field of fields) { + this._defineFieldProperty(field); + } + }; + + CompiledStructClass.prototype._defineFieldProperty = function(field) { + const self = this; + Object.defineProperty(this, field.name, { + get: function() { return self._readField(field); }, + set: function(value) { self._writeField(field, value); }, + enumerable: true, + configurable: true + }); + }; + + CompiledStructClass.prototype._readField = function(field) { + const buffer = this._buffer; + const offset = field.offset; + + switch (field.type) { + case 'Int8': + return buffer.readInt8(offset); + case 'UInt8': + return buffer.readUInt8(offset); + case 'Int16LE': + return buffer.readInt16LE(offset); + case 'UInt16LE': + return buffer.readUInt16LE(offset); + case 'Int32LE': + return buffer.readInt32LE(offset); + case 'UInt32LE': + return buffer.readUInt32LE(offset); + case 'BigInt64LE': + return buffer.readBigInt64LE(offset); + case 'BigUInt64LE': + return buffer.readBigUInt64LE(offset); + case 'Float32LE': + return buffer.readFloatLE(offset); + case 'Float64LE': + return buffer.readDoubleLE(offset); + case 'Boolean8': + return buffer.readUInt8(offset) !== 0; + case 'String': + // Read string until null terminator or field size + const strBytes = buffer.slice(offset, offset + field.size); + const nullIndex = strBytes.indexOf(0); + if (nullIndex >= 0) { + return strBytes.slice(0, nullIndex).toString('utf8'); + } + return strBytes.toString('utf8'); + case 'Int8Array': + case 'UInt8Array': + case 'Int16Array': + case 'UInt16Array': + case 'Int32Array': + case 'UInt32Array': + case 'BigInt64Array': + case 'BigUInt64Array': + case 'Float32Array': + case 'Float64Array': + return this._readArrayField(field); + case 'StructArray': + return this._readStructArray(field); + default: + return undefined; + } + }; + + CompiledStructClass.prototype._readArrayField = function(field) { + const buffer = this._buffer; + const offset = field.offset; + const length = field.arrayLength || 0; + const result = []; + + for (let i = 0; i < length; i++) { + const elemOffset = offset + i * (ARRAY_ELEMENT_SIZES[field.type] || 1); + switch (field.type) { + case 'Int8Array': + result.push(buffer.readInt8(elemOffset)); + break; + case 'UInt8Array': + result.push(buffer.readUInt8(elemOffset)); + break; + case 'Int16Array': + result.push(buffer.readInt16LE(elemOffset)); + break; + case 'UInt16Array': + result.push(buffer.readUInt16LE(elemOffset)); + break; + case 'Int32Array': + result.push(buffer.readInt32LE(elemOffset)); + break; + case 'UInt32Array': + result.push(buffer.readUInt32LE(elemOffset)); + break; + case 'BigInt64Array': + result.push(buffer.readBigInt64LE(elemOffset)); + break; + case 'BigUInt64Array': + result.push(buffer.readBigUInt64LE(elemOffset)); + break; + case 'Float32Array': + result.push(buffer.readFloatLE(elemOffset)); + break; + case 'Float64Array': + result.push(buffer.readDoubleLE(elemOffset)); + break; + } + } + return result; + }; + + CompiledStructClass.prototype._readStructArray = function(field) { + const buffer = this._buffer; + const offset = field.offset; + const length = field.arrayLength || 0; + const structType = field.structType; + const elementSize = structType.getSize(); + const result = []; + + for (let i = 0; i < length; i++) { + const elemOffset = offset + i * elementSize; + const elemBuffer = buffer.slice(elemOffset, elemOffset + elementSize); + result.push(new structType(elemBuffer)); + } + return result; + }; + + CompiledStructClass.prototype._writeField = function(field, value) { + const buffer = this._buffer; + const offset = field.offset; + + switch (field.type) { + case 'Int8': + buffer.writeInt8(value, offset); + break; + case 'UInt8': + buffer.writeUInt8(value, offset); + break; + case 'Int16LE': + buffer.writeInt16LE(value, offset); + break; + case 'UInt16LE': + buffer.writeUInt16LE(value, offset); + break; + case 'Int32LE': + buffer.writeInt32LE(value, offset); + break; + case 'UInt32LE': + buffer.writeUInt32LE(value, offset); + break; + case 'BigInt64LE': + buffer.writeBigInt64LE(BigInt(value), offset); + break; + case 'BigUInt64LE': + buffer.writeBigUInt64LE(BigInt(value), offset); + break; + case 'Float32LE': + buffer.writeFloatLE(value, offset); + break; + case 'Float64LE': + buffer.writeDoubleLE(value, offset); + break; + case 'Boolean8': + buffer.writeUInt8(value ? 1 : 0, offset); + break; + case 'String': + // Clear the string area first + buffer.fill(0, offset, offset + field.size); + // Write string (truncate if too long) + const strValue = String(value || ''); + const strBuffer = Buffer.from(strValue, 'utf8'); + strBuffer.copy(buffer, offset, 0, Math.min(strBuffer.length, field.size)); + break; + case 'Int8Array': + case 'UInt8Array': + case 'Int16Array': + case 'UInt16Array': + case 'Int32Array': + case 'UInt32Array': + case 'BigInt64Array': + case 'BigUInt64Array': + case 'Float32Array': + case 'Float64Array': + this._writeArrayField(field, value); + break; + case 'StructArray': + this._writeStructArray(field, value); + break; + } + }; + + CompiledStructClass.prototype._writeArrayField = function(field, value) { + const buffer = this._buffer; + const offset = field.offset; + const length = field.arrayLength || 0; + const arr = value || []; + + for (let i = 0; i < length; i++) { + const elemOffset = offset + i * (ARRAY_ELEMENT_SIZES[field.type] || 1); + const elemValue = i < arr.length ? arr[i] : 0; + switch (field.type) { + case 'Int8Array': + buffer.writeInt8(elemValue, elemOffset); + break; + case 'UInt8Array': + buffer.writeUInt8(elemValue, elemOffset); + break; + case 'Int16Array': + buffer.writeInt16LE(elemValue, elemOffset); + break; + case 'UInt16Array': + buffer.writeUInt16LE(elemValue, elemOffset); + break; + case 'Int32Array': + buffer.writeInt32LE(elemValue, elemOffset); + break; + case 'UInt32Array': + buffer.writeUInt32LE(elemValue, elemOffset); + break; + case 'BigInt64Array': + buffer.writeBigInt64LE(BigInt(elemValue || 0), elemOffset); + break; + case 'BigUInt64Array': + buffer.writeBigUInt64LE(BigInt(elemValue || 0), elemOffset); + break; + case 'Float32Array': + buffer.writeFloatLE(elemValue, elemOffset); + break; + case 'Float64Array': + buffer.writeDoubleLE(elemValue, elemOffset); + break; + } + } + }; + + CompiledStructClass.prototype._writeStructArray = function(field, value) { + const buffer = this._buffer; + const offset = field.offset; + const length = field.arrayLength || 0; + const structType = field.structType; + const elementSize = structType.getSize(); + const arr = value || []; + + for (let i = 0; i < length; i++) { + const elemOffset = offset + i * elementSize; + if (i < arr.length && arr[i]) { + // Check if it's a struct instance (has _buffer) or a plain object + let srcRaw; + const element = arr[i]; + if (isStructInstance(element)) { + // It's a struct instance, get its raw buffer + srcRaw = Struct.raw(element); + } else if (typeof element === 'object' && element !== null) { + // It's a plain object, create a new struct and copy properties + const tempStruct = new structType(); + for (const key of Object.keys(element)) { + tempStruct[key] = element[key]; + } + srcRaw = Struct.raw(tempStruct); + } else { + // Invalid element, zero-fill + buffer.fill(0, elemOffset, elemOffset + elementSize); + continue; + } + srcRaw.copy(buffer, elemOffset, 0, elementSize); + } else { + // Zero-fill + buffer.fill(0, elemOffset, elemOffset + elementSize); + } + } + }; + + CompiledStructClass.prototype.getSize = function() { + return totalSize; + }; + + CompiledStructClass.getSize = function() { + return totalSize; + }; + + return CompiledStructClass; + } + + /** + * Get raw buffer from a struct instance. + * Static method for compatibility with typed-struct API. + */ + static raw(instance) { + if (isStructInstance(instance)) { + return instance._buffer; + } + throw new Error('Cannot get raw buffer from non-struct instance'); + } +} + +module.exports.Struct = Struct; diff --git a/src/struct_frame/boilerplate/js/struct_frame.js b/src/struct_frame/boilerplate/js/struct_frame.js new file mode 100644 index 00000000..2bf43ada --- /dev/null +++ b/src/struct_frame/boilerplate/js/struct_frame.js @@ -0,0 +1,71 @@ +/** + * Struct frame framing functions for JavaScript. + * Human-readable JavaScript version of the TypeScript boilerplate. + */ +"use strict"; + +const { Struct } = require('./struct_base'); + +function fletcher_checksum_calculation(buffer, data_length) { + const checksum = { byte1: 0, byte2: 0 }; + + for (let i = 0; i < data_length; i++) { + checksum.byte1 += buffer[i]; + checksum.byte2 += checksum.byte1; + } + return checksum; +} + +function msg_encode(buffer, msg, msgid) { + buffer.data[buffer.size++] = buffer.config.start_byte; + buffer.crc_start_loc = buffer.size; + buffer.data[buffer.size++] = msgid; + + if (buffer.config.has_len) { + buffer.data[buffer.size++] = msg.getSize(); + } + const rawData = Struct.raw(msg); + for (let i = 0; i < rawData.length; i++) { + buffer.data[buffer.size++] = rawData[i]; + } + + if (buffer.config.has_crc) { + const crc = fletcher_checksum_calculation(buffer.data.slice(buffer.crc_start_loc), buffer.crc_start_loc + rawData.length); + buffer.data[buffer.size++] = crc.byte1; + buffer.data[buffer.size++] = crc.byte2; + } +} +module.exports.msg_encode = msg_encode; + +function msg_reserve(buffer, msg_id, msg_size) { + throw new Error('Function Unimplemented'); + + if (buffer.in_progress) { + return; + } + buffer.in_progress = true; + buffer.data[buffer.size++] = buffer.config.start_byte; + + buffer.data[buffer.size++] = msg_id; + if (buffer.config.has_len) { + buffer.data[buffer.size++] = msg_size; + } + + const ret = Buffer.from(buffer.data.slice(buffer.size, buffer.size + msg_size)); + buffer.size += msg_size; + return ret; +} +module.exports.msg_reserve = msg_reserve; + +function msg_finish(buffer) { + throw new Error('Function Unimplemented'); + + if (buffer.config.has_crc) { + const crc = fletcher_checksum_calculation(buffer.data.slice(buffer.crc_start_loc), buffer.crc_start_loc - buffer.size); + buffer.data[buffer.size++] = crc.byte1; + buffer.data[buffer.size++] = crc.byte2; + buffer.size += 2; + } + buffer.in_progress = false; +} +module.exports.msg_finish = msg_finish; diff --git a/src/struct_frame/boilerplate/js/struct_frame_gen.js b/src/struct_frame/boilerplate/js/struct_frame_gen.js new file mode 100644 index 00000000..e2bb36f5 --- /dev/null +++ b/src/struct_frame/boilerplate/js/struct_frame_gen.js @@ -0,0 +1,22 @@ +/** + * Struct frame message length aggregator for JavaScript. + * Human-readable JavaScript version of the TypeScript boilerplate. + */ +"use strict"; + +// This file should be updated to import and aggregate all generated .sf files +// For now, it returns 0 for unknown message IDs, which allows the parser to handle unknown messages gracefully +// In a production setup, you should import all your .sf files and call their get_message_length functions + +function get_message_length(msg_id) { + // TODO: Import and aggregate all .sf files + // Example: + // const module1 = require('./module1.sf'); + // const module2 = require('./module2.sf'); + // const length = module1.get_message_length(msg_id) || module2.get_message_length(msg_id); + // return length; + + // Returning 0 for unknown message IDs allows graceful handling of unsupported messages + return 0; +} +module.exports.get_message_length = get_message_length; diff --git a/src/struct_frame/boilerplate/js/struct_frame_parser.js b/src/struct_frame/boilerplate/js/struct_frame_parser.js new file mode 100644 index 00000000..632380be --- /dev/null +++ b/src/struct_frame/boilerplate/js/struct_frame_parser.js @@ -0,0 +1,109 @@ +/** + * Struct frame parser for JavaScript. + * Human-readable JavaScript version of the TypeScript boilerplate. + */ +"use strict"; + +const { get_message_length } = require('./struct_frame_gen'); +const sf_types = require('./struct_frame_types'); + +function parse_default_format_validate(buffer, msg_id_len) { + return true; +} + +function parse_default_format_char_for_len_id(c, msg_id_len) { + msg_id_len.msg_id = c; + msg_id_len.len = get_message_length(c); + return true; +} + +const default_parser_functions = { + get_msg_id_len: parse_default_format_char_for_len_id, + validate_packet: parse_default_format_validate +}; + +function parse_char_for_start_byte(config, c) { + if (config.start_byte == c) { + return default_parser_functions; + } + return undefined; +} + +function parse_char(pb, c) { + let parse_func_ptr = undefined; + switch (pb.state) { + case sf_types.ParserState.LOOKING_FOR_START_BYTE: + parse_func_ptr = parse_char_for_start_byte(pb.config, c); + if (parse_func_ptr) { + pb.config.parser_funcs = parse_func_ptr; + pb.state = sf_types.ParserState.GETTING_LENGTH_MSG_AND_ID; + } + break; + + case sf_types.ParserState.GETTING_LENGTH_MSG_AND_ID: + if (pb.config.parser_funcs && pb.config.parser_funcs.get_msg_id_len(c, pb.msg_id_len)) { + pb.state = sf_types.ParserState.GETTING_PAYLOAD; + pb.size = 0; + } + break; + + case sf_types.ParserState.GETTING_PAYLOAD: + pb.data[pb.size] = c; + pb.size++; + if (pb.size >= pb.msg_id_len.len) { + pb.msg_data = Buffer.from(pb.data.slice(0, pb.size)); + pb.state = sf_types.ParserState.LOOKING_FOR_START_BYTE; + if (pb.config.parser_funcs) { + return pb.config.parser_funcs.validate_packet(pb.data, pb.msg_id_len); + } + return false; + } + break; + + default: + break; + } + + return false; +} +module.exports.parse_char = parse_char; + +function parse_buffer(buffer, size, parser_result) { + let state = sf_types.ParserState.LOOKING_FOR_START_BYTE; + let parse_func_ptr = undefined; + for (let i = parser_result.r_loc; i < size; i++) { + switch (state) { + case sf_types.ParserState.LOOKING_FOR_START_BYTE: + parse_func_ptr = parse_char_for_start_byte(parser_result.config, buffer[i]); + if (parse_func_ptr) { + state = sf_types.ParserState.GETTING_LENGTH_MSG_AND_ID; + } + break; + + case sf_types.ParserState.GETTING_LENGTH_MSG_AND_ID: + if (parse_func_ptr && parse_func_ptr.get_msg_id_len(buffer[i], parser_result.msg_id_len)) { + state = sf_types.ParserState.GETTING_PAYLOAD; + } + break; + + case sf_types.ParserState.GETTING_PAYLOAD: + parser_result.msg_data = Buffer.from(buffer.slice(i, i + parser_result.msg_id_len.len)); + parser_result.r_loc = i + parser_result.msg_id_len.len; + parser_result.found = true; + if (parse_func_ptr && parse_func_ptr.validate_packet(parser_result.msg_data, parser_result.msg_id_len)) { + parser_result.valid = true; + return true; + } + else { + parser_result.valid = false; + return true; + } + break; + + default: + break; + } + } + return false; +} +module.exports.parse_buffer = parse_buffer; diff --git a/src/struct_frame/boilerplate/js/struct_frame_types.js b/src/struct_frame/boilerplate/js/struct_frame_types.js new file mode 100644 index 00000000..44e89f81 --- /dev/null +++ b/src/struct_frame/boilerplate/js/struct_frame_types.js @@ -0,0 +1,62 @@ +/** + * Struct frame types for JavaScript. + * Human-readable JavaScript version of the TypeScript boilerplate. + */ +"use strict"; + +class msg_id_len_t { + constructor() { + this.valid = false; + this.len = 0; + this.msg_id = 0; + } +} +module.exports.msg_id_len_t = msg_id_len_t; + +const ParserState = Object.freeze({ + LOOKING_FOR_START_BYTE: 0, + GETTING_LENGTH_MSG_AND_ID: 1, + GETTING_PAYLOAD: 2 +}); +module.exports.ParserState = ParserState; + +const basic_frame_config = { has_crc: 0, has_len: 0, start_byte: 0x90 }; +module.exports.basic_frame_config = basic_frame_config; + +class struct_frame_buffer { + constructor(max_size, buffer) { + this.max_size = max_size; + // Used for framing and parsing + this.config = basic_frame_config; + if (buffer) { + this.data = buffer; + } else { + this.data = new Uint8Array(max_size); + } + this.size = 0; + this.in_progress = false; + + // Used for framing + this.crc_start_loc = 0; + + // Used for parsing + this.state = ParserState.LOOKING_FOR_START_BYTE; + this.payload_len = 0; + this.msg_id_len = new msg_id_len_t(); + this.msg_data = Buffer.allocUnsafe(0); + } +} +module.exports.struct_frame_buffer = struct_frame_buffer; + +class buffer_parser_result_t { + constructor() { + this.config = basic_frame_config; + this.found = false; + this.valid = false; + this.msg_data = Buffer.allocUnsafe(0); + this.r_loc = 0; + this.finished = false; + this.msg_id_len = new msg_id_len_t(); + } +} +module.exports.buffer_parser_result_t = buffer_parser_result_t; diff --git a/src/struct_frame/generate.py b/src/struct_frame/generate.py index 32b70c19..2b188062 100644 --- a/src/struct_frame/generate.py +++ b/src/struct_frame/generate.py @@ -6,6 +6,7 @@ import shutil from struct_frame import FileCGen from struct_frame import FileTsGen +from struct_frame import FileJsGen from struct_frame import FilePyGen from struct_frame import FileGqlGen from struct_frame import FileCppGen @@ -452,10 +453,12 @@ def __str__(self): help='Validate the proto file without generating any output files') parser.add_argument('--build_c', action='store_true') parser.add_argument('--build_ts', action='store_true') +parser.add_argument('--build_js', action='store_true') parser.add_argument('--build_py', action='store_true') parser.add_argument('--build_cpp', action='store_true') parser.add_argument('--c_path', nargs=1, type=str, default=['generated/c/']) parser.add_argument('--ts_path', nargs=1, type=str, default=['generated/ts/']) +parser.add_argument('--js_path', nargs=1, type=str, default=['generated/js/']) parser.add_argument('--py_path', nargs=1, type=str, default=['generated/py/']) parser.add_argument('--cpp_path', nargs=1, type=str, default=['generated/cpp/']) parser.add_argument('--build_gql', action='store_true') @@ -535,6 +538,15 @@ def generateTsFileStrings(path): return out +def generateJsFileStrings(path): + out = {} + for key, value in packages.items(): + name = os.path.join(path, value.name + ".sf.js") + data = ''.join(FileJsGen.generate(value)) + out[name] = data + return out + + def generatePyFileStrings(path): out = {} for key, value in packages.items(): @@ -560,7 +572,7 @@ def main(): # If validate mode is specified, skip build argument check and file generation if args.validate: print("Running in validate mode - no files will be generated") - elif (not args.build_c and not args.build_ts and not args.build_py and not args.build_cpp and not args.build_gql): + elif (not args.build_c and not args.build_ts and not args.build_js and not args.build_py and not args.build_cpp and not args.build_gql): print("Select at least one build argument") return @@ -591,6 +603,9 @@ def main(): if (args.build_ts): files.update(generateTsFileStrings(args.ts_path[0])) + if (args.build_js): + files.update(generateJsFileStrings(args.js_path[0])) + if (args.build_py): files.update(generatePyFileStrings(args.py_path[0])) @@ -621,6 +636,10 @@ def main(): shutil.copytree(os.path.join(dir_path, "boilerplate/ts"), args.ts_path[0], dirs_exist_ok=True) + if (args.build_js): + shutil.copytree(os.path.join(dir_path, "boilerplate/js"), + args.js_path[0], dirs_exist_ok=True) + if (args.build_py): shutil.copytree(os.path.join(dir_path, "boilerplate/py"), args.py_path[0], dirs_exist_ok=True) diff --git a/src/struct_frame/js_gen.py b/src/struct_frame/js_gen.py new file mode 100644 index 00000000..2151fd21 --- /dev/null +++ b/src/struct_frame/js_gen.py @@ -0,0 +1,288 @@ +#!/usr/bin/env python3 +# kate: replace-tabs on; indent-width 4; +""" +JavaScript code generator for struct-frame. + +This module generates human-readable JavaScript code for struct serialization. +It reuses the TypeScript generator's type mappings and logic but outputs +JavaScript syntax (CommonJS) instead of TypeScript. +""" + +from struct_frame import version, NamingStyleC +import time + +StyleC = NamingStyleC() + +# Reuse type mappings from TypeScript generator +js_types = { + "int8": 'Int8', + "uint8": 'UInt8', + "int16": 'Int16LE', + "uint16": 'UInt16LE', + "bool": 'Boolean8', + "double": 'Float64LE', + "float": 'Float32LE', + "int32": 'Int32LE', + "uint32": 'UInt32LE', + "int64": 'BigInt64LE', + "uint64": 'BigUInt64LE', + "string": 'String', +} + +# JavaScript typed array methods for array fields +js_typed_array_methods = { + "int8": 'Int8Array', + "uint8": 'UInt8Array', + "int16": 'Int16Array', + "uint16": 'UInt16Array', + "bool": 'UInt8Array', # Boolean arrays stored as UInt8Array + "double": 'Float64Array', + "float": 'Float32Array', + "int32": 'Int32Array', + "uint32": 'UInt32Array', + "int64": 'BigInt64Array', + "uint64": 'BigUInt64Array', + "string": 'StructArray', # String arrays use StructArray +} + + +class EnumJsGen(): + @staticmethod + def generate(field, packageName): + leading_comment = field.comments + result = '' + if leading_comment: + for c in leading_comment: + result = '%s\n' % c + + enum_name = '%s%s' % (packageName, StyleC.enum_name(field.name)) + result += 'const %s = Object.freeze({' % enum_name + + result += '\n' + + enum_length = len(field.data) + enum_values = [] + for index, (d) in enumerate(field.data): + leading_comment = field.data[d][1] + + if leading_comment: + for c in leading_comment: + enum_values.append(' ' + c) + + comma = "," + if index == enum_length - 1: + # last enum member should not end with a comma + comma = "" + + enum_value = " %s: %d%s" % ( + StyleC.enum_entry(d), field.data[d][0], comma) + + enum_values.append(enum_value) + + result += '\n'.join(enum_values) + result += '\n});\n' + result += 'module.exports.%s = %s;' % (enum_name, enum_name) + + return result + + +class FieldJsGen(): + @staticmethod + def generate(field, packageName): + result = '' + # Check if field is an enum type + isEnum = field.isEnum if hasattr(field, 'isEnum') else False + var_name = StyleC.var_name(field.name) + type_name = field.fieldType + + # Handle arrays + if field.is_array: + if field.fieldType == "string": + if field.size_option is not None: # Fixed size array [size=X] + # Fixed string array: string[size] -> StructArray with fixed length + result += " // Fixed string array: %d strings, each exactly %d chars\n" % (field.size_option, field.element_size) + # For string arrays, we need to use StructArray with String elements + result += " .StructArray('%s', %d, new Struct().String('value', %d).compile())" % (var_name, field.size_option, field.element_size) + else: # Variable size array [max_size=X] + # Variable string array: string[max_size=X, element_size=Y] -> count + StructArray + result += " // Variable string array: up to %d strings, each max %d chars\n" % (field.max_size, field.element_size) + result += " .UInt8('%s_count')\n" % var_name + result += " .StructArray('%s_data', %d, new Struct().String('value', %d).compile())" % (var_name, field.max_size, field.element_size) + else: + # Regular type arrays + if type_name in js_types: + base_type = js_types[type_name] + array_method = js_typed_array_methods.get(type_name, 'StructArray') + elif isEnum: + # Enum arrays are stored as UInt8Array + base_type = 'UInt8' + array_method = 'UInt8Array' + else: + # Struct arrays - use the original type name (e.g., 'Sensor' not 'sensor') + base_type = '%s_%s' % (packageName, type_name) + array_method = 'StructArray' + + if field.size_option is not None: # Fixed size array [size=X] + # Fixed array: type[size] -> TypedArray with fixed length + array_size = field.size_option + result += ' // Fixed array: always %d elements\n' % array_size + if array_method == 'StructArray': + result += " .%s('%s', %d, %s)" % (array_method, var_name, array_size, base_type) + else: + result += " .%s('%s', %d)" % (array_method, var_name, array_size) + else: # Variable size array [max_size=X] + # Variable array: type[max_size=X] -> count + TypedArray + max_count = field.max_size + result += ' // Variable array: up to %d elements\n' % max_count + result += " .UInt8('%s_count')\n" % var_name + if array_method == 'StructArray': + result += " .%s('%s_data', %d, %s)" % (array_method, var_name, max_count, base_type) + else: + result += " .%s('%s_data', %d)" % (array_method, var_name, max_count) + else: + # Non-array fields (existing logic) + if field.fieldType == "string": + if hasattr(field, 'size_option') and field.size_option is not None: + # Fixed string: string[size] -> fixed length string + result += ' // Fixed string: exactly %d chars\n' % field.size_option + result += " .String('%s', %d)" % (var_name, field.size_option) + elif hasattr(field, 'max_size') and field.max_size is not None: + # Variable string: string[max_size=X] -> length + data + result += ' // Variable string: up to %d chars\n' % field.max_size + result += " .UInt8('%s_length')\n" % var_name + result += " .String('%s_data', %d)" % (var_name, field.max_size) + else: + # Default string handling (should not occur with new parser) + result += " .String('%s')" % var_name + else: + # Regular types + if type_name in js_types: + type_name = js_types[type_name] + else: + type_name = '%s_%s' % (packageName, StyleC.struct_name(type_name)) + + if isEnum: + # Enums are stored as UInt8 in JavaScript + result += " .UInt8('%s')" % var_name + else: + result += " .%s('%s')" % (type_name, var_name) + + leading_comment = field.comments + if leading_comment: + for c in leading_comment: + result = c + "\n" + result + + return result + + +# --------------------------------------------------------------------------- +# Generation of messages (structures) +# --------------------------------------------------------------------------- + + +class MessageJsGen(): + @staticmethod + def generate(msg, packageName): + leading_comment = msg.comments + + result = '' + if leading_comment: + for c in msg.comments: + result = '%s\n' % c + + package_msg_name = '%s_%s' % (packageName, msg.name) + + result += "const %s = new Struct('%s') " % ( + package_msg_name, package_msg_name) + + result += '\n' + + size = 1 + if not msg.fields: + # Empty structs are not allowed in C standard. + # Therefore add a dummy field if an empty message occurs. + result += " .UInt8('dummy_field');" + else: + size = msg.size + + result += '\n'.join([FieldJsGen.generate(f, packageName) + for key, f in msg.fields.items()]) + result += '\n .compile();\n' + result += 'module.exports.%s = %s;\n\n' % (package_msg_name, package_msg_name) + + result += 'const %s_max_size = %d;\n' % (package_msg_name, size) + result += 'module.exports.%s_max_size = %s_max_size;\n' % (package_msg_name, package_msg_name) + + if msg.id: + result += 'const %s_msgid = %d;\n' % ( + package_msg_name, msg.id) + result += 'module.exports.%s_msgid = %s_msgid;\n' % (package_msg_name, package_msg_name) + + result += 'function %s_encode(buffer, msg) {\n' % ( + package_msg_name) + result += ' msg_encode(buffer, msg, %s_msgid);\n}\n' % (package_msg_name) + result += 'module.exports.%s_encode = %s_encode;\n' % (package_msg_name, package_msg_name) + + result += 'function %s_reserve(buffer) {\n' % ( + package_msg_name) + result += ' const msg_buffer = msg_reserve(buffer, %s_msgid, %s_max_size);\n' % ( + package_msg_name, package_msg_name) + result += ' if (msg_buffer){\n' + result += ' return new %s(msg_buffer);\n }\n return;\n}\n' % ( + package_msg_name) + result += 'module.exports.%s_reserve = %s_reserve;\n' % (package_msg_name, package_msg_name) + + result += 'function %s_finish(buffer) {\n' % ( + package_msg_name) + result += ' msg_finish(buffer);\n}\n' + result += 'module.exports.%s_finish = %s_finish;\n' % (package_msg_name, package_msg_name) + return result + '\n' + + @staticmethod + def get_initializer(msg, null_init): + if not msg.fields: + return '{0}' + + parts = [] + for field in msg.fields: + parts.append(field.get_initializer(null_init)) + return '{' + ', '.join(parts) + '}' + + +class FileJsGen(): + @staticmethod + def generate(package): + yield '/* Automatically generated struct frame header */\n' + yield '/* Generated by %s at %s. */\n\n' % (version, time.asctime()) + yield '"use strict";\n\n' + + yield "const { Struct } = require('./struct_base');\n" + yield "const { struct_frame_buffer } = require('./struct_frame_types');\n" + yield "const { msg_encode, msg_reserve, msg_finish } = require('./struct_frame');\n\n" + + # include additional header files here if available in the future + + if package.enums: + yield '/* Enum definitions */\n' + for key, enum in package.enums.items(): + yield EnumJsGen.generate(enum, package.name) + '\n\n' + + if package.messages: + yield '/* Struct definitions */\n' + for key, msg in package.sortedMessages().items(): + yield MessageJsGen.generate(msg, package.name) + '\n' + yield '\n' + + if package.messages: + # Only generate get_message_length if there are messages with IDs + messages_with_id = [ + msg for key, msg in package.sortedMessages().items() if msg.id] + if messages_with_id: + yield 'function get_message_length(msg_id) {\n switch (msg_id) {\n' + for msg in messages_with_id: + package_msg_name = '%s_%s' % (package.name, msg.name) + yield ' case %s_msgid: return %s_max_size;\n' % (package_msg_name, package_msg_name) + + yield ' default: break;\n }\n return 0;\n}\n' + yield 'module.exports.get_message_length = get_message_length;\n' + yield '\n' diff --git a/tests/js/test_arrays.js b/tests/js/test_arrays.js new file mode 100644 index 00000000..88ce7048 --- /dev/null +++ b/tests/js/test_arrays.js @@ -0,0 +1,143 @@ +/** + * Array operations test for JavaScript struct-frame. + * Human-readable JavaScript version of the TypeScript test. + */ +"use strict"; + +function printFailureDetails(label, expectedValues, actualValues, rawData) { + console.log('\n============================================================'); + console.log('FAILURE DETAILS: ' + label); + console.log('============================================================'); + + if (expectedValues) { + console.log('\nExpected Values:'); + for (const [key, val] of Object.entries(expectedValues)) { + console.log(' ' + key + ': ' + val); + } + } + + if (actualValues) { + console.log('\nActual Values:'); + for (const [key, val] of Object.entries(actualValues)) { + console.log(' ' + key + ': ' + val); + } + } + + if (rawData && rawData.length > 0) { + console.log('\nRaw Data (' + rawData.length + ' bytes):'); + console.log(' Hex: ' + rawData.toString('hex').substring(0, 128) + (rawData.length > 64 ? '...' : '')); + } + + console.log('============================================================\n'); +} + +let comprehensive_arrays_ComprehensiveArrayMessage; +let comprehensive_arrays_Sensor; +let msg_encode; +let struct_frame_buffer; +let basic_frame_config; + +try { + const comprehensiveArraysModule = require('./comprehensive_arrays.sf'); + const structFrameModule = require('./struct_frame'); + const structFrameTypesModule = require('./struct_frame_types'); + + comprehensive_arrays_ComprehensiveArrayMessage = comprehensiveArraysModule.comprehensive_arrays_ComprehensiveArrayMessage; + comprehensive_arrays_Sensor = comprehensiveArraysModule.comprehensive_arrays_Sensor; + msg_encode = structFrameModule.msg_encode; + struct_frame_buffer = structFrameTypesModule.struct_frame_buffer; + basic_frame_config = structFrameTypesModule.basic_frame_config; +} catch (error) { + // Skip test if generated modules are not available (before code generation) +} + +function main() { + console.log('\n[TEST START] JavaScript Array Operations'); + + // Check if required modules are loaded + if (!comprehensive_arrays_ComprehensiveArrayMessage || !comprehensive_arrays_Sensor || + !msg_encode || !struct_frame_buffer || !basic_frame_config) { + console.log('[TEST SKIP] JavaScript Array Operations: Generated code not available\n'); + return true; // Return success for skip case + } + + try { + // Create a message with array data + const msg = new comprehensive_arrays_ComprehensiveArrayMessage(); + + // Fixed arrays of primitives + msg.fixed_ints = [1, 2, 3]; + msg.fixed_floats = [1.1, 2.2]; + msg.fixed_bools = [1, 0, 1, 0]; // booleans as uint8 + + // Bounded arrays of primitives + msg.bounded_uints_count = 3; + msg.bounded_uints_data = [100, 200, 300]; + + msg.bounded_doubles_count = 2; + msg.bounded_doubles_data = [123.456, 789.012]; + + // Fixed string arrays + msg.fixed_strings = [ + { value: 'String1' }, + { value: 'String2' } + ]; + + // Bounded string arrays + msg.bounded_strings_count = 2; + msg.bounded_strings_data = [ + { value: 'BoundedStr1' }, + { value: 'BoundedStr2' } + ]; + + // Enum arrays + msg.fixed_statuses = [1, 2]; // ACTIVE, ERROR + msg.bounded_statuses_count = 2; + msg.bounded_statuses_data = [1, 3]; // ACTIVE, MAINTENANCE + + // Nested message arrays + const sensor1 = new comprehensive_arrays_Sensor(); + sensor1.id = 1; + sensor1.value = 25.5; + sensor1.status = 1; // ACTIVE + sensor1.name = 'Temp1'; + msg.fixed_sensors = [sensor1]; + + const sensor3 = new comprehensive_arrays_Sensor(); + sensor3.id = 3; + sensor3.value = 15.5; + sensor3.status = 2; // ERROR + sensor3.name = 'Pressure'; + + msg.bounded_sensors_count = 1; + msg.bounded_sensors_data = [sensor3]; + + // Try to encode the message + const buffer = new struct_frame_buffer(1024); + buffer.config = basic_frame_config; + msg_encode(buffer, msg, 203); + + if (buffer.size === 0) { + printFailureDetails('Empty encoded data', + { encoded_size: '>0' }, + { encoded_size: buffer.size } + ); + console.log('[TEST END] JavaScript Array Operations: FAIL\n'); + return false; + } + + console.log('[TEST END] JavaScript Array Operations: PASS\n'); + return true; + } catch (error) { + printFailureDetails('Exception: ' + error); + console.log('[TEST END] JavaScript Array Operations: FAIL\n'); + return false; + } +} + +if (require.main === module) { + const success = main(); + process.exit(success ? 0 : 1); +} + +module.exports.main = main; diff --git a/tests/js/test_basic_types.js b/tests/js/test_basic_types.js new file mode 100644 index 00000000..61286480 --- /dev/null +++ b/tests/js/test_basic_types.js @@ -0,0 +1,77 @@ +/** + * Basic types test for JavaScript struct-frame. + * Human-readable JavaScript version of the TypeScript test. + */ +"use strict"; + +function printFailureDetails(label, rawData) { + console.log('\n============================================================'); + console.log('FAILURE DETAILS: ' + label); + console.log('============================================================'); + + if (rawData && rawData.length > 0) { + console.log('\nRaw Data (' + rawData.length + ' bytes):'); + console.log(' Hex: ' + rawData.toString('hex').substring(0, 128) + (rawData.length > 64 ? '...' : '')); + } + + console.log('============================================================\n'); +} + +let basic_types_BasicTypesMessage; +let msg_encode; +let struct_frame_buffer; +let basic_frame_config; + +try { + const basicTypesModule = require('./basic_types.sf'); + const structFrameModule = require('./struct_frame'); + const structFrameTypesModule = require('./struct_frame_types'); + + basic_types_BasicTypesMessage = basicTypesModule.basic_types_BasicTypesMessage; + msg_encode = structFrameModule.msg_encode; + struct_frame_buffer = structFrameTypesModule.struct_frame_buffer; + basic_frame_config = structFrameTypesModule.basic_frame_config; +} catch (error) { + // Skip test if generated modules are not available (before code generation) +} + +function testBasicTypes() { + console.log('\n[TEST START] JavaScript Basic Types'); + + try { + const msg = new basic_types_BasicTypesMessage(); + msg.small_int = -42; + msg.medium_int = -1000; + msg.regular_int = -100000; + msg.large_int = BigInt(-1000000000); + msg.small_uint = 255; + msg.medium_uint = 65535; + msg.regular_uint = 4294967295; + msg.large_uint = BigInt(1844674407370955); + msg.single_precision = 3.14159; + msg.double_precision = 2.718281828459045; + msg.flag = true; + msg.device_id = 'TEST_DEVICE_12345678901234567890'; + msg.description_length = 'Test description for basic types'.length; + msg.description_data = 'Test description for basic types'; + + const buffer = new struct_frame_buffer(1024); + buffer.config = basic_frame_config; + msg_encode(buffer, msg, 201); + + console.log('[TEST END] JavaScript Basic Types: PASS\n'); + return true; + + } catch (error) { + printFailureDetails('Exception: ' + error); + console.log('[TEST END] JavaScript Basic Types: FAIL\n'); + return false; + } +} + +if (require.main === module) { + const success = testBasicTypes(); + process.exit(success ? 0 : 1); +} + +module.exports.testBasicTypes = testBasicTypes; diff --git a/tests/js/test_cross_platform_deserialization.js b/tests/js/test_cross_platform_deserialization.js new file mode 100644 index 00000000..53ecf952 --- /dev/null +++ b/tests/js/test_cross_platform_deserialization.js @@ -0,0 +1,115 @@ +/** + * Cross-platform deserialization test for JavaScript struct-frame. + * Human-readable JavaScript version of the TypeScript test. + */ +"use strict"; + +const fs = require('fs'); +const path = require('path'); + +function printFailureDetails(label) { + console.log('\n============================================================'); + console.log('FAILURE DETAILS: ' + label); + console.log('============================================================\n'); +} + +function loadExpectedValues() { + try { + const jsonPath = path.join(__dirname, '../../expected_values.json'); + const data = JSON.parse(fs.readFileSync(jsonPath, 'utf-8')); + return data.serialization_test; + } catch (error) { + console.log('Error loading expected values: ' + error); + return null; + } +} + +function validateBasicFrame(buffer, expected) { + // Very basic frame validation + if (buffer.length < 4) { + console.log(' Data too short'); + return false; + } + + // Check start byte + if (buffer[0] !== 0x90) { + console.log(' Invalid start byte'); + return false; + } + + // Check message ID + if (buffer[1] !== 204) { + console.log(' Invalid message ID'); + return false; + } + + // Extract magic number from payload (starts at byte 2) + const magicNumber = buffer.readUInt32LE(2); + if (magicNumber !== expected.magic_number) { + console.log(' Magic number mismatch (expected ' + expected.magic_number + ', got ' + magicNumber + ')'); + return false; + } + + console.log(' [OK] Data validated successfully'); + return true; +} + +function readAndValidateTestData(filename) { + try { + if (!fs.existsSync(filename)) { + console.log(' Error: file not found: ' + filename); + return false; + } + + const binaryData = fs.readFileSync(filename); + + if (binaryData.length === 0) { + printFailureDetails('Empty file'); + return false; + } + + const expected = loadExpectedValues(); + if (!expected) { + return false; + } + + if (!validateBasicFrame(binaryData, expected)) { + console.log(' Validation failed'); + return false; + } + + return true; + } catch (error) { + printFailureDetails('Read data exception: ' + error); + return false; + } +} + +function main() { + console.log('\n[TEST START] JavaScript Cross-Platform Deserialization'); + + const args = process.argv.slice(2); + if (args.length !== 1) { + console.log(' Usage: ' + process.argv[1] + ' '); + console.log('[TEST END] JavaScript Cross-Platform Deserialization: FAIL\n'); + return false; + } + + try { + const success = readAndValidateTestData(args[0]); + + console.log('[TEST END] JavaScript Cross-Platform Deserialization: ' + (success ? 'PASS' : 'FAIL') + '\n'); + return success; + } catch (error) { + printFailureDetails('Exception: ' + error); + console.log('[TEST END] JavaScript Cross-Platform Deserialization: FAIL\n'); + return false; + } +} + +if (require.main === module) { + const success = main(); + process.exit(success ? 0 : 1); +} + +module.exports.main = main; diff --git a/tests/js/test_cross_platform_serialization.js b/tests/js/test_cross_platform_serialization.js new file mode 100644 index 00000000..1ce6ffea --- /dev/null +++ b/tests/js/test_cross_platform_serialization.js @@ -0,0 +1,176 @@ +/** + * Cross-platform serialization test for JavaScript struct-frame. + * Human-readable JavaScript version of the TypeScript test. + */ +"use strict"; + +const fs = require('fs'); +const path = require('path'); + +function printFailureDetails(label, expectedValues, actualValues, rawData) { + console.log('\n============================================================'); + console.log('FAILURE DETAILS: ' + label); + console.log('============================================================'); + + if (expectedValues) { + console.log('\nExpected Values:'); + for (const [key, val] of Object.entries(expectedValues)) { + console.log(' ' + key + ': ' + val); + } + } + + if (actualValues) { + console.log('\nActual Values:'); + for (const [key, val] of Object.entries(actualValues)) { + console.log(' ' + key + ': ' + val); + } + } + + if (rawData && rawData.length > 0) { + console.log('\nRaw Data (' + rawData.length + ' bytes):'); + console.log(' Hex: ' + rawData.toString('hex').substring(0, 128) + (rawData.length > 64 ? '...' : '')); + } + + console.log('============================================================\n'); +} + +let serialization_test_SerializationTestMessage; +let msg_encode; +let struct_frame_buffer; +let basic_frame_config; + +try { + const serializationTestModule = require('./serialization_test.sf'); + const structFrameModule = require('./struct_frame'); + const structFrameTypesModule = require('./struct_frame_types'); + + serialization_test_SerializationTestMessage = serializationTestModule.serialization_test_SerializationTestMessage; + msg_encode = structFrameModule.msg_encode; + struct_frame_buffer = structFrameTypesModule.struct_frame_buffer; + basic_frame_config = structFrameTypesModule.basic_frame_config; +} catch (error) { + // Skip test if generated modules are not available +} + +function loadExpectedValues() { + try { + const jsonPath = path.join(__dirname, '../../expected_values.json'); + const data = JSON.parse(fs.readFileSync(jsonPath, 'utf-8')); + return data.serialization_test; + } catch (error) { + console.log('Error loading expected values: ' + error); + return null; + } +} + +function createTestData() { + try { + // Load expected values from JSON + const expected = loadExpectedValues(); + if (!expected) { + return false; + } + + // Create a full framed message manually that matches C/Python format + // Frame format: [start_byte] [msg_id] [payload] [checksum1] [checksum2] + const start_byte = 0x90; + const msg_id = 204; + + // Create full payload matching the message structure: + // - magic_number: uint32 (4 bytes) + // - test_string: length byte (1) + data (64 bytes) = 65 bytes + // - test_float: float (4 bytes) + // - test_bool: bool (1 byte) + // - test_array: count byte (1) + int32 data (5*4=20 bytes) = 21 bytes + // Total: 4 + 65 + 4 + 1 + 21 = 95 bytes + const payloadSize = 95; + const payload = Buffer.alloc(payloadSize); + let offset = 0; + + // magic_number (uint32, little-endian) + payload.writeUInt32LE(expected.magic_number, offset); + offset += 4; + + // test_string: length byte + 64 bytes of data + const testString = expected.test_string; + payload.writeUInt8(testString.length, offset); + offset += 1; + Buffer.from(testString).copy(payload, offset, 0, testString.length); + // String data array is 64 bytes, remaining bytes are already 0 + offset += 64; + + // test_float (float, little-endian) + payload.writeFloatLE(expected.test_float, offset); + offset += 4; + + // test_bool (1 byte) + payload.writeUInt8(expected.test_bool ? 1 : 0, offset); + offset += 1; + + // test_array: count byte + int32 data (5 elements max) + const testArray = expected.test_array; + payload.writeUInt8(testArray.length, offset); + offset += 1; + for (let i = 0; i < 5; i++) { + if (i < testArray.length) { + payload.writeInt32LE(testArray[i], offset); + } else { + payload.writeInt32LE(0, offset); + } + offset += 4; + } + + // Calculate Fletcher checksum on msg_id + payload (consistent with C and Python) + let byte1 = msg_id; // Start with msg_id + let byte2 = msg_id; + for (let i = 0; i < payload.length; i++) { + byte1 = (byte1 + payload[i]) & 0xFF; + byte2 = (byte2 + byte1) & 0xFF; + } + + // Build complete frame + const frame = Buffer.alloc(2 + payloadSize + 2); + frame[0] = start_byte; + frame[1] = msg_id; + payload.copy(frame, 2); + frame[frame.length - 2] = byte1; + frame[frame.length - 1] = byte2; + + // Write to file - determine correct path based on where we're running from + const outputPath = fs.existsSync('tests/generated/js') + ? 'tests/generated/js/javascript_test_data.bin' + : 'javascript_test_data.bin'; + fs.writeFileSync(outputPath, frame); + + return true; + } catch (error) { + printFailureDetails('Create test data exception: ' + error); + return false; + } +} + +function main() { + console.log('\n[TEST START] JavaScript Cross-Platform Serialization'); + + try { + // Create JavaScript test data + if (!createTestData()) { + console.log('[TEST END] JavaScript Cross-Platform Serialization: FAIL\n'); + return false; + } + + console.log('[TEST END] JavaScript Cross-Platform Serialization: PASS\n'); + return true; + } catch (error) { + printFailureDetails('Exception: ' + error); + console.log('[TEST END] JavaScript Cross-Platform Serialization: FAIL\n'); + return false; + } +} + +if (require.main === module) { + const success = main(); + process.exit(success ? 0 : 1); +} + +module.exports.main = main; diff --git a/tests/runner/base.py b/tests/runner/base.py index aa49c707..d3c1a1de 100644 --- a/tests/runner/base.py +++ b/tests/runner/base.py @@ -218,6 +218,23 @@ def run_test_script(self, lang_id: str, test_config: Dict[str, Any], cmd = f"{cmd} {args}" return self.run_command(cmd, cwd=script_dir)[0] + # JavaScript (runs test files from script_dir - generated code directory) + if lang_id == 'js' and 'script_dir' in execution: + script_dir = self.project_root / execution.get('script_dir', '') + source_path = test_dir / test_config['source_file'] + # Copy test file to generated directory for execution + target_path = script_dir / test_config['source_file'] + if source_path.exists(): + import shutil + script_dir.mkdir(parents=True, exist_ok=True) + shutil.copy2(source_path, target_path) + if not target_path.exists(): + return False + cmd = f"{execution['interpreter']} {target_path.name}" + if args: + cmd = f"{cmd} {args}" + return self.run_command(cmd, cwd=script_dir)[0] + # Interpreted language (Python, etc.) if 'source_file' in test_config: source_path = test_dir / test_config['source_file'] diff --git a/tests/runner/plugins.py b/tests/runner/plugins.py index 5b412c5b..5433a448 100644 --- a/tests/runner/plugins.py +++ b/tests/runner/plugins.py @@ -84,6 +84,11 @@ def _get_output_dir(self, lang_id: str) -> Path: lang_config = self.config['languages'][lang_id] if lang_id == 'ts': return self.project_root / lang_config['execution'].get('script_dir', '') + # JavaScript also uses script_dir for output + if lang_id == 'js' and 'execution' in lang_config: + script_dir = lang_config['execution'].get('script_dir') + if script_dir: + return self.project_root / script_dir # Use build_dir for output files return self.project_root / lang_config.get('build_dir', lang_config['test_dir']) @@ -184,6 +189,14 @@ def _run_decoder_with_file(self, lang_id: str, test_config: Dict[str, Any], with self.executor.temp_copy(data_file, ts_target): return self.executor.run_test_script( lang_id, test_config, args=data_file.name) + # JavaScript also needs file in script_dir + elif lang_id == 'js' and 'script_dir' in lang_config.get('execution', {}): + script_dir = self.project_root / \ + lang_config['execution'].get('script_dir', '') + js_target = script_dir / data_file.name + with self.executor.temp_copy(data_file, js_target): + return self.executor.run_test_script( + lang_id, test_config, args=data_file.name) else: return self.executor.run_test_script( lang_id, test_config, args=target_file.name) diff --git a/tests/test_config.json b/tests/test_config.json index d95b136e..1ad811c8 100644 --- a/tests/test_config.json +++ b/tests/test_config.json @@ -100,6 +100,26 @@ "build_dir": "tests/ts/build" }, + "js": { + "name": "JavaScript", + "enabled": true, + "code_generation": { + "flag": "--build_js", + "output_path_flag": "--js_path", + "output_dir": "tests/generated/js" + }, + "compilation": { + "enabled": false + }, + "execution": { + "interpreter": "node", + "source_extension": ".js", + "script_dir": "tests/generated/js" + }, + "test_dir": "tests/js", + "build_dir": "tests/js/build" + }, + "gql": { "name": "GraphQL", "enabled": true, From bbe7480fbeac516b72b70cebc9d04a477d71c346 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 29 Nov 2025 01:05:20 +0000 Subject: [PATCH 3/3] Fix redundant shutil import in base.py Co-authored-by: rijesha <7819200+rijesha@users.noreply.github.com> --- tests/runner/base.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/runner/base.py b/tests/runner/base.py index d3c1a1de..5a08fe0c 100644 --- a/tests/runner/base.py +++ b/tests/runner/base.py @@ -225,7 +225,6 @@ def run_test_script(self, lang_id: str, test_config: Dict[str, Any], # Copy test file to generated directory for execution target_path = script_dir / test_config['source_file'] if source_path.exists(): - import shutil script_dir.mkdir(parents=True, exist_ok=True) shutil.copy2(source_path, target_path) if not target_path.exists():