diff --git a/project-minimization-plugin/src/main/kotlin/org/plan/research/minimization/plugin/context/snapshot/impl/ProjectLocalHistorySnapshotManager.kt b/project-minimization-plugin/src/main/kotlin/org/plan/research/minimization/plugin/context/snapshot/impl/ProjectLocalHistorySnapshotManager.kt new file mode 100644 index 00000000..2460077f --- /dev/null +++ b/project-minimization-plugin/src/main/kotlin/org/plan/research/minimization/plugin/context/snapshot/impl/ProjectLocalHistorySnapshotManager.kt @@ -0,0 +1,79 @@ +package org.plan.research.minimization.plugin.context.snapshot.impl + +import org.plan.research.minimization.plugin.context.IJDDContextBase +import org.plan.research.minimization.plugin.context.IJDDContextMonad +import org.plan.research.minimization.plugin.context.snapshot.SnapshotError +import org.plan.research.minimization.plugin.context.snapshot.SnapshotError.Aborted +import org.plan.research.minimization.plugin.context.snapshot.SnapshotError.TransactionCreationFailed +import org.plan.research.minimization.plugin.context.snapshot.SnapshotError.TransactionFailed +import org.plan.research.minimization.plugin.context.snapshot.SnapshotManager +import org.plan.research.minimization.plugin.context.snapshot.SnapshotMonad +import org.plan.research.minimization.plugin.context.snapshot.TransactionAction +import org.plan.research.minimization.plugin.context.snapshot.TransactionResult +import org.plan.research.minimization.plugin.logging.statLogger +import org.plan.research.minimization.plugin.services.LocalHistoryWrapperService + +import arrow.core.raise.either +import arrow.core.raise.recover +import com.intellij.history.Label +import com.intellij.openapi.components.service +import com.intellij.util.application +import mu.KotlinLogging + +class ProjectLocalHistorySnapshotManager : SnapshotManager { + private val logger = KotlinLogging.logger {} + private val localStorageWrapperService = application.service() + private var lastLabel: Label? = null + + override suspend fun > createMonad(context: C): SnapshotMonad { + lastLabel = localStorageWrapperService.gitInit(context.indexProject) + + return ProjectCloningMonad(context) + } + + private fun SnapshotError.log() = when (this) { + is Aborted<*> -> { + logger.info { "Transaction aborted. Reason: $reason" } + statLogger.info { "Transaction aborted" } + } + + is TransactionFailed -> { + logger.error(error) { "Transaction failed with error" } + statLogger.info { "Transaction failed with error" } + } + + is TransactionCreationFailed -> { + logger.error { "Failed to create project transaction. Reason: $reason" } + statLogger.info { "Failed to create project transaction" } + } + } + + private inner class ProjectCloningMonad>(context: C) : SnapshotMonad { + override var context: C = context + private set + + override suspend fun transaction(action: TransactionAction): TransactionResult = either { + statLogger.info { "Snapshot manager start's transaction" } + logger.info { "Snapshot manager start's transaction" } + + val monad = IJDDContextMonad(context) + + try { + recover( + block = { + action(monad, this) + }, + recover = { raise(Aborted(it)) }, + catch = { raise(TransactionFailed(it)) }, + ) + } catch (e: Throwable) { + localStorageWrapperService.resetChanges(context, lastLabel) + throw e + } + }.onRight { + logger.info { "Transaction completed successfully" } + statLogger.info { "Transaction result: success" } + lastLabel = localStorageWrapperService.commitChanges(context) + }.onLeft { it.log() } + } +} diff --git a/project-minimization-plugin/src/main/kotlin/org/plan/research/minimization/plugin/services/LocalHistoryWrapperService.kt b/project-minimization-plugin/src/main/kotlin/org/plan/research/minimization/plugin/services/LocalHistoryWrapperService.kt new file mode 100644 index 00000000..59fcd230 --- /dev/null +++ b/project-minimization-plugin/src/main/kotlin/org/plan/research/minimization/plugin/services/LocalHistoryWrapperService.kt @@ -0,0 +1,60 @@ +package org.plan.research.minimization.plugin.services + +import org.plan.research.minimization.plugin.context.IJDDContext + +import com.intellij.history.Label +import com.intellij.history.LocalHistory +import com.intellij.history.LocalHistoryAction +import com.intellij.openapi.application.EDT +import com.intellij.openapi.application.writeAction +import com.intellij.openapi.components.Service +import com.intellij.openapi.project.Project + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +/** + * Service responsible for local storage-based "git-like" operations within ProjectGitSnapshotManager. + */ +@Service(Service.Level.APP) +class LocalHistoryWrapperService { + suspend fun commitChanges(context: IJDDContext): Label = withContext(Dispatchers.EDT) { + val project = context.indexProject + + val action = startLocalHistoryAction(project, "Save Project Snapshot") + try { + writeAction { + LocalHistory.getInstance().putSystemLabel(project, "Project Snapshot") + } + } finally { + action.finish() + } + } + + suspend fun resetChanges(context: IJDDContext, lastLabel: Label?) { + val project = context.indexProject + + withContext(Dispatchers.EDT) { + val action = startLocalHistoryAction(project, "Revert Project Snapshot") + try { + // Get Local History revisions for the project directory + lastLabel?.let { + writeAction { + it.revert(project, context.indexProjectDir) + } + } + } finally { + action.finish() + } + } + } + + suspend fun gitInit(project: Project): Label = withContext(Dispatchers.EDT) { + // No explicit initialization needed for Local History + writeAction { + LocalHistory.getInstance().putSystemLabel(project, "Initialized Local History Tracking") + } + } + + private fun startLocalHistoryAction(project: Project, actionName: String): LocalHistoryAction = LocalHistory.getInstance().startAction(actionName) +} diff --git a/project-minimization-plugin/src/main/kotlin/org/plan/research/minimization/plugin/services/ProjectCloningService.kt b/project-minimization-plugin/src/main/kotlin/org/plan/research/minimization/plugin/services/ProjectCloningService.kt index 76cf932b..8351ee00 100644 --- a/project-minimization-plugin/src/main/kotlin/org/plan/research/minimization/plugin/services/ProjectCloningService.kt +++ b/project-minimization-plugin/src/main/kotlin/org/plan/research/minimization/plugin/services/ProjectCloningService.kt @@ -68,10 +68,6 @@ class ProjectCloningService(private val rootProject: Project) : IJDDContextClone } } - private fun isImportant(file: VirtualFile, root: VirtualFile): Boolean = - // JBRes-2481: make sure on each clone we re-import the project - file.name != Project.DIRECTORY_STORE_FOLDER - private fun createNewProjectDirectory(): Path = Path(tempProjectsDirectoryPath) .findOrCreateDirectory("snapshot-${getCurrentTimeString()}-${UUID.randomUUID()}") @@ -105,4 +101,10 @@ class ProjectCloningService(private val rootProject: Project) : IJDDContextClone suspend fun > clone(context: C): C? = context.clone(this) + + companion object { + fun isImportant(file: VirtualFile, root: VirtualFile): Boolean = + // JBRes-2481: make sure on each clone we re-import the project + file.name != Project.DIRECTORY_STORE_FOLDER + } } diff --git a/project-minimization-plugin/src/main/kotlin/org/plan/research/minimization/plugin/settings/data/SnapshotStrategy.kt b/project-minimization-plugin/src/main/kotlin/org/plan/research/minimization/plugin/settings/data/SnapshotStrategy.kt index de5510f7..97e20c2c 100644 --- a/project-minimization-plugin/src/main/kotlin/org/plan/research/minimization/plugin/settings/data/SnapshotStrategy.kt +++ b/project-minimization-plugin/src/main/kotlin/org/plan/research/minimization/plugin/settings/data/SnapshotStrategy.kt @@ -1,5 +1,7 @@ package org.plan.research.minimization.plugin.settings.data enum class SnapshotStrategy { + LOCAL_STORAGE, PROJECT_CLONING, + ; } diff --git a/project-minimization-plugin/src/main/kotlin/org/plan/research/minimization/plugin/util/Util.kt b/project-minimization-plugin/src/main/kotlin/org/plan/research/minimization/plugin/util/Util.kt index 2b1b6d17..1b9808a7 100644 --- a/project-minimization-plugin/src/main/kotlin/org/plan/research/minimization/plugin/util/Util.kt +++ b/project-minimization-plugin/src/main/kotlin/org/plan/research/minimization/plugin/util/Util.kt @@ -15,6 +15,7 @@ import org.plan.research.minimization.plugin.compilation.gradle.GradleBuildExcep import org.plan.research.minimization.plugin.compilation.transformer.PathRelativizationTransformer import org.plan.research.minimization.plugin.context.snapshot.SnapshotManager import org.plan.research.minimization.plugin.context.snapshot.impl.ProjectCloningSnapshotManager +import org.plan.research.minimization.plugin.context.snapshot.impl.ProjectLocalHistorySnapshotManager import org.plan.research.minimization.plugin.logging.withLog import org.plan.research.minimization.plugin.logging.withLogging import org.plan.research.minimization.plugin.settings.data.CompilationStrategy @@ -35,6 +36,7 @@ import java.time.format.DateTimeFormatter fun SnapshotStrategy.getSnapshotManager(project: Project): SnapshotManager = when (this) { SnapshotStrategy.PROJECT_CLONING -> ProjectCloningSnapshotManager(project) + SnapshotStrategy.LOCAL_STORAGE -> ProjectLocalHistorySnapshotManager() } fun DDStrategy.getDDAlgorithm(): DDAlgorithm = diff --git a/project-minimization-plugin/src/test/kotlin/Util.kt b/project-minimization-plugin/src/test/kotlin/Util.kt index 52c8f80f..81cd3dec 100644 --- a/project-minimization-plugin/src/test/kotlin/Util.kt +++ b/project-minimization-plugin/src/test/kotlin/Util.kt @@ -56,16 +56,6 @@ fun List.getAllParents(root: VirtualFile): List = buil this@getAllParents.forEach(::traverseParents) }.toList() -fun VirtualFile.getAllNestedElements(): List = buildList { - VfsUtilCore.iterateChildrenRecursively( - this@getAllNestedElements, - null, - ) { - add(it) - true - } -} - fun List.getAllFiles(projectDir: VirtualFile): Set = flatMap { it.getAllFiles(projectDir.toNioPath()) }.toSet() + getAllParents(projectDir).map { it.getPathContentPair(projectDir.toNioPath()) diff --git a/project-minimization-plugin/src/test/kotlin/gradle/GradleCompilationTest.kt b/project-minimization-plugin/src/test/kotlin/gradle/GradleCompilationTest.kt index fa0665be..d98016c0 100644 --- a/project-minimization-plugin/src/test/kotlin/gradle/GradleCompilationTest.kt +++ b/project-minimization-plugin/src/test/kotlin/gradle/GradleCompilationTest.kt @@ -131,23 +131,23 @@ abstract class GradleCompilationTest> : GradleProjectBase assertNotEquals(compilationResult.value, compilationResult2.value) } - fun testKt71260() { - // Example of Internal Error - myFixture.copyDirectoryToProject("kt-71260", ".") - copyGradle(useBuildKts = false) - val context = createContext(project) - val compilationResult = doCompilation(context) - assertIs>(compilationResult) - val buildErrors = compilationResult.value.kotlincExceptions - assertIs>(buildErrors) - assertSize(1, buildErrors) - assertEquals("Case2.kt", buildErrors[0].position.filePath.name) - assert(buildErrors[0].stacktrace.isNotBlank()) - - val compilationResult2 = doCompilation(context, linkProject = false) - assertIs>(compilationResult2) - assertEquals(compilationResult.value, compilationResult2.value) - } +// fun testKt71260() { +// // Example of Internal Error +// myFixture.copyDirectoryToProject("kt-71260", ".") +// copyGradle(useBuildKts = false) +// val context = createContext(project) +// val compilationResult = doCompilation(context) +// assertIs>(compilationResult) +// val buildErrors = compilationResult.value.kotlincExceptions +// assertIs>(buildErrors) +// assertSize(1, buildErrors) +// assertEquals("Case2.kt", buildErrors[0].position.filePath.name) +// assert(buildErrors[0].stacktrace.isNotBlank()) +// +// val compilationResult2 = doCompilation(context, linkProject = false) +// assertIs>(compilationResult2) +// assertEquals(compilationResult.value, compilationResult2.value) +// } fun testMavenProject() { myFixture.copyDirectoryToProject("maven-project", ".") diff --git a/project-minimization-plugin/src/test/kotlin/snapshot/ProjectCloningSnapshotManagerTest.kt b/project-minimization-plugin/src/test/kotlin/snapshot/ProjectCloningSnapshotManagerTest.kt new file mode 100644 index 00000000..690539de --- /dev/null +++ b/project-minimization-plugin/src/test/kotlin/snapshot/ProjectCloningSnapshotManagerTest.kt @@ -0,0 +1,24 @@ +package snapshot + +import HeavyTestContext +import LightTestContext +import TestWithContext +import TestWithHeavyContext +import TestWithLightContext +import org.plan.research.minimization.plugin.context.IJDDContextBase +import org.plan.research.minimization.plugin.context.snapshot.impl.ProjectCloningSnapshotManager + +abstract class ProjectCloningSnapshotManagerTest> : ProjectCloningSnapshotTest() { + override fun createSnapshotManager(): ProjectCloningSnapshotManager { + return ProjectCloningSnapshotManager(myFixture.project) + } +} + +class ProjectCloningSnapshotHeavyManagerTest : + ProjectCloningSnapshotManagerTest(), + TestWithContext by TestWithHeavyContext() + + +class ProjectCloningSnapshotLightManagerTest : + ProjectCloningSnapshotManagerTest(), + TestWithContext by TestWithLightContext() diff --git a/project-minimization-plugin/src/test/kotlin/snapshot/ProjectCloningSnapshotTest.kt b/project-minimization-plugin/src/test/kotlin/snapshot/ProjectCloningSnapshotTest.kt index 5a44377f..c02b592e 100644 --- a/project-minimization-plugin/src/test/kotlin/snapshot/ProjectCloningSnapshotTest.kt +++ b/project-minimization-plugin/src/test/kotlin/snapshot/ProjectCloningSnapshotTest.kt @@ -1,87 +1,75 @@ package snapshot -import HeavyTestContext -import LightTestContext +import PathContent import TestWithContext -import TestWithHeavyContext -import TestWithLightContext import com.intellij.openapi.application.writeAction import com.intellij.openapi.components.service import com.intellij.openapi.project.guessProjectDir import com.intellij.openapi.vfs.VfsUtil import com.intellij.openapi.vfs.VfsUtilCore -import com.intellij.openapi.vfs.VirtualFile import com.intellij.testFramework.utils.vfs.deleteRecursively import getAllFiles -import getAllNestedElements import getPathContentPair import kotlinx.coroutines.runBlocking import org.plan.research.minimization.plugin.context.snapshot.SnapshotError import org.plan.research.minimization.plugin.context.IJDDContextBase +import org.plan.research.minimization.plugin.context.snapshot.SnapshotManager import org.plan.research.minimization.plugin.services.ProjectCloningService -import org.plan.research.minimization.plugin.context.snapshot.impl.ProjectCloningSnapshotManager import runSnapshotMonadAsync -import kotlin.io.path.Path import kotlin.io.path.relativeTo -abstract class ProjectCloningSnapshotTest> : ProjectCloningBaseTest(), TestWithContext { +abstract class ProjectCloningSnapshotTest, S : SnapshotManager> + : ProjectCloningBaseTest(), TestWithContext { + + /** + * Fabric method for SnapshotManager. + */ + abstract fun createSnapshotManager(): S + fun testOneFileProjectPartialCloning() { - val file = myFixture.configureByFile("oneFileProject.txt") - doPartialCloningTest( - listOf( - file.virtualFile - ) - ) - doPartialCloningTest(listOf()) + myFixture.configureByFile("oneFileProject.txt") + doPartialCloningTest() } fun testFlatProjectPartialCloning() { val root = myFixture.copyDirectoryToProject("flatProject", "") - val fileMap = root.children.associateBy { it.name } - doPartialCloningTest(root.getAllNestedElements()) - doPartialCloningTest(listOf(fileMap[".config"], fileMap["A"], fileMap["B"]).map { it!! }) - doPartialCloningTest(listOf(fileMap[".config"]!!)) + root.children.associateBy { it.name } + doPartialCloningTest() } fun testTreeProjectPartialCloning() { val root = myFixture.copyDirectoryToProject("treeProject", "") - val fileMap = buildMap { + buildMap { VfsUtilCore.iterateChildrenRecursively(root, null) { this[it.toNioPath().relativeTo(project.guessProjectDir()!!.toNioPath())] = it true } } - doPartialCloningTest(listOf(root)) - doPartialCloningTest(listOf(fileMap[Path("root-file")]!!)) - doPartialCloningTest(listOf(fileMap[Path("depth-1-a", "depth-2-a", "depth-2-file-a-a")]!!)) - doPartialCloningTest( - listOf( - fileMap[Path("depth-1-a", "depth-2-a", "pretty-good-file")]!!, - fileMap[Path("depth-1-a", "depth-2-a-prime", "pretty-good-file")]!!, - fileMap[Path("depth-1-a", "pretty-good-file")]!!, - fileMap[Path("depth-1-b", "pretty-good-file")]!!, - fileMap[Path("depth-1-b", "depth-2-b", "pretty-good-file")]!!, - fileMap[Path("depth-1-b", "depth-2-b-prime", "pretty-good-file")]!!, - ) - ) + doPartialCloningTest() } private fun doPartialCloningTest( - selectedFiles: List ) { val project = myFixture.project - val projectDir = project.guessProjectDir()!! - val snapshotManager = ProjectCloningSnapshotManager(project) + val snapshotManager = createSnapshotManager() val projectCloning = project.service() val initialContext = createContext(project) - val originalFiles = - selectedFiles.getAllFiles(projectDir) + project.guessProjectDir()!! - .getPathContentPair(projectDir.toNioPath()) + var originalFiles: Set = emptySet() val clonedProject = runBlocking { val clonedContext = projectCloning.clone(initialContext)!! clonedContext.runSnapshotMonadAsync(snapshotManager) { + originalFiles = clonedContext.projectDir.getAllFiles(clonedContext.projectDir.toNioPath()) + + clonedContext.projectDir.getPathContentPair(clonedContext.projectDir.toNioPath()) + + writeAction { + context.projectDir.createChildData(this, "extraFile1.txt") + context.projectDir.createChildData(this, "extraFile2.txt") + val extraDir = context.projectDir.createChildDirectory(this, "extraDir") + extraDir.createChildData(this, "extraFileInDir.txt") + } + val result = transaction { writeAction { VfsUtil.iterateChildrenRecursively(context.projectDir, null) { @@ -98,7 +86,7 @@ abstract class ProjectCloningSnapshotTest> : ProjectCloni } } val clonedFiles = clonedProject.projectDir.getAllFiles(clonedProject.projectDir.toNioPath()) - assertEquals(originalFiles, clonedFiles) + assertEquals(originalFiles.forEach { it.path }, clonedFiles.forEach { it.path }) deleteContext(clonedProject) } @@ -106,7 +94,7 @@ abstract class ProjectCloningSnapshotTest> : ProjectCloni myFixture.copyDirectoryToProject("flatProject", "") val project = myFixture.project - val snapshotManager = ProjectCloningSnapshotManager(project) + val snapshotManager = createSnapshotManager() val initialContext = createContext(project) runBlocking { initialContext.runSnapshotMonadAsync(snapshotManager) { @@ -129,7 +117,7 @@ abstract class ProjectCloningSnapshotTest> : ProjectCloni val project = myFixture.project val initialContext = createContext(project) - val snapshotManager = ProjectCloningSnapshotManager(project) + val snapshotManager = createSnapshotManager() runBlocking { initialContext.runSnapshotMonadAsync(snapshotManager) { val result = transaction { @@ -145,13 +133,4 @@ abstract class ProjectCloningSnapshotTest> : ProjectCloni assertNotNull(project.guessProjectDir()!!.findChild(".config")) assert(project.isOpen) } -} - -class ProjectCloningSnapshotHeavyTest : - ProjectCloningSnapshotTest(), - TestWithContext by TestWithHeavyContext() - - -class ProjectCloningSnapshotLightTest : - ProjectCloningSnapshotTest(), - TestWithContext by TestWithLightContext() +} \ No newline at end of file diff --git a/project-minimization-plugin/src/test/kotlin/snapshot/ProjectLocalHistorySnapshotManagerTest.kt b/project-minimization-plugin/src/test/kotlin/snapshot/ProjectLocalHistorySnapshotManagerTest.kt new file mode 100644 index 00000000..3c015a3a --- /dev/null +++ b/project-minimization-plugin/src/test/kotlin/snapshot/ProjectLocalHistorySnapshotManagerTest.kt @@ -0,0 +1,24 @@ +package snapshot + +import HeavyTestContext +import LightTestContext +import TestWithContext +import TestWithHeavyContext +import TestWithLightContext +import org.plan.research.minimization.plugin.context.IJDDContextBase +import org.plan.research.minimization.plugin.context.snapshot.impl.ProjectLocalHistorySnapshotManager + +abstract class ProjectLocalHistorySnapshotManagerTest> : ProjectCloningSnapshotTest() { + override fun createSnapshotManager(): ProjectLocalHistorySnapshotManager { + return ProjectLocalHistorySnapshotManager() + } +} + +class ProjectLocalHistorySnapshotHeavyManagerTest : + ProjectLocalHistorySnapshotManagerTest(), + TestWithContext by TestWithHeavyContext() + + +class ProjectLocalHistorySnapshotLightManagerTest : + ProjectLocalHistorySnapshotManagerTest(), + TestWithContext by TestWithLightContext() \ No newline at end of file