const fs = require('node:fs'); const path = require('node:path'); const assert = require('node:assert/strict'); const root = path.resolve(__dirname, '../..'); const files = [ '104/business/IEC104_Command.c', '104/business/IEC104_Command.h', 'examples/iec104_slave_example.c', 'test/iec104_command_test.c', ]; // Preserve literals and token boundaries while ignoring only comments/whitespace. function tokens(source) { const input = source.replace(/\\\r?\n/g, ''); const result = []; let offset = 0; while (offset < input.length) { if (/\s/.test(input[offset])) { offset++; continue; } if (input.startsWith('//', offset)) { const end = input.indexOf('\n', offset + 2); offset = end < 0 ? input.length : end + 1; continue; } if (input.startsWith('/*', offset)) { const end = input.indexOf('*/', offset + 2); assert(end >= 0, 'unterminated comment'); offset = end + 2; continue; } const tail = input.slice(offset); const literal = /^(?:u8|u|U|L)?(?:"(?:\\[\s\S]|[^"\\])*"|'(?:\\[\s\S]|[^'\\])*')/.exec(tail); const word = literal || /^(?:[A-Za-z_][A-Za-z_0-9]*|(?:[0-9]|\.[0-9])(?:[eEpP][+-]|[A-Za-z_0-9.])*)/.exec(tail); const token = word || /^(?:>>=|<<=|\.\.\.|->|\+\+|--|<<|>>|<=|>=|==|!=|&&|\|\||\*=|\/=|%=|\+=|-=|&=|\^=|\|=|##|.)/.exec(tail); assert(token, `cannot tokenize at ${offset}`); result.push(token[0]); offset += token[0].length; } return result; } for (const file of files) { const originalBytes = fs.readFileSync(path.join(__dirname, `${path.basename(file)}.before`)); const currentBytes = fs.readFileSync(path.join(root, file)); const decoder = new TextDecoder('utf-8', { fatal: true }); const original = decoder.decode(originalBytes); const current = decoder.decode(currentBytes); assert.deepEqual(tokens(current), tokens(original), `${file}: code tokens changed`); assert.equal(currentBytes.subarray(0, 3).equals(Buffer.from([0xef, 0xbb, 0xbf])), originalBytes.subarray(0, 3).equals(Buffer.from([0xef, 0xbb, 0xbf])), `${file}: BOM changed`); assert(!current.includes('\r'), `${file}: CR line endings`); assert(!current.includes('\ufffd'), `${file}: replacement character`); assert(!/[\t ]+$/m.test(current), `${file}: trailing whitespace`); assert(current.endsWith('\n'), `${file}: missing terminal newline`); console.log(`${file}: ${tokens(current).length} unchanged tokens, UTF-8/LF OK`); } // The loopback update only adds standalone line comments; compare all other bytes. const loopbackPath = 'test/slave_example_loopback_test.cjs'; const loopbackBefore = fs.readFileSync(path.join(__dirname, 'slave_example_loopback_test.cjs.before'), 'utf8'); const loopbackAfter = fs.readFileSync(path.join(root, loopbackPath), 'utf8'); const withoutCommentLines = (source) => source.split('\n').filter(line => !/^\s*\/\//.test(line)).join('\n'); assert.equal(withoutCommentLines(loopbackAfter), withoutCommentLines(loopbackBefore)); assert(!loopbackAfter.includes('\r') && !loopbackAfter.includes('\ufffd')); assert(!/[\t ]+$/m.test(loopbackAfter) && loopbackAfter.endsWith('\n')); new TextDecoder('utf-8', { fatal: true }).decode(fs.readFileSync(path.join(root, loopbackPath))); console.log(`${loopbackPath}: only standalone comments changed, UTF-8/LF OK`);