From 6b412f4c48f936f9897273060c5b04e8220d8848 Mon Sep 17 00:00:00 2001 From: peter Date: Thu, 19 Oct 2017 21:03:28 +0200 Subject: [PATCH] a property test for psi-document consistency, failing miserably so far --- jetCheck/src/jetCheck/Generator.java | 4 + .../psi/impl/PsiEventConsistencyTest.kt | 122 ++++++++++++++++++ 2 files changed, 126 insertions(+) create mode 100644 platform/platform-tests/testSrc/com/intellij/psi/impl/PsiEventConsistencyTest.kt diff --git a/jetCheck/src/jetCheck/Generator.java b/jetCheck/src/jetCheck/Generator.java index 05a01ee9b4d4..5aef3f6b34e2 100644 --- a/jetCheck/src/jetCheck/Generator.java +++ b/jetCheck/src/jetCheck/Generator.java @@ -212,6 +212,10 @@ public class Generator { return from(data -> data.drawInt()); } + public static Generator naturals() { + return integers(0, Integer.MAX_VALUE); + } + /** Generates integers in the given range (both ends inclusive) */ public static Generator integers(int min, int max) { IntDistribution distribution = IntDistribution.uniform(min, max); diff --git a/platform/platform-tests/testSrc/com/intellij/psi/impl/PsiEventConsistencyTest.kt b/platform/platform-tests/testSrc/com/intellij/psi/impl/PsiEventConsistencyTest.kt new file mode 100644 index 000000000000..e4b334fff6d0 --- /dev/null +++ b/platform/platform-tests/testSrc/com/intellij/psi/impl/PsiEventConsistencyTest.kt @@ -0,0 +1,122 @@ +// Copyright 2000-2017 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package com.intellij.psi.impl + +import com.intellij.lang.ASTFactory +import com.intellij.openapi.command.WriteCommandAction +import com.intellij.openapi.fileTypes.PlainTextLanguage +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiFileFactory +import com.intellij.psi.impl.source.CharTableImpl +import com.intellij.psi.impl.source.DummyHolder +import com.intellij.psi.impl.source.codeStyle.CodeEditUtil +import com.intellij.psi.impl.source.tree.* +import com.intellij.psi.tree.IElementType +import com.intellij.testFramework.LightPlatformTestCase +import com.intellij.testFramework.fixtures.LightPlatformCodeInsightFixtureTestCase +import jetCheck.Generator +import jetCheck.IntDistribution +import jetCheck.PropertyChecker +import one.util.streamex.IntStreamEx + +/** + * @author peter + */ +abstract class PsiEventConsistencyTest : LightPlatformCodeInsightFixtureTestCase() { + + fun testPsiDocSynchronization() { + PropertyChecker.forAll(commands()).shouldHold { cmd -> + val file = PsiFileFactory.getInstance(project).createFileFromText("a.txt", PlainTextLanguage.INSTANCE, "", true, false) + val document = file.viewProvider.document!! + WriteCommandAction.runWriteCommandAction(project) { + cmd.performChange(file) + assertEquals(document.text, file.text) + assertEquals(document.text, file.node.text) + } + true + } + } + + private interface AstCommand { + fun performChange(file: PsiFile) + } + + private data class CommandGroup(val inside: List): AstCommand { + override fun performChange(file: PsiFile) { + ChangeUtil.prepareAndRunChangeAction(ChangeUtil.ChangeAction { + inside.forEach { it.performChange(file) } + }, file.node as FileElement) + } + } + + private data class DeleteElement(val coord: NodeCoordinates): AstCommand { + override fun performChange(file: PsiFile) { + val node = coord.findNode(file) + node.treeParent?.deleteChildInternal(node) + } + } + + private data class ReplaceElement(val coord: NodeCoordinates, val replacement: TreeElement): AstCommand { + override fun performChange(file: PsiFile) { + val node = coord.findNode(file) + node.treeParent?.replaceChild(node, replacement) + } + } + + private data class AddElement(val coord: NodeCoordinates, val indexHint: Int, val toAdd : TreeElement): AstCommand { + override fun performChange(file: PsiFile) { + val composite = coord.findNode(file).let { it as? CompositeElement ?: it.treeParent } + val children = composite.getChildren(null) + val anchorIndex = indexHint % (children.size + 1) + composite.addChild(toAdd, if (anchorIndex == 0) null else children[anchorIndex - 1]) + } + } + + private data class NodeCoordinates(val offsetHint: Int, val depth: Int) { + fun findNode(file: PsiFile): TreeElement { + val node = file.node as FileElement + var elem : TreeElement = node.findLeafElementAt(offsetHint % (file.textLength + 1)) ?: return node + for (i in 0..depth) { + elem = elem.treeParent ?: break + } + return elem + } + } + + private val genCoords = Generator.zipWith(Generator.naturals(), Generator.integers(0, 5), ::NodeCoordinates) + private fun commands(): Generator = Generator.frequency( + 1, Generator.from { CommandGroup(Generator.listsOf(IntDistribution.uniform(1, 5), commands()).generateValue(it)) }, + 5, genCoords.flatMap { coords -> + Generator.anyOf( + Generator.constant(DeleteElement(coords)), + nodes.map { ReplaceElement(coords, it) }, + Generator.zipWith(nodes, Generator.naturals()) { n, i -> AddElement(coords, i, n) } + ) }) + + private val leafTypes = IntStreamEx.range(1, 5).mapToObj { i -> IElementType("Leaf" + i, null) }.toList() + private val compositeTypes = IntStreamEx.range(1, 5).mapToObj { i -> IElementType("Composite" + i, null) }.toList() + + private val leaves = Generator.zipWith(Generator.sampledFrom(leafTypes), Generator.asciiLetters()) { type, c -> + withDummyHolder(ASTFactory.leaf(type, c.toString())) } + + private val composites : Generator = Generator.sampledFrom(compositeTypes).flatMap { type -> + Generator.listsOf(IntDistribution.uniform(0, 5), nodes).map { children -> + val composite = withDummyHolder(object: CompositePsiElement(type) { + override fun toString(): String { + return super.toString() + children.toString() + } + }) + children.forEach(composite::addChild) + composite + } + } + private val nodes = Generator.frequency(4, leaves, 2, composites) + + private fun withDummyHolder(e: TreeElement): TreeElement { + DummyHolder(LightPlatformTestCase.getPsiManager(), e, null, CharTableImpl()) + CodeEditUtil.setNodeGenerated(e, true) + return e + } + +} + +