Conversation
Summary of ChangesHello @lukkrusz, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request significantly extends the testing capabilities of the trunner framework by integrating Pytest. It provides a dedicated Pytest harness that allows users to define and execute tests using the popular Pytest framework, with results seamlessly reported back to the TestRunner. The changes include new plugins to manage Pytest's output and map its test outcomes to the framework's subtest structure, alongside a new TestType enum for improved test configuration and validation. Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request introduces pytest support, which is a great addition to the testing framework. The implementation is solid, with good use of pytest plugins to bridge with the existing TestRunner and to control output. The new TestType enum improves the configuration parsing by making it more robust and readable.
I've left a few comments with suggestions for improvement:
- In the sample test file, there's a fragile test that depends on execution order. I've suggested how to make it more robust.
- The error message for unknown test types in the configuration could be more descriptive.
- Most importantly, for failed pytest tests, the failure details were not being captured. I've suggested a change to include these details in the test report, which will be very helpful for debugging.
- There is some code duplication in the sample
conftest.pyfile which could be refactored.
mateusz-bloch
left a comment
There was a problem hiding this comment.
Only quick look.
I’m not sure what the expected behaviour were, but wouldn’t it be better to parse specific failures per test case? For example if there were hundreds of test cases, this could be more readable, and we would still have the full output available via -S. @damianloew
trunner/harness/pytest.py
Outdated
| result.status = Status.FAIL | ||
| elif all(sub.status == Status.SKIP for sub in result.subresults) and result.subresults: | ||
| result.status = Status.SKIP | ||
| elif not result.subresults and exit_code != 0: |
There was a problem hiding this comment.
While I'm on it, in a case when the teardown fails, do we want to treat the entire case as FAIL or just the ultimate test result?
One could argue that the test case passed but it was a post-test operation that failed.
As is now, in case of such a failure, the full pytest trace is being released like on the provided screenshot
There was a problem hiding this comment.
The decision has been made to treat setup/teardown ERROR as the entire test-case FAIL.
trunner/harness/pytest.py
Outdated
| *options, | ||
| "-v", | ||
| "-s", | ||
| "--tb=no", |
There was a problem hiding this comment.
Do we need to disable the traceback?
There was a problem hiding this comment.
The traceback suppression can shorten the logs, thus improving the readability when we hit many errors during a test run but I could change it to default and support a kwarg to set it in test yamls. What do you think, @damianloew ?
There was a problem hiding this comment.
I agree that in case of failure we should provide the traceback output next to the result of the particular test case. In the current approach there can be situation where the error message is not sufficient - when I simply removed dut fixture there is no information about that:

Referring to @mateusz-bloch comment about parsing specific failures per test case - I agree on that. We should provide the solution where we have only the message about the failure, not the whole pytest output when we have fail. Now the output looks very similar to this with streaming option when we have at least one fail.
Our python tests in phoenix-rtos-tests repo are outdated a bit, but after quickly rewriting one test case to the new approach with adding subresults the behavior in case of failure would be like that:

We want to have the similar behavior in pytest tests - traceback/longer error message printed next to the test case result (without the whole streaming output when this option is not set). When we have streaming option active I don't see any contraindications to not show the traceback 2 times - in streaming output and then next to the results.
There was a problem hiding this comment.
Sorry about misleading screenshot - of course the overall result should be FAIL.
damianloew
left a comment
There was a problem hiding this comment.
First suggestions, just to order the changes first
2ead454 to
75b6ffa
Compare
nalajcie
left a comment
There was a problem hiding this comment.
Pointing out some issues in the current code. Please note that the commit list is too verbose, the final history should not include fixes to your own code (added within the same PR) - I would expect 2-3 commits in the final history.
Adding separate commits after review might be beneficial, for tracking review progress, but they should be squashed sooner than later.
trunner/harness/pytest.py
Outdated
| def get_logs(self) -> str: | ||
| """Get the full, unsuppressed pytest log | ||
| """ | ||
| return self.buffer.getvalue() |
There was a problem hiding this comment.
does it work if we're streaming? (we're attaching the buffer only if _suppress == true
There was a problem hiding this comment.
It's indeed a dead functionality.
We are removing the log retention since the important to us bits are being parsed to trunner anyway.
I will also make use of trunner logging for this purpose and modify the pytest output to fit our needs.
There was a problem hiding this comment.
ok, the test stdout will still be caught by DUT wrapping? (I'm wondering what will finally be available in JUnit XML output)
There was a problem hiding this comment.
I'm not sure if I follow. JUnit XML will contain testsuites, testcases, fail/skip messages followed by system-out traceback (in case of failure).
Each testcase result is being handled before the terminal output, internally.
The expected terminal output, as established with @damianloew, would be a mix of output from DUT and test stdout, it would look like:
/sample/test/pytest.py::test_send_hello...
[DUT] Hello
/sample/test/pytest.py::test_send_hello PASS
trunner/config.py
Outdated
| self.test.harness = PyHarness(self.ctx.target.dut, self.ctx, unity_harness, self.test.kwargs) | ||
|
|
||
| def _parse_pytest(self): | ||
| self.test.shell = None |
There was a problem hiding this comment.
why overwriting shell? If the shell must not be set, then the parser should probably fail when the key is present?
can't we have shell for pytest-based tests?
There was a problem hiding this comment.
We established that for now we keep the assumption that all pytest tests won't use the shell. The current pytest tests that we want to introduce into the trunner do not interact with the system shell, and the primary goal now is to run them in the unified environment.
If we planned to write tests that interact with our system using pytest, we would extend this support. Tt would probably be a nice idea, but we want to carry out this process gradually.
About failing - now test.shell is always being set - depending on config in yaml ShellOptions with additional arguments or not is being set (always we want to parse prompt). I think that we can keep this approach and just treat pytest tests separately. That's why we cannot raise fail in such case.
However, @lukkrusz we should leave a comment noting that it's temporary - ultimately I'd require to set shell: False or sth similar in test.yaml for tests, which don't interact with a system shell.
There was a problem hiding this comment.
When you do some design decision (like "interacting with shell via pytest is not possible") you need to write them down (at least in the code) - otherwise it looks like a bug / hack / unfinished implementation.
I feel that this is done only to make running tests on host-generic-pc easier, having shell on other targets won't complicate running pytest-based tests at all, so probably this assumption can be removed in the future (I might be wrong, as I've only read the code).
I would appreciate adding at least one real test which actually interacts with the DUT in CI, as I'm not sure what's the intended use case.
There was a problem hiding this comment.
I see that @lukkrusz left the following comment elsewhere:
# Specific case where we do not need the shell command
# TODO: In the future, we could support shell communication
However, we can also clarify that in the commit description and maybe express it in a better way.
The main reason of this support now is providing the possibility to write or run the existent functional tests using pytest in our internal project, where we don't interact with a system console - to have the ability to run them even if we don't have a port with this console available (release fw).
There was a problem hiding this comment.
You've quoted the Host test runner implementation, so I consider it a limitation of this target, not an overall requirement. Overwriting self.test.shell = None while parsing a config (generic code) without any comment is a bad code pattern.
There was a problem hiding this comment.
So we want to accept the scenario where we don't have access to the system console - empty DUT like on host target, but also the situation where dut device is available and we can parse some additional information there.
We will try to show it somehow in a test.
There was a problem hiding this comment.
We discussed a new approach and I hope that setting shell to None and changing Host target won't be needed then. Thanks for the suggestions!
damianloew
left a comment
There was a problem hiding this comment.
Suggestions for the code before the recent force push. I haven't seen much unnecessary code, which is good - changes are quite clear for now
trunner/harness/pytest.py
Outdated
|
|
||
| def pytest_harness(dut: Dut, ctx: TestContext, result: TestResult, **kwargs) -> TestResult: | ||
| test_path = ctx.project_path / kwargs.get("path") | ||
| options = kwargs.get("options", "").split() |
There was a problem hiding this comment.
It seems that we won't want to change pytest options per test (all of the command line options are rather for the whole pytest campaign configuration), so probably we can remove that.
There was a problem hiding this comment.
After some thinking, we can bring it back - it's helpful when we want to add a custom argument for a test - for example to skip initialization phase, which is not always necessary.
trunner/harness/pytest.py
Outdated
|
|
||
| try: | ||
| exit_code = pytest.main(cmd_args, plugins=[bridge_plugin, log_plugin]) | ||
| except Exception as e: |
There was a problem hiding this comment.
Let's handle various exception types separately - when you provided dut to be used in pytest tests - you have to be ready for failures typical for pexpect, so similarly to pyharness.
UnicodeDecodeError- should be handledAssertionError- seems to be fine (printing traceback from pytest)- EOF, or TIMEOUT from pexpect - I am not sure how it behaves now, to be verified
HarnessError- Probably won't be raised from pytest, so we could skip that
a09a72d to
988c5d7
Compare
988c5d7 to
5848076
Compare
damianloew
left a comment
There was a problem hiding this comment.
Looking only at tests for now
sample/pytest/conftest.py
Outdated
|
|
||
| @pytest.fixture(scope="session") | ||
| def pexpect_bin(dut): | ||
| pexpect = dut.pexpect_proc |
There was a problem hiding this comment.
pexpect var name may be problematic, especially if there was import pexpect, let's change it a bit.
sample/pytest/conftest.py
Outdated
| HARDCODED_VERB = 2 | ||
|
|
||
|
|
||
| def _print(text: str): |
There was a problem hiding this comment.
We can remove this debug print - python prints are not a valid use case in our tests, so I don't see a value of leaving it. You already have shown session scope utility even without conftest_print, you can also use global_session_resource simply without additional logs.
sample/pytest/conftest.py
Outdated
| import pytest | ||
| import trunner | ||
|
|
||
| HARDCODED_VERB = 2 |
There was a problem hiding this comment.
To be removed according to the comment below.
sample/pytest/test-pytest.py
Outdated
| assert True | ||
|
|
||
|
|
||
| @pytest.mark.skip(reason="CI CHECK") |
There was a problem hiding this comment.
Let's keep our format - for every skip we want to leave the issue number. As it's a demo we can do it this way:
| @pytest.mark.skip(reason="CI CHECK") | |
| @pytest.mark.skip(reason="#x issue") |
sample/pytest/test-pytest.py
Outdated
| assert False, "Always Fail" | ||
|
|
||
|
|
||
| @pytest.mark.skip(reason="Should not fail because it's skipped") |
There was a problem hiding this comment.
I don't see the reason to leave this case - as it's almost the same as the previous one.
sample/pytest/test-psh.py
Outdated
| psh.assert_cmd( | ||
| pexpect_psh, | ||
| f"echo {msg}", | ||
| expected=f"{resp}\r+\n", |
There was a problem hiding this comment.
| expected=f"{resp}\r+\n", | |
| expected=fr"{resp}\r+\n", |
sample/pytest/test-psh.py
Outdated
| pexpect_psh, | ||
| f"echo {msg}", | ||
| expected=f"{resp}\r+\n", | ||
| result="dont-check", |
There was a problem hiding this comment.
This argument is intended to check the return code, instead of 0 or 1 in echo_resp_code_list you can pass "success"/"fail" and pass it here. Would be simpler than marking that you don't want to check return code and then checking it on your own.
aab8f8f to
fc8dab1
Compare
6e6faba to
ee690c8
Compare
b1fb0fd to
77c859e
Compare
6cc0d0a to
8023e6d
Compare
Includes conftest.py, yaml config, binary and Makefile JIRA: CI-614
JIRA: CI-614
JIRA: CI-614
8023e6d to
3159a4b
Compare

PyTest support implementation for our existing testing framework.
Description
The implementation includes
Motivation and Context
Types of changes
How Has This Been Tested?
Checklist:
Special treatment
Previous conversation: #443