-
Notifications
You must be signed in to change notification settings - Fork 10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
test: increase unit test coverage #52
Conversation
93dc6ab
to
e12a97d
Compare
de0afc7
to
1c99f8d
Compare
if isinstance(omap[k], tuple): | ||
try: | ||
obj = omap[k][0] | ||
field = _unmarshal(v, obj) | ||
except Exception: | ||
obj = omap[k][1] | ||
field = _unmarshal(v, obj) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have unmarshalled to every TES object and this code never ran. Also the model definitions do not contain any tuples. Maybe this was added to support some legacy model, but I'm pretty sure the code is not used with the current models.
Anyway, instead of trying to create some artificial class to use with the unmarshal()
function to force this code to run, I have just removed it. We can always add more code if needed by future model versions.
Or maybe I missed something? Do you know why these tuple conditionals made it here in the first place?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My guess is that the tuple conditionals were added when outputs
and logs
were represented as tuples in a much earlier TES schema (first added here in commit 8f43a45):
Lines 35 to 43 in 8f43a45
omap = { | |
"tasks": Task, | |
"inputs": TaskParameter, | |
"outputs": (TaskParameter, OutputFileLog), | |
"logs": (TaskLog, ExecutorLog), | |
"ports": Ports, | |
"resources": Resources, | |
"executors": Executor | |
} |
Agreed on the removal as it's no longer necessary.
m: Any = None | ||
if isinstance(j, str): | ||
m = json.loads(j) | ||
elif isinstance(j, dict): | ||
m = j | ||
try: | ||
m = json.loads(j) | ||
except json.decoder.JSONDecodeError: | ||
pass | ||
elif j is None: | ||
return None | ||
else: | ||
raise TypeError("j must be a str, a dict or None") | ||
m = j | ||
|
||
if not isinstance(m, dict): | ||
raise TypeError("j must be a dictionary, a JSON string evaluation to " | ||
"a dictionary, or None") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Previously the behavior of unmarshal()
was not well defined for strings that could be parsed to JSON but that do not end up being dictionaries (list
, int
, float
, bool
, None
). And for those strings that could not be parsed, a JSON decode error would be implicitly raised by the code, which I thought was a little inconsistent.
So here I am ignoring decode parsers but then check that we are really dealing with a dictionary later on, and if not, give a single specific message of what input is expected here (dict, JSON string evaluating to dict, or None
). I find it a bit easier to read as well.
for k, v in e.env: | ||
if not isinstance(k, str) and not isinstance(k, str): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Obviously the check should be for both the key and the value. And Python3 requires .items()
.
@@ -339,7 +335,7 @@ def is_valid(self) -> Tuple[bool, Union[None, TypeError]]: | |||
errs.append("Volume paths must be absolute") | |||
|
|||
if self.tags is not None: | |||
for k, v in self.tags: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Python3 requires .items()
, as above.
@@ -294,7 +290,7 @@ def is_valid(self) -> Tuple[bool, Union[None, TypeError]]: | |||
for e in self.executors: | |||
if e.image is None: | |||
errs.append("Executor image must be provided") | |||
if len(e.command) == 0: | |||
if e.command is None or len(e.command) == 0: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this additional check is necessary, or otherwise an error with traceback is thrown when the user/client passes command=None
- which is rather inconsistent, as we otherwise nicely collect all the (common) problems that may arise.
if value is not None: | ||
return int(value) | ||
return value | ||
if value is None: | ||
return value | ||
return int(value) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No functional changes here. This one (and the next right below for timestampconv()
I just reordered for improved readability (at least in my opinion)
1c99f8d
to
3283e6a
Compare
@attrs | ||
@attrs(repr=False) | ||
class _ListOfValidator(object): | ||
type: Type = attrib() | ||
|
||
def __call__(self, inst, attr, value): | ||
""" | ||
We use a callable class to be able to change the ``__repr__``. | ||
""" | ||
def __call__(self, inst, attr, value) -> None: | ||
if not all([isinstance(n, self.type) for n in value]): | ||
raise TypeError( | ||
"'{attr.name}' must be a list of {self.type!r} (got {value!r} " | ||
"that is a list of {values[0].__class__!r}).", | ||
attr, self.type, value, | ||
f"'{attr.name}' must be a list of {self.type!r} (got " | ||
f"{value!r}", attr |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This one took me a while: I couldn't figure out why the __repr__
never got called in the unit tests and instead repr()
on the validator always returned _ListOfValidator(type=<class 'tes.models.Input'>)
or some such. Turns out that attrs
overrides __repr__
unless you explicitly tell it not to. Probably due to a change in attrs
that came up now that we upgraded dependencies. Setting repr
to False
in the decorator fixed that.
"that is a list of {values[0].__class__!r}).", | ||
attr, self.type, value, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Checking for just values[0]
may be misleading if a list is composed of several different types. I think it's fine to just print the value as received, the user can then see themselves what they passed.
if self.user is not None and self.password is not None: | ||
kwargs['auth'] = (self.user, self.password) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is to address a depreciation warning as already mentioned in the PR description.
3283e6a
to
e5647ca
Compare
e5647ca
to
756d3c4
Compare
--cov=tes/ \ | ||
--cov-branch \ | ||
--cov-report=term-missing \ | ||
--cov-fail-under=99 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The --cov-fail-under=99
condition is a great addition. Awesome to see and something I'll use from now on in other pytest projects!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I've also only found out about it only now, when I replaced the old test runner nose
(which had an equivalent option that was used in the previous CI version) with pytest
. Note that it requires the pytest-cov
extension, though.
@@ -27,14 +27,20 @@ def __init__(self, *args, **kwargs): | |||
|
|||
|
|||
def unmarshal(j: Any, o: Type, convert_camel_case=True) -> Any: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm reviewing the Python docs now, but what are the use cases for a variable having type Type
vs Any
like o
and j
have here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I admit that I have added the type hints in one of the last PRs rather quickly, mostly aiming for completion and making the VS Code Pyright extension shut up (and possibly to make it easier to include static code analysis via mypy
in the CI in the future, if desired). And sometimes I have resorted to annotating with Any
, which is indeed not extremely helpful.
Type
on the other hand is probably a reasonable type hint for o
though, as it is the appropriate (generic) type hint for a class. It may be more accurate to annotate it as the union of all classes that the function is supposed to handle, but it would end up being really really long.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So to answer your question: If I called the function with an object that is not a class, but, e.g., an instance of a class, tools like mypy
or the VS Code Pyright extension would complain. For example, unmarshal(j=..., o=str)
is fine, but unmarshal(j=..., o="some_string")
is not, because:
>>> type(str)
<class 'type'>
>>> type("some_string")
<class 'str'>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's really interesting! I haven't encountered the Type
type before but that makes perfect sense.
if isinstance(omap[k], tuple): | ||
try: | ||
obj = omap[k][0] | ||
field = _unmarshal(v, obj) | ||
except Exception: | ||
obj = omap[k][1] | ||
field = _unmarshal(v, obj) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My guess is that the tuple conditionals were added when outputs
and logs
were represented as tuples in a much earlier TES schema (first added here in commit 8f43a45):
Lines 35 to 43 in 8f43a45
omap = { | |
"tasks": Task, | |
"inputs": TaskParameter, | |
"outputs": (TaskParameter, OutputFileLog), | |
"logs": (TaskLog, ExecutorLog), | |
"ports": Ports, | |
"resources": Resources, | |
"executors": Executor | |
} |
Agreed on the removal as it's no longer necessary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
* feat: support Python 3.7+; drop Python 2.7,<3.7 * feat: pass through json serialization options * docs: add advanced usage examples * feat: spec-compliant routes, with legacy support * fix: remove debug messages * test: increase unit test coverage (#52) * test: increase unit test coverage * Update tests.yml --------- Co-authored-by: Liam Beckman <lbeckman314@gmail.com> * docs: browsable API reference on GitHub Pages (#49) * docs: browsable API reference on GitHub Pages * Update tests.yml --------- Co-authored-by: Liam Beckman <lbeckman314@gmail.com> * Minor release testing updates * Update support for Service Info (#55) - Fix version to match PyPi release (#59) * Add integration test with Funnel * Add initial TES integration test (Funnel) * Add Tmate debug session * Update Funnel installation * Re-add all unit tests * Fix Funnel download issue with correct rc version * Add latest stable Python to test matrix * Fix unit tests not reaching Funnel * Increase test coverage * Minor linting fix * Update raised exception for invalid server response * Update README * Update README.md * Exclude tests from packages (#58) * Add in 3 fields missing in the models for tes. --------- Co-authored-by: Alex Kanitz <alexander.kanitz@alumni.ethz.ch> Co-authored-by: Kyle Ellrott <kellrott@gmail.com> Co-authored-by: Ben Beasley <code@musicinmybrain.net> Co-authored-by: Venkat Malladi <vmalladi@microsoft.com>
unittest
's deprecated.assertEquals()
and.assertAlmostEquals()
calls with.assertEqual()
and.assertAlmostEqual
auth
as parameter torequests
ifuser
and/orpassword
are not specified (acceptingNone
for these is deprecated fromrequests 3.0.0
)pytest-cov
(now intests/requirements.txt
), along with the branch testing flag--cov-branch
switched on and the--cov-fail-under
option set to 99 (can be changed to 100, once chore: remove unused method & exception #50 is merged or tests are provided for the missing statement intes.client.HTTPClient.wait()
)Resolves #47