[Concept,02/24] scripts: Fix RST formatting in release_version.py

Message ID 20250922180116.3088502-3-sjg@u-boot.org
State New
Headers
Series boot: efi: Various improvements to booting with the EFI app |

Commit Message

Simon Glass Sept. 22, 2025, 6 p.m. UTC
  From: Simon Glass <sjg@chromium.org>

Fix line continuation in generate_schedule() that caused Sphinx to fail
with "Bullet list ends without a blank line; unexpected unindent" error.
Add tests to validate RST formatting of generated documentation.

Co-developed-by: Claude <noreply@anthropic.com>
Signed-off-by: Simon Glass <sjg@chromium.org>
---

 scripts/release_version.py           |   3 +-
 test/scripts/test_release_version.py | 164 +++++++++++++++++++++++++++
 2 files changed, 165 insertions(+), 2 deletions(-)
  

Patch

diff --git a/scripts/release_version.py b/scripts/release_version.py
index 476f394ed1a..5ff179dafb8 100755
--- a/scripts/release_version.py
+++ b/scripts/release_version.py
@@ -190,8 +190,7 @@  Release candidate schedule:
 * **{target_year}.{target_month:02d}-rc3**: {rc3_date.strftime('%a %d-%b-%Y')}
 * **{target_year}.{target_month:02d}-rc2**: {rc2_date.strftime('%a %d-%b-%Y')}
 * **{target_year}.{target_month:02d}-rc1**: {rc1_date.strftime('%a %d-%b-%Y')}
-* **{target_year}.{target_month:02d}** (Final): \\
-{final_date.strftime('%a %d-%b-%Y')}
+* **{target_year}.{target_month:02d}** (Final): {final_date.strftime('%a %d-%b-%Y')}
 
 '''
     return schedule_text
diff --git a/test/scripts/test_release_version.py b/test/scripts/test_release_version.py
index 14a63beb68c..3758404672d 100755
--- a/test/scripts/test_release_version.py
+++ b/test/scripts/test_release_version.py
@@ -9,6 +9,7 @@  import io
 import json
 import os
 import shutil
+import subprocess
 import sys
 import tempfile
 import unittest
@@ -876,6 +877,169 @@  class TestMainFunction(unittest.TestCase):
             self.assertIn(expected_msg, mock_stdout.getvalue())
 
 
+class TestRSTFormatting(unittest.TestCase):
+    """Test that generated RST content is valid for Sphinx"""
+
+    def setUp(self):
+        """Set up test fixtures"""
+        self.test_dir = tempfile.mkdtemp()
+
+    def tearDown(self):
+        """Clean up test fixtures"""
+        shutil.rmtree(self.test_dir)
+
+    def test_generate_schedule_rst_validity(self):
+        """Test that generate_schedule() produces valid RST"""
+        schedule = generate_schedule()
+
+        # Write schedule to a test file
+        test_rst_path = os.path.join(self.test_dir, 'test_schedule.rst')
+        with open(test_rst_path, 'w', encoding='utf-8') as f:
+            f.write('''.. SPDX-License-Identifier: GPL-2.0+
+
+Test Document
+=============
+
+''')
+            f.write(schedule)
+
+        # Try to validate with rst2html if available (fallback test)
+        try:
+            # Run docutils rst2html to validate the RST syntax
+            result = subprocess.run(['rst2html', test_rst_path],
+                                  capture_output=True, text=True, timeout=30)
+            # If rst2html is available and succeeds, the RST is valid
+            if result.returncode == 0:
+                self.assertTrue(True, "RST validation passed with rst2html")
+            else:
+                # Check for specific formatting errors
+                stderr = result.stderr.lower()
+                if 'bullet list ends without a blank line' in stderr:
+                    self.fail(f"RST formatting error in schedule: {result.stderr}")
+                elif 'unexpected unindent' in stderr:
+                    self.fail(f"RST formatting error in schedule: {result.stderr}")
+        except (subprocess.TimeoutExpired, FileNotFoundError):
+            # rst2html not available or timeout, do basic content checks
+            pass
+
+        # Basic RST format validation checks
+        lines = schedule.split('\n')
+
+        # Check for proper bullet list formatting
+        in_bullet_list = False
+        last_line_was_bullet = False
+
+        for i, line in enumerate(lines):
+            stripped = line.strip()
+
+            # Check if this line starts a bullet list item
+            if stripped.startswith('* '):
+                in_bullet_list = True
+                last_line_was_bullet = True
+
+                # Check for line continuation issues
+                if '\\' in line:
+                    # If there's a backslash continuation, ensure it's properly formatted
+                    # The continuation should be on the same line, not the next line
+                    self.assertFalse(line.rstrip().endswith('\\'),
+                                   f"Line {i+1} has improper line continuation: {line}")
+            elif in_bullet_list and stripped == '':
+                # Empty line might end the bullet list
+                last_line_was_bullet = False
+            elif in_bullet_list and stripped and not stripped.startswith('  '):
+                # Non-indented content after bullet list should have blank line before it
+                if last_line_was_bullet:
+                    self.fail(f"Line {i+1} lacks blank line after bullet list: {line}")
+                in_bullet_list = False
+                last_line_was_bullet = False
+            elif in_bullet_list and stripped.startswith('  '):
+                # Indented content is part of the bullet list
+                last_line_was_bullet = False
+            else:
+                in_bullet_list = False
+                last_line_was_bullet = False
+
+    def test_update_docs_rst_validity(self):
+        """Test that update_docs() produces valid RST"""
+        docs_path = os.path.join(self.test_dir, 'concept_releases.rst')
+
+        # Create initial file
+        with open(docs_path, 'w', encoding='utf-8') as f:
+            f.write('''.. SPDX-License-Identifier: GPL-2.0+
+
+U-Boot Concept Releases
+=======================
+
+This document tracks all concept releases of U-Boot.
+
+Release History
+---------------
+
+''')
+
+        # Add a release that includes schedule generation
+        release_info = ReleaseInfo(
+            is_final=False,
+            version='2025.02-rc1',
+            year=2025,
+            month=2,
+            rc_number=1
+        )
+
+        changes_made = update_docs(release_info, 'abc123def', docs_path)
+        self.assertTrue(changes_made)
+
+        # Read the updated content
+        with open(docs_path, 'r', encoding='utf-8') as f:
+            content = f.read()
+
+        # Try to validate with rst2html if available
+        try:
+            result = subprocess.run(['rst2html', docs_path],
+                                  capture_output=True, text=True, timeout=30)
+            if result.returncode != 0:
+                stderr = result.stderr.lower()
+                if 'bullet list ends without a blank line' in stderr:
+                    self.fail(f"RST formatting error in generated docs: {result.stderr}")
+                elif 'unexpected unindent' in stderr:
+                    self.fail(f"RST formatting error in generated docs: {result.stderr}")
+        except (subprocess.TimeoutExpired, FileNotFoundError):
+            # rst2html not available, continue with manual checks
+            pass
+
+        # Manual validation of RST structure
+        lines = content.split('\n')
+
+        # Find the Next Release section and check its formatting
+        next_release_idx = -1
+        for i, line in enumerate(lines):
+            if line.strip() == 'Next Release':
+                next_release_idx = i
+                break
+
+        if next_release_idx != -1:
+            # Check the bullet list in the schedule section
+            in_candidate_schedule = False
+            for i in range(next_release_idx, len(lines)):
+                line = lines[i]
+                if 'Release candidate schedule:' in line:
+                    in_candidate_schedule = True
+                    continue
+                elif in_candidate_schedule and line.strip().startswith('* '):
+                    # This is a bullet list item
+                    # Check it doesn't have improper line continuation
+                    self.assertFalse(line.rstrip().endswith('\\'),
+                                   f"Line {i+1} has improper line continuation in schedule: {line}")
+                elif in_candidate_schedule and line.strip() == '':
+                    # Empty line - bullet list might be ending
+                    continue
+                elif (in_candidate_schedule and line.strip() and
+                      not line.strip().startswith('* ') and
+                      not line.startswith('  ')):
+                    # End of bullet list section
+                    break
+
+
 class TestReleaseVersionScenarios(unittest.TestCase):
     """Test realistic release scenarios"""