Skip to content
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 +LINUX and +WINDOWS doctest options #2507

Merged
merged 11 commits into from
Jan 21, 2025
15 changes: 15 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -284,10 +284,25 @@ jobs:
pip install --upgrade pip
pip install --upgrade --editable .

- name: Install documentation dependencies
run: pip install -r docs/requirements.txt

- name: Sanity checks
run: |
python -bb -c 'from pwn import *'
python -bb examples/text.py

- name: Coverage doctests
run: |
python -bb -m coverage run -m sphinx -b doctest docs/source docs/build/doctest

# FIXME: Paths are broken when uploading coverage on ubuntu
# coverage.exceptions.NoSource: No source for code: '/home/runner/work/pwntools/pwntools/D:\a\pwntools\pwntools\pwn\__init__.py'.
# - uses: actions/upload-artifact@v4
# with:
# name: coverage-windows
# path: .coverage*
# include-hidden-files: true

upload-coverage:
runs-on: ubuntu-latest
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ The table below shows which release corresponds to each branch, and what date th

## 5.0.0 (`dev`)

- [#2507][2507] Add `+LINUX` and `+WINDOWS` doctest options and start proper testing on Windows

[2507]: https://github.com/Gallopsled/pwntools/pull/2507

## 4.15.0 (`beta`)
- [#2508][2508] Ignore a warning when compiling with asm on nix
Expand Down
2 changes: 1 addition & 1 deletion docs/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,6 @@ psutil
requests>=2.5.1
ropgadget>=5.3
sphinx==1.8.6; python_version<'3'
sphinx>=7.0.0; python_version>='3'
sphinx>=8.1.3, <9; python_version>='3'
sphinx_rtd_theme
sphinxcontrib-autoprogram<=0.1.5
3 changes: 3 additions & 0 deletions docs/source/adb.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
from pwn import *
adb = pwnlib.adb

import doctest
doctest_additional_flags = doctest.OPTIONFLAGS_BY_NAME['LINUX']

:mod:`pwnlib.adb` --- Android Debug Bridge
=====================================================

Expand Down
4 changes: 4 additions & 0 deletions docs/source/asm.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
import subprocess
from pwn import *

# TODO: Remove global POSIX flag
import doctest
doctest_additional_flags = doctest.OPTIONFLAGS_BY_NAME['POSIX']

:mod:`pwnlib.asm` --- Assembler functions
=========================================

Expand Down
84 changes: 76 additions & 8 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,15 @@ def dont_skip_any_doctests(app, what, name, obj, skip, options):

class _DummyClass(object): pass

# doctest optionflags for platform-specific tests
# they are skipped on other platforms
WINDOWS = doctest.register_optionflag('WINDOWS')
LINUX = doctest.register_optionflag('LINUX')
POSIX = doctest.register_optionflag('POSIX')

# doctest optionflag for tests that haven't been looked at yet
TODO = doctest.register_optionflag('TODO')

class Py2OutputChecker(_DummyClass, doctest.OutputChecker):
def check_output(self, want, got, optionflags):
sup = super(Py2OutputChecker, self).check_output
Expand Down Expand Up @@ -425,27 +434,86 @@ def check_output(self, want, got, optionflags):
return False
return True

import sphinx.ext.doctest

class PlatformDocTestRunner(sphinx.ext.doctest.SphinxDocTestRunner):
def run(self, test, compileflags=None, out=None, clear_globs=True):
original_optionflags = self.optionflags | test.globs.get('doctest_additional_flags', 0)
def filter_platform(example):
optionflags = original_optionflags
if example.options:
for (optionflag, val) in example.options.items():
if val:
optionflags |= optionflag
else:
optionflags &= ~optionflag

if (optionflags & WINDOWS) == WINDOWS and sys.platform != 'win32':
return False
if (optionflags & LINUX) == LINUX and sys.platform != 'linux':
peace-maker marked this conversation as resolved.
Show resolved Hide resolved
return False
if (optionflags & POSIX) == POSIX and os.name != 'posix':
return False
return True

test.examples[:] = [example for example in test.examples if filter_platform(example)]

return super(PlatformDocTestRunner, self).run(test, compileflags, out, clear_globs)

class PlatformDocTestBuilder(sphinx.ext.doctest.DocTestBuilder):
_test_runner = None

@property
def test_runner(self):
return self._test_runner

@test_runner.setter
def test_runner(self, value):
self._test_runner = PlatformDocTestRunner(value._checker, value._verbose, value.optionflags)

def py2_doctest_init(self, checker=None, verbose=None, optionflags=0):
if checker is None:
checker = Py2OutputChecker()
doctest.DocTestRunner.__init__(self, checker, verbose, optionflags)

if 'doctest' in sys.argv:
def setup(app):
pass # app.connect('autodoc-skip-member', dont_skip_any_doctests)

if sys.version_info[:1] < (3,):
import sphinx.ext.doctest
sphinx.ext.doctest.SphinxDocTestRunner.__init__ = py2_doctest_init
else:
def setup(app):
app.add_builder(PlatformDocTestBuilder, override=True)
# app.connect('autodoc-skip-member', dont_skip_any_doctests)
# monkey patching paramiko due to https://github.com/paramiko/paramiko/pull/1661
import paramiko.client
import binascii
paramiko.client.hexlify = lambda x: binascii.hexlify(x).decode()
paramiko.util.safe_string = lambda x: '' # function result never *actually used*
class EndlessLoop(Exception): pass
def alrm_handler(sig, frame):
signal.alarm(180) # three minutes
raise EndlessLoop()
signal.signal(signal.SIGALRM, alrm_handler)
signal.alarm(600) # ten minutes
if hasattr(signal, 'alarm'):
def alrm_handler(sig, frame):
signal.alarm(180) # three minutes
raise EndlessLoop()
signal.signal(signal.SIGALRM, alrm_handler)
signal.alarm(600) # ten minutes
else:
def sigabrt_handler(signum, frame):
raise EndlessLoop()
# thread.interrupt_main received the signum parameter in Python 3.10
if sys.version_info >= (3, 10):
signal.signal(signal.SIGABRT, sigabrt_handler)
def alrm_handler():
try:
import thread
except ImportError:
import _thread as thread
# pre Python 3.10 this raises a KeyboardInterrupt in the main thread.
# it might not show a traceback in that case, but it will stop the endless loop.
thread.interrupt_main(signal.SIGABRT)
timer = threading.Timer(interval=180, function=alrm_handler) # three minutes
timer.daemon = True
timer.start()
import threading
timer = threading.Timer(interval=600, function=alrm_handler) # ten minutes
timer.daemon = True
timer.start()
3 changes: 3 additions & 0 deletions docs/source/elf/corefile.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
# Set the environment here so it's not in the middle of our tests.
os.environ.setdefault('SHELL', '/bin/sh')

import doctest
doctest_additional_flags = doctest.OPTIONFLAGS_BY_NAME['POSIX']


:mod:`pwnlib.elf.corefile` --- Core Files
===========================================================
Expand Down
4 changes: 4 additions & 0 deletions docs/source/elf/elf.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
from pwnlib.elf.maps import CAT_PROC_MAPS_EXIT
import shutil

# TODO: Remove global POSIX flag
import doctest
doctest_additional_flags = doctest.OPTIONFLAGS_BY_NAME['POSIX']

:mod:`pwnlib.elf.elf` --- ELF Files
===========================================================

Expand Down
4 changes: 4 additions & 0 deletions docs/source/encoders.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
.. testsetup:: *

from pwn import *

# TODO: Remove global POSIX flag
import doctest
doctest_additional_flags = doctest.OPTIONFLAGS_BY_NAME['POSIX']

:mod:`pwnlib.encoders` --- Encoding Shellcode
===============================================
Expand Down
4 changes: 4 additions & 0 deletions docs/source/filesystem.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
from pwnlib.tubes.ssh import ssh
from pwnlib.filesystem import *

# TODO: Remove global POSIX flag
import doctest
doctest_additional_flags = doctest.OPTIONFLAGS_BY_NAME['POSIX']

:mod:`pwnlib.filesystem` --- Manipulating Files Locally and Over SSH
====================================================================

Expand Down
4 changes: 4 additions & 0 deletions docs/source/gdb.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
context.arch = 'amd64'
context.terminal = [os.path.join(os.path.dirname(pwnlib.__file__), 'gdb_faketerminal.py')]

# TODO: Test on cygwin too
import doctest
doctest_additional_flags = doctest.OPTIONFLAGS_BY_NAME['POSIX']

:mod:`pwnlib.gdb` --- Working with GDB
======================================

Expand Down
3 changes: 3 additions & 0 deletions docs/source/intro.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

from pwn import *

import doctest
doctest_additional_flags = doctest.OPTIONFLAGS_BY_NAME['POSIX']

Getting Started
========================

Expand Down
4 changes: 4 additions & 0 deletions docs/source/libcdb.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
from pwn import *
from pwnlib.libcdb import *

# TODO: Remove global POSIX flag
import doctest
doctest_additional_flags = doctest.OPTIONFLAGS_BY_NAME['POSIX']

:mod:`pwnlib.libcdb` --- Libc Database
===========================================

Expand Down
4 changes: 4 additions & 0 deletions docs/source/qemu.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

from pwn import *

# TODO: Remove global POSIX flag
import doctest
doctest_additional_flags = doctest.OPTIONFLAGS_BY_NAME['POSIX']


:mod:`pwnlib.qemu` --- QEMU Utilities
==========================================
Expand Down
4 changes: 4 additions & 0 deletions docs/source/rop/rop.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@

context.clear()

# TODO: Remove global LINUX flag
import doctest
doctest_additional_flags = doctest.OPTIONFLAGS_BY_NAME['LINUX']


:mod:`pwnlib.rop.rop` --- Return Oriented Programming
==========================================================
Expand Down
3 changes: 3 additions & 0 deletions docs/source/rop/srop.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
from pwnlib.elf import ELF
from pwnlib.tubes.process import process

import doctest
doctest_additional_flags = doctest.OPTIONFLAGS_BY_NAME['LINUX']

:mod:`pwnlib.rop.srop` --- Sigreturn Oriented Programming
==========================================================

Expand Down
4 changes: 4 additions & 0 deletions docs/source/runner.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
from pwnlib.runner import *
from pwnlib.asm import asm

# TODO: Remove global POSIX flag
import doctest
doctest_additional_flags = doctest.OPTIONFLAGS_BY_NAME['POSIX']

:mod:`pwnlib.runner` --- Running Shellcode
===========================================

Expand Down
4 changes: 4 additions & 0 deletions docs/source/shellcraft.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

from pwnlib import shellcraft

# TODO: Remove global POSIX flag
import doctest
doctest_additional_flags = doctest.OPTIONFLAGS_BY_NAME['POSIX']

:mod:`pwnlib.shellcraft` --- Shellcode generation
=================================================

Expand Down
3 changes: 3 additions & 0 deletions docs/source/shellcraft/aarch64.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
from pwn import *
context.clear(arch='aarch64')

import doctest
doctest_additional_flags = doctest.OPTIONFLAGS_BY_NAME['LINUX']

:mod:`pwnlib.shellcraft.aarch64` --- Shellcode for AArch64
===========================================================

Expand Down
4 changes: 4 additions & 0 deletions docs/source/shellcraft/amd64.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
from pwn import *
context.clear(arch='amd64')

# TODO: POSIX/WINDOWS shellcode test
import doctest
doctest_additional_flags = doctest.OPTIONFLAGS_BY_NAME['LINUX']

:mod:`pwnlib.shellcraft.amd64` --- Shellcode for AMD64
===========================================================

Expand Down
3 changes: 3 additions & 0 deletions docs/source/shellcraft/arm.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
from pwn import *
context.clear(arch='arm')

import doctest
doctest_additional_flags = doctest.OPTIONFLAGS_BY_NAME['LINUX']

:mod:`pwnlib.shellcraft.arm` --- Shellcode for ARM
===========================================================

Expand Down
3 changes: 3 additions & 0 deletions docs/source/shellcraft/i386.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
from pwn import *
context.clear(arch='i386')

import doctest
doctest_additional_flags = doctest.OPTIONFLAGS_BY_NAME['POSIX']

:mod:`pwnlib.shellcraft.i386` --- Shellcode for Intel 80386
===========================================================

Expand Down
3 changes: 3 additions & 0 deletions docs/source/shellcraft/mips.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@

context.clear(arch='mips')

import doctest
doctest_additional_flags = doctest.OPTIONFLAGS_BY_NAME['LINUX']

:mod:`pwnlib.shellcraft.mips` --- Shellcode for MIPS
===========================================================

Expand Down
3 changes: 3 additions & 0 deletions docs/source/shellcraft/riscv64.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
from pwn import *
context.clear(arch='riscv64')

import doctest
doctest_additional_flags = doctest.OPTIONFLAGS_BY_NAME['LINUX']

:mod:`pwnlib.shellcraft.riscv64` --- Shellcode for RISCV64
==========================================================

Expand Down
3 changes: 3 additions & 0 deletions docs/source/shellcraft/thumb.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
from pwn import *
context.clear(arch='thumb')

import doctest
doctest_additional_flags = doctest.OPTIONFLAGS_BY_NAME['LINUX']

:mod:`pwnlib.shellcraft.thumb` --- Shellcode for Thumb Mode
===========================================================

Expand Down
4 changes: 4 additions & 0 deletions docs/source/tubes/processes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

from pwn import *

# TODO: Remove global POSIX flag
import doctest
doctest_additional_flags = doctest.OPTIONFLAGS_BY_NAME['POSIX']

:mod:`pwnlib.tubes.process` --- Processes
===========================================================

Expand Down
4 changes: 4 additions & 0 deletions docs/source/tubes/serial.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

from pwn import *

# TODO: Remove global POSIX flag
import doctest
doctest_additional_flags = doctest.OPTIONFLAGS_BY_NAME['POSIX']

:mod:`pwnlib.tubes.serialtube` --- Serial Ports
===========================================================

Expand Down
Loading
Loading