Stream of Consciousness

Mark Eschbach's random writings on various topics.

Sharing Hypothesis example database from Github Actions

Categories: programming

Tags: python hypothesis github-actions

Hypothesis is an awesome framework for producing fuzz testing. Investment in using the framework has definitely paid off with flapping tests showing logic errors. When running Hypothesis via Github Actions with Pytest directly you lose the database unless you upload it. Meaning you can reproduce the error only if you are lucky enough to generate the exact same example.

NOTE: The examples database is not always generated. Only when Hypothesis deems the examples complicated enough does the framework choose to generate this database. If you set CI to any value at all it will produce a long entry with something along the lines of the following. You should prefer this method this approach:

assert 128 <= 127 Falsifying example: test_broken( value=128, ) You can reproduce this example by temporarily adding @reproduce_failure(‘6.119.3’, b’AAAAgA==’) as a decorator on your test case

Ideally we publish the database as an Artifact of the test pipeline. This would allow an engineer to download the failing run and investigate the results.

Using the failure database

On a practical level this involves grabbing .hypothesis/examples and placing it in your local file system. As a best practice I would recommend .hypothesis/{branch-name}. From here you would annotate your failing test with something like the following:

from hypothesis import given, settings
from hypothesis.database import DirectoryBasedExampleDatabase

#
# Loads the failing example database
#
branch_name="branch-name"
failed_db = DirectoryBasedExampleDatabase(path=f".hypothesis/{branch_name}")

#
# Actual test which failed
#
@given(value=integers(min_value=0, max_value=128))
@settings(database=failed_db)
def test_value(value:int) -> None:
    assert value > 0
    assert value < (2 << 8) - 1

Ideally in the future I can find a mechanism to avoid having to manually create a database and just add to the local test cases Hypothesis knows about.

Publishing the Examples Database

Now the question of retrieving the database when the database fails. Uploading artifacts is the easy part and should look like the following. The tricky part is adding an if statement which runs correctly.

jobs:
  python_unit_tests:
    steps:
      - name: Run unit tests
        id: unit_tests # important so we may reference below
      - name: Publish Hypothesis Database on failure
        id: publish_hypothesis_database
        uses: actions/upload-artifact@v4
        if: "${{ failure() && steps.unit_tests.conclusion == 'failure' }}"
        with:
          name: hypothesis-database
          path: ".hypothesis/examples"

if and only if failure

By default, Github Actions will append success() to all steps. Meaning all prior steps in the job must succeed for the step to be run. To work around this, the if statement must include failure().

To reference the unit_test step result the filter steps.<id>.outcome is utilized. outcome is the result of the step, meaning failure when the step did not complete. While conclusion is overridden by the continue-on-error if outcome is failure.

Testing on Github Actions

For verification there needs to be a failing test. Easiest way is to set a variable for to flag for explicit failure. As a result unit_test step now has a export CI_BREAK_TESTS=yes prior to running pytest.

Using the skipif annotation makes this easy with some cheating on condition. When True the test is skipped which is a bit confusing. Either way the ideal implementation is getenv("CI_BREAK_TESTS", None) is None meaning we skip if the environment variable does not exist.

# cicd/broken_test.py
from os import getenv
from pytest import mark


@mark.skipif(
    condition=getenv("CI_BREAK_TESTS", None) is None,
    reason="CI_BREAK_TESTS environment variable not set; set to verify breaking tests produce desired behavior",
)
def test_broken():
    assert False, "intentional failure to ensure machinery works as expected"

Considered Alternative - GitHubArtifact

Ideal solution until each developer needs GITHUB_TOKEN. Effectively this pulls the examples directly from Github’s uploaded artifact.

https://hypothesis.readthedocs.io/en/latest/database.html#hypothesis.database.GitHubArtifactDatabase