Skip to content

Commit

Permalink
Migrate tests to RPyC (#1040)
Browse files Browse the repository at this point in the history
This is a major PR, but long overdue.

GEF can now use RPyC to completely control the tests which not only make
them way faster, also allow to control directly from the `pytest` tests
themselves `gdb` and `gef` (see examples). Tests are therefore smaller,
less messy, and more accurate than when using the legacy function
`gdb_run_cmd`.

The documentation in `docs/testing.md` has been updated accordingly.

This allowed to find a few missing things in gef.py itself, which were
added but nothing major.

Note that currently the PR is not perfect as I did not re-write the
tests: I've migrated most of the code to the new API to ensure coverage
stays identical. This was done because the PR is already big enough as
it is. More accurate tests can be added in following PRs.


Co-authored-by: Grazfather <[email protected]>
  • Loading branch information
hugsy and Grazfather authored Jan 9, 2024
1 parent deeab2f commit bcaabff
Show file tree
Hide file tree
Showing 66 changed files with 2,347 additions and 1,823 deletions.
8 changes: 3 additions & 5 deletions .github/workflows/coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,10 @@ jobs:
* ${(forbidden_words.length === 0 || forbidden_words[0] === '') ? "**does not** include forbidden words" : "includes the forbidden words:" + forbidden_words.join(", ")}
`;
const { owner, repo, number } = context.issue;
await github.rest.issues.createComment({ owner, repo, issue_number: number, body: comment });
try {
const { owner, repo, number } = context.issue;
await github.rest.issues.createComment({ owner, repo, issue_number: number, body: comment });
if(docs_changes > 0) {
await github.rest.issues.addLabels({
owner: owner,
Expand All @@ -92,9 +92,7 @@ jobs:
labels: ['documentation']
});
}
} catch (err) { console.log(err); }
try {
if(tests_changes > 0) {
await github.rest.issues.addLabels({
owner: owner,
Expand Down
20 changes: 19 additions & 1 deletion .pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,25 @@ confidence=
; anomalous-backslash-in-string,
; bad-open-mode

enable = F,E,unreachable,duplicate-key,unnecessary-semicolon,unused-variable,binary-op-exception,bad-format-string,anomalous-backslash-in-string,bad-open-mode,dangerous-default-value,trailing-whitespace,unneeded-not,singleton-comparison,unused-import,line-too-long,multiple-statements,consider-using-f-string,global-variable-not-assigned
enable =
F,
E,
anomalous-backslash-in-string,
bad-format-string,
bad-open-mode,
consider-using-f-string,
dangerous-default-value,
duplicate-key,
global-variable-not-assigned
line-too-long,
singleton-comparison,
trailing-whitespace,
unnecessary-semicolon,
unneeded-not,
unreachable,
unused-import,
unused-variable,
binary-op-exception
disable = all

[REPORTS]
Expand Down
69 changes: 58 additions & 11 deletions docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,11 @@ You can then use `pytest` directly to help you fix each error specifically.
GEF entirely relies on [`pytest`](https://pytest.org) for its testing. Refer to the project
documentation for details.

Adding a new command __requires__ for extensive testing in a new dedicated test module that should
be located in `/root/of/gef/tests/commands/my_new_command.py`
Adding new code __requires__ extensive testing. Tests can be added in their own module in the
`tests/` folder. For example, if adding a new command to `gef`, a new test module should be created
and located in `/root/of/gef/tests/commands/my_new_command.py`. The test class __must__ inherit
`tests.base.RemoteGefUnitTestGeneric`. This class allows one to manipulate gdb and gef through rpyc
under their respective `self._gdb` and `self._gef` attributes.

A skeleton of a test module would look something like:

Expand All @@ -60,30 +63,74 @@ A skeleton of a test module would look something like:
"""


from tests.utils import GefUnitTestGeneric, gdb_run_cmd, gdb_start_silent_cmd
from tests.utils import RemoteGefUnitTestGeneric


class MyCommandCommand(GefUnitTestGeneric):
class MyCommandCommand(RemoteGefUnitTestGeneric):
"""`my-command` command test module"""

def setUp(self) -> None:
# By default, tests will be executed against the default.out binary
# You can change this behavior in the `setUp` function
self._target = debug_target("my-custom-binary-for-tests")
return super().setUp()

def test_cmd_my_command(self):
# `my-command` is expected to fail if the session is not active
self.assertFailIfInactiveSession(gdb_run_cmd("my-command"))
# some convenience variables
root, gdb, gef = self._conn.root, self._gdb, self._gef

# You can then interact with any command from gdb or any class/function/variable from gef
# For instance:

# `my-command` should never throw an exception in GDB when running
res = gdb_start_silent_cmd("my-command")
self.assertNoException(res)
# * tests that `my-command` is expected to fail if the session is not active
output = gdb.execute("my-command", to_string=True)
assert output == ERROR_INACTIVE_SESSION_MESSAGE

# it also must print out a "Hello World" message
self.assertIn("Hello World", res)
# * `my-command` must print "Hello World" message when executed in running context
gdb.execute("start")
output = gdb.execute("my-command", to_string=True)
assert "Hello World" == output
```

You might want to refer to the following documentations:

* [`pytest`](https://docs.pytest.org/en/)
* [`gdb Python API`](https://sourceware.org/gdb/current/onlinedocs/gdb.html/Python-API.html)
* (maybe) [`rpyc`](https://rpyc.readthedocs.io/en/latest/)

When running your test, you can summon `pytest` with the `--pdb` flag to enter the python testing
environment to help you get more information about the reason of failure.

One of the most convenient ways to test `gef` properly is using the `pytest` integration of modern
editors such as VisualStudio Code or PyCharm. Without proper tests, new code will not be integrated.

Also note that GEF can be remotely controlled using the script `scripts/remote_debug.py` as such:

```text
$ gdb -q -nx
(gdb) source /path/to/gef/gef.py
[...]
gef➤ source /path/to/gef/scripts/remote_debug.py
gef➤ pi start_rpyc_service(4444)
```

Here RPyC will be started on the local host, and bound to the TCP port 4444. We can now connect
using a regular Python REPL:

```text
>>> import rpyc
>>> c = rpyc.connect("localhost", 4444)
>>> gdb = c.root._gdb
>>> gef = c.root._gef
# We can now fully control the remote GDB
>>> gdb.execute("file /bin/ls")
>>> gdb.execute("start")
>>> print(hex(gef.arch.pc))
0x55555555aab0
>>> print(hex(gef.arch.sp))
0x7fffffffdcf0
```

### Linting GEF

You can use the Makefile at the root of the project to get the proper linting settings. For most
Expand Down
Loading

0 comments on commit bcaabff

Please sign in to comment.