├── .github ├── cloudformation │ ├── README.md │ ├── oidc.yaml │ └── token.yaml ├── dependabot.yml └── workflows │ ├── ctags.yaml │ ├── publish.yaml │ ├── release-brew.yaml │ ├── release-pypi.yaml │ ├── release.yaml │ └── run-differential-tests.yaml ├── .gitignore ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── NOTICE ├── README.md ├── docs ├── .gitignore ├── Makefile ├── README.md ├── book.toml └── src │ ├── README.md │ └── SUMMARY.md ├── pyproject.toml ├── setup.cfg ├── src └── cbmc_viewer │ ├── MANIFEST.in │ ├── __init__.py │ ├── cbmc_viewer.py │ ├── configt.py │ ├── coveraget.py │ ├── ctagst.py │ ├── etc │ └── bash_completion.d │ │ └── cbmc-viewer.sh │ ├── filet.py │ ├── loopt.py │ ├── markup_code.py │ ├── markup_link.py │ ├── markup_summary.py │ ├── markup_trace.py │ ├── optionst.py │ ├── parse.py │ ├── propertyt.py │ ├── reachablet.py │ ├── report.py │ ├── resultt.py │ ├── runt.py │ ├── sourcet.py │ ├── srcloct.py │ ├── symbol_table.py │ ├── symbolt.py │ ├── templates.py │ ├── templates │ ├── code.jinja.html │ ├── link.jinja.html │ ├── summary.jinja.html │ └── trace.jinja.html │ ├── tracet.py │ ├── util.py │ ├── version.py │ ├── viewer.css │ ├── viewer.js │ └── viewer.py ├── summary ├── Makefile ├── README.md ├── projects.json ├── stubs.py └── summary.py └── tests ├── bin ├── Makefile ├── arguments.py ├── build-viewer-reports ├── compare-result ├── compare-source ├── compare-trace ├── compare-viewer-reports ├── difference.md └── difference.py ├── coreHTTP ├── .gitignore ├── Makefile ├── README.md └── bug.patch ├── kani ├── .gitignore ├── Makefile └── write_build_ninja.py ├── repo-tests └── coreHTTP │ ├── .gitignore │ ├── Makefile │ └── bug.patch └── unit-tests ├── ctags ├── .gitignore └── Makefile └── visible-steps ├── Makefile ├── README.md ├── main.c └── viewer-trace.json /.github/cloudformation/README.md: -------------------------------------------------------------------------------- 1 | These are 2 | [AWS CloudFormation templates](https://aws.amazon.com/cloudformation/resources/templates/) 3 | for maintaining pypi and homebrew credentials used to publish the CBMC viewer. 4 | -------------------------------------------------------------------------------- /.github/cloudformation/oidc.yaml: -------------------------------------------------------------------------------- 1 | Description: 2 | Register the GitHub identity provider with the AWS security token service. 3 | 4 | Resources: 5 | GithubIdentityProvider: 6 | Type: AWS::IAM::OIDCProvider 7 | Properties: 8 | Url: 9 | # The GitHub identity provider supporting OIDC 10 | https://token.actions.githubusercontent.com 11 | ThumbprintList: 12 | # The GitHub certification authority (the signature of its certificate) 13 | - 6938fd4d98bab03faadb97b34396831e3780aea1 14 | ClientIdList: 15 | # The AWS security token service 16 | - sts.amazonaws.com 17 | 18 | 19 | Outputs: 20 | GithubIdentityProvider: 21 | Value: !Ref GithubIdentityProvider 22 | Export: 23 | Name: GithubIdentityProvider 24 | -------------------------------------------------------------------------------- /.github/cloudformation/token.yaml: -------------------------------------------------------------------------------- 1 | Description: > 2 | Enable storage of access tokens in AWS Secrets Manager and access to the PAT 3 | from the GitHub workflows in model-checking/cbmc-viewer. 4 | 5 | Parameters: 6 | GithubRepoOrganization: 7 | Type: String 8 | Description: GitHub organization for the CBMC viewer 9 | Default: model-checking 10 | CbmcViewerRepoName: 11 | Type: String 12 | Description: GitHub repository for CBMC viewer 13 | Default: cbmc-viewer 14 | CbmcViewerPublicationTag: 15 | Type: String 16 | Description: GitHub tag for CBMC viewer triggering the GitHub publication workflow 17 | Default: viewer-* 18 | 19 | Resources: 20 | 21 | BrewBotEmail: 22 | Type: AWS::SecretsManager::Secret 23 | Properties: 24 | Name: BOT_EMAIL 25 | Description: > 26 | The email address to use with Homebrew commits. 27 | 28 | BrewToken: 29 | Type: AWS::SecretsManager::Secret 30 | Properties: 31 | Name: RELEASE_CI_ACCESS_TOKEN 32 | Description: > 33 | GitHub access token. 34 | 35 | PypiToken: 36 | Type: AWS::SecretsManager::Secret 37 | Properties: 38 | Name: PYPI_ACCESS_TOKEN 39 | Description: > 40 | Pypi access token. 41 | 42 | PublisherTokenReader: 43 | Type: AWS::IAM::Role 44 | Properties: 45 | RoleName: PublisherTokenReader 46 | Description: > 47 | This role can retrieve the personal access token for the model 48 | checking publisher in the Microsoft Marketplace. 49 | 50 | AssumeRolePolicyDocument: 51 | Version: "2012-10-17" 52 | Statement: 53 | - Effect: Allow 54 | Principal: 55 | Federated: !ImportValue GithubIdentityProvider 56 | Action: sts:AssumeRoleWithWebIdentity 57 | Condition: 58 | StringEquals: 59 | token.actions.githubusercontent.com:aud: sts.amazonaws.com 60 | StringLike: 61 | token.actions.githubusercontent.com:sub: 62 | !Sub repo:${GithubRepoOrganization}/${CbmcViewerRepoName}:ref:refs/tags/${CbmcViewerPublicationTag} 63 | 64 | Policies: 65 | - PolicyName: PublisherTokenAccess 66 | PolicyDocument: 67 | Version: "2012-10-17" 68 | Statement: 69 | - Effect: Allow 70 | Action: secretsmanager:GetSecretValue 71 | Resource: !Ref BrewBotEmail 72 | - Effect: Allow 73 | Action: secretsmanager:GetSecretValue 74 | Resource: !Ref BrewToken 75 | - Effect: Allow 76 | Action: secretsmanager:GetSecretValue 77 | Resource: !Ref PypiToken 78 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 2 | 3 | version: 2 4 | updates: 5 | - package-ecosystem: "github-actions" 6 | directory: "/" 7 | schedule: 8 | interval: "monthly" 9 | -------------------------------------------------------------------------------- /.github/workflows/ctags.yaml: -------------------------------------------------------------------------------- 1 | name: Test generating symbols from ctags 2 | on: 3 | pull_request: 4 | types: [opened, synchronize, reopened, labeled, unlabeled] 5 | 6 | jobs: 7 | legacy: 8 | name: Legacy ctags test 9 | runs-on: macos-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | with: 13 | fetch-depth: 0 14 | - name: Install brew packages and ensure legacy ctags 15 | run: | 16 | brew uninstall ctags || true 17 | brew uninstall universal-ctags || true 18 | brew install cbmc litani 19 | - name: Compare symbol tables with legacy ctags 20 | run: | 21 | cd tests/unit-tests/ctags 22 | ctags || true 23 | make COMMIT1=${{ github.event.pull_request.head.sha }} COMMIT2=${{ github.event.pull_request.base.sha }} 24 | 25 | exuberant: 26 | name: Exuberant ctags test 27 | runs-on: macos-latest 28 | steps: 29 | - uses: actions/checkout@v4 30 | with: 31 | fetch-depth: 0 32 | - name: Install brew packages and ensure exuberant ctags 33 | run: | 34 | brew uninstall ctags || true 35 | brew uninstall universal-ctags || true 36 | brew install cbmc litani ctags 37 | - name: Compare symbol tables with exuberant ctags 38 | run: | 39 | cd tests/unit-tests/ctags 40 | ctags || true 41 | make COMMIT1=${{ github.event.pull_request.head.sha }} COMMIT2=${{ github.event.pull_request.base.sha }} 42 | 43 | universal: 44 | name: Universal ctags test 45 | runs-on: macos-latest 46 | steps: 47 | - uses: actions/checkout@v4 48 | with: 49 | fetch-depth: 0 50 | - name: Install brew packages and ensure universal ctags 51 | run: | 52 | brew uninstall ctags || true 53 | brew uninstall universal-ctags || true 54 | brew install cbmc litani universal-ctags 55 | - name: Compare symbol tables with universal ctags 56 | run: | 57 | cd tests/unit-tests/ctags 58 | ctags || true 59 | make COMMIT1=${{ github.event.pull_request.head.sha }} COMMIT2=${{ github.event.pull_request.base.sha }} 60 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT 3 | 4 | name: Publish cbmc-viewer documentation 5 | on: 6 | push: 7 | branches: 8 | - 'documentation' 9 | 10 | jobs: 11 | publish: 12 | runs-on: macos-latest 13 | steps: 14 | - name: Checkout repository 15 | uses: actions/checkout@v4 16 | 17 | - name: Install mdbook 18 | run: brew install mdbook 19 | 20 | - name: Build documentation 21 | run: cd docs && mdbook build && touch book/.nojekyll 22 | 23 | - name: Publish documentation 24 | uses: JamesIves/github-pages-deploy-action@v4.7.3 25 | with: 26 | branch: gh-pages 27 | folder: docs/book/ 28 | 29 | # This conditional might be useful on the publish step in the future 30 | # if: ${{ github.event_name == 'push' && startsWith('refs/heads/master', github.ref) }} 31 | -------------------------------------------------------------------------------- /.github/workflows/release-brew.yaml: -------------------------------------------------------------------------------- 1 | # See following documentations for more information - 2 | 3 | # Creation and maintenance of personal taps: 4 | # https://docs.brew.sh/How-to-Create-and-Maintain-a-Tap 5 | 6 | # More information about bottles: 7 | # https://docs.brew.sh/Bottles 8 | 9 | # Create a tap with automatic tests and bottle creation workflows: 10 | # https://brew.sh/2020/11/18/homebrew-tap-with-bottles-uploaded-to-github-releases/ 11 | 12 | # This workflow can be used to perform certain tasks from main repository rather 13 | # than a Tap - 14 | # - Formula Syntax Check 15 | # - Run brew test-bot for testing and bottle creation 16 | # (and upload bottles to release which triggered this workflow) 17 | # - Add commit on top of PR created by brew bump-formla-pr to add generate 18 | # bottle DSL to formula 19 | 20 | # USAGE: 21 | # Copy this workflow to .github/workflows of your repository and update 22 | # following variables in env below - 23 | # - FORMULA 24 | # - TAP 25 | # - BOT_USER 26 | # - BOT_EMAIL 27 | # - BOT_TOKEN 28 | 29 | # NOTE: 30 | # In case any other changes are required in the formula such as - 31 | # - Updating python dependencies 32 | # - Updating brew dependencies 33 | # - Updating test 34 | # - Updating Install block etc 35 | # Make sure to merge those changes before this workflow gets triggered as this 36 | # workflow will bump the formula and also create bottles right away 37 | # automatically. 38 | 39 | 40 | name: Release to brew 41 | on: 42 | release: 43 | types: [created] 44 | 45 | env: 46 | FORMULA: cbmc-viewer 47 | TAP: aws/tap 48 | BOT_USER: aws-viewer-for-cbmc-release-ci 49 | RELEASE_TAG: ${GITHUB_REF/refs\/tags\/} # GITHUB_REF = refs/tags/STRING-MAJOR.MINOR 50 | VERSION: $(echo $GITHUB_REF | cut -d "/" -f 3 | cut -d "-" -f 2) 51 | AWS_ROLE: arn:aws:iam::${{secrets.AWS_ACCOUNT}}:role/PublisherTokenReader 52 | AWS_REGION: us-west-2 53 | 54 | jobs: 55 | homebrew-pr: 56 | name: Homebrew Bump Formula PR 57 | runs-on: macos-latest 58 | permissions: 59 | id-token: write 60 | steps: 61 | - name: Authenticate GitHub workflow to AWS 62 | uses: aws-actions/configure-aws-credentials@v4 63 | with: 64 | role-to-assume: ${{ env.AWS_ROLE }} 65 | aws-region: ${{ env.AWS_REGION }} 66 | 67 | - name: Fetch secrets 68 | run: | 69 | bot_email=$(aws secretsmanager get-secret-value --secret-id BOT_EMAIL | jq -r '.SecretString') 70 | echo "::add-mask::$bot_email" 71 | echo "BOT_EMAIL=$bot_email" >> $GITHUB_ENV 72 | homebrew_github_api_token=$(aws secretsmanager get-secret-value --secret-id RELEASE_CI_ACCESS_TOKEN | jq -r '.SecretString') 73 | echo "::add-mask::$homebrew_github_api_token" 74 | echo "HOMEBREW_GITHUB_API_TOKEN=$homebrew_github_api_token" >> $GITHUB_ENV 75 | 76 | - name: Configure git user name and email 77 | run: | 78 | git config --global user.name ${{ env.BOT_USER }} 79 | git config --global user.email ${{ env.BOT_EMAIL }} 80 | 81 | - name: Create homebrew PR 82 | run: | 83 | brew tap ${{ env.TAP }} 84 | brew update-reset 85 | brew bump-formula-pr --tag "${{ env.RELEASE_TAG }}" --revision "${{ github.sha }}" ${{ env.TAP }}/${{ env.FORMULA }} --force 86 | 87 | build-bottle: 88 | needs: homebrew-pr 89 | strategy: 90 | matrix: 91 | os: [ubuntu-latest, macos-latest] 92 | runs-on: ${{ matrix.os }} 93 | permissions: 94 | id-token: write 95 | contents: write 96 | steps: 97 | - name: Set up Homebrew 98 | id: set-up-homebrew 99 | uses: Homebrew/actions/setup-homebrew@master 100 | 101 | - name: Authenticate GitHub workflow to AWS 102 | uses: aws-actions/configure-aws-credentials@v4 103 | with: 104 | role-to-assume: ${{ env.AWS_ROLE }} 105 | aws-region: ${{ env.AWS_REGION }} 106 | 107 | - name: Fetch secrets 108 | run: | 109 | fork_repo="https://$(aws secretsmanager get-secret-value --secret-id RELEASE_CI_ACCESS_TOKEN | jq -r '.SecretString')@github.com/${{ env.BOT_USER }}/homebrew-$(echo ${{ env.TAP }} |cut -d / -f 2).git" 110 | echo "::add-mask::$fork_repo" 111 | echo "FORK_REPO=$fork_repo" >> $GITHUB_ENV 112 | github_token="$(aws secretsmanager get-secret-value --secret-id RELEASE_CI_ACCESS_TOKEN | jq -r '.SecretString')" 113 | echo "::add-mask::$github_token" 114 | echo "GITHUB_TOKEN=$github_token" >> $GITHUB_ENV 115 | 116 | - name: Checkout PR 117 | run: | 118 | brew tap ${{ env.TAP }} 119 | brew untap --force hashicorp/tap 120 | brew update-reset 121 | cd $(brew --repo ${{ env.TAP }}) 122 | git remote add fork-repo ${{ env.FORK_REPO }} 123 | git fetch fork-repo 124 | git checkout -B bump-${{ env.FORMULA }}-${{ env.VERSION }} fork-repo/bump-${{ env.FORMULA }}-${{ env.VERSION }} 125 | 126 | - name: Tap Syntax 127 | run: | 128 | brew audit --online --git --skip-style ${{ env.TAP }}/${{ env.FORMULA }} 129 | brew style ${{ env.TAP }}/${{ env.FORMULA }} 130 | 131 | - name: Build bottle 132 | run: | 133 | brew test-bot --tap ${{ env.TAP }} --testing-formulae ${{ env.TAP }}/${{ env.FORMULA }} --only-formulae --root-url=https://github.com/${{ github.repository }}/releases/download/${{ env.RELEASE_TAG }} 134 | 135 | - name: Get Package Path 136 | id: get_package_path 137 | run: | 138 | echo "bottle_name=$(ls *.tar.gz)" >> $GITHUB_OUTPUT 139 | 140 | - name: Upload bottles as artifact 141 | uses: actions/upload-artifact@v4 142 | with: 143 | name: bottle-${{ matrix.os }} 144 | path: '*.bottle.*' 145 | 146 | - name: Upload release binary 147 | uses: softprops/action-gh-release@v2 148 | with: 149 | files: ${{ steps.get_package_path.outputs.bottle_name }} 150 | 151 | update-pr: 152 | needs: build-bottle 153 | runs-on: macos-latest 154 | permissions: 155 | id-token: write 156 | contents: write 157 | steps: 158 | - uses: actions/download-artifact@v4 159 | with: 160 | pattern: bottle-* 161 | 162 | - name: Authenticate GitHub workflow to AWS 163 | uses: aws-actions/configure-aws-credentials@v4 164 | with: 165 | role-to-assume: ${{ env.AWS_ROLE }} 166 | aws-region: ${{ env.AWS_REGION }} 167 | 168 | - name: Fetch secrets 169 | run: | 170 | bot_email="$(aws secretsmanager get-secret-value --secret-id BOT_EMAIL | jq -r '.SecretString')" 171 | echo "::add-mask::$bot_email" 172 | echo "BOT_EMAIL=$bot_email" >> $GITHUB_ENV 173 | fork_repo="https://$(aws secretsmanager get-secret-value --secret-id RELEASE_CI_ACCESS_TOKEN | jq -r '.SecretString')@github.com/${{ env.BOT_USER }}/homebrew-$(echo ${{ env.TAP }} |cut -d / -f 2).git" 174 | echo "::add-mask::$fork_repo" 175 | echo "FORK_REPO=$fork_repo" >> $GITHUB_ENV 176 | 177 | - name: Configure git user name and email 178 | run: | 179 | git config --global user.name ${{ env.BOT_USER }} 180 | git config --global user.email ${{ env.BOT_EMAIL }} 181 | 182 | - name: Checkout PR 183 | run: | 184 | brew tap ${{ env.TAP }} 185 | brew update-reset 186 | cd $(brew --repo ${{ env.TAP }}) 187 | git remote add fork-repo ${{ env.FORK_REPO }} 188 | git fetch fork-repo 189 | git checkout -B bump-${{ env.FORMULA }}-${{ env.VERSION }} fork-repo/bump-${{ env.FORMULA }}-${{ env.VERSION }} 190 | 191 | - name: Generate and merge bottle DSL 192 | run: | 193 | brew bottle --merge --write $(ls bottle-*/*.json) 194 | cd $(brew --repo ${{ env.TAP }}) 195 | git push fork-repo bump-${{ env.FORMULA }}-${{ env.VERSION }} 196 | -------------------------------------------------------------------------------- /.github/workflows/release-pypi.yaml: -------------------------------------------------------------------------------- 1 | name: Release to PyPi 2 | on: 3 | release: 4 | types: [created] 5 | 6 | env: 7 | AWS_ROLE: arn:aws:iam::${{secrets.AWS_ACCOUNT}}:role/PublisherTokenReader 8 | AWS_REGION: us-west-2 9 | 10 | jobs: 11 | upload-to-pypi: 12 | name: Upload to PyPi 13 | runs-on: ubuntu-latest 14 | permissions: 15 | id-token: write 16 | contents: write 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Install dependencies 20 | run: python3 -m pip install --upgrade pip build setuptools wheel 21 | - name: Build pip package 22 | run: python3 -m build 23 | - name: Authenticate GitHub workflow to AWS 24 | uses: aws-actions/configure-aws-credentials@v4 25 | with: 26 | role-to-assume: ${{ env.AWS_ROLE }} 27 | aws-region: ${{ env.AWS_REGION }} 28 | - name: Fetch secrets 29 | run: | 30 | github_token="$(aws secretsmanager get-secret-value --secret-id RELEASE_CI_ACCESS_TOKEN | jq -r '.SecretString')" 31 | echo "::add-mask::$github_token" 32 | echo "GITHUB_TOKEN=$github_token" >> $GITHUB_ENV 33 | - name: set asset path and name 34 | id: get_package_name 35 | run: | 36 | echo "package_name=$(ls dist/*.whl | cut -d / -f 2)" >> $GITHUB_OUTPUT 37 | - name: Upload release binary 38 | uses: softprops/action-gh-release@v2 39 | with: 40 | files: dist/${{ steps.get_package_name.outputs.package_name }} 41 | - name: Publish package distributions to PyPI 42 | uses: pypa/gh-action-pypi-publish@release/v1 43 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: CBMC viewer release 2 | # A new release is triggered by a new tag of the form viewer-VERSION 3 | on: 4 | push: 5 | tags: 6 | - viewer-* 7 | 8 | env: 9 | AWS_ROLE: arn:aws:iam::${{secrets.AWS_ACCOUNT}}:role/PublisherTokenReader 10 | AWS_REGION: us-west-2 11 | 12 | jobs: 13 | Release: 14 | name: CBMC viewer release 15 | runs-on: ubuntu-latest 16 | permissions: 17 | id-token: write 18 | contents: write 19 | steps: 20 | - name: Checkout code 21 | uses: actions/checkout@v4 22 | - name: Get version 23 | run: | 24 | # The environment variable GITHUB_REF is refs/tags/viewer-* 25 | echo "SETUP_VERSION=$(python3 -c "import configparser; config = configparser.ConfigParser(); config.read('setup.cfg'); print(config['metadata']['version'])")" >> $GITHUB_ENV 26 | echo "SOURCE_VERSION=$(python3 -c "import src.cbmc_viewer.version; print(src.cbmc_viewer.version.NUMBER)")" >> $GITHUB_ENV 27 | echo "TAG_VERSION=$(echo ${{ github.ref }} | cut -d "/" -f 3 | cut -d "-" -f 2)" >> $GITHUB_ENV 28 | - name: Version Check 29 | run: | 30 | if [[ ${{ env.SETUP_VERSION }} != ${{ env.TAG_VERSION }} ]] || [[ ${{ env.SOURCE_VERSION }} != ${{ env.TAG_VERSION }} ]]; then 31 | echo "Setup and source versions ${{env.SETUP_VERSION}} and ${{env.SOURCE_VERSION}} did not match tag version ${{env.TAG_VERSION}}" 32 | exit 1 33 | fi 34 | - name: Authenticate GitHub workflow to AWS 35 | uses: aws-actions/configure-aws-credentials@v4 36 | with: 37 | role-to-assume: ${{ env.AWS_ROLE }} 38 | aws-region: ${{ env.AWS_REGION }} 39 | - name: Fetch secrets 40 | run: | 41 | github_token="$(aws secretsmanager get-secret-value --secret-id RELEASE_CI_ACCESS_TOKEN | jq -r '.SecretString')" 42 | echo "::add-mask::$github_token" 43 | echo "GITHUB_TOKEN=$github_token" >> $GITHUB_ENV 44 | - name: Create release 45 | uses: actions/create-release@v1 46 | with: 47 | tag_name: viewer-${{ env.TAG_VERSION }} 48 | release_name: viewer-${{ env.TAG_VERSION }} 49 | body: | 50 | This is CBMC Viewer version ${{ env.TAG_VERSION }}. 51 | 52 | On MacOS, you can install with brew: 53 | ``` 54 | brew install aws/tap/cbmc-viewer 55 | ``` 56 | The prefix `aws/tap` refers to the AWS repository with the brew package. 57 | 58 | On all machines, you can install with pip: 59 | ``` 60 | python3 -m pip install cbmc-viewer 61 | ``` 62 | 63 | For best results, install [universal ctags](https://github.com/universal-ctags/ctags) or [exuberant ctags](https://github.com/universal-ctags/ctags) with 64 | 65 | * MacOS: `brew install universal-ctags` or `brew install ctags` 66 | * Ubuntu: `sudo apt install universal-ctags` or `sudo apt install ctags` 67 | * Windows: Follow the installation instructions in the [universal-ctags](https://github.com/universal-ctags/ctags) or [exuberant ctags](http://ctags.sourceforge.net/) repository. 68 | 69 | The installation of ctags is optional, but without ctags, `cbmc-viewer` will fail to link some symbols appearing in error traces to their definitions in the source code. The ctags tool has a long history. The original ctags was replaced by exhuberant ctags which was replaced by universal ctags. They all claim to be backwards compatible. We recommend universal ctags. 70 | draft: false 71 | prerelease: false 72 | -------------------------------------------------------------------------------- /.github/workflows/run-differential-tests.yaml: -------------------------------------------------------------------------------- 1 | name: Run Differential Tests 2 | on: 3 | pull_request: 4 | types: [opened, synchronize, reopened, labeled, unlabeled] 5 | 6 | jobs: 7 | run-differential-tests: 8 | if: "!contains(github.event.pull_request.labels.*.name, 'no-test')" 9 | name: Run Differential Tests 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | python-version: [ '3.8', '3.9', '3.10' ] 14 | steps: 15 | - uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | - name: Set up Python 19 | uses: actions/setup-python@v5 20 | with: 21 | python-version: ${{ matrix.python-version }} 22 | - name: Install Dependencies 23 | run: | 24 | get_latest_release() { 25 | curl --silent "https://api.github.com/repos/$1/releases/latest" | 26 | jq -r '.tag_name' 27 | } 28 | cbmc_latest_release=$(get_latest_release diffblue/cbmc) 29 | litani_latest_release=$(get_latest_release awslabs/aws-build-accumulator) 30 | curl -o cbmc.deb -L \ 31 | https://github.com/diffblue/cbmc/releases/download/$cbmc_latest_release/ubuntu-24.04-$cbmc_latest_release-Linux.deb 32 | curl -o litani.deb -L \ 33 | https://github.com/awslabs/aws-build-accumulator/releases/download/$litani_latest_release/litani-$litani_latest_release.deb 34 | sudo apt-get update \ 35 | && sudo apt-get install --no-install-recommends --yes \ 36 | ./cbmc.deb \ 37 | ./litani.deb \ 38 | ninja-build \ 39 | universal-ctags 40 | rm -f cbmc.deb litani.deb 41 | python3 -m pip install jinja2 cbmc-viewer 42 | - name: Proof Run 43 | run: | 44 | cd tests/repo-tests/coreHTTP 45 | make clone 46 | make build 47 | - name: Run Differential Test 48 | run: | 49 | git checkout -b pr 50 | ./tests/bin/difference.py --proofs tests/repo-tests/coreHTTP/coreHTTP/test/cbmc/proofs \ 51 | --viewer-repository $(pwd) \ 52 | --commits ${{ github.event.pull_request.head.sha }} ${{ github.event.pull_request.base.sha }} 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | .*~ 3 | \#* 4 | .\#* 5 | 6 | /cbmc-viewer 7 | /make-coverage 8 | /make-loop 9 | /make-property 10 | /make-reachable 11 | /make-result 12 | /make-source 13 | /make-symbol 14 | /make-trace 15 | /src/cbmc_viewer.egg-info 16 | 17 | /build 18 | /dist 19 | /.tox 20 | 21 | .litani_cache_dir 22 | .ninja_log 23 | __pycache__/ 24 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @markrtuttle 2 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *master* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | 61 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 62 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | # See https://packaging.python.org/en/latest/tutorials/installing-packages/ 5 | # See https://packaging.python.org/tutorials/packaging-projects/ 6 | # python3 -m ensurepip 7 | # python3 -m pip install --upgrade pip setuptools wheel 8 | # python3 -m pip install --upgrade build 9 | # python3 -m pip install --upgrade pylint 10 | 11 | default: 12 | @echo Nothing to make 13 | 14 | ################################################################ 15 | # Run pylint over the package 16 | 17 | PYLINT=pylint 18 | 19 | pylint: pylint-viewer pylint-tests 20 | 21 | pylint-viewer: 22 | $(PYLINT) \ 23 | --disable=duplicate-code \ 24 | --disable=fixme \ 25 | --disable=invalid-repr-returned \ 26 | --disable=too-few-public-methods \ 27 | --disable=too-many-arguments \ 28 | --disable=too-many-branches \ 29 | --module-rgx '[\w-]+' \ 30 | src/cbmc_viewer/*.py 31 | 32 | pylint-tests: 33 | $(MAKE) -C tests/bin PYLINT=$(PYLINT) pylint 34 | 35 | ################################################################ 36 | # Build the distribution package 37 | 38 | build: 39 | python3 -m build 40 | 41 | unbuild: 42 | $(RM) -r dist 43 | 44 | ################################################################ 45 | # Install the package into a virtual environment in development mode 46 | # 47 | # Note: Editable installs from pyproject.toml require at least pip 21.3 48 | 49 | VENV = /tmp/cbmc-viewer 50 | develop: 51 | python3 -m venv $(VENV) 52 | $(VENV)/bin/python3 -m pip install --upgrade pip 53 | $(VENV)/bin/python3 -m pip install -e . 54 | @ echo 55 | @ echo "Package installed into virtual environment at $(VENV)." 56 | @ echo "Activate virtual environment with 'source $(VENV)/bin/activate'" 57 | @ echo "(or add it to PATH with 'export PATH=\$$PATH:$(VENV)/bin')." 58 | @ echo 59 | 60 | undevelop: 61 | $(RM) -r $(VENV) 62 | 63 | ################################################################ 64 | # Install the package 65 | # 66 | # Note: This requires write permission (sudo): It updates the system 67 | # site-packages directory. 68 | 69 | 70 | install: 71 | python3 -m pip install --verbose . 72 | 73 | uninstall: 74 | python3 -m pip uninstall --verbose --yes cbmc-viewer 75 | 76 | ################################################################ 77 | # Clean up after packaging and installation 78 | 79 | clean: 80 | $(RM) *~ 81 | $(RM) *.pyc 82 | $(RM) -r __pycache__ 83 | 84 | veryclean: clean unbuild undevelop 85 | 86 | ################################################################ 87 | 88 | .PHONY: default pylint build unbuild develop undevelop install uninstall 89 | .PHONY: clean veryclean 90 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | AWS Viewer for CBMC 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## CBMC Viewer 2 | 3 | [CBMC](https://github.com/diffblue/cbmc) is a Bounded Model Checker for C. 4 | It can prove that (for computations of bounded depth) a C program exhibits 5 | no memory safe errors (no buffer overflows, no invalid pointers, etc), 6 | no undefined behaviors, and no failures of assertions in the code. 7 | [CBMC Viewer](https://github.com/awslabs/aws-viewer-for-cbmc) is a tool 8 | that scans the output of CBMC and produces a browsable summary of its findings. 9 | 10 | ## Example 11 | 12 | Here is a simple example of using cbmc-viewer. 13 | Running this example requires installing [CBMC](https://github.com/diffblue/cbmc). 14 | Installation on MacOS is just `brew install cbmc`. 15 | Installation on other operation systems is described on the [CBMC 16 | release page](https://github.com/diffblue/cbmc/releases/latest). 17 | 18 | Create a source file `main.c` containing 19 | ``` 20 | #include 21 | 22 | static int global; 23 | 24 | int main() { 25 | int *ptr = malloc(sizeof(int)); 26 | 27 | assert(global > 0); 28 | assert(*ptr > 0); 29 | 30 | return 0; 31 | } 32 | ``` 33 | and run the commands 34 | ``` 35 | goto-cc -o main.goto main.c 36 | cbmc main.goto --trace --xml-ui > result.xml 37 | cbmc main.goto --cover location --xml-ui > coverage.xml 38 | cbmc main.goto --show-properties --xml-ui > property.xml 39 | cbmc-viewer --goto main.goto --result result.xml --coverage coverage.xml --property property.xml --srcdir . 40 | ``` 41 | and open the report created by cbmc-viewer in a web browser with 42 | ``` 43 | open report/html/index.html 44 | ``` 45 | 46 | What you will see is 47 | 48 | * A *coverage report* summarizing what lines of source code were 49 | exercised by cbmc. In this case, coverage is 100%. Clicking on `main`, 50 | you can see the source code for `main` annotated with coverage data 51 | (all lines are green because all lines were hit). 52 | 53 | * A *bug report* summarizing what issues cbmc found with the code. In this case, 54 | the bugs are violations of the assertions because, for example, it is possible 55 | that the uninitialized integer allocated on the heap contains a negative value. 56 | For each bug, there is a link to 57 | 58 | * The line of code where the bug occurred. 59 | 60 | * An error trace showing the steps of the program leading to the bug. 61 | For each step, there a link to the line of code that generated the step, 62 | making it easy to follow the error trace and root cause the bug. 63 | 64 | ## Documentation 65 | 66 | The [cbmc-viewer documentation](https://model-checking.github.io/cbmc-viewer) includes a 67 | [reference manual](https://model-checking.github.io/cbmc-viewer/reference-manual) and a 68 | [user guide](https://model-checking.github.io/cbmc-viewer/user-guide). 69 | These documents are currently works in progress and will improve over time. 70 | 71 | ## Installation 72 | 73 | Most people should just follow the instructions on the 74 | [release page](https://github.com/awslabs/aws-viewer-for-cbmc/releases/latest). 75 | 76 | Developers can install the package in Python "development mode" as follows. 77 | 78 | * Clone the repository and install dependencies with 79 | ``` 80 | git clone https://github.com/awslabs/aws-viewer-for-cbmc.git cbmc-viewer 81 | apt install python3-pip python3-venv python3-jinja2 python3-voluptuous universal-ctags 82 | ``` 83 | Installing ctags is optional. See the ctags discussion at the end of the 84 | [release page](https://github.com/awslabs/aws-viewer-for-cbmc/releases/latest). 85 | * Install development mode with 86 | ``` 87 | cd cbmc-viewer 88 | make develop 89 | export PATH=/tmp/cbmc-viewer/bin:$PATH 90 | ``` 91 | * Uninstall development mode with 92 | ``` 93 | cd cbmc-viewer 94 | make undevelop 95 | ``` 96 | 97 | ## Security 98 | 99 | See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. 100 | 101 | ## License 102 | 103 | This project is licensed under the Apache-2.0 License. 104 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | book 2 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | mdbook build 3 | 4 | browse: 5 | mdbook serve --open 6 | 7 | clean: 8 | $(RM) books 9 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | This directory contains our documentation on how to use cbmc-viewer. 2 | 3 | The [`mdbook` documentation](https://rust-lang.github.io/mdBook/) 4 | explains how to use `mdbook` to generate documentation. 5 | * The file `book.toml` is the configuration file used by `mdbook` to 6 | build the documentation. 7 | * The directory `src` is the tree of markdown files that generate the 8 | "book" containing the documentation. 9 | * The file `src/SUMMARY.md` is the list of "chapters" and 10 | "subchapters" that will appear in the book. The only pages that 11 | will appear in the book are those generated from the markdown files 12 | listed in this summary. 13 | * The directory `book` will contain the html making up the book. 14 | 15 | You can build and browse the documentation by running the following 16 | commands in this directory: 17 | ```bash 18 | mdbook build 19 | mdbook serve --open 20 | ``` 21 | -------------------------------------------------------------------------------- /docs/book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = ["Mark R. Tuttle"] 3 | language = "en" 4 | multilingual = false 5 | src = "src" 6 | title = "CBMC viewer" 7 | -------------------------------------------------------------------------------- /docs/src/README.md: -------------------------------------------------------------------------------- 1 | # Getting started with CBMC viewer 2 | 3 | [CBMC](https://github.com/diffblue/cbmc) is a model checker for 4 | C. This means that CBMC will explore all possible paths through your code 5 | on all possible inputs, and will check that all assertions in your code are 6 | true. 7 | CBMC can also check for the possibility of 8 | memory safety errors (like buffer overflow) and for instances of 9 | undefined behavior (like signed integer overflow). 10 | CBMC is a bounded model checker, however, which means that using CBMC may 11 | require restricting this set of all possible inputs to inputs of some 12 | bounded size. 13 | 14 | [CBMC Viewer](https://github.com/model-checking/cbmc-viewer) is a tool 15 | that scans the output of CBMC and produces a browsable summary of its findings. 16 | 17 | ## Example 18 | 19 | Here is a simple example of using cbmc-viewer. 20 | Running this example requires installing [CBMC](https://github.com/diffblue/cbmc). 21 | Installation on MacOS is just `brew install cbmc`. 22 | Installation on other operation systems is described on the [CBMC 23 | release page](https://github.com/diffblue/cbmc/releases/latest). 24 | 25 | Create a source file `main.c` containing 26 | ``` 27 | #include 28 | 29 | static int global; 30 | 31 | int main() { 32 | int *ptr = malloc(sizeof(int)); 33 | 34 | assert(global > 0); 35 | assert(*ptr > 0); 36 | 37 | return 0; 38 | } 39 | ``` 40 | and run the commands 41 | ``` 42 | goto-cc -o main.goto main.c 43 | cbmc main.goto --trace --xml-ui > result.xml 44 | cbmc main.goto --cover location --xml-ui > coverage.xml 45 | cbmc main.goto --show-properties --xml-ui > property.xml 46 | cbmc-viewer --goto main.goto --result result.xml --coverage coverage.xml --property property.xml --srcdir . 47 | ``` 48 | and open the report created by cbmc-viewer in a web browser with 49 | ``` 50 | open report/html/index.html 51 | ``` 52 | 53 | What you will see is 54 | 55 | * A *coverage report* summarizing what lines of source code were 56 | exercised by cbmc. In this case, coverage is 100%. Clicking on `main`, 57 | you can see the source code for `main` annotated with coverage data 58 | (all lines are green because all lines were hit). 59 | 60 | * A *bug report* summarizing what issues cbmc found with the code. In this case, 61 | the bugs are violations of the assertions because, for example, it is possible 62 | that the uninitialized integer allocated on the heap contains a negative value. 63 | For each bug, there is a link to 64 | 65 | * The line of code where the bug occurred. 66 | 67 | * An error trace showing the steps of the program leading to the bug. 68 | For each step, there a link to the line of code that generated the step, 69 | making it easy to follow the error trace and root cause the bug. 70 | 71 | ## Installation 72 | 73 | Most people should just follow the instructions on the 74 | [release page](https://github.com/awslabs/aws-viewer-for-cbmc/releases/latest). 75 | 76 | Developers can install the package in Python "development mode" as follows. 77 | First, follow the instructions on the 78 | [release page](https://github.com/model-checking/cbmc-viewer/releases/latest) 79 | to install the dependencies. Then, 80 | 81 | * Clone the repository with 82 | ``` 83 | git clone https://github.com/model-checking/cbmc-viewer cbmc-viewer 84 | ``` 85 | * Install development mode with 86 | ``` 87 | cd cbmc-viewer 88 | make develop 89 | export PATH=$PATH:$(pwd) 90 | ``` 91 | * Uninstall development mode with 92 | ``` 93 | cd cbmc-viewer 94 | make undevelop 95 | ``` 96 | 97 | ## Helping others 98 | 99 | This training material is a work in progress. If you have suggestions, 100 | corrections, or questions, contact us by submitting a 101 | [GitHub issue](https://github.com/model-checking/cbmc-viewer/issues). 102 | If you have some training of your own that you would like to contribute, 103 | submit your contributions as a 104 | [GitHub pull request](https://github.com/model-checking/cbmc-viewer/pulls). 105 | -------------------------------------------------------------------------------- /docs/src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | * [Getting started](README.md) 4 | * [User manual]() 5 | * [Reference manual]() 6 | * [Frequently asked questions]() 7 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | # See https://packaging.python.org/en/latest/tutorials/packaging-projects 2 | 3 | [build-system] 4 | requires = [ 5 | "setuptools>=42", 6 | "wheel" 7 | ] 8 | build-backend = "setuptools.build_meta" 9 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | # See https://packaging.python.org/en/latest/tutorials/packaging-projects 2 | 3 | [metadata] 4 | name = cbmc-viewer 5 | version = 3.11.1 6 | author = Mark R. Tuttle 7 | author_email = mrtuttle@amazon.com 8 | description = CBMC viewer produces a browsable summary of CBMC findings 9 | long_description = file: README.md 10 | long_description_content_type = text/markdown 11 | url = https://github.com/model-checking/cbmc-viewer 12 | license = Apache License 2.0 13 | classifiers = 14 | Programming Language :: Python :: 3 15 | License :: OSI Approved :: Apache Software License 16 | Operating System :: OS Independent 17 | 18 | [options] 19 | package_dir = 20 | = src 21 | packages = find: 22 | include_package_data = True 23 | install_requires = 24 | jinja2 25 | setuptools 26 | voluptuous 27 | python_requires = >=3.7 28 | 29 | [options.packages.find] 30 | where = src 31 | 32 | [options.package_data] 33 | cbmc_viewer = 34 | etc/* 35 | etc/bash_completion.d/* 36 | templates/* 37 | viewer.css 38 | viewer.js 39 | 40 | [options.entry_points] 41 | console_scripts = 42 | cbmc-viewer = cbmc_viewer.cbmc_viewer:main 43 | -------------------------------------------------------------------------------- /src/cbmc_viewer/MANIFEST.in: -------------------------------------------------------------------------------- 1 | include templates/* 2 | include viewer.css 3 | include viewer.js 4 | -------------------------------------------------------------------------------- /src/cbmc_viewer/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/model-checking/cbmc-viewer/3750e8d30f9c8c9e1291a2e0dfa534458b2a4818/src/cbmc_viewer/__init__.py -------------------------------------------------------------------------------- /src/cbmc_viewer/cbmc_viewer.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | """Main entry point for cbmc-viewer and subcommands.""" 5 | 6 | from cbmc_viewer import optionst 7 | 8 | def main(): 9 | """Construct the cbmc report.""" 10 | 11 | args = optionst.create_parser().parse_args() 12 | args = optionst.defaults(args) 13 | args.func(args) 14 | -------------------------------------------------------------------------------- /src/cbmc_viewer/configt.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | """CBMC viewer configuration.""" 5 | 6 | from pathlib import Path 7 | import logging 8 | 9 | from cbmc_viewer import parse 10 | 11 | EXPECTED_MISSING = 'expected-missing-functions' 12 | 13 | class Config: 14 | """Manage CBMC viewer configuration settings.""" 15 | 16 | def __init__(self, config_file=None): 17 | self.file = config_file 18 | self.missing_functions = [] 19 | 20 | if config_file is None: 21 | return 22 | 23 | if not Path(config_file).exists(): 24 | logging.info("Config file does not exist: %s", config_file) 25 | return 26 | 27 | config_data = parse.parse_json_file(config_file) 28 | self.missing_functions = config_data.get(EXPECTED_MISSING, []) 29 | 30 | def expected_missing_functions(self): 31 | """Return list of expected missing functions.""" 32 | 33 | return self.missing_functions 34 | -------------------------------------------------------------------------------- /src/cbmc_viewer/ctagst.py: -------------------------------------------------------------------------------- 1 | """Ctags support for locating symbol definitions""" 2 | 3 | from pathlib import Path 4 | import json 5 | import logging 6 | import subprocess 7 | import sys 8 | 9 | ################################################################ 10 | # This popen method is used to subprocess-out the invocation of ctags. 11 | # This method duplicates code in other modules to make this ctags 12 | # module a stand-alone module that can be copied and reused in other 13 | # projects. 14 | 15 | def popen(cmd, cwd=None, stdin=None, encoding=None): 16 | """Run a command with string stdin on stdin, return stdout and stderr.""" 17 | 18 | cmd = [str(word) for word in cmd] 19 | kwds = {'cwd': cwd, 20 | 'universal_newlines': True, 21 | 'stdin': subprocess.PIPE, 22 | 'stdout': subprocess.PIPE, 23 | 'stderr': subprocess.PIPE} 24 | if sys.version_info >= (3, 6): # encoding is new in Python 3.6 25 | kwds['encoding'] = encoding or 'utf-8' 26 | try: 27 | logging.debug('Popen command: "%s"', ' '.join(cmd)) 28 | logging.debug('Popen stdin: "%s"', stdin) 29 | with subprocess.Popen(cmd, **kwds) as pipe: 30 | stdout, stderr = pipe.communicate(input=stdin) 31 | logging.debug('Popen stdout: "%s"', stdout) 32 | logging.debug('Popen stderr: "%s"', stderr) 33 | if pipe.returncode: 34 | logging.debug('Popen command failed: "%s"', ' '.join(cmd)) 35 | logging.debug('Popen return code: "%s"', pipe.returncode) 36 | raise UserWarning(f"Failed to run command: {' '.join(cmd)}") 37 | return stdout, stderr 38 | except FileNotFoundError as error: 39 | logging.debug("FileNotFoundError: command '%s': %s", ' '.join(cmd), error) 40 | raise UserWarning(f"Failed to run command: {' '.join(cmd)}") from error 41 | 42 | ################################################################ 43 | 44 | def ctags(root, files): 45 | """List symbols defined in files under root.""" 46 | 47 | root = Path(root) 48 | files = [str(file_) for file_ in files] 49 | return (universal_ctags(root, files) or 50 | exhuberant_ctags(root, files) or 51 | legacy_ctags(root, files) or 52 | []) 53 | 54 | ################################################################ 55 | 56 | def universal_ctags(root, files): 57 | """Use universal ctags to list symbols defined in files under root.""" 58 | 59 | # See universal ctags man page at https://docs.ctags.io/en/latest/man/ctags.1.html 60 | cmd = [ 61 | 'ctags', 62 | '-L', '-', # read files from standard input, one file per line 63 | '-f', '-', # write tags to standard output, one tag per line 64 | '--output-format=json', # each tag is a one-line json blob 65 | '--fields=FNnK' # json blob is {"name": symbol, "path": file, "line": line, "kind": kind} 66 | ] 67 | try: 68 | logging.info("Running universal ctags") 69 | stdout, _ = popen(cmd, cwd=root, stdin='\n'.join(files)) 70 | strings = stdout.splitlines() 71 | except UserWarning: 72 | logging.info("Universal ctags failed") 73 | strings = [] 74 | 75 | return [tag for string in strings for tag in universal_tag(root, string)] 76 | 77 | def universal_tag(root, string): 78 | """Extract tag from universal ctag output.""" 79 | 80 | try: 81 | # universal ctag json output is '{"name": symbol, "path": file, "line": line, "kind": kind}' 82 | blob = json.loads(string) 83 | return [{'symbol': blob['name'], 'file': root/blob['path'], 'line': int(blob['line']), 84 | 'kind': blob['kind']}] 85 | except (json.decoder.JSONDecodeError, # json is unparsable 86 | KeyError, # json key is missing 87 | ValueError) as error: # invalid literal for int() 88 | logging.debug("Bad universal ctag: %s: %s", string, error) 89 | return [] 90 | 91 | ################################################################ 92 | 93 | def exhuberant_ctags(root, files): 94 | """Use exhuberant ctags to list symbols defined in files under root.""" 95 | 96 | # See exhuberant ctags man page at https://linux.die.net/man/1/ctags 97 | cmd = [ 98 | 'ctags', 99 | '-L', '-', # read files from standard input, one file per line 100 | '-f', '-', # write tags to standard output, one tag per line 101 | '-n', # use line numbers (not search expressions) to locate symbol in file 102 | '--fields=K' # include symbol kind among extension fields 103 | ] 104 | try: 105 | logging.info("Running exhuberant ctags") 106 | stdout, _ = popen(cmd, cwd=root, stdin='\n'.join(files)) 107 | strings = stdout.splitlines() 108 | except UserWarning: 109 | logging.info("Exhuberant ctags failed") 110 | strings = [] 111 | 112 | return [tag for string in strings for tag in exhuberant_tag(root, string)] 113 | 114 | def exhuberant_tag(root, string): 115 | """Extract tag from exhuberant ctag output.""" 116 | 117 | try: 118 | # exhuberant ctag output is 'symbolpathline;"kind' 119 | left, right = string.split(';"')[:2] 120 | symbol, path, line = left.split("\t")[:3] 121 | kind = right.split("\t")[1] 122 | return [{'symbol': symbol, 'file': root/path, 'line': int(line), 'kind': kind}] 123 | except (ValueError, IndexError): # not enough values to unpack, invalid literal for int() 124 | logging.debug('Bad exhuberant ctag: "%s"', string) 125 | return [] 126 | 127 | ################################################################ 128 | 129 | def legacy_ctags(root, files): 130 | """Use legacy ctags to list symbols defined in files under root.""" 131 | 132 | # MacOS ships with a legacy ctags from BSD installed in /usr/bin/ctags. 133 | # See the MacOS man page for the documentation used to implement this method. 134 | cmd = ['ctags', 135 | '-x', # write human-readable summary to standard output 136 | *files # legacy ctags cannot read list of files from stdin 137 | ] 138 | try: 139 | logging.info("Running legacy ctags") 140 | stdout, _ = popen(cmd, cwd=root) 141 | strings = stdout.splitlines() 142 | except UserWarning: 143 | logging.info("Legacy ctags failed") 144 | strings = [] 145 | return [tag for string in strings for tag in legacy_tag(root, string)] 146 | 147 | def legacy_tag(root, string): 148 | """Extract tag from legacy ctag output.""" 149 | 150 | try: 151 | # legacy ctag -x output is 'symbol line path source_code_fragment' 152 | symbol, line, path = string.split()[:3] 153 | return [{'symbol': symbol, 'file': root/path, 'line': int(line), 'kind': None}] 154 | except ValueError: # not enough values to unpack, invalid literal for int() 155 | logging.debug('Bad legacy ctag: "%s"', string) 156 | return [] 157 | 158 | ################################################################ 159 | -------------------------------------------------------------------------------- /src/cbmc_viewer/etc/bash_completion.d/cbmc-viewer.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | # SPDX-License-Identifier: Apache-2.0 5 | 6 | ################################################################ 7 | # Documentation 8 | # 9 | # compgen, complete: 10 | # https://www.gnu.org/software/bash/manual/html_node/Programmable-Completion-Builtins.html 11 | # _filedir: 12 | # https://github.com/nanoant/bash-completion-lib/blob/master/include/_filedir 13 | 14 | ################################################################ 15 | # Options 16 | # 17 | # omit deprecated options: --block --htmldir --srcexclude --blddir --storm 18 | 19 | common_options="--help -h --verbose --debug --version" 20 | 21 | viewer_options="--result --coverage --property --srcdir --exclude --extensions --source-method --wkdir --goto --reportdir --json-summary --viewer-coverage --viewer-loop --viewer-property --viewer-reachable --viewer-result --viewer-source --viewer-symbol --viewer-trace --config" 22 | viewer_subcommands="coverage loop property reachable result source symbol trace" 23 | 24 | coverage_options="--coverage --srcdir --viewer-coverage" 25 | loop_options="--srcdir --goto --viewer-loop" 26 | property_options="--property --srcdir --viewer-property" 27 | reachable_options="--srcdir --goto --viewer-reachable" 28 | result_options="--result --viewer-result" 29 | source_options="--srcdir --exclude --extensions --source-method --wkdir --goto --viewer-source" 30 | symbol_options="--srcdir --wkdir --goto --viewer-source --viewer-symbol" 31 | trace_options="--result --srcdir --wkdir --viewer-trace" 32 | 33 | _core_autocomplete() 34 | { 35 | local options=$1 36 | local subcommands=$2 37 | local cur=${COMP_WORDS[COMP_CWORD]} 38 | local prev=${COMP_WORDS[COMP_CWORD-1]} 39 | 40 | case "$prev" in 41 | --srcdir|--wkdir) 42 | _filedir -d 43 | return 0 44 | ;; 45 | 46 | --result|--coverage|--property) 47 | # typically *.txt, *.xml, *.json 48 | _filedir 49 | return 0 50 | ;; 51 | 52 | --goto) 53 | # typically *.goto 54 | _filedir 55 | return 0 56 | ;; 57 | 58 | --json-summary) 59 | # typically *.json 60 | _filedir 61 | return 0 62 | ;; 63 | 64 | --config) 65 | # typically cbmc-viewer.json 66 | _filedir 67 | return 0 68 | ;; 69 | 70 | --viewer-coverage|--viewer-loop|--viewer-property|--viewer-reachable|--viewer-result|--viewer-source|--viewer-symbol|--viewer-trace) 71 | # typically viewer-*.json 72 | _filedir 73 | return 0 74 | ;; 75 | 76 | --reportdir) 77 | # typically report 78 | _filedir -d 79 | return 0 80 | ;; 81 | 82 | --source-method) 83 | # typically report 84 | COMPREPLY=( $( compgen -W "goto find walk make" -- $cur ) ) 85 | _filedir -d 86 | return 0 87 | ;; 88 | 89 | coverage) 90 | COMPREPLY=( $( compgen -W "$coverage_options $common_options" -- $cur ) ) 91 | return 0 92 | ;; 93 | loop) 94 | COMPREPLY=( $( compgen -W "$loop_options $common_options" -- $cur ) ) 95 | return 0 96 | ;; 97 | property) 98 | COMPREPLY=( $( compgen -W "$property_options $common_options" -- $cur ) ) 99 | return 0 100 | ;; 101 | reachable) 102 | COMPREPLY=( $( compgen -W "$reachable_options $common_options" -- $cur ) ) 103 | return 0 104 | ;; 105 | result) 106 | COMPREPLY=( $( compgen -W "$result_options $common_options" -- $cur ) ) 107 | return 0 108 | ;; 109 | source) 110 | COMPREPLY=( $( compgen -W "$source_options $common_options" -- $cur ) ) 111 | return 0 112 | ;; 113 | symbol) 114 | COMPREPLY=( $( compgen -W "$symbol_options $common_options" -- $cur ) ) 115 | return 0 116 | ;; 117 | trace) 118 | COMPREPLY=( $( compgen -W "$trace_options $common_options" -- $cur ) ) 119 | return 0 120 | ;; 121 | 122 | 123 | esac 124 | 125 | if [[ "$cur" == -* ]]; then 126 | COMPREPLY=( $( compgen -W "$options $common_options" -- $cur ) ) 127 | return 0 128 | fi 129 | 130 | COMPREPLY=( $( compgen -W "$options $common_options $subcommands" -- $cur ) ) 131 | return 0 132 | } 133 | 134 | _viewer_autocomplete() 135 | { 136 | _core_autocomplete "$viewer_options" "$viewer_subcommands" 137 | } 138 | 139 | complete -F _viewer_autocomplete cbmc-viewer 140 | -------------------------------------------------------------------------------- /src/cbmc_viewer/filet.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | """The known file types.""" 5 | 6 | import enum 7 | import os 8 | 9 | ################################################################ 10 | 11 | class File(enum.Enum): 12 | """The known file types.""" 13 | 14 | TEXT = 1 15 | XML = 2 16 | JSON = 3 17 | 18 | def filetype(filename): 19 | """Return the file type denoted by the filename extension.""" 20 | 21 | if filename is None: 22 | return None 23 | 24 | if not isinstance(filename, str): 25 | raise UserWarning(f"Filename is not a string: {filename}") 26 | 27 | # The filename extension: expected to be one of txt, jsn, json, or xml 28 | file_extension = os.path.splitext(filename)[1].lower().lstrip('.') 29 | 30 | # Return the file type 31 | try: 32 | return { 33 | 'log': File.TEXT, 34 | 'txt': File.TEXT, 35 | 'jsn': File.JSON, 36 | 'json': File.JSON, 37 | 'xml': File.XML 38 | }[file_extension] 39 | except KeyError: 40 | raise UserWarning( 41 | f"Can't determine file type of file {filename}" 42 | ) from None # squash the KeyError context, raise just a UserWarning 43 | 44 | ################################################################ 45 | 46 | def is_text_file(txt): 47 | """File is a text file.""" 48 | return filetype(txt) == File.TEXT 49 | def is_json_file(json): 50 | """File is a json file.""" 51 | return filetype(json) == File.JSON 52 | def is_xml_file(xml): 53 | """File is an xml file.""" 54 | return filetype(xml) == File.XML 55 | 56 | def all_text_files(txts): 57 | """Files are text files.""" 58 | return all(is_text_file(txt) for txt in txts) 59 | def all_json_files(jsons): 60 | """Files are json files.""" 61 | return all(is_json_file(json) for json in jsons) 62 | def all_xml_files(xmls): 63 | """Files are xml files.""" 64 | return all(is_xml_file(xml) for xml in xmls) 65 | 66 | def any_text_files(txts): 67 | """Any of the files are text files.""" 68 | return any(is_text_file(txt) for txt in txts) 69 | 70 | ################################################################ 71 | -------------------------------------------------------------------------------- /src/cbmc_viewer/markup_code.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | """Annotated source code.""" 5 | 6 | import html 7 | import logging 8 | import os 9 | import re 10 | 11 | import voluptuous 12 | import voluptuous.humanize 13 | 14 | from cbmc_viewer import markup_link 15 | from cbmc_viewer import templates 16 | from cbmc_viewer import util 17 | 18 | ################################################################ 19 | # Data passed to jinja to generate annotated code from code.jinja.html 20 | 21 | VALID_LINE = voluptuous.Schema({ 22 | 'num': int, 23 | 'status': voluptuous.Any('hit', 'missed', 'both', 'none'), 24 | 'code': str, 25 | }, required=True) 26 | 27 | VALID_CODE = voluptuous.Schema({ 28 | 'filename': str, 29 | 'path_to_root': str, 30 | 'lines': [VALID_LINE], 31 | 'outdir': str 32 | }, required=True) 33 | 34 | ################################################################ 35 | # An annotated file of source code. 36 | 37 | class Code: 38 | """An annotated file of source code.""" 39 | 40 | def __init__(self, root, path, symbols, coverage, outdir='.'): 41 | 42 | try: 43 | # load code into a string 44 | with open(os.path.join(root, path), encoding='utf-8') as source: 45 | code = html.escape(untabify_code(source.read()), quote=False) 46 | 47 | # split code into blocks of code, comments, and string literals 48 | blocks = split_code_into_blocks(code) 49 | 50 | # link symbols in code blocks to symbol definitions 51 | linked_blocks = link_symbols_in_code_blocks(path, blocks, symbols) 52 | 53 | # reform code as a string with symbols linked to definitions 54 | linked_code = ''.join(linked_blocks) 55 | 56 | # break code into lines annotated with line number and line coverage 57 | annotated_lines = annotate_code(path, linked_code, coverage) 58 | 59 | self.lines = annotated_lines 60 | except FileNotFoundError: 61 | # The goto symbol table occassional refers to header files 62 | # like gcc_builtin_headers_types.h that are part of the 63 | # CBMC implementation. 64 | # * We skip source annotation: We treat the file as a 65 | # zero-length file with nothing to annotate. 66 | # * We print a simple info message: The relative path 67 | # to the file in the symbol table was interpreted by 68 | # the symbol table parser as relative to the working 69 | # directory. Printing this path in the info is 70 | # confusing, so we print just the base name. 71 | logging.info("Skipping source file annotation: %s", 72 | os.path.basename(path)) 73 | self.lines = [] 74 | 75 | self.filename = path 76 | self.path_to_root = markup_link.path_to_file('.', path) 77 | self.outdir = outdir 78 | self.validate() 79 | 80 | def __str__(self): 81 | """Render annotated code as html.""" 82 | 83 | return templates.render_code(self.filename, 84 | self.path_to_root, 85 | self.lines) 86 | 87 | def validate(self): 88 | """Validate members of a code object.""" 89 | 90 | return voluptuous.humanize.validate_with_humanized_errors( 91 | self.__dict__, VALID_CODE 92 | ) 93 | 94 | def dump(self, filename=None, outdir=None): 95 | """Write annotated code to a file rendered as html.""" 96 | 97 | util.dump(self, 98 | filename or self.filename + ".html", 99 | outdir or self.outdir) 100 | 101 | ################################################################ 102 | # Untabify code: replace tabs with spaces. 103 | 104 | def untabify_code(code, tabstop=8): 105 | """Untabify a block of code.""" 106 | 107 | return '\n'.join( 108 | [untabify_line(line, tabstop) for line in code.splitlines()] 109 | ) 110 | 111 | def untabify_line(line, tabstop=8): 112 | """Untabify a line of code.""" 113 | 114 | strings = [] 115 | length = 0 116 | for string in re.split('(\t)', line): 117 | if string == '\t': 118 | string = ' '*(tabstop - (length % tabstop)) 119 | length += len(string) 120 | strings.append(string) 121 | return ''.join(strings) 122 | 123 | ################################################################ 124 | # Split code into code blocks and strings/comments 125 | 126 | def split_code_into_blocks(code): 127 | """Split code into blocks of code, comments, and string literals.""" 128 | 129 | def is_noncode_start(code, idx=0): 130 | """String starts something other than source code (eg, a comment).""" 131 | 132 | return (is_quote(code, idx) or 133 | is_multiline_comment_start(code, idx) or 134 | is_singleline_comment_start(code, idx)) 135 | 136 | def find_predicate(code, predicate, idx=0): 137 | """First position in a string satisfying a predicate.""" 138 | 139 | while idx < len(code) and not predicate(code, idx): 140 | idx += 1 141 | return idx 142 | 143 | blocks = [] 144 | 145 | while code: 146 | idx = find_predicate(code, is_noncode_start) 147 | block, code, idx = code[:idx], code[idx:], 0 148 | if block: 149 | blocks.append(block) 150 | 151 | if not code: 152 | break 153 | 154 | if is_quote(code): 155 | idx = find_predicate(code, is_quote, 1) 156 | elif is_multiline_comment_start(code): 157 | idx = find_predicate(code, is_multiline_comment_end, 2) 158 | elif is_singleline_comment_start(code): 159 | idx = find_predicate(code, is_singleline_comment_end, 2) 160 | block, code, idx = code[:idx+1], code[idx+1:], 0 161 | if block: 162 | blocks.append(block) 163 | 164 | return blocks 165 | 166 | def is_quote(code, idx=0): 167 | """Position in string is an unescaped quotation mark.""" 168 | return (0 <= idx < len(code) and 169 | code[idx] == '"' and 170 | (idx == 0 or code[idx-1] != '\\')) 171 | 172 | def is_multiline_comment_start(code, idx=0): 173 | """Position in string starts a multi-line comment.""" 174 | return idx >= 0 and idx+2 <= len(code) and code[idx:idx+2] == '/*' 175 | 176 | def is_multiline_comment_end(code, idx=0): 177 | """Position in string ends a multi-line comment.""" 178 | return idx-1 >= 0 and idx+1 <= len(code) and code[idx-1:idx+1] == '*/' 179 | 180 | def is_singleline_comment_start(code, idx=0): 181 | """Position in string starts a one-line comment.""" 182 | return idx >= 0 and idx+2 <= len(code) and code[idx:idx+2] == '//' 183 | 184 | def is_singleline_comment_end(code, idx=0): 185 | """Position in string ends a one-line comment.""" 186 | return idx >= 0 and idx+1 < len(code) and code[idx+1] == '\n' 187 | 188 | ################################################################ 189 | # Link symbols in code blocks 190 | 191 | def link_symbols_in_code_blocks(path, blocks, symbols): 192 | """Link symbols appearing a sequence of blocks.""" 193 | 194 | return [link_symbols_in_code_block(path, block, symbols) 195 | for block in blocks] 196 | 197 | def link_symbols_in_code_block(path, block, symbols): 198 | """Link symbols appearing a code block.""" 199 | 200 | if (is_quote(block) or 201 | is_multiline_comment_start(block) or 202 | is_singleline_comment_start(block)): 203 | return block 204 | 205 | return link_symbols(path, block, symbols) 206 | 207 | def link_symbols(path, code, symbols): 208 | """Link symbols appearing a code string.""" 209 | 210 | tokens = split_code_into_symbols(code) 211 | return ''.join( 212 | [markup_link.link_text_to_symbol(tkn, tkn, symbols, from_file=path, escape_text=False) 213 | for tkn in tokens] 214 | ) 215 | 216 | def split_code_into_symbols(code): 217 | """Split a code string into a list of symbols and nonsymbols.""" 218 | 219 | return re.split('([_a-zA-Z][_a-zA-Z0-9]*)', code) 220 | 221 | ################################################################ 222 | # Annotate lines of symbol-linked code with line numbers and coverage 223 | 224 | def annotate_code(path, code, coverage): 225 | """Annotate lines of code with line numbers and coverage status.""" 226 | 227 | return [{ # line_num is 0-based, line numbers are 1-based 228 | 'num': line_num+1, 229 | 'status': str(coverage.lookup(path, line_num+1)).lower(), 230 | 'code': line 231 | } for (line_num, line) in enumerate(code.splitlines())] 232 | -------------------------------------------------------------------------------- /src/cbmc_viewer/markup_link.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | """Links to source code. 5 | 6 | This module is a set of methods for constructing links into the 7 | annotated source code. All other modules use these methods for 8 | consistent links to source code. All paths in this module are 9 | assumed to be relative to the root of the source code. 10 | 11 | """ 12 | 13 | import html 14 | import os 15 | import re 16 | 17 | from cbmc_viewer import srcloct 18 | 19 | ################################################################ 20 | 21 | def path_to_file(dst, src): 22 | """The path from src to dst for use in a hyperlink from src to dst. 23 | 24 | Given two paths src and dst relative to a common root, return the 25 | relative path from src to dst. This is the path to use in a 26 | hyperlink from src to dst. For example, 27 | the path from 'a/b/foo.html' to 'c/bar.html' is '../../c/bar.html', 28 | the path from 'a/b/foo.html' to '.' is '../..', and 29 | the path from '.' to 'a/b/foo.html' is 'a/b/foo.html'. 30 | """ 31 | 32 | src_dir = os.path.dirname(src) 33 | src_to_dst = os.path.relpath(dst, src_dir) 34 | cwd_to_dst = os.path.normpath(os.path.join(src_dir, src_to_dst)) 35 | if dst != cwd_to_dst: 36 | raise UserWarning(f"{dst} != {cwd_to_dst}") 37 | return src_to_dst 38 | 39 | ################################################################ 40 | # Method to link into the source tree. 41 | # By default, links are from the root of the source tree to the source file. 42 | # 43 | # In what follows, the text being linked into the source tree may be 44 | # None. For example, the source location for a global variable (a 45 | # static variable) will give a file name and a line number, but the 46 | # function name will be omitted (because there is no enclosing 47 | # function to name). This may result in one of the following 48 | # functions being invoked with text set to None as the value of the 49 | # missing function name. 50 | 51 | def link_text_to_file(text, to_file, from_file=None, escape_text=True): 52 | """Link text to a file in the source tree.""" 53 | 54 | if not text: 55 | return text 56 | 57 | text = html.escape(str(text)) if escape_text else str(text) 58 | 59 | if srcloct.file_is_not_a_source_file(to_file): 60 | return text 61 | 62 | from_file = from_file or '.' 63 | path = path_to_file(to_file, from_file) 64 | return f'{text}' 65 | 66 | def link_text_to_line(text, to_file, line, from_file=None, escape_text=True): 67 | """Link text to a line in a file in the source tree.""" 68 | 69 | if not text: 70 | return text 71 | 72 | text = html.escape(str(text)) if escape_text else str(text) 73 | 74 | if srcloct.file_is_not_a_source_file(to_file): 75 | return text 76 | 77 | from_file = from_file or '.' 78 | line = int(line) 79 | path = path_to_file(to_file, from_file) 80 | return f'{text}' 81 | 82 | def link_text_to_srcloc(text, srcloc, from_file=None, escape_text=True): 83 | """Link text to a source location in a file in the source tree.""" 84 | 85 | if not text: 86 | return text 87 | 88 | if srcloc is None: 89 | return html.escape(text) if escape_text else text 90 | return link_text_to_line(text, srcloc['file'], srcloc['line'], from_file, escape_text) 91 | 92 | def link_text_to_symbol(text, symbol, symbols, from_file=None, escape_text=True): 93 | """Link text to a symbol definition in the source tree.""" 94 | 95 | if not text: 96 | return text 97 | 98 | srcloc = symbols.lookup(symbol) 99 | return link_text_to_srcloc(text, srcloc, from_file, escape_text=escape_text) 100 | 101 | def split_text_into_symbols(text): 102 | """Split text into substrings that could be symbols.""" 103 | 104 | return re.split('([_a-zA-Z][_a-zA-Z0-9]*)', text) 105 | 106 | def link_symbols_in_text(text, symbols, from_file=None, escape_text=True): 107 | """Link symbols appearing in text to their definitions.""" 108 | 109 | if text is None: 110 | return None 111 | 112 | tokens = split_text_into_symbols(text) 113 | return ''.join( 114 | [link_text_to_symbol(tkn, tkn, symbols, from_file, escape_text) 115 | for tkn in tokens] 116 | ) 117 | 118 | ################################################################ 119 | -------------------------------------------------------------------------------- /src/cbmc_viewer/markup_summary.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | """Proof summary.""" 5 | 6 | import voluptuous 7 | import voluptuous.humanize 8 | 9 | from cbmc_viewer import templates 10 | from cbmc_viewer import util 11 | 12 | ################################################################ 13 | # Data passed to jinja to generate proof summary from summary.jinja.html 14 | 15 | VALID_SUMMARY_DATA = voluptuous.Schema({ 16 | # coverage section of the summary 17 | 'coverage': { 18 | 'overall': voluptuous.Any( 19 | { 20 | 'percentage': float, # percentage of lines hit 21 | 'hit': int, # lines hit 22 | 'total': int # lines total 23 | }, 24 | {} 25 | ), 26 | 'function': [{ 27 | 'percentage': float, # percentage of lines hit 28 | 'hit': int, # lines hit 29 | 'total': int, # lines total 30 | 'file_name': str, 31 | 'func_name': str, 32 | 'line_num': int, 33 | 34 | }] 35 | }, 36 | # warning section of the summary 37 | 'warnings': { 38 | 'expected_missing_function': [str], # function names 39 | 'unexpected_missing_function': [str], # function names 40 | 'other': [str] # warning messages 41 | }, 42 | # failure sections of the summary 43 | 'failures': { 44 | 'property': [{ # assertion failures 45 | 'prop_name': str, 46 | 'prop_desc': str, 47 | 'file_name': str, 48 | 'func_name': str, 49 | 'line_num': int, 50 | 'func_line': int 51 | }], 52 | 'loop': [{ # loop unwinding assertion failures 53 | 'loop_name': str, 54 | 'file_name': str, 55 | 'func_name': str, 56 | 'line_num': int 57 | }], 58 | 'other': [str] # unrecognized failure names 59 | } 60 | }, required=True) 61 | 62 | VALID_SUMMARY = voluptuous.Schema({ 63 | 'summary': VALID_SUMMARY_DATA, 64 | 'outdir': str # default output directory for dump() 65 | }, required=True) 66 | 67 | ################################################################ 68 | 69 | class Summary: 70 | """Proof summary. 71 | 72 | The summary merges results produced by cbmc into a single report. 73 | """ 74 | 75 | def __init__(self, coverage, symbols, results, properties, loops, 76 | config, outdir='.'): 77 | # pylint: disable=too-many-arguments 78 | 79 | self.summary = { 80 | 'coverage': { 81 | 'overall': overall_coverage(coverage), 82 | 'function': function_coverage(coverage, symbols) 83 | }, 84 | 'warnings': { 85 | 'expected_missing_function': 86 | expected_missing_functions(results, config), 87 | 'unexpected_missing_function': 88 | unexpected_missing_functions(results, config), 89 | 'other': other_warnings(results) 90 | }, 91 | 'failures': { 92 | 'property': property_failures(results, properties, symbols), 93 | 'loop': loop_failures(results, loops), 94 | 'other': other_failures(results, properties, loops) 95 | } 96 | } 97 | self.outdir = outdir 98 | self.validate() 99 | 100 | def __str__(self): 101 | """Render the proof summary as html.""" 102 | 103 | return templates.render_summary(self.summary) 104 | 105 | def validate(self): 106 | """Validate members of a summary object.""" 107 | 108 | return voluptuous.humanize.validate_with_humanized_errors( 109 | self.__dict__, VALID_SUMMARY 110 | ) 111 | 112 | def dump(self, filename=None, outdir=None): 113 | """Write the proof summary to a file rendered as html.""" 114 | 115 | util.dump(self, filename or "index.html", outdir or self.outdir) 116 | 117 | ################################################################ 118 | # Coverage data 119 | 120 | def overall_coverage(coverage): 121 | """Overall proof coverage.""" 122 | 123 | cov = coverage.overall_coverage 124 | if not cov: 125 | return {} 126 | 127 | return { 128 | 'percentage': cov['percentage'], 129 | 'hit': cov['hit'], 130 | 'total': cov['total'] 131 | } 132 | 133 | def function_coverage(coverage, symbols): 134 | """Function proof coverage.""" 135 | 136 | return [ 137 | { 138 | 'percentage': func_cov['percentage'], 139 | 'hit': func_cov['hit'], 140 | 'total': func_cov['total'], 141 | 'file_name': file_name, 142 | 'func_name': func_name, 143 | 'line_num': symbols.lookup(func_name).get("line") if symbols.lookup(func_name) else 0 144 | } 145 | for file_name, file_data in coverage.function_coverage.items() 146 | for func_name, func_cov in file_data.items() 147 | ] 148 | 149 | ################################################################ 150 | # Warning data 151 | 152 | # TODO: This classification of warnings should be done by make-results 153 | 154 | def warnings(results): 155 | """Proof warnings.""" 156 | 157 | prefixes = ["**** WARNING:", "warning:"] 158 | 159 | def strip_prefixes(string, prefixes): 160 | """Strip warning prefixes from a warning string.""" 161 | 162 | for prefix in prefixes: 163 | if string.startswith(prefix): 164 | return string[len(prefix):].strip() 165 | return string 166 | 167 | return [strip_prefixes(warning, prefixes) for warning in results.warning] 168 | 169 | def missing_functions(messages): 170 | """Names of missing functions.""" 171 | 172 | prefix = "no body for function" 173 | length = len(prefix) 174 | return [warning[length:].strip() for warning in messages 175 | if warning.startswith(prefix)] 176 | 177 | def expected_missing_functions(results, config): 178 | """Names of missing functions expected to be missing.""" 179 | 180 | return [ 181 | function for function in missing_functions(warnings(results)) 182 | if function in config.expected_missing_functions() 183 | ] 184 | 185 | def unexpected_missing_functions(results, config): 186 | """Names of missing functions not expected to be missing.""" 187 | 188 | return [ 189 | function for function in missing_functions(warnings(results)) 190 | if function not in config.expected_missing_functions() 191 | ] 192 | 193 | def other_warnings(results): 194 | """Warnings unrelated to missing functions.""" 195 | 196 | prefix = "no body for function" 197 | return [warning.strip() for warning in warnings(results) 198 | if not warning.startswith(prefix)] 199 | 200 | ################################################################ 201 | # Failure data 202 | 203 | def property_definition(prop_name, properties, symbols): 204 | """Details for a property failure.""" 205 | 206 | prop_def = properties.lookup(prop_name) 207 | if prop_def is None: 208 | return None 209 | 210 | srcloc = prop_def['location'] 211 | return { 212 | 'prop_name': prop_name, 213 | 'prop_desc': prop_def['description'], 214 | 'file_name': srcloc['file'], 215 | 'func_name': srcloc['function'] or '', 216 | 'line_num': srcloc['line'], 217 | 'func_line': 218 | # Symbol table won't contain functions modeled by CBMC (eg, strcmp) 219 | # Return a line number 0 for such functions 220 | (symbols.lookup(srcloc['function']) or {'line': 0})['line'] 221 | } 222 | 223 | def loop_definition(prop_name, loops): 224 | """Details for a loop unwinding assertion failure.""" 225 | 226 | srcloc = loops.lookup_assertion(prop_name) 227 | if srcloc is None: 228 | return None 229 | 230 | return { 231 | 'loop_name': prop_name, 232 | 'file_name': srcloc['file'], 233 | 'func_name': srcloc['function'], 234 | 'line_num': srcloc['line'] 235 | } 236 | 237 | def other_definition(prop_name, properties, loops): 238 | """Name of an unrecognized failure.""" 239 | 240 | if properties.lookup(prop_name) or loops.lookup_assertion(prop_name): 241 | return None 242 | return prop_name 243 | 244 | def filter_none(items): 245 | """Remove instances of None from a list of items.""" 246 | 247 | return [item for item in items if item is not None] 248 | 249 | def property_failures(results, properties, symbols): 250 | """Details for property failures.""" 251 | 252 | failures = results.results[False] 253 | return filter_none( 254 | [property_definition(failure, properties, symbols) 255 | for failure in failures] 256 | ) 257 | 258 | def loop_failures(results, loops): 259 | """Details for loop unwinding assertion failures.""" 260 | 261 | failures = results.results[False] 262 | return filter_none( 263 | [loop_definition(failure, loops) for failure in failures] 264 | ) 265 | 266 | def other_failures(results, properties, loops): 267 | """Names of unrecognized failures.""" 268 | 269 | failures = results.results[False] 270 | return filter_none( 271 | [other_definition(failure, properties, loops) for failure in failures] 272 | ) 273 | 274 | ################################################################ 275 | -------------------------------------------------------------------------------- /src/cbmc_viewer/markup_trace.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | """Trace annotated with debugging information.""" 5 | 6 | import html 7 | import logging 8 | import os 9 | import re 10 | 11 | import voluptuous 12 | import voluptuous.humanize 13 | 14 | from cbmc_viewer import markup_link 15 | from cbmc_viewer import srcloct 16 | from cbmc_viewer import templates 17 | from cbmc_viewer import util 18 | 19 | # Name of directory holding annotated traces 20 | TRACES = "traces" 21 | 22 | ################################################################ 23 | # Data passed to jinja to generate annotated traces from trace.jinja.html 24 | 25 | VALID_STEP = voluptuous.Schema({ 26 | 'kind': voluptuous.Any( 27 | 'function-call', 28 | 'function-return', 29 | 'variable-assignment', 30 | 'parameter-assignment', 31 | 'assumption', 32 | 'failure' 33 | ), 34 | 'num': int, 35 | 'srcloc': str, 36 | 'code': voluptuous.Any(str, None), 37 | 'cbmc': str 38 | }, required=True) 39 | 40 | VALID_TRACE = voluptuous.Schema({ 41 | 'prop_name': str, 42 | 'prop_desc': str, 43 | 'prop_srcloc': str, 44 | 'steps': [VALID_STEP], 45 | 'outdir': str 46 | }, required=True) 47 | 48 | ################################################################ 49 | # Source code fragments used to annotate traces. 50 | 51 | class CodeSnippet: 52 | """Source code fragments.""" 53 | 54 | def __init__(self, root): 55 | self.root = root # source root 56 | self.source = {} # cache mapping file name -> lines of source code 57 | 58 | def lookup(self, path, line): 59 | """A line of source code.""" 60 | 61 | if line <= 0: # line numbers are 1-based 62 | logging.info("CodeSnippet lookup: line number not positive: %s", line) 63 | return None 64 | line -= 1 # list indices are 0-based 65 | 66 | try: 67 | if path not in self.source: 68 | with open(os.path.join(self.root, path), encoding='utf-8') as code: 69 | self.source[path] = code.read().splitlines() 70 | except FileNotFoundError as error: 71 | if srcloct.is_builtin(path): # , etc. 72 | return None 73 | raise UserWarning(f"CodeSnippet lookup: file not found: {path}") from error 74 | 75 | # return the whole statement which may be broken over several lines 76 | snippet = ' '.join(self.source[path][line:line+5]) 77 | snippet = re.sub(r'\s+', ' ', snippet).strip() 78 | idx = snippet.find(';') # end of statement 79 | if idx >= 0: 80 | return html.escape(snippet[:idx+1]) 81 | idx = snippet.find('}') # end of block 82 | if idx >= 0: 83 | return html.escape(snippet[:idx+1]) 84 | return html.escape(snippet) # statement extends over more that 5 lines 85 | 86 | 87 | def lookup_srcloc(self, srcloc): 88 | """A line of source code (at a source location).""" 89 | 90 | return self.lookup(srcloc['file'], srcloc['line']) 91 | 92 | ################################################################ 93 | 94 | class Trace: 95 | """Trace annotated with debugging information.""" 96 | 97 | def __init__(self, name, trace, symbols, properties, loops, snippets, 98 | outdir='.'): 99 | self.prop_name = name 100 | self.prop_desc = properties.get_description(name) 101 | self.prop_srcloc = format_srcloc(properties.get_srcloc(name) 102 | or loops.lookup_assertion(name), 103 | symbols) 104 | self.steps = [{ 105 | 'kind': step['kind'], 106 | 'num': num+1, # convert 0-based index to 1-based line number 107 | 'srcloc': format_srcloc(step['location'], symbols), 108 | 'code': snippets.lookup_srcloc(step['location']), 109 | 'cbmc': format_step(step) 110 | } for num, step in enumerate(trace)] 111 | self.outdir = outdir 112 | self.validate() 113 | 114 | def __str__(self): 115 | """Render annotated trace as html.""" 116 | 117 | return templates.render_trace(self.prop_name, 118 | self.prop_desc, 119 | self.prop_srcloc, 120 | self.steps) 121 | 122 | def validate(self): 123 | """Validate members of an annotated trace object.""" 124 | 125 | return voluptuous.humanize.validate_with_humanized_errors( 126 | self.__dict__, VALID_TRACE 127 | ) 128 | 129 | def dump(self, filename=None, outdir=None): 130 | """Write annotated trace to a file rendered as html.""" 131 | 132 | util.dump(self, 133 | filename or self.prop_name + ".html", 134 | outdir or self.outdir) 135 | 136 | ################################################################ 137 | # Format a source location 138 | 139 | def format_srcloc(srcloc, symbols): 140 | """Format a source location for a trace step.""" 141 | 142 | if srcloc is None: 143 | return 'Function none, File none, Line none' 144 | 145 | fyle, func, line = srcloc['file'], srcloc['function'], srcloc['line'] 146 | func_srcloc = symbols.lookup(func) 147 | # Warning: next line assumes trace root is subdirectory of code root 148 | from_file = os.path.join(TRACES, 'foo.html') # any name foo.html will do 149 | # pylint: disable=consider-using-f-string 150 | return 'Function {}, File {}, Line {}'.format( 151 | markup_link.link_text_to_srcloc(func, func_srcloc, from_file), 152 | markup_link.link_text_to_file(fyle, fyle, from_file), 153 | markup_link.link_text_to_line(line, fyle, line, from_file) 154 | ) 155 | 156 | ################################################################ 157 | # Format a trace step 158 | 159 | def format_step(step): 160 | """Format a trace step.""" 161 | 162 | markup = { 163 | "function-call": format_function_call, 164 | "function-return": format_function_return, 165 | "variable-assignment": format_variable_assignment, 166 | "parameter-assignment": format_parameter_assignment, 167 | "assumption": format_assumption, 168 | "failure": format_failure 169 | }[step['kind']] 170 | return markup(step) 171 | 172 | def format_function_call(step): 173 | """Format a function call.""" 174 | 175 | name, srcloc = step['detail']['name'], step['detail']['location'] 176 | 177 | line = f'-> {markup_link.link_text_to_srcloc(name, srcloc, "./trace/trace.html")}' 178 | return line 179 | 180 | def format_function_return(step): 181 | """Format a function return.""" 182 | 183 | name, srcloc = step['detail']['name'], step['detail']['location'] 184 | line = f'<- {markup_link.link_text_to_srcloc(name, srcloc, "./trace/trace.html")}' 185 | return line 186 | 187 | def format_variable_assignment(step): 188 | """Format an assignment statement.""" 189 | 190 | asn = step['detail'] 191 | lhs, rhs, binary = asn['lhs'], asn['rhs-value'], asn['rhs-binary'] 192 | binary = f'({binary})' if binary else '' 193 | return f'{lhs} = {rhs} {binary}' 194 | 195 | def format_parameter_assignment(step): 196 | """Format an assignment of an actual to formal function argument.""" 197 | 198 | asn = step['detail'] 199 | lhs, rhs, binary = asn['lhs'], asn['rhs-value'], asn['rhs-binary'] 200 | binary = f'({binary})' if binary else '' 201 | return f'{lhs} = {rhs} {binary}' 202 | 203 | def format_assumption(step): 204 | """Format a proof assumption.""" 205 | 206 | pred = step['detail']['predicate'] 207 | return f'assumption: {pred}' 208 | 209 | def format_failure(step): 210 | """Format a proof failure.""" 211 | 212 | prop = step['detail']['property'] or "Unnamed" 213 | reason = step['detail']['reason'] or "Not given" 214 | return f'failure: {prop}: {reason}' 215 | 216 | ################################################################ 217 | -------------------------------------------------------------------------------- /src/cbmc_viewer/parse.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | """Parsing of xml and json files with limited error handling.""" 5 | 6 | from pathlib import Path 7 | import xml.etree.cElementTree as ElementTree 8 | 9 | import json 10 | import logging 11 | 12 | def parse_xml_file(xfile): 13 | """Parse an xml file.""" 14 | 15 | try: 16 | return ElementTree.parse(xfile) 17 | except (IOError, ElementTree.ParseError) as err: 18 | logging.debug("%s", err) 19 | raise UserWarning(f"Can't load xml file '{xfile}'") from None 20 | 21 | def parse_xml_string(xstr): 22 | """Parse an xml string.""" 23 | 24 | try: 25 | return ElementTree.fromstring(xstr) 26 | except ElementTree.ParseError as err: 27 | logging.debug("%s", err) 28 | raise UserWarning(f"Can't parse xml string '{xstr[:40]}...'") from None 29 | 30 | def parse_json_file(jfile): 31 | """Parse an json file.""" 32 | 33 | try: 34 | with open(jfile, encoding='utf-8') as data: 35 | return json.load(data) 36 | except (IOError, json.JSONDecodeError) as err: 37 | logging.debug("%s", err) 38 | raise UserWarning(f"Can't load json file '{jfile}' in {Path.cwd()}") from None 39 | 40 | def parse_json_string(jstr): 41 | """Parse a json string.""" 42 | 43 | try: 44 | return json.loads(jstr) 45 | except json.JSONDecodeError as err: 46 | logging.debug("%s", err) 47 | raise UserWarning(f"Can't parse json string '{jstr[:40]}...'") from None 48 | -------------------------------------------------------------------------------- /src/cbmc_viewer/propertyt.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | """CBMC properties checked during property checking. 5 | 6 | The Result module describes the results of CBMC property checking, the 7 | Coverage module describes the results of CBMC coverage checking, and 8 | the Property module describes the properties checked during property 9 | checking. 10 | """ 11 | 12 | import json 13 | import logging 14 | 15 | import voluptuous 16 | import voluptuous.humanize 17 | 18 | from cbmc_viewer import filet 19 | from cbmc_viewer import parse 20 | from cbmc_viewer import srcloct 21 | from cbmc_viewer import util 22 | 23 | JSON_TAG = "viewer-property" 24 | 25 | ################################################################ 26 | # Property validator 27 | 28 | VALID_PROPERTY_DEFINITION = voluptuous.Schema({ 29 | 'class': str, # eg, "pointer dereference" 30 | 'description': str, # eg, "pointer outside dynamic object" 31 | 'expression': str, 32 | 'location': srcloct.VALID_SRCLOC 33 | }, required=True) 34 | 35 | VALID_PROPERTY = voluptuous.Schema({ 36 | 'properties': { 37 | # property name -> property definition 38 | voluptuous.Optional(str): VALID_PROPERTY_DEFINITION 39 | } 40 | }, required=True) 41 | 42 | ################################################################ 43 | 44 | def key(name): 45 | """A key for sorting property names like function.property.index""" 46 | 47 | try: 48 | dot = name.rindex('.') 49 | return (name[:dot].lower(), int(name[dot+1:])) 50 | except ValueError: 51 | return (name, 0) 52 | 53 | ################################################################ 54 | 55 | class Property: 56 | """CBMC properties checked during property checking.""" 57 | 58 | def __init__(self, property_lists=None): 59 | """Load CBMC properties from lists of properties.""" 60 | 61 | property_lists = property_lists or [] 62 | 63 | def handle_duplicates(name, defn1, defn2): 64 | logging.warning("Found duplicate property definition: " 65 | "%s: %s %s <- %s %s", 66 | name, 67 | defn1["location"]["file"], 68 | defn1["location"]["line"], 69 | defn2["location"]["file"], 70 | defn2["location"]["line"]) 71 | 72 | self.properties = util.merge_dicts(property_lists, handle_duplicates) 73 | self.validate() 74 | 75 | def __repr__(self): 76 | """A dict representation of an property table.""" 77 | 78 | self.validate() 79 | return self.__dict__ 80 | 81 | def __str__(self): 82 | """A string representation of an property table.""" 83 | 84 | return json.dumps({JSON_TAG: self.__repr__()}, indent=2, sort_keys=True) 85 | 86 | def validate(self): 87 | """Validate properties.""" 88 | 89 | return voluptuous.humanize.validate_with_humanized_errors( 90 | self.__dict__, VALID_PROPERTY 91 | ) 92 | 93 | def dump(self, filename=None, directory=None): 94 | """Write properties to a file or stdout.""" 95 | 96 | util.dump(self, filename, directory) 97 | 98 | def names(self): 99 | """Names of known properties.""" 100 | 101 | return self.properties.keys() 102 | 103 | def lookup(self, name): 104 | """Look up all known information for named property.""" 105 | 106 | return self.properties.get(name) 107 | 108 | def get_description(self, name): 109 | """Get description of named property.""" 110 | 111 | return (self.lookup(name) or {}).get('description') or name 112 | 113 | def get_srcloc(self, name): 114 | """Get source location of named property.""" 115 | 116 | return (self.lookup(name) or {}).get('location') 117 | 118 | ################################################################ 119 | 120 | class PropertyFromJson(Property): 121 | """Load property table from the output of make-property. 122 | 123 | Given a list of json files containing property definitions 124 | produced by make-property, merge these lists into a single list 125 | of properties. 126 | """ 127 | 128 | def __init__(self, json_files): 129 | 130 | super().__init__( 131 | [parse.parse_json_file(json_file)[JSON_TAG]["properties"] 132 | for json_file in json_files] 133 | ) 134 | 135 | ################################################################ 136 | 137 | class PropertyFromCbmcJson(Property): 138 | """Load properties from output of 'cbmc --show-properties --json-ui'.""" 139 | 140 | def __init__(self, json_files, root): 141 | 142 | super().__init__( 143 | [load_cbmc_json(json_file, root) for json_file in json_files] 144 | ) 145 | 146 | def load_cbmc_json(jsonfile, root): 147 | """Load a json file produced by cbmc --show-properties --json-ui.""" 148 | 149 | json_data = parse.parse_json_file(jsonfile) 150 | assert json_data is not None 151 | 152 | # Search cbmc output for {"properties": [ PROPERTY ]} 153 | asserts = [json_map for json_map in json_data if "properties" in json_map] 154 | if len(asserts) != 1: 155 | raise UserWarning(f"Expected 1 set of properties in cbmc output, found {len(asserts)}") 156 | 157 | # Each PROPERTY a loop property and definition 158 | root = srcloct.abspath(root) 159 | return { 160 | property['name']: { 161 | 'class': property['class'], 162 | 'description': property['description'], 163 | 'expression': property['expression'], 164 | 'location': srcloct.json_srcloc(property['sourceLocation'], root) 165 | } 166 | for property in asserts[0]["properties"] 167 | } 168 | 169 | ################################################################ 170 | 171 | class PropertyFromCbmcXml(Property): 172 | """Load properties from output of 'cbmc --show-properties --xml-ui'.""" 173 | 174 | def __init__(self, xml_files, root): 175 | 176 | super().__init__( 177 | [load_cbmc_xml(xml_file, root) for xml_file in xml_files] 178 | ) 179 | 180 | def load_cbmc_xml(xmlfile, root): 181 | """Load a json file produced by cbmc --show-properties --xml-ui.""" 182 | 183 | xml_data = parse.parse_xml_file(xmlfile) 184 | assert xml_data is not None 185 | 186 | root = srcloct.abspath(root) 187 | return { 188 | property.get("name"): { 189 | "class": property.get("class"), 190 | "description": property.find("description").text, 191 | "expression": property.find("expression").text, 192 | 'location': srcloct.xml_srcloc(property.find("location"), root) 193 | } 194 | for property in xml_data.iter("property") 195 | } 196 | 197 | ################################################################ 198 | # make-property 199 | 200 | # pylint: disable=inconsistent-return-statements 201 | 202 | def fail(msg): 203 | """Log failure and raise exception.""" 204 | 205 | logging.info(msg) 206 | raise UserWarning(msg) 207 | 208 | def make_property(args): 209 | """The implementation of make-property.""" 210 | 211 | viewer_property, cbmc_property, srcdir = args.viewer_property, args.property, args.srcdir 212 | 213 | if viewer_property: 214 | if filet.all_json_files(viewer_property): 215 | return PropertyFromJson(viewer_property) 216 | fail(f"Expected json files: {viewer_property}") 217 | 218 | if cbmc_property and srcdir: 219 | if filet.all_json_files(cbmc_property): 220 | return PropertyFromCbmcJson(cbmc_property, srcdir) 221 | if filet.all_xml_files(cbmc_property): 222 | return PropertyFromCbmcXml(cbmc_property, srcdir) 223 | fail(f"Expected json files or xml files, not both: {cbmc_property}") 224 | 225 | logging.info("make-property: nothing to do: need " 226 | "cbmc property listing results (cbmc --show-properties) and --srcdir or " 227 | "--viewer-property") 228 | return Property() 229 | 230 | def make_and_save_property(args, path=None): 231 | """Make property object and write to file or stdout""" 232 | 233 | obj = make_property(args) 234 | util.save(obj, path) 235 | return obj 236 | 237 | ################################################################ 238 | -------------------------------------------------------------------------------- /src/cbmc_viewer/reachablet.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | """CBMC reachable functions""" 5 | 6 | import json 7 | import logging 8 | import os 9 | import subprocess 10 | import tempfile 11 | 12 | import voluptuous 13 | import voluptuous.humanize 14 | 15 | from cbmc_viewer import filet 16 | from cbmc_viewer import parse 17 | from cbmc_viewer import runt 18 | from cbmc_viewer import srcloct 19 | from cbmc_viewer import util 20 | 21 | JSON_TAG = 'viewer-reachable' 22 | 23 | ################################################################ 24 | # Reachable validator 25 | 26 | VALID_REACHABLE = voluptuous.Schema({ 27 | 'reachable': { 28 | # file name -> list of reachable functions 29 | voluptuous.Optional(str): [str] 30 | } 31 | }, required=True) 32 | 33 | ################################################################ 34 | 35 | class Reachable: 36 | """CBMC reachable functions.""" 37 | 38 | # TODO: again dicts -> dict 39 | 40 | def __init__(self, function_lists=None): 41 | """Load CBMC reachable functions from lists of functions.""" 42 | 43 | function_lists = function_lists or [] 44 | 45 | functions = self.merge_function_lists(function_lists) 46 | self.reachable = self.sort_function_names(functions) 47 | self.validate() 48 | 49 | def __repr__(self): 50 | """A dict representation of the reachable functions.""" 51 | 52 | self.validate() 53 | return self.__dict__ 54 | 55 | def __str__(self): 56 | """A string representation of the reachable functions.""" 57 | 58 | return json.dumps({JSON_TAG: self.__repr__()}, indent=2, sort_keys=True) 59 | 60 | def validate(self): 61 | """Validate reachable functions.""" 62 | 63 | return voluptuous.humanize.validate_with_humanized_errors( 64 | self.__dict__, VALID_REACHABLE 65 | ) 66 | 67 | def dump(self, filename=None, directory=None): 68 | """Write reachable functions to a file or stdout.""" 69 | 70 | util.dump(self, filename, directory) 71 | 72 | @staticmethod 73 | def merge_function_lists(function_lists): 74 | """Merge lists of functions into a single list.""" 75 | 76 | if len(function_lists) == 1: 77 | return function_lists[0] 78 | 79 | functions = {} 80 | for function_list in function_lists: 81 | for file_name, func_names in function_list.items(): 82 | functions[file_name] = functions.get(file_name, set()) 83 | functions[file_name] = functions[file_name].union(func_names) 84 | 85 | return functions 86 | 87 | @staticmethod 88 | def sort_function_names(functions): 89 | """Sort the names of reachable functions in a file.""" 90 | 91 | return {file_name: sorted(func_names) 92 | for file_name, func_names in functions.items()} 93 | 94 | ################################################################ 95 | 96 | class ReachableFromJson(Reachable): 97 | """Load the reachable functions from the output of make-reachable. 98 | 99 | Given a list of json files containing symbol tables produced by 100 | make-reachable, merge these function lists into a single list. 101 | """ 102 | 103 | def __init__(self, json_files): 104 | 105 | super().__init__( 106 | [parse.parse_json_file(json_file)[JSON_TAG]["reachable"] 107 | for json_file in json_files] 108 | ) 109 | 110 | ################################################################ 111 | 112 | class ReachableFromCbmcJson(Reachable): 113 | """Load functions from output of goto-analyzer --reachable-functions --json. 114 | """ 115 | 116 | def __init__(self, json_files, root): 117 | 118 | super().__init__( 119 | [load_cbmc_json(json_file, root) for json_file in json_files] 120 | ) 121 | 122 | def load_cbmc_json(json_file, root): 123 | """Load json file produced by goto-analyzer --reachable-functions --json json_file""" 124 | 125 | json_data = parse.parse_json_file(json_file) 126 | return parse_cbmc_json(json_data, root) 127 | 128 | # TODO: Does file-local name mangling mess this up? 129 | 130 | def parse_cbmc_json(json_data, root): 131 | """Parse json output of goto-analyzer --reachable-functions --json.""" 132 | 133 | root = srcloct.abspath(root) 134 | reachable = {} 135 | for function in json_data: 136 | func_name = function['function'] 137 | if func_name.startswith('__CPROVER'): 138 | continue 139 | 140 | # goto-analyzer --reachable-functions produces a malformed 141 | # entry for __CPROVER_Start with an empty string for the 142 | # function name. 143 | if not func_name: 144 | continue 145 | 146 | file_name = function.get('file') 147 | if file_name is None: 148 | logging.warning('Skipping reachable function with invalid source location: %s', 149 | function) 150 | continue 151 | 152 | file_name = srcloct.abspath(file_name) 153 | if srcloct.is_builtin(file_name): 154 | continue 155 | if not file_name.startswith(root): 156 | continue 157 | 158 | path = srcloct.relpath(file_name, root) 159 | reachable[path] = reachable.get(path, set()) 160 | reachable[path].add(func_name) 161 | 162 | return reachable 163 | 164 | ################################################################ 165 | 166 | class ReachableFromCbmcXml(Reachable): 167 | """Load functions from output of goto-analyzer --reachable-functions --xml. 168 | """ 169 | 170 | def __init__(self, xml_files, root): 171 | 172 | super().__init__([]) 173 | 174 | _, _ = xml_files, root 175 | raise UserWarning("The goto-analyzer --xml option generates text " 176 | "and not xml.") 177 | 178 | ################################################################ 179 | 180 | class ReachableFromGoto(Reachable): 181 | """Load reachable functions of a goto binary.""" 182 | 183 | def __init__(self, goto, root, cwd=None): 184 | 185 | # pylint: disable=consider-using-with 186 | data = tempfile.NamedTemporaryFile(delete=False) 187 | 188 | cmd = ["goto-analyzer", "--reachable-functions", "--json", data.name, goto] 189 | try: 190 | runt.run(cmd, cwd=cwd) 191 | except subprocess.CalledProcessError as err: 192 | raise UserWarning(f'Failed to run {cmd}: {err}') from err 193 | 194 | json_data = parse.parse_json_file(data.name) 195 | os.unlink(data.name) 196 | 197 | super().__init__( 198 | [parse_cbmc_json(json_data, root)] 199 | ) 200 | 201 | ################################################################ 202 | # make-reachable 203 | 204 | # pylint: disable=inconsistent-return-statements 205 | 206 | def fail(msg): 207 | """Log failure and raise exception.""" 208 | 209 | logging.info(msg) 210 | raise UserWarning(msg) 211 | 212 | def make_reachable(args): 213 | """The implementation of make-reachable.""" 214 | 215 | viewer_reachable, cbmc_reachable, srcdir, goto = ( 216 | args.viewer_reachable, None, args.srcdir, args.goto) 217 | 218 | if viewer_reachable: 219 | if filet.all_json_files(viewer_reachable): 220 | return ReachableFromJson(viewer_reachable) 221 | fail(f"Expected json files: {viewer_reachable}") 222 | 223 | if cbmc_reachable and srcdir: 224 | if filet.all_json_files(cbmc_reachable): 225 | return ReachableFromCbmcJson(cbmc_reachable, srcdir) 226 | if filet.all_xml_files(cbmc_reachable): 227 | return ReachableFromCbmcXml(cbmc_reachable, srcdir) 228 | fail(f"Expected json files or xml files, not both: {cbmc_reachable}") 229 | 230 | if goto and srcdir: 231 | return ReachableFromGoto(goto, srcdir) 232 | 233 | logging.info("make-reachable: nothing to do: need " 234 | "--srcdir and --goto, or " 235 | "cbmc reachable functions results (goto-analyzer --reachable-functions)" 236 | " and --srcdir, or " 237 | "--viewer-reachable") 238 | return Reachable() 239 | 240 | def make_and_save_reachable(args, path=None): 241 | """Make reachable object and write to file or stdout""" 242 | 243 | obj = make_reachable(args) 244 | util.save(obj, path) 245 | return obj 246 | 247 | ################################################################ 248 | -------------------------------------------------------------------------------- /src/cbmc_viewer/report.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | """Assemble the full report for cbmc viewer.""" 5 | 6 | import logging 7 | import os 8 | import shutil 9 | 10 | import pkg_resources 11 | 12 | from cbmc_viewer import markup_code 13 | from cbmc_viewer import markup_summary 14 | from cbmc_viewer import markup_trace 15 | 16 | PACKAGE = 'cbmc_viewer' 17 | VIEWER_JS = 'viewer.js' 18 | VIEWER_CSS = 'viewer.css' 19 | 20 | def progress_default(string): 21 | """A default method for logging progress.""" 22 | 23 | logging.info(string) 24 | 25 | def report(config, sources, symbols, results, coverage, traces, properties, 26 | loops, report_dir='.', progress=progress_default): 27 | """Assemble the full report for cbmc viewer.""" 28 | 29 | # The report is assembled from many sources of data 30 | # pylint: disable=too-many-locals 31 | 32 | # Some code depends on these definitions 33 | # * links to traces in summary produced with jinja summary template 34 | # * links to sources in traces produced by markup_trace 35 | # * links to css and js in traces produced with jinja trace template 36 | code_dir = report_dir 37 | trace_dir = os.path.join(report_dir, markup_trace.TRACES) 38 | 39 | os.makedirs(report_dir, exist_ok=True) 40 | shutil.copy(pkg_resources.resource_filename(PACKAGE, VIEWER_CSS), 41 | report_dir) 42 | shutil.copy(pkg_resources.resource_filename(PACKAGE, VIEWER_JS), 43 | report_dir) 44 | 45 | progress("Preparing report summary") 46 | markup_summary.Summary( 47 | coverage, symbols, results, properties, loops, config).dump( 48 | outdir=report_dir) 49 | progress("Preparing report summary", True) 50 | 51 | progress("Annotating source tree") 52 | for path in sources.files: 53 | markup_code.Code(sources.root, path, symbols, coverage).dump( 54 | outdir=code_dir) 55 | progress("Annotating source tree", True) 56 | 57 | progress("Annotating traces") 58 | snippets = markup_trace.CodeSnippet(sources.root) 59 | for name, trace in traces.traces.items(): 60 | markup_trace.Trace(name, trace, symbols, properties, loops, snippets).dump( 61 | outdir=trace_dir) 62 | progress("Annotating traces", True) 63 | -------------------------------------------------------------------------------- /src/cbmc_viewer/runt.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | """Run a command with simple error handling.""" 5 | 6 | import logging 7 | import subprocess 8 | import sys 9 | 10 | def run(cmd, cwd=None, ignored=None, encoding=None): 11 | """Run command cmd in directory cwd. 12 | 13 | The argument 'ignored' may be a list of integers giving command 14 | return codes that are acceptable and can be ignored. 15 | 16 | The argument 'encoding' is a character encoding for the text 17 | string captured as stdout and stderr. The default encoding for 18 | modern Python is utf-8, but source code written on Windows 19 | platforms uses a different character encoding. In this case, 20 | 'latin1' is a reasonable choice, since it agrees with utf-8 on the 21 | ascii character set. The wikipedia page on latin1 goes so far as 22 | to say latin1 is "often assumed to be the encoding of 8-bit text 23 | on Unix and Microsoft Windows...". 24 | """ 25 | 26 | kwds = { 27 | 'cwd': cwd, 28 | 'stdout': subprocess.PIPE, 29 | 'stderr': subprocess.PIPE, 30 | 'universal_newlines': True, 31 | } 32 | 33 | # encoding keyword argument was introduced in Python 3.6 34 | if sys.version_info >= (3, 6): 35 | kwds['encoding'] = encoding 36 | 37 | logging.debug('run: cmd: %s', ' '.join(cmd)) 38 | logging.debug('run: kwds: %s', kwds) 39 | 40 | result = subprocess.run(cmd, **kwds, check=False) 41 | 42 | if result.returncode: 43 | logging.debug('Failed to run command: %s', ' '.join(cmd)) 44 | logging.debug('Failed return code: %s', result.returncode) 45 | logging.debug('Failed stdout: %s', result.stdout) 46 | logging.debug('Failed stderr: %s', result.stderr) 47 | if ignored is None or result.returncode not in ignored: 48 | result.check_returncode() 49 | logging.debug('Ignoring failure to run command: %s', cmd) 50 | 51 | return result.stdout 52 | 53 | def popen(cmd, cwd=None, stdin=None, encoding=None): 54 | """Run a command with string stdin on stdin, return stdout and stderr.""" 55 | 56 | cmd = [str(word) for word in cmd] 57 | kwds = {'cwd': cwd, 58 | 'text': True, 59 | 'stdin': subprocess.PIPE, 60 | 'stdout': subprocess.PIPE, 61 | 'stderr': subprocess.PIPE} 62 | if sys.version_info >= (3, 6): # encoding is new in Python 3.6 63 | kwds['encoding'] = encoding or 'utf-8' 64 | try: 65 | logging.debug('Command for popen: "%s"', ' '.join(cmd)) 66 | logging.debug('Command stdin: "%s"', stdin) 67 | with subprocess.Popen(cmd, **kwds) as pipe: 68 | stdout, stderr = pipe.communicate(input=stdin) 69 | logging.debug('Command stdout: "%s"', stdout) 70 | logging.debug('Command stderr: "%s"', stderr) 71 | if pipe.returncode: 72 | logging.debug('Command failed "%s"', ' '.join(cmd)) 73 | logging.debug('Command return code: "%s"', pipe.returncode) 74 | raise UserWarning(f"Failed to run command: {' '.join(cmd)}") 75 | return stdout, stderr 76 | except FileNotFoundError as error: 77 | logging.debug("FileNotFoundError: command '%s': %s", ' '.join(cmd), error) 78 | raise UserWarning(f"Failed to run command: {' '.join(cmd)}") from error 79 | -------------------------------------------------------------------------------- /src/cbmc_viewer/symbol_table.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | """Symbol table parser for a goto binary symbol table.""" 5 | 6 | import logging 7 | import os 8 | import re 9 | 10 | from cbmc_viewer import runt 11 | from cbmc_viewer import srcloct 12 | 13 | def symbol_table(goto): 14 | """Extract symbol table from goto binary as lines of text.""" 15 | 16 | # The --show-symbol-table flag produces a sequence of symbol 17 | # definitions. Definitions are separated by blank lines. Each 18 | # definition is a sequence of lines including 19 | # 20 | # Symbol......: symbol_name 21 | # Pretty name.: simple_symbol_name 22 | # Location....: file file_name line line_number 23 | 24 | # Code with definitions like 25 | # static const char *UTF_16_BE_BOM = "\xFE\xFF"; 26 | # will produce values in the symbol table that cannot be read as 27 | # strings of UTF-8 characters. We read the symbol table using the 28 | # latin1 encoding in place of the Python default UTF-8 encoding, 29 | # since latin1 agrees with UTF-8 on the ASCII characters. 30 | 31 | cmd = ['cbmc', '--show-symbol-table', goto] 32 | definitions = re.split(r'[\n\r][\n\r]+', runt.run(cmd, encoding='latin1')) 33 | return [definition.strip().splitlines() for definition in definitions] 34 | 35 | def is_symbol_line(line): 36 | """Line from symbol table defines a symbol name.""" 37 | 38 | return line.startswith('Symbol.') # Match Symbol. not Symbols: 39 | 40 | def is_location_line(line): 41 | """Line from symbol table defines a location of a symbol definition.""" 42 | 43 | return line.startswith('Location') 44 | 45 | def is_pretty_name_line(line): 46 | """Line from symbol table defines a simple symbol name (a pretty name).""" 47 | 48 | return line.startswith('Pretty name') 49 | 50 | def parse_symbol(sym): 51 | """Symbol name from symbol line.""" 52 | 53 | if not is_symbol_line(sym): 54 | return None 55 | 56 | # Examples of symbol lines 57 | # Symbol......: function_name 58 | # Symbol......: function_name$link1 59 | # Symbol......: function_name::parameter_name 60 | # Symbol......: function_name::1::1::variable_name 61 | # Symbol......: tag-struct_name 62 | # Symbol......: tag-union_name 63 | 64 | name = sym.split(":", 1)[1].strip() 65 | if name.startswith('tag-'): 66 | return name[len('tag-'):] 67 | if name: 68 | return name 69 | return None 70 | 71 | def parse_location(loc, wkdir): 72 | """Symbol source location from location line.""" 73 | 74 | if not is_location_line(loc): 75 | return None, None 76 | 77 | # Examples of location lines (may be no location for symbol) 78 | # Location....: 79 | # Location....: file file_name line line_number 80 | 81 | match = re.match('.* file (.*) line ([0-9]*)', loc) 82 | if match is None: 83 | return None, None 84 | 85 | rel_path, line = match.group(1), int(match.group(2)) 86 | abs_path = srcloct.normpath(os.path.join(wkdir, rel_path)) 87 | if srcloct.is_builtin(abs_path): 88 | return None, None 89 | 90 | return abs_path, line 91 | 92 | def parse_pretty_name(sym): 93 | """Symbols pretty name from pretty name line.""" 94 | 95 | if not is_pretty_name_line(sym): 96 | return None 97 | 98 | # Examples of pretty name lines (may be no pretty name for symbol) 99 | # Pretty name.: 100 | # Pretty name.: function_name 101 | # Pretty name.: function_name::1::1::variable_name 102 | # Pretty name.: struct struct_name 103 | # Pretty name.: union union_name 104 | 105 | name = sym.split(":", 1)[1].strip() 106 | if name.startswith('struct '): 107 | return name[len('struct '):] 108 | if name.startswith('union '): 109 | return name[len('union '):] 110 | if name: 111 | return name 112 | return None 113 | 114 | def parse_symbol_table(definitions, wkdir): 115 | """Extract symbols and source locations from symbol table definitions.""" 116 | 117 | def nonnull(items): 118 | nonnull_items = [item for item in items if item is not None] 119 | if nonnull_items: 120 | return nonnull_items[0] 121 | return None 122 | 123 | def nonnull_pair(items): 124 | nonnull_items = [item for item in items if item is not None 125 | and (item[0] is not None or item[1] is not None)] 126 | if nonnull_items: 127 | return nonnull_items[0] 128 | return None, None 129 | 130 | def symbol(dfn): 131 | return nonnull([parse_symbol(line) for line in dfn]) 132 | 133 | def pretty(dfn): 134 | return nonnull([parse_pretty_name(line) for line in dfn]) 135 | 136 | def location(dfn, wkdir): 137 | return nonnull_pair([parse_location(line, wkdir) for line in dfn]) 138 | 139 | def parse_definition(dfn, wkdir): 140 | loc = location(dfn, wkdir) 141 | return { 142 | 'symbol': pretty(dfn) or symbol(dfn), 143 | 'file': loc[0], 144 | 'line': loc[1] 145 | } 146 | 147 | return [parse_definition(dfn, wkdir) for dfn in definitions] 148 | 149 | def source_files(goto, wkdir, srcdir=None): 150 | """Source files appearing in symbol table. 151 | 152 | Source file path names in symbol table are absolute or relative to 153 | wkdir. If srcdir is given, return only files under srcdir. 154 | """ 155 | 156 | wkdir = srcloct.abspath(wkdir) 157 | srcs = [dfn['file'] 158 | for dfn in parse_symbol_table(symbol_table(goto), wkdir)] 159 | srcs = [src for src in srcs if src and not srcloct.is_builtin(src)] 160 | 161 | if srcdir: 162 | srcdir = srcloct.abspath(srcdir) 163 | srcs = [src for src in srcs if src.startswith(srcdir)] 164 | 165 | return sorted(set(srcs)) 166 | 167 | def symbol_definitions(goto, wkdir, srcdir=None): 168 | """Symbol definitions appearing in symbol table. 169 | 170 | Source file path names in symbol table are absolute or relative to 171 | wkdir. If srcdir is given, return only symbols defined in files 172 | under srcdir. 173 | """ 174 | 175 | wkdir = srcloct.abspath(wkdir) 176 | srcdir = srcloct.abspath(srcdir) 177 | 178 | symbols = {} 179 | for dfn in parse_symbol_table(symbol_table(goto), wkdir): 180 | sym, src, num = dfn['symbol'], dfn['file'], dfn['line'] 181 | 182 | if sym is None or src is None or num is None: 183 | logging.info("Skipping symbol table entry: %s: %s, %s", 184 | sym, src, num) 185 | continue 186 | 187 | if srcdir and not src.startswith(srcdir): 188 | logging.info("Skipping symbol table entry: %s: %s, %s", 189 | sym, src, num) 190 | continue 191 | 192 | srcloc = srcloct.make_srcloc(src, None, num, wkdir, srcdir) 193 | if sym in symbols and srcloc != symbols[sym]: 194 | logging.info("Skipping redefinition of symbol name: %s", sym) 195 | logging.info(" Old symbol %s: file %s, line %s", 196 | sym, symbols[sym]["file"], symbols[sym]["line"]) 197 | logging.info(" New symbol %s: file %s, line %s", 198 | sym, srcloc["file"], srcloc["line"]) 199 | continue 200 | symbols[sym] = srcloc 201 | 202 | return symbols 203 | -------------------------------------------------------------------------------- /src/cbmc_viewer/templates.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | """Jinja templates.""" 5 | 6 | import jinja2 7 | from jinja2 import select_autoescape 8 | 9 | import pkg_resources 10 | 11 | PACKAGE = 'cbmc_viewer' 12 | TEMPLATES = 'templates' 13 | 14 | SUMMARY_TEMPLATE = 'summary.jinja.html' 15 | CODE_TEMPLATE = 'code.jinja.html' 16 | TRACE_TEMPLATE = 'trace.jinja.html' 17 | 18 | ENV = None 19 | 20 | def env(): 21 | """The jinja environment.""" 22 | 23 | # pylint: disable=global-statement 24 | global ENV 25 | 26 | if ENV is None: 27 | template_dir = pkg_resources.resource_filename(PACKAGE, TEMPLATES) 28 | ENV = jinja2.Environment( 29 | loader=jinja2.FileSystemLoader(template_dir), 30 | autoescape=select_autoescape( 31 | enabled_extensions=('html'), 32 | default_for_string=True) 33 | ) 34 | return ENV 35 | 36 | def render_summary(summary): 37 | """Render summary as html.""" 38 | 39 | return env().get_template(SUMMARY_TEMPLATE).render( 40 | summary=summary 41 | ) 42 | 43 | def render_code(filename, path_to_root, lines): 44 | """Render annotated source code as html.""" 45 | 46 | return env().get_template(CODE_TEMPLATE).render( 47 | filename=filename, path_to_root=path_to_root, lines=lines 48 | ) 49 | 50 | def render_trace(name, desc, srcloc, steps): 51 | """Render annotated trace as html.""" 52 | 53 | return env().get_template(TRACE_TEMPLATE).render( 54 | prop_name=name, prop_desc=desc, prop_srcloc=srcloc, steps=steps 55 | ) 56 | -------------------------------------------------------------------------------- /src/cbmc_viewer/templates/code.jinja.html: -------------------------------------------------------------------------------- 1 | {% import 'link.jinja.html' as link %} 2 | 3 | 4 | {{filename}} 5 | {{link.to_viewer_css(path_to_root)}} 6 | 7 | 8 | 9 |
10 | {% for line in lines -%} 11 |
{{"{:>5}".format(line.num)}} {{line.code}}
12 | {%- endfor %} 13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /src/cbmc_viewer/templates/link.jinja.html: -------------------------------------------------------------------------------- 1 | {%- macro to_file(file_name, text, root='.') -%} 2 | {%- if file_name.startswith("<") and file_name.endswith(">") -%} 3 | {{text|e}} 4 | {%- else -%} 5 | {{text|e}} 6 | {%- endif -%} 7 | {%- endmacro -%} 8 | 9 | {%- macro to_line(file_name, line_num, text, root='.') -%} 10 | {%- if file_name.startswith("<") and file_name.endswith(">") -%} 11 | {{text|e}} 12 | {%- else -%} 13 | {{text|e}} 14 | {%- endif -%} 15 | {%- endmacro -%} 16 | 17 | {%- macro to_file_in_code(file_name, text, root='.') -%} 18 | {%- if file_name.startswith("<") and file_name.endswith(">") -%} 19 | {{text|e}} 20 | {%- else -%} 21 | {{text|e}} 22 | {%- endif -%} 23 | {%- endmacro -%} 24 | 25 | {%- macro to_line_in_code(file_name, line_num, text, root='.') -%} 26 | {%- if file_name.startswith("<") and file_name.endswith(">") -%} 27 | {{text|e}} 28 | {%- else -%} 29 | {{text|e}} 30 | {%- endif -%} 31 | {%- endmacro -%} 32 | 33 | {%- macro to_trace(trace_name, text='trace', root='.') -%} 34 | {{text|e}} 35 | {%- endmacro -%} 36 | 37 | {%- macro to_viewer_css(root='.') -%} 38 | 39 | {%- endmacro -%} 40 | 41 | {%- macro to_viewer_js(root='.') -%} 42 | 43 | {%- endmacro -%} 44 | -------------------------------------------------------------------------------- /src/cbmc_viewer/templates/summary.jinja.html: -------------------------------------------------------------------------------- 1 | {% import 'link.jinja.html' as link %} 2 | 3 | 4 | 5 | CBMC 6 | {{link.to_viewer_css(path_to_root)}} 7 | 8 | 9 | 10 |

CBMC report

11 | 12 |
13 |

Coverage

14 | {% set coverage = summary.coverage %} 15 | 16 | {% if coverage.overall %} 17 | {% set overall = coverage.overall %} 18 |

19 | Coverage: {{'{:.2f}'.format(overall.percentage)}} (reached {{overall.hit}} of {{overall.total}} reachable lines) 20 |

21 | {% endif %} 22 | 23 | {% if coverage.function %} 24 | {% set function = coverage.function %} 25 | 26 | 27 | 28 | 29 | 30 | 31 | {% for func in function|sort(attribute="func_name")|sort(attribute="file_name")|sort(attribute="percentage", reverse=True) %} 32 | 33 | 34 | 35 | 36 | 37 | {% endfor %} 38 |
CoverageFunctionFile
{{'{:.2f}'.format(func.percentage)}} ({{func.hit}}/{{func.total}}){{link.to_line(func.file_name, func.line_num, func.func_name)}}{{link.to_file(func.file_name, func.file_name)}}
39 |
40 | {% endif %} 41 | 42 | {% if not coverage.overall and not coverage.function %} 43 | None 44 | {% endif %} 45 | 46 |
47 |

Warnings

48 | {% set warnings = summary.warnings %} 49 | 50 | {% if warnings.expected_missing_function %} 51 |

52 | Functions omitted from test (expected): 53 |

54 |
    55 | {% for func in warnings.expected_missing_function|sort %} 56 |
  • {{func}}
  • 57 | {% endfor %} 58 |
59 | {% endif %} 60 | 61 | {% if warnings.unexpected_missing_function %} 62 |

63 | Functions omitted from test (unexpected): 64 |

65 |
    66 | {% for func in warnings.unexpected_missing_function|sort %} 67 |
  • {{func}}
  • 68 | {% endfor %} 69 |
70 | {% endif %} 71 | 72 | {% if warnings.other %} 73 |

74 | Other warnings: 75 |

76 |
    77 | {% for warning in warnings.other|sort %} 78 |
  • {{warning}}
  • 79 | {% endfor %} 80 |
81 | {% endif %} 82 | 83 | {% if not warnings.expected_missing_function and 84 | not warnings.unexpected_missing_function and 85 | not warnings.other %} 86 | None 87 | {% endif %} 88 | 89 |
90 |

Errors

91 | {% set errors = summary.failures %} 92 | 93 | {% if errors.loop %} 94 |
    95 |
  • Loop unwinding failures 96 |
      97 | {% for error in errors.loop %} 98 |
    • [{{link.to_trace(error.loop_name)}}] 99 | {{error.loop_name}} 100 | in line 101 | {{link.to_line(error.file_name, error.line_num, error.line_num)}} 102 | in file 103 | {{link.to_file(error.file_name, error.file_name)}} 104 |
    • 105 | {% endfor %} 106 |
    107 |
  • 108 |
109 | {% endif %} 110 | 111 | {% if errors.other %} 112 |
    113 |
  • Other failures 114 |
      115 | {% for error in errors.other %} 116 |
    • {{error}}
    • 117 | {% endfor %} 118 |
    119 |
  • 120 |
121 | {% endif %} 122 | 123 | {% if errors.property %} 124 |
    125 | {% for file_name, file_errors in errors.property|groupby("file_name") %} 126 |
  • File {{link.to_file(file_name, file_name)}} 127 |
      128 | {% for func_name, func_errors in file_errors|groupby("func_name") %} 129 | {% set func_line = func_errors[0]["func_line"] %} 130 |
    • 131 | Function {{link.to_line(file_name, func_line, func_name)}} 132 |
        133 | {% for line_num, line_errors in func_errors|groupby("line_num") %} 134 |
      • 135 | Line {{link.to_line(file_name, line_num, line_num)}} 136 |
          137 | {% for error in line_errors|sort(attribute="prop_name") %} 138 |
        • 139 | [{{link.to_trace(error.prop_name)}}] 140 | {{error.prop_desc}} 141 |
        • 142 | {% endfor %} {# end for error #} 143 |
        144 |
      • 145 | {% endfor %} {# end for line number #} 146 |
      147 |
    • 148 | {% endfor %} {# end for function #} 149 |
    150 |
  • 151 | {% endfor %} {# end for file #} 152 |
153 | {% endif %} 154 | 155 | {% if not errors.loop and 156 | not errors.other and 157 | not errors.property %} 158 | None 159 | {% endif %} 160 | 161 |
162 | 163 | 164 | -------------------------------------------------------------------------------- /src/cbmc_viewer/templates/trace.jinja.html: -------------------------------------------------------------------------------- 1 | {% import 'link.jinja.html' as link %} 2 | 3 | 4 | {{prop_name}} 5 | {{link.to_viewer_css('..')}} 6 | 7 | 8 | 9 |

Error trace for property {{prop_name}}

10 | 11 |
12 |
13 |

14 | Property that failed: 15 |

    16 |
  • Name: {{prop_name}} 17 |
  • Description: {{prop_desc}} 18 |
  • Location: {{prop_srcloc}} 19 |
20 |

21 |
22 | 23 |
24 |

25 | Trace navigation tips: 26 |

    27 |
  • 28 | Shift-Click: Hide function steps: 29 | Shift-click on any step of a function to toggle the display 30 | of function steps (hide or display steps). 31 | This corresponds to stepping over a function in a debugger. 32 | Example: Shift-click on any step of malloc to hide 33 | the initialization of the struct members being malloc'd. 34 |
  • 35 |
  • 36 | Shift-Alt-Click: Focus on function steps: 37 | Shift-Alt-click on any step of a function to hide all steps 38 | of all functions not currently on the call stack. 39 | This corresponds to viewing the call stack in a debugger. 40 | Example: Shift-Alt-click on a failure step to the display the 41 | most direct path from the initial state to the failure. 42 |
  • 43 |
44 |

45 |
46 |
47 | 48 |
49 | 50 | {% for step in steps %} 51 | 52 | {% if step.kind == 'function-call' %} 53 |
54 |
55 | {% endif %} 56 | 57 | {% if step.kind == 'function-return' %} 58 |
{# end function-body #} 59 |
60 | {% endif %} 61 | 62 |
63 |
Step {{step.num}}: {{step.srcloc}}
64 | {% if step.code %} 65 |
{{step.code}}
66 | {% endif %} 67 |
{{step.cbmc}}
68 |
69 | 70 | {% if step.kind == 'function-call' %} 71 |
{# end function-call #} 72 |
73 | {% endif %} 74 | 75 | {% if step.kind == 'function-return' %} 76 |
{# end function-return #} 77 |
{# end function #} 78 | {% endif %} 79 | 80 | {% endfor %} 81 |
82 | 83 | {{link.to_viewer_js('..')}} 84 | 85 | -------------------------------------------------------------------------------- /src/cbmc_viewer/util.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | """Miscellaneous functions.""" 5 | 6 | import logging 7 | import os 8 | 9 | ################################################################ 10 | 11 | def flatten(groups): 12 | """Flatten a list of lists to a single list""" 13 | 14 | return [item 15 | for group in groups if group is not None 16 | for item in group if item is not None] 17 | 18 | def choose(items): 19 | """Choose an item from a list (use a deterministic choice)""" 20 | 21 | items = sorted({item for item in items if item is not None}) 22 | if len(items) != 1: 23 | logging.debug("No unique element in %s", items) 24 | return items[0] if items else None 25 | 26 | ################################################################ 27 | 28 | def merge_dicts(dicts, handle_duplicate=None): 29 | """Merge a list of dictionaries. 30 | 31 | Invoke handle_duplicate(key, val1, val2) when two dicts maps the 32 | same key to different values val1 and val2, maybe logging the 33 | duplication. 34 | """ 35 | 36 | if not dicts: 37 | return {} 38 | 39 | if len(dicts) == 1: 40 | return dicts[0] 41 | 42 | if handle_duplicate is None: 43 | return {key: val for dict_ in dicts for key, val in dict_.items()} 44 | 45 | result = {} 46 | for dict_ in dicts: 47 | for key, val in dict_.items(): 48 | if key in result and val != result[key]: 49 | handle_duplicate(key, result[key], val) 50 | continue 51 | result[key] = val 52 | return result 53 | 54 | ################################################################ 55 | 56 | def dump(data, filename=None, directory='.'): 57 | """Write data to a file or stdout.""" 58 | 59 | # directory defaults to '.' even if dump is called with directory=None 60 | directory = directory or '.' 61 | 62 | if filename: 63 | path = os.path.normpath(os.path.join(directory, filename)) 64 | os.makedirs(os.path.dirname(path), exist_ok=True) 65 | with open(path, 'w', encoding='utf-8') as fileobj: 66 | print(data, file=fileobj) 67 | else: 68 | print(data) 69 | 70 | def save(obj, path=None): 71 | """Save an object to a file or to stdout""" 72 | 73 | if path is None: 74 | print(obj) 75 | return 76 | 77 | with open(path, 'w', encoding='utf-8') as output: 78 | print(obj, file=output) 79 | return 80 | 81 | ################################################################ 82 | -------------------------------------------------------------------------------- /src/cbmc_viewer/version.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | """Version number.""" 5 | 6 | NAME = "CBMC viewer" 7 | NUMBER = "3.11.1" 8 | VERSION = f"{NAME} {NUMBER}" 9 | 10 | def version(display=False): 11 | """The version of cbmc viewer.""" 12 | 13 | if display: 14 | print(VERSION) 15 | return VERSION 16 | -------------------------------------------------------------------------------- /src/cbmc_viewer/viewer.css: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | .coverage table td 7 | { 8 | padding-right: 1em; 9 | } 10 | 11 | .coverage table th 12 | { 13 | padding-right: 1em; 14 | text-align: left; 15 | } 16 | 17 | /****************************************************************/ 18 | 19 | .code .line 20 | { 21 | font-family: monospace; 22 | white-space: pre; 23 | } 24 | 25 | .code .line.hit, 26 | .code .line.hit a 27 | { 28 | color: green; 29 | } 30 | 31 | .code .line.missed, 32 | .code .line.missed a 33 | { 34 | color: red; 35 | } 36 | 37 | .code .line.both, 38 | .code .line.both a 39 | { 40 | color: orange; 41 | } 42 | 43 | /****************************************************************/ 44 | 45 | .trace 46 | { 47 | margin-top: 1em; 48 | margin-bottom: 1em; 49 | border-style: solid; 50 | border-width: 1px; 51 | padding-left: 1em; 52 | padding-right: 1em; 53 | } 54 | 55 | .trace .step 56 | { 57 | margin-top: 1em; 58 | margin-bottom: 1em; 59 | } 60 | 61 | .trace .step .cbmc, 62 | .trace .step .header, 63 | .trace .step .code 64 | { 65 | font-family: monospace; 66 | white-space: pre; 67 | } 68 | 69 | .trace .step .cbmc, 70 | .trace .step .code 71 | { 72 | margin-left: 1em; 73 | } 74 | 75 | .trace .function-body 76 | { 77 | border-left-width: 1px; 78 | border-left-style: solid; 79 | border-left-color: blue; 80 | margin-left: 1em; 81 | padding-left: .5em; 82 | } 83 | 84 | .trace .function-body.hidden 85 | { 86 | display: none; 87 | } 88 | 89 | /****************************************************************/ 90 | 91 | #trace-header 92 | { 93 | border-style: solid; 94 | border-width: 1px; 95 | padding-left: 1em; 96 | padding-right: 1em; 97 | } 98 | 99 | #trace-navigation ul, 100 | #trace-description ul { 101 | margin-block-start: 0px; 102 | } 103 | 104 | #trace-navigation p, 105 | #trace-description p { 106 | margin-block-end: 0px; 107 | } 108 | -------------------------------------------------------------------------------- /src/cbmc_viewer/viewer.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | /****************************************************************/ 7 | 8 | function isFunction(element) { 9 | return element.classList && element.classList.contains("function"); 10 | } 11 | 12 | function isFunctionCall(element) { 13 | return element.classList && element.classList.contains("function-call"); 14 | } 15 | 16 | function isFunctionBody(element) { 17 | return element.classList && element.classList.contains("function-body"); 18 | } 19 | 20 | function isFunctionReturn(element) { 21 | return element.classList && element.classList.contains("function-return"); 22 | } 23 | 24 | function isHidden(element) { 25 | return element.classList && element.classList.contains("hidden"); 26 | } 27 | 28 | /****************************************************************/ 29 | 30 | function hideFunctionBody(element) { 31 | if (isFunctionBody(element)) { 32 | element.classList.add("hidden"); 33 | } 34 | } 35 | 36 | function showFunctionBody(element) { 37 | if (isFunctionBody(element)) { 38 | element.classList.remove("hidden"); 39 | } 40 | } 41 | 42 | function toggleFunctionBody(element) { 43 | if (isFunctionBody(element)) { 44 | element.classList.toggle("hidden"); 45 | } 46 | } 47 | 48 | /****************************************************************/ 49 | 50 | function hideFunction(element) { 51 | if (isFunction(element)) { 52 | for (elt of element.children) { 53 | hideFunctionBody(elt); 54 | } 55 | } 56 | } 57 | 58 | function showFunction(element) { 59 | if (isFunction(element)) { 60 | for (elt of element.children) { 61 | showFunctionBody(elt); 62 | } 63 | } 64 | } 65 | 66 | function toggleFunction(element) { 67 | if (isFunction(element)) { 68 | for (elt of element.children) { 69 | toggleFunctionBody(elt); 70 | } 71 | } 72 | } 73 | 74 | function eventToggleFunction(event) { 75 | if (event.shiftKey && !event.altKey) { 76 | toggleFunction(this); 77 | event.stopPropagation(); 78 | } 79 | } 80 | 81 | for (elt of document.getElementsByClassName("function")) { 82 | elt.addEventListener("click", eventToggleFunction); 83 | } 84 | 85 | for (elt of document.getElementsByClassName("step")) { 86 | elt.addEventListener("click", eventHideSiblings); 87 | } 88 | 89 | /****************************************************************/ 90 | 91 | function hideSiblings(element) { 92 | for (elt of element.parentElement.children) { 93 | if (elt != element) { 94 | hideFunction(elt); 95 | } 96 | } 97 | } 98 | 99 | function eventHideSiblings(element) { 100 | if (event.shiftKey && event.altKey) { 101 | hideSiblings(this); 102 | } 103 | } 104 | 105 | for (elt of document.getElementsByClassName("function")) { 106 | elt.addEventListener("click", eventHideSiblings); 107 | } 108 | 109 | /****************************************************************/ 110 | -------------------------------------------------------------------------------- /src/cbmc_viewer/viewer.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | """Report the results of cbmc. 5 | 6 | Report the results of cbmc with annotated source files indiciating 7 | line coverage, with coverage reports for statically reachable functions, 8 | and with lists of property violations and traces for each violation. 9 | """ 10 | 11 | from pathlib import Path 12 | import datetime 13 | import logging 14 | import os 15 | import sys 16 | 17 | from cbmc_viewer import configt 18 | from cbmc_viewer import coveraget 19 | from cbmc_viewer import loopt 20 | from cbmc_viewer import propertyt 21 | from cbmc_viewer import reachablet 22 | from cbmc_viewer import report 23 | from cbmc_viewer import resultt 24 | from cbmc_viewer import sourcet 25 | from cbmc_viewer import symbolt 26 | from cbmc_viewer import tracet 27 | 28 | ################################################################ 29 | 30 | class Timer: 31 | """A simple timer for display in progress information.""" 32 | 33 | def __init__(self): 34 | self.time = datetime.datetime.now() 35 | 36 | def reset(self): 37 | """Reset timer to current time.""" 38 | 39 | self.time = datetime.datetime.now() 40 | 41 | def elapsed(self): 42 | """Reset timer and return time elapsed since last reset.""" 43 | 44 | old = self.time 45 | self.time = datetime.datetime.now() 46 | return (self.time - old).total_seconds() 47 | 48 | TIMER = Timer() 49 | 50 | def progress(msg, done=False): 51 | """Display local progress for a single step.""" 52 | 53 | if done: 54 | time = TIMER.elapsed() 55 | logging.info("viewer: %s...done (%s sec)", msg, time) 56 | else: 57 | TIMER.reset() 58 | logging.info("viewer: %s...", msg) 59 | 60 | GLOBAL_TIMER = Timer() 61 | 62 | def global_progress(msg, done=False): 63 | """Display overall progress for all steps.""" 64 | 65 | if done: 66 | time = GLOBAL_TIMER.elapsed() 67 | logging.info("viewer: %s...done (%s sec)", msg, time) 68 | else: 69 | GLOBAL_TIMER.reset() 70 | logging.info("viewer: %s...", msg) 71 | 72 | ################################################################ 73 | 74 | def viewer(args): 75 | """Construct the cbmc report.""" 76 | 77 | if not (args.result or args.viewer_result or args.coverage or args.viewer_coverage): 78 | logging.error("Need property checking results or coverage checking results.") 79 | sys.exit(1) 80 | 81 | global_progress("CBMC viewer") 82 | 83 | htmldir = os.path.join(args.reportdir, "html") 84 | jsondir = os.path.join(args.reportdir, "json") 85 | os.makedirs(htmldir, exist_ok=True) 86 | os.makedirs(jsondir, exist_ok=True) 87 | jsondir = Path(jsondir) 88 | 89 | progress("Scanning property checking results") 90 | results = resultt.make_and_save_result(args, jsondir / 'viewer-result.json') 91 | progress("Scanning property checking results", True) 92 | 93 | progress("Scanning error traces") 94 | traces = tracet.make_and_save_trace(args, jsondir / 'viewer-trace.json') 95 | progress("Scanning error traces", True) 96 | 97 | progress("Scanning coverage data") 98 | coverage = coveraget.make_and_save_coverage(args, jsondir / 'viewer-coverage.json') 99 | progress("Scanning coverage data", True) 100 | 101 | progress("Scanning loop definitions") 102 | loops = loopt.make_and_save_loop(args, jsondir / 'viewer-loop.json') 103 | progress("Scanning loop definitions", True) 104 | 105 | progress("Scanning properties") 106 | properties = propertyt.make_and_save_property(args, jsondir / 'viewer-property.json') 107 | progress("Scanning properties", True) 108 | 109 | progress("Computing reachable functions") 110 | reachablet.make_and_save_reachable(args, jsondir / 'viewer-reachable.json') 111 | progress("Computing reachable functions", True) 112 | 113 | # Make sources last, it may delete the goto binary 114 | progress("Scanning source tree") 115 | sources = sourcet.make_and_save_source(args, jsondir / 'viewer-source.json') 116 | progress("Scanning source tree", True) 117 | 118 | progress("Preparing symbol table") 119 | symbols = symbolt.make_and_save_symbol(args, jsondir / 'viewer-symbol.json') 120 | progress("Preparing symbol table", True) 121 | 122 | config = configt.Config(args.config) 123 | report.report(config, sources, symbols, results, coverage, traces, 124 | properties, loops, htmldir, progress) 125 | 126 | global_progress("CBMC viewer", True) 127 | return 0 # exit with normal return code 128 | -------------------------------------------------------------------------------- /summary/Makefile: -------------------------------------------------------------------------------- 1 | default: 2 | @echo Nothing to do for $@ 3 | 4 | pylint: 5 | pylint \ 6 | --disable missing-function-docstring \ 7 | --disable missing-module-docstring \ 8 | --disable duplicate-code \ 9 | --module-rgx '[\w-]+' \ 10 | --max-line-length 80 \ 11 | *.py 12 | -------------------------------------------------------------------------------- /summary/README.md: -------------------------------------------------------------------------------- 1 | This directory contains a script that scans the viewer output for a 2 | set of and produce a summary of the stubs and undefined functions 3 | being used in the proofs. This is a work-in-progress. 4 | 5 | * projects.json is a list of projects on gitub. Each entry gives the 6 | url for the github repository and the path within the repository to 7 | the cbmc proofs. See more below. 8 | 9 | * summary.py scans the list of projects and clones the repositories, 10 | runs the proofs, summarizes the results, and produces a table of the 11 | results. See `summary.py --help`. 12 | 13 | * stubs.py summarizes the stubs and undefined functions used in a proof. 14 | It is used as a library by summary.py, but see `stubs.py --help`. 15 | 16 | Here is an example of a project list: 17 | 18 | ``` 19 | projects.json: 20 | { 21 | "Baseball": { 22 | "github": "https://github.com/johnqpublic/baseball-fantasy-team.git", 23 | "proofs": "tools/cbmc/proofs" 24 | }, 25 | "Football": { 26 | "github": "https://github.com/johnqpublic/football-fantasy-team.git", 27 | "proofs": "test/cbmc/proofs" 28 | } 29 | } 30 | ``` 31 | 32 | This list describes two projects named "Baseball" and "Football". For 33 | "Baseball", 34 | 35 | * `https://github.com/johnqpublic/baseball-fantasy-team.git` is the 36 | URL for the code repository, and 37 | * `tools/cbmc/proofs` is the path within the repository to the 38 | directory under which all of the CBMC proofs can be found. 39 | 40 | Similarly for "Football". The names "Baseball" and "Football" are just 41 | short names for the projects. They are used to refer the the projects 42 | on the command line with `summary.py --project Baseball Football` and 43 | they are used as column headers for the projects in the spreadsheet 44 | produced by `summary.py --chart`. 45 | -------------------------------------------------------------------------------- /summary/projects.json: -------------------------------------------------------------------------------- 1 | { 2 | "Amn FreeRTOS": { 3 | "github": "git@github.com:aws/amazon-freertos", 4 | "proofs": "tools/cbmc/proofs" 5 | }, 6 | "Defender": { 7 | "github": "git@github.com:aws/device-defender-for-aws-iot-embedded-sdk", 8 | "proofs": "test/cbmc/proofs" 9 | }, 10 | "Shadow": { 11 | "github": "git@github.com:aws/device-shadow-for-aws-iot-embedded-sdk", 12 | "proofs": "test/cbmc/proofs" 13 | }, 14 | "Jobs": { 15 | "github": "git@github.com:aws/jobs-for-aws-iot-embedded-sdk", 16 | "proofs": "test/cbmc/proofs" 17 | }, 18 | "OTA": { 19 | "github": "git@github.com:aws/ota-for-aws-iot-embedded-sdk", 20 | "proofs": "test/cbmc/proofs" 21 | }, 22 | "SigV4": { 23 | "github": "git@github.com:aws/SigV4-for-AWS-IoT-embedded-sdk", 24 | "proofs": "test/cbmc/proofs" 25 | }, 26 | "FreeRTOS": { 27 | "github": "git@github.com:FreeRTOS/FreeRTOS", 28 | "proofs": "FreeRTOS/Test/CBMC/proofs" 29 | }, 30 | "FreeRTOS TCP": { 31 | "github": "git@github.com:FreeRTOS/FreeRTOS-Plus-TCP", 32 | "proofs": "test/cbmc/proofs" 33 | }, 34 | "HTTP": { 35 | "github": "git@github.com:FreeRTOS/coreHTTP", 36 | "proofs": "test/cbmc/proofs" 37 | }, 38 | "JSON": { 39 | "github": "git@github.com:FreeRTOS/coreJSON", 40 | "proofs": "test/cbmc/proofs" 41 | }, 42 | "MQTT": { 43 | "github": "git@github.com:FreeRTOS/coreMQTT", 44 | "proofs": "test/cbmc/proofs" 45 | }, 46 | "MQTT-Agent": { 47 | "github": "git@github.com:FreeRTOS/coreMQTT-Agent", 48 | "proofs": "test/cbmc/proofs" 49 | }, 50 | "PKCS11": { 51 | "github": "git@github.com:FreeRTOS/corePKCS11", 52 | "proofs": "test/cbmc/proofs" 53 | }, 54 | "SNTP": { 55 | "github": "git@github.com:FreeRTOS/coreSNTP", 56 | "proofs": "test/cbmc/proofs" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /summary/summary.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import csv 4 | import argparse 5 | import logging 6 | import sys 7 | import subprocess 8 | import os 9 | import json 10 | 11 | import stubs 12 | 13 | ################################################################ 14 | 15 | def create_parser(): 16 | desc = "Summarize stubs and undefined functions in proof repositories." 17 | args = [ 18 | { 19 | "flag": "--github", 20 | "default": "projects.json", 21 | "help": "JSON file defining proof projects and repositories " 22 | "(default: %(default)s)" 23 | }, 24 | { 25 | "flag": "--project", 26 | "nargs": "*", 27 | "default": [], 28 | "help": "Names of proof projects to summarize " 29 | "(default: all projects)" 30 | }, 31 | { 32 | "flag": "--clean", 33 | "action": "store_true", 34 | "help": "Remove proof results and proof summaries" 35 | }, 36 | { 37 | "flag": "--clone", 38 | "action": "store_true", 39 | "help": "Clone proof project" 40 | }, 41 | { 42 | "flag": "--build", 43 | "action": "store_true", 44 | "help": "Build proof project (run proofs)" 45 | }, 46 | { 47 | "flag": "--summarize", 48 | "action": "store_true", 49 | "help": "Summarize project proof results" 50 | }, 51 | { 52 | "flag": "--chart", 53 | "action": "store_true", 54 | "help": "Write project summary data as JSON to stdout" 55 | }, 56 | { 57 | "flag": "--verbose", 58 | "action": "store_true", 59 | "help": "Verbose output" 60 | }, 61 | { 62 | "flag": "--debug", 63 | "action": "store_true", 64 | "help": "Debug output" 65 | } 66 | ] 67 | epilog = None 68 | 69 | parser = argparse.ArgumentParser(description=desc, epilog=epilog) 70 | for arg in args: 71 | flag = arg['flag'] 72 | del arg['flag'] 73 | parser.add_argument(flag, **arg) 74 | return parser 75 | 76 | def parse_arguments(): 77 | return create_parser().parse_args() 78 | 79 | def configure_logger(verbose=False, debug=False): 80 | # Logging is configured by the first invocation of logging.basicConfig 81 | fmt = '%(levelname)s: %(message)s' 82 | if debug: 83 | logging.basicConfig(level=logging.DEBUG, format=fmt) 84 | if verbose: 85 | logging.basicConfig(level=logging.INFO, format=fmt) 86 | logging.basicConfig(format=fmt) 87 | 88 | ################################################################ 89 | # Shell out commands 90 | 91 | def run(cmd, cwd=None, encoding=None): 92 | """Run a command in a subshell and return the standard output.""" 93 | 94 | kwds = { 95 | 'cwd': cwd, 96 | } 97 | if sys.version_info >= (3, 6): # encoding introduced in Python 3.6 98 | kwds['encoding'] = encoding 99 | 100 | logging.debug("Running %s", ' '.join(cmd)) 101 | result = subprocess.run(cmd, **kwds, check=False) 102 | if result.returncode: 103 | logging.debug('Failed command: %s', ' '.join(cmd)) 104 | result.check_returncode() 105 | 106 | ################################################################ 107 | # 108 | 109 | def clean_project(project_path): 110 | if not os.path.isdir(project_path): 111 | logging.info('%s does not exist: not cleaning %s', 112 | project_path, project_path) 113 | return 114 | 115 | logging.info('Cleaning %s', project_path) 116 | run(['git', 'clean', '-fdx'], cwd=project_path) 117 | run(['git', 'checkout', '.'], cwd=project_path) 118 | 119 | def clone_project(project_url): 120 | project_path = os.path.basename(project_url) 121 | 122 | if os.path.isdir(project_path): 123 | logging.info('%s exists: not cloning %s into %s', 124 | project_path, project_url, project_path) 125 | return project_path 126 | 127 | logging.info('Cloning %s into %s', project_url, project_path) 128 | run(['git', 'clone', project_url]) 129 | run(['git', 'submodule', 'update', '--init', '--checkout', '--recursive'], 130 | cwd=project_path) 131 | 132 | return project_path 133 | 134 | def build_project(project_path, proofs_path): 135 | 136 | if not os.path.isdir(project_path): 137 | logging.info('%s does not exist: not running proofs in project %s', 138 | project_path, project_path) 139 | return 140 | 141 | path = os.path.join(project_path, proofs_path) 142 | 143 | logging.info('Running %s proofs in %s', project_path, path) 144 | run(['./run-cbmc-proofs.py'], cwd=path) 145 | 146 | def summarize_project(project_path, proofs_path): 147 | if not os.path.isdir(project_path): 148 | logging.info('%s does not exist: not summarizing project %s', 149 | project_path, project_path) 150 | return None 151 | 152 | path = os.path.join(project_path, proofs_path) 153 | 154 | # Simulate invocation of summary script until args removed from script API 155 | args = stubs.parse_arguments( 156 | ['--srcdir', project_path, '--proofdir', path] 157 | ) 158 | args = stubs.compute_default_arguments(args) 159 | 160 | summary = stubs.scan_proofs(args.proof, args) 161 | stubbed = stubs.defined_histogram(summary, 'stubbed') 162 | removed = stubs.undefined_histogram(summary, 'removed') 163 | missing = stubs.undefined_histogram(summary, 'missing') 164 | 165 | return { 166 | 'stubbed': stubbed, 167 | 'removed': removed, 168 | 'missing': missing 169 | } 170 | 171 | ################################################################ 172 | # 173 | 174 | def chart_projects_csv(summary): 175 | # pylint: disable = line-too-long 176 | writer = csv.writer(sys.stdout) 177 | writer.writerow([''] + list(summary)) 178 | writer.writerow([]) 179 | writer.writerow(['stubbed'] + [len(summary[project]['stubbed']['count'] or []) for project in summary]) 180 | writer.writerow(['removed'] + [len(summary[project]['removed']['count'] or []) for project in summary]) 181 | writer.writerow(['missing'] + [len(summary[project]['missing']['count'] or []) for project in summary]) 182 | writer.writerow([]) 183 | writer.writerow(['stubbed audited'] + [0 for project in summary]) 184 | writer.writerow(['removed audited'] + [0 for project in summary]) 185 | writer.writerow(['missing audited'] + [0 for project in summary]) 186 | writer.writerow([]) 187 | writer.writerow(['stubbed issues'] + [0 for project in summary]) 188 | writer.writerow(['removed issues'] + [0 for project in summary]) 189 | writer.writerow(['missing issues'] + [0 for project in summary]) 190 | writer.writerow([]) 191 | writer.writerow(['stubbed unaudited'] + [len(summary[project]['stubbed']['count'] or []) for project in summary]) 192 | writer.writerow(['removed unaudited'] + [len(summary[project]['removed']['count'] or []) for project in summary]) 193 | writer.writerow(['missing unaudited'] + [len(summary[project]['missing']['count'] or []) for project in summary]) 194 | 195 | def chart_projects_json(summary): 196 | result = {} 197 | 198 | for project in summary: 199 | data = summary[project] 200 | result[project] = { 201 | "stubbed": sorted(data['stubbed']['count']), 202 | "removed": sorted(data['removed']['count']), 203 | "missing": sorted(data['missing']['count']), 204 | "stubbed-audited": [], 205 | "removed-audited": [], 206 | "missing-audited": [], 207 | "stubbed-issues": [], 208 | "removed-issues": [], 209 | "missing-issues": [], 210 | "stubbed-resolved": [], 211 | "removed-resolved": [], 212 | "missing-resolved": [], 213 | } 214 | return json.dumps(result, indent=2) 215 | 216 | ################################################################ 217 | # 218 | 219 | def main(): 220 | args = parse_arguments() 221 | args.project = [os.path.normpath(project) for project in args.project] 222 | 223 | configure_logger(args.verbose, args.debug) 224 | 225 | with open(args.github, encoding="utf-8") as handle: 226 | github = json.load(handle) 227 | 228 | summary = {} 229 | 230 | if not args.project: 231 | args.project = list(github) 232 | 233 | for project in args.project: 234 | project_repo = github[project]['github'] 235 | proofs_path = github[project]['proofs'] 236 | project_path = os.path.basename(project_repo) 237 | if args.clean: 238 | clean_project(project_path) 239 | if args.clone: 240 | clone_project(project_repo) 241 | if args.build: 242 | build_project(project_path, proofs_path) 243 | if args.summarize or args.chart: 244 | result = summarize_project(project_path, proofs_path) 245 | if result: 246 | summary[project] = result 247 | 248 | if args.summarize: 249 | print(json.dumps(summary, indent=2)) 250 | 251 | if args.chart: 252 | print(chart_projects_json(summary)) 253 | 254 | if __name__ == "__main__": 255 | main() 256 | -------------------------------------------------------------------------------- /tests/bin/Makefile: -------------------------------------------------------------------------------- 1 | SCRIPTS = \ 2 | build-viewer-reports \ 3 | compare-result \ 4 | compare-source \ 5 | compare-trace \ 6 | compare-viewer-reports 7 | 8 | PYLINT=pylint 9 | 10 | pylint: 11 | $(PYLINT) \ 12 | --disable=missing-function-docstring \ 13 | --disable=too-many-arguments \ 14 | \ 15 | --disable=duplicate-code \ 16 | --module-rgx '[\w-]+' \ 17 | $(SCRIPTS) *.py 18 | -------------------------------------------------------------------------------- /tests/bin/arguments.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | """Methods for common command-line argument parsing.""" 5 | 6 | import argparse 7 | import logging 8 | 9 | def create_parser(options=None, description=None): 10 | """Create a parser for command line arguments.""" 11 | 12 | options = options or [] 13 | description = description or "" 14 | 15 | flags = [option.get('flag') for option in options] 16 | if '--verbose' not in flags: 17 | options.append({'flag': '--verbose', 'action': 'store_true', 'help': 'Verbose output'}) 18 | if '--debug' not in flags: 19 | options.append({'flag': '--debug', 'action': 'store_true', 'help': 'Debug output'}) 20 | 21 | parser = argparse.ArgumentParser(description=description) 22 | for option in options: 23 | flag = option.pop('flag') 24 | parser.add_argument(flag, **option) 25 | return parser 26 | 27 | def configure_logging(args): 28 | """Configure logging level based on command line arguments.""" 29 | 30 | # Logging is configured by first invocation of basicConfig 31 | fmt = '%(levelname)s: %(message)s' 32 | if args.debug: 33 | logging.basicConfig(level=logging.DEBUG, format=fmt) 34 | return 35 | if args.verbose: 36 | logging.basicConfig(level=logging.INFO, format=fmt) 37 | return 38 | logging.basicConfig(format=fmt) 39 | -------------------------------------------------------------------------------- /tests/bin/build-viewer-reports: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | # SPDX-License-Identifier: Apache-2.0 5 | 6 | # Set emacs mode to Python 7 | # -*- mode: Python;-*- 8 | 9 | """Rebuild and save cbmc-viewer reports using litani proof artifacts. 10 | 11 | The run-cbmc-proofs.py script uses litani to run cbmc and cbmc-viewer 12 | to build proof reports. This script assumes that run-cbmc-proofs.py 13 | has been run in the current directory, and uses the litani artifacts 14 | produced by the script to rebuild the cbmc-viewer reports. This 15 | script then saves the cbmc-viewer reports by copying them into a 16 | directory specified on the command line. 17 | """ 18 | 19 | from pathlib import Path 20 | import json 21 | import logging 22 | import shutil 23 | import subprocess 24 | 25 | import ninja 26 | 27 | import arguments 28 | 29 | ################################################################ 30 | # Command line arguments 31 | 32 | def parse_arguments(): 33 | """Parse command line arguments""" 34 | 35 | description= 'Rebuild and save cbmc-viewer reports using litani proof artifacts.' 36 | options = [ 37 | {'flag': 'results', 38 | 'help': 'Copy results into RESULTS.'}, 39 | {'flag': '--copy-only', 40 | 'action': 'store_true', 41 | 'help': 'Copy results without generating results'}, 42 | ] 43 | args = arguments.create_parser(options, description).parse_args() 44 | arguments.configure_logging(args) 45 | return args 46 | 47 | ################################################################ 48 | # The litani cache 49 | # 50 | # When litani runs, it caches everything there is to know about the 51 | # run in a file named 'run.json'. It writes the path to the cache 52 | # directory containing 'run.json' to a file named '.litani_cache_dir' 53 | # in the current working directory. 54 | # 55 | # The file run.json contains the cbmc-viewer commands that this script 56 | # should re-run. The file is huge, but stripped down to the data we 57 | # care about it looks like 58 | # 59 | # { 60 | # "pipelines": [ 61 | # { 62 | # "ci_stages": [ 63 | # { 64 | # "jobs": [ 65 | # { 66 | # "wrapper_arguments": { 67 | # "ci_stage": "report", 68 | # "command": COMMAND, 69 | # "outputs": [ REPORT ], 70 | # } 71 | # } 72 | # ] 73 | # } 74 | # ] 75 | # } 76 | # ] 77 | # } 78 | # 79 | # There is one pipeline for each proof. There are multiple stages for each 80 | # pipeline. There are multiple jobs for each stage. And there are 81 | # wrapper arguments for each job that record the stage name, the command, 82 | # and output produced by the command. 83 | # 84 | # Each report stage consists of a single job that includes a single 85 | # command and a single output. The command is the invocation of 86 | # cbmc-viewer and the output is the directory containing the report 87 | # produced by cbmc-viewer. 88 | 89 | RUN_JSON = 'run.json' 90 | LITANI_CACHE_DIR = '.litani_cache_dir' 91 | COMMAND = 'command' 92 | REPORT = 'report' 93 | 94 | def find_cache(): 95 | """Locate the file run.json in the litani cache.""" 96 | 97 | cache = Path(RUN_JSON) 98 | if cache.is_file(): 99 | return cache 100 | 101 | with open(LITANI_CACHE_DIR, encoding='utf-8') as handle: 102 | cache_dir = Path(handle.readline().strip()) 103 | cache = cache_dir/RUN_JSON 104 | if cache.is_file(): 105 | return cache 106 | 107 | raise UserWarning(f"Can't find {RUN_JSON}") 108 | 109 | def load_cache(cache): 110 | """Load the file run.json in the litani cache.""" 111 | 112 | with open(cache, encoding='utf-8') as handle: 113 | return json.load(handle) 114 | 115 | def parse_cache(cache): 116 | """Parse the file run.json in the litani cache.""" 117 | 118 | jobs = [job 119 | for pipeline in cache['pipelines'] 120 | for ci_stage in pipeline['ci_stages'] 121 | for job in ci_stage['jobs'] 122 | if job['wrapper_arguments']['ci_stage'] == 'report'] 123 | return [{ 124 | COMMAND: job['wrapper_arguments']['command'], 125 | REPORT: job['wrapper_arguments']['outputs'][0] 126 | } for job in jobs] 127 | 128 | ################################################################ 129 | # Generate reports using cbmc-viewer commands from the litani cache, 130 | # and save the reports (copy them) for later use. 131 | 132 | def delete_viewer_reports(jobs): 133 | """Delete cbmc-viewer reports.""" 134 | 135 | for job in jobs: 136 | report = job[REPORT] 137 | logging.info("Removing %s", report) 138 | shutil.rmtree(report, ignore_errors=True) 139 | 140 | def build_viewer_reports(jobs): 141 | """Build cbmc-viewer reports.""" 142 | 143 | for job in jobs: 144 | report = job[REPORT] 145 | command = job[COMMAND] 146 | logging.info("Generating %s", report) 147 | logging.debug("Running %s", command) 148 | result = subprocess.run(command.split(), 149 | check=True, capture_output=True, text=True) 150 | for line in result.stdout.splitlines(): 151 | logging.debug(line) 152 | for line in result.stderr.splitlines(): 153 | logging.debug(line) 154 | 155 | def copy_viewer_reports(jobs, reports): 156 | """Copy cbmc-viewer reports to the reports directory.""" 157 | 158 | cwd = Path.cwd() 159 | shutil.rmtree(reports, ignore_errors=True) 160 | for job in jobs: 161 | report = Path(job[REPORT]) 162 | path = report.relative_to(cwd) 163 | logging.info("Copying %s to %s", path, reports/path) 164 | shutil.copytree(path, reports/path, dirs_exist_ok=True) 165 | 166 | ################################################################ 167 | # Run cbmc-viewer commands concurrently with ninja 168 | 169 | def build_ninja(jobs, debug=False): 170 | """Build ninja file for running cbmc-viewer commands""" 171 | 172 | rules, builds = [], [] 173 | for job in jobs: 174 | report_dir = Path(job[REPORT]) 175 | proof_dir = report_dir.parent 176 | proof_name = proof_dir.name 177 | cbmc_viewer = job[COMMAND] 178 | 179 | rules.append({ 180 | "name": proof_name, 181 | "description": proof_name, 182 | "command": cbmc_viewer + ("" if debug else " >/dev/null") + " 2>&1" 183 | }) 184 | builds.append({ 185 | "inputs": [], 186 | "outputs": str(report_dir), 187 | "rule": proof_name 188 | }) 189 | 190 | with open("build.ninja", "w", encoding='utf-8') as handle: 191 | writer = ninja.ninja_syntax.Writer(handle) 192 | for rule in rules: 193 | writer.rule(**rule) 194 | writer.newline() 195 | for build in builds: 196 | writer.build(**build) 197 | writer.newline() 198 | 199 | def run_ninja(verbose=False): 200 | """Run ninja to run cbmc-viewer commands""" 201 | 202 | cmd = ['ninja'] + (['--verbose'] if verbose else []) 203 | logging.debug("Running %s", ' '.join(cmd)) 204 | subprocess.run(cmd, check=True) 205 | 206 | 207 | ################################################################ 208 | 209 | def main(): 210 | """Rebuild and save cbmc-viewer reports.""" 211 | 212 | args = parse_arguments() 213 | logging.debug(args) 214 | 215 | jobs = parse_cache(load_cache(find_cache())) 216 | 217 | if not args.copy_only: 218 | delete_viewer_reports(jobs) 219 | build_ninja(jobs, args.debug) 220 | run_ninja(args.verbose) 221 | copy_viewer_reports(jobs, args.results) 222 | 223 | if __name__ == '__main__': 224 | main() 225 | -------------------------------------------------------------------------------- /tests/bin/compare-result: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | 3 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | # SPDX-License-Identifier: Apache-2.0 5 | 6 | # Set emacs mode to Python 7 | # -*- mode: Python;-*- 8 | 9 | """Compare make-result output ignoring known variations in cbmc output.""" 10 | 11 | import json 12 | 13 | import arguments 14 | 15 | def parse_arguments(): 16 | """Parse command line arguments""" 17 | 18 | description = 'Compare make-result output ignoring known variations in cbmc output' 19 | options = [ 20 | {'flag': 'RESULT1', 21 | 'help': 'make-result output'}, 22 | {'flag': 'RESULT2', 23 | 'help': 'make-result output'}, 24 | ] 25 | args = arguments.create_parser(options, description).parse_args() 26 | arguments.configure_logging(args) 27 | return args 28 | 29 | def clean_program(result): 30 | """Ignore variable data like commit ids in cbmc version strings.""" 31 | 32 | del result['viewer-result']['program'] 33 | return result 34 | 35 | def clean_status(result): 36 | """Ignore variable data like run times in cbmc status messages.""" 37 | 38 | result['viewer-result']['status'] = [ 39 | line for line in result['viewer-result']['status'] 40 | if 41 | # Ignore reported version number for CBMC 42 | not line.startswith('CBMC version') and 43 | # Ignore reported runtimes 44 | not line.startswith('Runtime') and 45 | # Ignore path to goto binary 46 | not line.startswith('Reading') and 47 | # Ignore status lines with embedded path names 48 | not line.startswith('Unwinding loop') and 49 | not line.startswith('aborting path') and 50 | not line.startswith('Not unwinding loop') and 51 | # Ignore line emitted by json and xml but not text 52 | not line.startswith('Building error trace') 53 | ] 54 | return result 55 | 56 | def clean(result): 57 | """Ignore variable data in cbmc results.""" 58 | 59 | return clean_status(clean_program(result)) 60 | 61 | def main(): 62 | """Compare make-result output.""" 63 | 64 | args = parse_arguments() 65 | with open(args.RESULT1, encoding='utf-8') as handle: 66 | result1 = clean(json.load(handle)) 67 | with open(args.RESULT2, encoding='utf-8') as handle: 68 | result2 = clean(json.load(handle)) 69 | 70 | with open('/tmp/result1.json', "w", encoding='utf-8') as handle: 71 | json.dump(result1, handle, indent=2) 72 | with open('/tmp/result2.json', "w", encoding='utf-8') as handle: 73 | json.dump(result2, handle, indent=2) 74 | 75 | if result1 != result2: 76 | raise UserWarning( 77 | f"make-result output differs: {args.RESULT1} {args.RESULT2}" 78 | ) 79 | 80 | if __name__ == "__main__": 81 | main() 82 | -------------------------------------------------------------------------------- /tests/bin/compare-source: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | 3 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | # SPDX-License-Identifier: Apache-2.0 5 | 6 | # Set emacs mode to Python 7 | # -*- mode: Python;-*- 8 | 9 | """Compare make-source output ignoring known differences in path names.""" 10 | 11 | import json 12 | 13 | import arguments 14 | 15 | def parse_arguments(): 16 | """Parse command line arguments""" 17 | 18 | description = 'Compare make-source output ignoring known differences in path names.' 19 | options = [ 20 | {'flag': 'SOURCE1', 21 | 'help': 'make-source output'}, 22 | {'flag': 'SOURCE2', 23 | 'help': 'make-source output'}, 24 | ] 25 | args = arguments.create_parser(options, description).parse_args() 26 | arguments.configure_logging(args) 27 | return args 28 | 29 | def clean(source): 30 | """Strip references to path to the source root.""" 31 | 32 | root = source['viewer-source']['root'] + '/' 33 | all_files = source['viewer-source']['all_files'] 34 | 35 | # Ignore system files not under the source root 36 | # Strip the source root from absolute paths of files under the source root 37 | all_files = [path[len(root):] for path in all_files 38 | if path.startswith(root)] 39 | 40 | source['viewer-source']['root'] = '' 41 | source['viewer-source']['all_files'] = all_files 42 | 43 | return source 44 | 45 | def main(): 46 | """Compare make-source output.""" 47 | 48 | args = parse_arguments() 49 | with open(args.SOURCE1, encoding='utf-8') as handle: 50 | source1 = clean(json.load(handle)) 51 | with open(args.SOURCE2, encoding='utf-8') as handle: 52 | source2 = clean(json.load(handle)) 53 | 54 | if source1 != source2: 55 | raise UserWarning( 56 | f"make-source output differs: {args.SOURCE1} {args.SOURCE2}" 57 | ) 58 | 59 | if __name__ == "__main__": 60 | main() 61 | -------------------------------------------------------------------------------- /tests/bin/compare-trace: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | 3 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | # SPDX-License-Identifier: Apache-2.0 5 | 6 | # Set emacs mode to Python 7 | # -*- mode: Python;-*- 8 | 9 | """Compare make-trace output ignoring known variations in cbmc output.""" 10 | 11 | import json 12 | 13 | import arguments 14 | 15 | def parse_arguments(): 16 | """Parse command line arguments""" 17 | 18 | description = 'Compare make-trace output ignoring known variations in cbmc output.' 19 | options = [ 20 | {'flag': 'TRACE1', 21 | 'help': 'make-trace output'}, 22 | {'flag': 'TRACE2', 23 | 'help': 'make-trace output'}, 24 | ] 25 | args = arguments.create_parser(options, description).parse_args() 26 | arguments.configure_logging(args) 27 | return args 28 | 29 | def clean_step_detail(detail): 30 | """Clean up step details.""" 31 | 32 | for key in ["lhs-lexical-scope", "rhs-binary", "rhs-value"]: 33 | if key in detail: 34 | del detail[key] 35 | return detail 36 | 37 | def clean_step(step): 38 | """Clean up a step.""" 39 | 40 | step["detail"] = clean_step_detail(step["detail"]) 41 | return step 42 | 43 | def clean_trace(trace): 44 | """Clean up a trace.""" 45 | 46 | return [clean_step(step) for step in trace] 47 | 48 | def clean_traces(data): 49 | """Clean up traces in output of make-traces.""" 50 | 51 | data["viewer-trace"]["traces"] = { 52 | name: clean_trace(trace) for name, trace in data["viewer-trace"]["traces"].items() 53 | } 54 | return data 55 | 56 | def main(): 57 | """Compare make-trace output.""" 58 | 59 | args = parse_arguments() 60 | with open(args.TRACE1, encoding='utf-8') as handle: 61 | trace1 = clean_traces(json.load(handle)) 62 | with open(args.TRACE2, encoding='utf-8') as handle: 63 | trace2 = clean_traces(json.load(handle)) 64 | 65 | with open('/tmp/trace1.json', "w", encoding='utf-8') as handle: 66 | json.dump(trace1, handle, indent=2) 67 | with open('/tmp/trace2.json', "w", encoding='utf-8') as handle: 68 | json.dump(trace2, handle, indent=2) 69 | 70 | if trace1 != trace2: 71 | raise UserWarning( 72 | f"make-trace output differs: {args.TRACE1} {args.TRACE2}" 73 | ) 74 | 75 | if __name__ == "__main__": 76 | main() 77 | -------------------------------------------------------------------------------- /tests/bin/compare-viewer-reports: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | 3 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | # SPDX-License-Identifier: Apache-2.0 5 | 6 | # Set emacs mode to Python 7 | # -*- mode: Python;-*- 8 | 9 | """Compare sets of reports produced by cbmc-viewer.""" 10 | 11 | from pathlib import Path 12 | import logging 13 | import subprocess 14 | import sys 15 | 16 | import arguments 17 | 18 | RESULT_JSON = 'viewer-result.json' 19 | SOURCE_JSON = 'viewer-source.json' 20 | OTHER_JSON = [ 21 | 'viewer-coverage.json', 22 | 'viewer-loop.json', 23 | 'viewer-property.json', 24 | 'viewer-reachable.json', 25 | 'viewer-symbol.json', 26 | 'viewer-trace.json' 27 | ] 28 | KNOWN_JSON = [RESULT_JSON, SOURCE_JSON] + OTHER_JSON 29 | 30 | SCRIPT_DIR = Path(__file__).resolve().parent 31 | 32 | ################################################################ 33 | # Command line arguments 34 | 35 | def parse_arguments(): 36 | """Parse command line arguments""" 37 | 38 | description = 'Compare sets of reports produced by cbmc-viewer.' 39 | options = [ 40 | {'flag': 'reports1', 41 | 'type': Path, 42 | 'help': 'Directory of cbmc-viewer reports'}, 43 | {'flag': 'reports2', 44 | 'type': Path, 45 | 'help': 'Directory of cbmc-viewer reports'}, 46 | {'flag': '--proof', 47 | 'nargs': '+', 48 | 'action': 'append', 49 | 'default': [], 50 | 'help': 'Relative path to a proof directory containing a cbmc-viewer report'}, 51 | {'flag': '--report', 52 | 'default': 'report', 53 | 'help': 'Subdirectory of PROOF containing the cbmc-viewer report (default: %(default)s)'}, 54 | {'flag': '--html', 55 | 'default': 'html', 56 | 'help': 'Subdirectory of REPORT containing the html output (default: %(default)s)'}, 57 | {'flag': '--json', 58 | 'default': 'json', 59 | 'help': 'Subdirectory of REPORT containing the json output (default: %(default)s)'}, 60 | ] 61 | args = arguments.create_parser(options, description).parse_args() 62 | arguments.configure_logging(args) 63 | 64 | # A little too complicated for the `type` keyword to argparse 65 | args.proof = [Path(proof) 66 | for proof_list in args.proof 67 | for proof in proof_list] 68 | 69 | return args 70 | 71 | def configure_logging(verbose=False, debug=False): 72 | """Configure logging based on command line arguments.""" 73 | 74 | if debug: 75 | logging.basicConfig(level=logging.DEBUG, 76 | format='%(levelname)s: %(message)s') 77 | return 78 | if verbose: 79 | logging.basicConfig(level=logging.INFO, 80 | format='%(levelname)s: %(message)s') 81 | return 82 | logging.basicConfig(format='%(levelname)s: %(message)s') 83 | 84 | ################################################################ 85 | 86 | def common_proofs(reports1, reports2, proofs): 87 | """Find the proofs in common to both sets of proof reports.""" 88 | 89 | def subdirs(reports, proofs): 90 | paths = [reports/proof for proof in proofs] or Path(reports).iterdir() 91 | return {path.relative_to(reports) for path in paths if path.is_dir()} 92 | 93 | proofs1 = subdirs(reports1, proofs) 94 | proofs2 = subdirs(reports2, proofs) 95 | common = proofs1.intersection(proofs2) 96 | for proof in proofs: 97 | if proof not in common: 98 | logging.warning('Skipping "%s": Not found in "%s" or not found in "%s"', 99 | proof, reports1, reports2) 100 | return common 101 | 102 | ################################################################ 103 | # Comparison methods 104 | # 105 | # The comparison methods are basically textual diff combined with the 106 | # compare-result and compare-source scripts. In the future, as the 107 | # content of the html reports and json summaries evolves, textual diff 108 | # may become too cumbersome, and we may want to use difflib or 109 | # json-diff to do a more semantic (less textual) comparison. 110 | 111 | def run(cmd): 112 | """Run a command.""" 113 | 114 | return subprocess.run(cmd, capture_output=True, check=False, text=True) 115 | 116 | def stringify(cmd): 117 | """Render a command as a string.""" 118 | 119 | return ' '.join([str(word) for word in cmd]) 120 | 121 | def truncate(string, verbose=False, length=800): 122 | """Truncate a long string like a json blob.""" 123 | 124 | if not verbose and len(string) > length: 125 | return string[:800].strip() + ' ...[truncated]...' 126 | return string.strip() 127 | 128 | def compare_diff(path1, path2, verbose): 129 | """Use diff to compare two files or two directories.""" 130 | 131 | cmd = ['diff', '-r', path1, path2] 132 | result = run(cmd) 133 | if result.returncode: 134 | print(stringify(cmd)) 135 | print(truncate(result.stdout, verbose)) 136 | return result.returncode == 0 137 | 138 | def compare_result(path1, path2): 139 | """Use compare-result to compare two copies of viewer-result.json.""" 140 | 141 | cmd = [SCRIPT_DIR/'compare-result', path1, path2] 142 | try: 143 | return run(cmd).returncode == 0 144 | except UserWarning as err: 145 | print(stringify(cmd)) 146 | print(err) 147 | return False 148 | 149 | def compare_source(path1, path2): 150 | """Use compare-source to compare two copies of viewer-source.json.""" 151 | 152 | cmd = [SCRIPT_DIR/'compare-source', path1, path2] 153 | try: 154 | return run(cmd).returncode == 0 155 | except UserWarning as err: 156 | print(stringify(cmd)) 157 | print(err) 158 | return False 159 | 160 | def compare_html(proof, reports1, reports2, 161 | report='report', html='html', verbose=False): 162 | # pylint: disable=too-many-arguments 163 | """Compare html reports produced by cbmc-viewer.""" 164 | 165 | html_path = proof/report/html 166 | if not (reports1/html_path).is_dir(): 167 | raise FileNotFoundError(reports1/html_path) 168 | if not (reports2/html_path).is_dir(): 169 | raise FileNotFoundError(reports2/html_path) 170 | 171 | logging.info("Comparing %s", html_path) 172 | return compare_diff(reports1/html_path, reports2/html_path, verbose) 173 | 174 | def compare_json(proof, reports1, reports2, 175 | report='report', jsn='json', verbose=False): 176 | # pylint: disable=too-many-arguments 177 | """Compare json summaries produced by cbmc-viewer.""" 178 | 179 | json_path = proof/report/jsn 180 | if not (reports1/json_path).is_dir(): 181 | raise FileNotFoundError(reports1/json_path) 182 | if not (reports2/json_path).is_dir(): 183 | raise FileNotFoundError(reports2/json_path) 184 | 185 | # Warn if our list of known json files ever becomes incomplete 186 | for path in Path(reports1/json_path).glob('viewer-*.json'): 187 | if path.name not in KNOWN_JSON: 188 | logging.warning('Found unknown json file %s', path) 189 | for path in Path(reports2/json_path).glob('viewer-*.json'): 190 | if path.name not in KNOWN_JSON: 191 | logging.warning('Found unknown json file %s', path) 192 | 193 | equal_json = True 194 | logging.info("Comparing %s", json_path) 195 | 196 | result_path = json_path/RESULT_JSON 197 | logging.info("Comparing %s", result_path) 198 | equal_json = compare_result(reports1/result_path, reports2/result_path) and equal_json 199 | 200 | source_path = json_path/SOURCE_JSON 201 | logging.info("Comparing %s", source_path) 202 | equal_json = compare_source(reports1/source_path, reports2/source_path) and equal_json 203 | 204 | for other in OTHER_JSON: 205 | other_path = json_path/other 206 | logging.info("Comparing %s", other_path) 207 | equal_json = compare_diff(reports1/other_path, reports2/other_path, verbose) and equal_json 208 | 209 | return equal_json 210 | 211 | def main(): 212 | """Compare viewer reports.""" 213 | 214 | args = parse_arguments() 215 | configure_logging(args.verbose, args.debug) 216 | 217 | proofs = common_proofs(args.reports1, args.reports2, args.proof) 218 | equal = True 219 | for proof in proofs: 220 | logging.info("Comparing %s", proof) 221 | equal = compare_html(proof, args.reports1, args.reports2, args.report, 222 | args.html, args.verbose) and equal 223 | equal = compare_json(proof, args.reports1, args.reports2, args.report, 224 | args.json, args.verbose) and equal 225 | return equal 226 | 227 | if __name__ == '__main__': 228 | sys.exit(0 if main() else 1) 229 | -------------------------------------------------------------------------------- /tests/bin/difference.md: -------------------------------------------------------------------------------- 1 | The script makes it easy to compare two versions of cbmc-viewer. 2 | 3 | A common use case is that we have two versions of cbmc-viewer in branches 4 | names master and bugfix, and we want to see how the output of the two 5 | versions differ on a set of proofs built using litani as we do with the 6 | starter kit. 7 | 8 | * Checkout the repository. Make the output of cbmc-viewer interesting 9 | by deleting some proof assumptions to break the proofs. Run the 10 | proofs with run-cbmc-proofs.py (or some subset of the proofs using the 11 | script's --proof command line argument) in /repo/test/cbmc/proofs. 12 | 13 | * Run 14 | ``` 15 | difference.py \\ 16 | --viewer-repository . \\ 17 | --proofs /repo/test/cbmc/proofs \\ 18 | --commits master bugfix \\ 19 | --litani /repo/test/cbmc/proofs/litani/litani 20 | ``` 21 | to run the two versions master and bugfix of cbmc-viewer on the 22 | output of cbmc on the proofs in /repo/test/cbmc/proofs, and to write 23 | the proof reports into the directories /tmp/reports/master and 24 | /tmp/reports/bugfix. 25 | 26 | * Run 27 | ``` 28 | diff -r /tmp/reports/master /tmp/reports/bugfix 29 | ``` 30 | to compare the results. 31 | 32 | In the simple case, where the difference between master and bugfix is 33 | just code clean-up, there is no actual difference in the output of 34 | cbmc-viewer and the diff will be clean. 35 | 36 | In the common case, however, there will be some difference. Perhaps 37 | the json summary of the traces differ in that some of the steps in the 38 | trace that are internal to cbmc are now omitted. In this case, the 39 | output of diff may be hard to read, but some simple queries with jq 40 | may be able to confirm that the changes are as desired. 41 | 42 | The script works as follows: 43 | 44 | * For each branch master and bugfix, create python virtual environments 45 | /tmp/viewer/master and /tmp/viewer/bugfix, check out the versions master 46 | and bugfix, and install them into the corresponding virtual environments. 47 | 48 | * Load the run.json summary produced by the litani build of the proofs 49 | and extract the jobs that invoked cbmc-viewer 50 | 51 | * Reconstruct the 'litani add-job' commands the added these cbmc-viewer 52 | jobs to the build. 53 | 54 | * Modify these add-job commands to produce two sets of add-job 55 | commands: One set invokes the cbmc-viewer in /tmp/viewer/master and 56 | writes the reports into /tmp/reports/master, and the other set does 57 | the same for bugfix. 58 | 59 | * Now issue litani init, these new litani add-jobs, and finally litani 60 | run-build. 61 | 62 | This script solves the following problem with writing unit tests for 63 | cbmc-viewer. 64 | 65 | * First, the input to cbmc-viewer is too big to store in the 66 | repository. Generating interesting output with cbmc-viewer requires 67 | having interesting proofs for cbmc to analyze, and size of the code 68 | and size of the cbmc output is too big to store in a repository. 69 | 70 | * Second, the expected output of cbmc-viewer is too big to store in 71 | the repository. Generating interesting output with cbmc-viewer 72 | generates a lot of output. The json output itself can be several 73 | megabytes. 74 | 75 | Even if we did store this in the repository, no one would want to code 76 | review changes to the data as cbmc-viewer changes. 77 | 78 | This method lets us use existing proof repositories for unit tests, 79 | use the output of an existing version of cbmc-viewer like master as 80 | the expected output, and compare that with the output of a new version 81 | like bugfix. This makes it easy to test during development, and makes 82 | it possible to test in continuous integration by comparing the HEAD 83 | with the branch we want to merge into HEAD. -------------------------------------------------------------------------------- /tests/coreHTTP/.gitignore: -------------------------------------------------------------------------------- 1 | coreHTTP 2 | -------------------------------------------------------------------------------- /tests/coreHTTP/Makefile: -------------------------------------------------------------------------------- 1 | # The FreeRTOS coreHTTP respository 2 | REPO_DIR = coreHTTP 3 | REPO_URL = https://github.com/FreeRTOS/coreHTTP.git 4 | 5 | # Paths in the repository 6 | PROOF_DIR = test/cbmc/proofs 7 | 8 | # Proofs in the repository 9 | PROOFS = \ 10 | HTTPClient_AddRangeHeader \ 11 | HTTPClient_ReadHeader \ 12 | HTTPClient_strerror \ 13 | findHeaderFieldParserCallback \ 14 | findHeaderOnHeaderCompleteCallback \ 15 | findHeaderValueParserCallback \ 16 | httpParserOnBodyCallback \ 17 | httpParserOnHeaderFieldCallback \ 18 | httpParserOnHeaderValueCallback \ 19 | httpParserOnHeadersCompleteCallback \ 20 | httpParserOnMessageBeginCallback \ 21 | httpParserOnMessageCompleteCallback \ 22 | httpParserOnStatusCallback 23 | 24 | # Omit slow proofs in the repository 25 | # HTTPClient_AddHeader \ 26 | # HTTPClient_InitializeRequestHeaders \ 27 | # HTTPClient_Send \ 28 | 29 | # Commits of cbmc-viewer to compare on coreHTTP 30 | COMMIT1 ?= master 31 | COMMIT2 ?= $$(git branch --show-current) 32 | 33 | default: clone build compare 34 | 35 | # Clone the repository and break the proofs 36 | clone: 37 | $(RM) -r $(REPO_DIR) 38 | git clone $(REPO_URL) $(REPO_DIR) 39 | cd $(REPO_DIR); git submodule update --init --checkout --recursive 40 | cd $(REPO_DIR); git apply $(abspath bug.patch) 41 | 42 | # Run the proofs and build the reports (using any version of viewer) 43 | # EXTERNAL_SAT_SOLVER= to ensure cbmc uses minisat and not kissat 44 | build: 45 | cd $(REPO_DIR)/$(PROOF_DIR); \ 46 | EXTERNAL_SAT_SOLVER= \ 47 | run-cbmc-proofs.py --proofs $(PROOFS) 48 | 49 | # Run the two commits of cbmc-viewer on the coreHTTP proofs and 50 | # compare the results 51 | compare: 52 | ../bin/difference.py --verbose --force \ 53 | --viewer $(abspath ../..) \ 54 | --commits $(COMMIT1) $(COMMIT2) \ 55 | --proofs $(abspath coreHTTP/test/cbmc/proofs) 56 | 57 | clean: 58 | $(RM) *~ 59 | $(RM) -r $(REPO_DIR) 60 | -------------------------------------------------------------------------------- /tests/coreHTTP/README.md: -------------------------------------------------------------------------------- 1 | This regression test makes it easy to compare the output of two 2 | cbmc-viewer commits on coreHTTP. 3 | 4 | * Run `make` to compare the current branch with the master branch. 5 | 6 | * Run `make COMMIT1=B1 COMMIT2=B2` to compare branch B1 with branch B2. 7 | B1 and B2 can be any commits in the repository. 8 | 9 | In more detail: 10 | * `make clone` will clone coreHTTP and break the proofs. 11 | * `make build` will run the fast coreHTTP proofs and build the reports 12 | with the installed cbmc-viewer. 13 | * `make compare` will rebuild the reports with the two cbmc-viewer 14 | commits and compare the results. 15 | -------------------------------------------------------------------------------- /tests/coreHTTP/bug.patch: -------------------------------------------------------------------------------- 1 | diff --git a/test/cbmc/sources/http_cbmc_state.c b/test/cbmc/sources/http_cbmc_state.c 2 | index 004e428..beb8993 100644 3 | --- a/test/cbmc/sources/http_cbmc_state.c 4 | +++ b/test/cbmc/sources/http_cbmc_state.c 5 | @@ -44,7 +44,7 @@ HTTPRequestHeaders_t * allocateHttpRequestHeaders( HTTPRequestHeaders_t * pReque 6 | 7 | if( pRequestHeaders != NULL ) 8 | { 9 | - __CPROVER_assume( pRequestHeaders->bufferLen < CBMC_MAX_OBJECT_SIZE ); 10 | + // __CPROVER_assume( pRequestHeaders->bufferLen < CBMC_MAX_OBJECT_SIZE ); 11 | pRequestHeaders->pBuffer = mallocCanFail( pRequestHeaders->bufferLen ); 12 | } 13 | 14 | @@ -73,13 +73,13 @@ HTTPRequestInfo_t * allocateHttpRequestInfo( HTTPRequestInfo_t * pRequestInfo ) 15 | 16 | if( pRequestInfo != NULL ) 17 | { 18 | - __CPROVER_assume( pRequestInfo->methodLen < CBMC_MAX_OBJECT_SIZE ); 19 | + // __CPROVER_assume( pRequestInfo->methodLen < CBMC_MAX_OBJECT_SIZE ); 20 | pRequestInfo->pMethod = mallocCanFail( pRequestInfo->methodLen ); 21 | 22 | - __CPROVER_assume( pRequestInfo->hostLen < CBMC_MAX_OBJECT_SIZE ); 23 | + // __CPROVER_assume( pRequestInfo->hostLen < CBMC_MAX_OBJECT_SIZE ); 24 | pRequestInfo->pHost = mallocCanFail( pRequestInfo->hostLen ); 25 | 26 | - __CPROVER_assume( pRequestInfo->pathLen < CBMC_MAX_OBJECT_SIZE ); 27 | + // __CPROVER_assume( pRequestInfo->pathLen < CBMC_MAX_OBJECT_SIZE ); 28 | pRequestInfo->pPath = mallocCanFail( pRequestInfo->pathLen ); 29 | } 30 | 31 | @@ -111,10 +111,10 @@ HTTPResponse_t * allocateHttpResponse( HTTPResponse_t * pResponse ) 32 | 33 | if( pResponse != NULL ) 34 | { 35 | - __CPROVER_assume( pResponse->bufferLen < CBMC_MAX_OBJECT_SIZE ); 36 | + // __CPROVER_assume( pResponse->bufferLen < CBMC_MAX_OBJECT_SIZE ); 37 | pResponse->pBuffer = mallocCanFail( pResponse->bufferLen ); 38 | 39 | - __CPROVER_assume( headerOffset <= pResponse->bufferLen ); 40 | + // __CPROVER_assume( headerOffset <= pResponse->bufferLen ); 41 | 42 | if( pResponse->pBuffer != NULL ) 43 | { 44 | @@ -128,7 +128,7 @@ HTTPResponse_t * allocateHttpResponse( HTTPResponse_t * pResponse ) 45 | { 46 | /* The length of the headers MUST be between the start of the 47 | * headers and the end of the buffer. */ 48 | - __CPROVER_assume( pResponse->headersLen < ( pResponse->bufferLen - headerOffset ) ); 49 | + // __CPROVER_assume( pResponse->headersLen < ( pResponse->bufferLen - headerOffset ) ); 50 | } 51 | 52 | if( pResponse->bufferLen == 0 ) 53 | @@ -137,7 +137,7 @@ HTTPResponse_t * allocateHttpResponse( HTTPResponse_t * pResponse ) 54 | } 55 | else 56 | { 57 | - __CPROVER_assume( bodyOffset <= pResponse->bufferLen ); 58 | + // __CPROVER_assume( bodyOffset <= pResponse->bufferLen ); 59 | } 60 | 61 | if( pResponse->pBuffer != NULL ) 62 | @@ -150,7 +150,7 @@ HTTPResponse_t * allocateHttpResponse( HTTPResponse_t * pResponse ) 63 | * of the buffer. */ 64 | if( pResponse->pBody ) 65 | { 66 | - __CPROVER_assume( pResponse->bodyLen < ( pResponse->bufferLen - bodyOffset ) ); 67 | + // __CPROVER_assume( pResponse->bodyLen < ( pResponse->bufferLen - bodyOffset ) ); 68 | } 69 | } 70 | 71 | @@ -207,11 +207,11 @@ llhttp_t * allocateHttpSendParser( llhttp_t * pHttpParser ) 72 | if( pHttpParser == NULL ) 73 | { 74 | pHttpParser = malloc( sizeof( llhttp_t ) ); 75 | - __CPROVER_assume( pHttpParser != NULL ); 76 | + // __CPROVER_assume( pHttpParser != NULL ); 77 | } 78 | 79 | pHttpParsingContext = allocateHttpSendParsingContext( NULL ); 80 | - __CPROVER_assume( isValidHttpSendParsingContext( pHttpParsingContext ) ); 81 | + // __CPROVER_assume( isValidHttpSendParsingContext( pHttpParsingContext ) ); 82 | pHttpParser->data = ( void * ) pHttpParsingContext; 83 | 84 | return pHttpParser; 85 | @@ -225,16 +225,16 @@ HTTPParsingContext_t * allocateHttpSendParsingContext( HTTPParsingContext_t * pH 86 | if( pHttpParsingContext == NULL ) 87 | { 88 | pHttpParsingContext = malloc( sizeof( HTTPParsingContext_t ) ); 89 | - __CPROVER_assume( pHttpParsingContext != NULL ); 90 | + // __CPROVER_assume( pHttpParsingContext != NULL ); 91 | 92 | pResponse = allocateHttpResponse( NULL ); 93 | - __CPROVER_assume( isValidHttpResponse( pResponse ) && 94 | - pResponse != NULL && 95 | - pResponse->pBuffer != NULL && 96 | - pResponse->bufferLen > 0 ); 97 | + // __CPROVER_assume( isValidHttpResponse( pResponse ) && 98 | + // pResponse != NULL && 99 | + // pResponse->pBuffer != NULL && 100 | + // pResponse->bufferLen > 0 ); 101 | pHttpParsingContext->pResponse = pResponse; 102 | 103 | - __CPROVER_assume( bufferOffset < pResponse->bufferLen ); 104 | + // __CPROVER_assume( bufferOffset < pResponse->bufferLen ); 105 | pHttpParsingContext->pBufferCur = pResponse->pBuffer + bufferOffset; 106 | } 107 | 108 | @@ -258,7 +258,7 @@ llhttp_t * allocateHttpReadHeaderParser( llhttp_t * pHttpParser ) 109 | if( pHttpParser == NULL ) 110 | { 111 | pHttpParser = malloc( sizeof( llhttp_t ) ); 112 | - __CPROVER_assume( pHttpParser != NULL ); 113 | + // __CPROVER_assume( pHttpParser != NULL ); 114 | } 115 | 116 | pFindHeaderContext = allocateFindHeaderContext( NULL ); 117 | @@ -272,7 +272,7 @@ findHeaderContext_t * allocateFindHeaderContext( findHeaderContext_t * pFindHead 118 | if( pFindHeaderContext == NULL ) 119 | { 120 | pFindHeaderContext = malloc( sizeof( findHeaderContext_t ) ); 121 | - __CPROVER_assume( pFindHeaderContext != NULL ); 122 | + // __CPROVER_assume( pFindHeaderContext != NULL ); 123 | } 124 | 125 | return pFindHeaderContext; 126 | -------------------------------------------------------------------------------- /tests/kani/.gitignore: -------------------------------------------------------------------------------- 1 | kani 2 | build.ninja 3 | -------------------------------------------------------------------------------- /tests/kani/Makefile: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | SHELL=/bin/bash 5 | 6 | # Assume cbmc and cbmc-viewer are installed in PATH 7 | # skip ./kani/scripts/setup/ubuntu/install_cbmc.sh 8 | # skip ./kani/scripts/setup/install_viewer.sh 2.11 9 | 10 | KANI?=kani 11 | 12 | # ARCH is ubuntu 13 | ARCH?=ubuntu 14 | 15 | clone: 16 | git clone https://github.com/model-checking/kani.git $(KANI) 17 | cd $(KANI) && git submodule update --init 18 | 19 | # MacOS warning: If brew fails trying to install ctags over 20 | # universal-ctags, then replace ctags with universal-ctags 21 | 22 | build: 23 | PATH=$$HOME/.cargo/bin:$$PATH $(MAKE) build_ 24 | 25 | build_: 26 | ./$(KANI)/scripts/setup/$(ARCH)/install_deps.sh 27 | ./$(KANI)/scripts/setup/install_rustup.sh 28 | cd $(KANI) && cargo build --workspace 29 | 30 | test: 31 | PATH=$$HOME/.cargo/bin:$$(pwd)/$(KANI)/scripts:$$PATH $(MAKE) version test_ 32 | 33 | test_: 34 | ./write_build_ninja.py $(KANI)/tests/kani 35 | ninja 36 | 37 | version: 38 | @ echo cbmc --version: $$(cbmc --version) >&2 39 | @ echo cbmc-viewer --version: $$(cbmc-viewer --version) >&2 40 | @ echo kani --version: $$(kani --version) >&2 41 | 42 | clean: 43 | $(RM) -r *~ build.ninja .ninja_log __pycache__ 44 | 45 | veryclean: clean 46 | $(RM) -r $(KANI) 47 | -------------------------------------------------------------------------------- /tests/kani/write_build_ninja.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | # SPDX-License-Identifier: Apache-2.0 5 | 6 | from pathlib import Path 7 | import re 8 | import subprocess 9 | import sys 10 | 11 | import ninja_syntax 12 | 13 | RULE_NAME = 'kani' 14 | 15 | def rust_sources(root): 16 | sources = subprocess.run( 17 | ['find', root, '-name', '*.rs'], 18 | encoding='utf-8', 19 | universal_newlines=True, 20 | check=True, 21 | stdout=subprocess.PIPE, 22 | stderr=subprocess.PIPE 23 | ).stdout.splitlines() 24 | # source files with names including 'ignore' and 'fixme' are broken 25 | # source file 'ForeignItems/main.rs' is also broken 26 | tags = ['ignore', 'fixme', 'tests/kani/ForeignItems/main.rs'] 27 | return [Path(src) for src in sources if all(tag not in src for tag in tags)] 28 | 29 | def kani_pragma(pragma, rust_code): 30 | # Examples of kani pragmas: 31 | # // kani-verify-fail 32 | # // kani-check-fail 33 | # // kani-flags: --default-unwind 8 34 | try: 35 | regexp = fr'//[ \t]*{pragma}([^\n]*)' 36 | return re.search(regexp, rust_code).group(1).strip() 37 | except AttributeError: # 'NoneType' object has no attribute 'group' 38 | return None 39 | 40 | def kani_verify_fail(rust_code): 41 | return kani_pragma('kani-verify-fail', rust_code) is not None 42 | 43 | def kani_check_fail(rust_code): 44 | return kani_pragma('kani-check-fail', rust_code) is not None 45 | 46 | def kani_flags(rust_code): 47 | return kani_pragma('kani-flags:', rust_code) or "" 48 | 49 | def pool_name(pool): 50 | return re.sub('[^a-zA-Z0-9_]', '_', str(pool)) 51 | 52 | def ninja_pools(sources): 53 | pools = {source.parent for source in sources} 54 | return [{'name': pool_name(pool), 'depth': 1} for pool in pools] 55 | 56 | def ninja_rules(sources): 57 | dirs = {source.parent for source in sources} 58 | return [{'name': pool_name(dir), 59 | 'command': f'cd "{dir}" && kani ${{flags}} --visualize ${{src}}', 60 | 'pool': pool_name(dir)} for dir in dirs] 61 | 62 | def ninja_builds(source): 63 | with open(source, encoding='utf-8') as src: 64 | rust_code = src.read() 65 | if kani_verify_fail(rust_code) or kani_check_fail(rust_code): 66 | return [] 67 | 68 | return [{'outputs': [f'build-{source}'], 69 | 'rule': pool_name(source.parent), 70 | 'inputs': str(source), 71 | 'variables': {'src': str(source.name), 72 | 'flags': kani_flags(rust_code)}}] 73 | 74 | def build_ninja_file(sources, build_ninja='build.ninja'): 75 | with open(build_ninja, 'w', encoding='utf-8') as output: 76 | writer = ninja_syntax.Writer(output) 77 | for pool in ninja_pools(sources): 78 | writer.pool(**pool) 79 | for rule in ninja_rules(sources): 80 | writer.rule(**rule) 81 | for source in sources: 82 | for build in ninja_builds(source): 83 | writer.build(**build) 84 | 85 | def main(): 86 | try: 87 | root = sys.argv[1] 88 | except IndexError: 89 | root = 'kani/tests/kani' 90 | sources = rust_sources(root) 91 | build_ninja_file(sources) 92 | 93 | if __name__ == "__main__": 94 | main() 95 | -------------------------------------------------------------------------------- /tests/repo-tests/coreHTTP/.gitignore: -------------------------------------------------------------------------------- 1 | coreHTTP/ 2 | reports1/ 3 | reports2/ 4 | -------------------------------------------------------------------------------- /tests/repo-tests/coreHTTP/Makefile: -------------------------------------------------------------------------------- 1 | # Use the FreeRTOS coreHTTP proofs to compare two versions of cbmc-viewer. 2 | # 3 | # 1. make clone: Clone the repository and "break" the proofs 4 | # 2. make build: Run the proofs and build the reports 5 | # 3. Rebuild reports with viewer1 6 | # a. Ensure the command 'cbmc-viewer' runs viewer1 7 | # b. make reports1: Build the reports with viewer1 8 | # 4. Rebuild reports with viewer2 9 | # a. Ensure the command 'cbmc-viewer' runs viewer2 10 | # b. make reports2: Build the reports with viewer2 11 | # 5. make compare: Compare the results of viewer1 and viewer2 12 | 13 | # The FreeRTOS coreHTTP respository 14 | REPO_DIR = coreHTTP 15 | REPO_URL = https://github.com/FreeRTOS/coreHTTP.git 16 | 17 | # Paths in the repository 18 | PROOF_DIR = test/cbmc/proofs 19 | TEMPLATE_DIR = test/cbmc/aws-templates-for-cbmc-proofs 20 | 21 | # Proofs in the repository 22 | PROOFS = \ 23 | findHeaderFieldParserCallback \ 24 | findHeaderOnHeaderCompleteCallback \ 25 | findHeaderValueParserCallback \ 26 | httpParserOnStatusCallback 27 | 28 | # BUG: GitHub runners fail to terminate running all the fast proofs 29 | # HTTPClient_AddRangeHeader \ 30 | # HTTPClient_ReadHeader \ 31 | # HTTPClient_strerror \ 32 | # findHeaderFieldParserCallback \ 33 | # findHeaderOnHeaderCompleteCallback \ 34 | # findHeaderValueParserCallback \ 35 | # httpParserOnBodyCallback \ 36 | # httpParserOnHeaderFieldCallback \ 37 | # httpParserOnHeaderValueCallback \ 38 | # httpParserOnHeadersCompleteCallback \ 39 | # httpParserOnMessageBeginCallback \ 40 | # httpParserOnMessageCompleteCallback \ 41 | # httpParserOnStatusCallback 42 | 43 | # Omit slow proofs in the repository 44 | # HTTPClient_AddHeader \ 45 | # HTTPClient_InitializeRequestHeaders \ 46 | # HTTPClient_Send \ 47 | 48 | # Comparison scripts 49 | BUILD_REPORTS = $(abspath ../../bin/build-viewer-reports) 50 | COMPARE_REPORTS = $(abspath ../../bin/compare-viewer-reports) 51 | 52 | REPORTS1 = $(abspath reports1) 53 | REPORTS2 = $(abspath reports2) 54 | 55 | default: 56 | @ echo Run 57 | @ echo " make clone" 58 | @ echo " make build" 59 | @ echo " make reports1 # Ensure cbmc-viewer runs viewer1" 60 | @ echo " make reports2 # Ensure cbmc-viewer runs viewer2" 61 | @ echo " make compare" 62 | 63 | # Clone the repository and break the proofs 64 | clone: 65 | $(RM) -r $(REPO_DIR) 66 | git clone $(REPO_URL) $(REPO_DIR) 67 | cd $(REPO_DIR); git submodule update --init --checkout --recursive 68 | cd $(REPO_DIR); git apply $(abspath bug.patch) 69 | 70 | # Run the proofs and build the reports (using any version of viewer) 71 | build: 72 | cd $(REPO_DIR)/$(PROOF_DIR) && \ 73 | python3 run-cbmc-proofs.py --proofs $(PROOFS) 74 | 75 | 76 | # Rebuild reports with viewer1 (with cbmc-viewer running viewer1) 77 | reports1: 78 | cd $(REPO_DIR)/$(PROOF_DIR); $(BUILD_REPORTS) $(FLAGS) $(REPORTS1) 79 | 80 | # Rebuild reports with viewer2 (with cbmc-viewer running viewer2) 81 | reports2: 82 | cd $(REPO_DIR)/$(PROOF_DIR); $(BUILD_REPORTS) $(FLAGS) $(REPORTS2) 83 | 84 | # Compare the results of viewer1 and viewer2 85 | compare: 86 | $(COMPARE_REPORTS) $(FLAGS) $(REPORTS1) $(REPORTS2) 87 | 88 | clean: 89 | $(RM) *~ 90 | $(RM) -r $(REPORTS1) 91 | $(RM) -r $(REPORTS2) 92 | 93 | veryclean: clean 94 | $(RM) -r $(REPO_DIR) 95 | 96 | .PHONY: default clone build reports1 reports2 compare clean veryclean 97 | -------------------------------------------------------------------------------- /tests/repo-tests/coreHTTP/bug.patch: -------------------------------------------------------------------------------- 1 | diff --git a/test/cbmc/sources/http_cbmc_state.c b/test/cbmc/sources/http_cbmc_state.c 2 | index 004e428..beb8993 100644 3 | --- a/test/cbmc/sources/http_cbmc_state.c 4 | +++ b/test/cbmc/sources/http_cbmc_state.c 5 | @@ -44,7 +44,7 @@ HTTPRequestHeaders_t * allocateHttpRequestHeaders( HTTPRequestHeaders_t * pReque 6 | 7 | if( pRequestHeaders != NULL ) 8 | { 9 | - __CPROVER_assume( pRequestHeaders->bufferLen < CBMC_MAX_OBJECT_SIZE ); 10 | + // __CPROVER_assume( pRequestHeaders->bufferLen < CBMC_MAX_OBJECT_SIZE ); 11 | pRequestHeaders->pBuffer = mallocCanFail( pRequestHeaders->bufferLen ); 12 | } 13 | 14 | @@ -73,13 +73,13 @@ HTTPRequestInfo_t * allocateHttpRequestInfo( HTTPRequestInfo_t * pRequestInfo ) 15 | 16 | if( pRequestInfo != NULL ) 17 | { 18 | - __CPROVER_assume( pRequestInfo->methodLen < CBMC_MAX_OBJECT_SIZE ); 19 | + // __CPROVER_assume( pRequestInfo->methodLen < CBMC_MAX_OBJECT_SIZE ); 20 | pRequestInfo->pMethod = mallocCanFail( pRequestInfo->methodLen ); 21 | 22 | - __CPROVER_assume( pRequestInfo->hostLen < CBMC_MAX_OBJECT_SIZE ); 23 | + // __CPROVER_assume( pRequestInfo->hostLen < CBMC_MAX_OBJECT_SIZE ); 24 | pRequestInfo->pHost = mallocCanFail( pRequestInfo->hostLen ); 25 | 26 | - __CPROVER_assume( pRequestInfo->pathLen < CBMC_MAX_OBJECT_SIZE ); 27 | + // __CPROVER_assume( pRequestInfo->pathLen < CBMC_MAX_OBJECT_SIZE ); 28 | pRequestInfo->pPath = mallocCanFail( pRequestInfo->pathLen ); 29 | } 30 | 31 | @@ -111,10 +111,10 @@ HTTPResponse_t * allocateHttpResponse( HTTPResponse_t * pResponse ) 32 | 33 | if( pResponse != NULL ) 34 | { 35 | - __CPROVER_assume( pResponse->bufferLen < CBMC_MAX_OBJECT_SIZE ); 36 | + // __CPROVER_assume( pResponse->bufferLen < CBMC_MAX_OBJECT_SIZE ); 37 | pResponse->pBuffer = mallocCanFail( pResponse->bufferLen ); 38 | 39 | - __CPROVER_assume( headerOffset <= pResponse->bufferLen ); 40 | + // __CPROVER_assume( headerOffset <= pResponse->bufferLen ); 41 | 42 | if( pResponse->pBuffer != NULL ) 43 | { 44 | @@ -128,7 +128,7 @@ HTTPResponse_t * allocateHttpResponse( HTTPResponse_t * pResponse ) 45 | { 46 | /* The length of the headers MUST be between the start of the 47 | * headers and the end of the buffer. */ 48 | - __CPROVER_assume( pResponse->headersLen < ( pResponse->bufferLen - headerOffset ) ); 49 | + // __CPROVER_assume( pResponse->headersLen < ( pResponse->bufferLen - headerOffset ) ); 50 | } 51 | 52 | if( pResponse->bufferLen == 0 ) 53 | @@ -137,7 +137,7 @@ HTTPResponse_t * allocateHttpResponse( HTTPResponse_t * pResponse ) 54 | } 55 | else 56 | { 57 | - __CPROVER_assume( bodyOffset <= pResponse->bufferLen ); 58 | + // __CPROVER_assume( bodyOffset <= pResponse->bufferLen ); 59 | } 60 | 61 | if( pResponse->pBuffer != NULL ) 62 | @@ -150,7 +150,7 @@ HTTPResponse_t * allocateHttpResponse( HTTPResponse_t * pResponse ) 63 | * of the buffer. */ 64 | if( pResponse->pBody ) 65 | { 66 | - __CPROVER_assume( pResponse->bodyLen < ( pResponse->bufferLen - bodyOffset ) ); 67 | + // __CPROVER_assume( pResponse->bodyLen < ( pResponse->bufferLen - bodyOffset ) ); 68 | } 69 | } 70 | 71 | @@ -207,11 +207,11 @@ llhttp_t * allocateHttpSendParser( llhttp_t * pHttpParser ) 72 | if( pHttpParser == NULL ) 73 | { 74 | pHttpParser = malloc( sizeof( llhttp_t ) ); 75 | - __CPROVER_assume( pHttpParser != NULL ); 76 | + // __CPROVER_assume( pHttpParser != NULL ); 77 | } 78 | 79 | pHttpParsingContext = allocateHttpSendParsingContext( NULL ); 80 | - __CPROVER_assume( isValidHttpSendParsingContext( pHttpParsingContext ) ); 81 | + // __CPROVER_assume( isValidHttpSendParsingContext( pHttpParsingContext ) ); 82 | pHttpParser->data = ( void * ) pHttpParsingContext; 83 | 84 | return pHttpParser; 85 | @@ -225,16 +225,16 @@ HTTPParsingContext_t * allocateHttpSendParsingContext( HTTPParsingContext_t * pH 86 | if( pHttpParsingContext == NULL ) 87 | { 88 | pHttpParsingContext = malloc( sizeof( HTTPParsingContext_t ) ); 89 | - __CPROVER_assume( pHttpParsingContext != NULL ); 90 | + // __CPROVER_assume( pHttpParsingContext != NULL ); 91 | 92 | pResponse = allocateHttpResponse( NULL ); 93 | - __CPROVER_assume( isValidHttpResponse( pResponse ) && 94 | - pResponse != NULL && 95 | - pResponse->pBuffer != NULL && 96 | - pResponse->bufferLen > 0 ); 97 | + // __CPROVER_assume( isValidHttpResponse( pResponse ) && 98 | + // pResponse != NULL && 99 | + // pResponse->pBuffer != NULL && 100 | + // pResponse->bufferLen > 0 ); 101 | pHttpParsingContext->pResponse = pResponse; 102 | 103 | - __CPROVER_assume( bufferOffset < pResponse->bufferLen ); 104 | + // __CPROVER_assume( bufferOffset < pResponse->bufferLen ); 105 | pHttpParsingContext->pBufferCur = pResponse->pBuffer + bufferOffset; 106 | } 107 | 108 | @@ -258,7 +258,7 @@ llhttp_t * allocateHttpReadHeaderParser( llhttp_t * pHttpParser ) 109 | if( pHttpParser == NULL ) 110 | { 111 | pHttpParser = malloc( sizeof( llhttp_t ) ); 112 | - __CPROVER_assume( pHttpParser != NULL ); 113 | + // __CPROVER_assume( pHttpParser != NULL ); 114 | } 115 | 116 | pFindHeaderContext = allocateFindHeaderContext( NULL ); 117 | @@ -272,7 +272,7 @@ findHeaderContext_t * allocateFindHeaderContext( findHeaderContext_t * pFindHead 118 | if( pFindHeaderContext == NULL ) 119 | { 120 | pFindHeaderContext = malloc( sizeof( findHeaderContext_t ) ); 121 | - __CPROVER_assume( pFindHeaderContext != NULL ); 122 | + // __CPROVER_assume( pFindHeaderContext != NULL ); 123 | } 124 | 125 | return pFindHeaderContext; 126 | -------------------------------------------------------------------------------- /tests/unit-tests/ctags/.gitignore: -------------------------------------------------------------------------------- 1 | coreHTTP 2 | symbol.json 3 | -------------------------------------------------------------------------------- /tests/unit-tests/ctags/Makefile: -------------------------------------------------------------------------------- 1 | # This compares the symbol tables produced by two versions of 2 | # cbmc-viewer using the currently installed version of ctags. Given 3 | # two branches A and B of the cbmc-viewer repository, the command 4 | # 5 | # make COMMIT1=A COMMIT2=B 6 | # 7 | # will install viewer in branch A into a virtual environment and run 8 | # viewer to generate the symbols of a coreHTTP goto binary, do the 9 | # same for branch B, and run diff on the results. 10 | # 11 | # The command will fail (correctly) if the repository contains dirty 12 | # copies of files tracked by the repository. 13 | # 14 | # There are three versions of ctags: legacy, exuberant, and universal. 15 | # On MacOS: 16 | # legacy ctags: installed by default 17 | # exuberant ctags: installed by "brew install ctags" 18 | # universal ctags: installed by "brew install universal-ctags" 19 | 20 | # The FreeRTOS coreHTTP respository 21 | REPO_DIR = coreHTTP 22 | REPO_URL = https://github.com/FreeRTOS/coreHTTP.git 23 | 24 | # The path to the proofs directory in coreHTTP 25 | PROOF_DIR = test/cbmc/proofs 26 | 27 | # The path to a proof under the proofs directory in coreHTTP 28 | PROOF = HTTPClient_AddRangeHeader 29 | 30 | default: clone goto compare 31 | 32 | # Clone the coreHTTP repository 33 | clone: 34 | $(RM) -r $(REPO_DIR) 35 | git clone $(REPO_URL) $(REPO_DIR) 36 | cd $(REPO_DIR) && git submodule update --init --checkout --recursive 37 | 38 | # Make the coreHTTP goto binary 39 | goto: 40 | $(MAKE) -C $(REPO_DIR)/$(PROOF_DIR)/$(PROOF) goto 41 | 42 | # Compare the symbol tables produced by the two versions of 43 | # cbmc-viewer denoted by COMMIT1 and COMMIT2. For each commit, 44 | # install viewer and build the symbol table, and the run diff. 45 | COMMIT1 = master 46 | COMMIT2 = master 47 | SYMBOLS1 = symbol1.json 48 | SYMBOLS2 = symbol2.json 49 | compare: 50 | $(MAKE) VENV=$(VENV) COMMIT=$(COMMIT1) viewer 51 | PATH="$(VENV)/bin:$(PATH)" $(MAKE) SYMBOLS=$(SYMBOLS1) symbols 52 | 53 | $(MAKE) VENV=$(VENV) COMMIT=$(COMMIT2) viewer 54 | PATH="$(VENV)/bin:$(PATH)" $(MAKE) SYMBOLS=$(SYMBOLS2) symbols 55 | 56 | diff $(SYMBOLS1) $(SYMBOLS2) 57 | 58 | # Install the version of cbmc-viewer denoted by COMMIT into a virtual 59 | # environment denoted by VENV. Record the name of the current branch 60 | # in BRANCH, checkout and install the commit, and restore the current 61 | # branch. 62 | # 63 | # NOTE: In what follows, using a SHA to restore the original commit 64 | # leaves the repository in a detached HEAD state, which may be 65 | # annoying to developers, but GitHub workflows check out the commits 66 | # by SHA and not by branch, so `git branch --show-current` returns an 67 | # empty string. 68 | COMMIT = master 69 | VENV = /tmp/cbmc-viewer 70 | viewer: 71 | $(RM) -r $(VENV) 72 | SHA=$$(git rev-parse HEAD) && \ 73 | git checkout $(COMMIT) && \ 74 | $(MAKE) -C ../../.. VENV=$(VENV) develop && \ 75 | git checkout $$SHA 76 | 77 | # Generate the symbol table with cbmc-viewer 78 | SYMBOLS = symbol.json 79 | symbols: 80 | cbmc-viewer symbol \ 81 | --goto $(REPO_DIR)/$(PROOF_DIR)/$(PROOF)/gotos/$(PROOF)_harness.goto \ 82 | --srcdir $(REPO_DIR) > $(SYMBOLS) 83 | 84 | clean: 85 | $(RM) *~ $(SYMBOLS1) $(SYMBOLS2) 86 | 87 | veryclean: clean 88 | $(RM) -r $(REPO_DIR) $(VENV) 89 | 90 | .PHONY: default goto clone compare viewer symbols clean veryclean 91 | -------------------------------------------------------------------------------- /tests/unit-tests/visible-steps/Makefile: -------------------------------------------------------------------------------- 1 | default: 2 | goto-cc -o main.goto main.c 3 | -cbmc main.goto --trace --xml-ui > cbmc.xml 4 | cbmc main.goto --cover location --xml-ui > coverage.xml 5 | cbmc main.goto --show-properties --xml-ui > property.xml 6 | cbmc-viewer --goto main.goto --result cbmc.xml --coverage coverage.xml --property property.xml --srcdir . 7 | diff viewer-trace.json report/json/viewer-trace.json 8 | 9 | clean: 10 | $(RM) -r *~ main.goto cbmc.xml coverage.xml property.xml report 11 | -------------------------------------------------------------------------------- /tests/unit-tests/visible-steps/README.md: -------------------------------------------------------------------------------- 1 | This is the visible-steps unit test for cbmc-viewer: Run "make". 2 | 3 | CBMC marks some steps in the xml and json representation of an error 4 | trace as hidden. A hidden step is generally an internal step of CBMC 5 | that is of no interest to a user debugging a code issue raised by 6 | CBMC, but there are two exceptions: 7 | 8 | * Function invocations and returns should be visible. CBMC marks 9 | malloc invocation as visible and return as hidden. 10 | 11 | * Initialization of static data should be visible. CBMC marks 12 | assignments initializing static data within CBMC initialization as 13 | hidden. 14 | 15 | [Pull request 76](https://github.com/awslabs/aws-viewer-for-cbmc/pull/76) 16 | makes this adjustment, and this unit test is an example. 17 | -------------------------------------------------------------------------------- /tests/unit-tests/visible-steps/main.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | static int global; 4 | 5 | int main() { 6 | int *ptr = malloc(sizeof(int)); 7 | assert(global > 0); 8 | return 0; 9 | } 10 | -------------------------------------------------------------------------------- /tests/unit-tests/visible-steps/viewer-trace.json: -------------------------------------------------------------------------------- 1 | { 2 | "viewer-trace": { 3 | "traces": { 4 | "main.assertion.1": [ 5 | { 6 | "detail": { 7 | "location": { 8 | "file": "", 9 | "function": null, 10 | "line": 35 11 | }, 12 | "name": "__CPROVER_initialize", 13 | "name-path": "__CPROVER_initialize" 14 | }, 15 | "hidden": true, 16 | "kind": "function-call", 17 | "location": { 18 | "file": "MISSING", 19 | "function": "MISSING", 20 | "line": 0 21 | } 22 | }, 23 | { 24 | "detail": { 25 | "lhs": "global", 26 | "lhs-lexical-scope": "global", 27 | "rhs-binary": "00000000 00000000 00000000 00000000", 28 | "rhs-value": "0" 29 | }, 30 | "hidden": true, 31 | "kind": "variable-assignment", 32 | "location": { 33 | "file": "main.c", 34 | "function": null, 35 | "line": 3 36 | } 37 | }, 38 | { 39 | "detail": { 40 | "location": { 41 | "file": "", 42 | "function": null, 43 | "line": 35 44 | }, 45 | "name": "__CPROVER_initialize", 46 | "name-path": "__CPROVER_initialize" 47 | }, 48 | "hidden": true, 49 | "kind": "function-return", 50 | "location": { 51 | "file": "MISSING", 52 | "function": "MISSING", 53 | "line": 0 54 | } 55 | }, 56 | { 57 | "detail": { 58 | "location": { 59 | "file": "main.c", 60 | "function": null, 61 | "line": 5 62 | }, 63 | "name": "main", 64 | "name-path": "main" 65 | }, 66 | "hidden": false, 67 | "kind": "function-call", 68 | "location": { 69 | "file": "main.c", 70 | "function": null, 71 | "line": 5 72 | } 73 | }, 74 | { 75 | "detail": { 76 | "lhs": "ptr", 77 | "lhs-lexical-scope": "main::1::ptr", 78 | "rhs-binary": "00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000", 79 | "rhs-value": "((signed int *)NULL)" 80 | }, 81 | "hidden": false, 82 | "kind": "variable-assignment", 83 | "location": { 84 | "file": "main.c", 85 | "function": "main", 86 | "line": 6 87 | } 88 | }, 89 | { 90 | "detail": { 91 | "location": { 92 | "file": "", 93 | "function": null, 94 | "line": 6 95 | }, 96 | "name": "malloc", 97 | "name-path": "malloc" 98 | }, 99 | "hidden": false, 100 | "kind": "function-call", 101 | "location": { 102 | "file": "main.c", 103 | "function": "main", 104 | "line": 6 105 | } 106 | }, 107 | { 108 | "detail": { 109 | "lhs": "malloc_size", 110 | "lhs-lexical-scope": "malloc::malloc_size", 111 | "rhs-binary": "00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000100", 112 | "rhs-value": "sizeof(signed int) /*4ul*/ " 113 | }, 114 | "hidden": false, 115 | "kind": "parameter-assignment", 116 | "location": { 117 | "file": "main.c", 118 | "function": "main", 119 | "line": 6 120 | } 121 | }, 122 | { 123 | "detail": { 124 | "location": { 125 | "file": "", 126 | "function": null, 127 | "line": 6 128 | }, 129 | "name": "malloc", 130 | "name-path": "malloc" 131 | }, 132 | "hidden": true, 133 | "kind": "function-return", 134 | "location": { 135 | "file": "", 136 | "function": "malloc", 137 | "line": 58 138 | } 139 | }, 140 | { 141 | "detail": { 142 | "lhs": "ptr", 143 | "lhs-lexical-scope": "main::1::ptr", 144 | "rhs-binary": "00000010 00000000 00000000 00000000 00000000 00000000 00000000 00000000", 145 | "rhs-value": "&dynamic_object1" 146 | }, 147 | "hidden": false, 148 | "kind": "variable-assignment", 149 | "location": { 150 | "file": "main.c", 151 | "function": "main", 152 | "line": 6 153 | } 154 | }, 155 | { 156 | "detail": { 157 | "property": "main.assertion.1", 158 | "reason": "assertion global > 0" 159 | }, 160 | "hidden": false, 161 | "kind": "failure", 162 | "location": { 163 | "file": "main.c", 164 | "function": "main", 165 | "line": 7 166 | } 167 | }, 168 | { 169 | "detail": { 170 | "location": { 171 | "file": "main.c", 172 | "function": null, 173 | "line": 5 174 | }, 175 | "name": "main", 176 | "name-path": "main" 177 | }, 178 | "hidden": true, 179 | "kind": "function-return", 180 | "location": { 181 | "file": "main.c", 182 | "function": "main", 183 | "line": 7 184 | } 185 | } 186 | ] 187 | } 188 | } 189 | } --------------------------------------------------------------------------------