From acc3d276ab5d8fa9ca353974f14f1cae8f899fa3 Mon Sep 17 00:00:00 2001 From: zpetrace Date: Tue, 3 Jun 2025 12:31:48 +0200 Subject: [PATCH 1/2] feat(test): Add traceability requirements The intention of CCT-1311 is to add a gating.el10 job for python-iniparse. Before that it would be good to have the traceability matrix to be able to report our tests to Polarion. This PR therefore adds the neccessary docstrings for Betelgeuse to each test. It also adds two github workflows - checks for betelgeuse and for testimony so that on every PR we have a check that the tests have these requirements. --- .github/workflows/docstrings_validation.yml | 29 ++ .github/workflows/testimony.yml | 27 ++ tests/custom_betelgeuse_config.py | 15 + tests/test_compat.py | 405 +++++++++++++++++++ tests/test_fuzz.py | 32 ++ tests/test_ini.py | 425 ++++++++++++++++++++ tests/test_misc.py | 397 +++++++++++++++++- tests/test_multiprocessing.py | 33 ++ tests/test_tidy.py | 119 +++++- tests/test_unicode.py | 70 ++++ tests/testimony.yml | 59 +++ 11 files changed, 1589 insertions(+), 22 deletions(-) create mode 100644 .github/workflows/docstrings_validation.yml create mode 100644 .github/workflows/testimony.yml create mode 100644 tests/custom_betelgeuse_config.py create mode 100644 tests/testimony.yml 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..97aef41 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,6 +31,20 @@ 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) @@ -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: {} From da1ad482a52d6e3338753a3b02443e4f16c0a620 Mon Sep 17 00:00:00 2001 From: zpetrace Date: Tue, 3 Jun 2025 12:34:05 +0200 Subject: [PATCH 2/2] fix(test): Removing the warning message Small change of removing the warning message. --- tests/test_ini.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_ini.py b/tests/test_ini.py index 97aef41..359a2b9 100644 --- a/tests/test_ini.py +++ b/tests/test_ini.py @@ -51,7 +51,7 @@ def test_invalid(self): 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)),