Skip to content

Instantly share code, notes, and snippets.

@andy0130tw
Last active August 3, 2024 19:05
Show Gist options
  • Save andy0130tw/6653ec76118c55440bcaa19e09fe0d79 to your computer and use it in GitHub Desktop.
Save andy0130tw/6653ec76118c55440bcaa19e09fe0d79 to your computer and use it in GitHub Desktop.
I cannot write something reliable so I write a fuzzer first ε(´。•᎑•`)っ 💕
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