scripts: Fix RST formatting in release_version.py
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>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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"""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user