diff --git a/.github/workflows/docstrings_validation.yml b/.github/workflows/docstrings_validation.yml new file mode 100644 index 0000000..261948c --- /dev/null +++ b/.github/workflows/docstrings_validation.yml @@ -0,0 +1,29 @@ +--- +name: Test Docstrings Validation + +on: + pull_request: + paths: + - "tests/**" + +jobs: + betelgeuse: + name: "betelgeuse dry-run" + runs-on: ubuntu-latest + container: + image: fedora:latest + + steps: + - uses: actions/checkout@v4 + + - name: Base setup for Betelgeuse + run: | + dnf --setopt install_weak_deps=False install -y \ + python3-pip + python3 -m pip install betelgeuse + + - name: Run Betelgeuse + run: | + PYTHONPATH=tests/ betelgeuse --config-module \ + custom_betelgeuse_config test-case --dry-run \ + tests/ dryrun_project ./test_case.xml diff --git a/.github/workflows/testimony.yml b/.github/workflows/testimony.yml new file mode 100644 index 0000000..99911eb --- /dev/null +++ b/.github/workflows/testimony.yml @@ -0,0 +1,27 @@ +--- +name: Testimony Validation + +on: + pull_request: + paths: + - "tests/**" + +jobs: + testimony: + name: testimony validate + runs-on: ubuntu-latest + container: + image: fedora:latest + + steps: + - name: Setup for Testimony + run: | + dnf --setopt install_weak_deps=False install -y \ + python3-pip + python3 -m pip install testimony + - uses: actions/checkout@v4 + + - name: Run Testimony + run: | + testimony validate --config \ + tests/testimony.yml tests/test* diff --git a/tests/custom_betelgeuse_config.py b/tests/custom_betelgeuse_config.py new file mode 100644 index 0000000..529e86e --- /dev/null +++ b/tests/custom_betelgeuse_config.py @@ -0,0 +1,15 @@ +from betelgeuse import default_config + +TESTCASE_CUSTOM_FIELDS = default_config.TESTCASE_CUSTOM_FIELDS + ( + "component", + "poolteam", + "polarionincludeskipped", + "polarionlookupmethod", + "polarionprojectid", +) + +DEFAULT_COMPONENT_VALUE = "" +DEFAULT_POOLTEAM_VALUE = "" +POLARION_INCLUDE_SKIPED = "" +POLARION_LOOKUP_METHOD = "" +POLARION_PROJECT_ID = "" diff --git a/tests/test_compat.py b/tests/test_compat.py index 5244388..611e743 100644 --- a/tests/test_compat.py +++ b/tests/test_compat.py @@ -1,3 +1,14 @@ +""" +:component: python-iniparse +:requirement: RHSS-291606 +:polarion-project-id: RHELSS +:polarion-include-skipped: false +:polarion-lookup-method: id +:poolteam: rhel-sst-csi-client-tools +:caseautomation: Automated +:upstream: No +""" + import os from iniparse import compat as ConfigParser from io import StringIO @@ -56,6 +67,29 @@ def fromstring(self, string, defaults=None): return cf def test_basic(self): + """ + :id: 3c6e489a-8b8b-4f68-a799-fb4b4fe5a0a1 + :title: Basic INI parsing and retrieval + :description: + Tests parsing of multiple INI sections with various formatting styles, + comments, spacing, and internationalized keys. Also tests option removal + and error handling. + :tags: Tier 1 + :steps: + 1. Load a configuration string containing multiple sections and options. + 2. Retrieve and sort the list of sections. + 3. Retrieve option values from multiple sections with various formatting. + 4. Remove an existing option and check that it no longer exists. + 5. Attempt to remove a non-existent option and expect a False return. + 6. Attempt to remove an option from a non-existent section and expect an exception. + :expectedresults: + 1. Configuration is successfully loaded into a parser object. + 2. Section names match the expected list after sorting. + 3. All option values are correctly retrieved and match expected results. + 4. Existing option is successfully removed. + 5. Second removal attempt returns False, indicating option doesn't exist. + 6. Removing from a non-existent section raises NoSectionError. + """ cf = self.fromstring( "[Foo Bar]\n" "foo=bar\n" @@ -117,6 +151,28 @@ def test_basic(self): 'this line is much, much longer than my editor\nlikes it.') def test_case_sensitivity(self): + """ + :id: e8ddc4a3-2c42-42fd-9436-27a64b1f7f5a + :title: Test case sensitivity of section and option names + :description: + Verifies that section names are case-sensitive, while option names are + case-insensitive. + :tags: Tier 1 + :steps: + 1. Add two sections that differ only in case ("A" and "a"). + 2. Set options using mixed-case keys. + 3. Retrieve the option using a different case than used during setting. + 4. Remove an option using a case-variant key. + 5. Parse a multiline option value and retrieve it. + 6. Load config with a default and verify it appears as a fallback. + :expectedresults: + 1. Both sections are added and retained as separate entities. + 2. All mixed-case options are accepted without error. + 3. Options are retrieved correctly regardless of case. + 4. Option is removed successfully using case-insensitive key. + 5. The multiline value is parsed and retrieved correctly. + 6. Option fallback from DEFAULT section works as expected. + """ cf = self.newconfig() cf.add_section("A") cf.add_section("a") @@ -151,6 +207,24 @@ def test_case_sensitivity(self): self.assertTrue(cf.has_option("section", "Key")) def test_default_case_sensitivity(self): + """ + :id: 5f15e8f5-2ac3-4b1f-956c-d441d295e854 + :title: Test case-insensitivity in DEFAULT section + :description: + Ensures DEFAULT section option names are accessible in a case-insensitive + manner. + :tags: Tier 2 + :steps: + 1. Initialize config with default options using lowercase keys. + 2. Retrieve values using uppercase key. + 3. Initialize config with capitalized keys. + 4. Retrieve values using exact key. + :expectedresults: + 1. DEFAULT section is created with lowercase options. + 2. Retrieval using uppercase key returns expected value. + 3. DEFAULT section is created with capitalized options. + 4. Retrieval using same case returns expected value. + """ cf = self.newconfig({"foo": "Bar"}) self.assertEqual( cf.get("DEFAULT", "Foo"), "Bar", @@ -161,6 +235,21 @@ def test_default_case_sensitivity(self): "could not locate option, expecting case-insensitive defaults") def test_parse_errors(self): + """ + :id: 9518b6cf-16b2-48f0-b95a-7c6ccf235981 + :title: Invalid INI parsing raises correct exceptions + :description: + Verifies that malformed INI input raises appropriate exceptions during parsing. + :tags: Tier 2 + :steps: + 1. Attempt to parse a line with extra whitespace before the option. + 2. Attempt to parse a line with missing key but present value. + 3. Attempt to parse a line with missing section header. + :expectedresults: + 1. ParsingError is raised due to bad spacing. + 2. ParsingError is raised due to malformed key-value syntax. + 3. MissingSectionHeaderError is raised due to absent section header. + """ self.newconfig() self.parse_error(ConfigParser.ParsingError, "[Foo]\n extra-spaces: splat\n") @@ -180,6 +269,26 @@ def parse_error(self, exc, src): self.assertRaises(exc, self.cf.readfp, sio) def test_query_errors(self): + """ + :id: d10dbb38-9e33-420f-8d3f-6bcf07e0e6db + :title: Exception handling for missing sections and options + :description: + Verifies correct exception types are raised when accessing or + modifying missing sections/options. + :tags: Tier 2 + :steps: + 1. Call `.sections()` and `.has_section()` on a new config object. + 2. Call `.options()` on a non-existent section. + 3. Attempt to `.set()` an option on a non-existent section. + 4. Attempt to `.get()` an option from a non-existent section. + 5. Add a section and attempt to `.get()` a non-existent option. + :expectedresults: + 1. Empty list is returned; `has_section()` returns False. + 2. NoSectionError is raised. + 3. NoSectionError is raised. + 4. NoSectionError is raised. + 5. NoOptionError is raised. + """ cf = self.newconfig() self.assertEqual(cf.sections(), [], "new ConfigParser should have no defined sections") @@ -203,6 +312,26 @@ def get_error(self, exc, section, option): % (exc.__module__, exc.__name__)) def test_boolean(self): + """ + :id: 46cf56c2-8ae9-4dc7-b760-401cd5f31633 + :title: Boolean value parsing + :description: + Verifies that common truthy and falsy values are parsed correctly + by `getboolean` and invalid ones raise a ValueError. + :tags: Tier 1 + :steps: + 1. Create a config section with values like '1', 'TRUE', 'yes', etc. + 2. Create another section with values like '0', 'False', 'nO', etc. + 3. Add invalid boolean values (e.g., "foo", "2"). + 4. Use `getboolean()` to retrieve and assert truthy/falsy values. + 5. Use `getboolean()` on invalid values and expect exceptions. + :expectedresults: + 1. All truthy values are parsed as True. + 2. All falsy values are parsed as False. + 3. Invalid values are present in the config. + 4. Calls to `getboolean()` return correct boolean results. + 5. Calls to `getboolean()` on invalid values raise ValueError. + """ cf = self.fromstring( "[BOOLTEST]\n" "T1=1\n" @@ -228,12 +357,46 @@ def test_boolean(self): cf.getboolean, 'BOOLTEST', 'e%d' % x) def test_weird_errors(self): + """ + :id: 7240b6f5-63a0-4eaf-a4c5-ef488e59e118 + :title: Duplicate section creation raises exception + :description: + Ensures that adding a section that already exists raises + DuplicateSectionError. + :tags: Tier 2 + :steps: + 1. Create a new configuration object. + 2. Add a section named "Foo". + 3. Attempt to add the same section "Foo" again. + :expectedresults: + 1. Configuration is initialized successfully. + 2. Section "Foo" is added without error. + 3. DuplicateSectionError is raised when trying to re-add + the section. + """ cf = self.newconfig() cf.add_section("Foo") self.assertRaises(ConfigParser.DuplicateSectionError, cf.add_section, "Foo") def test_write(self): + """ + :id: 5796aef7-06ea-4fd6-b983-55902aa8d7d2 + :title: Config write operation preserves formatting + :description: + Verifies that configuration written to a buffer matches + the input formatting exactly. + :tags: Tier 1 + :steps: + 1. Load a multiline configuration string. + 2. Use the `.write()` method to write to a StringIO buffer. + 3. Compare the output string with the expected formatted result. + :expectedresults: + 1. Configuration is parsed without error. + 2. Data is written to the buffer correctly. + 3. Output matches the expected string exactly, preserving line + breaks and spacing. + """ cf = self.fromstring( "[Long Line]\n" "foo: this line is much, much longer than my editor\n" @@ -255,6 +418,24 @@ def test_write(self): ) def test_set_string_types(self): + """ + :id: 5dc6ea85-6cb7-40db-bdb8-c0c82ea8f933 + :title: Setting options using string subclasses + :description: + Ensures that setting option values using subclasses of `str` + works without error. + :tags: Tier 2 + :steps: + 1. Create a configuration with an existing section. + 2. Set an option using a plain string. + 3. Set the same option using a custom string subclass. + 4. Add a new option using both plain string and subclass. + :expectedresults: + 1. Section is created successfully. + 2. Plain string is accepted and set correctly. + 3. Subclass of string is accepted without error. + 4. New option is added successfully with both value types. + """ cf = self.fromstring("[sect]\n" "option1=foo\n") # Check that we don't get an exception when setting values in @@ -267,6 +448,24 @@ class mystr(str): cf.set("sect", "option2", mystr("splat")) def test_read_returns_file_list(self): + """ + :id: a9b5a53c-bb35-4656-9673-df5e258b099a + :title: Read method returns list of successfully read files + :description: + Verifies that the `.read()` method returns a list of files that were + successfully parsed. + :tags: Tier 1 + :steps: + 1. Attempt to read from a valid file and a non-existent file. + 2. Read from a valid file only. + 3. Read from a non-existent file only. + 4. Read from an empty list of files. + :expectedresults: + 1. Only the valid file is returned in the list; the other is skipped. + 2. Valid file is returned and parsed correctly. + 3. Empty list is returned due to file not existing. + 4. Empty list is returned since no files were passed. + """ file1 = test_support.findfile("cfgparser.1") if not os.path.exists(file1): file1 = test_support.findfile("configdata/cfgparser.1") @@ -335,6 +534,24 @@ class ConfigParserTestCase(TestCaseBase): config_class = ConfigParser.ConfigParser def test_interpolation(self): + """ + :id: 9e6235d1-16d1-41b8-bd7d-b8d6c74601c4 + :title: Interpolation resolves nested and recursive values + :description: + Verifies that string interpolation works correctly for nested references + and that exceeding the maximum depth raises the appropriate error. + :tags: Tier 1 + :steps: + 1. Load a configuration with multiple levels of interpolation (1, 9, 10, 11 steps). + 2. Retrieve an interpolated value with 1 level of depth. + 3. Retrieve values with 9 and 10 levels of depth. + 4. Attempt to retrieve a value with 11 levels to trigger InterpolationDepthError. + :expectedresults: + 1. Configuration is parsed successfully. + 2. Interpolation is resolved for 1-level reference. + 3. Interpolation works for deeper nesting up to the limit. + 4. InterpolationDepthError is raised when exceeding the limit. + """ cf = self.get_interpolation_config() eq = self.assertEqual eq(cf.get("Foo", "getname"), "Foo") @@ -346,6 +563,20 @@ def test_interpolation(self): self.get_error(ConfigParser.InterpolationDepthError, "Foo", "bar11") def test_interpolation_missing_value(self): + """ + :id: b7383b3e-55f4-4e9c-b9c5-ef25f5a3f3f9 + :title: Interpolation fails with undefined reference + :description: + Ensures that referencing an undefined variable in interpolation + raises InterpolationError with the correct metadata. + :tags: Tier 2 + :steps: + 1. Load configuration with an option that references an undefined key. + 2. Attempt to retrieve the value of the option. + :expectedresults: + 1. Configuration loads successfully with missing reference. + 2. InterpolationError is raised, containing section, option, and reference name. + """ cf = self.get_interpolation_config() e = self.get_error(ConfigParser.InterpolationError, "Interpolation Error", "name") @@ -354,6 +585,22 @@ def test_interpolation_missing_value(self): self.assertEqual(e.option, "name") def test_items(self): + """ + :id: 9cdd38f1-7000-4032-b3e5-0289ac3c8d3e + :title: Test resolution of interpolated items with defaults + :description: + Verifies that calling `.items()` returns the resolved values including + defaults and interpolated strings. + :tags: Tier 1 + :steps: + 1. Load configuration with default values and references using %(name)s and %(__name__)s. + 2. Call `.items()` on the target section. + 3. Sort the returned key-value pairs and verify correctness. + :expectedresults: + 1. Configuration is parsed without error. + 2. Items are retrieved correctly. + 3. Interpolated values are resolved and included in the output. + """ self.check_items_config([('default', ''), ('getdefault', '||'), ('getname', '|section|'), @@ -361,6 +608,22 @@ def test_items(self): ('name', 'value')]) def test_set_nonstring_types(self): + """ + :id: 065ec0ab-d6ef-4c7c-8dc3-8e8fba187e6a + :title: Setting non-string types raises TypeError unless raw is used + :description: + Ensures that non-string types (int, list, dict) can be stored in raw mode, + but raise TypeError when accessed with interpolation. + :tags: Tier 2 + :steps: + 1. Create a section and set options with int, list, and dict values. + 2. Retrieve each value using raw=True. + 3. Attempt to retrieve each value without raw (interpolation enabled). + :expectedresults: + 1. Non-string types are accepted and stored. + 2. Raw retrieval returns the original type. + 3. Standard retrieval raises TypeError for incompatible types. + """ cf = self.newconfig() cf.add_section('non-string') cf.set('non-string', 'int', 1) @@ -386,6 +649,20 @@ class RawConfigParserTestCase(TestCaseBase): config_class = ConfigParser.RawConfigParser def test_interpolation(self): + """ + :id: 8fc89e68-1d4d-49ad-bb1c-3c66ec1a0730 + :title: RawConfigParser preserves interpolation placeholders + :description: + Ensures that when using RawConfigParser, interpolation references like + %(foo)s are not resolved and are returned as-is. + :tags: Tier 2 + :steps: + 1. Load a configuration with multiple levels of interpolation. + 2. Retrieve the value of each option that contains a placeholder. + :expectedresults: + 1. Configuration loads successfully. + 2. Values are returned with interpolation placeholders untouched. + """ cf = self.get_interpolation_config() eq = self.assertEqual eq(cf.get("Foo", "getname"), "%(__name__)s") @@ -399,6 +676,22 @@ def test_interpolation(self): "something %(with11)s lots of interpolation (11 steps)") def test_items(self): + """ + :id: 2ed8ea5d-0a1e-47f4-b92f-1822b13a3ec8 + :title: RawConfigParser returns unresolved values in items + :description: + Verifies that `.items()` returns literal values including unresolved + interpolation placeholders in RawConfigParser. + :tags: Tier 2 + :steps: + 1. Load configuration with default values and interpolation references. + 2. Call `.items()` on the target section. + 3. Sort the result and verify that values include interpolation syntax. + :expectedresults: + 1. Configuration is parsed successfully. + 2. Items are returned without resolving any interpolation. + 3. Output matches expected literal strings with placeholders. + """ self.check_items_config([('default', ''), ('getdefault', '|%(default)s|'), ('getname', '|%(__name__)s|'), @@ -406,6 +699,22 @@ def test_items(self): ('name', 'value')]) def test_set_nonstring_types(self): + """ + :id: 4d6bd9a0-6505-4628-96d3-362a3a055fb6 + :title: RawConfigParser allows non-string types + :description: + Ensures that RawConfigParser can accept and return non-string types + without raising TypeError. + :tags: Tier 2 + :steps: + 1. Create a new section in the config. + 2. Set options with int, list, and dict values. + 3. Retrieve each value using standard `.get()`. + :expectedresults: + 1. Section is added without issues. + 2. Non-string values are stored successfully. + 3. Retrieval returns values of original type with no error. + """ cf = self.newconfig() cf.add_section('non-string') cf.set('non-string', 'int', 1) @@ -421,6 +730,23 @@ class SafeConfigParserTestCase(ConfigParserTestCase): config_class = ConfigParser.SafeConfigParser def test_safe_interpolation(self): + """ + :id: c9e5d19e-fb3c-456a-9f6c-97b9e758ec26 + :title: SafeConfigParser resolves safe interpolation syntax + :description: + Verifies that SafeConfigParser can correctly handle interpolation + involving percent signs and preserves literal percent sequences. + :tags: Tier 1 + :steps: + 1. Load configuration with interpolated values that include + both placeholders and escaped percent signs. + 2. Retrieve an interpolated value using `%(option1)s`. + 3. Retrieve a value with a literal percent using `%%s`. + :expectedresults: + 1. Configuration is parsed successfully. + 2. Placeholder interpolation is resolved. + 3. Escaped percent sequence is preserved in the final output. + """ # See http://www.python.org/sf/511737 cf = self.fromstring("[section]\n" "option1=xxx\n" @@ -431,6 +757,24 @@ def test_safe_interpolation(self): self.assertEqual(cf.get("section", "not_ok"), "xxx/xxx/%s") def test_set_malformatted_interpolation(self): + """ + :id: b4a5c7bc-8231-4416-99dc-84d6f42a0fcf + :title: Malformed interpolation strings raise ValueError + :description: + Ensures that setting options with malformed interpolation syntax + (e.g. single % without a key) raises a ValueError. + :tags: Tier 2 + :steps: + 1. Create a section and set an initial valid string. + 2. Attempt to set a string with an unmatched % token (e.g. '%foo'). + 3. Attempt to set another with trailing % (e.g. 'foo%'). + 4. Attempt to set a mid-string malformed token (e.g. 'f%oo'). + :expectedresults: + 1. Initial string is set successfully. + 2. ValueError is raised when using '%foo'. + 3. ValueError is raised when using 'foo%'. + 4. ValueError is raised when using 'f%oo'. + """ cf = self.fromstring("[sect]\n" "option1=foo\n") @@ -443,6 +787,22 @@ def test_set_malformatted_interpolation(self): self.assertEqual(cf.get('sect', "option1"), "foo") def test_set_nonstring_types(self): + """ + :id: f2c70993-f539-4cb5-92a2-1ff97d3e4891 + :title: SafeConfigParser disallows non-string values + :description: + Ensures that SafeConfigParser enforces string-only values and + raises TypeError otherwise. + :tags: Tier 2 + :steps: + 1. Create a config section. + 2. Attempt to set int, float, and object as option values. + 3. Attempt to set a new option with each non-string type. + :expectedresults: + 1. Section is created successfully. + 2. TypeError is raised for int, float, and object assignments. + 3. New options are not added if value is non-string. + """ cf = self.fromstring("[sect]\n" "option1=foo\n") # Check that we get a TypeError when setting non-string values @@ -455,10 +815,38 @@ def test_set_nonstring_types(self): self.assertRaises(TypeError, cf.set, "sect", "option2", object()) def test_add_section_default_1(self): + """ + :id: 95174993-fab5-4a3d-8dc2-d9449828b848 + :title: Adding section named 'default' is invalid + :description: + Verifies that attempting to add a section named "default" + (lowercase) raises a ValueError. + :tags: Tier 3 + :steps: + 1. Create a SafeConfigParser instance. + 2. Attempt to add a section named 'default'. + :expectedresults: + 1. Parser is created successfully. + 2. ValueError is raised due to reserved section name. + """ cf = self.newconfig() self.assertRaises(ValueError, cf.add_section, "default") def test_add_section_default_2(self): + """ + :id: 4a9a77a3-003c-4f6f-93cb-d63f45a76e4a + :title: Adding section named 'DEFAULT' is invalid + :description: + Verifies that adding the reserved 'DEFAULT' section explicitly + raises a ValueError. + :tags: Tier 3 + :steps: + 1. Create a SafeConfigParser instance. + 2. Attempt to add a section named 'DEFAULT'. + :expectedresults: + 1. Parser is initialized successfully. + 2. ValueError is raised when using the reserved name. + """ cf = self.newconfig() self.assertRaises(ValueError, cf.add_section, "DEFAULT") @@ -470,6 +858,23 @@ def newconfig(self, defaults=None): return self.cf def test_sorted(self): + """ + :id: 64284591-981b-440b-8649-b0d8c8fbc7d6 + :title: Sorted dictionary preserves section and option order + :description: + Ensures that using a sorted dictionary for storage preserves + the ordering of sections and keys when the config is written + to a string. + :tags: Tier 3 + :steps: + 1. Create a configuration with multiple sections and unordered keys. + 2. Write the configuration to a string. + 3. Compare the output against expected sorted section and option order. + :expectedresults: + 1. Configuration is stored with unordered insertion. + 2. Output is written successfully. + 3. Output shows sections and options sorted alphabetically. + """ self.fromstring("[b]\n" "o4=1\n" "o3=2\n" diff --git a/tests/test_fuzz.py b/tests/test_fuzz.py index 2604298..305a348 100644 --- a/tests/test_fuzz.py +++ b/tests/test_fuzz.py @@ -1,3 +1,14 @@ +""" +:component: python-iniparse +:requirement: RHSS-291606 +:polarion-project-id: RHELSS +:polarion-include-skipped: false +:polarion-lookup-method: id +:poolteam: rhel-sst-csi-client-tools +:caseautomation: Automated +:upstream: No +""" + import re import os import random @@ -73,6 +84,27 @@ def random_ini_file(): class TestFuzz(unittest.TestCase): def test_fuzz(self): + """ + :id: 144b65b2-74fa-4bc3-97b5-861e9c011af3 + :title: Fuzz test for INIConfig parser robustness + :description: + Performs fuzz testing by generating randomized INI-formatted input containing + a mix of comments, sections, options, and multiline values. Verifies that parsing + and serialization do not raise exceptions and preserve config integrity. + :tags: Tier 1 + :steps: + 1. Generate random key-value pairs, section headers, and comments using random tokens. + 2. Construct a large synthetic INI string by combining the elements in random order. + 3. Parse the synthetic string using `INIConfig`. + 4. Convert the resulting config back to a string. + 5. Verify that the output can be parsed again without error. + :expectedresults: + 1. INI string is generated successfully with randomized components. + 2. A large synthetic INI string is constructed + 3. Parser handles all elements without raising exceptions. + 4. Output string is generated without error. + 5. Re-parsing the output string succeeds, indicating parser stability. + """ random.seed(42) try: num_iter = int(os.environ['INIPARSE_FUZZ_ITERATIONS']) diff --git a/tests/test_ini.py b/tests/test_ini.py index 7c0d20d..359a2b9 100644 --- a/tests/test_ini.py +++ b/tests/test_ini.py @@ -1,3 +1,14 @@ +""" +:component: python-iniparse +:requirement: RHSS-291606 +:polarion-project-id: RHELSS +:polarion-include-skipped: false +:polarion-lookup-method: id +:poolteam: rhel-sst-csi-client-tools +:caseautomation: Automated +:upstream: No +""" + import unittest from io import StringIO @@ -20,13 +31,27 @@ class TestSectionLine(unittest.TestCase): ] def test_invalid(self): + """ + :id: b04b3e76-0c9e-4eb4-a9cd-6a1f4d527e88 + :title: SectionLine rejects invalid lines + :description: + Verifies that lines which are not valid section headers + return None when parsed by SectionLine. + :tags: Tier 2 + :steps: + 1. Define a list of malformed or non-section strings. + 2. Attempt to parse each string using SectionLine.parse(). + :expectedresults: + 1. Each invalid line returns None when parsed. + 2. No exception is raised during parsing + """ for l in self.invalid_lines: p = ini.SectionLine.parse(l) self.assertEqual(p, None) lines = [ ('[section]', ('section', None, None, -1)), - ('[se\ct%[ion\t]', ('se\ct%[ion\t', None, None, -1)), + (r'[se\ct%[ion\t]', (r'se\ct%[ion\t', None, None, -1)), ('[sec tion] ; hi', ('sec tion', ' hi', ';', 12)), ('[section] #oops!', ('section', 'oops!', '#', 11)), ('[section] ; ', ('section', '', ';', 12)), @@ -34,6 +59,25 @@ def test_invalid(self): ] def test_parsing(self): + """ + :id: 62aa28db-f730-4b67-85b9-cd1ee4fa2186 + :title: SectionLine parses valid lines with comments + :description: + Verifies that SectionLine correctly extracts the section + name and optional comment details. + :tags: Tier 2 + :steps: + 1. Create a list of valid comment lines starting with + '#', ';', or 'Rem'. + 2. Parse each comment line using CommentLine.parse(). + 3. Convert each parsed line to a string using `str()`. + 4. Convert each parsed line using `to_string()`. + :expectedresults: + 1. All input lines are valid comment lines. + 2. Parsing returns a valid CommentLine object for each line. + 3. `str()` returns the original unmodified comment line. + 4. `to_string()` returns the line stripped of trailing whitespace. + """ for l in self.lines: p = ini.SectionLine.parse(l[0]) self.assertNotEqual(p, None) @@ -43,6 +87,20 @@ def test_parsing(self): self.assertEqual(p.comment_offset, l[1][3]) def test_printing(self): + """ + :id: fcb270b2-65c0-4485-9151-9791ae35cbe4 + :title: SectionLine preserves formatting on output + :description: + Ensures that calling `str()` or `to_string()` on a SectionLine returns + the original or stripped line. + :tags: Tier 2 + :steps: + 1. Parse a list of valid section lines. + 2. Call `str()` and `to_string()` on the result. + :expectedresults: + 1. `str()` returns the exact original line. + 2. `to_string()` returns the line with trailing spaces removed. + """ for l in self.lines: p = ini.SectionLine.parse(l[0]) self.assertEqual(str(p), l[0]) @@ -58,6 +116,22 @@ def test_printing(self): ] def test_preserve_indentation(self): + """ + :id: 2ebeff23-b508-4307-8164-16a1e405708c + :title: SectionLine preserves spacing when name changes + :description: + Verifies that changing a section name maintains the original + alignment and indentation of the line. + :tags: Tier 2 + :steps: + 1. Parse a section line with a specific alignment and comment. + 2. Change the section name to something longer or shorter. + 3. Convert the object back to a string. + :expectedresults: + 1. Section line with a specific alignment and comment is parsed + 2. Name changes are applied correctly. + 3. Output preserves spacing and comment alignment. + """ for l in self.indent_test_lines: p = ini.SectionLine.parse(l[0]) p.name = l[1] @@ -83,6 +157,26 @@ class TestOptionLine(unittest.TestCase): ';', '; comm;ent', 22), ] def test_parsing(self): + """ + :id: 4d2a847f-65f6-49ff-9637-2a6b1b6a7e65 + :title: OptionLine parses valid lines and extracts components correctly + :description: + Verifies that OptionLine correctly parses option name, separator, value + and optional comment fields from a variety of valid formats. + :tags: Tier 2 + :steps: + 1. Define multiple valid option lines using different separators and + comment placements. + 2. Parse each line using OptionLine.parse(). + 3. Extract the name, separator, and value from each parsed line. + 4. Extract optional comment separator, comment text, and comment offset. + :expectedresults: + 1. Each line is accepted as valid input. + 2. Parsing returns a valid OptionLine object. + 3. The name, separator, and value match expected values. + 4. Comment-related fields are either correctly populated or None, + depending on input. + """ for l in self.lines: p = ini.OptionLine.parse(l[0]) self.assertEqual(p.name, l[1]) @@ -102,6 +196,21 @@ def test_parsing(self): ] def test_invalid(self): + """ + :id: b56e1536-3270-4dd1-b5d1-d6e1a93b982a + :title: OptionLine rejects invalid or misformatted lines + :description: + Ensures that lines which do not match valid option syntax are + rejected and return None when parsed. + :tags: Tier 2 + :steps: + 1. Define lines that resemble options but are either misaligned, + incomplete, or structurally invalid. + 2. Attempt to parse each line using OptionLine.parse(). + :expectedresults: + 1. Each invalid line is correctly identified as malformed. + 2. Parsing returns None for each of them. + """ for l in self.invalid_lines: p = ini.OptionLine.parse(l) self.assertEqual(p, None) @@ -118,6 +227,25 @@ def test_invalid(self): ] def test_printing(self): + """ + :id: 17d9d243-fabe-4f83-976b-1b1e10f39c2a + :title: OptionLine preserves formatting during string conversion + :description: + Verifies that the string representation of OptionLine objects matches + the original input formatting and spacing. + :tags: Tier 2 + :steps: + 1. Define a list of option lines with varying formats (spacing, + separators, and comments). + 2. Parse each line using OptionLine.parse(). + 3. Call `str()` on the parsed object and compare with the original line. + 4. Call `to_string()` and compare it with the stripped version + of the original line. + :expectedresults: + 1. All lines are parsed successfully. + 2. `str()` returns the exact original input including spacing. + 3. `to_string()` returns the same line but without trailing whitespace. + """ for l in self.print_lines: p = ini.OptionLine.parse(l) self.assertEqual(str(p), l) @@ -131,6 +259,24 @@ def test_printing(self): ] def test_preserve_indentation(self): + """ + :id: 31275c63-cda1-4726-bc9b-08fd9b2cf8e4 + :title: OptionLine preserves spacing and alignment when modified + :description: + Ensures that after modifying the option name and value, the output string + retains the original alignment and spacing of the comment. + :tags: Tier 2 + :steps: + 1. Parse an option line that contains an aligned comment. + 2. Change the option name and value. + 3. Convert the modified OptionLine to a string. + 4. Compare it to the expected output with correct alignment. + :expectedresults: + 1. The line is parsed successfully and the comment position is captured. + 2. New name and value are assigned without breaking structure. + 3. `str()` returns a correctly formatted line with updated values. + 4. Alignment between value and comment remains consistent. + """ for l in self.indent_test_lines: p = ini.OptionLine.parse(l[0]) p.name = l[1] @@ -146,6 +292,23 @@ class TestCommentLine(unittest.TestCase): ] def test_invalid(self): + """ + :id: e492f9f0-0b70-466f-94d8-33ad716aa5a6 + :title: CommentLine rejects lines that are not valid comments + :description: + Verifies that lines which do not start with a valid comment prefix in + column 0 are not parsed as CommentLine objects. + :tags: Tier 2 + :steps: + 1. Define a list of lines that resemble options, sections + or indented comments. + 2. Attempt to parse each line using CommentLine.parse(). + :expectedresults: + 1. Each input line is valid for testing (e.g., starts with + `[`, contains `=`, or is indented). + 2. Parsing returns None for each line, confirming they are not recognized + as valid comments. + """ for l in self.invalid_lines: p = ini.CommentLine.parse(l) self.assertEqual(p, None) @@ -158,6 +321,24 @@ def test_invalid(self): ] def test_parsing(self): + """ + :id: d14f3d15-4c04-4718-9e79-06661b84b514 + :title: CommentLine correctly parses and preserves formatting + :description: + Ensures that valid comment lines starting with `#`, `;`, or `Rem` are + parsed as CommentLine objects and retain their exact original formatting. + :tags: Tier 2 + :steps: + 1. Define a list of valid comment lines with various prefixes and spacing. + 2. Parse each line using CommentLine.parse(). + 3. Convert the parsed object back to a string using `str()`. + 4. Convert the parsed object using `to_string()`. + :expectedresults: + 1. Each line is recognized as a valid comment format. + 2. Parsing returns a valid CommentLine object. + 3. `str()` returns the exact original line including spacing. + 4. `to_string()` returns the line without trailing whitespace. + """ for l in self.lines: p = ini.CommentLine.parse(l) self.assertEqual(str(p), l) @@ -166,12 +347,54 @@ def test_parsing(self): class TestOtherLines(unittest.TestCase): def test_empty(self): + """ + :id: c5e24b8c-fb0d-4bd1-970a-fc979e6493c7 + :title: EmptyLine identifies only blank or whitespace-only lines + :description: + Ensures that only lines which contain no visible characters (or just whitespace) + are recognized as EmptyLine, and all other lines return None. + :tags: Tier 2 + :steps: + 1. Create a list of lines that contain text, options, or section headers. + 2. Parse each of those lines using EmptyLine.parse(). + 3. Create a list of lines that are completely empty or contain only whitespace. + 4. Parse each of those lines using EmptyLine.parse(). + 5. Convert each valid EmptyLine to string using `str()`. + :expectedresults: + 1. Lines with text are correctly identified as invalid for EmptyLine. + 2. Parsing returns None for each non-empty line. + 3. Whitespace-only lines are accepted as valid EmptyLine instances. + 4. Parsing returns a valid object for empty/whitespace lines. + 5. String conversion of EmptyLine objects returns the original whitespace line. + """ for s in ['asdf', '; hi', ' #rr', '[sec]', 'opt=val']: self.assertEqual(ini.EmptyLine.parse(s), None) for s in ['', ' ', '\t \t']: self.assertEqual(str(ini.EmptyLine.parse(s)), s) def test_continuation(self): + """ + :id: 01a2f7ec-6b43-4c69-8776-05a2c999ea6f + :title: ContinuationLine identifies and preserves indented lines + :description: + Verifies that lines with leading whitespace are recognized as continuation lines, + and that their value and formatting are preserved correctly. + :tags: Tier 2 + :steps: + 1. Create a list of lines that are clearly not continuation lines + (e.g. section headers, options, comments). + 2. Parse each of these using ContinuationLine.parse(). + 3. Create a list of indented lines containing values. + 4. Parse each using ContinuationLine.parse() and access `.value`. + 5. Convert the parsed objects using `to_string()`. + :expectedresults: + 1. Lines not intended as continuations are rejected. + 2. Parsing returns None for those lines. + 3. Indented lines are accepted as continuation lines. + 4. The `.value` returns the stripped value of the line. + 5. The `to_string()` returns the line without trailing whitespace + and with tabs replaced by spaces. + """ for s in ['asdf', '; hi', '[sec]', 'a=3']: self.assertEqual(ini.ContinuationLine.parse(s), None) for s in [' asdfasd ', '\t mmmm']: @@ -199,6 +422,34 @@ class TestIni(unittest.TestCase): """ def test_basic(self): + """ + :id: 7d6fd4eb-374f-4b17-9783-318d9d3fdde4 + :title: INIConfig parses repeated sections and preserves line numbers + :description: + Verifies that INIConfig correctly handles multiple repeated sections, resolves + values based on last occurrence, and tracks the correct line numbers for options. + :tags: Tier 1 + :steps: + 1. Load a configuration string with two '[section1]' blocks and one '[section2]'. + 2. Use internal `.find()` method to retrieve specific options from + the parsed config. + 3. Assert that each option contains the correct final value. + 4. Assert that each option has the correct original line number. + 5. Create an iterator for all '[section1]' blocks using '.finditer()'. + 6. Iterate through values and validate correct content from both blocks. + 7. Assert that the iterator raises StopIteration at the end. + 8. Attempt to access a missing section and option to ensure correct + exception handling. + :expectedresults: + 1. Config is parsed without error and recognizes both section1 and section2. + 2. `.find()` correctly locates each option. + 3. Final values reflect the last assigned value in repeated sections. + 4. Each option has the expected line number from the input string. + 5. Iterator over '[section1]' entries yields both blocks in correct order. + 6. Values from each block match expected keys and values. + 7. StopIteration is correctly raised after the last section. + 8. Missing sections/options raise KeyError as expected. + """ sio = StringIO(self.s1) p = ini.INIConfig(sio) section1_but = p._data.find('section1').find('but') @@ -224,6 +475,26 @@ def test_basic(self): self.assertRaises(KeyError, p._data.find('section2').find, 'ahem') def test_lookup(self): + """ + :id: 1b264c55-82f2-4c9a-a0a2-3f4f48f4c0ec + :title: INIConfig attribute-style access returns correct values + :description: + Verifies that values can be accessed using attribute-style access + (`config.section.option`) and that undefined options return an + `Undefined` placeholder object. + :tags: Tier 1 + :steps: + 1. Load a config string with multiple sections and options. + 2. Access defined options using dot notation (e.g., `p.section1.help`). + 3. Access a key with an apostrophe via `getattr()`. + 4. Access options that are not defined. + :expectedresults: + 1. Configuration is parsed correctly. + 2. All defined keys return their correct values using attribute-style + access. + 3. Special character keys (like `"I'm"`) are accessible via `getattr`. + 4. Accessing undefined options returns an `Undefined` instance. + """ sio = StringIO(self.s1) p = ini.INIConfig(sio) self.assertEqual(p.section1.help, 'yourself') @@ -235,6 +506,30 @@ def test_lookup(self): self.assertEqual(p.section2.help.__class__, config.Undefined) def test_newsection(self): + """ + :id: 19f37957-7ef1-4db3-b924-d60e32ccf038 + :title: New sections and options can be added using multiple access styles + :description: + Verifies that new sections and options can be added dynamically using various + syntaxes including attribute-style, dictionary-style, and mixed forms. + :tags: Tier 1 + :steps: + 1. Load the configuration string into an INIConfig object. + 2. Add a new section and key using dot notation (`p.new1.created = 1`). + 3. Add a new section and key using `getattr()` and `setattr()`. + 4. Add a new section and key using dictionary-style access '(`p.new3['created'] = 1`)'. + 5. Add a new section and key using bracket and attribute '(`p['new4'].created = 1`)'. + 6. Add a new section and key using double-bracket dictionary access. + 7. Retrieve all newly added values to confirm they were stored. + :expectedresults: + 1. Configuration is parsed successfully. + 2. Section and key are added using dot notation. + 3. Section and key are added using dynamic attribute access. + 4. Section and key are added using dictionary access. + 5. Mixed syntax access adds key successfully. + 6. All key-value pairs are added correctly. + 7. Each key returns the expected value when accessed. + """ sio = StringIO(self.s1) p = ini.INIConfig(sio) p.new1.created = 1 @@ -249,6 +544,24 @@ def test_newsection(self): self.assertEqual(p.new5.created, 1) def test_order(self): + """ + :id: a7e8cabc-3c29-4692-8b52-22570cdb2f46 + :title: INIConfig preserves the order of sections and options + :description: + Ensures that when iterating over sections and options, the original order + in the config is maintained. + :tags: Tier 1 + :steps: + 1. Load the configuration into an INIConfig object. + 2. Iterate over the top-level sections using `list(p)`. + 3. Iterate over the options in `section1` using `list(p.section1)`. + 4. Iterate over the options in `section2` using `list(p.section2)`. + :expectedresults: + 1. The config is parsed correctly. + 2. The section order is preserved (`['section1', 'section2']`). + 3. The options in `section1` appear in the correct order. + 4. The options in `section2` appear in the correct order. + """ sio = StringIO(self.s1) p = ini.INIConfig(sio) self.assertEqual(list(p), ['section1','section2']) @@ -256,6 +569,29 @@ def test_order(self): self.assertEqual(list(p.section2), ['just']) def test_delete(self): + """ + :id: 740aa7c4-6c2c-4e7d-bef4-5fd15a28b50d + :title: Deleting sections and options updates the config structure and string output + :description: + Verifies that deleting options and sections removes them from the + structure and updates the string representation accordingly, while + preserving remaining content and formatting. + :tags: Tier 1 + :steps: + 1. Load the config string into an INIConfig object. + 2. Delete the option `help` from `section1`. + 3. Convert the config to a string and verify the output. + 4. Delete the entire section `section2`. + 5. Convert the config again and verify the final output. + :expectedresults: + 1. Config is parsed without error. + 2. The option `help` is removed from the first section1 block. + 3. String output reflects the removal and retains formatting + and comments. + 4. Section2 is fully removed from the config. + 5. Final output string reflects both changes while preserving + formatting of remaining content. + """ sio = StringIO(self.s1) p = ini.INIConfig(sio) del p.section1.help @@ -297,6 +633,27 @@ def check_order(self, c): ]) def test_compat_order(self): + """ + :id: 2fd01fc8-d891-49c5-a7d7-1c8b5d501af0 + :title: ConfigParser compatibility preserves section and option order + :description: + Verifies that `RawConfigParser` and `ConfigParser` from the + iniparse compatibility layer preserve the correct order of sections + and options and correctly merge default values. + :tags: Tier 1 + :steps: + 1. Initialize the parser with a default option using `{'pi': '3.14153'}`. + 2. Read the configuration string using `.readfp()`. + 3. List all section names and assert their order. + 4. List all option names in section1 and assert their order including defaults. + 5. List all items (key-value pairs) in section1 and assert the content and order. + :expectedresults: + 1. The parser is initialized correctly with default values. + 2. Configuration is read without errors. + 3. Section order matches expected order from the config. + 4. All expected options are present and ordered as written. + 5. Default values appear last and are included in the item list with correct values. + """ self.check_order(compat.RawConfigParser) self.check_order(compat.ConfigParser) @@ -341,6 +698,27 @@ def test_compat_order(self): """)) def test_invalid(self): + """ + :id: e6d2ccfb-1b42-4cfd-81c4-e3cfa0e9f7be + :title: INIConfig handles invalid input lines by converting them to comments + :description: + Verifies that invalid INI syntax such as values outside sections or + misplaced continuation lines are preserved as commented lines when + `parse_exc=False` is used. + :tags: Tier 1 + :steps: + 1. Define two configuration strings with invalid syntax: A value outside + any section and Continuation lines not following any option. + 2. Load each configuration using `INIConfig(StringIO(...), parse_exc=False)`. + 3. Convert each loaded configuration back to a string. + 4. Compare the string output to expected versions where invalid lines + are commented out. + :expectedresults: + 1. Input strings are created and contain intentional syntax errors. + 2. INIConfig loads the input without raising exceptions due to `parse_exc=False`. + 3. Output string conversion succeeds. + 4. Invalid lines are preserved and transformed into comments in the final output. + """ for (org, mod) in self.inv: ip = ini.INIConfig(StringIO(org), parse_exc=False) self.assertEqual(str(ip), mod) @@ -372,6 +750,28 @@ def test_invalid(self): ) def test_option_continuation(self): + """ + :id: 3aa8a403-f129-44b9-b8b7-52c2b3cc0e4f + :title: INIConfig preserves and updates multi-line option values + :description: + Verifies that multi-line option values are preserved during parsing and + can be updated programmatically while maintaining formatting. + :tags: Tier 1 + :steps: + 1. Load a configuration string with a single option value spread over multiple lines. + 2. Confirm that converting the config back to a string yields the same format. + 3. Parse the multi-line value using .split. + 4. Modify the value by inserting a new line. + 5. Update the config with the new value. + 6. Convert the config to a string and verify the updated format. + :expectedresults: + 1. Configuration is parsed successfully and contains a multi-line value. + 2. The string representation of the config matches the original input. + 3. The value is correctly split into a list of lines. + 4. A new line is inserted into the correct position. + 5. The modified value is saved back to the config without error. + 6. The output string reflects the updated multi-line value and preserves formatting. + """ ip = ini.INIConfig(StringIO(self.s2)) self.assertEqual(str(ip), self.s2) value = ip.section.option.split('\n') @@ -403,6 +803,31 @@ def test_option_continuation(self): ) def test_option_continuation_single(self): + """ + :id: 52fa9d9a-4f7c-43a0-9b84-9c3f005f6632 + :title: INIConfig handles sparse multi-line values with empty lines + :description: + Verifies that an option value consisting of a multi-line string with + blank lines is preserved correctly when parsed, modified, and serialized. + :tags: Tier 1 + :steps: + 1. Load a configuration string where an option's value + spans multiple lines. + 2. Confirm that converting the config back to a string matches + the original input. + 3. Update the value with additional blank lines and content between them. + 4. Add another option to the section. + 5. Convert the updated config to a string. + 6. Verify that the final output preserves the new spacing and both options. + :expectedresults: + 1. Configuration is parsed and multi-line value is detected. + 2. Output string matches the original formatting. + 3. Updated value with blank lines is assigned successfully. + 4. Additional option is added without error. + 5. Config is converted to string without formatting issues. + 6. Output includes both the updated multi-line value and the + new option, with correct spacing. + """ ip = ini.INIConfig(StringIO(self.s5)) self.assertEqual(str(ip), self.s5) ip.section.option = '\n'.join(['', '', '', 'foo', '', '', '']) diff --git a/tests/test_misc.py b/tests/test_misc.py index 7ef4b7a..b54af40 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -1,3 +1,14 @@ +""" +:component: python-iniparse +:requirement: RHSS-291606 +:polarion-project-id: RHELSS +:polarion-include-skipped: false +:polarion-lookup-method: id +:poolteam: rhel-sst-csi-client-tools +:caseautomation: Automated +:upstream: No +""" + import re import unittest import pickle @@ -8,7 +19,9 @@ class CaseSensitiveConfigParser(compat.ConfigParser): - """Case Sensitive version of ConfigParser""" + """ + :description: Case Sensitive version of ConfigParser + """ def optionxform(self, option): """Use str()""" return str(option) @@ -16,6 +29,22 @@ def optionxform(self, option): class TestOptionxFormOverride(unittest.TestCase): def test_derivedclass(self): + """ + :id: f1e4d841-d527-48bc-81c2-dcf7f4e5f7b6 + :title: Optionxform override in derived class respects case sensitivity + :description: + Verifies that subclassing ConfigParser and overriding optionxform + preserves key casing and allows retrieval of similarly-named keys. + :tags: Tier 2 + :steps: + 1. Create a subclass of ConfigParser with case-sensitive optionxform. + 2. Add section `foo` and set two options: `bar` and `Bar` with different values. + 3. Retrieve both keys and verify correct values are returned. + :expectedresults: + 1. Parser preserves case-sensitive keys due to override. + 2. Keys `bar` and `Bar` are stored and retrieved independently. + 3. Values `'a'` and `'b'` are returned for `bar` and `Bar` respectively. + """ c = CaseSensitiveConfigParser() c.add_section('foo') c.set('foo', 'bar', 'a') @@ -24,6 +53,22 @@ def test_derivedclass(self): self.assertEqual(c.get('foo', 'Bar'), 'b') def test_assignment(self): + """ + :id: 9b3226b0-2042-4fa1-a02a-34bb4f1a4a83 + :title: Direct assignment of optionxform preserves case-sensitive keys + :description: + Verifies that setting optionxform manually on a ConfigParser instance + enables storing and retrieving multiple keys with differing cases. + :tags: Tier 2 + :steps: + 1. Create a ConfigParser instance and assign `str` to `optionxform`. + 2. Add section `foo` and set keys `bar` and `Bar` with different values. + 3. Retrieve both keys and check values. + :expectedresults: + 1. ConfigParser accepts manual override of optionxform. + 2. Keys `bar` and `Bar` are stored as distinct. + 3. Retrieved values match assigned ones. + """ c = compat.ConfigParser() c.optionxform = str c.add_section('foo') @@ -33,6 +78,26 @@ def test_assignment(self): self.assertEqual(c.get('foo', 'Bar'), 'b') def test_dyanamic(self): + """ + :id: 30b18b29-2e11-43e1-9615-2739021c4910 + :title: Dynamically changing optionxform affects key resolution + :description: + Verifies that dynamically changing optionxform at runtime alters + how keys are resolved, demonstrating case folding or preserving behavior. + :tags: Tier 2 + :steps: + 1. Create ConfigParser and set `optionxform = str`, then insert keys + with various cases. + 2. Change `optionxform` to `str.upper` and check lookup behavior. + 3. Change to `str.lower` and repeat the lookup. + 4. Restore `optionxform = str` and verify final resolution. + :expectedresults: + 1. All keys are stored successfully with original casing. + 2. Uppercase transformation causes lookup to resolve to latest + uppercase match. + 3. Lowercase transformation resolves to lowercase match. + 4. Resetting to `str` resolves the key to original mixed-case match. + """ c = compat.ConfigParser() c.optionxform = str c.add_section('foo') @@ -56,11 +121,13 @@ def readline(self): class TestReadline(unittest.TestCase): - """Test that the file object passed to readfp only needs to - support the .readline() method. As of Python-2.4.4, this is - true of the standard librariy's ConfigParser, and so other - code uses that to guide what is sufficiently file-like.""" - + """ + :description: + Test that the file object passed to readfp only needs to + support the .readline() method. As of Python-2.4.4, this is + true of the standard librariy's ConfigParser, and so other + code uses that to guide what is sufficiently file-like. + """ test_strings = [ """\ [foo] @@ -78,6 +145,25 @@ class TestReadline(unittest.TestCase): """] def test_readline_iniconfig(self): + """ + :id: 56ae3f56-b690-4a0f-85c7-fd57e4f874b6 + :title: INIConfig supports file-like input with only readline() + :description: + Verifies that INIConfig can read configuration from an object + that only implements the `readline()` method. + :tags: Tier 2 + :steps: + 1. Create a fake file-like object implementing only `readline()` + with valid INI content. + 2. Pass it to `INIConfig._readfp()` for parsing. + 3. Convert the resulting config back to string. + 4. Compare it with the original input. + :expectedresults: + 1. The file-like object is initialized correctly. + 2. INIConfig parses the input without error. + 3. The output string is generated successfully. + 4. The output exactly matches the original input. + """ for s in self.test_strings: fp = OnlyReadline(s) c = ini.INIConfig() @@ -85,6 +171,25 @@ def test_readline_iniconfig(self): self.assertEqual(s, str(c)) def test_readline_configparser(self): + """ + :id: 7e0e278e-9e9f-4a1d-92b5-97cc244d287f + :title: ConfigParser supports file-like input with only readline() + :description: + Verifies that the ConfigParser-compatible interface accepts file-like + objects implementing only `readline()` and maintains content correctly. + :tags: Tier 2 + :steps: + 1. Create a fake file-like object implementing only `readline()` with + valid INI content. + 2. Pass it to `ConfigParser.readfp()` for parsing. + 3. Write the parsed config to a new string buffer. + 4. Compare the output with the original input. + :expectedresults: + 1. The file-like object is created correctly. + 2. ConfigParser parses the input without error. + 3. The output is written to buffer successfully. + 4. The output matches the original configuration input. + """ for s in self.test_strings: fp = OnlyReadline(s) c = compat.ConfigParser() @@ -95,8 +200,9 @@ def test_readline_configparser(self): class TestMultilineWithComments(unittest.TestCase): - """Test that multiline values are allowed to span comments.""" - + """ + :description: Test that multiline values are allowed to span comments. + """ s = """\ [sec] opt = 1 @@ -106,11 +212,44 @@ class TestMultilineWithComments(unittest.TestCase): 3""" def test_read(self): + """ + :id: 2c3b60a5-9e31-4411-a027-6e81c47b37a1 + :title: Multiline values span comments correctly on read + :description: + Verifies that `INIConfig` allows multi-line option values to continue + even after blank lines or comments. + :tags: Tier 2 + :steps: + 1. Create an INI string where a value spans multiple lines and includes + an inline comment and blank line. + 2. Load the config into `INIConfig` using `_readfp`. + 3. Retrieve the multiline option value. + :expectedresults: + 1. The config is loaded correctly without error. + 2. Multiline content is preserved across blank lines and comments. + 3. The value matches the expected multiline string. + """ c = ini.INIConfig() c._readfp(StringIO(self.s)) self.assertEqual(c.sec.opt, '1\n2\n\n3') def test_write(self): + """ + :id: 7d8d14ec-8ac1-41fd-8035-e117800b3be4 + :title: Writing replaces multiline value with new single-line string + :description: + Verifies that assigning a new value to a previously multiline option + replaces it with a single-line value in the output. + :tags: Tier 2 + :steps: + 1. Load the multiline config into `INIConfig`. + 2. Assign a single-line value to the multiline option. + 3. Convert the updated config to a string. + :expectedresults: + 1. The config is loaded correctly with the original multiline value. + 2. The new value replaces the old one without error. + 3. The output string contains the updated single-line option. + """ c = ini.INIConfig() c._readfp(StringIO(self.s)) c.sec.opt = 'xyz' @@ -120,16 +259,47 @@ def test_write(self): class TestEmptyFile(unittest.TestCase): - """Test if it works with an blank file""" - + """ + :description: Test if it works with a blank file. + """ s = "" def test_read(self): + """ + :id: e31b9e0b-0c41-41fd-9a95-4e1e7cc4a298 + :title: Reading an empty file results in empty config + :description: + Verifies that reading an empty file using INIConfig results in an empty + configuration object. + :tags: Tier 2 + :steps: + 1. Initialize INIConfig and load an empty string using `_readfp`. + 2. Convert the config to a string. + :expectedresults: + 1. The config is loaded successfully without errors. + 2. The output string is empty, confirming no content exists. + """ c = ini.INIConfig() c._readfp(StringIO(self.s)) self.assertEqual(str(c), '') def test_write(self): + """ + :id: 4bfa2b67-9427-4c91-948e-f2961c83fc2b + :title: Writing to empty config results in valid output + :description: + Verifies that assigning values to a previously empty config + results in valid INI output. + :tags: Tier 2 + :steps: + 1. Initialize INIConfig and load an empty string. + 2. Add a section and an option with a value. + 3. Convert the config to a string. + :expectedresults: + 1. The config is initialized and remains empty after loading. + 2. Section and option are added successfully. + 3. Output string contains a valid section and key-value pair. + """ c = ini.INIConfig() c._readfp(StringIO(self.s)) c.sec.opt = 'xyz' @@ -140,12 +310,26 @@ def test_write(self): class TestCustomDict(unittest.TestCase): def test_custom_dict_not_supported(self): + """ + :id: c9b615d3-4a56-4604-baad-41f588b3cb51 + :title: Custom dict type is not supported in RawConfigParser + :description: + Verifies that attempting to initialize RawConfigParser with + an unsupported custom dictionary type raises a ValueError. + :tags: Tier 2 + :steps: + 1. Attempt to create a RawConfigParser instance with `dict_type='foo'`. + :expectedresults: + 1. A ValueError is raised, indicating that custom dictionary types are + not supported. + """ self.assertRaises(ValueError, compat.RawConfigParser, None, 'foo') class TestCompat(unittest.TestCase): - """Miscellaneous compatibility tests.""" - + """ + :description: Miscellaneous compatibility tests. + """ s = dedent("""\ [DEFAULT] pi = 3.1415 @@ -278,12 +462,65 @@ def do_configparser_test(self, cfg_class): '']) def test_py_rawcfg(self): + """ + :id: 3eb8aa70-17f5-4a2e-bb97-0de0b8e90ba1 + :title: RawConfigParser preserves multiline values and defaults + :description: + Verifies that RawConfigParser correctly reads and writes multiline + values, merges defaults, and removes empty lines from values. + :tags: Tier 1 + :steps: + 1. Initialize a RawConfigParser instance and load multiline + configuration string. + 2. Validate that options from DEFAULT section are merged into + other sections. + 3. Confirm correct parsing and preservation of multiline values. + 4. Serialize the config using `write()` and verify output format. + :expectedresults: + 1. RawConfigParser loads the config without error. + 2. Merged defaults are visible in other sections. + 3. Multiline values are parsed and cleaned correctly. + 4. Output format is consistent and matches expected tidy layout. + """ self.do_configparser_test(configparser.RawConfigParser) def test_py_cfg(self): + """ + :id: e32431e0-61cf-486c-b215-1739e16f4029 + :title: ConfigParser handles multiline and default merging correctly + :description: + Verifies that ConfigParser reads a complex configuration with + multiline values and default merging, and serializes it as expected. + :tags: Tier 1 + :steps: + 1. Initialize ConfigParser and load config with DEFAULT and section values. + 2. Check merged keys and formatted output. + 3. Serialize config and compare lines to expected output. + :expectedresults: + 1. ConfigParser loads and merges values correctly. + 2. Multiline options are processed and stripped of extra spacing. + 3. Output matches expected layout with correct indentation and order. + """ self.do_configparser_test(configparser.ConfigParser) def test_py_safecfg(self): + """ + :id: 42f9fdc4-2dbe-4e91-88f7-2cd229582c97 + :title: SafeConfigParser handles multiline values and default inheritance + :description: + Ensures that SafeConfigParser handles complex default merging + and multi-line value formatting properly and writes a clean config. + :tags: Tier 1 + :steps: + 1. Initialize SafeConfigParser and load config string with + mixed formatting. + 2. Verify section and default resolution. + 3. Confirm output serialization matches cleaned structure. + :expectedresults: + 1. SafeConfigParser reads the input config without error. + 2. Merged values and options are resolved correctly. + 3. Output string reflects normalized config layout. + """ self.do_configparser_test(configparser.SafeConfigParser) def do_compat_test(self, cfg_class): @@ -318,12 +555,65 @@ def do_compat_test(self, cfg_class): '']) def test_py_rawcfg(self): + """ + :id: 3eb8aa70-17f5-4a2e-bb97-0de0b8e90ba2 + :title: RawConfigParser preserves multiline values and defaults + :description: + Verifies that RawConfigParser correctly reads and writes multiline + values, merges defaults, and removes empty lines from values. + :tags: Tier 1 + :steps: + 1. Initialize a RawConfigParser instance and load multiline + configuration string. + 2. Validate that options from DEFAULT section are merged into + other sections. + 3. Confirm correct parsing and preservation of multiline values. + 4. Serialize the config using `write()` and verify output format. + :expectedresults: + 1. RawConfigParser loads the config without error. + 2. Merged defaults are visible in other sections. + 3. Multiline values are parsed and cleaned correctly. + 4. Output format is consistent and matches expected tidy layout. + """ self.do_compat_test(compat.RawConfigParser) def test_py_cfg(self): + """ + :id: e32431e0-61cf-486c-b215-1739e16f4030 + :title: ConfigParser handles multiline and default merging correctly + :description: + Verifies that ConfigParser reads a complex configuration with + multiline values and default merging, and serializes it as expected. + :tags: Tier 1 + :steps: + 1. Initialize ConfigParser and load config with DEFAULT and section values. + 2. Check merged keys and formatted output. + 3. Serialize config and compare lines to expected output. + :expectedresults: + 1. ConfigParser loads and merges values correctly. + 2. Multiline options are processed and stripped of extra spacing. + 3. Output matches expected layout with correct indentation and order. + """ self.do_compat_test(compat.ConfigParser) def test_py_safecfg(self): + """ + :id: 42f9fdc4-2dbe-4e91-88f7-2cd229582c98 + :title: SafeConfigParser handles multiline values and default inheritance + :description: + Ensures that SafeConfigParser handles complex default merging + and multi-line value formatting properly and writes a clean config. + :tags: Tier 1 + :steps: + 1. Initialize SafeConfigParser and load config string with + mixed formatting. + 2. Verify section and default resolution. + 3. Confirm output serialization matches cleaned structure. + :expectedresults: + 1. SafeConfigParser reads the input config without error. + 2. Merged values and options are resolved correctly. + 3. Output string reflects normalized config layout. + """ self.do_compat_test(compat.SafeConfigParser) @@ -388,6 +678,23 @@ def do_ini_checks(self, c): self.assertEqual(str(c), self.s) def test_compat(self): + """ + :id: 6dbdef0f-4291-4e78-91aa-0541f57b25bb + :title: ConfigParser instances retain structure after pickling + :description: + Verifies that ConfigParser-compatible classes maintain structure + and values after being serialized and deserialized via pickle. + :tags: Tier 1 + :steps: + 1. Initialize multiple ConfigParser-compatible objects and load + the test config. + 2. Serialize each object using pickle with all supported protocols. + 3. Deserialize the data and validate the structure and values. + :expectedresults: + 1. Each config object is loaded correctly with expected data. + 2. Pickling and unpickling completes without error. + 3. Deserialized objects match the original in structure and content. + """ for cfg_class in (compat.ConfigParser, compat.RawConfigParser, compat.SafeConfigParser): c = cfg_class() c.readfp(StringIO(self.s)) @@ -398,6 +705,23 @@ def test_compat(self): self.do_compat_checks(c2) def test_ini(self): + """ + :id: 2a2d48e6-2ff7-4ef5-b38e-84a39be29909 + :title: INIConfig retains structure after pickling + :description: + Verifies that INIConfig objects can be serialized and deserialized + via pickle without losing data, formatting, or section layout. + :tags: Tier 1 + :steps: + 1. Load a complex config into an INIConfig object. + 2. Serialize the object using pickle with all available protocols. + 3. Deserialize the result and check that structure and values + are preserved. + :expectedresults: + 1. INIConfig loads the input config and maintains section layout. + 2. Pickling with each protocol succeeds. + 3. Deserialized objects retain all original content and formatting. + """ c = ini.INIConfig() c._readfp(StringIO(self.s)) self.do_ini_checks(c) @@ -408,14 +732,37 @@ def test_ini(self): class TestCommentSyntax(unittest.TestCase): - """Test changing comment syntax with change_comment_syntax""" - + """ + :description: Test changing comment syntax with change_comment_syntax. + """ def test_regex(self): + """ + :id: 5a646ff2-3488-4a7c-bcf3-820059b1f786 + :title: Changing comment syntax updates parsing regex + :description: + Verifies that the `change_comment_syntax()` method updates the regular + expression used for parsing comment lines and properly escapes special + characters. + :tags: Tier 2 + :steps: + 1. Set the default regex via `change_comment_syntax(';#', True)` + and verify result. + 2. Set Mercurial-compatible syntax using `%;#` and verify regex update. + 3. Call `change_comment_syntax()` with no arguments and confirm fallback + to Mercurial-safe regex. + 4. Use special characters in the syntax string and ensure they are + escaped properly. + :expectedresults: + 1. The default regex matches the original pattern. + 2. Mercurial-safe regex is set correctly. + 3. Default call uses Mercurial-safe mode. + 4. Special characters are escaped and included in the regex. + """ # original regular expression org_regex = re.compile(r'^(?P[;#]|[rR][eE][mM])(?P.*)$') ini.change_comment_syntax(';#', True) self.assertEqual(ini.CommentLine.regex, org_regex) - + # mercurial-safe comment line regex, as given by Steve Borho & Paul Lambert # bitbucket.org/tortoisehg/stable/src/tip/tortoisehg/hgtk/thgconfig.py#cl-1084 # http://groups.google.com/group/iniparse-discuss/msg/b41a54aa185a9b7c @@ -433,6 +780,26 @@ def test_regex(self): self.assertEqual(ini.CommentLine.regex, regex) def test_ignore_includes(self): + """ + :id: 8c3f45a3-1af0-4fd6-a345-73ce9cdaef35 + :title: Config with comment-style includes is parsed correctly + :description: + Verifies that comment lines using `% include` in Mercurial-style configs + do not interfere with parsing and are preserved in output. + :tags: Tier 2 + :steps: + 1. Set Mercurial-style comment syntax using `change_comment_syntax()`. + 2. Load a config containing `% include` using `INIConfig`. + 3. Access a known value to ensure parsing succeeded. + 4. Convert the config back to a string and confirm comment line is + preserved. + :expectedresults: + 1. Comment style is updated to match Mercurial syntax. + 2. Config is parsed correctly without ignoring or misinterpreting + comment lines. + 3. The key `username` is accessible as expected. + 4. Output contains `% include` line unchanged. + """ ini.change_comment_syntax() cfg = ini.INIConfig(StringIO(dedent(""" # This is a mercurial-style config diff --git a/tests/test_multiprocessing.py b/tests/test_multiprocessing.py index 44a891b..667fdce 100644 --- a/tests/test_multiprocessing.py +++ b/tests/test_multiprocessing.py @@ -1,3 +1,14 @@ +""" +:component: python-iniparse +:requirement: RHSS-291606 +:polarion-project-id: RHELSS +:polarion-include-skipped: false +:polarion-lookup-method: id +:poolteam: rhel-sst-csi-client-tools +:caseautomation: Automated +:upstream: No +""" + import unittest try: from multiprocessing import Process, Queue, Pipe @@ -15,6 +26,28 @@ class TestIni(unittest.TestCase): """Test sending INIConfig objects.""" def test_queue(self): + """ + :id: 14f275ae-3bcb-45d7-9999-0ce942b7c47f + :title: INIConfig object can be sent between processes using multiprocessing.Queue + :description: + Verifies that an INIConfig object can be serialized, passed through a multiprocessing queue, + and deserialized in another process while retaining access to nested values. + :tags: Tier 1 + :steps: + 1. Create an empty INIConfig object and assign a nested value (`cfg.x.y = '42'`). + 2. Create two multiprocessing queues (`q` and `w`). + 3. Put the INIConfig object into queue `q`. + 4. Start a new process which pulls the config from `q`, reads `x.y`, and puts it into queue `w`. + 5. Wait for a result from queue `w` with a timeout. + 6. Assert that the value returned is the original nested value. + :expectedresults: + 1. INIConfig object is created and contains nested key-value structure. + 2. Multiprocessing queues are initialized successfully. + 3. Config is successfully placed into the first queue. + 4. New process starts and retrieves the config object from the queue. + 5. The nested value is accessed without error and placed into the second queue. + 6. The main process retrieves the correct value from queue `w` and matches it against expected result. + """ def getxy(_q, _w): _cfg = _q.get_nowait() _w.put(_cfg.x.y) diff --git a/tests/test_tidy.py b/tests/test_tidy.py index f50123c..bfbdd74 100644 --- a/tests/test_tidy.py +++ b/tests/test_tidy.py @@ -1,3 +1,14 @@ +""" +:component: python-iniparse +:requirement: RHSS-291606 +:polarion-project-id: RHELSS +:polarion-include-skipped: false +:polarion-lookup-method: id +:poolteam: rhel-sst-csi-client-tools +:caseautomation: Automated +:upstream: No +""" + import unittest from textwrap import dedent from io import StringIO @@ -11,11 +22,47 @@ def setUp(self): self.cfg = INIConfig() def test_empty_file(self): + """ + :id: 8b72c86e-f1a0-4e1e-acc3-548c17d1a489 + :title: Tidy leaves empty INIConfig unchanged + :description: + Verifies that running tidy on an empty INIConfig does not + introduce any content or whitespace. + :tags: Tier 2 + :steps: + 1. Create an empty INIConfig object. + 2. Convert it to a string and verify it is empty. + 3. Apply the tidy() function to the config. + 4. Convert the config to a string again. + :expectedresults: + 1. The config is initialized successfully and is empty. + 2. Initial string output is an empty string. + 3. tidy() runs without errors. + 4. Final string output remains an empty string. + """ self.assertEqual(str(self.cfg), '') tidy(self.cfg) self.assertEqual(str(self.cfg), '') def test_last_line(self): + """ + :id: 6a573735-b8ea-4f84-86cc-3c70f90dc248 + :title: Tidy appends newline to end of file if missing + :description: + Ensures that tidy adds a trailing newline to the end of a config + file if one is not present. + :tags: Tier 2 + :steps: + 1. Create a config with one section and one option. + 2. Convert it to a string and verify it ends without a newline. + 3. Apply tidy() to the config. + 4. Verify that the final string output ends with a newline. + :expectedresults: + 1. Config is created and contains valid content. + 2. Initial output string does not end with a newline. + 3. tidy() appends a newline during cleanup. + 4. Final output ends with a single newline. + """ self.cfg.newsection.newproperty = "Ok" self.assertEqual(str(self.cfg), dedent("""\ [newsection] @@ -27,8 +74,27 @@ def test_last_line(self): """)) def test_first_line(self): + """ + :id: 40b08735-5bc6-489b-a3b8-84d61f30cf4f + :title: Tidy removes empty lines before the first section + :description: + Verifies that tidy removes leading blank lines from the beginning + of the config. + :tags: Tier 2 + :steps: + 1. Create a config string with blank lines before the first section. + 2. Load the config into an INIConfig object. + 3. Apply tidy() to clean the config. + 4. Convert the config to a string and verify that it starts with + the section header. + :expectedresults: + 1. Config string is successfully created + 2. Config string with leading whitespace is loaded successfully. + 3. tidy() removes the initial blank lines. + 4. Final output starts directly with the section header. + """ s = dedent("""\ - + [newsection] newproperty = Ok """) @@ -40,12 +106,30 @@ def test_first_line(self): """)) def test_remove_newlines(self): + """ + :id: d1a9b12d-f6ec-4e4c-bf1e-d97b3f47ab0c + :title: Tidy removes unnecessary blank lines but preserves continuation lines + :description: + Verifies that tidy removes excessive blank lines between sections and options, + while preserving properly indented continuation lines. + :tags: Tier 1 + :steps: + 1. Create a config string with excessive blank lines and continuation lines. + 2. Load the config into an INIConfig object. + 3. Apply tidy() to clean the formatting. + 4. Convert the config to a string and compare to expected cleaned format. + :expectedresults: + 1. Config is parsed with both extra blank lines and continuation lines. + 2. tidy() removes blank lines that do not serve a formatting purpose. + 3. Continuation lines are preserved without changes. + 4. Final output has a clean and compact structure without affecting data. + """ s = dedent("""\ [newsection] newproperty = Ok - + @@ -55,8 +139,8 @@ def test_remove_newlines(self): newproperty3 = yup - - + + [newsection4] @@ -67,13 +151,13 @@ def test_remove_newlines(self): b = l1 l2 - + # asdf l5 c = 2 - - + + """) self.cfg._readfp(StringIO(s)) tidy(self.cfg) @@ -102,6 +186,27 @@ def test_remove_newlines(self): """)) def test_compat(self): + """ + :id: 5040a7c7-61de-466a-abe1-4c15ef12bfcf + :title: Tidy cleans config created via ConfigParser and preserves structure + :description: + Verifies that tidy can be used with a config object from + `iniparse.compat.ConfigParser` and correctly removes unnecessary newlines + between sections and options. + :tags: Tier 2 + :steps: + 1. Define a multi-section INI string with excessive blank lines. + 2. Parse the config using ConfigParser and `.readfp()`. + 3. Apply the tidy() function to the ConfigParser object. + 4. Convert the cleaned config to string using `str(cfg.data)`. + 5. Compare the output string with the expected tidy format. + :expectedresults: + 1. The config string is successfully defined for testing. + 2. ConfigParser loads the config and parses it correctly. + 3. tidy() removes the excessive newlines without affecting data structure. + 4. The config is successfully converted back to a string. + 5. The final output matches the expected tidy version in both structure and content. + """ s = dedent(""" [sec1] a=1 diff --git a/tests/test_unicode.py b/tests/test_unicode.py index 7f185c8..4a3ac0d 100644 --- a/tests/test_unicode.py +++ b/tests/test_unicode.py @@ -1,3 +1,14 @@ +""" +:component: python-iniparse +:requirement: RHSS-291606 +:polarion-project-id: RHELSS +:polarion-include-skipped: false +:polarion-lookup-method: id +:poolteam: rhel-sst-csi-client-tools +:caseautomation: Automated +:upstream: No +""" + import unittest from io import StringIO from iniparse import ini @@ -29,15 +40,74 @@ def basic_tests(self, s, strable): return i def test_ascii(self): + """ + :id: 3d55663a-6c1d-4cf9-920e-875b183be39f + :title: ASCII input is parsed and serialized correctly + :description: + Verifies that standard ASCII INI strings are parsed successfully + and that all values are retrievable and printable using `str()` + without encoding issues. + :tags: Tier 1 + :steps: + 1. Create an INI string containing only ASCII characters. + 2. Parse the string using `INIConfig`. + 3. Retrieve a known value from the config. + 4. Convert the config back to a string using `str()`. + :expectedresults: + 1. The INI string is parsed without errors. + 2. The expected value is retrieved correctly. + 3. Serialization works with `str()` and returns valid ASCII. + """ i = self.basic_tests(self.s1, strable=True) self.assertEqual(i.foo.bar, 'fish') def test_unicode_without_bom(self): + """ + :id: 963a6e17-262e-4c85-9baf-7e2d9a88f92d + :title: Unicode INI input without BOM is parsed correctly + :description: + Verifies that Unicode input without a Byte Order Mark (BOM) is parsed + correctly and values with special characters are accessible, even if + not ASCII-encodable. + :tags: Tier 2 + :steps: + 1. Create a Unicode INI string with non-ASCII characters, excluding BOM. + 2. Parse the string using `INIConfig`. + 3. Retrieve both values, including one with a non-ASCII byte (`\202`). + 4. Attempt to serialize the config with `str().encode("ascii")` and + expect failure. + :expectedresults: + 1. Input string is parsed successfully. + 2. All values are retrievable. + 3. Non-ASCII characters are preserved. + 4. Encoding to ASCII raises `UnicodeEncodeError`. + """ i = self.basic_tests(self.s2[1:], strable=False) self.assertEqual(i.foo.bar, 'mammal') self.assertEqual(i.foo.baz, u'Marc-Andr\202') def test_unicode_with_bom(self): + """ + :id: b0eabc52-bb35-41c7-bb7c-cc7c5479c964 + :title: Unicode INI input with BOM is parsed correctly + :description: + Ensures that Unicode strings containing a BOM (`\ufeff`) are parsed + without error and values with extended characters are preserved + in the configuration object. + :tags: Tier 2 + :steps: + 1. Create a Unicode INI string that includes a BOM and non-ASCII + characters. + 2. Parse the string using `INIConfig`. + 3. Retrieve both options and verify values. + 4. Attempt to serialize with `str().encode("ascii")` and expect + a `UnicodeEncodeError`. + :expectedresults: + 1. Input with BOM is parsed successfully. + 2. Both values are correctly accessible from the config. + 3. Unicode characters are preserved. + 4. ASCII encoding fails with `UnicodeEncodeError`. + """ i = self.basic_tests(self.s2, strable=False) self.assertEqual(i.foo.bar, 'mammal') self.assertEqual(i.foo.baz, u'Marc-Andr\202') diff --git a/tests/testimony.yml b/tests/testimony.yml new file mode 100644 index 0000000..e0a95e3 --- /dev/null +++ b/tests/testimony.yml @@ -0,0 +1,59 @@ +--- +Id: + casesensitive: false + required: true + type: string +Polarion-Project-Id: + casesensitive: false + required: true + type: choice + choices: + - RHELSS +Polarion-Include-Skipped: + casesensitive: false + required: true + type: string +Polarion-Lookup-Method: + casesensitive: false + required: true + type: string +Component: + casesensitive: false + required: true + type: string +Requirement: + casesensitive: false + required: true + type: choice + choices: + - RHSS-291606 +PoolTeam: + casesensitive: false + required: true + type: choice + choices: + - rhel-sst-csi-client-tools +CaseAutomation: + casesensitive: false + required: true + type: string +Upstream: + casesensitive: false + type: string +Title: + casesensitive: false + type: string +Description: + casesensitive: false + required: true + type: string +Tags: + casesensitive: true + required: true + type: choice + choices: + - Tier 1 + - Tier 2 + - Tier 3 +Steps: {} +ExpectedResults: {}