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

Open
wants to merge 9 commits into
base: dev
Choose a base branch
from
Open
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
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
3 changes: 3 additions & 0 deletions docs/source/asm.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
import subprocess
from pwn import *

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

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

Expand Down
64 changes: 56 additions & 8 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,11 @@ 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')

class Py2OutputChecker(_DummyClass, doctest.OutputChecker):
def check_output(self, want, got, optionflags):
sup = super(Py2OutputChecker, self).check_output
Expand Down Expand Up @@ -425,27 +430,70 @@ 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
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 sys.platform == 'win32':
def alrm_handler():
raise EndlessLoop()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it really interrupt hanging tests? If so, this should be the default way for all platforms.

Copy link
Member Author

@peace-maker peace-maker Dec 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, I'll actually try this with a shorter timeout. I was blindly trusting stack overflow like one should do.

Do you know why the alarm signal is reset to 180 in the signal handler?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The following timeout is smaller so that as many hanging tests are discovered as possible in a single Sphinx batch run (and to save CI minutes on hangs, as a byproduct). This is just a debugging measure that happens to be useful more often than it should, so I guess we can skip it altogether if there is no platform support. But would be nice to have regardless.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I read up on timeouts and signals in Python. I think I came up with a platform independent way which behaves the same as the signal.alarm implementation does right now.

import threading
timer = threading.Timer(600, alrm_handler)
timer.daemon = True
timer.start()
else:
def alrm_handler(sig, frame):
signal.alarm(180) # three minutes
raise EndlessLoop()
signal.signal(signal.SIGALRM, alrm_handler)
signal.alarm(600) # ten minutes
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['LINUX']


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

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

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

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

from pwn import *

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

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

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

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

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

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

: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['LINUX']

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

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

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

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

Expand Down
3 changes: 3 additions & 0 deletions docs/source/qemu.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['LINUX']


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

context.clear()

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
3 changes: 3 additions & 0 deletions docs/source/runner.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
from pwnlib.runner import *
from pwnlib.asm import asm

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

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

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

from pwnlib import shellcraft

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

: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
3 changes: 3 additions & 0 deletions docs/source/shellcraft/amd64.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
from pwn import *
context.clear(arch='amd64')

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['LINUX']

: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
3 changes: 3 additions & 0 deletions docs/source/tubes.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['LINUX']

:mod:`pwnlib.tubes` --- Talking to the World!
=============================================

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

from pwnlib.tubes.buffer import *

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

:mod:`pwnlib.tubes.buffer` --- buffer implementation for tubes
==============================================================

Expand Down
3 changes: 3 additions & 0 deletions docs/source/tubes/processes.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['LINUX']

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

Expand Down
3 changes: 3 additions & 0 deletions docs/source/tubes/serial.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['LINUX']

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

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

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

:mod:`pwnlib.tubes.sock` --- Sockets
===========================================================

Expand Down
3 changes: 3 additions & 0 deletions docs/source/tubes/ssh.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['LINUX']

:mod:`pwnlib.tubes.ssh` --- SSH
===========================================================

Expand Down
Loading
Loading