-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Refactoring in progress (migration to pydantic)
- Loading branch information
1 parent
ea1b8bb
commit be72ac7
Showing
4 changed files
with
265 additions
and
153 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,152 +1,99 @@ | ||
"""YAML Schema for Baygon test files.""" | ||
|
||
from voluptuous import ( | ||
All, | ||
Any, | ||
Boolean, | ||
Coerce, | ||
ExactSequence, | ||
Exclusive, | ||
Optional, | ||
Required, | ||
Self, | ||
) | ||
from voluptuous import Schema as VSchema | ||
from voluptuous.humanize import validate_with_humanized_errors | ||
|
||
from .id import TrackId | ||
|
||
Value = Any(str, All(Any(int, float, All(bool, Coerce(int))), Coerce(str))) | ||
|
||
|
||
class ToList: | ||
"""Convert the given value to a list.""" | ||
|
||
def __call__(self, v): | ||
return [v] | ||
|
||
|
||
class ToDict: | ||
"""Convert the given value to a dict.""" | ||
|
||
def __init__(self, key): | ||
self.key = key | ||
|
||
def __call__(self, v): | ||
return {self.key: v} | ||
|
||
|
||
class Join: | ||
"""Join the given values.""" | ||
|
||
def __init__(self, sep): | ||
self.sep = sep | ||
|
||
def __call__(self, v): | ||
return self.sep.join(v) | ||
|
||
|
||
# Eval module | ||
evaluate = { | ||
Optional("eval", description="Eval mustaches"): Any( | ||
{ | ||
Optional("start", default="{{"): str, | ||
Optional("end", default="}}"): str, | ||
Optional("init", str): Any(All(str, ToList()), [str]), | ||
}, | ||
All(Any(Boolean(), None), lambda x: {"init": []}), | ||
) | ||
} | ||
|
||
# Global test filters | ||
filters = { | ||
Optional("uppercase"): Boolean(), | ||
Optional("lowercase"): Boolean(), | ||
Optional("trim"): Boolean(), | ||
Optional(Any("ignorespaces", "ignore-spaces")): Boolean(), | ||
Optional("regex"): ExactSequence([str, str]), | ||
Optional("replace"): ExactSequence([str, str]), | ||
} | ||
|
||
# Test case | ||
case = VSchema( | ||
{ | ||
Optional("filters", default={}): filters, | ||
Any("equals", "regex", "contains"): Value, | ||
Optional("not"): [{Required(Any("equals", "regex", "contains")): Value}], | ||
Optional("expected", description="Expected value when used with regex"): Value, | ||
} | ||
) | ||
|
||
# Nested test cases | ||
match = Any( | ||
All(Value, ToDict("equals"), ToList()), | ||
All(case, ToList()), | ||
[All(Value, lambda x: ToDict("equals")), case], | ||
) | ||
|
||
|
||
common = { | ||
Optional("name", default="", description="Test Name"): str, | ||
Optional("executable", default=None, description="Path to the executable"): Any( | ||
str, None | ||
), | ||
Optional( | ||
Exclusive( | ||
"points", "points_or_weight", description="Points given for this test" | ||
) | ||
): Any(float, int), | ||
Optional( | ||
Exclusive("weight", "points_or_weight", description="Weight of the test") | ||
): Any(float, int), | ||
Optional("min-points", default=0.1): Any(float, int), | ||
} | ||
|
||
test = VSchema( | ||
{ | ||
Optional("args", default=[]): [Value], | ||
Optional("env", default={}): {str: Value}, | ||
Optional("stdin", default=""): Any(Value, None), | ||
Optional("stdout", default=[]): match, | ||
Optional("stderr", default=[]): match, | ||
Optional("repeat", default=1): int, | ||
Optional("exit"): Any(int, str, bool), | ||
} | ||
).extend(common) | ||
|
||
Num = TrackId() | ||
|
||
group = VSchema( | ||
{Required("tests"): All(Num.down(), [All(Any(Self, test), Num.next())], Num.up())} | ||
).extend(common) | ||
|
||
cli = { | ||
Optional("verbose"): Any(int), | ||
Optional("report"): str, | ||
Optional("format"): Any("json", "yaml"), | ||
Optional("table", default=False): Boolean(), | ||
} | ||
|
||
|
||
def Schema(data, humanize=False): # noqa: N802 | ||
"""Validate the given data against the schema.""" | ||
schema = ( | ||
VSchema( | ||
{ | ||
Optional("name"): str, | ||
Optional("version", default=2): Any(1, 2), | ||
Optional("filters", default={}): filters, | ||
Required("tests"): All( | ||
Num.reset(), [All(Any(test, group), Num.next())] | ||
), | ||
Optional("points"): Any(float, int), | ||
Optional("min-points", default=0.1): Any(float, int), | ||
} | ||
) | ||
.extend(common) | ||
.extend(evaluate) | ||
.extend(cli) | ||
) | ||
if humanize: | ||
return validate_with_humanized_errors(data, schema) | ||
return schema(data) | ||
from typing import Dict, List, Optional, Union, Literal | ||
from pydantic import BaseModel, Field, field_validator, ValidationError | ||
|
||
Value = Union[str, int, float, bool] | ||
|
||
|
||
class EvaluateConfig(BaseModel): | ||
start: str = "{{" | ||
end: str = "}}" | ||
init: Optional[List[str]] = None | ||
|
||
|
||
class MinPointsMixin(BaseModel): | ||
min_points: float = Field(0.1, alias="min-points") | ||
|
||
|
||
class FiltersConfig(BaseModel): | ||
uppercase: Optional[bool] = None | ||
lowercase: Optional[bool] = None | ||
trim: Optional[bool] = None | ||
ignorespaces: Optional[bool] = Field(None, alias="ignore-spaces") | ||
regex: Optional[List[str]] = None | ||
replace: Optional[List[str]] = None | ||
|
||
|
||
class CaseConfig(BaseModel): | ||
filters: FiltersConfig = FiltersConfig() | ||
equals: Optional[Value] = None | ||
regex: Optional[Value] = None | ||
contains: Optional[Value] = None | ||
not_: Optional[List[Dict[str, Value]]] = Field(None, alias="not") | ||
expected: Optional[Value] = None | ||
|
||
@field_validator("not_", mode="before") | ||
def rename_not(cls, v): | ||
return v | ||
|
||
|
||
class CommonConfig(MinPointsMixin, BaseModel): | ||
name: str = "" | ||
executable: Optional[str] = None | ||
points: Optional[float] = None | ||
weight: Optional[float] = None | ||
|
||
@field_validator("points") | ||
def check_points_weight(cls, v, values): | ||
weight = values.get("weight") | ||
if weight is not None and v is not None: | ||
raise ValueError("Cannot specify both 'points' and 'weight'") | ||
return v | ||
|
||
|
||
class TestConfig(CommonConfig): | ||
args: List[Value] = [] | ||
env: Dict[str, Value] = {} | ||
stdin: Optional[Value] = "" | ||
stdout: Optional[List[Union[Value, "CaseConfig"]]] = [] | ||
stderr: Optional[List[Union[Value, "CaseConfig"]]] = [] | ||
repeat: int = 1 | ||
exit: Optional[Union[int, str, bool]] = None | ||
|
||
|
||
class GroupConfig(CommonConfig): | ||
tests: List[Union["GroupConfig", TestConfig]] | ||
|
||
class Config: | ||
arbitrary_types_allowed = True | ||
|
||
|
||
GroupConfig.model_rebuild() | ||
|
||
|
||
class CLIConfig(BaseModel): | ||
"""CLI configuration that can be overridden by CLI arguments""" | ||
|
||
verbose: Optional[int] = 0 | ||
report: Optional[str] = None | ||
format: Optional[Literal["json", "yaml"]] = "json" | ||
table: bool = False | ||
|
||
|
||
class SchemaConfig(MinPointsMixin, CLIConfig, BaseModel): | ||
name: Optional[str] = None | ||
version: int = 2 | ||
filters: FiltersConfig = FiltersConfig() | ||
tests: List[Union[GroupConfig, TestConfig]] | ||
points: Optional[float] = None | ||
evaluate: Optional[EvaluateConfig] = None | ||
|
||
@field_validator("version") | ||
def _check_version(cls, v): | ||
if v > 2: | ||
raise ValidationError( | ||
"Only version up to 2 is accepted, you may used a newer schema" | ||
) | ||
return v | ||
|
||
class Config: | ||
populate_by_name = True |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.