Last active
August 3, 2024 19:05
-
-
Save andy0130tw/6653ec76118c55440bcaa19e09fe0d79 to your computer and use it in GitHub Desktop.
I cannot write something reliable so I write a fuzzer first ε(´。•᎑•`)っ 💕
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { EditorState, Text } from '@codemirror/state' | |
import { changeTracker, lineStatistics, commit } from './lib/codemirror/experiments.js' | |
/** @typedef {{ len: number, count: number }} LineStat */ | |
function countSurrogates(/** @type {string} */ s) { | |
const regex = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g | |
let n = 0 | |
while (regex.exec(s)) n++ | |
return n | |
} | |
/** @param {LineStat[]} lines | |
* @param {import('@codemirror/state').Text} doc */ | |
function verifyIntegrity(lines, doc) { | |
if (lines.length != doc.lines) { | |
throw new Error(`Inconsistent state: Incorrect line count ${lines.length} != actual ${doc.lines}.`) | |
} | |
for (let i = 0; i < lines.length; i++) { | |
let a, b | |
a = lines[i].len | |
b = doc.line(i + 1).length | |
if (a != b) { | |
throw new Error(`Inconsistent state: Incorrect line length at #${i + 1}: ${a} != ${b}`) | |
} | |
a = lines[i].count | |
b = countSurrogates(doc.line(i + 1).text) | |
if (a != b) { | |
throw new Error(`Inconsistent state: Incorrect surrogate count at #${i + 1}: ${a} != ${b}`) | |
} | |
} | |
} | |
/** | |
* @param {{ len: number; }[]} arr | |
*/ | |
function debugStats(arr) { | |
return arr.map(({len}) => len < 0 ? '??' : len.toString().padStart(2)).join(', ') | |
} | |
/** | |
* @param {string[]} text | |
* @param {{from: number, to: number, insert?: string}[]} changes | |
* @param {number[]} assertKnowns */ | |
function testCase(text, changes, assertKnowns = []) { | |
const doc = Text.of(text) | |
let state0 = EditorState.create({doc, extensions: [changeTracker()]}) | |
state0 = state0.update({effects: commit.of(null)}).state | |
console.log('stats init :', debugStats(state0.field(lineStatistics).lines)) | |
for (const ch of changes) { | |
state0 = state0.update({changes: ch}).state | |
} | |
const tr = state0.update({effects: commit.of(null)}) | |
const { newDoc, state } = tr | |
const stats = state.field(lineStatistics) | |
console.log('stats before:', debugStats(stats._prim)) | |
console.log('stats after :', debugStats(stats.lines)) | |
verifyIntegrity(stats.lines, newDoc) | |
for (const a of assertKnowns) { | |
const stat = stats._prim[a] | |
if (!stat || stat.len < 0) { | |
throw new Error(`The line length #${a} is asserted to be known`) | |
} | |
} | |
console.log('----------------') | |
} | |
const docSample = ['aa', 'bbb', 'cccc'] | |
// 012 3456 789ab | |
// insert an NL within a line | |
// testCase(docSample, [{from: 1, to: 1, insert: '\n'}], [2, 3]) | |
// delete an NL between two lines | |
// testCase(docSample, [{from: 2, to: 3, insert: ''}], [1]) | |
// replace an NL between two lines | |
// testCase(docSample, [{from: 2, to: 3, insert: 'x'}], [1]) | |
// insert an NL at line end should not invalidate the next line | |
// testCase(docSample, [{from: 2, to: 2, insert: '\n'}], [0, 2, 3]) | |
// but inserting extra will | |
// testCase(docSample, [{from: 2, to: 2, insert: '\nxxx'}], [0, 3]) | |
// insert an NL at a line beginning should not invalidate that line | |
// testCase(docSample, [{from: 7, to: 7, insert: '\n'}], [0, 1, 3]) | |
// but inserting extra will | |
// testCase(docSample, [{from: 7, to: 7, insert: '\n\nxxx'}], [0, 1]) | |
// FIXME: replacing an NL should not result in invalidations | |
// testCase(docSample, [{from: 2, to: 3, insert: '\n'}], [2]) | |
fuzz() | |
function fuzz() { | |
// const doc0 = ['a', 'bb', 'ccc', 'dddd', '', 'e'] | |
// 01 234 5678 9abcd ef g | |
for (let i = 0; i < 10000; i++) { | |
const numLines = Math.floor(Math.random() * 6) + 6 | |
const doc0 = [...new Array(numLines)].map(() => Math.random() < .1 ? '' : 'a'.repeat(Math.floor(Math.random() * 9) + 1)) | |
const len0 = doc0.join('\n').length | |
const ops = Math.floor(Math.random() * 4) + 1 | |
const changes = [] | |
let len = len0 | |
for (let i = 0; i < ops; i++) { | |
let t = Math.random() | |
let a = Math.floor(Math.random() * len) | |
let b = t < .1 ? a : | |
t < .5 ? Math.max(0, Math.min(len, a + Math.floor(Math.random() * 5) - 10)) : | |
Math.floor(Math.random() * len) | |
if (a > b) { | |
[a, b] = [b, a] | |
} | |
const insertChoice = ['', '\n', 'xxx', 'xxx\n', '\nxxx', '\nxxx\n', 'xxx\nyyyyy'] | |
if (a == b) { | |
// force insertion if there is deletion range is empty | |
insertChoice.shift() | |
} | |
const insert = insertChoice[Math.floor(Math.random() * insertChoice.length)] | |
const change = {from: a, to: b, insert} | |
changes.push(change) | |
len += insert.length - (b - a) | |
} | |
console.log('Testing', changes, '...') | |
testCase(doc0, changes) | |
} | |
console.log('Finished!!') | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment