diff --git a/.github/workflows/coverage.yaml b/.github/workflows/coverage.yaml new file mode 100644 index 0000000..b831d3c --- /dev/null +++ b/.github/workflows/coverage.yaml @@ -0,0 +1,51 @@ +# +# Copyright (C) 2019 - 2020 Tuono, Inc. +# All Rights Reserved +# +# On a pull request or push into master, this runs the unit tests +# and reports coverage. +# +--- +name: coverage +on: + pull_request: + push: + branches: + - master + +jobs: + coverage: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.7.7] + + steps: + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "${GITHUB_CONTEXT}" + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - uses: actions/checkout@v2 + + - name: Install Build Dependencies + run: python3 -m pip install -rrequirements/ci.txt + + - name: Enforce pre-commit + run: | + python3 -m pip install pre-commit + pre-commit install + pre-commit run -a + + - name: Build, Test, Coverage + run: make coverage + + - uses: codecov/codecov-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: ./coverage.xml diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..3b15ef6 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,57 @@ +# +# Copyright (C) 2019 - 2020 Tuono, Inc. +# All Rights Reserved +# +--- +name: release +on: + release: + types: + - published + +jobs: + publish-gemfury: + # publishes a pypi package to GemFury + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.7.7] + + steps: + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "${GITHUB_CONTEXT}" + + - uses: actions/checkout@v2 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - name: Install Build Dependencies + run: python3 -m pip install -rrequirements/ci.txt + + - name: Publish + id: publish + run: | + rm -rf dist/ + VERSION=$(python3 setup.py --version) + echo "::set-output name=version::$VERSION" + python3 setup.py sdist + PACKAGE=$(ls dist/*) + curl --fail -F package=@${PACKAGE} https://${{ secrets.GEMFURY_PUSH_TOKEN }}@push.fury.io/tuono/ + + - name: Show Version + run: echo "${{ steps.publish.outputs.version }}" + + - name: Notify + uses: homoluctus/slatify@v2.1.0 + if: always() + with: + type: ${{ job.status }} + job_name: ${{ github.repository }} publish ${{ steps.publish.outputs.version }} to gemfury + username: tubot + icon_emoji: ":monorail:" + url: ${{ secrets.SLACK_WEBHOOK_URL }} diff --git a/.yamllint b/.yamllint index 3197762..30d7f58 100644 --- a/.yamllint +++ b/.yamllint @@ -6,3 +6,4 @@ rules: max: 120 truthy: level: error + allowed-values: ['true', 'false', 'on'] diff --git a/README.md b/README.md index 146478c..12df6b0 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,91 @@ # interposer -FIXME: write content +[![Build Status](https://github.com/tuono/interposer/workflows/coverage/badge.svg)](https://github.com/tuono/interposer/actions?query=workflow%3Acoverage) +[![Release Status](https://github.com/tuono/interposer/workflows/release/badge.svg)](https://github.com/tuono/interposer/actions?query=workflow%3Arelease) +[![codecov](https://codecov.io/gh/tuono/interposer/branch/master/graph/badge.svg?token=HKUTULQQSA)](https://codecov.io/gh/tuono/interposer) +[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) + +This library lets you wrap any class or bare function and: + +1. Record all the method calls (parameters, return values, exceptions). +2. Playback all the method calls. +3. Inspect the method calls to ensure they meet certain criteria. + +## Record and Playback + +The recorder is useful where you are dealing with a third party +library and you would like to: + +- Occasionally ensure your code works live, +- Record detailed responses from third party libraries instead + of mocking them, +- Always ensure your code works. + +Recording has advantages and disadvantages, so the right solution +for your situation depends on many things. Recording eliminates +the need to produce and maintain mocks. Mocks of third party +libraries that change or are not well understood are fragile and +lead to a false sense of safety. Recordings on the other hand +are always correct, but they need to be regenerated when your +logic changes around the third party calls. + +## Call Inspection + +You may want to limit the types of methods that can be called in +third party libraries as an extra measure of protection in certain +runtime modes. Interposer lets you intercept every method called +in a wrapped class. + +## Usage + +1. Instantiate an Interposer with a datafile path, and set + the mode to Recording. +2. To wrap something, call wrap() and pass in the definition. +3. Use the returned wrapper as if it were the actual definition + that was wrapped. +4. Every use of the wrapped definition will record: + - The call name + - The parameters (positional and keyword) + - The return value, if no exception was raised + - The exception raised, should one be raised + +In most cases you want to leverage environment variables with +your own method that retrieves a class, for example if you want +to wrap the AWS boto3 library: + + import boto3 + import interposer + import os + + if os.get("RECORDING_FILE"): + global client_args + wrapper = interposer.Interposer(os.get("RECORDING_FILE"), interposer.Mode.Recording) + client = boto3.client("ec2", **client_args) + wrapped_client = wrapper.wrap(client) + return wrapped_client + +Now any method call on `client` will be recorded. To play back the same +stream with the same code, change the interposer mode to Playback and re-run +the code. Instead of calling boto3, the interposer will intercept and provide +the same return values or exceptions that boto3 provided during recording for +given combinations of method names and parameters. + +## Restrictions + +- Return values and Exceptions must be safe for pickling. Some + third party APIs use local definitions for exceptions, for example, + and local definitions cannot be pickled. If you get a pickling + error, you should subclass Interposer and provide your own + cleanup routine(s) as needed to substitute a class that can be + substituted for the local definition. + +## Notes + +- This is a resource, so you need to call open() and close() or + use the ScopedInterposer context manager. +- The class variables are ignored for purposes of hashing the + method calls into unique signatures (channel name + method name + + parameters). + +This documentation is not complete, for example pre and post cleanup +mechanisms are not documented, nor is the security check for call inspection.