1
0
mirror of https://github.com/containers/ramalama.git synced 2026-02-05 06:46:39 +01:00
Files
ramalama/test/unit/test_config_documentation.py
Ian Eaves 38e5e5cf8d adds benchmark metrics persistence
Signed-off-by: Ian Eaves <ian.k.eaves@gmail.com>
2026-01-24 22:06:46 -06:00

266 lines
11 KiB
Python

"""
Test to ensure all CONFIG options are properly documented in ramalama.conf and ramalama.conf.5.md
"""
import re
from dataclasses import fields
from pathlib import Path
import pytest
from ramalama.config import BaseConfig
def get_config_fields():
"""Extract all field names from BaseConfig dataclass, excluding internal/system fields."""
excluded_fields = {
'settings', # Internal RamalamaSettings, not user-configurable
'default_image', # Internal constant, users configure 'image' instead
'default_rag_image', # Internal constant, users configure 'rag_image' instead
'rag_image', # Derived from rag_images, not directly configured
'stack_image', # Internal constant for stack operations
'dryrun', # Runtime flag, not a persistent config option
'ocr', # Runtime flag, not a persistent config option
'verify', # Runtime flag for model verification, not typically configured
}
config_fields = [field.name for field in fields(BaseConfig) if field.name not in excluded_fields]
config_fields.extend(('benchmarks', 'http_client', 'images', 'rag_images', 'user'))
return sorted(set(config_fields))
def get_documented_fields_in_conf():
"""Extract documented field names from docs/ramalama.conf."""
conf_path = Path(__file__).parent.parent.parent / "docs" / "ramalama.conf"
if not conf_path.exists():
pytest.skip(f"ramalama.conf not found at {conf_path}")
with open(conf_path) as f:
lines = f.readlines()
# Match commented and uncommented config options like:
# #api = "none"
# api_key = ""
# [ramalama.images]
pattern = r'^\s*#?\s*([a-z_]+)\s*='
documented = set()
# Subsections that contain their own field documentation (these fields should not be extracted)
subsections_with_fields = {'benchmarks', 'http_client', 'user'}
# Track which section we're in to exclude nested fields under commented subsections
in_commented_nested_section = False
section_pattern = r'^\s*(#?)\s*\[ramalama\.([a-z_]+)\]'
main_section_pattern = r'^\s*\[ramalama\]'
prev_line_blank = False
for line in lines:
current_line_blank = line.strip() == ''
# Check if we're at the main [ramalama] section
if re.match(main_section_pattern, line):
in_commented_nested_section = False
prev_line_blank = current_line_blank
continue
# Check if we're entering a subsection
section_match = re.match(section_pattern, line)
if section_match:
is_commented = section_match.group(1) == '#'
section_name = section_match.group(2)
documented.add(section_name)
# Skip fields if it's a commented nested section OR if it's a subsection with its own fields
in_commented_nested_section = is_commented or (section_name in subsections_with_fields)
prev_line_blank = current_line_blank
continue
# Check if we hit a new uncommented section (which ends any nested section)
if re.match(r'^\s*\[', line) and not line.strip().startswith('#'):
in_commented_nested_section = False
# If we're in a commented subsection, stay in it until we see a clear exit point
# Exit point: blank line followed by a comment that starts a new field's documentation
if in_commented_nested_section:
# If this line has a field definition, skip it
if '=' in line:
prev_line_blank = current_line_blank
continue
# If previous line was blank and this is a comment (but not empty), might be exiting
elif prev_line_blank and line.strip().startswith('#') and line.strip() not in ['#', '']:
# This looks like the start of a new field's documentation, exit
in_commented_nested_section = False
# Otherwise stay in the section
else:
prev_line_blank = current_line_blank
continue
# Only match fields when not in a commented nested subsection
if not in_commented_nested_section:
match = re.match(pattern, line)
if match:
field_name = match.group(1)
documented.add(field_name)
prev_line_blank = current_line_blank
return sorted(documented)
def get_documented_fields_in_manpage():
"""Extract documented field names from docs/ramalama.conf.5.md."""
manpage_path = Path(__file__).parent.parent.parent / "docs" / "ramalama.conf.5.md"
if not manpage_path.exists():
pytest.skip(f"ramalama.conf.5.md not found at {manpage_path}")
with open(manpage_path) as f:
lines = f.readlines()
# Match markdown bold options like:
# **api**="none"
# **api_key**=""
# But exclude option values like **always**, **missing**, etc. which appear in pull documentation
pattern = r'^\*\*([a-z_]+)\*\*'
# Ignore values, not config keys
ignored_values = {'always', 'missing', 'never', 'newer'}
documented = set()
# Subsections that contain their own **field** documentation (these fields should not be extracted)
subsections_with_fields = {'http_client', 'user'}
# Track which section we're in
current_section = None
main_section_pattern = r'^`\[\[ramalama\]\]`$'
section_pattern = r'^`\[\[ramalama\.([a-z_]+)\]\]`'
in_main_section = False
for line in lines:
# Check if we're entering the main [[ramalama]] section
if re.match(main_section_pattern, line):
in_main_section = True
current_section = None
continue
# Check if we're entering a subsection like [[ramalama.images]]
section_match = re.match(section_pattern, line)
if section_match:
section_name = section_match.group(1)
current_section = section_name
documented.add(section_name)
continue
# Reset section when we hit a new top-level heading
if line.startswith('## '):
if 'RAMALAMA TABLE' in line.upper():
in_main_section = False
current_section = None
else:
# Hitting a new ## heading ends any subsection
in_main_section = False
current_section = None
# When we're in the main [[ramalama]] section
if in_main_section:
match = re.match(pattern, line)
if match:
field = match.group(1)
if field not in ignored_values:
# If we're in a subsection that has its own field documentation,
# skip those fields (they're not top-level config options)
if current_section in subsections_with_fields:
continue
# If we found a field while in another kind of subsection (like images),
# it means we're back in the main section
if current_section is not None:
current_section = None
documented.add(field)
return sorted(documented)
class TestConfigDocumentation:
"""Test suite to ensure CONFIG options are properly documented."""
# Aliases and special cases that are documented but map to actual fields
KNOWN_ALIASES = {
'default_image', # Alias for 'image' configuration
'default_rag_image', # Alias for rag image configuration
'no_missing_gpu_prompt', # Nested field under user.no_missing_gpu_prompt
}
def test_config_fields_in_ramalama_conf(self):
"""Verify all CONFIG fields are documented in ramalama.conf."""
config_fields = get_config_fields()
documented_fields = get_documented_fields_in_conf()
missing = set(config_fields) - set(documented_fields)
assert not missing, (
f"The following CONFIG fields are missing from docs/ramalama.conf: "
f"`{', '.join(sorted(missing))}`. "
f"Please add documentation for these fields in docs/ramalama.conf"
)
def test_config_fields_in_manpage(self):
"""Verify all CONFIG fields are documented in ramalama.conf.5.md."""
config_fields = get_config_fields()
documented_fields = get_documented_fields_in_manpage()
missing = set(config_fields) - set(documented_fields)
warning_message = (
f"The following CONFIG fields are missing from docs/ramalama.conf.5.md:"
f"`{', '.join(sorted(missing))}`. "
f"Please add documentation for these fields in docs/ramalama.conf.5.md"
)
assert not missing, warning_message
def test_no_undocumented_fields_in_conf(self):
"""Verify ramalama.conf doesn't document non-existent fields."""
config_fields = get_config_fields()
documented_fields = get_documented_fields_in_conf()
extra = set(documented_fields) - set(config_fields) - self.KNOWN_ALIASES
assert not extra, (
f"The following fields are documented in docs/ramalama.conf but not in CONFIG:"
f"`{', '.join(sorted(extra))}`. "
f"These might be typos or outdated documentation."
)
def test_no_undocumented_fields_in_manpage(self):
"""Verify ramalama.conf.5.md doesn't document non-existent fields."""
config_fields = get_config_fields()
documented_fields = get_documented_fields_in_manpage()
extra = set(documented_fields) - set(config_fields) - self.KNOWN_ALIASES
assert not extra, (
f"The following fields are documented in docs/ramalama.conf.5.md but not in CONFIG: "
f"`{', '.join(sorted(extra))}`. "
f"These might be typos or outdated documentation."
)
def test_consistency_between_conf_and_manpage(self):
"""Verify both documentation files document the same fields."""
conf_fields = get_documented_fields_in_conf()
manpage_fields = get_documented_fields_in_manpage()
only_in_conf = set(conf_fields) - set(manpage_fields)
only_in_manpage = set(manpage_fields) - set(conf_fields)
error_msg = []
if only_in_conf:
error_msg.append(f"Fields documented only in ramalama.conf:\n{', '.join(sorted(only_in_conf))}")
if only_in_manpage:
error_msg.append(f"Fields documented only in ramalama.conf.5.md:\n{', '.join(sorted(only_in_manpage))}")
assert not error_msg, (
"Documentation inconsistency between ramalama.conf and ramalama.conf.5.md:"
+ " ".join(error_msg)
+ ". Both files should document the same configuration options."
)