-
Notifications
You must be signed in to change notification settings - Fork 18
Make infratest optional #231
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
base: main
Are you sure you want to change the base?
Conversation
This removes the use of testinfra from the main codebase and the test suite making it an optional dependency. You can still use "connection.connection" to get testinfra's connection object if you have it installed.
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #231 +/- ##
==========================================
- Coverage 94.88% 94.31% -0.57%
==========================================
Files 9 10 +1
Lines 1114 1214 +100
Branches 164 182 +18
==========================================
+ Hits 1057 1145 +88
- Misses 42 45 +3
- Partials 15 24 +9 ☔ View full report in Codecov by Sentry. |
dcermak
left a 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.
Thank you for this giant PR! I haven't managed to go through this fully and will need a bit of time. In principle I'm not opposed to making testinfra optional, but it must remain usable and preferably via the same API as before, as we heavily rely on testinfra in tests utilizing pytest_container.
Also, we shouldn't decrease test coverage and will need to adjust the docs (but let's figure out the code first)
| Sphinx = ">=5.0" | ||
| pytest-rerunfailures = ">=10.2" | ||
| typeguard = ">=2.13" | ||
| ifaddr = "^0.2.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.
Please make this an
| ifaddr = "^0.2.0" | |
| ifaddr = ">=0.2.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.
sure
| return self.__class__.__name__ | ||
|
|
||
|
|
||
| LOCALHOST = testinfra.host.get_host("local://") |
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 a public object, please wrap this in a try: import testinfra … except block
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.
right, missed this.
| ) | ||
|
|
||
| def _get_container_inspect(self, container_id: str) -> Any: | ||
| def _run_inspect(self, container_id: str) -> Union[Dict[str, Any], 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.
Technically the return type should be Optional[Dict[str, Any]], but I really don't like the option of us returning None here. We should either return an inspect or error out. Otherwise we just push error handling down on the API user.
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.
If I throw an error on missing, I need to catch it in prepare_container (and then check if the error is because missing or parse error) or make a second function for the same thing (like it is now). Didn't want to refactor prepare_container() too much.
| if len(inspect) != 1: | ||
| if not isinstance(inspect, list): | ||
| raise RuntimeError( | ||
| f"Expected a list of results, but got {inspect} instead" |
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.
Did you want to include the type in the error message instead?
| f"Expected a list of results, but got {inspect} instead" | |
| f"Expected a list of results, but got {type(inspect)} instead" |
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, I wanted to result of the command, so it's clear what went wrong.
| raise ValueError(f"Invalid CONTAINER_RUNTIME value: {requested}") | ||
| current = requested | ||
| if current == "auto": | ||
| podmain_path = shutil.which("podman") |
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.
typo: podmain -> podman
| resolved_buildah_binary: Optional[str] = None | ||
|
|
||
| if buildah_binary is _AutoDetect.INSTANCE: | ||
| # try to guess if argument is omitted | ||
| buildah_binary = shutil.which("buildah") | ||
|
|
||
| if buildah_binary is None: | ||
| # if explicitly set to None or not found, we don't have buildah | ||
| resolved_buildah_binary = None | ||
| self._buildah_functional = False | ||
|
|
||
| elif isinstance(buildah_binary, str): | ||
| # make sure it's fully resolved and not just a binary name | ||
| buildah_binary = shutil.which("buildah") | ||
| self._buildah_functional = ( | ||
| subprocess.run( | ||
| [buildah_binary, "--version"], check=False | ||
| ).returncode | ||
| == 0 | ||
| if buildah_binary is not None | ||
| else False | ||
| ) | ||
| resolved_buildah_binary = buildah_binary |
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.
Which high level problem are you trying to solve 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.
As with rest, I don't want the checks to run on module imports and I tried to preserve the original logic of detection if someone creates PodmanRuntime() directly. I can simplify this if you don't really care about the detection in that case and only through get_selected_platform()
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 detection can be as dead-simple as possible, there's really no need to try be "smart" here as no one requires it (for now).
| @dataclass(frozen=True) | ||
| class ContainerRemoteEndpoint: | ||
| _container_id: str | ||
| _runtime: OciRuntimeBase | ||
|
|
||
| def __post_init__(self) -> None: | ||
| assert self._container_id, "Container ID must not be empty" | ||
|
|
||
| def check_output(self, cmd: str, strip: bool = True) -> str: | ||
| """Run a command in the container and return its output.""" | ||
| return self._runtime.run_command( | ||
| "exec", | ||
| self._container_id, | ||
| "/bin/sh", | ||
| "-c", | ||
| cmd, | ||
| strip=strip, | ||
| ) | ||
|
|
||
| def exists(self, command: str) -> bool: | ||
| """Check if a command exists in the container.""" | ||
| return ( | ||
| self.check_output(f"command -v {command} || echo '<missing>'") | ||
| != "<missing>" | ||
| ) | ||
|
|
||
| def file(self, path: str) -> "ContainerConnectionFile": | ||
| return ContainerConnectionFile(path=path, _remote=self) | ||
|
|
||
| def copy( | ||
| self, local: Union[str, Path], remote: str | ||
| ) -> "ContainerConnectionFile": | ||
| """Copy a file from the host to the container.""" | ||
| if isinstance(local, Path): | ||
| local = str(local.absolute()) | ||
| self._runtime.run_command( | ||
| "cp", local, f"{self._container_id}:{remote}" | ||
| ) | ||
| return self.file(remote) | ||
|
|
||
|
|
||
| @dataclass(frozen=True) | ||
| class ContainerConnectionFile: | ||
| path: str | ||
| _remote: ContainerRemoteEndpoint | ||
|
|
||
| def _test(self, test: str) -> bool: | ||
| return ( | ||
| self._remote.check_output( | ||
| f"test {test} {self.path} && echo 1 || echo 0" | ||
| ) | ||
| == "1" | ||
| ) | ||
|
|
||
| @property | ||
| def exists(self) -> bool: | ||
| return self._test("-e") | ||
|
|
||
| @property | ||
| def is_file(self) -> bool: | ||
| return self._test("-f") | ||
|
|
||
| @property | ||
| def is_directory(self) -> bool: | ||
| return self._test("-d") | ||
|
|
||
| @property | ||
| def content_string(self) -> str: | ||
| try: | ||
| return self._remote.check_output(f"cat {self.path}", strip=False) | ||
| except subprocess.CalledProcessError as e: | ||
| if "Is a directory" in e.stderr: | ||
| raise ValueError(f"{self.path} is a directory") from e | ||
| raise | ||
|
|
||
| def listdir(self) -> List[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.
I don't think that it is maintainable to start implementing a subset of testinfra in pytest_container. I think all that we should offer is check_output. If a user doesn't wish to use testinfra, then that's fine by me, but then they should handle these checks by themself.
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 only the set that is needed in the test suite. I can change it to private API. I personally, don't have a use for any of it.
| try: | ||
| # universal_newlines is an alias for text before python 3.7 | ||
| result = subprocess.check_output( | ||
| cmd, stderr=subprocess.PIPE, universal_newlines=True | ||
| ) | ||
| except subprocess.CalledProcessError as exc: | ||
| if not ignore_errors: | ||
| _logger.error(exc.stderr) | ||
| raise exc | ||
| result = exc.stdout |
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.
Why not use subprocess.run() instead?
Also, I would, highly, highly suggest to log the call. Testinfra does this and it is a savior knowing which commands are run.
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'll change it to {{run()}} and add logging. Note that the current code doesn't log any of these either.
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 current code does in indeed not log the call, but since this now becomes the replacement of testinfra's check_output, which logs the output & result, I think it should log too.
| Sphinx = ">=5.0" | ||
| pytest-rerunfailures = ">=10.2" | ||
| typeguard = ">=2.13" | ||
| ifaddr = "^0.2.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.
To be frankly honest, we do not need ifaddr. Our own testsuite can rely on testinfra, even if testinfra became optional.
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.
Well, then I can't run the test suite on Windows or OS X.
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.
Then please make this a windows & OSX only dependency
| with pytest.raises( | ||
| RuntimeError, match=f"Container with id {name} not found" | ||
| ): | ||
| container_runtime.inspect_container(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.
This will inspect the container i_will_fail_to_launch not the pod i_will_fail_to_launch. As we never created that container, this will fail always
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, missed the "pod" argument. Will fix this.
dcermak
left a 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've taken a first cursory pass, but I can already tell you that this PR is too large for my taste. You're trying to change too many things in one pull request/commit:
- stylistic import changes and reformats (some of them I agree with, others are really a matter of personal preference)
- change how buildah is found
- container runtime detection in general
- introduce the new testinfra compat classes
Therefore, please split this pull request into multiple smaller ones. I would suggest to start with the buildah & autodetection refactoring and then the testinfra compat class.
Also, you entered into the changelog, that the test suite now runs on OSX, but there's no CI run added to that. And I would really like to keep most of the test relying on testinfra, as that is the major focus of pytest_container. I am not opposed to adding new tests though, I'd actually very much prefer that
|
What problem does this solve
My team is using
pytest-containersfor some time now, but we faced a lot of problems trying to get it working on Windows. This is mainly due topytest-testinfranot really supporting that platform.The change aims to make the core functionalities of the project usable on Windows.
Summary of the changes
pytest-testinfrato an optional dependency installable via[testinfra]extrahost.package()is not implement on OS Xhostto perform local operations. Instead I implemented the minimal set of functions need by the suite in a newremoteattribute.ifaddrlibrary as a test dependency for queries available network interfaces. This is not used outside of tests.get_selected_platform()and the runtime classes to not run any shell commands while being imported. The absolute path to the podman/docker command is now resolved lazily and the available platform is selected (following the same podman > docker preference).