99
1010from pathlib import Path
1111
12- from pydantic import BaseModel , Field , FilePath
12+ from pydantic import BaseModel , Field , FilePath , ValidationError
13+ from rich .console import Console
14+ from rich .panel import Panel
1315from ruamel .yaml import YAML
16+ from ruamel .yaml .error import YAMLError
17+
18+
19+ class TestYamlHeaderError (Exception ):
20+ """Raised when a test file's YAML config header is missing, malformed, or fails validation."""
21+
22+ def __init__ (self , file : Path , problem : str ) -> None :
23+ self .file = file
24+ self .problem = problem
25+ super ().__init__ (f"Malformed { file .name } YAML header: { problem } ({ file } )" )
26+
27+ def print (self ) -> None :
28+ """Render this error as a formatted panel on stderr."""
29+ body = (
30+ f"[bold red]Malformed YAML header in[/] [underline]{ self .file .name } [/]\n \n "
31+ f"[bold]Problem:[/] { self .problem } \n "
32+ f"[bold]File:[/] [cyan]{ self .file } [/]"
33+ )
34+ Console (stderr = True ).print (
35+ Panel (body , title = "[bold red]Test YAML Header Error[/]" , border_style = "red" , expand = False )
36+ )
1437
1538
1639class TestMetadata (BaseModel ):
@@ -55,6 +78,21 @@ def e_ext(self) -> bool:
5578 return self .march .startswith (("rv32e" , "rv64e" , "rv${XLEN}e" ))
5679
5780
81+ def _describe_validation_error (err : ValidationError ) -> str :
82+ """Translate the first Pydantic error into a short human-readable error."""
83+ e = err .errors ()[0 ]
84+ field = "." .join (str (p ) for p in e ["loc" ]) or "<root>"
85+ etype = e ["type" ]
86+ got = e .get ("input" )
87+ if etype == "extra_forbidden" :
88+ return f"unexpected key '{ field } ' found"
89+ if etype == "missing" :
90+ return f"required key '{ field } ' is missing"
91+ if etype .startswith (("string_pattern_mismatch" , "value_error" )):
92+ return f"illegal value for key '{ field } ': { got !r} "
93+ return f"invalid value for key '{ field } ': { e ['msg' ]} "
94+
95+
5896def extract_yaml_config (file : Path ) -> TestMetadata :
5997 """Extract YAML configuration from a test file between START_TEST_CONFIG and END_TEST_CONFIG markers."""
6098 content = file .read_text ()
@@ -67,7 +105,7 @@ def extract_yaml_config(file: Path) -> TestMetadata:
67105 end_pos = content .find (end_marker )
68106
69107 if start_pos == - 1 or end_pos == - 1 :
70- raise ValueError ( f"Could not find YAML config section in { file } " )
108+ raise TestYamlHeaderError ( file , f"missing { start_marker } / { end_marker } markers " )
71109
72110 # Extract content between markers
73111 start_pos = content .find ("\n " , start_pos ) + 1 # Skip to next line after start marker
@@ -76,15 +114,19 @@ def extract_yaml_config(file: Path) -> TestMetadata:
76114 yaml_section = content [start_pos :end_pos ]
77115
78116 # Process lines to remove comment prefixes
79- yaml_lines : list [str ] = []
80- for line in yaml_section .split ("\n " ):
81- line = line .lstrip ("#" )
82- yaml_lines .append (line )
117+ yaml_lines = [line .lstrip ("#" ) for line in yaml_section .split ("\n " )]
83118 yaml_lines .append (f" test_path: '{ file .absolute ()} '" ) # Add test_path to config data
84119
85120 yaml = YAML (typ = "safe" , pure = True )
86- config_dict = yaml .load ("\n " .join (yaml_lines ))
87- return TestMetadata .model_validate (config_dict )
121+ try :
122+ config_dict = yaml .load ("\n " .join (yaml_lines ))
123+ except YAMLError as e :
124+ raise TestYamlHeaderError (file , f"YAML parse error: { e } " ) from None
125+
126+ try :
127+ return TestMetadata .model_validate (config_dict )
128+ except ValidationError as e :
129+ raise TestYamlHeaderError (file , _describe_validation_error (e )) from None
88130
89131
90132def generate_test_dict (tests_dir : Path , extensions : str , exclude : str = "" ) -> dict [str , TestMetadata ]:
0 commit comments