View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *     http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.commons.configuration2;
19  
20  import static org.apache.commons.configuration2.TempDirUtils.newFile;
21  import static org.junit.jupiter.api.Assertions.assertEquals;
22  import static org.junit.jupiter.api.Assertions.assertFalse;
23  import static org.junit.jupiter.api.Assertions.assertNull;
24  import static org.junit.jupiter.api.Assertions.assertThrows;
25  import static org.junit.jupiter.api.Assertions.assertTrue;
26  import static org.junit.jupiter.api.Assertions.fail;
27  
28  import java.io.File;
29  import java.io.FileWriter;
30  import java.io.IOException;
31  import java.io.PrintWriter;
32  import java.io.StringReader;
33  import java.io.StringWriter;
34  import java.io.Writer;
35  import java.text.MessageFormat;
36  import java.util.Arrays;
37  import java.util.Collections;
38  import java.util.HashSet;
39  import java.util.Iterator;
40  import java.util.List;
41  import java.util.Set;
42  import java.util.stream.Stream;
43  
44  import org.apache.commons.configuration2.SynchronizerTestImpl.Methods;
45  import org.apache.commons.configuration2.builder.FileBasedBuilderParametersImpl;
46  import org.apache.commons.configuration2.builder.FileBasedConfigurationBuilder;
47  import org.apache.commons.configuration2.builder.fluent.Parameters;
48  import org.apache.commons.configuration2.convert.DefaultListDelimiterHandler;
49  import org.apache.commons.configuration2.ex.ConfigurationException;
50  import org.apache.commons.configuration2.sync.ReadWriteSynchronizer;
51  import org.apache.commons.configuration2.tree.DefaultExpressionEngine;
52  import org.apache.commons.configuration2.tree.DefaultExpressionEngineSymbols;
53  import org.apache.commons.configuration2.tree.ImmutableNode;
54  import org.apache.commons.configuration2.tree.NodeHandler;
55  import org.apache.commons.configuration2.tree.NodeNameMatchers;
56  import org.junit.jupiter.api.Test;
57  import org.junit.jupiter.api.io.TempDir;
58  import org.junit.jupiter.params.ParameterizedTest;
59  import org.junit.jupiter.params.provider.Arguments;
60  import org.junit.jupiter.params.provider.MethodSource;
61  
62  /**
63   * Test class for {@code INIConfiguration}.
64   */
65  public class TestINIConfiguration {
66  
67      /**
68       * A thread class for testing concurrent access to the global section.
69       */
70      private static final class GlobalSectionTestThread extends Thread {
71  
72          /** The configuration. */
73          private final INIConfiguration config;
74  
75          /** A flag whether an error was found. */
76          private volatile boolean error;
77  
78          /**
79           * Creates a new instance of {@code GlobalSectionTestThread} and initializes it.
80           *
81           * @param conf the configuration object
82           */
83          public GlobalSectionTestThread(final INIConfiguration conf) {
84              config = conf;
85          }
86  
87          /**
88           * Accesses the global section in a loop. If there is no correct synchronization, this can cause an exception.
89           */
90          @Override
91          public void run() {
92              final int loopCount = 250;
93  
94              for (int i = 0; i < loopCount && !error; i++) {
95                  try {
96                      config.getSection(null);
97                  } catch (final IllegalStateException istex) {
98                      error = true;
99                  }
100             }
101         }
102     }
103 
104     private static final String LINE_SEPARATOR = System.lineSeparator();
105 
106     /** Constant for the content of an ini file. */
107     private static final String INI_DATA = "[section1]" + LINE_SEPARATOR + "var1 = foo" + LINE_SEPARATOR + "var2 = 451" + LINE_SEPARATOR + LINE_SEPARATOR
108         + "[section2]" + LINE_SEPARATOR + "var1 = 123.45" + LINE_SEPARATOR + "var2 = bar" + LINE_SEPARATOR + LINE_SEPARATOR + "[section3]" + LINE_SEPARATOR
109         + "var1 = true" + LINE_SEPARATOR + "interpolated = ${section3.var1}" + LINE_SEPARATOR + "multi = foo" + LINE_SEPARATOR + "multi = bar" + LINE_SEPARATOR
110         + LINE_SEPARATOR;
111 
112     private static final String INI_DATA2 = "[section4]" + LINE_SEPARATOR + "var1 = \"quoted value\"" + LINE_SEPARATOR
113         + "var2 = \"quoted value\\nwith \\\"quotes\\\"\"" + LINE_SEPARATOR + "var3 = 123 ; comment" + LINE_SEPARATOR + "var4 = \"1;2;3\" ; comment"
114         + LINE_SEPARATOR + "var5 = '\\'quoted\\' \"value\"' ; comment" + LINE_SEPARATOR + "var6 = \"\"" + LINE_SEPARATOR;
115 
116     private static final String INI_DATA3 = "[section5]" + LINE_SEPARATOR + "multiLine = one \\" + LINE_SEPARATOR + "    two      \\" + LINE_SEPARATOR
117         + " three" + LINE_SEPARATOR + "singleLine = C:\\Temp\\" + LINE_SEPARATOR + "multiQuoted = one \\" + LINE_SEPARATOR + "\"  two  \" \\" + LINE_SEPARATOR
118         + "  three" + LINE_SEPARATOR + "multiComment = one \\ ; a comment" + LINE_SEPARATOR + "two" + LINE_SEPARATOR
119         + "multiQuotedComment = \" one \" \\ ; comment" + LINE_SEPARATOR + "two" + LINE_SEPARATOR + "noFirstLine = \\" + LINE_SEPARATOR + "  line 2"
120         + LINE_SEPARATOR + "continueNoLine = one \\" + LINE_SEPARATOR;
121 
122     private static final String INI_DATA4 = "[section6]" + LINE_SEPARATOR + "key1{0}value1" + LINE_SEPARATOR + "key2{0}value2" + LINE_SEPARATOR + LINE_SEPARATOR
123         + "[section7]" + LINE_SEPARATOR + "key3{0}value3" + LINE_SEPARATOR;
124 
125     private static final String INI_DATA5 = "[section4]" + LINE_SEPARATOR + "var1 = \"quoted value\"" + LINE_SEPARATOR
126         + "var2 = \"quoted value\\nwith \\\"quotes\\\"\"" + LINE_SEPARATOR + "var3 = 123 # comment" + LINE_SEPARATOR + "var4 = \"1#2;3\" # comment"
127         + LINE_SEPARATOR + "var5 = '\\'quoted\\' \"value\"' # comment" + LINE_SEPARATOR + "var6 = \"\"" + LINE_SEPARATOR;
128 
129     /** Constant for the content of an ini file - with section inline comment defined with semicolon */
130     private static final String INI_DATA6 = "[section1]; main section" + LINE_SEPARATOR + "var1 = foo" + LINE_SEPARATOR + LINE_SEPARATOR
131         + "[section11] ; sub-section related to [section1]" + LINE_SEPARATOR + "var1 = 123.45" + LINE_SEPARATOR;
132 
133     /** Constant for the content of an ini file - with section inline comment defined with number sign */
134     private static final String INI_DATA7 = "[section1]# main section" + LINE_SEPARATOR + "var1 = foo" + LINE_SEPARATOR + LINE_SEPARATOR
135         + "[section11] # sub-section related to [section1]" + LINE_SEPARATOR + "var1 = 123.45" + LINE_SEPARATOR;
136 
137     private static final String INI_DATA_SEPARATORS = "[section]" + LINE_SEPARATOR + "var1 = value1" + LINE_SEPARATOR + "var2 : value2" + LINE_SEPARATOR
138         + "var3=value3" + LINE_SEPARATOR + "var4:value4" + LINE_SEPARATOR + "var5 : value=5" + LINE_SEPARATOR + "var:6=value" + LINE_SEPARATOR
139         + "var:7=\"value7\"" + LINE_SEPARATOR + "var:8 =  \"value8\"" + LINE_SEPARATOR;
140 
141     /** An ini file that contains only a property in the global section. */
142     private static final String INI_DATA_GLOBAL_ONLY = "globalVar = testGlobal" + LINE_SEPARATOR + LINE_SEPARATOR;
143 
144     /** An ini file with a global section. */
145     private static final String INI_DATA_GLOBAL = INI_DATA_GLOBAL_ONLY + INI_DATA;
146 
147     /**
148      * Loads the specified content into the given configuration instance.
149      *
150      * @param instance the configuration
151      * @param data the data to be loaded
152      * @throws ConfigurationException if an error occurs
153      */
154     private static void load(final INIConfiguration instance, final String data) throws ConfigurationException {
155         try (StringReader reader = new StringReader(data)) {
156             instance.read(reader);
157         } catch (final IOException e) {
158             throw new ConfigurationException(e);
159         }
160     }
161 
162     private static Stream<Arguments> provideSectionsWithComments() {
163         return Stream.of(
164                 Arguments.of(INI_DATA6, false, new String[]{null, "section11] ; sub-section related to [section1"}),
165                 Arguments.of(INI_DATA7, false, new String[]{null, "section11] # sub-section related to [section1"}),
166                 Arguments.of(INI_DATA6, true, new String[]{"section1", "section11"}),
167                 Arguments.of(INI_DATA7, true, new String[]{"section1", "section11"})
168         );
169     }
170 
171     private static Stream<Arguments> provideValuesWithComments() {
172         return Stream.of(
173                 Arguments.of(INI_DATA2, "section4.var3", "123"),
174                 Arguments.of(INI_DATA2, "section4.var4", "1;2;3"),
175                 Arguments.of(INI_DATA2, "section4.var5", "'quoted' \"value\""),
176                 Arguments.of(INI_DATA5, "section4.var3", "123"),
177                 Arguments.of(INI_DATA5, "section4.var4", "1#2;3"),
178                 Arguments.of(INI_DATA5, "section4.var5", "'quoted' \"value\"")
179         );
180     }
181 
182     /**
183      * Saves the specified configuration to a string. The string can be compared with an expected value or again loaded into
184      * a configuration.
185      *
186      * @param config the configuration to be saved
187      * @return the content of this configuration saved to a string
188      * @throws ConfigurationException if an error occurs
189      */
190     private static String saveToString(final INIConfiguration config) throws ConfigurationException {
191         final StringWriter writer = new StringWriter();
192         try {
193             config.write(writer);
194         } catch (final IOException e) {
195             throw new ConfigurationException(e);
196         }
197         return writer.toString();
198     }
199 
200     /**
201      * Creates a INIConfiguration object that is initialized from the given data.
202      *
203      * @param data the data of the configuration (an ini file as string)
204      * @return the initialized configuration
205      * @throws ConfigurationException if an error occurs
206      */
207     private static INIConfiguration setUpConfig(final String data) throws ConfigurationException {
208         return setUpConfig(data, false);
209     }
210 
211     /**
212      * Creates a INIConfiguration object that is initialized from the given data.
213      *
214      * @param data the data of the configuration (an ini file as string)
215      * @param inLineCommentsAllowed when true, inline comments on section line are allowed
216      * @return the initialized configuration
217      * @throws ConfigurationException if an error occurs
218      */
219     private static INIConfiguration setUpConfig(final String data, final boolean inLineCommentsAllowed) throws ConfigurationException {
220         // @formatter:off
221         final INIConfiguration instance = INIConfiguration.builder()
222                 .setSectionInLineCommentsAllowed(inLineCommentsAllowed)
223                 .build();
224         // @formatter:on
225         instance.setListDelimiterHandler(new DefaultListDelimiterHandler(','));
226         load(instance, data);
227         return instance;
228     }
229 
230     /** A folder for temporary files. */
231     @TempDir
232     public File tempFolder;
233 
234     /**
235      * Tests the values of some properties to ensure that the configuration was correctly loaded.
236      *
237      * @param instance the configuration to check
238      */
239     private void checkContent(final INIConfiguration instance) {
240         assertEquals("foo", instance.getString("section1.var1"));
241         assertEquals(451, instance.getInt("section1.var2"));
242         assertEquals(123.45, instance.getDouble("section2.var1"), .001);
243         assertEquals("bar", instance.getString("section2.var2"));
244         assertTrue(instance.getBoolean("section3.var1"));
245         assertEquals(new HashSet<>(Arrays.asList("section1", "section2", "section3")), instance.getSections());
246     }
247 
248     /**
249      * Helper method for testing the load operation. Loads the specified content into a configuration and then checks some
250      * properties.
251      *
252      * @param data the data to load
253      */
254     private void checkLoad(final String data) throws ConfigurationException {
255         final INIConfiguration instance = setUpConfig(data);
256         checkContent(instance);
257     }
258 
259     /**
260      * Helper method for testing a save operation. This method constructs a configuration from the specified content string.
261      * Then it saves this configuration and checks whether the result matches the original content.
262      *
263      * @param content the content of the configuration
264      * @throws ConfigurationException if an error occurs
265      */
266     private void checkSave(final String content) throws ConfigurationException {
267         final INIConfiguration config = setUpConfig(content);
268         final String sOutput = saveToString(config);
269         assertEquals(content, sOutput);
270     }
271 
272     /**
273      * Tests whether the specified configuration contains exactly the expected sections.
274      *
275      * @param config the configuration to check
276      * @param expected an array with the expected sections
277      */
278     private void checkSectionNames(final INIConfiguration config, final String[] expected) {
279         final Set<String> sectionNames = config.getSections();
280         assertEquals(new HashSet<>(Arrays.asList(expected)), sectionNames);
281     }
282 
283     /**
284      * Tests the names of the sections returned by the configuration.
285      *
286      * @param data the data of the ini configuration
287      * @param expected the expected section names
288      * @return the configuration instance
289      */
290     private INIConfiguration checkSectionNames(final String data, final String[] expected) throws ConfigurationException {
291         final INIConfiguration config = setUpConfig(data);
292         checkSectionNames(config, expected);
293         return config;
294     }
295 
296     /**
297      * Test of read method with changed comment leading separator
298      */
299     @Test
300     public void testCommentLeadingSeparatorUsedInINIInput() throws Exception {
301         final String inputCommentLeadingSeparator = ";";
302         final String input = "[section]" + LINE_SEPARATOR + "key1=a;b;c" + LINE_SEPARATOR + "key2=a#b#c" + LINE_SEPARATOR + ";key3=value3" + LINE_SEPARATOR
303             + "#key4=value4" + LINE_SEPARATOR;
304 
305         final INIConfiguration instance = new FileBasedConfigurationBuilder<>(INIConfiguration.class)
306             .configure(new Parameters().ini().setCommentLeadingCharsUsedInInput(inputCommentLeadingSeparator)).getConfiguration();
307         load(instance, input);
308 
309         assertEquals("a;b;c", instance.getString("section.key1"));
310         assertEquals("a#b#c", instance.getString("section.key2"));
311         assertNull(instance.getString("section.;key3"));
312         assertEquals("value4", instance.getString("section.#key4"));
313     }
314 
315     /**
316      * Tests correct handling of empty sections "[ ]".
317      */
318     @Test
319     public void testEmptySection() throws ConfigurationException {
320         final INIConfiguration config = setUpConfig("[]" + LINE_SEPARATOR + "key=value" + LINE_SEPARATOR);
321         final String value = config.getString(" .key");
322         assertEquals("value", value);
323     }
324 
325     /**
326      * Tests whether an expression engine can be used which ignores case.
327      */
328     @Test
329     public void testExpressionEngineIgnoringCase() throws ConfigurationException {
330         final DefaultExpressionEngine engine = new DefaultExpressionEngine(DefaultExpressionEngineSymbols.DEFAULT_SYMBOLS, NodeNameMatchers.EQUALS_IGNORE_CASE);
331         final INIConfiguration config = new INIConfiguration();
332         config.setExpressionEngine(engine);
333         load(config, INI_DATA);
334 
335         checkContent(config);
336         assertEquals("foo", config.getString("Section1.var1"));
337         assertEquals("foo", config.getString("section1.Var1"));
338         assertEquals("foo", config.getString("SECTION1.VAR1"));
339     }
340 
341     /**
342      * Tests a property that has no key.
343      */
344     @Test
345     public void testGetPropertyNoKey() throws ConfigurationException {
346         final String data = INI_DATA2 + LINE_SEPARATOR + "= noKey" + LINE_SEPARATOR;
347         final INIConfiguration config = setUpConfig(data);
348         assertEquals("noKey", config.getString("section4. "));
349     }
350 
351     /**
352      * Tests a property that has no value.
353      */
354     @Test
355     public void testGetPropertyNoValue() throws ConfigurationException {
356         final String data = INI_DATA2 + LINE_SEPARATOR + "noValue =" + LINE_SEPARATOR;
357         final INIConfiguration config = setUpConfig(data);
358         assertEquals("", config.getString("section4.noValue"));
359     }
360 
361     /**
362      * Tests whether the sub configuration returned by getSection() is connected to the parent.
363      */
364     @Test
365     public void testGetSectionConnected() throws ConfigurationException {
366         final INIConfiguration config = setUpConfig(INI_DATA);
367         final HierarchicalConfiguration<ImmutableNode> section = config.getSection("section1");
368         section.setProperty("var1", "foo2");
369         assertEquals("foo2", config.getString("section1.var1"));
370     }
371 
372     /**
373      * Tests whether getSection() can deal with duplicate sections.
374      */
375     @Test
376     public void testGetSectionDuplicate() {
377         final INIConfiguration config = new INIConfiguration();
378         config.addProperty("section.var1", "value1");
379         config.addProperty("section(-1).var2", "value2");
380         final HierarchicalConfiguration<ImmutableNode> section = config.getSection("section");
381         final Iterator<String> keys = section.getKeys();
382         assertEquals("var1", keys.next());
383         assertFalse(keys.hasNext());
384     }
385 
386     /**
387      * Tests querying the properties of an existing section.
388      */
389     @Test
390     public void testGetSectionExisting() throws ConfigurationException {
391         final INIConfiguration config = setUpConfig(INI_DATA);
392         final HierarchicalConfiguration<ImmutableNode> section = config.getSection("section1");
393         assertEquals("foo", section.getString("var1"));
394         assertEquals("451", section.getString("var2"));
395     }
396 
397     /**
398      * Tests concurrent access to the global section.
399      */
400     @Test
401     public void testGetSectionGloabalMultiThreaded() throws ConfigurationException, InterruptedException {
402         final INIConfiguration config = setUpConfig(INI_DATA_GLOBAL);
403         config.setSynchronizer(new ReadWriteSynchronizer());
404         final int threadCount = 10;
405         final GlobalSectionTestThread[] threads = new GlobalSectionTestThread[threadCount];
406         for (int i = 0; i < threadCount; i++) {
407             threads[i] = new GlobalSectionTestThread(config);
408             threads[i].start();
409         }
410         for (int i = 0; i < threadCount; i++) {
411             threads[i].join();
412             assertFalse(threads[i].error);
413         }
414     }
415 
416     /**
417      * Tests querying the content of the global section.
418      */
419     @Test
420     public void testGetSectionGlobal() throws ConfigurationException {
421         final INIConfiguration config = setUpConfig(INI_DATA_GLOBAL);
422         final HierarchicalConfiguration<ImmutableNode> section = config.getSection(null);
423         assertEquals("testGlobal", section.getString("globalVar"));
424     }
425 
426     /**
427      * Tests querying the content of the global section if there is none.
428      */
429     @Test
430     public void testGetSectionGlobalNonExisting() throws ConfigurationException {
431         final INIConfiguration config = setUpConfig(INI_DATA);
432         final HierarchicalConfiguration<ImmutableNode> section = config.getSection(null);
433         assertTrue(section.isEmpty());
434     }
435 
436     /**
437      * Tests querying the properties of a section that was merged from two sections with the same name.
438      */
439     @Test
440     public void testGetSectionMerged() throws ConfigurationException {
441         final String data = INI_DATA + "[section1]" + LINE_SEPARATOR + "var3 = merged" + LINE_SEPARATOR;
442         final INIConfiguration config = setUpConfig(data);
443         final HierarchicalConfiguration<ImmutableNode> section = config.getSection("section1");
444         assertEquals("foo", section.getString("var1"));
445         assertEquals("451", section.getString("var2"));
446         assertEquals("merged", section.getString("var3"));
447     }
448 
449     /**
450      * Tests querying a non existing section.
451      */
452     @Test
453     public void testGetSectionNonExisting() throws ConfigurationException {
454         final INIConfiguration config = setUpConfig(INI_DATA);
455         final HierarchicalConfiguration<ImmutableNode> section = config.getSection("Non existing section");
456         assertTrue(section.isEmpty());
457     }
458 
459     /**
460      * Tests whether a section that was created by getSection() can be manipulated.
461      */
462     @Test
463     public void testGetSectionNonExistingManipulate() throws ConfigurationException, IOException {
464         final INIConfiguration config = setUpConfig(INI_DATA);
465         HierarchicalConfiguration<ImmutableNode> section = config.getSection("newSection");
466         section.addProperty("test", "success");
467         assertEquals("success", config.getString("newSection.test"));
468         final StringWriter writer = new StringWriter();
469         config.write(writer);
470         final INIConfiguration config2 = setUpConfig(writer.toString());
471         section = config2.getSection("newSection");
472         assertEquals("success", section.getString("test"));
473     }
474 
475     /**
476      * Test of getSections method, of class {@link INIConfiguration}.
477      */
478     @Test
479     public void testGetSections() {
480         final INIConfiguration instance = new INIConfiguration();
481         instance.addProperty("test1.foo", "bar");
482         instance.addProperty("test2.foo", "abc");
483         final Set<String> expResult = new HashSet<>();
484         expResult.add("test1");
485         expResult.add("test2");
486         final Set<String> result = instance.getSections();
487         assertEquals(expResult, result);
488     }
489 
490     /**
491      * Tests whether a section added later is also found by getSections().
492      */
493     @Test
494     public void testGetSectionsAdded() throws ConfigurationException {
495         final INIConfiguration config = setUpConfig(INI_DATA2);
496         config.addProperty("section5.test", Boolean.TRUE);
497         checkSectionNames(config, new String[] {"section4", "section5"});
498     }
499 
500     /**
501      * Tests whether variables containing a dot are not misinterpreted as sections. This test is related to
502      * CONFIGURATION-327.
503      */
504     @Test
505     public void testGetSectionsDottedVar() throws ConfigurationException {
506         final String data = "dotted.var = 1" + LINE_SEPARATOR + INI_DATA_GLOBAL;
507         final INIConfiguration config = checkSectionNames(data, new String[] {null, "section1", "section2", "section3"});
508         assertEquals(1, config.getInt("dotted..var"));
509     }
510 
511     /**
512      * Tests whether the sections of a configuration can be queried that contains only a global section.
513      */
514     @Test
515     public void testGetSectionsGlobalOnly() throws ConfigurationException {
516         checkSectionNames(INI_DATA_GLOBAL_ONLY, new String[] {null});
517     }
518 
519     /**
520      * Tests querying the sections if there is no global section.
521      */
522     @Test
523     public void testGetSectionsNoGlobal() throws ConfigurationException {
524         checkSectionNames(INI_DATA, new String[] {"section1", "section2", "section3"});
525     }
526 
527     /**
528      * Tests whether synchronization is performed when querying the configuration's sections.
529      */
530     @Test
531     public void testGetSectionsSynchronized() throws ConfigurationException {
532         final INIConfiguration config = setUpConfig(INI_DATA);
533         final SynchronizerTestImpl sync = new SynchronizerTestImpl();
534         config.setSynchronizer(sync);
535         assertFalse(config.getSections().isEmpty());
536         sync.verify(Methods.BEGIN_READ, Methods.END_READ);
537     }
538 
539     /**
540      * Tests querying the sections if a global section if available.
541      */
542     @Test
543     public void testGetSectionsWithGlobal() throws ConfigurationException {
544         checkSectionNames(INI_DATA_GLOBAL, new String[] {null, "section1", "section2", "section3"});
545     }
546 
547     /**
548      * Tests whether a section with inline comment is correctly parsed.
549      */
550     @ParameterizedTest
551     @MethodSource("provideSectionsWithComments")
552     public void testGetSectionsWithInLineComment(final String source, final boolean allowComments, final String[] results) throws ConfigurationException {
553         final INIConfiguration config = setUpConfig(source, allowComments);
554         checkSectionNames(config, results);
555     }
556 
557     /**
558      * Tests reading a property from the global section.
559      */
560     @Test
561     public void testGlobalProperty() throws ConfigurationException {
562         final INIConfiguration config = setUpConfig(INI_DATA_GLOBAL);
563         assertEquals("testGlobal", config.getString("globalVar"));
564     }
565 
566     /**
567      * Tests whether the sub configuration for the global section is connected to its parent.
568      */
569     @Test
570     public void testGlobalSectionConnected() throws ConfigurationException {
571         final INIConfiguration config = setUpConfig(INI_DATA_GLOBAL);
572         final HierarchicalConfiguration<ImmutableNode> sub = config.getSection(null);
573         config.setProperty("globalVar", "changed");
574         assertEquals("changed", sub.getString("globalVar"));
575     }
576 
577     /**
578      * Tests whether the node handler of a global section correctly returns a child by index.
579      */
580     @Test
581     public void testGlobalSectionNodeHandlerGetChildByIndex() throws ConfigurationException {
582         final INIConfiguration config = setUpConfig(INI_DATA_GLOBAL);
583         final SubnodeConfiguration sub = config.getSection(null);
584         final NodeHandler<ImmutableNode> handler = sub.getModel().getNodeHandler();
585         final ImmutableNode rootNode = handler.getRootNode();
586         final ImmutableNode child = handler.getChild(rootNode, 0);
587         assertEquals("globalVar", child.getNodeName());
588         assertThrows(IndexOutOfBoundsException.class, () -> handler.getChild(rootNode, 1));
589     }
590 
591     /**
592      * Tests whether the node handler of a global section correctly filters named children.
593      */
594     @Test
595     public void testGlobalSectionNodeHandlerGetChildrenByName() throws ConfigurationException {
596         final INIConfiguration config = setUpConfig(INI_DATA_GLOBAL);
597         final SubnodeConfiguration sub = config.getSection(null);
598         final NodeHandler<ImmutableNode> handler = sub.getModel().getNodeHandler();
599         assertTrue(handler.getChildren(sub.getModel().getNodeHandler().getRootNode(), "section1").isEmpty());
600     }
601 
602     /**
603      * Tests whether the node handler of a global section correctly determines the number of children.
604      */
605     @Test
606     public void testGlobalSectionNodeHandlerGetChildrenCount() throws ConfigurationException {
607         final INIConfiguration config = setUpConfig(INI_DATA_GLOBAL);
608         final SubnodeConfiguration sub = config.getSection(null);
609         final NodeHandler<ImmutableNode> handler = sub.getModel().getNodeHandler();
610         assertEquals(1, handler.getChildrenCount(handler.getRootNode(), null));
611     }
612 
613     /**
614      * Tests whether the node handler of a global section correctly determines the index of a child.
615      */
616     @Test
617     public void testGlobalSectionNodeHandlerIndexOfChild() throws ConfigurationException {
618         final INIConfiguration config = setUpConfig(INI_DATA_GLOBAL);
619         final SubnodeConfiguration sub = config.getSection(null);
620         final NodeHandler<ImmutableNode> handler = sub.getModel().getNodeHandler();
621         final List<ImmutableNode> children = handler.getRootNode().getChildren();
622         assertEquals(0, handler.indexOfChild(handler.getRootNode(), children.get(0)));
623         assertEquals(-1, handler.indexOfChild(handler.getRootNode(), children.get(1)));
624     }
625 
626     /**
627      * Test of isCommentLine method, of class {@link INIConfiguration}.
628      */
629     @Test
630     public void testIsCommentLine() {
631         final INIConfiguration instance = new INIConfiguration();
632         assertTrue(instance.isCommentLine("#comment1"));
633         assertTrue(instance.isCommentLine(";comment1"));
634         assertFalse(instance.isCommentLine("nocomment=true"));
635         assertFalse(instance.isCommentLine(null));
636     }
637 
638     /**
639      * Test of isSectionLine method, of class {@link INIConfiguration}.
640      */
641     @Test
642     public void testIsSectionLine() {
643         final INIConfiguration instance = new INIConfiguration();
644         assertTrue(instance.isSectionLine("[section]"));
645         assertFalse(instance.isSectionLine("nosection=true"));
646         assertFalse(instance.isSectionLine(null));
647     }
648 
649     /**
650      * Tests whether only properties with values occur in the enumeration of the global section.
651      */
652     @Test
653     public void testKeysOfGlobalSection() throws ConfigurationException {
654         final INIConfiguration config = setUpConfig(INI_DATA_GLOBAL);
655         final HierarchicalConfiguration<ImmutableNode> sub = config.getSection(null);
656         final Iterator<String> keys = sub.getKeys();
657         assertEquals("globalVar", keys.next());
658         if (keys.hasNext()) {
659             final StringBuilder buf = new StringBuilder();
660             do {
661                 buf.append(keys.next()).append(' ');
662             } while (keys.hasNext());
663             fail("Got additional keys: " + buf);
664         }
665     }
666 
667     /**
668      * Tests a property whose value spans multiple lines.
669      */
670     @Test
671     public void testLineContinuation() throws ConfigurationException {
672         final INIConfiguration config = setUpConfig(INI_DATA3);
673         assertEquals("one" + LINE_SEPARATOR + "two" + LINE_SEPARATOR + "three", config.getString("section5.multiLine"));
674     }
675 
676     /**
677      * Tests a line continuation at the end of the file.
678      */
679     @Test
680     public void testLineContinuationAtEnd() throws ConfigurationException {
681         final INIConfiguration config = setUpConfig(INI_DATA3);
682         assertEquals("one" + LINE_SEPARATOR, config.getString("section5.continueNoLine"));
683     }
684 
685     /**
686      * Tests a property whose value spans multiple lines with a comment.
687      */
688     @Test
689     public void testLineContinuationComment() throws ConfigurationException {
690         final INIConfiguration config = setUpConfig(INI_DATA3);
691         assertEquals("one" + LINE_SEPARATOR + "two", config.getString("section5.multiComment"));
692     }
693 
694     /**
695      * Tests a multi-line property value with an empty line.
696      */
697     @Test
698     public void testLineContinuationEmptyLine() throws ConfigurationException {
699         final INIConfiguration config = setUpConfig(INI_DATA3);
700         assertEquals(LINE_SEPARATOR + "line 2", config.getString("section5.noFirstLine"));
701     }
702 
703     /**
704      * Tests a property value that ends on a backslash, which is no line continuation character.
705      */
706     @Test
707     public void testLineContinuationNone() throws ConfigurationException {
708         final INIConfiguration config = setUpConfig(INI_DATA3);
709         assertEquals("C:\\Temp\\", config.getString("section5.singleLine"));
710     }
711 
712     /**
713      * Tests a property whose value spans multiple lines when quoting is involved. In this case whitespace must not be
714      * trimmed.
715      */
716     @Test
717     public void testLineContinuationQuoted() throws ConfigurationException {
718         final INIConfiguration config = setUpConfig(INI_DATA3);
719         assertEquals("one" + LINE_SEPARATOR + "  two  " + LINE_SEPARATOR + "three", config.getString("section5.multiQuoted"));
720     }
721 
722     /**
723      * Tests a property with a quoted value spanning multiple lines and a comment.
724      */
725     @Test
726     public void testLineContinuationQuotedComment() throws ConfigurationException {
727         final INIConfiguration config = setUpConfig(INI_DATA3);
728         assertEquals(" one " + LINE_SEPARATOR + "two", config.getString("section5.multiQuotedComment"));
729     }
730 
731     /**
732      * Tests whether the configuration deals correctly with list delimiters.
733      */
734     @Test
735     public void testListDelimiterHandling() throws ConfigurationException {
736         final INIConfiguration config = new INIConfiguration();
737         config.setListDelimiterHandler(new DefaultListDelimiterHandler(','));
738         config.addProperty("list", "a,b,c");
739         config.addProperty("listesc", "3\\,1415");
740         final String output = saveToString(config);
741         final INIConfiguration config2 = setUpConfig(output);
742         assertEquals(Arrays.asList("a", "b", "c"), config2.getList("list"));
743         assertEquals("3,1415", config2.getString("listesc"));
744     }
745 
746     /**
747      * Tests whether property values are correctly escaped even if they are part of a property with multiple values.
748      */
749     @Test
750     public void testListDelimiterHandlingInList() throws ConfigurationException {
751         final String data = INI_DATA + "[sectest]" + LINE_SEPARATOR + "list = 3\\,1415,pi,\\\\Test\\,5" + LINE_SEPARATOR;
752         final INIConfiguration config = setUpConfig(data);
753         final INIConfiguration config2 = setUpConfig(saveToString(config));
754         final List<Object> list = config2.getList("sectest.list");
755         assertEquals(Arrays.asList("3,1415", "pi", "\\Test,5"), list);
756     }
757 
758     /**
759      * Tests whether parsing of lists can be disabled.
760      */
761     @Test
762     public void testListParsingDisabled() throws ConfigurationException {
763         final INIConfiguration config = new INIConfiguration();
764         load(config, "[test]" + LINE_SEPARATOR + "nolist=1,2,3");
765         assertEquals("1,2,3", config.getString("test.nolist"));
766     }
767 
768     /**
769      * Test of load method, of class {@link INIConfiguration}.
770      */
771     @Test
772     public void testLoad() throws Exception {
773         checkLoad(INI_DATA);
774     }
775 
776     /**
777      * Tests the load() method when the alternative value separator is used (a ':' for '=').
778      */
779     @Test
780     public void testLoadAlternativeSeparator() throws Exception {
781         checkLoad(INI_DATA.replace('=', ':'));
782     }
783 
784     /**
785      * Tests whether an instance can be created using a file-based builder.
786      */
787     @Test
788     public void testLoadFromBuilder() throws ConfigurationException, IOException {
789         final File file = writeTestFile(INI_DATA);
790         final FileBasedConfigurationBuilder<INIConfiguration> builder = new FileBasedConfigurationBuilder<>(INIConfiguration.class);
791         builder.configure(new FileBasedBuilderParametersImpl().setFile(file));
792         final INIConfiguration config = builder.getConfiguration();
793         checkContent(config);
794     }
795 
796     /**
797      * Tests whether a duplicate session is merged.
798      */
799     @Test
800     public void testMergeDuplicateSection() throws ConfigurationException, IOException {
801         final String data = "[section]\nvar1 = sec1\n\n" + "[section]\nvar2 = sec2\n";
802         final INIConfiguration config = setUpConfig(data);
803         assertEquals("sec1", config.getString("section.var1"));
804         assertEquals("sec2", config.getString("section.var2"));
805         final HierarchicalConfiguration<ImmutableNode> sub = config.getSection("section");
806         assertEquals("sec1", sub.getString("var1"));
807         assertEquals("sec2", sub.getString("var2"));
808         final StringWriter writer = new StringWriter();
809         config.write(writer);
810         final String content = writer.toString();
811         final int pos = content.indexOf("[section]");
812         assertTrue(pos >= 0);
813         assertTrue(content.indexOf("[section]", pos + 1) < 0);
814     }
815 
816     /**
817      * Tests property definitions containing multiple separators.
818      */
819     @Test
820     public void testMultipleSeparators() throws ConfigurationException {
821         final INIConfiguration config = setUpConfig(INI_DATA_SEPARATORS);
822         assertEquals("value=5", config.getString("section.var5"));
823         assertEquals("6=value", config.getString("section.var"));
824     }
825 
826     /**
827      * Tests property definitions containing multiple separators that are quoted.
828      */
829     @Test
830     public void testMultipleSeparatorsQuoted() throws ConfigurationException {
831         final INIConfiguration config = setUpConfig(INI_DATA_SEPARATORS);
832         assertEquals("value7", config.getString("section.var:7"));
833         assertEquals("value8", config.getString("section.var:8"));
834     }
835 
836     /**
837      * Tests that loading and saving a configuration that contains keys with delimiter characters works correctly. This test
838      * is related to CONFIGURATION-622.
839      */
840     @Test
841     public void testPropertyWithDelimiter() throws ConfigurationException {
842         final String data = INI_DATA + "key.dot = dotValue";
843         final INIConfiguration conf = new INIConfiguration();
844         load(conf, data);
845         assertEquals("dotValue", conf.getString("section3.key..dot"));
846         final String output = saveToString(conf);
847         assertTrue(output.contains("key.dot = dotValue"));
848     }
849 
850     @Test
851     public void testQuotedValue() throws Exception {
852         final INIConfiguration config = setUpConfig(INI_DATA2);
853         assertEquals("quoted value", config.getString("section4.var1"));
854     }
855 
856     /**
857      * Tests an empty quoted value.
858      */
859     @Test
860     public void testQuotedValueEmpty() throws ConfigurationException {
861         final INIConfiguration config = setUpConfig(INI_DATA2);
862         assertEquals("", config.getString("section4.var6"));
863     }
864 
865     @Test
866     public void testQuotedValueWithComment() throws Exception {
867         final INIConfiguration config = setUpConfig(INI_DATA2);
868         assertEquals("1;2;3", config.getString("section4.var4"));
869     }
870 
871     @Test
872     public void testQuotedValueWithQuotes() throws Exception {
873         final INIConfiguration config = setUpConfig(INI_DATA2);
874         assertEquals("quoted value\\nwith \"quotes\"", config.getString("section4.var2"));
875     }
876 
877     @Test
878     public void testQuotedValueWithSingleQuotes() throws Exception {
879         final INIConfiguration config = setUpConfig(INI_DATA2);
880         assertEquals("'quoted' \"value\"", config.getString("section4.var5"));
881     }
882 
883     /**
884      * Tests whether whitespace is left unchanged for quoted values.
885      */
886     @Test
887     public void testQuotedValueWithWhitespace() throws Exception {
888         final String content = "CmdPrompt = \" [test@cmd ~]$ \"";
889         final INIConfiguration config = setUpConfig(content);
890         assertEquals(" [test@cmd ~]$ ", config.getString("CmdPrompt"));
891     }
892 
893     /**
894      * Tests a quoted value with space and a comment.
895      */
896     @Test
897     public void testQuotedValueWithWhitespaceAndComment() throws Exception {
898         final String content = "CmdPrompt = \" [test@cmd ~]$ \" ; a comment";
899         final INIConfiguration config = setUpConfig(content);
900         assertEquals(" [test@cmd ~]$ ", config.getString("CmdPrompt"));
901     }
902 
903     /**
904      * Test of save method, of class {@link INIConfiguration}.
905      */
906     @Test
907     public void testSave() throws Exception {
908         final Writer writer = new StringWriter();
909         final INIConfiguration instance = new INIConfiguration();
910         instance.addProperty("section1.var1", "foo");
911         instance.addProperty("section1.var2", "451");
912         instance.addProperty("section2.var1", "123.45");
913         instance.addProperty("section2.var2", "bar");
914         instance.addProperty("section3.var1", "true");
915         instance.addProperty("section3.interpolated", "${section3.var1}");
916         instance.addProperty("section3.multi", "foo");
917         instance.addProperty("section3.multi", "bar");
918         instance.write(writer);
919 
920         assertEquals(INI_DATA, writer.toString());
921     }
922 
923     /**
924      * Tests whether a section that has been cleared can be manipulated and saved later.
925      */
926     @Test
927     public void testSaveClearedSection() throws ConfigurationException, IOException {
928         final String data = "[section]\ntest = failed\n";
929         final INIConfiguration config = setUpConfig(data);
930         SubnodeConfiguration sub = config.getSection("section");
931         assertFalse(sub.isEmpty());
932         sub.clear();
933         sub.close();
934         sub = config.getSection("section");
935         sub.setProperty("test", "success");
936         final StringWriter writer = new StringWriter();
937         config.write(writer);
938         final HierarchicalConfiguration<?> config2 = setUpConfig(writer.toString());
939         assertEquals("success", config2.getString("section.test"));
940     }
941 
942     /**
943      * Tests whether a configuration can be saved that contains section keys with delimiter characters. This test is related
944      * to CONFIGURATION-409.
945      */
946     @Test
947     public void testSaveKeysWithDelimiters() throws ConfigurationException, IOException {
948         INIConfiguration conf = new INIConfiguration();
949         final String section = "Section..with..dots";
950         conf.addProperty(section + ".test1", "test1");
951         conf.addProperty(section + ".test2", "test2");
952         final StringWriter writer = new StringWriter();
953         conf.write(writer);
954         conf = new INIConfiguration();
955         conf.read(new StringReader(writer.toString()));
956         assertEquals("test1", conf.getString(section + ".test1"));
957         assertEquals("test2", conf.getString(section + ".test2"));
958     }
959 
960     /**
961      * Tests whether list delimiter parsing can be disabled.
962      */
963     @Test
964     public void testSaveWithDelimiterParsingDisabled() throws ConfigurationException {
965         final INIConfiguration config = new INIConfiguration();
966         final String data = INI_DATA.substring(0, INI_DATA.length() - LINE_SEPARATOR.length()) + "nolist = 1,2, 3";
967         load(config, data);
968         assertEquals("1,2, 3", config.getString("section3.nolist"));
969         final String content = saveToString(config);
970         final INIConfiguration config2 = new INIConfiguration();
971         load(config2, content);
972         assertEquals("1,2, 3", config2.getString("section3.nolist"));
973     }
974 
975     /**
976      * Tests saving a configuration that contains a global section.
977      */
978     @Test
979     public void testSaveWithGlobalSection() throws ConfigurationException {
980         checkSave(INI_DATA_GLOBAL);
981     }
982 
983     /**
984      * Tests whether a configuration that contains only a global section can be saved correctly.
985      */
986     @Test
987     public void testSaveWithOnlyGlobalSection() throws ConfigurationException {
988         checkSave(INI_DATA_GLOBAL_ONLY);
989     }
990 
991     /**
992      * Tests whether the different separators with or without whitespace are recognized.
993      */
994     @Test
995     public void testSeparators() throws ConfigurationException {
996         final INIConfiguration config = setUpConfig(INI_DATA_SEPARATORS);
997         for (int i = 1; i <= 4; i++) {
998             assertEquals("value" + i, config.getString("section.var" + i));
999         }
1000     }
1001 
1002     /**
1003      * Test of read method with changed separator.
1004      */
1005     @Test
1006     public void testSeparatorUsedInINIInput() throws Exception {
1007         final String inputSeparator = "=";
1008         final String input = "[section]" + LINE_SEPARATOR + "k1:v1$key1=value1" + LINE_SEPARATOR + "k1:v1,k2:v2$key2=value2" + LINE_SEPARATOR + "key3:value3"
1009             + LINE_SEPARATOR + "key4 = value4" + LINE_SEPARATOR;
1010 
1011         final INIConfiguration instance = new FileBasedConfigurationBuilder<>(INIConfiguration.class)
1012             .configure(new Parameters().ini().setSeparatorUsedInInput(inputSeparator)).getConfiguration();
1013         load(instance, input);
1014 
1015         assertEquals("value1", instance.getString("section.k1:v1$key1"));
1016         assertEquals("value2", instance.getString("section.k1:v1,k2:v2$key2"));
1017         assertEquals("", instance.getString("section.key3:value3"));
1018         assertEquals("value4", instance.getString("section.key4").trim());
1019     }
1020 
1021     /**
1022      * Test of save method with changed separator
1023      */
1024     @Test
1025     public void testSeparatorUsedInINIOutput() throws Exception {
1026         final String outputSeparator = ": ";
1027         final String input = MessageFormat.format(INI_DATA4, "=").trim();
1028         final String expectedOutput = MessageFormat.format(INI_DATA4, outputSeparator).trim();
1029 
1030         final INIConfiguration instance = new FileBasedConfigurationBuilder<>(INIConfiguration.class)
1031             .configure(new Parameters().ini().setSeparatorUsedInOutput(outputSeparator)).getConfiguration();
1032         load(instance, input);
1033 
1034         final Writer writer = new StringWriter();
1035         instance.write(writer);
1036         final String result = writer.toString().trim();
1037 
1038         assertEquals(expectedOutput, result);
1039     }
1040 
1041     /**
1042      * Test correct handling of in line comments on value line
1043      */
1044     @ParameterizedTest
1045     @MethodSource("provideValuesWithComments")
1046     public void testValueWithComment(final String source, final String key, final String value) throws Exception {
1047         final INIConfiguration config = setUpConfig(source);
1048         assertEquals(value, config.getString(key));
1049     }
1050 
1051     /**
1052      * Tests whether the list delimiter character is recognized.
1053      */
1054     @Test
1055     public void testValueWithDelimiters() throws ConfigurationException {
1056         final INIConfiguration config = setUpConfig("[test]" + LINE_SEPARATOR + "list=1,2,3" + LINE_SEPARATOR);
1057         final List<Object> list = config.getList("test.list");
1058         assertEquals(Arrays.asList("1", "2", "3"), list);
1059     }
1060 
1061     /**
1062      * Tests whether a value which contains a semicolon can be loaded successfully. This test is related to
1063      * CONFIGURATION-434.
1064      */
1065     @Test
1066     public void testValueWithSemicolon() throws ConfigurationException {
1067         final String path = "C:\\Program Files\\jar\\manage.jar;" + "C:\\Program Files\\jar\\guiLauncher.jar";
1068         final String content = "[Environment]" + LINE_SEPARATOR + "Application Type=any" + LINE_SEPARATOR + "Class Path=" + path + "  ;comment" + LINE_SEPARATOR
1069             + "Path=" + path + "\t; another comment";
1070         final INIConfiguration config = setUpConfig(content);
1071         assertEquals(path, config.getString("Environment.Class Path"));
1072         assertEquals(path, config.getString("Environment.Path"));
1073     }
1074 
1075     /**
1076      * Tests whether an empty section can be saved. This is related to CONFIGURATION-671.
1077      */
1078     @Test
1079     public void testWriteEmptySection() throws ConfigurationException, IOException {
1080         final String section = "[EmptySection]";
1081         final INIConfiguration config = setUpConfig(section);
1082         assertEquals(Collections.singleton("EmptySection"), config.getSections());
1083 
1084         final StringWriter writer = new StringWriter();
1085         config.write(writer);
1086         assertEquals(section + LINE_SEPARATOR + LINE_SEPARATOR, writer.toString());
1087     }
1088 
1089     @Test
1090     public void testWriteValueWithCommentChar() throws Exception {
1091         final INIConfiguration config = new INIConfiguration();
1092         config.setProperty("section.key1", "1;2;3");
1093 
1094         final StringWriter writer = new StringWriter();
1095         config.write(writer);
1096 
1097         final INIConfiguration config2 = new INIConfiguration();
1098         config2.read(new StringReader(writer.toString()));
1099 
1100         assertEquals("1;2;3", config2.getString("section.key1"));
1101     }
1102 
1103     /**
1104      * Writes a test ini file.
1105      *
1106      * @param content the content of the file
1107      * @return the newly created file
1108      * @throws IOException if an error occurs
1109      */
1110     private File writeTestFile(final String content) throws IOException {
1111         final File file = newFile(tempFolder);
1112         try (PrintWriter out = new PrintWriter(new FileWriter(file))) {
1113             out.println(content);
1114         }
1115         return file;
1116     }
1117 }