IDEA-CR-15578 final fix `rename A to B and B to A`
[idea/community.git] / platform / configuration-store-impl / testSrc / SchemeManagerTest.kt
1 /*
2  * Copyright 2000-2015 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.configurationStore
17
18 import com.intellij.openapi.application.ApplicationManager
19 import com.intellij.openapi.options.ExternalizableScheme
20 import com.intellij.openapi.options.SchemeManagerFactory
21 import com.intellij.openapi.util.io.FileUtil
22 import com.intellij.openapi.util.text.StringUtil
23 import com.intellij.testFramework.PlatformTestUtil
24 import com.intellij.testFramework.ProjectRule
25 import com.intellij.testFramework.TemporaryDirectory
26 import com.intellij.testFramework.runInEdtAndWait
27 import com.intellij.util.SmartList
28 import com.intellij.util.io.createDirectories
29 import com.intellij.util.io.directoryStreamIfExists
30 import com.intellij.util.io.readText
31 import com.intellij.util.io.write
32 import com.intellij.util.lang.CompoundRuntimeException
33 import com.intellij.util.loadElement
34 import com.intellij.util.toByteArray
35 import com.intellij.util.xmlb.XmlSerializer
36 import com.intellij.util.xmlb.annotations.Tag
37 import com.intellij.util.xmlb.serialize
38 import gnu.trove.THashMap
39 import org.assertj.core.api.Assertions.assertThat
40 import org.assertj.core.api.Assertions.assertThatThrownBy
41 import org.junit.ClassRule
42 import org.junit.Rule
43 import org.junit.Test
44 import java.io.File
45 import java.nio.file.Path
46 import java.util.function.Function
47
48 internal val FILE_SPEC = "REMOTE"
49
50 /**
51  * Functionality without stream provider covered, ICS has own test suite
52  */
53 internal class SchemeManagerTest {
54   companion object {
55     @JvmField
56     @ClassRule val projectRule = ProjectRule()
57   }
58
59   private val tempDirManager = TemporaryDirectory()
60   @Rule fun getTemporaryFolder() = tempDirManager
61
62   private var localBaseDir: Path? = null
63   private var remoteBaseDir: Path? = null
64
65   private fun getTestDataPath() = PlatformTestUtil.getCommunityPath().replace(File.separatorChar, '/') + "/platform/platform-tests/testData/options"
66
67   @Test fun loadSchemes() {
68     doLoadSaveTest("options1", "1->first;2->second")
69   }
70
71   @Test fun loadSimpleSchemes() {
72     doLoadSaveTest("options", "1->1")
73   }
74
75   @Test fun deleteScheme() {
76     val manager = createAndLoad("options1")
77     manager.removeScheme("first")
78     manager.save()
79
80     checkSchemes("2->second")
81   }
82
83   @Test fun renameScheme() {
84     val manager = createAndLoad("options1")
85
86     val scheme = manager.findSchemeByName("first")
87     assertThat(scheme).isNotNull()
88     scheme!!.name = "renamed"
89     manager.save()
90
91     checkSchemes("2->second;renamed->renamed")
92   }
93
94   @Test fun testRenameScheme2() {
95     val manager = createAndLoad("options1")
96
97     val first = manager.findSchemeByName("first")
98     assertThat(first).isNotNull()
99     assert(first != null)
100     first!!.name = "2"
101     val second = manager.findSchemeByName("second")
102     assertThat(second).isNotNull()
103     assert(second != null)
104     second!!.name = "1"
105     manager.save()
106
107     checkSchemes("1->1;2->2")
108   }
109
110   @Test fun testDeleteRenamedScheme() {
111     val manager = createAndLoad("options1")
112
113     val firstScheme = manager.findSchemeByName("first")
114     assertThat(firstScheme).isNotNull()
115     assert(firstScheme != null)
116     firstScheme!!.name = "first_renamed"
117     manager.save()
118
119     checkSchemes(remoteBaseDir!!.resolve("REMOTE"), "first_renamed->first_renamed;2->second", true)
120     checkSchemes(localBaseDir!!, "", false)
121
122     firstScheme.name = "first_renamed2"
123     manager.removeScheme(firstScheme)
124     manager.save()
125
126     checkSchemes(remoteBaseDir!!.resolve("REMOTE"), "2->second", true)
127     checkSchemes(localBaseDir!!, "", false)
128   }
129
130   @Test fun testDeleteAndCreateSchemeWithTheSameName() {
131     val manager = createAndLoad("options1")
132     val firstScheme = manager.findSchemeByName("first")
133     assertThat(firstScheme).isNotNull()
134
135     manager.removeScheme(firstScheme!!)
136     manager.addScheme(TestScheme("first"))
137     manager.save()
138     checkSchemes("2->second;first->first")
139   }
140
141   @Test fun testGenerateUniqueSchemeName() {
142     val manager = createAndLoad("options1")
143     val scheme = TestScheme("first")
144     manager.addNewScheme(scheme, false)
145
146     assertThat("first2").isEqualTo(scheme.name)
147   }
148
149   fun TestScheme.save(file: Path) {
150     file.write(serialize().toByteArray())
151   }
152
153   @Test fun `different extensions`() {
154     val dir = tempDirManager.newPath()
155
156     val scheme = TestScheme("local", "true")
157     scheme.save(dir.resolve("1.icls"))
158     TestScheme("local", "false").save(dir.resolve("1.xml"))
159
160     class ATestSchemesProcessor : TestSchemesProcessor(), SchemeExtensionProvider {
161       override val schemeExtension = ".icls"
162     }
163
164     val schemesManager = SchemeManagerImpl(FILE_SPEC, ATestSchemesProcessor(), null, dir)
165     schemesManager.loadSchemes()
166     assertThat(schemesManager.allSchemes).containsOnly(scheme)
167
168     assertThat(dir.resolve("1.icls")).isRegularFile()
169     assertThat(dir.resolve("1.xml")).isRegularFile()
170
171     scheme.data = "newTrue"
172     schemesManager.save()
173
174     assertThat(dir.resolve("1.icls")).isRegularFile()
175     assertThat(dir.resolve("1.xml")).doesNotExist()
176   }
177
178   @Test fun setSchemes() {
179     val dir = tempDirManager.newPath()
180     val schemeManager = createSchemeManager(dir)
181     schemeManager.loadSchemes()
182     assertThat(schemeManager.allSchemes).isEmpty()
183
184     val scheme = TestScheme("s1")
185     schemeManager.setSchemes(listOf(scheme))
186
187     val schemes = schemeManager.allSchemes
188     assertThat(schemes).containsOnly(scheme)
189
190     assertThat(dir.resolve("s1.xml")).doesNotExist()
191
192     scheme.data = "newTrue"
193     schemeManager.save()
194
195     assertThat(dir.resolve("s1.xml")).isRegularFile()
196
197     schemeManager.setSchemes(emptyList())
198
199     schemeManager.save()
200
201     assertThat(dir).doesNotExist()
202   }
203
204   @Test fun `reload schemes`() {
205     val dir = tempDirManager.newPath()
206     val schemeManager = createSchemeManager(dir)
207     schemeManager.loadSchemes()
208     assertThat(schemeManager.allSchemes).isEmpty()
209
210     val scheme = TestScheme("s1", "oldData")
211     schemeManager.setSchemes(listOf(scheme))
212     assertThat(schemeManager.allSchemes).containsOnly(scheme)
213     schemeManager.save()
214
215     dir.resolve("s1.xml").write("""<scheme name="s1" data="newData" />""")
216     schemeManager.reload()
217
218     assertThat(schemeManager.allSchemes).containsOnly(TestScheme("s1", "newData"))
219   }
220
221   @Test fun `save only if scheme differs from bundled`() {
222     val dir = tempDirManager.newPath()
223     var schemeManager = createSchemeManager(dir)
224     val bundledPath = "/com/intellij/configurationStore/bundledSchemes/default"
225     schemeManager.loadBundledScheme(bundledPath, this)
226     val customScheme = TestScheme("default")
227     assertThat(schemeManager.allSchemes).containsOnly(customScheme)
228
229     schemeManager.save()
230     assertThat(dir).doesNotExist()
231
232     schemeManager.save()
233     schemeManager.setSchemes(listOf(customScheme))
234     assertThat(dir).doesNotExist()
235
236     assertThat(schemeManager.allSchemes).containsOnly(customScheme)
237
238     customScheme.data = "foo"
239     schemeManager.save()
240     assertThat(dir.resolve("default.xml")).isRegularFile()
241
242     schemeManager = createSchemeManager(dir)
243     schemeManager.loadBundledScheme(bundledPath, this)
244     schemeManager.loadSchemes()
245
246     assertThat(schemeManager.allSchemes).containsOnly(customScheme)
247   }
248
249   @Test fun `don't remove dir if no schemes but at least one non-hidden file exists`() {
250     val dir = tempDirManager.newPath()
251     val schemeManager = createSchemeManager(dir)
252
253     val scheme = TestScheme("s1")
254     schemeManager.setSchemes(listOf(scheme))
255
256     schemeManager.save()
257
258     val schemeFile = dir.resolve("s1.xml")
259     assertThat(schemeFile).isRegularFile()
260
261     schemeManager.setSchemes(emptyList())
262
263     dir.resolve("empty").write(byteArrayOf())
264
265     schemeManager.save()
266
267     assertThat(schemeFile).doesNotExist()
268     assertThat(dir).isDirectory()
269   }
270
271   @Test fun `remove empty directory only if some file was deleted`() {
272     val dir = tempDirManager.newPath()
273     val schemeManager = createSchemeManager(dir)
274     schemeManager.loadSchemes()
275
276     dir.createDirectories()
277     schemeManager.save()
278     assertThat(dir).isDirectory()
279
280     schemeManager.addScheme(TestScheme("test"))
281     schemeManager.save()
282     assertThat(dir).isDirectory()
283
284     schemeManager.setSchemes(emptyList())
285     schemeManager.save()
286     assertThat(dir).doesNotExist()
287   }
288
289   @Test fun rename() {
290     val dir = tempDirManager.newPath()
291     val schemeManager = createSchemeManager(dir)
292     schemeManager.loadSchemes()
293     assertThat(schemeManager.allSchemes).isEmpty()
294
295     val scheme = TestScheme("s1")
296     schemeManager.setSchemes(listOf(scheme))
297
298     val schemes = schemeManager.allSchemes
299     assertThat(schemes).containsOnly(scheme)
300
301     assertThat(dir.resolve("s1.xml")).doesNotExist()
302
303     scheme.data = "newTrue"
304     schemeManager.save()
305
306     assertThat(dir.resolve("s1.xml")).isRegularFile()
307
308     scheme.name = "s2"
309
310     schemeManager.save()
311
312     assertThat(dir.resolve("s1.xml")).doesNotExist()
313     assertThat(dir.resolve("s2.xml")).isRegularFile()
314   }
315
316   @Test fun `rename A to B and B to A`() {
317     val dir = tempDirManager.newPath()
318     val schemeManager = createSchemeManager(dir)
319
320     val a = TestScheme("a", "a")
321     val b = TestScheme("b", "b")
322     schemeManager.setSchemes(listOf(a, b))
323     schemeManager.save()
324
325     assertThat(dir.resolve("a.xml")).isRegularFile()
326     assertThat(dir.resolve("b.xml")).isRegularFile()
327
328     a.name = "b"
329     b.name = "a"
330
331     schemeManager.save()
332
333     assertThat(dir.resolve("a.xml").readText()).isEqualTo("""<scheme name="a" data="b" />""")
334     assertThat(dir.resolve("b.xml").readText()).isEqualTo("""<scheme name="b" data="a" />""")
335   }
336
337   @Test fun `VFS - rename A to B and B to A`() {
338     val dir = tempDirManager.newPath()
339     val schemeManager = SchemeManagerImpl(FILE_SPEC, TestSchemesProcessor(), null, dir, messageBus = ApplicationManager.getApplication().messageBus)
340
341     val a = TestScheme("a", "a")
342     val b = TestScheme("b", "b")
343     schemeManager.setSchemes(listOf(a, b))
344     runInEdtAndWait { schemeManager.save() }
345
346     assertThat(dir.resolve("a.xml")).isRegularFile()
347     assertThat(dir.resolve("b.xml")).isRegularFile()
348
349     a.name = "b"
350     b.name = "a"
351
352     runInEdtAndWait { schemeManager.save() }
353
354     assertThat(dir.resolve("a.xml").readText()).isEqualTo("""<scheme name="a" data="b" />""")
355     assertThat(dir.resolve("b.xml").readText()).isEqualTo("""<scheme name="b" data="a" />""")
356   }
357
358   @Test fun `path must not contains ROOT_CONFIG macro`() {
359     assertThatThrownBy({ SchemeManagerFactory.getInstance().create("\$ROOT_CONFIG$/foo", TestSchemesProcessor()) }).hasMessage("Path must not contains ROOT_CONFIG macro, corrected: foo")
360   }
361
362   @Test fun `path must be system-independent`() {
363     assertThatThrownBy({ SchemeManagerFactory.getInstance().create("foo\\bar", TestSchemesProcessor())}).hasMessage("Path must be system-independent, use forward slash instead of backslash")
364   }
365
366   private fun createSchemeManager(dir: Path) = SchemeManagerImpl(FILE_SPEC, TestSchemesProcessor(), null, dir)
367
368   private fun createAndLoad(testData: String): SchemeManagerImpl<TestScheme, TestScheme> {
369     createTempFiles(testData)
370     return createAndLoad()
371   }
372
373   private fun doLoadSaveTest(testData: String, expected: String, localExpected: String = "") {
374     val schemesManager = createAndLoad(testData)
375     schemesManager.save()
376     checkSchemes(remoteBaseDir!!.resolve("REMOTE"), expected, true)
377     checkSchemes(localBaseDir!!, localExpected, false)
378   }
379
380   private fun checkSchemes(expected: String) {
381     checkSchemes(remoteBaseDir!!.resolve("REMOTE"), expected, true)
382     checkSchemes(localBaseDir!!, "", false)
383   }
384
385   private fun createAndLoad(): SchemeManagerImpl<TestScheme, TestScheme> {
386     val schemesManager = SchemeManagerImpl(FILE_SPEC, TestSchemesProcessor(), MockStreamProvider(remoteBaseDir!!), localBaseDir!!)
387     schemesManager.loadSchemes()
388     return schemesManager
389   }
390
391   private fun createTempFiles(testData: String) {
392     val temp = tempDirManager.newPath()
393     localBaseDir = temp.resolve("__local")
394     remoteBaseDir = temp
395     FileUtil.copyDir(File("${getTestDataPath()}/$testData"), temp.resolve("REMOTE").toFile())
396   }
397 }
398
399 private fun checkSchemes(baseDir: Path, expected: String, ignoreDeleted: Boolean) {
400   val filesToScheme = StringUtil.split(expected, ";")
401   val fileToSchemeMap = THashMap<String, String>()
402   for (fileToScheme in filesToScheme) {
403     val index = fileToScheme.indexOf("->")
404     fileToSchemeMap.put(fileToScheme.substring(0, index), fileToScheme.substring(index + 2))
405   }
406
407   baseDir.directoryStreamIfExists {
408     for (file in it) {
409       val fileName = FileUtil.getNameWithoutExtension(file.fileName.toString())
410       if ("--deleted" == fileName && ignoreDeleted) {
411         assertThat(fileToSchemeMap).containsKey(fileName)
412       }
413     }
414   }
415
416   for (file in fileToSchemeMap.keys) {
417     assertThat(baseDir.resolve("$file.xml")).isRegularFile()
418   }
419
420   baseDir.directoryStreamIfExists {
421     for (file in it) {
422       val scheme = XmlSerializer.deserialize(loadElement(file), TestScheme::class.java)!!
423       assertThat(fileToSchemeMap.get(FileUtil.getNameWithoutExtension(file.fileName.toString()))).isEqualTo(scheme.name)
424     }
425   }
426 }
427
428 @Tag("scheme")
429 data class TestScheme(@field:com.intellij.util.xmlb.annotations.Attribute @field:kotlin.jvm.JvmField var name: String = "", @field:com.intellij.util.xmlb.annotations.Attribute var data: String? = null) : ExternalizableScheme, SerializableScheme {
430   override fun getName() = name
431
432   override fun setName(value: String) {
433     name = value
434   }
435
436   override fun writeScheme() = serialize()
437 }
438
439 open class TestSchemesProcessor : LazySchemeProcessor<TestScheme, TestScheme>() {
440   override fun createScheme(dataHolder: SchemeDataHolder<TestScheme>,
441                             name: String,
442                             attributeProvider: Function<String, String?>,
443                             isBundled: Boolean): TestScheme {
444     val scheme = XmlSerializer.deserialize(dataHolder.read(), TestScheme::class.java)!!
445     dataHolder.updateDigest(scheme)
446     return scheme
447   }
448 }
449
450 fun SchemeManagerImpl<*, *>.save() {
451   val errors = SmartList<Throwable>()
452   save(errors)
453   CompoundRuntimeException.throwIfNotEmpty(errors)
454 }