-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Introduce pydantic to validate Oxygen handler result value
- Loading branch information
Showing
6 changed files
with
254 additions
and
2 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,6 +1,7 @@ | ||
robotframework>=3.0.4 | ||
junitparser==2.0 | ||
PyYAML>=3.13 | ||
pydantic>=2.4.2 | ||
|
||
### Dev | ||
mock>=2.0.0 | ||
|
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
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
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 |
---|---|---|
@@ -0,0 +1,65 @@ | ||
import functools | ||
|
||
from typing import List, Optional | ||
from typing_extensions import TypedDict | ||
|
||
from pydantic import TypeAdapter, ValidationError | ||
|
||
from .errors import InvalidOxygenResultException | ||
|
||
# OxygenKeywordDict is defined like this since key `pass` is reserved | ||
# word in Python, and thus raises SyntaxError if defined like a class. | ||
# However, in the functional style you cannot refer to the TypedDict itself, | ||
# like you can with with class style. Oh bother. | ||
# | ||
# See more: | ||
# - https://docs.python.org/3/library/typing.html?highlight=typeddict#typing.TypedDict | ||
# - https://stackoverflow.com/a/72460065 | ||
|
||
_Pass = TypedDict('_Pass', { 'pass': bool }) | ||
class OxygenKeywordDict(_Pass, total=False): | ||
name: str | ||
elapsed: Optional[float] # milliseconds | ||
tags: Optional[List[str]] | ||
messages: Optional[List[str]] | ||
teardown: Optional['OxygenKeywordDict'] # in RF, keywords do not have setup kw; just put it as first kw in `keywords` | ||
keywords: Optional[List['OxygenKeywordDict']] | ||
|
||
|
||
class OxygenTestCaseDict(TypedDict, total=False): | ||
name: str | ||
tags: List[str] | ||
setup: OxygenKeywordDict | ||
teardown: OxygenKeywordDict | ||
keywords: List[OxygenKeywordDict] | ||
|
||
|
||
class OxygenSuiteDict(TypedDict, total=False): | ||
name: str | ||
tags: List[str] | ||
setup: OxygenKeywordDict | ||
teardown: OxygenKeywordDict | ||
suites: List['OxygenSuiteDict'] | ||
tests: List[OxygenTestCaseDict] | ||
|
||
|
||
def _change_validationerror_to_oxygenexception(func): | ||
@functools.wraps(func) | ||
def wrapper(*args, **kwargs): | ||
try: | ||
return func(*args, **kwargs) | ||
except ValidationError as e: | ||
raise InvalidOxygenResultException(e) | ||
return wrapper | ||
|
||
@_change_validationerror_to_oxygenexception | ||
def validate_oxygen_suite(oxygen_result_dict): | ||
return TypeAdapter(OxygenSuiteDict).validate_python(oxygen_result_dict) | ||
|
||
@_change_validationerror_to_oxygenexception | ||
def validate_oxygen_test_case(oxygen_test_case_dict): | ||
return TypeAdapter(OxygenTestCaseDict).validate_python(oxygen_test_case_dict) | ||
|
||
@_change_validationerror_to_oxygenexception | ||
def validate_oxygen_keyword(oxygen_kw_dict): | ||
return TypeAdapter(OxygenKeywordDict).validate_python(oxygen_kw_dict) |
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
181 changes: 181 additions & 0 deletions
181
tests/utest/oxygen_handler_result/test_OxygenKeywordDict.py
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 |
---|---|---|
@@ -0,0 +1,181 @@ | ||
from unittest import TestCase | ||
|
||
from oxygen.errors import InvalidOxygenResultException | ||
from oxygen.oxygen_handler_result import (validate_oxygen_keyword, | ||
OxygenKeywordDict) | ||
|
||
|
||
class _ListSubclass(list): | ||
'''Used in test cases''' | ||
pass | ||
|
||
|
||
class _KwSubclass(OxygenKeywordDict): | ||
'''Used in test cases''' | ||
pass | ||
|
||
|
||
class TestOxygenKeywordDict(TestCase): | ||
def setUp(self): | ||
self.minimal = { 'name': 'someKeyword', 'pass': True } | ||
|
||
def test_validate_oxygen_keyword_validates_correctly(self): | ||
with self.assertRaises(InvalidOxygenResultException): | ||
validate_oxygen_keyword({}) | ||
|
||
def test_validate_oxygen_keyword_with_minimal_valid(self): | ||
minimal1 = { 'name': 'somename', 'pass': True } | ||
minimal2 = { 'name': 'somename', 'pass': False } | ||
|
||
self.assertEqual(validate_oxygen_keyword(minimal1), minimal1) | ||
self.assertEqual(validate_oxygen_keyword(minimal2), minimal2) | ||
|
||
def valid_inputs_for(self, attribute, *valid_inputs): | ||
for valid_input in valid_inputs: | ||
self.assertTrue(validate_oxygen_keyword({**self.minimal, | ||
attribute: valid_input})) | ||
|
||
def invalid_inputs_for(self, attribte, *invalid_inputs): | ||
for invalid_input in invalid_inputs: | ||
with self.assertRaises(InvalidOxygenResultException): | ||
validate_oxygen_keyword({**self.minimal, | ||
attribte: invalid_input}) | ||
|
||
def test_validate_oxygen_keyword_validates_name(self): | ||
class StrSubclass(str): | ||
pass | ||
valid_inherited = StrSubclass('someKeyword') | ||
this_is_not_None = StrSubclass(None) | ||
|
||
self.valid_inputs_for('name', | ||
'', | ||
'someKeyword', | ||
b'someKeyword', | ||
valid_inherited, | ||
this_is_not_None) | ||
|
||
self.invalid_inputs_for('name', None) | ||
|
||
def test_validate_oxygen_keyword_validates_pass(self): | ||
''' | ||
Due note that boolean cannot be subclassed in Python: | ||
https://mail.python.org/pipermail/python-dev/2002-March/020822.html | ||
''' | ||
self.valid_inputs_for('pass', True, False, 0, 1, 0.0, 1.0) | ||
self.invalid_inputs_for('pass', [], {}, None, object(), -999, -99.9) | ||
|
||
def test_validate_oxygen_keyword_validates_tags(self): | ||
self.valid_inputs_for('tags', | ||
[], | ||
['some-tag', 'another-tag'], | ||
None, | ||
_ListSubclass()) | ||
|
||
invalid_inherited = _ListSubclass() | ||
invalid_inherited.append(123) | ||
|
||
self.invalid_inputs_for('tags', [123], {'foo': 'bar'}, object()) | ||
|
||
def test_validate_oxygen_keyword_validates_elapsed(self): | ||
class FloatSubclass(float): | ||
pass | ||
|
||
self.valid_inputs_for('elapsed', | ||
123.4, | ||
-123.0, | ||
'123.4', | ||
'-999.999', | ||
123, | ||
None, | ||
FloatSubclass()) | ||
|
||
self.invalid_inputs_for('elapsed', '', object()) | ||
|
||
def test_validate_oxygen_keyword_validates_messages(self): | ||
valid_inherited = _ListSubclass() | ||
valid_inherited.append('message') | ||
|
||
self.valid_inputs_for('messages', | ||
[], | ||
['message'], | ||
None, | ||
_ListSubclass(), | ||
valid_inherited) | ||
|
||
invalid_inherited = _ListSubclass() | ||
invalid_inherited.append('message') | ||
invalid_inherited.append(123) | ||
|
||
self.invalid_inputs_for('messages', 'some,messages', invalid_inherited) | ||
|
||
def test_validate_oxygen_keyword_validates_teardown(self): | ||
valid_inherited = _KwSubclass(**self.minimal) | ||
|
||
self.valid_inputs_for('teardown', | ||
None, | ||
self.minimal, | ||
valid_inherited, | ||
{**self.minimal, | ||
'something_random': 'will-be-ignored'}) | ||
|
||
self.invalid_inputs_for('teardown', {}) | ||
|
||
def test_validate_oxygen_keyword_validates_keywords(self): | ||
valid_inherited = _ListSubclass() | ||
valid_inherited.append(_KwSubclass(**self.minimal)) | ||
|
||
self.valid_inputs_for('keywords', | ||
None, | ||
[], | ||
[self.minimal, {**self.minimal, | ||
'something_random': 'will-be-ignored'}], | ||
_ListSubclass(), # empty inherited list | ||
valid_inherited) | ||
|
||
invalid_inherited = _ListSubclass() | ||
invalid_inherited.append(_KwSubclass(**self.minimal)) | ||
invalid_inherited.append(123) | ||
self.invalid_inputs_for('keywords', invalid_inherited) | ||
|
||
def test_validate_oxygen_keyword_with_maximal_valid(self): | ||
expected = { | ||
'name': 'keyword', | ||
'pass': True, | ||
'tags': ['some-tag'], | ||
'messages': ['some message'], | ||
'teardown': { | ||
'name': 'teardownKeyword', | ||
'pass': True, | ||
'tags': ['teardown-kw'], | ||
'messages': ['Teardown passed'], | ||
'teardown': None, | ||
'keywords': [] | ||
}, | ||
'keywords': [{ | ||
'name': 'subKeyword', | ||
'pass': False, | ||
# tags missing intentionally | ||
'messages': ['This particular kw failed'], | ||
'teardown': { | ||
'name': 'anotherTeardownKw', | ||
'pass': True, | ||
'tags': ['teardown-kw'], | ||
'messages': ['message from anotherTeardownKw'], | ||
# teardown missing intentionally | ||
'keywords': [] | ||
}, | ||
'keywords': [{ | ||
'name': 'subsubKeyword', | ||
'pass': True | ||
}] | ||
},{ | ||
'name': 'anotherSubKeyword', | ||
'pass': True, | ||
'tags': [], | ||
'messages': [], | ||
'teardown': None, | ||
'keywords': [] | ||
}] | ||
} | ||
|
||
self.assertEqual(validate_oxygen_keyword(expected), expected) |