Skip to content

Commit

Permalink
Refactoring in progress (migration to pydantic)
Browse files Browse the repository at this point in the history
  • Loading branch information
yves-chevallier committed Oct 10, 2024
1 parent ea1b8bb commit be72ac7
Show file tree
Hide file tree
Showing 4 changed files with 265 additions and 153 deletions.
251 changes: 99 additions & 152 deletions baygon/schema.py
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
29 changes: 29 additions & 0 deletions docs/docs/guide/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,32 @@ ok.
::: tip
You may need to use `pip3` instead of `pip` depending on your system.
:::

## Options

Baygon has a few options to help you run your tests:

```
Usage: baygon [OPTIONS] [EXECUTABLE]
Baygon functional test runner.
Options:
--version Shows version
-v, --verbose Shows more details
-l, --limit INTEGER Limit errors to N
-d, --debug Debug mode
-r, --report PATH Report file
-t, --table Summary table
-f, --format [json|yaml] Report format
-t, --config PATH Choose config file (.yml or .json)
--help Show this message and exit.
```
### Verbose

The `-v` or `--verbose` is cumulative. The more you use it, the more details you get, you can use it such as `-vvv`.

0. Only minimum output will be displayed (default value)
1. Display all tests
2. Display all tests and steps
3. Display all tests, steps and output
Loading

0 comments on commit be72ac7

Please sign in to comment.