mirror of
https://github.com/atom/atom.git
synced 2026-01-23 13:58:08 -05:00
Add randomized fuzz test for TextEditorPresenter
This test performs random operations on the editor and assigns random measurements from the view. After each operation, the state of a pre-existing presenter is compared with that of a new presenter created with the same parameters. Since it’s easier to reason about building fresh state than it is to reason about state updates, I hope this will catch any bugs in our update logic as we optimize it and explore every corner case.
This commit is contained in:
@@ -1,38 +1,41 @@
|
||||
_ = require 'underscore-plus'
|
||||
randomWords = require 'random-words'
|
||||
TextBuffer = require 'text-buffer'
|
||||
{Point, Range} = TextBuffer
|
||||
TextEditor = require '../src/text-editor'
|
||||
TextEditorPresenter = require '../src/text-editor-presenter'
|
||||
|
||||
describe "TextEditorPresenter", ->
|
||||
[buffer, editor] = []
|
||||
|
||||
beforeEach ->
|
||||
# These *should* be mocked in the spec helper, but changing that now would break packages :-(
|
||||
spyOn(window, "setInterval").andCallFake window.fakeSetInterval
|
||||
spyOn(window, "clearInterval").andCallFake window.fakeClearInterval
|
||||
|
||||
buffer = new TextBuffer(filePath: require.resolve('./fixtures/sample.js'))
|
||||
editor = new TextEditor({buffer})
|
||||
waitsForPromise -> buffer.load()
|
||||
|
||||
afterEach ->
|
||||
editor.destroy()
|
||||
buffer.destroy()
|
||||
|
||||
expectValues = (actual, expected) ->
|
||||
for key, value of expected
|
||||
expect(actual[key]).toEqual value
|
||||
|
||||
expectStateUpdate = (presenter, fn) ->
|
||||
updatedState = false
|
||||
disposable = presenter.onDidUpdateState ->
|
||||
updatedState = true
|
||||
disposable.dispose()
|
||||
fn()
|
||||
expect(updatedState).toBe true
|
||||
|
||||
# These `describe` and `it` blocks mirror the structure of the ::state object.
|
||||
# Please maintain this structure when adding specs for new state fields.
|
||||
describe "::state", ->
|
||||
[buffer, editor] = []
|
||||
|
||||
beforeEach ->
|
||||
# These *should* be mocked in the spec helper, but changing that now would break packages :-(
|
||||
spyOn(window, "setInterval").andCallFake window.fakeSetInterval
|
||||
spyOn(window, "clearInterval").andCallFake window.fakeClearInterval
|
||||
|
||||
buffer = new TextBuffer(filePath: require.resolve('./fixtures/sample.js'))
|
||||
editor = new TextEditor({buffer})
|
||||
waitsForPromise -> buffer.load()
|
||||
|
||||
afterEach ->
|
||||
editor.destroy()
|
||||
buffer.destroy()
|
||||
|
||||
expectValues = (actual, expected) ->
|
||||
for key, value of expected
|
||||
expect(actual[key]).toEqual value
|
||||
|
||||
expectStateUpdate = (presenter, fn) ->
|
||||
updatedState = false
|
||||
disposable = presenter.onDidUpdateState ->
|
||||
updatedState = true
|
||||
disposable.dispose()
|
||||
fn()
|
||||
expect(updatedState).toBe true
|
||||
|
||||
describe ".horizontalScrollbar", ->
|
||||
describe ".visible", ->
|
||||
it "is true if the scrollWidth exceeds the computed client width", ->
|
||||
@@ -1692,3 +1695,204 @@ describe "TextEditorPresenter", ->
|
||||
|
||||
expectStateUpdate presenter, -> editor.getBuffer().append("\n\n\n")
|
||||
expect(presenter.state.height).toBe editor.getScreenLineCount() * 20
|
||||
|
||||
describe "when the model and view measurements are mutated randomly", ->
|
||||
[editor, buffer, presenter, presenterParams] = []
|
||||
|
||||
it "correctly maintains the presenter state", ->
|
||||
_.times 10, ->
|
||||
waits(0)
|
||||
|
||||
runs ->
|
||||
buffer = new TextBuffer
|
||||
editor = new TextEditor({buffer})
|
||||
editor.setEditorWidthInChars(80)
|
||||
|
||||
presenterParams =
|
||||
model: editor
|
||||
height: 50
|
||||
contentFrameWidth: 300
|
||||
scrollTop: 0
|
||||
scrollLeft: 0
|
||||
lineHeight: 10
|
||||
baseCharacterWidth: 10
|
||||
lineOverdrawMargin: 1
|
||||
|
||||
presenter = new TextEditorPresenter(presenterParams)
|
||||
referencePresenter = new TextEditorPresenter(presenterParams)
|
||||
expect(presenter.state).toEqual referencePresenter.state
|
||||
|
||||
_.times 30, ->
|
||||
actions = []
|
||||
performRandomAction (action) -> actions.push(action)
|
||||
actualState = presenter.state
|
||||
expectedState = new TextEditorPresenter(presenterParams).state
|
||||
delete actualState.content.scrollingVertically
|
||||
delete expectedState.content.scrollingVertically
|
||||
|
||||
unless _.isEqual(actualState, expectedState)
|
||||
console.log "Prestenter states differ >>>>>>>>>>>>>>>>"
|
||||
console.log "Actual:", actualState
|
||||
console.log "Expected:", expectedState
|
||||
console.log "Uncomment code below this line to see a JSON diff"
|
||||
# {diff} = require 'json-diff' # !!! Run `npm install json-diff` in your `atom/` repository
|
||||
# console.log "Difference:", diff(actualState, expectedState)
|
||||
console.log ""
|
||||
console.log "Actions:"
|
||||
console.log action for action in actions
|
||||
console.log ""
|
||||
console.log "<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<"
|
||||
throw new Error("Unexpected presenter state after random mutation. Check console output for details.")
|
||||
|
||||
buffer.destroy()
|
||||
|
||||
performRandomAction = (log) ->
|
||||
getRandomElement([
|
||||
changeScrollLeft
|
||||
changeScrollTop
|
||||
toggleSoftWrap
|
||||
insertText
|
||||
changeCursors
|
||||
changeSelections
|
||||
changeLineDecorations
|
||||
])(log)
|
||||
|
||||
changeScrollTop = (log) ->
|
||||
scrollHeight = presenterParams.lineHeight * editor.getScreenLineCount()
|
||||
newScrollTop = Math.max(0, _.random(0, scrollHeight - presenterParams.height))
|
||||
log "Changing scrollTop: #{newScrollTop}"
|
||||
presenterParams.scrollTop = newScrollTop
|
||||
presenter.setScrollTop(newScrollTop)
|
||||
|
||||
changeScrollLeft = (log) ->
|
||||
scrollWidth = presenter.computeScrollWidth()
|
||||
newScrollLeft = Math.max(0, _.random(0, scrollWidth - presenterParams.contentFrameWidth))
|
||||
log "Changing scrollLeft: #{newScrollLeft}"
|
||||
presenterParams.scrollLeft = newScrollLeft
|
||||
presenter.setScrollLeft(newScrollLeft)
|
||||
|
||||
changeHeight = (log) ->
|
||||
scrollHeight = presenterParams.lineHeight * editor.getScreenLineCount()
|
||||
newHeight = _.random(30, scrollHeight * 1.5)
|
||||
log "Chaning height: #{newHeight}"
|
||||
presenterParams.height = newHeight
|
||||
presenter.setHeight(newHeight)
|
||||
|
||||
changeContentFrameWidth = (log) ->
|
||||
scrollWidth = presenter.computeScrollWidth()
|
||||
newContentFrameWidth = _.random(100, scrollWidth * 1.5)
|
||||
log "Chaning contentFrameWidth: #{newContentFrameWidth}"
|
||||
presenterParams.contentFrameWidth = newContentFrameWidth
|
||||
presenter.setContentFrameWidth(newContentFrameWidth)
|
||||
|
||||
toggleSoftWrap = (log) ->
|
||||
softWrapped = not editor.isSoftWrapped()
|
||||
log "Changing softWrapped: #{softWrapped}"
|
||||
editor.setSoftWrapped(softWrapped)
|
||||
|
||||
insertText = (log) ->
|
||||
range = buildRandomRange()
|
||||
text = buildRandomText()
|
||||
log "Inserting text in range #{range}: #{text}"
|
||||
editor.setTextInBufferRange(range, text)
|
||||
|
||||
changeCursors = (log) ->
|
||||
actions = [addCursor, moveCursor]
|
||||
actions.push(destroyCursor) if editor.getCursors().length > 1
|
||||
getRandomElement(actions)(log)
|
||||
|
||||
addCursor = (log) ->
|
||||
position = buildRandomPoint()
|
||||
log "Adding cursor at #{position}"
|
||||
editor.addCursorAtBufferPosition(position)
|
||||
|
||||
moveCursor = (log) ->
|
||||
position = buildRandomPoint()
|
||||
cursor = getRandomElement(editor.getCursors())
|
||||
log "Moving cursor from #{cursor.getBufferPosition()} to #{position}"
|
||||
cursor.selection.clear()
|
||||
cursor.setBufferPosition(position)
|
||||
|
||||
destroyCursor = (log) ->
|
||||
cursor = getRandomElement(editor.getCursors())
|
||||
log "Destroying cursor at #{cursor.getBufferPosition()}"
|
||||
cursor.destroy()
|
||||
|
||||
changeSelections = (log) ->
|
||||
actions = [addSelection, changeSelection]
|
||||
actions.push(destroySelection) if editor.getSelections().length > 1
|
||||
getRandomElement(actions)(log)
|
||||
|
||||
addSelection = (log) ->
|
||||
range = buildRandomRange()
|
||||
log "Adding selection at #{range}"
|
||||
editor.addSelectionForBufferRange(range)
|
||||
|
||||
changeSelection = (log) ->
|
||||
range = buildRandomRange()
|
||||
selection = getRandomElement(editor.getSelections())
|
||||
log "Changing selection from #{selection.getBufferRange()} to #{range}"
|
||||
selection.setBufferRange(range)
|
||||
|
||||
destroySelection = (log) ->
|
||||
selection = getRandomElement(editor.getSelections())
|
||||
log "Destroying selection at #{selection.getBufferRange()}"
|
||||
selection.destroy()
|
||||
|
||||
changeLineDecorations = (log) ->
|
||||
actions = [addLineDecoration]
|
||||
actions.push(changeLineDecoration, destroyLineDecoration) if editor.getLineDecorations().length > 0
|
||||
getRandomElement(actions)(log)
|
||||
|
||||
addLineDecoration = (log) ->
|
||||
range = buildRandomRange()
|
||||
options = {
|
||||
type: getRandomElement(['line', 'line-number'])
|
||||
class: randomWords(exactly: 1)[0]
|
||||
}
|
||||
if Math.random() > .2
|
||||
options.onlyEmpty = true
|
||||
else if Math.random() > .2
|
||||
options.onlyNonEmpty = true
|
||||
else if Math.random() > .2
|
||||
options.onlyHead = true
|
||||
|
||||
log "Adding line decoration at #{range}: #{JSON.stringify(options)}"
|
||||
|
||||
marker = editor.markBufferRange(range)
|
||||
editor.decorateMarker(marker, options)
|
||||
|
||||
changeLineDecoration = (log) ->
|
||||
decoration = getRandomElement(editor.getLineDecorations())
|
||||
marker = decoration.getMarker()
|
||||
range = buildRandomRange()
|
||||
log "Changing line decoration (#{JSON.stringify(decoration.getProperties())}) from #{marker.getBufferRange()} to #{range}"
|
||||
marker.setBufferRange(range)
|
||||
|
||||
destroyLineDecoration = (log) ->
|
||||
decoration = getRandomElement(editor.getLineDecorations())
|
||||
marker = decoration.getMarker()
|
||||
log "Destroying line decoration (#{JSON.stringify(decoration.getProperties())}) at #{marker.getBufferRange()}"
|
||||
decoration.destroy()
|
||||
|
||||
buildRandomPoint = ->
|
||||
row = _.random(0, buffer.getLastRow())
|
||||
column = _.random(0, buffer.lineForRow(row).length)
|
||||
new Point(row, column)
|
||||
|
||||
buildRandomRange = ->
|
||||
new Range(buildRandomPoint(), buildRandomPoint())
|
||||
|
||||
buildRandomText = ->
|
||||
text = []
|
||||
|
||||
_.times _.random(20, 60), ->
|
||||
if Math.random() < .2
|
||||
text += '\n'
|
||||
else
|
||||
text += " " if /\w$/.test(text)
|
||||
text += randomWords(exactly: 1)
|
||||
text
|
||||
|
||||
getRandomElement = (array) ->
|
||||
array[Math.floor(Math.random() * array.length)]
|
||||
|
||||
Reference in New Issue
Block a user