diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..88a73b3 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,10 @@ +__pycache__ +.venv +.gdb_history +.DS_Store +.mypy_cache +.ruff_cache +gdbinit-gep +geprc.py +!example/gdbinit-gep +!example/geprc.py \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..f28cacb --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,28 @@ +name: Build and run GEP +on: [push, pull_request] + +jobs: + build: + strategy: + fail-fast: false + runs-on: ubuntu-22.04 + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + - name: Build + run: | + sudo apt-get update && sudo apt-get install -y git gdb python3 python3-pip python3-venv tmux + git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf + ~/.fzf/install --all + ./install.sh + + - name: Check GEP is running normally + run: | + gdb --version + export PATH=~/.fzf/bin:$PATH + tmux new-session -d -s check gdb -q + sleep 5 + OUTPUT=$(tmux capture-pane -p -t check) + echo $OUTPUT + grep -q "GEP is running" <(echo "$OUTPUT") + ! grep -q "Install fzf for better experience with GEP" <(echo "$OUTPUT") \ No newline at end of file diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index cbec143..c6f1efb 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -8,11 +8,12 @@ jobs: runs-on: ubuntu-22.04 timeout-minutes: 10 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install dependencies run: | - python3 -m pip install --user mypy ruff types-gdb prompt_toolkit==3.0.40 vermin sudo apt-get update && sudo apt-get install -y shellcheck shfmt + curl -sSL https://install.python-poetry.org | python3 - + poetry install --with dev - name: Run linters run: | diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..0e076d6 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,20 @@ +name: Test GEP on different platforms +on: [push, pull_request] + +jobs: + tests: + strategy: + fail-fast: false + matrix: + images: [ubuntu22.04] # TODO: Add more targets here + + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + + - name: Docker Build ${{ matrix.images }} + run: docker-compose build ${{ matrix.images }} + + - name: Test on ${{ matrix.images }} + run: docker-compose run ${{ matrix.images }} ./tests.sh diff --git a/.gitignore b/.gitignore index 03038ef..88a73b3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ -__pycache__/* -.venv/* +__pycache__ +.venv .gdb_history +.DS_Store +.mypy_cache +.ruff_cache gdbinit-gep geprc.py !example/gdbinit-gep diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e732cc5 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,28 @@ +ARG image +FROM $image + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && \ + apt-get install -y \ + python3 \ + python3-pip \ + python3-venv \ + tmux \ + git \ + gdb \ + curl \ + wget \ + vim && \ + curl -sSL https://install.python-poetry.org | python3 - && \ + git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf && \ + ~/.fzf/install --all + +ENV PATH="/root/.fzf/bin:/root/.local/bin:${PATH}" + +COPY . /root/.local/share/GEP + +WORKDIR /root/.local/share/GEP + +RUN ./install.sh -d && \ + poetry install --with dev diff --git a/README.md b/README.md index e2782d8..6f250c0 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ And also, GEP has some awesome features already, you can directly use it! ## How to install it? -Make sure you have GDB 8.0 or higher compiled with Python3.7+ bindings, then: +Make sure you have GDB 8.0 or higher compiled with Python3.8+ bindings, then: 1. Install git 2. Make sure you have [virtualenv](https://packaging.python.org/en/latest/guides/installing-using-pip-and-virtual-environments/#installing-virtualenv) installed diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..29c0f74 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,26 @@ +version: "3.8" + +services: + base: &base-spec + build: . + platform: linux/amd64 + security_opt: + - seccomp:unconfined + cap_add: + - SYS_PTRACE + + ubuntu22.04: + <<: *base-spec + build: + context: . + dockerfile: Dockerfile + args: + image: ubuntu:22.04 + + ubuntu20.04: + <<: *base-spec + build: + context: . + dockerfile: Dockerfile + args: + image: ubuntu:20.04 diff --git a/install.sh b/install.sh index 6dd64f6..45b1216 100755 --- a/install.sh +++ b/install.sh @@ -1,5 +1,32 @@ #!/bin/bash -set -ex + +set -o errexit + +help_and_exit() { + echo "Usage: $0 [-d|--dev]" + cat << EOF + -d, --dev install development dependencies +EOF + exit 1 +} + +if [[ $# -gt 1 ]]; then + help_and_exit +fi + +DEV=0 + +while [[ $# -gt 0 ]]; do + case $1 in + -d | --dev) + DEV=1 + shift + ;; + *) + help_and_exit + ;; + esac +done cd "$(dirname "${BASH_SOURCE[0]}")" GEP_BASE=$(pwd) @@ -18,9 +45,13 @@ VENV_PATH=$GEP_BASE/.venv echo "Creating virtualenv in path: ${VENV_PATH}" "$PYTHON" -m venv "$VENV_PATH" PYTHON=$VENV_PATH/bin/python -echo "Installing prompt_toolkit" +echo "Installing dependencies" "$PYTHON" -m pip install -U pip -"$VENV_PATH/bin/pip" install --no-cache-dir prompt_toolkit==3.0.40 +if [[ $DEV == 1 ]]; then + poetry install --with dev +else + "$VENV_PATH/bin/pip" install --no-cache-dir -e . +fi # copy example config to GEP_BASE if not exists echo "Copying default config to $GEP_BASE if not exists" diff --git a/lint.sh b/lint.sh index 5ca3a86..51aa911 100755 --- a/lint.sh +++ b/lint.sh @@ -4,7 +4,7 @@ set -o errexit help_and_exit() { - echo "Usage: ./lint.sh [-f|--filter]" + echo "Usage: $0 [-f|--filter]" echo " -f, --filter format code instead of just checking the format" exit 1 } @@ -29,9 +29,15 @@ done set -o xtrace -LINT_PYTHON_FILES=( +cd "$(dirname "${BASH_SOURCE[0]}")" +GEP_BASE=$(pwd) +# shellcheck disable=SC1091 +source "$GEP_BASE/.venv/bin/activate" + +VERMIN_TARGETS=( "gdbinit-gep.py" "example/geprc.py" + "tests/" ) LINT_SHELL_FILES=( "install.sh" @@ -46,7 +52,7 @@ else ruff format --diff fi -mypy "${LINT_PYTHON_FILES[@]}" +mypy if [[ $FIX == 1 ]]; then shfmt -i 4 -bn -ci -sr -w "${LINT_SHELL_FILES[@]}" @@ -56,4 +62,4 @@ fi shellcheck "${LINT_SHELL_FILES[@]}" -vermin -vvv --no-tips -q -t=3.7 --violations "${LINT_PYTHON_FILES[@]}" +vermin -vvv --no-tips -q -t=3.8- --violations "${VERMIN_TARGETS[@]}" diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..3c1849e --- /dev/null +++ b/poetry.lock @@ -0,0 +1,243 @@ +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.1" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"}, + {file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "mypy" +version = "1.10.0" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mypy-1.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da1cbf08fb3b851ab3b9523a884c232774008267b1f83371ace57f412fe308c2"}, + {file = "mypy-1.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:12b6bfc1b1a66095ab413160a6e520e1dc076a28f3e22f7fb25ba3b000b4ef99"}, + {file = "mypy-1.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e36fb078cce9904c7989b9693e41cb9711e0600139ce3970c6ef814b6ebc2b2"}, + {file = "mypy-1.10.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2b0695d605ddcd3eb2f736cd8b4e388288c21e7de85001e9f85df9187f2b50f9"}, + {file = "mypy-1.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:cd777b780312ddb135bceb9bc8722a73ec95e042f911cc279e2ec3c667076051"}, + {file = "mypy-1.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3be66771aa5c97602f382230165b856c231d1277c511c9a8dd058be4784472e1"}, + {file = "mypy-1.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8b2cbaca148d0754a54d44121b5825ae71868c7592a53b7292eeb0f3fdae95ee"}, + {file = "mypy-1.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ec404a7cbe9fc0e92cb0e67f55ce0c025014e26d33e54d9e506a0f2d07fe5de"}, + {file = "mypy-1.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e22e1527dc3d4aa94311d246b59e47f6455b8729f4968765ac1eacf9a4760bc7"}, + {file = "mypy-1.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:a87dbfa85971e8d59c9cc1fcf534efe664d8949e4c0b6b44e8ca548e746a8d53"}, + {file = "mypy-1.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a781f6ad4bab20eef8b65174a57e5203f4be627b46291f4589879bf4e257b97b"}, + {file = "mypy-1.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b808e12113505b97d9023b0b5e0c0705a90571c6feefc6f215c1df9381256e30"}, + {file = "mypy-1.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f55583b12156c399dce2df7d16f8a5095291354f1e839c252ec6c0611e86e2e"}, + {file = "mypy-1.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4cf18f9d0efa1b16478c4c129eabec36148032575391095f73cae2e722fcf9d5"}, + {file = "mypy-1.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:bc6ac273b23c6b82da3bb25f4136c4fd42665f17f2cd850771cb600bdd2ebeda"}, + {file = "mypy-1.10.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9fd50226364cd2737351c79807775136b0abe084433b55b2e29181a4c3c878c0"}, + {file = "mypy-1.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f90cff89eea89273727d8783fef5d4a934be2fdca11b47def50cf5d311aff727"}, + {file = "mypy-1.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fcfc70599efde5c67862a07a1aaf50e55bce629ace26bb19dc17cece5dd31ca4"}, + {file = "mypy-1.10.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:075cbf81f3e134eadaf247de187bd604748171d6b79736fa9b6c9685b4083061"}, + {file = "mypy-1.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:3f298531bca95ff615b6e9f2fc0333aae27fa48052903a0ac90215021cdcfa4f"}, + {file = "mypy-1.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fa7ef5244615a2523b56c034becde4e9e3f9b034854c93639adb667ec9ec2976"}, + {file = "mypy-1.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3236a4c8f535a0631f85f5fcdffba71c7feeef76a6002fcba7c1a8e57c8be1ec"}, + {file = "mypy-1.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a2b5cdbb5dd35aa08ea9114436e0d79aceb2f38e32c21684dcf8e24e1e92821"}, + {file = "mypy-1.10.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:92f93b21c0fe73dc00abf91022234c79d793318b8a96faac147cd579c1671746"}, + {file = "mypy-1.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:28d0e038361b45f099cc086d9dd99c15ff14d0188f44ac883010e172ce86c38a"}, + {file = "mypy-1.10.0-py3-none-any.whl", hash = "sha256:f8c083976eb530019175aabadb60921e73b4f45736760826aa1689dda8208aee"}, + {file = "mypy-1.10.0.tar.gz", hash = "sha256:3d087fcbec056c4ee34974da493a826ce316947485cef3901f511848e687c131"}, +] + +[package.dependencies] +mypy-extensions = ">=1.0.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = ">=4.1.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "packaging" +version = "24.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, + {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "prompt-toolkit" +version = "3.0.43" +description = "Library for building powerful interactive command lines in Python" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "prompt_toolkit-3.0.43-py3-none-any.whl", hash = "sha256:a11a29cb3bf0a28a387fe5122cdb649816a957cd9261dcedf8c9f1fef33eacf6"}, + {file = "prompt_toolkit-3.0.43.tar.gz", hash = "sha256:3527b7af26106cbc65a040bcc84839a3566ec1b051bb0bfe953631e704b0ff7d"}, +] + +[package.dependencies] +wcwidth = "*" + +[[package]] +name = "pytest" +version = "8.2.0" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-8.2.0-py3-none-any.whl", hash = "sha256:1733f0620f6cda4095bbf0d9ff8022486e91892245bb9e7d5542c018f612f233"}, + {file = "pytest-8.2.0.tar.gz", hash = "sha256:d507d4482197eac0ba2bae2e9babf0672eb333017bcedaa5fb1a3d42c1174b3f"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.5,<2.0" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "ruff" +version = "0.4.4" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.4.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:29d44ef5bb6a08e235c8249294fa8d431adc1426bfda99ed493119e6f9ea1bf6"}, + {file = "ruff-0.4.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c4efe62b5bbb24178c950732ddd40712b878a9b96b1d02b0ff0b08a090cbd891"}, + {file = "ruff-0.4.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c8e2f1e8fc12d07ab521a9005d68a969e167b589cbcaee354cb61e9d9de9c15"}, + {file = "ruff-0.4.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:60ed88b636a463214905c002fa3eaab19795679ed55529f91e488db3fe8976ab"}, + {file = "ruff-0.4.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b90fc5e170fc71c712cc4d9ab0e24ea505c6a9e4ebf346787a67e691dfb72e85"}, + {file = "ruff-0.4.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:8e7e6ebc10ef16dcdc77fd5557ee60647512b400e4a60bdc4849468f076f6eef"}, + {file = "ruff-0.4.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9ddb2c494fb79fc208cd15ffe08f32b7682519e067413dbaf5f4b01a6087bcd"}, + {file = "ruff-0.4.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c51c928a14f9f0a871082603e25a1588059b7e08a920f2f9fa7157b5bf08cfe9"}, + {file = "ruff-0.4.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5eb0a4bfd6400b7d07c09a7725e1a98c3b838be557fee229ac0f84d9aa49c36"}, + {file = "ruff-0.4.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b1867ee9bf3acc21778dcb293db504692eda5f7a11a6e6cc40890182a9f9e595"}, + {file = "ruff-0.4.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1aecced1269481ef2894cc495647392a34b0bf3e28ff53ed95a385b13aa45768"}, + {file = "ruff-0.4.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9da73eb616b3241a307b837f32756dc20a0b07e2bcb694fec73699c93d04a69e"}, + {file = "ruff-0.4.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:958b4ea5589706a81065e2a776237de2ecc3e763342e5cc8e02a4a4d8a5e6f95"}, + {file = "ruff-0.4.4-py3-none-win32.whl", hash = "sha256:cb53473849f011bca6e754f2cdf47cafc9c4f4ff4570003a0dad0b9b6890e876"}, + {file = "ruff-0.4.4-py3-none-win_amd64.whl", hash = "sha256:424e5b72597482543b684c11def82669cc6b395aa8cc69acc1858b5ef3e5daae"}, + {file = "ruff-0.4.4-py3-none-win_arm64.whl", hash = "sha256:39df0537b47d3b597293edbb95baf54ff5b49589eb7ff41926d8243caa995ea6"}, + {file = "ruff-0.4.4.tar.gz", hash = "sha256:f87ea42d5cdebdc6a69761a9d0bc83ae9b3b30d0ad78952005ba6568d6c022af"}, +] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[[package]] +name = "types-gdb" +version = "12.1.4.20240408" +description = "Typing stubs for gdb" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types-gdb-12.1.4.20240408.tar.gz", hash = "sha256:f2136ccf15ab74b5b2702b88b3c38a41029a89c12d9c12851c6a0b9789047d83"}, + {file = "types_gdb-12.1.4.20240408-py3-none-any.whl", hash = "sha256:b5fc44c1929cc5eb071792d5f66b5f74e880589683d065651bf9464d8f41ad75"}, +] + +[[package]] +name = "typing-extensions" +version = "4.11.0" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"}, + {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"}, +] + +[[package]] +name = "vermin" +version = "1.6.0" +description = "Concurrently detect the minimum Python versions needed to run code" +optional = false +python-versions = ">=3.0" +files = [ + {file = "vermin-1.6.0-py2.py3-none-any.whl", hash = "sha256:f1fa9ee40f59983dc40e0477eb2b1fa8061a3df4c3b2bcf349add462a5610efb"}, + {file = "vermin-1.6.0.tar.gz", hash = "sha256:6266ca02f55d1c2aa189a610017c132eb2d1934f09e72a955b1eb3820ee6d4ef"}, +] + +[[package]] +name = "wcwidth" +version = "0.2.13" +description = "Measures the displayed width of unicode strings in a terminal" +optional = false +python-versions = "*" +files = [ + {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, + {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, +] + +[metadata] +lock-version = "2.0" +python-versions = "^3.8" +content-hash = "f86097a1bd9fbafe1a48171386f1dc8ddaeca02e36a7db9d324331e63ac0d2c1" diff --git a/poetry.toml b/poetry.toml new file mode 100644 index 0000000..a7b197a --- /dev/null +++ b/poetry.toml @@ -0,0 +1,5 @@ +[virtualenvs] +in-project = true + +[keyring] +enabled = false \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 94721f6..8f17f03 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,34 @@ +[tool.poetry] +name = "GEP" +version = "2024.05" +description = "GDB Enhanced Prompt" +authors = ["Alan Li <61896187+lebr0nli@users.noreply.github.com>"] +readme = "README.md" +packages = [ + { include = "gdbinit-gep.py" }, +] + +[tool.poetry.dependencies] +python = "^3.8" +prompt-toolkit = "^3.0.43" + +[tool.poetry.group.dev] +optional = true + +[tool.poetry.group.dev.dependencies] +ruff = "^0.4.4" +mypy = "^1.10.0" +types-gdb = "^12.1.4.20240408" +vermin = "^1.6.0" +pytest = "8.2.0" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" + [tool.ruff] line-length = 100 -target-version = "py37" +target-version = "py38" [tool.ruff.lint] select = [ @@ -28,6 +56,7 @@ known-third-party = ["gdb", "prompt_toolkit"] force-single-line = true [tool.mypy] +files = ["gdbinit-gep.py", "example/geprc.py"] strict_optional = false check_untyped_defs = true allow_redefinition = true diff --git a/tests.sh b/tests.sh new file mode 100755 index 0000000..b505f64 --- /dev/null +++ b/tests.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +set -o errexit + +help_and_exit() { + echo "Usage: $0" + exit 1 +} + +if [[ $# -gt 0 ]]; then + help_and_exit +fi + +set -o xtrace + +cd "$(dirname "${BASH_SOURCE[0]}")" +GEP_BASE=$(pwd) +# shellcheck disable=SC1091 +source "$GEP_BASE/.venv/bin/activate" +pytest -v tests/ diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..f4c8fbd --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,174 @@ +from __future__ import annotations + +import functools +import os +import subprocess +import tempfile +import time +import typing as T + +import pytest + +SESSION_STARTUP_TIMEOUT = 15 +STARTUP_BANNER = b"GEP is running now!" +GDB_HISTORY_NAME = ".gdb_history" + + +def run_with_screen_256color( + cmd: list[str], capture_output: bool = False +) -> subprocess.CompletedProcess: + """ + Run a command with `TERM=screen-256color`. + + :param list[str] cmd: The command to run. + :param bool capture_output: Whether to capture the output. + :return: The result of the command. + :rtype: subprocess.CompletedProcess + """ + env = os.environ.copy() + env["TERM"] = "screen-256color" + return subprocess.run(cmd, capture_output=capture_output, env=env) + + +class GDBSession: + def __init__(self) -> None: + """ + Initialize the GDB session. + """ + self.tmpdir = tempfile.TemporaryDirectory() + + self.session_name = None + self.__session_started = False + + def start(self, gdb_args: list[str] | None = None, histories: list[str] = None) -> None: + """ + Start the GDB session. + + :param list[str] gdb_args: The arguments to pass to GDB. + :param list[str] histories: The histories to load into GDB. + :return: None + """ + if histories: + with open(os.path.join(self.tmpdir.name, GDB_HISTORY_NAME), "w") as f: + f.write("\n".join(histories)) + + cmd = [ + "tmux", + "new-session", + "-f", + os.devnull, + "-d", + "-P", + "-F", + "#{session_name}", + "-c", + self.tmpdir.name, + "gdb", + "-q", + ] + if gdb_args: + cmd.extend(gdb_args) + + self.session_name = ( + run_with_screen_256color(cmd, capture_output=True).stdout.decode().strip() + ) + self.__session_started = True + + # wait `STARTUP_BANNER` appears in pane + now = time.time() + while time.time() - now < SESSION_STARTUP_TIMEOUT: + if STARTUP_BANNER in self.capture_pane(): + break + time.sleep(1) + else: + raise TimeoutError("GDB session did not start in time") + self.clear_pane() + + def stop(self) -> None: + """ + Remove the temporary directory and stop the GDB session. + + :return: None + """ + self.tmpdir.cleanup() + if self.__session_started: + run_with_screen_256color(["tmux", "kill-session", "-t", self.session_name]) + self.__session_started = False + + def check_session_started(func: T.Callable) -> T.Callable: + """ + Check if the GDB session is started before calling the decorated function. + + :param Callable func: The function to decorate. + :return: The decorated function. + :rtype: Callable + :raises RuntimeError: If the GDB session is not started. + """ + + @functools.wraps(func) + def wrapper(self: GDBSession, *args, **kwargs): + if not self.__session_started: + raise RuntimeError("GDB session is not started") + return func(self, *args, **kwargs) + + return wrapper + + @check_session_started + def send_literal(self, literal: str) -> None: + """ + Send a literal string to the GDB session. + + :param str literal: The literal string to send to the GDB session. + :return: None + """ + run_with_screen_256color(["tmux", "send-keys", "-l", "-t", self.session_name, literal]) + time.sleep(1) + + @check_session_started + def send_key(self, key: str) -> None: + """ + Send a key to the GDB session. + + :param str key: The key to send to the GDB session. + :return: None + """ + run_with_screen_256color(["tmux", "send-keys", "-t", self.session_name, key]) + time.sleep(1) + + @check_session_started + def clear_pane(self) -> None: + """ + Clear the screen of the pane. + + :return: None + :rtype: None + """ + run_with_screen_256color(["tmux", "send-keys", "-t", self.session_name, "C-l"]) + time.sleep(1) + run_with_screen_256color(["tmux", "clear-history", "-t", self.session_name]) + + @check_session_started + def capture_pane(self, with_color: bool = False) -> bytes: + """ + Capture the content of the pane. + + :param bool with_color: Whether to capture the content with color. + :return: The content of the pane. + :rtype: bytes + """ + cmd = ["tmux", "capture-pane", "-p", "-t", self.session_name] + if with_color: + cmd.append("-e") + return run_with_screen_256color(cmd, capture_output=True).stdout.rstrip(b"\n") + + def __enter__(self) -> GDBSession: + return self + + def __exit__(self, exc_type, exc_value, traceback) -> None: + self.stop() + + +@pytest.fixture +def gdb_session() -> T.Iterator[GDBSession]: + with GDBSession() as session: + yield session diff --git a/tests/test_gep.py b/tests/test_gep.py new file mode 100644 index 0000000..76d5a17 --- /dev/null +++ b/tests/test_gep.py @@ -0,0 +1,84 @@ +from conftest import GDBSession + + +def grey(b: bytes) -> bytes: + """ + Return the grey version of the bytes. + + :param bytes b: The bytes to make grey. + :return: The grey version of the bytes. + :rtype: bytes + """ + return b"\x1b[38;5;241m" + b + b"\x1b[39m" + + +def test_autosuggestion(gdb_session: GDBSession) -> None: + gdb_session.start(histories=["print 12", "print 34"]) + + # the autosuggestion should not be shown when no match + gdb_session.send_literal("print x") + assert b"(gdb) print x" == gdb_session.capture_pane(with_color=True) + + # make buffer to "print " + gdb_session.send_key("BSpace") + gdb_session.send_key("BSpace") + gdb_session.send_literal(" ") + # match "print 34" + assert b"(gdb) print " + grey(b"34") == gdb_session.capture_pane(with_color=True) + # accept the suggestion + gdb_session.send_key("Right") + assert b"(gdb) print 34" == gdb_session.capture_pane(with_color=True) + + # make buffer to "print 1" + gdb_session.send_key("BSpace") + gdb_session.send_key("BSpace") + gdb_session.send_literal("1") + # match "print 2" + assert b"(gdb) print 1" + grey(b"2") == gdb_session.capture_pane(with_color=True) + # accept the suggestion + gdb_session.send_key("Right") + assert b"(gdb) print 12" == gdb_session.capture_pane(with_color=True) + + +def test_fzf_history_search(gdb_session: GDBSession) -> None: + gdb_session.start(histories=["print 10", "print 11", "print 20"]) + + # search with empty buffer + gdb_session.send_key("C-r") + pane_content = gdb_session.capture_pane() + assert b"3/3" in pane_content + b"""\ +> print 20 + print 11 + print 10""" in pane_content + + # search "11" in buffer + gdb_session.send_literal("11") + pane_content = gdb_session.capture_pane() + assert b"> 11" in pane_content + assert b"1/3" in pane_content + assert b"> print 11" in pane_content + + # the selected history should be replaced in buffer + gdb_session.send_key("Enter") + assert b"(gdb) print 11" == gdb_session.capture_pane() + + # clear the buffer + gdb_session.send_key("C-u") + assert b"(gdb)" == gdb_session.capture_pane() + + # search with "print " in buffer + gdb_session.send_literal("print ") + original_pane = gdb_session.capture_pane( + with_color=True + ) # use color to make sure it is exactly the same + gdb_session.send_key("C-r") + assert b"3/3" in gdb_session.capture_pane() + + # put some garbage in buffer + gdb_session.send_literal("garbage") + assert b"0/3" in gdb_session.capture_pane() + + # check if we cancel the search, it will restore the buffer + gdb_session.send_key("C-c") + assert original_pane == gdb_session.capture_pane(with_color=True)