Skip to content

Commit 1162799

Browse files
committed
Improve stdin support for files
1 parent 4c4f7ae commit 1162799

File tree

15 files changed

+302
-73
lines changed

15 files changed

+302
-73
lines changed

tested/dsl/schema-strict.json

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -276,11 +276,18 @@
276276
},
277277
"stdin" : {
278278
"description" : "Stdin for this context",
279-
"type" : [
280-
"string",
281-
"number",
282-
"integer",
283-
"boolean"
279+
"oneOf" : [
280+
{
281+
"type" : [
282+
"string",
283+
"number",
284+
"integer",
285+
"boolean"
286+
]
287+
},
288+
{
289+
"$ref": "#/definitions/fileData"
290+
}
284291
]
285292
},
286293
"arguments" : {
@@ -890,6 +897,35 @@
890897
"$ref" : "#/definitions/fileConfigurationOptions"
891898
}
892899
}
900+
},
901+
"fileData": {
902+
"type": "object",
903+
"additionalProperties" : false,
904+
"anyOf" : [
905+
{
906+
"required" : [
907+
"content"
908+
]
909+
},
910+
{
911+
"required" : [
912+
"path"
913+
]
914+
}
915+
],
916+
"properties": {
917+
"content": {
918+
"type": [
919+
"string",
920+
"path"
921+
],
922+
"description" : "Content of the file, which will be provided inline or written to disk in the workdir. If a !path, the file contents will be read from the provided path."
923+
},
924+
"path": {
925+
"type": "string",
926+
"description" : "Path to the file, relative to the workdir. Used to display in the output."
927+
}
928+
}
893929
}
894930
}
895931
}

tested/dsl/schema.json

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -276,11 +276,18 @@
276276
},
277277
"stdin" : {
278278
"description" : "Stdin for this context",
279-
"type" : [
280-
"string",
281-
"number",
282-
"integer",
283-
"boolean"
279+
"oneOf" : [
280+
{
281+
"type" : [
282+
"string",
283+
"number",
284+
"integer",
285+
"boolean"
286+
]
287+
},
288+
{
289+
"$ref": "#/definitions/fileData"
290+
}
284291
]
285292
},
286293
"arguments" : {
@@ -884,6 +891,32 @@
884891
"$ref" : "#/definitions/fileConfigurationOptions"
885892
}
886893
}
894+
},
895+
"fileData": {
896+
"type": "object",
897+
"additionalProperties" : false,
898+
"anyOf" : [
899+
{
900+
"required" : [
901+
"content"
902+
]
903+
},
904+
{
905+
"required" : [
906+
"path"
907+
]
908+
}
909+
],
910+
"properties": {
911+
"content": {
912+
"type": "string",
913+
"description" : "Content of the file, which will be provided inline or written to disk in the workdir. If a !path, the file contents will be read from the provided path."
914+
},
915+
"path": {
916+
"type": "string",
917+
"description" : "Path to the file, relative to the workdir. Used to display in the output."
918+
}
919+
}
887920
}
888921
}
889922
}

tested/dsl/translate_parser.py

Lines changed: 63 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
Value,
4646
)
4747
from tested.testsuite import (
48+
ContentPath,
4849
Context,
4950
CustomCheckOracle,
5051
EmptyChannel,
@@ -88,9 +89,22 @@ class ReturnOracle(dict):
8889
pass
8990

9091

92+
class ContentPathString(str):
93+
pass
94+
95+
9196
OptionDict = dict[str, int | bool]
9297
YamlObject = (
93-
YamlDict | list | bool | float | int | str | None | ExpressionString | ReturnOracle
98+
YamlDict
99+
| list
100+
| bool
101+
| float
102+
| int
103+
| str
104+
| None
105+
| ExpressionString
106+
| ReturnOracle
107+
| ContentPathString
94108
)
95109

96110

@@ -138,6 +152,12 @@ def _return_oracle(loader: yaml.Loader, node: yaml.Node) -> ReturnOracle:
138152
return ReturnOracle(result)
139153

140154

155+
def _return_path(loader: yaml.Loader, node: yaml.Node) -> ContentPathString:
156+
result = _parse_yaml_value(loader, node)
157+
assert isinstance(result, str), f"A path must be a string, got {result}"
158+
return ContentPathString(result)
159+
160+
141161
def _parse_yaml(yaml_stream: str) -> YamlObject:
142162
"""
143163
Parse a string or stream to YAML.
@@ -148,6 +168,7 @@ def _parse_yaml(yaml_stream: str) -> YamlObject:
148168
yaml.add_constructor("!" + actual_type, _custom_type_constructors, loader)
149169
yaml.add_constructor("!expression", _expression_string, loader)
150170
yaml.add_constructor("!oracle", _return_oracle, loader)
171+
yaml.add_constructor("!path", _return_path, loader)
151172

152173
try:
153174
return yaml.load(yaml_stream, loader)
@@ -187,6 +208,10 @@ def is_expression(_checker: TypeChecker, instance: Any) -> bool:
187208
return isinstance(instance, ExpressionString)
188209

189210

211+
def is_path(_checker: TypeChecker, instance: Any) -> bool:
212+
return isinstance(instance, ContentPathString)
213+
214+
190215
def load_schema_validator(
191216
dsl_object: YamlObject = None, file: str = "schema-strict.json"
192217
) -> Validator:
@@ -215,9 +240,13 @@ def validate_tested_dsl_expression(value: object) -> bool:
215240
schema_object = json.load(schema_file)
216241

217242
original_validator: Type[Validator] = validator_for(schema_object)
218-
type_checker = original_validator.TYPE_CHECKER.redefine(
219-
"oracle", is_oracle
220-
).redefine("expression", is_expression)
243+
type_checker = original_validator.TYPE_CHECKER.redefine_many(
244+
{
245+
"oracle": is_oracle,
246+
"expression": is_expression,
247+
"path": is_path,
248+
}
249+
)
221250
format_checker = original_validator.FORMAT_CHECKER
222251
format_checker.checks("tested-dsl-expression", SyntaxError)(
223252
validate_tested_dsl_expression
@@ -469,16 +498,16 @@ def _convert_text_output_channel(
469498
data = raw_data
470499

471500
if isinstance(stream, str):
472-
return TextOutputChannel(data=data, oracle=GenericTextOracle(options=config))
501+
return TextOutputChannel(content=data, oracle=GenericTextOracle(options=config))
473502
else:
474503
assert isinstance(stream, dict)
475504
if "oracle" not in stream or stream["oracle"] == "builtin":
476505
return TextOutputChannel(
477-
data=data, oracle=GenericTextOracle(options=config)
506+
content=data, oracle=GenericTextOracle(options=config)
478507
)
479508
elif stream["oracle"] == "custom_check":
480509
return TextOutputChannel(
481-
data=data, oracle=_convert_custom_check_oracle(stream)
510+
content=data, oracle=_convert_custom_check_oracle(stream)
482511
)
483512
raise TypeError(f"Unknown text oracle type: {stream['oracle']}")
484513

@@ -599,8 +628,33 @@ def _convert_testcase(testcase: YamlDict, context: DslContext) -> Testcase:
599628
return_channel = IgnoredChannel.IGNORED if "statement" in testcase else None
600629
else:
601630
if "stdin" in testcase:
602-
assert isinstance(testcase["stdin"], str)
603-
stdin = TextData(data=_ensure_trailing_newline(testcase["stdin"]))
631+
if isinstance(testcase["stdin"], dict):
632+
stdin_object = testcase["stdin"]
633+
raw_content = stdin_object.get("content")
634+
raw_path = stdin_object.get("path")
635+
636+
if isinstance(raw_content, ContentPathString):
637+
file_content = ContentPath(path=raw_content)
638+
elif isinstance(raw_content, str):
639+
file_content = _ensure_trailing_newline(raw_content)
640+
elif isinstance(raw_path, str):
641+
file_content = ContentPath(path=raw_path)
642+
else:
643+
assert (
644+
False
645+
), f"Invalid stdin content is required but got {type(raw_content)}"
646+
647+
assert raw_path is None or isinstance(
648+
raw_path, str
649+
), "Path must be a string if given."
650+
651+
file_path = raw_path
652+
else:
653+
assert isinstance(testcase["stdin"], str)
654+
file_content = _ensure_trailing_newline(testcase["stdin"])
655+
file_path = None
656+
657+
stdin = TextData(content=file_content, path=file_path)
604658
else:
605659
stdin = EmptyChannel.NONE
606660
arguments = testcase.get("arguments", [])

tested/languages/generation.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@
3838
MainInput,
3939
Testcase,
4040
TextData,
41-
TextChannelType,
4241
)
4342
from tested.utils import is_statement_strict
4443

@@ -129,8 +128,8 @@ def get_readable_input(
129128
args = f"$ {command}"
130129
# Determine the stdin
131130
if isinstance(case.input.stdin, TextData):
132-
if case.input.stdin.type == TextChannelType.FILE:
133-
stdin = Path(case.input.stdin.data)
131+
if case.input.stdin.path is not None:
132+
stdin = Path(case.input.stdin.path)
134133
else:
135134
stdin = case.input.stdin.get_data_as_string(bundle.config.resources)
136135
else:

tested/languages/preparation.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -487,10 +487,10 @@ def prepare_context(
487487
"""
488488
language = bundle.config.programming_language
489489
resources = bundle.config.resources
490-
before_code = context.before.get(language, TextData(data="")).get_data_as_string(
490+
before_code = context.before.get(language, TextData(content="")).get_data_as_string(
491491
resources
492492
)
493-
after_code = context.after.get(language, TextData(data="")).get_data_as_string(
493+
after_code = context.after.get(language, TextData(content="")).get_data_as_string(
494494
resources
495495
)
496496
testcases, evaluator_names = prepare_testcases(bundle, context)

tested/parsing.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import logging
99
from collections.abc import Callable
1010
from decimal import Decimal
11-
from typing import TYPE_CHECKING, Any
11+
from typing import TYPE_CHECKING, Any, Optional
1212

1313
from cattrs import Converter
1414
from cattrs.gen import make_dict_structure_fn
@@ -133,7 +133,7 @@ def structure(d, cl):
133133

134134
def custom_fallback_field(
135135
converter_arg: Converter,
136-
old_to_new_field: dict[str, tuple[str, Callable[[Any], Any]]],
136+
old_to_new_field: dict[str, tuple[str, Callable[[Any, Optional[Any]], Any]]],
137137
):
138138
def decorator(cls):
139139
struct = make_dict_structure_fn(
@@ -147,7 +147,7 @@ def structure(d, cl):
147147
raise ValueError(
148148
f"You cannot use {new_name} and {k} simultaneously. Migrate to {new_name}."
149149
)
150-
d[new_name] = mapper(d[k])
150+
d[new_name] = mapper(d[k], d)
151151

152152
return struct(d, cl)
153153

0 commit comments

Comments
 (0)