-
Notifications
You must be signed in to change notification settings - Fork 119
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
Add process execution simulation to test harness #993
Conversation
@weiiwang01 Is this ready for review? If so, I can take a look tomorrow. Thanks for the One other thing: it looks like the docs are failing right now. You can generate those locally with |
Yes, this is ready for review. I have fixed the document issue and reverted changes in |
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 great work again, thanks @weiiwang01. I've left a bunch of comments, but most of them are minor -- structurally it seems fine.
ops/pebble.py
Outdated
t = _start_thread(shutil.copyfileobj, self.stdout, out) | ||
self._threads.append(t) | ||
# self.stdout can be None, when exec is invoked with the stdout argument | ||
if self.stdout is not 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.
I think the correct fix here is to raise an exception in this case (at the top of the method), because ExecProcess.wait_output
shouldn't be called if exec
is invoked with the stdout
argument. wait_output
is intended to be used when you want to collect output as a string. If using exec
's stdout
argument, you'd use ExecProcess.wait
. I'd probably raise TypeError
with a nice message.
We can tweak the docstring to be more explicit about that too.
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've rolled back the changes. Let's address this issue specifically in the next pull request. I hope there's a way so we can use some type gymnastics to avoid using exceptions while keep the typing.
ops/testing.py
Outdated
if self._exit_code != 0: | ||
raise pebble.ExecError(self._command, cast(int, self._exit_code), None, None) | ||
|
||
def wait_output(self) -> Tuple[Optional[AnyStr], Optional[AnyStr]]: |
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.
We should change this back to the same signature (non-optional stdout) and check that they didn't pass stdout in to exec -- similar to my first comment.
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 is a private class within the testing module. I believe this function signature has no effect. I'll update it in the next pull request to align with pebble.py
.
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.
Perhaps it has no effect, but let's change it to the same signature as the real wait_output
in this PR to avoid confusion.
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.
Updated, thanks!
Co-authored-by: Ben Hoyt <[email protected]>
Co-authored-by: Ben Hoyt <[email protected]>
Co-authored-by: Ben Hoyt <[email protected]>
Co-authored-by: Ben Hoyt <[email protected]>
Co-authored-by: Ben Hoyt <[email protected]>
Co-authored-by: Ben Hoyt <[email protected]>
Co-authored-by: Ben Hoyt <[email protected]>
Co-authored-by: Ben Hoyt <[email protected]>
Co-authored-by: Ben Hoyt <[email protected]>
Co-authored-by: Ben Hoyt <[email protected]>
Co-authored-by: Ben Hoyt <[email protected]>
Co-authored-by: Ben Hoyt <[email protected]>
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.
Thanks for the updates! This is very close now. Let's just make the simplification to use a dict for the registered handlers, and I've added a couple of other minor suggestions.
I also tweaked the handle_exec docstring and fixed it in a couple of places, and reverted the line-break change to ops/pebble.py to avoid that in the diff.
ops/testing.py
Outdated
if isinstance(result, int): | ||
result = ExecResult(exit_code=result) | ||
if isinstance(result, (str, bytes)): | ||
result = ExecResult(stdout=result) |
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.
How about this, to help people not using static type checking? The "not bool" test is because bools are ints, but we don't want someone confusing things by returning True (1) or False (0).
if isinstance(result, int): | |
result = ExecResult(exit_code=result) | |
if isinstance(result, (str, bytes)): | |
result = ExecResult(stdout=result) | |
if isinstance(result, int) and not isinstance(result, bool): | |
result = ExecResult(exit_code=result) | |
elif isinstance(result, (str, bytes)): | |
result = ExecResult(stdout=result) | |
elif not isinstance(result, ExecResult): | |
raise TypeError(f"result must be int, str, bytes, or ExecResult, not {result.__class__.__name__}") |
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.
Never know the bool
is int
, but I guess it makes sense, updated, thanks!
ops/testing.py
Outdated
def wait(self): | ||
if self._is_timeout: | ||
raise pebble.TimeoutError( | ||
f'timed out waiting for change 123 ({self._timeout} seconds)' |
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.
What I was suggesting here is that we simply remove the change ID, the " 123" part, from the error message entirely. I think it will be confusing to have a hard-coded value 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.
Gotcha, removed, thanks!
ops/testing.py
Outdated
if self._exit_code != 0: | ||
raise pebble.ExecError(self._command, cast(int, self._exit_code), None, None) | ||
|
||
def wait_output(self) -> Tuple[Optional[AnyStr], Optional[AnyStr]]: |
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.
Perhaps it has no effect, but let's change it to the same signature as the real wait_output
in this PR to avoid confusion.
ops/testing.py
Outdated
self._exec_handlers.insert(idx, (prefix, handler)) | ||
inserted = True | ||
if not inserted: | ||
self._exec_handlers.append((prefix, handler)) |
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 think we should -- the current code is fairly tricky and complex, for no practical benefit in this use case. We're only ever going to have a small handful of registered handlers, so whizzing through them linearly each time is fine. To get simple code, let's use a dict and a _find_exec_handler
along the lines of what I suggested.
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.
Excellent, thanks. Love the new simple lookup technique! Just a couple of nits and then we're good to go.
ops/testing.py
Outdated
raise TypeError("Either handler or result must be provided, but not both.") | ||
container_name = container if isinstance(container, str) else container.name | ||
if result is None: | ||
pass |
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.
Ah yes, thanks. Though this confused me a bit at first. How about we put this in an indented block and perform the checks and set the handlers to lambda _: result
only if result is not None
(then we can skip the slightly awkward inline if-else below):
if result is not None:
if isinstance(result, int) and not isinstance(result, bool):
result = ExecResult(exit_code=result)
elif isinstance(result, (str, bytes)):
result = ExecResult(stdout=result)
elif not isinstance(result, ExecResult):
raise TypeError(
f"result must be int, str, bytes, or ExecResult, not {result.__class__.__name__}")
handler = lambda _: result
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.
Understood, updated. However, I can't change handler = lambda _: result
because of the linting checks.
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.
Ugh, I hate it when our tools get in the way of clean simple code. I guess we could do this, but I'm not sure it's any better. I leave it up to you. I'll probably merge this tomorrow!
# autopep8: off
handler = lambda _: result # noqa
# autopep8: on
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.
Three annotations for a single statement might be a little bit too much. I'll probably keep the original statement, thanks.
prometheus-k8s-operator unit tests are failing, but that's not related to this PR -- I've made them aware. Merging! Thanks for your work on this, @weiiwang01. |
The Pebble process execution plays an important role in a lot of charms. However, the ops framework testing module currently lacks built-in functionality to simulate this process, leading many charms to roll their own patching and mocking mechanisms for unit tests.
To address this problem, this pull request introduces
ops.testing.Harness.handle_exec
, a new feature that enables the simulation of Pebble process execution within the Ops framework testing module.See spec OP039 for discussion.