├── .github ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE │ ├── bug.md │ └── task.md ├── pull_request_template.md └── workflows │ ├── autorelease.yml │ ├── ci.yml │ ├── danger.yml │ └── dockerhub-release.yml ├── .gitignore ├── .gitmodules ├── .hlint.yaml ├── .reuse └── dep5 ├── .stylish-haskell.yaml ├── .xrefcheck.yaml ├── CHANGES.md ├── LICENSE ├── LICENSES ├── LicenseRef-MIT-OA.txt ├── MPL-2.0.txt └── Unlicense.txt ├── Makefile ├── README.md ├── Setup.hs ├── danger ├── branch-name.rb ├── commit-style.rb ├── helpers.rb ├── instant-checks.rb ├── licenses.rb ├── premerge-checks.rb └── trailing-whitespaces.rb ├── default.nix ├── docs └── output-sample │ ├── README.md │ ├── docs │ └── installation.md │ └── output-sample.png ├── exec └── Main.hs ├── flake.lock ├── flake.lock.license ├── flake.nix ├── ftp-tests ├── Main.hs ├── Test │ └── Xrefcheck │ │ └── FtpLinks.hs ├── Tree.hs └── ftp_root │ ├── empty │ └── .gitkeep │ └── pub │ └── file_exists.txt ├── make └── Makefile ├── package.yaml ├── release └── default.nix ├── scripts ├── upload-docker-image.sh └── validate-stylish.sh ├── src └── Xrefcheck │ ├── CLI.hs │ ├── Command.hs │ ├── Config.hs │ ├── Config │ └── Default.hs │ ├── Core.hs │ ├── Data │ ├── Redirect.hs │ └── URI.hs │ ├── Orphans.hs │ ├── Progress.hs │ ├── Scan.hs │ ├── Scanners.hs │ ├── Scanners │ ├── Markdown.hs │ └── Symlink.hs │ ├── System.hs │ ├── Util.hs │ ├── Util │ ├── Colorize.hs │ └── Interpolate.hs │ └── Verify.hs ├── stack.yaml ├── stack.yaml.lock ├── stack.yaml.lock.license └── tests ├── Main.hs ├── Test └── Xrefcheck │ ├── AnchorsInHeadersSpec.hs │ ├── AnchorsSpec.hs │ ├── CanonicalRelPosixLinkSpec.hs │ ├── ConfigSpec.hs │ ├── IgnoreAnnotationsSpec.hs │ ├── IgnoreRegexSpec.hs │ ├── RedirectChainSpec.hs │ ├── RedirectConfigSpec.hs │ ├── RedirectDefaultSpec.hs │ ├── TimeoutSpec.hs │ ├── TooManyRequestsSpec.hs │ ├── TrailingSlashSpec.hs │ ├── URIParsingSpec.hs │ ├── Util.hs │ └── UtilRequests.hs ├── Tree.hs ├── configs └── github-config.yaml ├── golden ├── check-anchors │ ├── ambiguous-anchors │ │ ├── a.md │ │ └── b.md │ ├── check-anchors.bats │ ├── expected1.gold │ ├── expected2.gold │ └── non-existing-anchors │ │ └── a.md ├── check-autolinks │ ├── check-autolinks.bats │ ├── expected.gold │ └── file-with-autolinks.md ├── check-backslash │ ├── a.md │ ├── check-backslash.bats │ └── expected.gold ├── check-case-sensitivity-anchor │ ├── a.md │ ├── check-case-sensitivity-anchor.bats │ ├── config-github.yaml │ ├── config-gitlab.yaml │ ├── expected1.gold │ └── expected2.gold ├── check-case-sensitivity-path │ ├── a.md │ ├── check-case-sensitivity-path.bats │ ├── config-github.yaml │ ├── config-gitlab.yaml │ ├── dir │ │ └── b.md │ └── expected.gold ├── check-cli │ ├── check-cli.bats │ ├── config-no-scan-ignored.yaml │ ├── expected1.gold │ ├── expected2.gold │ ├── expected3.gold │ ├── single-file.md │ └── to-ignore │ │ └── broken-link.md ├── check-color │ ├── check-color.bats │ ├── color.md │ ├── expected-color.gold │ └── expected-no-color.gold ├── check-dump-config │ ├── .config.yaml │ ├── .xrefcheck.yaml │ └── check-dump-config.bats ├── check-footnotes │ ├── broken-link-in-footnote │ │ └── file-with-footnote-with-broken-link.md │ ├── check-footnotes.bats │ ├── expected.gold │ └── one-word-footnote │ │ └── file-with-one-word-footnote.md ├── check-git │ ├── check-git.bats │ ├── expected1.gold │ ├── expected2.gold │ └── expected3.gold ├── check-html │ ├── check-html.md │ └── check.html.bats ├── check-ignore │ ├── check-ignore.bats │ ├── check-ignore.md │ ├── config-ignore-bad-path-absolute.yaml │ ├── config-ignore-malformed-glob.yaml │ ├── config-ignore.yaml │ ├── expected1.gold │ ├── expected2.gold │ ├── referenced-file.md │ └── to-ignore │ │ └── inner-directory │ │ └── broken_annotation.md ├── check-ignoreExternalRefsTo │ ├── check-ignoreExternalRefsTo.bats │ ├── check-ignoreExternalRefsTo.md │ ├── config-check-disabled.yaml │ ├── config-check-enabled.yaml │ └── expected.gold ├── check-ignoreLocalRefsTo │ ├── check-ignoreLocalRefsTo.bats │ ├── check-ignoreLocalRefsTo.md │ ├── config-ignoreLocalRefsTo.yaml │ ├── expected.gold │ └── one │ │ └── file.md ├── check-ignoreRefsFrom │ ├── check-ignoreRefsFrom.bats │ ├── config-directory.yaml │ ├── config-full-path.yaml │ ├── config-nested-directories.yaml │ ├── config-wildcard.yaml │ ├── expected.gold │ └── ignoreRefsFrom │ │ └── inner-directory │ │ └── bad-reference.md ├── check-images │ ├── check-images.bats │ ├── check-images.md │ └── expected.gold ├── check-local-refs │ ├── check-local-refs.bats │ ├── config-with-virtual-files.yaml │ ├── d0f1.md │ ├── dir1 │ │ ├── d1f1.md │ │ └── dir2 │ │ │ ├── d2f1.md │ │ │ ├── d2f2.md │ │ │ └── d2f3.yaml │ ├── expected1.gold │ ├── expected2.gold │ └── expected3.gold ├── check-redirect-parse │ ├── bad-code.yaml │ ├── bad-on.yaml │ ├── bad-outcome.yaml │ ├── bad-rule.yaml │ ├── bad-rules.yaml │ ├── bad-to.yaml │ ├── check-redirect-parse.bats │ ├── expected1.gold │ ├── expected2.gold │ ├── expected3.gold │ ├── expected4.gold │ ├── expected5.gold │ ├── full-rule.yaml │ ├── no-outcome.yaml │ ├── no-rules.yaml │ ├── only-outcome-on.yaml │ ├── only-outcome-to.yaml │ └── only-outcome.yaml ├── check-scan-errors │ ├── check-scan-errors.bats │ ├── check-scan-errors.md │ ├── check-second-file.md │ ├── expected.gold │ ├── no_link_eof.md │ └── no_paragraph_eof.md ├── check-symlinks │ ├── check-symlinks.bats │ ├── config-ignore.yaml │ ├── dir │ │ └── b.md │ ├── expected1.gold │ └── expected2.gold └── helpers.bash └── markdowns ├── with-annotations ├── ignore_file.md ├── ignore_link.md ├── ignore_paragraph.md ├── no_link.md ├── no_paragraph.md ├── unexpected_ignore_file.md └── unrecognised_option.md └── without-annotations ├── all_checked.md ├── all_ignored.md ├── anchors_in_headers.md ├── anchors_in_headers_with_id_attribute.md └── non_stripped_spaces.md /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # Contribution Guidelines 8 | 9 | ## Reporting issues 10 | 11 | Please [open an issue](https://github.com/serokell/xrefcheck/issues/new/choose) if you find a bug or have a feature request. 12 | Note: you need to login (e. g. using your GitHub account) first. 13 | Before submitting a bug report or feature request, check to make sure it hasn't already been submitted 14 | 15 | The more detailed your report is, the faster it can be resolved. 16 | If you report a bug, please provide steps to reproduce this bug and revision of code in which this bug reproduces. 17 | 18 | ## Code 19 | 20 | If you would like to contribute code to fix a bug, add a new feature, or 21 | otherwise improve our project, pull requests are most welcome. 22 | 23 | Our pull request template contains a [checklist](./pull_request_template.md#white_check_mark-checklist-for-your-pull-request) of acceptance criteria for your pull request. 24 | Please read it before you start contributing and make sure your contributions adhere to this checklist. 25 | 26 | ## Legal 27 | 28 | We want to make sure that our projects come with correct licensing information 29 | and that this information is machine-readable, thus we are following the 30 | [REUSE Practices][reuse] – feel free to click the link and read about them, 31 | but, basically, it all boils down to the following: 32 | 33 | * Add the following header at the very top (but below the shebang, if there 34 | is one) of each source file in the repository (yes, each and every source 35 | file – it is not as hard as it might sound): 36 | 37 | ```haskell 38 | {- SPDX-FileCopyrightText: 2019 Serokell 39 | - 40 | - SPDX-License-Identifier: MPL-2.0 41 | -} 42 | ``` 43 | 44 | (This is an example for Haskell; adapt it as needed for other languages.) 45 | 46 | The license identifier should be the same as the one in the `LICENSE` file. 47 | 48 | * If you are copying any source files from some other project, and they do not 49 | contain a header with a copyright and a machine-readable license identifier, 50 | add it, but be extra careful and make sure that information you are recording 51 | is correct. 52 | 53 | If the license of the file is different from the one used in the project and 54 | you do not plan to relicense it, use the appropriate license identifier and 55 | make sure the license text exists in the `LICENSES` directory. 56 | 57 | If the file contains the entire license in its header, it is best to move the 58 | text to a separate file in the `LICENSES` directory and leave a reference. 59 | 60 | * If you are copying pieces of code from some other project, leave a note in the 61 | comments, stating where you copied it from, who is the copyright owner, and 62 | what license applies. 63 | 64 | * All the same rules apply to documentation that is stored in the repository. 65 | 66 | These simple rules should cover most of situation you are likely to encounter. 67 | In case of doubt, consult the [REUSE Practices][reuse] document. 68 | 69 | [reuse]: https://reuse.software/spec/ 70 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: If you think our software behaves not the way it should, report a bug 4 | title: "[BUG]" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | # Description 11 | 12 | 15 | 16 | # To Reproduce 17 | 18 | Steps to reproduce the behavior: 19 | 25 | 26 | # Expected behavior 27 | 28 | 31 | 32 | # Environment 33 | 34 | - OS 35 | - branch/revision 36 | 37 | # Additional context 38 | 39 | 42 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/task.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Task 3 | about: Suggest a task for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | # Clarification and motivation 11 | 12 | 15 | 16 | # Acceptance criteria 17 | 18 | 21 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | 8 | 9 | ## Related issue(s) 10 | 11 | 18 | 19 | Fixes # 20 | 21 | ## :white_check_mark: Checklist for your Pull Request 22 | 23 | Ideally a PR has all of the checkmarks set. 24 | 25 | If something in this list is irrelevant to your PR, you should still set this 26 | checkmark indicating that you are sure it is dealt with (be that by irrelevance). 27 | 28 | #### Related changes (conditional) 29 | 30 | - Tests 31 | - [ ] If I added new functionality, I added tests covering it. 32 | - [ ] If I fixed a bug, I added a regression test to prevent the bug from 33 | silently reappearing again. 34 | 35 | - Documentation 36 | - [ ] I checked whether I should update the docs and did so if necessary: 37 | - [README](https://github.com/serokell/xrefcheck/tree/master/README.md) 38 | - Haddock 39 | 40 | - Public contracts 41 | - [ ] Any modifications of public contracts comply with the [Evolution 42 | of Public Contracts](https://www.notion.so/serokell/Evolution-of-Public-Contracts-2a3bf7971abe4806a24f63c84e7076c5) policy. 43 | - [ ] I added an entry to the [changelog](https://github.com/serokell/xrefcheck/tree/master/CHANGES.md) if my changes are visible to the users 44 | and 45 | - [ ] provided a migration guide for breaking changes if possible 46 | 47 | #### Stylistic guide (mandatory) 48 | 49 | - [ ] My commits comply with [the policy used in Serokell](https://www.notion.so/serokell/Where-and-how-to-commit-your-work-58f8973a4b3142c8abbd2e6fd5b3a08e). 50 | - [ ] My code complies with the [style guide](https://github.com/serokell/style/blob/master/haskell.md). 51 | 52 | #### ✓ Release Checklist 53 | 54 | - [ ] I updated the version number in `package.yaml`. 55 | - [ ] I updated the [changelog](https://github.com/serokell/xrefcheck/tree/master/CHANGES.md) and moved everything 56 | under the "Unreleased" section to a new section for this release version. 57 | - [ ] (After merging) I edited the [auto-release](https://github.com/serokell/xrefcheck/releases/tag/auto-release). 58 | * Change the tag and title using the format `vX.Y.Z`. 59 | * Write a summary of all user-facing changes. 60 | * Deselect the "This is a pre-release" checkbox at the bottom. 61 | - [ ] (After merging) I updated [`xrefcheck-action`](https://github.com/serokell/xrefcheck-action#updating-supported-versions). 62 | - [ ] (After merging) I uploaded the package to [hackage](https://hackage.haskell.org/package/xrefcheck). 63 | -------------------------------------------------------------------------------- /.github/workflows/autorelease.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Serokell 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | name: master-update 6 | 7 | on: 8 | push: 9 | branches: 10 | - master 11 | 12 | jobs: 13 | release: 14 | runs-on: [self-hosted, nix] 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - name: Create a pre-release 19 | env: 20 | OVERWRITE_RELEASE: true 21 | # https://github.com/serokell/serokell.nix/blob/c1e2a33040438443c7721523e897db5e32a52a74/overlay/github.nix#L26 22 | run: | 23 | nix run .#github.autorelease -- "$(nix-build ./release)" "Automatic release on "$(date +\"%Y%m%d%H%M\")"" 24 | 25 | - name: Push latest image to dockerhub 26 | run: | 27 | nix build -L .#docker-image 28 | nix shell .#skopeo -c ./scripts/upload-docker-image.sh "docker-archive:$(readlink result)" "docker://docker.io/serokell/xrefcheck:latest" 29 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Kowainik 2 | # SPDX-FileCopyrightText: 2022 Serokell 3 | # 4 | # SPDX-License-Identifier: MPL-2.0 5 | 6 | # Sources: 7 | # • https://github.com/kowainik/validation-selective/blob/5b46cd4810bbaa09b704062ebbfa2bb47137425d/.github/workflows/ci.yml 8 | # • https://kodimensional.dev/github-actions 9 | # • https://github.com/serokell/tztime/blob/336f585c2c7125a8ba58ffbf3dbea4f36a7c40e7/.github/workflows/ci.yml 10 | 11 | name: CI 12 | 13 | on: 14 | pull_request: 15 | push: 16 | branches: master 17 | 18 | jobs: 19 | validate: 20 | runs-on: [self-hosted, nix] 21 | steps: 22 | - uses: actions/checkout@v4 23 | 24 | - name: reuse 25 | run: nix build -L .#checks.x86_64-linux.reuse-lint 26 | # Run step even if the previous one has failed 27 | if: success() || failure() 28 | 29 | - name: hlint 30 | run: nix build -L .#checks.x86_64-linux.hlint 31 | if: success() || failure() 32 | 33 | - name: shellcheck 34 | run: nix build -L .#checks.x86_64-linux.shellcheck 35 | if: success() || failure() 36 | 37 | - name: stylish-haskell 38 | run: nix build -L .#checks.x86_64-linux.stylish-haskell 39 | if: success() || failure() 40 | 41 | - name: check-trailing-whitespace 42 | run: nix build -L .#checks.x86_64-linux.trailing-whitespace 43 | if: success() || failure() 44 | 45 | xrefcheck-build-and-test-windows: 46 | runs-on: windows-latest 47 | strategy: 48 | matrix: 49 | stack: ["3.1.1"] 50 | ghc: ["9.6.6"] 51 | include: 52 | - ghc: "9.6.6" 53 | stackyaml: stack.yaml 54 | steps: 55 | - uses: actions/checkout@v4 56 | with: 57 | submodules: 'true' 58 | 59 | - uses: haskell/actions/setup@v2.4.3 60 | id: setup-haskell-stack 61 | name: Setup Haskell Stack 62 | with: 63 | ghc-version: ${{ matrix.ghc }} 64 | stack-version: ${{ matrix.stack }} 65 | 66 | - uses: actions/cache@v3 67 | name: Cache stack root 68 | with: 69 | path: ~/AppData/Roaming/stack 70 | key: ${{ runner.os }}-${{ matrix.ghc }}-stack 71 | 72 | - uses: actions/cache@v3 73 | name: Cache AppData/Local/Programs/stack 74 | with: 75 | path: ~/AppData/Local/Programs/stack 76 | key: ${{ runner.os }}-${{ matrix.ghc }}-appdata-stack 77 | 78 | - name: Build 79 | run: | 80 | stack build --system-ghc --stack-yaml ${{ matrix.stackyaml }} --test --bench --no-run-tests --no-run-benchmarks --ghc-options '-Werror' 81 | 82 | - name: stack test xrefcheck:xrefcheck-tests 83 | run: | 84 | stack test --system-ghc --stack-yaml ${{ matrix.stackyaml }} xrefcheck:xrefcheck-tests 85 | 86 | - name: install xrefcheck to use with golden tests 87 | run: | 88 | stack --system-ghc --stack-yaml ${{ matrix.stackyaml }} install; 89 | 90 | - uses: mig4/setup-bats@v1 91 | name: Setup bats 92 | 93 | - name: Golden tests 94 | run: | 95 | export PATH=$PATH:/c/Users/runneradmin/AppData/Roaming/local/bin; 96 | bats ./tests/golden/** 97 | shell: bash 98 | 99 | xrefcheck-build-and-test-nix: 100 | runs-on: [self-hosted, nix] 101 | steps: 102 | - uses: actions/checkout@v4 103 | with: 104 | submodules: true 105 | 106 | - name: build 107 | run: nix build -L .#xrefcheck 108 | 109 | - name: test 110 | run: nix build -L .#checks.x86_64-linux.test 111 | 112 | - name: Golden tests (bats) 113 | run: nix shell .#bats .#diffutils .#xrefcheck -c bash -c "cd tests/golden/ && bats ./**" 114 | 115 | - name: windows cross-compilation 116 | run: | 117 | nix build -L .#xrefcheck-windows 118 | echo "WINDOWS_BINARY_PATH=$(readlink -f result)" >> $GITHUB_ENV 119 | 120 | - name: Upload windows executable 121 | uses: actions/upload-artifact@v4 122 | with: 123 | name: xrefcheck-windows 124 | path: ${{ env.WINDOWS_BINARY_PATH }}/bin/* 125 | 126 | - name: docker-image 127 | run: nix build -L .#docker-image 128 | 129 | - name: static binary 130 | run: | 131 | nix build -L .#xrefcheck-static 132 | echo "STATIC_BINARY_PATH=$(readlink -f result)" >> $GITHUB_ENV 133 | 134 | - name: Upload static binary 135 | uses: actions/upload-artifact@v4 136 | with: 137 | name: xrefcheck-static 138 | path: ${{ env.STATIC_BINARY_PATH }}/bin/xrefcheck 139 | 140 | - name: Xrefcheck itself 141 | run: nix run . -L 142 | -------------------------------------------------------------------------------- /.github/workflows/danger.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Serokell 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | name: Danger 6 | 7 | on: [pull_request] 8 | 9 | jobs: 10 | run-danger-checks: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | with: 15 | fetch-depth: 0 16 | - uses: ruby/setup-ruby@v1 17 | with: 18 | ruby-version: '3.3' 19 | bundler-cache: true 20 | - uses: MeilCli/danger-action@v5 21 | name: Instant checks 22 | with: 23 | install_path: 'vendor/bundle' 24 | danger_file: './danger/instant-checks.rb' 25 | danger_id: 'instant-checks' 26 | danger_version: '= 9.4.2' 27 | env: 28 | DANGER_GITHUB_API_TOKEN: ${{ secrets.DANGER_BOT_TOKEN }} 29 | - uses: MeilCli/danger-action@v5 30 | name: Premerge checks 31 | with: 32 | install_path: 'vendor/bundle' 33 | danger_file: './danger/premerge-checks.rb' 34 | danger_id: 'premerge-checks' 35 | danger_version: '= 9.4.2' 36 | env: 37 | DANGER_GITHUB_API_TOKEN: ${{ secrets.DANGER_BOT_TOKEN }} 38 | -------------------------------------------------------------------------------- /.github/workflows/dockerhub-release.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Serokell 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | name: dockerhub-release 6 | 7 | on: 8 | push: 9 | tags: 10 | - v[0-9]+.* 11 | 12 | jobs: 13 | dockerhub-release: 14 | runs-on: [self-hosted, nix] 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - name: Push release image to dockerhub 19 | run: | 20 | export DOCKERHUB_PASSWORD=${{ env.DOCKERHUB_PASSWORD }} 21 | nix build .#docker-image 22 | nix shell .#skopeo -c ./scripts/upload-docker-image.sh "docker-archive:$(readlink result)" "docker://docker.io/serokell/xrefcheck:${{ github.ref_name }}" 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2018-2021 Serokell 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | dist 6 | dist-* 7 | *.o 8 | *.hi 9 | *.chi 10 | *.chs.h 11 | *.dyn_o 12 | *.dyn_hi 13 | .hpc 14 | .hsenv 15 | *cabal* 16 | .cabal-sandbox/ 17 | cabal.sandbox.config 18 | *.prof 19 | *.aux 20 | *.hp 21 | *.eventlog 22 | .stack-work/ 23 | .HTF/ 24 | .ghc.environment.* 25 | result 26 | result-* 27 | hie.yaml 28 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Serokell 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | [submodule "tests/golden/helpers/bats-assert"] 6 | path = tests/golden/helpers/bats-assert 7 | url = git@github.com:bats-core/bats-assert.git 8 | [submodule "tests/golden/helpers/bats-support"] 9 | path = tests/golden/helpers/bats-support 10 | url = git@github.com:bats-core/bats-support.git 11 | [submodule "tests/golden/helpers/bats-file"] 12 | path = tests/golden/helpers/bats-file 13 | url = https://github.com/bats-core/bats-file.git 14 | -------------------------------------------------------------------------------- /.hlint.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Serokell 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | ########################################################################### 6 | # Settings 7 | ########################################################################### 8 | 9 | - arguments: [-XTypeApplications, -XRecursiveDo, -XBlockArguments, -XQuasiQuotes] 10 | 11 | # These are just too annoying 12 | - ignore: { name: Redundant do } 13 | - ignore: { name: Redundant bracket } 14 | - ignore: { name: Redundant lambda } 15 | - ignore: { name: Redundant flip } 16 | - ignore: { name: Move brackets to avoid $ } 17 | - ignore: { name: Avoid lambda using `infix` } 18 | 19 | # Losing variable names can be not-nice 20 | - ignore: { name: Eta reduce } 21 | - ignore: { name: Avoid lambda } 22 | 23 | # Humans know better 24 | - ignore: { name: Use camelCase } 25 | - ignore: { name: Use const } 26 | - ignore: { name: Use section } 27 | - ignore: { name: Use if } 28 | - ignore: { name: Use notElem } 29 | - ignore: { name: Use fromMaybe } 30 | - ignore: { name: Replace case with fromMaybe } 31 | - ignore: { name: Use maybe } 32 | - ignore: { name: Use fmap } 33 | - ignore: { name: Use foldl } 34 | - ignore: { name: "Use :" } 35 | - ignore: { name: Use ++ } 36 | - ignore: { name: Use || } 37 | - ignore: { name: Use && } 38 | - ignore: { name: 'Use ?~' } 39 | - ignore: { name: Use <$> } 40 | - ignore: { name: Use unless } 41 | 42 | # Sometimes [Char] is okay (if it means "a sequence of characters") 43 | - ignore: { name: Use String } 44 | 45 | # Sometimes TemplateHaskell is needed to please stylish-haskell 46 | - ignore: { name: Unused LANGUAGE pragma } 47 | 48 | # Some 'data' records will be extended with more fields later, 49 | # so they shouldn't be replaced with 'newtype' blindly 50 | - ignore: { name: Use newtype instead of data } 51 | 52 | ########################################################################### 53 | # Various stuff 54 | ########################################################################### 55 | 56 | - warn: 57 | name: "Avoid 'both'" 58 | lhs: both 59 | rhs: Control.Lens.each 60 | note: | 61 | If you use 'both' on a 2-tuple and later it's accidentally 62 | replaced with a longer tuple, 'both' will be silently applied to only 63 | the *last two elements* instead of failing with a type error. 64 | * If you want to traverse all elements of the tuple, use 'each'. 65 | * If 'both' is used on 'Either' here, replace it with 'chosen'. 66 | 67 | - warn: { lhs: either (const True) (const False), rhs: isLeft } 68 | - warn: { lhs: either (const False) (const True), rhs: isRight } 69 | 70 | - warn: { lhs: map fst &&& map snd, rhs: unzip } 71 | 72 | - warn: 73 | name: "'fromIntegral' is unsafe without type annotations." 74 | lhs: fromIntegral x 75 | rhs: fromIntegral @t1 @t2 x 76 | - warn: 77 | name: "'fromIntegral' is unsafe without TWO type annotations." 78 | lhs: fromIntegral @t1 x 79 | rhs: fromIntegral @t1 @t2 x 80 | - warn: 81 | name: "Avoid the use of '(+||)' and '(||+)'" 82 | lhs: '(Fmt.+||)' 83 | rhs: '(Fmt.+|)' 84 | note: "The use of '(+||)' may result in outputting raw Haskell into user-facing code" 85 | - warn: 86 | name: "Avoid the use of '(+||)' and '(||+)'" 87 | lhs: '(Fmt.||+)' 88 | rhs: '(Fmt.|+)' 89 | note: "The use of '(||+)' may result in outputting raw Haskell into user-facing code" 90 | - warn: 91 | name: "Avoid the use of '(||++||)'" 92 | lhs: '(Fmt.||++||)' 93 | rhs: '(Fmt.|++|)' 94 | note: "The use of '(||++||)' may result in outputting raw Haskell into user-facing code" 95 | - warn: 96 | name: "Avoid the use of '(||++|)'" 97 | lhs: '(Fmt.||++|)' 98 | rhs: '(Fmt.|++|)' 99 | note: "The use of '(||++|)' may result in outputting raw Haskell into user-facing code" 100 | - warn: 101 | name: "Avoid colorize function that ignore ColorMode" 102 | lhs: 'System.Console.Pretty.colorize' 103 | rhs: 'Xrefcheck.Util.Colorize.colorizeIfNeeded' 104 | - warn: 105 | name: "Avoid color function that ignore ColorMode" 106 | lhs: 'System.Console.Pretty.color' 107 | rhs: 'Xrefcheck.Util.Colorize.colorIfNeeded' 108 | - warn: 109 | name: "Avoid style function that ignore ColorMode" 110 | lhs: 'System.Console.Pretty.style' 111 | rhs: 'Xrefcheck.Util.Colorize.styleIfNeeded' 112 | - warn: 113 | name: "Avoid functions that generate extra trailing newlines/whitespaces" 114 | lhs: 'Fmt.indentF' 115 | rhs: 'Xrefcheck.Util.Interpolate.interpolateIndentF' 116 | - warn: 117 | name: "Avoid functions that generate extra trailing newlines/whitespaces" 118 | lhs: 'Fmt.blockListF' 119 | rhs: 'Xrefcheck.Util.Interpolate.interpolateBlockListF' 120 | - warn: 121 | name: "Avoid functions that generate extra trailing newlines/whitespaces" 122 | lhs: "Fmt.blockListF'" 123 | rhs: "Xrefcheck.Util.Interpolate.interpolateBlockListF'" 124 | -------------------------------------------------------------------------------- /.reuse/dep5: -------------------------------------------------------------------------------- 1 | Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | 3 | Files: .github/pull_request_template.md .github/ISSUE_TEMPLATE/*.md 4 | Copyright: 2018-2019 Serokell 5 | License: Unlicense 6 | 7 | Files: ftp-tests/ftp_root/**/* 8 | Copyright: 2021 Serokell 9 | License: Unlicense 10 | 11 | Files: tests/configs/github-config.yaml 12 | Copyright: 2019-2021 Serokell 13 | License: Unlicense 14 | 15 | Files: tests/golden/**/*.gold 16 | Copyright: 2022 Serokell 17 | License: Unlicense 18 | 19 | Files: docs/**/*.png 20 | Copyright: 2022 Serokell 21 | License: Unlicense 22 | -------------------------------------------------------------------------------- /.stylish-haskell.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Serokell 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | steps: 6 | - simple_align: 7 | cases: false 8 | top_level_patterns: false 9 | records: false 10 | - imports: 11 | align: none 12 | list_align: after_alias 13 | pad_module_names: false 14 | long_list_align: new_line 15 | empty_list_align: inherit 16 | list_padding: 2 17 | separate_lists: true 18 | space_surround: false 19 | post_qualify: true 20 | - trailing_whitespace: {} 21 | columns: 100 22 | newline: native 23 | language_extensions: 24 | - BangPatterns 25 | - BlockArguments 26 | - ConstraintKinds 27 | - DataKinds 28 | - DefaultSignatures 29 | - DeriveAnyClass 30 | - DeriveDataTypeable 31 | - DeriveGeneric 32 | - DerivingStrategies 33 | - DerivingVia 34 | - EmptyCase 35 | - ExistentialQuantification 36 | - ExplicitNamespaces 37 | - FlexibleContexts 38 | - FlexibleInstances 39 | - FunctionalDependencies 40 | - GADTs 41 | - GeneralizedNewtypeDeriving 42 | - ImportQualifiedPost 43 | - LambdaCase 44 | - MultiParamTypeClasses 45 | - MultiWayIf 46 | - NamedFieldPuns 47 | - NumericUnderscores 48 | - OverloadedLabels 49 | - OverloadedStrings 50 | - PatternSynonyms 51 | - QuantifiedConstraints 52 | - QuasiQuotes 53 | - RebindableSyntax 54 | - RecordWildCards 55 | - RecursiveDo 56 | - ScopedTypeVariables 57 | - StandaloneDeriving 58 | - TemplateHaskell 59 | - TemplateHaskellQuotes 60 | - TupleSections 61 | - TypeApplications 62 | - TypeFamilies 63 | - TypeOperators 64 | - ViewPatterns 65 | -------------------------------------------------------------------------------- /.xrefcheck.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2018-2021 Serokell 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | exclusions: 6 | ignore: 7 | - tests/markdowns/**/* 8 | - tests/golden/**/* 9 | - docs/output-sample/**/* 10 | 11 | scanners: 12 | markdown: 13 | flavor: GitHub 14 | -------------------------------------------------------------------------------- /LICENSES/LicenseRef-MIT-OA.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | Copyright (c) 2021-2022 Oxhead Alpha 3 | Copyright (c) 2019-2021 Tocqueville Group 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is furnished 10 | to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice (including the next 13 | paragraph) shall be included in all copies or substantial portions of the 14 | Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 18 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS 19 | OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF 21 | OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /LICENSES/Unlicense.txt: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Serokell 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | .PHONY: xrefcheck test lint stylish clean bats all 6 | 7 | # Build target from the common utility Makefile 8 | MAKEU = $(MAKE) -C make/ 9 | 10 | MAKE_PACKAGE = $(MAKEU) PACKAGE=xrefcheck 11 | 12 | xrefcheck: 13 | $(MAKE_PACKAGE) dev 14 | 15 | # Runs all tests. 16 | # Usage: 17 | # * make test 18 | # * make test BATSFILTER='test name' TEST_ARGUMENTS='--match \"test name\"' 19 | test: 20 | make test-unit 21 | make test-ftp 22 | make bats 23 | 24 | test-dumb-term: 25 | $(MAKE_PACKAGE) test-dumb-term 26 | 27 | test-hide-successes: 28 | $(MAKE_PACKAGE) test-hide-successes 29 | 30 | clean: 31 | $(MAKE_PACKAGE) clean 32 | 33 | lint: 34 | hlint . 35 | 36 | stylish: 37 | find . -name '.stack-work' -prune -o -name '.dist-newstyle' -prune -o -name '*.hs' -exec stylish-haskell -i '{}' \; 38 | 39 | #################################### 40 | # Individual test suites 41 | 42 | test-unit: 43 | $(MAKEU) test PACKAGE="xrefcheck:test:xrefcheck-tests" 44 | 45 | test-ftp: 46 | vsftpd \ 47 | -orun_as_launching_user=yes \ 48 | -olisten_port=2221 \ 49 | -olisten=yes \ 50 | -oftp_username=$(shell whoami) \ 51 | -oanon_root=./ftp-tests/ftp_root \ 52 | -opasv_min_port=2222 \ 53 | -ohide_file='{.*}' \ 54 | -odeny_file='{.*}' \ 55 | -oseccomp_sandbox=no \ 56 | -olog_ftp_protocol=yes \ 57 | -oxferlog_enable=yes \ 58 | -ovsftpd_log_file=/tmp/vsftpd.log & 59 | 60 | $(MAKEU) test PACKAGE="xrefcheck:test:ftp-tests" \ 61 | TEST_ARGUMENTS="--ftp-host ftp://127.0.0.1:2221" 62 | 63 | # Usage: 64 | # * make bats 65 | # * make bats BATSFILTER="test name" 66 | # * make bats BATSFILTER="regex to match several tests" 67 | bats: 68 | stack install --fast xrefcheck:exe:xrefcheck 69 | git submodule update --init --recursive 70 | bats ./tests/golden/** -f $(if $(BATSFILTER),"$(BATSFILTER)",".*") 71 | -------------------------------------------------------------------------------- /Setup.hs: -------------------------------------------------------------------------------- 1 | {- SPDX-FileCopyrightText: 2018-2019 Serokell 2 | - 3 | - SPDX-License-Identifier: MPL-2.0 4 | -} 5 | 6 | import Distribution.Simple 7 | main = defaultMain 8 | -------------------------------------------------------------------------------- /danger/branch-name.rb: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Oxhead Alpha 2 | # SPDX-License-Identifier: LicenseRef-MIT-OA 3 | 4 | require_relative 'helpers' 5 | 6 | def check_branch_name 7 | # Proper branch name 8 | if branch_match = githost.branch_for_head.match(/([^\/]+)\/([^\-]+)-(.+)/) 9 | nick, issue_id, desc = branch_match.captures 10 | 11 | # We've decided not to put any restrictions on nickname for now 12 | 13 | unless /^(#\d+|chore)$/.match?(issue_id) 14 | warn( 15 | "Bad issue ID in branch name.\n"\ 16 | "Valid format for issue IDs: `#123` or `chore`." 17 | ) 18 | end 19 | 20 | weird_chars = desc.scan(/[^a-zA-Z\-\d]/) 21 | unless weird_chars.empty? 22 | warn( 23 | "Please, only use letters, digits and dashes in the branch name. 24 | Found: #{weird_chars}" 25 | ) 26 | end 27 | elsif 28 | warn( 29 | "Please use `/-`` format for branch names.`\n"\ 30 | "Example: `lazyman/#123-my-commit`" 31 | ) 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /danger/commit-style.rb: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Oxhead Alpha 2 | # SPDX-License-Identifier: LicenseRef-MIT-OA 3 | 4 | require_relative 'helpers' 5 | 6 | def wrap_workarounds(fun) 7 | return lambda { |msg| 8 | method(fun).call(msg + "\nSee also \\[Note\\].") 9 | markdown( 10 | "\\[Note\\]: Skip this check by adding `wip`, `tmp` or `[temporary]` to the commit subject. "\ 11 | "Fixup commits (marked with `fixup!` or `squash!`) are also exempt from this check.") 12 | } 13 | end 14 | 15 | def check_commit_style (mywarn = wrap_workarounds(:warn), myfail = wrap_workarounds(:fail)) 16 | # Proper commit style 17 | # Note: we do not use commit_lint plugin because it triggers on fixup commits 18 | git.commits.each { |commit| 19 | if commit.fixup? || commit.wip? 20 | next 21 | end 22 | 23 | subject = commit.subject 24 | subject_payload = subject.sub(issue_tags_pattern, "") 25 | subject_ticked = commit.subject_ticked 26 | 27 | unless has_valid_issue_tags(subject) 28 | # If any of these substrings is included into commit message, 29 | # we are fine with issue tag absence. 30 | exclusions = [ 31 | # In lower-case 32 | "changelog" 33 | ] 34 | if exclusions.none? { |exc| subject.downcase.include?(exc) } 35 | mywarn.call("In #{commit.sha} message lacks issue id: #{subject_ticked}.") 36 | end 37 | end 38 | 39 | if subject_payload.start_with?(" ") 40 | mywarn.call("Extra space in commit #{commit.sha} subject after the issue tags: #{subject_ticked}.") 41 | elsif !subject_payload.start_with?(/[A-Z]/) 42 | mywarn.call("In #{commit.sha} subject does not begin with an uppercase letter: #{subject_ticked}.") 43 | end 44 | 45 | if subject[-1..-1] == '.' 46 | mywarn.call("In #{commit.sha} message ends with a dot: #{subject_ticked} :fire_engine:") 47 | end 48 | 49 | if subject.length > 90 50 | myfail.call("Nooo, such long commit message names do not work (#{commit.sha}).") 51 | elsif subject.length > 72 52 | mywarn.call("In commit #{commit.sha} message is too long (#{subject.length} chars), "\ 53 | "please keep its length within 72 characters.") 54 | end 55 | 56 | if commit.message_body.empty? 57 | # If any of these substrings is included into commit message, 58 | # we are fine with commit description absence. 59 | exclusions = [ 60 | # In lower-case 61 | "changelog" 62 | ] 63 | unless commit.chore? || exclusions.any? { |exc| subject.downcase.include?(exc) } 64 | myfail.call( 65 | "Commit #{commit.sha} lacks description :unamused:\n"\ 66 | "Commits marked as `[Chore]` are exempt from this check." 67 | ) 68 | end 69 | else 70 | # Checks on description 71 | 72 | if !commit.blank_line_after_subject? 73 | mywarn.call("In #{commit.sha} blank line is missing after the commit's subject.") 74 | end 75 | 76 | if !commit.chore? 77 | description_patterns = [ 78 | /^Problem:[ \n].*^Solution:[ \n]/m, 79 | /And yes, I don't care about templates/ 80 | ] 81 | unless description_patterns.any? { |pattern| pattern.match?(commit.description) } 82 | mywarn.call( 83 | "Description of #{commit.sha} does not follow the template.\n"\ 84 | "Try `Problem:`/`Solution:` structure.\n"\ 85 | "If you really have to, you can add `And yes, I don't care about templates` "\ 86 | "to the commit message body." 87 | ) 88 | end 89 | end 90 | end 91 | 92 | } 93 | end 94 | -------------------------------------------------------------------------------- /danger/helpers.rb: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Oxhead Alpha 2 | # SPDX-License-Identifier: LicenseRef-MIT-OA 3 | 4 | ### Some convenient extensions and helpers ### 5 | 6 | class Danger::Dangerfile 7 | # Unified `github`/`gitlab` variable. 8 | def githost 9 | if defined?(github) 10 | githost = github 11 | elsif defined?(gitlab) 12 | githost = gitlab 13 | else 14 | error "Failed to figure out which service are we running from." 15 | end 16 | end 17 | 18 | # The original PR title (excludes "Draft" tags). 19 | def pr_title_payload 20 | githost.mr_title 21 | .sub(/^(Draft|WIP): /, "") 22 | .sub(/^\[Draft\] /, "") 23 | end 24 | alias_method :mr_title_payload, :pr_title_payload 25 | end 26 | 27 | class Git::Diff::DiffFile 28 | # When a file is renamed (e.g. with `git mv`) 'path' will return the old 29 | # path, this is true even if the file was modified a little. 30 | # However we'd probably like to access the destination path instead, so 31 | # this parses the new path from the 'file.patch'. 32 | def destination_path 33 | rename_match = /(?<=(\nrename to ))(\S)*/.match(self.patch) 34 | if rename_match.nil? 35 | self.path 36 | else 37 | rename_match.to_s 38 | end 39 | end 40 | end 41 | 42 | # Add some helpers to Commit class. 43 | class Git::Object::Commit 44 | # Commit subject (unlike the 'message' field which includes description). 45 | def subject 46 | self.message.lines.first.rstrip 47 | end 48 | alias_method :message_subject, :subject 49 | 50 | def subject_ticked 51 | "`" + self.subject.gsub("`", "'") + "`" 52 | end 53 | 54 | # Commit description. 55 | # If absent, set to empty string. 56 | def description 57 | self.message.lines.drop(1).drop_while{ |s| s == "\n" }.join 58 | end 59 | alias_method :message_body, :description 60 | 61 | # Whether there is a blank line between commit subject and body. 62 | def blank_line_after_subject? 63 | self.message.lines[1] == "\n" 64 | end 65 | 66 | # Whether this commit is fixup commit. 67 | def fixup? 68 | return /\bfixup!|\bsquash!/.match?(subject) 69 | end 70 | 71 | # Whether this commit is a temporary commit. 72 | def wip? 73 | return /\bwip\b|\btmp\b|\[temporary\]/i.match?(subject) 74 | end 75 | 76 | # Whether this commit is a minor chore commit. 77 | # Such commits usually have an obvious purpose and are not related to the 78 | # business logic. 79 | def chore? 80 | return subject.include?("[Chore]") 81 | end 82 | 83 | end 84 | 85 | module Danger::Helpers::CommentsHelper 86 | # By default, every comment for a particular source code also includes 87 | # the name of the referred file. 88 | # 89 | # We don't need this feature. 90 | # The source code welcomes us to override the respective method, and this is 91 | # exactly what we do. 92 | def markdown_link_to_message(_, _) 93 | "" 94 | end 95 | end 96 | 97 | # Example: `[Chore][#123] My commit` 98 | def issue_tags_pattern 99 | /^(\[(#\d+|Chore|Style)\])+ (?=\w)/ 100 | end 101 | 102 | # Whether a string starts with an appropriate ticket tag. 103 | def has_valid_issue_tags(name) 104 | return name.start_with?(issue_tags_pattern) 105 | end 106 | -------------------------------------------------------------------------------- /danger/instant-checks.rb: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Oxhead Alpha 2 | # SPDX-License-Identifier: LicenseRef-MIT-OA 3 | 4 | # Checks that, when hit, should be fixed as soon as possible. 5 | 6 | require_relative 'helpers' 7 | require_relative 'trailing-whitespaces' 8 | require_relative 'commit-style' 9 | require_relative 'branch-name' 10 | require_relative 'licenses' 11 | 12 | check_trailing_whitespaces() 13 | 14 | # Clean commits history 15 | if git.commits.any? { |c| c.subject =~ /^Merge branch/ } 16 | fail 'Please, no merge commits. Rebase for the win.' 17 | end 18 | 19 | check_commit_style() 20 | 21 | # Proper MR content 22 | mr_title_payload = githost.mr_title_payload 23 | 24 | unless has_valid_issue_tags(mr_title_payload) 25 | warn( 26 | "Inappropriate title for PR.\n"\ 27 | "Should start from issue ID (e.g. `[#123]`), `[Chore]` or `[Style]` tag.\n"\ 28 | "Note: please use `[Chore]` also for tickets tracked internally on YouTrack." 29 | ) 30 | end 31 | 32 | check_branch_name() 33 | 34 | check_licenses() 35 | -------------------------------------------------------------------------------- /danger/licenses.rb: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Oxhead Alpha 2 | # SPDX-License-Identifier: LicenseRef-MIT-OA 3 | 4 | require_relative 'helpers' 5 | 6 | def check_licenses 7 | # Licenses 8 | # Check that the REUSE license header contains the current year. 9 | cur_year = Time.new.year 10 | # Only go over new files; see https://gitlab.com/morley-framework/morley/-/merge_requests/1091 11 | # for the discussion and rationale for this. 12 | git.added_files.each do |file| 13 | File.foreach(file).with_index(1).find do |line, line_num| 14 | if year_match = line.match(/(^.*SPDX-FileCopyrightText:)\s+(\w+-)?(\w+)\s+(.*)$/) 15 | head, start, year, holder = year_match.captures 16 | unless (year == cur_year.to_s) 17 | markdown( 18 | ":warning: The year in this license header is outdated, time to update!\n\n", 19 | file: file, line: line_num 20 | ) 21 | end 22 | # either way, we return 'true' to stop looking after the first 'match' 23 | true 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /danger/premerge-checks.rb: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Oxhead Alpha 2 | # SPDX-License-Identifier: LicenseRef-MIT-OA 3 | 4 | # Checks that are fine to fail during development, but must be fixed before merging. 5 | 6 | require_relative 'helpers' 7 | 8 | # Fixup commits 9 | if git.commits.any? &:fixup? 10 | fail "Some fixup commits are still there." 11 | end 12 | 13 | # Work-in-progress commits 14 | if git.commits.any? &:wip? 15 | fail "WIP commits are still there." 16 | end 17 | -------------------------------------------------------------------------------- /danger/trailing-whitespaces.rb: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Oxhead Alpha 2 | # SPDX-License-Identifier: LicenseRef-MIT-OA 3 | 4 | require_relative 'helpers' 5 | 6 | # Report that there are some issues with trailing whitespaces or newlines. 7 | def report_trailing_whitespaces_violation 8 | # This can be safely called multiple times 9 | # because Danger deduplicates messages. 10 | fail("Trailing whitespaces and/or incorrect end-of-file newlines detected.") 11 | end 12 | 13 | def check_trailing_whitespaces 14 | git.diff.each do |file| 15 | if !["new", "modified"].include?(file.type) || file.binary? 16 | next 17 | end 18 | 19 | path = file.destination_path 20 | contents = File.read(path) 21 | lines = contents.lines 22 | 23 | if contents.empty? 24 | next 25 | end 26 | 27 | lines.each.with_index(1) do |line, line_index| 28 | if line[-1..-1] == "\n" 29 | line = line[0..-2] 30 | end 31 | if /\s$/.match?(line) 32 | report_trailing_whitespaces_violation 33 | markdown( 34 | "I have found some trailing whitespaces here:\n"\ 35 | "```suggestion:-0+0\n"\ 36 | "#{line.rstrip}\n"\ 37 | "```\n", 38 | file: path, line: line_index 39 | ) 40 | end 41 | end 42 | 43 | last_line = lines.last 44 | unless last_line[-1..-1] == "\n" 45 | report_trailing_whitespaces_violation 46 | markdown( 47 | "I have found a missing newline at the end of this file:\n"\ 48 | "```suggestion:-0+0\n"\ 49 | "#{last_line.rstrip}\n\n"\ 50 | "```", 51 | file: path, line: lines.length 52 | ) 53 | end 54 | 55 | trailing_empty_lines = 0 56 | lines.reverse_each do |line| 57 | if line == "\n" then 58 | trailing_empty_lines = trailing_empty_lines + 1 59 | else 60 | break 61 | end 62 | end 63 | if trailing_empty_lines == 0 64 | elsif trailing_empty_lines == 1 65 | trailing_newline_err_msg = "Extra newline at the end of the file." 66 | elsif trailing_empty_lines <= 3 67 | trailing_newline_err_msg = "Yay, that's a combo!" 68 | else 69 | pic_url = "https://raw.githubusercontent.com/serokell/resources/ed58049e3724f11cef43d45bf3958878a716fc47/dangerbot/pics/trailing-whitespaces-voilation.jpg" 70 | trailing_newline_err_msg = "![:thinking:](#{pic_url})" 71 | end 72 | if !trailing_newline_err_msg.nil? 73 | report_trailing_whitespaces_violation 74 | markdown( 75 | "#{trailing_newline_err_msg}\n"\ 76 | "```suggestion:-#{trailing_empty_lines - 1}+0\n"\ 77 | "```\n", 78 | file: path, line: lines.length 79 | ) 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Serokell 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | let 5 | inherit (import 6 | (let lock = builtins.fromJSON (builtins.readFile ./flake.lock); in 7 | fetchTarball { 8 | url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; 9 | sha256 = lock.nodes.flake-compat.locked.narHash; 10 | }) 11 | { src = ./.; }) defaultNix; 12 | system = builtins.currentSystem; 13 | in defaultNix // 14 | (defaultNix.legacyPackages.${system}.lib.attrsets.mapAttrs (_: val: val.${system}) defaultNix) 15 | -------------------------------------------------------------------------------- /docs/output-sample/README.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # Output sample 8 | 9 | You can run `xrefcheck` from inside this directory to produce the output that is shown in [this image](./output-sample.png) in case you need to update it. 10 | 11 | This is a fake [installation document](/docs/installation.md#installation-step). 12 | -------------------------------------------------------------------------------- /docs/output-sample/docs/installation.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | ## Installation steps 8 | 9 | A [Markdown](https://www.markdowntutorial.com/lssons) tutorial. 10 | 11 | ## How to install other 12 | 13 | Some [examples](./examples) 14 | -------------------------------------------------------------------------------- /docs/output-sample/output-sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serokell/xrefcheck/03dda2ec2c20b359cb68ab15f034afcc7395f92d/docs/output-sample/output-sample.png -------------------------------------------------------------------------------- /exec/Main.hs: -------------------------------------------------------------------------------- 1 | {- SPDX-FileCopyrightText: 2018-2019 Serokell 2 | - 3 | - SPDX-License-Identifier: MPL-2.0 4 | -} 5 | 6 | module Main where 7 | 8 | import Universum 9 | 10 | import Main.Utf8 (withUtf8) 11 | import System.IO.CodePage (withCP65001) 12 | 13 | import System.Directory (doesFileExist) 14 | import Xrefcheck.CLI (Command (..), DumpConfigMode (..), getCommand) 15 | import Xrefcheck.Command (defaultAction) 16 | import Xrefcheck.Config (defConfigText) 17 | 18 | main :: IO () 19 | main = withUtf8 $ withCP65001 $ do 20 | command <- getCommand 21 | case command of 22 | DefaultCommand options -> 23 | defaultAction options 24 | DumpConfig repoType (DCMFile forceFlag path) -> do 25 | whenM ((not forceFlag &&) <$> doesFileExist path) do 26 | putTextLn "Output file exists. Use --force to overwrite." 27 | exitFailure 28 | writeFile path (defConfigText repoType) 29 | DumpConfig repoType DCMStdout -> 30 | putStr (defConfigText repoType) 31 | -------------------------------------------------------------------------------- /flake.lock.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023 Serokell 2 | SPDX-License-Identifier: MPL-2.0 3 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Serokell 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | { 5 | description = "The xrefcheck flake"; 6 | 7 | nixConfig.flake-registry = "https://github.com/serokell/flake-registry/raw/master/flake-registry.json"; 8 | 9 | outputs = { self, flake-utils, haskell-nix, serokell-nix, ... }: 10 | flake-utils.lib.eachSystem [ "x86_64-linux" ] (system: 11 | let 12 | pkgs = haskell-nix.legacyPackages.${system}.extend 13 | (haskell-nix.legacyPackages.${system}.lib.composeManyExtensions [ 14 | serokell-nix.overlay 15 | ]); 16 | 17 | flake = (pkgs.haskell-nix.stackProject { 18 | src = builtins.path { 19 | name = "xrefcheck"; 20 | path = ./.; 21 | }; 22 | modules = [({ pkgs, ... }: { 23 | packages.xrefcheck = { 24 | ghcOptions = 25 | [ "-Werror" ]; 26 | 27 | components.tests = { 28 | ftp-tests = { 29 | build-tools = [ pkgs.vsftpd ]; 30 | preCheck = '' 31 | echo "Starting vsftpd..." 32 | touch /tmp/vsftpd.log 33 | vsftpd \ 34 | -orun_as_launching_user=yes \ 35 | -olisten_port=2221 \ 36 | -olisten=yes \ 37 | -oftp_username=$(whoami) \ 38 | -oanon_root=${./ftp-tests/ftp_root} \ 39 | -opasv_min_port=2222 \ 40 | -ohide_file='{.*}' \ 41 | -odeny_file='{.*}' \ 42 | -oseccomp_sandbox=no \ 43 | -olog_ftp_protocol=yes \ 44 | -oxferlog_enable=yes \ 45 | -ovsftpd_log_file=/tmp/vsftpd.log & 46 | sleep 1 47 | tail -f /tmp/vsftpd.log & 48 | ''; 49 | testFlags = [ "--ftp-host" "ftp://localhost:2221" ]; 50 | }; 51 | xrefcheck-tests.build-tools = [ pkgs.git ]; 52 | }; 53 | }; 54 | # bitvec compilation on mingw64 with 'simd' flag fails with 55 | # unknown symbol `__cpu_model' 56 | packages.bitvec.flags.simd = !pkgs.stdenv.targetPlatform.isWindows; 57 | })]; 58 | }).flake { crossPlatforms = p: [ p.musl64 p.mingwW64 ]; }; 59 | 60 | in 61 | pkgs.lib.lists.foldr pkgs.lib.recursiveUpdate {} [ 62 | { inherit (flake) packages apps devShells; } 63 | { 64 | legacyPackages = pkgs; 65 | 66 | apps.default = self.apps.${system}."x86_64-unknown-linux-musl:xrefcheck:exe:xrefcheck"; 67 | 68 | packages = { 69 | default = self.packages.${system}.xrefcheck; 70 | 71 | xrefcheck = self.packages.${system}."xrefcheck:exe:xrefcheck"; 72 | 73 | xrefcheck-static = self.packages.${system}."x86_64-unknown-linux-musl:xrefcheck:exe:xrefcheck"; 74 | 75 | xrefcheck-windows = self.packages.${system}."x86_64-w64-mingw32:xrefcheck:exe:xrefcheck"; 76 | 77 | docker-image = 78 | let 79 | executable = self.packages.${system}.xrefcheck-static; 80 | binOnly = pkgs.runCommand "xrefcheck-bin" {} '' 81 | mkdir -p $out/bin 82 | cp ${executable}/bin/xrefcheck $out/bin 83 | ${pkgs.nukeReferences}/bin/nuke-refs $out/bin/xrefcheck 84 | ''; 85 | in pkgs.dockerTools.buildImage { 86 | name = "xrefcheck"; 87 | contents = [ binOnly pkgs.cacert ]; 88 | config.Entrypoint = "xrefcheck"; 89 | }; 90 | }; 91 | 92 | checks = { 93 | trailing-whitespace = pkgs.build.checkTrailingWhitespace ./.; 94 | reuse-lint = pkgs.build.reuseLint ./.; 95 | shellcheck = pkgs.build.shellcheck ./.; 96 | hlint = pkgs.build.haskell.hlint ./.; 97 | stylish-haskell = pkgs.build.haskell.stylish-haskell ./.; 98 | 99 | test = self.packages.${system}."xrefcheck:test:xrefcheck-tests"; 100 | }; 101 | } 102 | ] 103 | ); 104 | } 105 | -------------------------------------------------------------------------------- /ftp-tests/Main.hs: -------------------------------------------------------------------------------- 1 | -- SPDX-FileCopyrightText: 2021 Serokell 2 | -- 3 | -- SPDX-License-Identifier: MPL-2.0 4 | 5 | module Main 6 | ( main 7 | ) where 8 | 9 | import Universum 10 | 11 | import Test.Tasty (defaultIngredients, defaultMainWithIngredients, includingOptions) 12 | import Test.Tasty.Ingredients (Ingredient) 13 | 14 | import Test.Xrefcheck.FtpLinks (ftpOptions) 15 | import Tree (tests) 16 | 17 | main :: IO () 18 | main = tests >>= defaultMainWithIngredients ingredients 19 | 20 | ingredients :: [Ingredient] 21 | ingredients = includingOptions ftpOptions : defaultIngredients 22 | -------------------------------------------------------------------------------- /ftp-tests/Test/Xrefcheck/FtpLinks.hs: -------------------------------------------------------------------------------- 1 | -- SPDX-FileCopyrightText: 2021 Serokell 2 | -- 3 | -- SPDX-License-Identifier: MPL-2.0 4 | 5 | module Test.Xrefcheck.FtpLinks 6 | ( ftpOptions 7 | , test_FtpLinks 8 | ) where 9 | 10 | import Universum hiding ((.~)) 11 | 12 | import Control.Lens ((.~)) 13 | import Data.Tagged (untag) 14 | import Options.Applicative (help, long, strOption) 15 | import Test.Tasty (TestTree, askOption, testGroup) 16 | import Test.Tasty.HUnit (assertBool, assertFailure, testCase, (@?=)) 17 | import Test.Tasty.Options as Tasty (IsOption (..), OptionDescription (Option), safeRead) 18 | 19 | import Xrefcheck.Config 20 | import Xrefcheck.Core (Flavor (GitHub)) 21 | import Xrefcheck.Scan (ecIgnoreExternalRefsToL) 22 | import Xrefcheck.Verify (VerifyError (..), checkExternalResource) 23 | 24 | -- | A list with all the options needed to configure FTP links tests. 25 | ftpOptions :: [OptionDescription] 26 | ftpOptions = 27 | [ Tasty.Option (Proxy @FtpHostOpt) 28 | ] 29 | 30 | -- | Option specifying FTP host. 31 | newtype FtpHostOpt = FtpHostOpt Text 32 | deriving stock (Show, Eq) 33 | 34 | instance IsOption FtpHostOpt where 35 | defaultValue = FtpHostOpt "ftp://localhost" 36 | optionName = "ftp-host" 37 | optionHelp = "[Test.Xrefcheck.FtpLinks] FTP host without trailing slash" 38 | parseValue v = FtpHostOpt <$> safeRead v 39 | optionCLParser = FtpHostOpt <$> strOption 40 | ( long (untag @FtpHostOpt optionName) 41 | <> help (untag @FtpHostOpt optionHelp) 42 | ) 43 | 44 | config :: Config 45 | config = defConfig GitHub & cExclusionsL . ecIgnoreExternalRefsToL .~ [] 46 | 47 | test_FtpLinks :: TestTree 48 | test_FtpLinks = askOption $ \(FtpHostOpt host) -> do 49 | testGroup "Ftp links handler" 50 | [ testCase "handles correct link to file" $ do 51 | let link = host <> "/pub/file_exists.txt" 52 | result <- runExceptT $ checkExternalResource emptyChain config link 53 | result @?= Right () 54 | 55 | , testCase "handles empty link (host only)" $ do 56 | let link = host 57 | result <- runExceptT $ checkExternalResource emptyChain config link 58 | result @?= Right () 59 | 60 | , testCase "handles correct link to non empty directory" $ do 61 | let link = host <> "/pub/" 62 | result <- runExceptT $ checkExternalResource emptyChain config link 63 | result @?= Right () 64 | 65 | , testCase "handles correct link to empty directory" $ do 66 | let link = host <> "/empty/" 67 | result <- runExceptT $ checkExternalResource emptyChain config link 68 | result @?= Right () 69 | 70 | , testCase "throws exception when file not found" $ do 71 | let link = host <> "/pub/file_does_not_exists.txt" 72 | result <- runExceptT $ checkExternalResource emptyChain config link 73 | case result of 74 | Right () -> 75 | assertFailure "No exception was raised, FtpEntryDoesNotExist expected" 76 | Left err -> 77 | assertBool "Expected FtpEntryDoesNotExist, got other exceptions" $ 78 | case err of 79 | FtpEntryDoesNotExist _ -> True 80 | ExternalFtpException _ -> True 81 | _ -> False 82 | ] 83 | -------------------------------------------------------------------------------- /ftp-tests/Tree.hs: -------------------------------------------------------------------------------- 1 | -- SPDX-FileCopyrightText: 2021 Serokell 2 | -- 3 | -- SPDX-License-Identifier: MPL-2.0 4 | 5 | {-# OPTIONS_GHC -F -pgmF tasty-discover -optF --tree-display -optF --generated-module -optF Tree #-} 6 | -------------------------------------------------------------------------------- /ftp-tests/ftp_root/empty/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serokell/xrefcheck/03dda2ec2c20b359cb68ab15f034afcc7395f92d/ftp-tests/ftp_root/empty/.gitkeep -------------------------------------------------------------------------------- /ftp-tests/ftp_root/pub/file_exists.txt: -------------------------------------------------------------------------------- 1 | File exists! 2 | -------------------------------------------------------------------------------- /make/Makefile: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Serokell 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | # Defines utilities for other Makefiles 6 | 7 | .PHONY: dev test test-dumb-term test-hide-successes clean 8 | 9 | # Options for development 10 | STACK_DEV_OPTIONS = --fast --ghc-options -Wwarn --file-watch 11 | # Options to build more stuff (tests and benchmarks) 12 | STACK_BUILD_MORE_OPTIONS = --test --bench --no-run-tests --no-run-benchmarks 13 | # Options for tests 14 | STACK_DEV_TEST_OPTIONS = --fast 15 | # Addtional (specified by user) options passed to test executable 16 | TEST_ARGUMENTS ?= "" 17 | # Packages to apply the command (build, test, e.t.c) for. 18 | PACKAGE ?= non-defined-package 19 | 20 | define call_test 21 | stack test $(PACKAGE) --test-arguments "$(TEST_ARGUMENTS) $1" $2 22 | endef 23 | 24 | # Build everything (including tests and benchmarks) with development options. 25 | dev: 26 | stack build $(STACK_DEV_OPTIONS) $(STACK_BUILD_MORE_OPTIONS) $(PACKAGE) 27 | 28 | # Run tests in all packages which have them. 29 | test: 30 | $(call call_test,"",$(STACK_DEV_TEST_OPTIONS)) 31 | 32 | # Like 'test' command, but enforces dumb terminal which may be useful to 33 | # workardoung some issues with `tasty`. 34 | # Primarily this one: https://github.com/feuerbach/tasty/issues/152 35 | test-dumb-term: 36 | TERM=dumb $(call call_test,"",$(STACK_DEV_TEST_OPTIONS)) 37 | 38 | # Run tests with `--hide-successes` option. It forces dumb terminal, 39 | # because otherwise this option is likely to work incorrectly. 40 | test-hide-successes: 41 | TERM=dumb $(call call_test,"--hide-successes",$(STACK_DEV_TEST_OPTIONS)) 42 | 43 | clean: 44 | stack clean $(PACKAGE) 45 | -------------------------------------------------------------------------------- /package.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2018-2021 Serokell 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | spec-version: 0.31.0 6 | 7 | name: xrefcheck 8 | version: 0.3.1 9 | github: serokell/xrefcheck 10 | license: MPL-2.0 11 | license-file: LICENSE 12 | author: Kostya Ivanov, Serokell 13 | maintainer: Serokell 14 | copyright: 2018-2019 Serokell 15 | description: Please see the README on GitHub at 16 | 17 | extra-source-files: 18 | - README.md 19 | - CHANGES.md 20 | 21 | default-extensions: 22 | - AllowAmbiguousTypes 23 | - BangPatterns 24 | - BlockArguments 25 | - ConstraintKinds 26 | - DataKinds 27 | - DefaultSignatures 28 | - DeriveDataTypeable 29 | - DeriveGeneric 30 | - DerivingStrategies 31 | - FlexibleContexts 32 | - FlexibleInstances 33 | - FunctionalDependencies 34 | - GeneralizedNewtypeDeriving 35 | - ImportQualifiedPost 36 | - LambdaCase 37 | - MultiParamTypeClasses 38 | - MultiWayIf 39 | - NamedFieldPuns 40 | - NoImplicitPrelude 41 | - OverloadedStrings 42 | - QuasiQuotes 43 | - RankNTypes 44 | - RecordWildCards 45 | - ScopedTypeVariables 46 | - StandaloneDeriving 47 | - TemplateHaskell 48 | - TupleSections 49 | - TypeApplications 50 | - TypeFamilies 51 | - TypeOperators 52 | - UndecidableInstances 53 | - ViewPatterns 54 | 55 | ghc-options: 56 | - -Weverything 57 | - -Wincomplete-record-updates 58 | - -Wincomplete-uni-patterns 59 | - -Wmissing-deriving-strategies 60 | - -Wno-missing-safe-haskell-mode 61 | - -Wno-unsafe 62 | - -Wno-missing-import-lists 63 | - -Wno-missing-local-signatures 64 | - -Wno-missing-export-lists 65 | - -Wno-all-missed-specialisations 66 | - -Wno-prepositive-qualified-module 67 | - -Wno-monomorphism-restriction 68 | - -Wno-missing-kind-signatures 69 | 70 | # This option avoids a warning on case-insensitive systems: 71 | # https://github.com/haskell/cabal/issues/4739 72 | # https://github.com/commercialhaskell/stack/issues/3918 73 | - -optP-Wno-nonportable-include-path 74 | 75 | dependencies: 76 | - base >=4.14.3.0 && <5 77 | 78 | library: 79 | source-dirs: src 80 | 81 | generated-other-modules: 82 | - Paths_xrefcheck 83 | 84 | dependencies: 85 | - aeson 86 | - aeson-casing 87 | - async 88 | - bytestring 89 | - cmark-gfm >= 0.2.5 90 | - containers 91 | - directory 92 | - dlist 93 | - filepath 94 | - fmt 95 | - ftp-client 96 | - crypton-connection 97 | - Glob 98 | - http-client 99 | - http-types 100 | - lens 101 | - modern-uri 102 | - mtl 103 | - nyan-interpolation 104 | - o-clock 105 | - optparse-applicative 106 | - pretty-terminal 107 | - process 108 | - reflection 109 | - regex-tdfa 110 | - req 111 | - safe-exceptions 112 | - tagsoup 113 | - text 114 | - text-metrics 115 | - time 116 | - transformers 117 | - universum 118 | - uri-bytestring 119 | - yaml 120 | 121 | executables: 122 | xrefcheck: 123 | main: Main.hs 124 | source-dirs: exec 125 | generated-other-modules: 126 | - Paths_xrefcheck 127 | ghc-options: 128 | - -threaded 129 | - -rtsopts 130 | - -with-rtsopts=-N 131 | - -O2 132 | dependencies: 133 | - code-page 134 | - directory 135 | - universum 136 | - with-utf8 137 | - xrefcheck 138 | 139 | tests: 140 | xrefcheck-tests: 141 | main: Main.hs 142 | source-dirs: tests 143 | build-tools: tasty-discover:tasty-discover 144 | generated-other-modules: 145 | - Paths_xrefcheck 146 | dependencies: 147 | - optparse-applicative 148 | - tagged 149 | - case-insensitive 150 | - cmark-gfm 151 | - containers 152 | - directory 153 | - filepath 154 | - wai 155 | - warp 156 | - scotty 157 | - http-types 158 | - lens 159 | - modern-uri 160 | - nyan-interpolation 161 | - o-clock 162 | - reflection 163 | - regex-tdfa 164 | - tasty 165 | - tasty-hunit 166 | - tasty-quickcheck 167 | - time 168 | - universum 169 | - uri-bytestring 170 | - xrefcheck 171 | - yaml 172 | 173 | ftp-tests: 174 | main: Main.hs 175 | source-dirs: ftp-tests 176 | build-tools: tasty-discover:tasty-discover 177 | generated-other-modules: 178 | - Paths_xrefcheck 179 | dependencies: 180 | - lens 181 | - optparse-applicative 182 | - tagged 183 | - tasty 184 | - tasty-hunit 185 | - universum 186 | - xrefcheck 187 | -------------------------------------------------------------------------------- /release/default.nix: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2019 Serokell 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | let 5 | defaultNix = import ../default.nix; 6 | in 7 | { pkgs ? defaultNix.legacyPackages }: 8 | let 9 | xrefcheck-x86_64-linux = defaultNix.packages.xrefcheck-static; 10 | 11 | xrefcheck-x86_64-windows = defaultNix.packages.xrefcheck-windows; 12 | 13 | mkZip = { name, paths, compression ? 5 }: 14 | pkgs.stdenvNoCC.mkDerivation { 15 | inherit name; 16 | buildInputs = [ pkgs.zip ]; 17 | buildCommand = '' 18 | mkdir -p "$out" 19 | zip "$out/$name.zip" -jr ${toString paths} -${toString compression} 20 | ''; 21 | }; 22 | 23 | xrefcheck-x86_64-windows-zip = mkZip { 24 | name = "xrefcheck-x86_64-windows"; 25 | paths = [ "${xrefcheck-x86_64-windows}/bin" ]; 26 | }; 27 | 28 | in pkgs.linkFarm "xrefcheck-release" [ 29 | { 30 | name = "xrefcheck-x86_64-linux"; 31 | path = "${xrefcheck-x86_64-linux}/bin/xrefcheck"; 32 | } 33 | { 34 | name = "xrefcheck-x86_64-windows.zip"; 35 | path = "${xrefcheck-x86_64-windows-zip}/xrefcheck-x86_64-windows.zip"; 36 | } 37 | ] 38 | -------------------------------------------------------------------------------- /scripts/upload-docker-image.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # SPDX-FileCopyrightText: 2021 Serokell 4 | # 5 | # SPDX-License-Identifier: MPL-2.0 6 | 7 | skopeo --insecure-policy copy --dest-creds "serokell:${DOCKERHUB_PASSWORD}" "$1" "$2" 8 | -------------------------------------------------------------------------------- /scripts/validate-stylish.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # SPDX-FileCopyrightText: 2022 Serokell 4 | # 5 | # SPDX-License-Identifier: MPL-2.0 6 | 7 | # This script verifies that the repo adheres to the stylish-haskell rules. 8 | # 9 | # It does this by running `make stylish` on the repo and checking 10 | # that no files were affected. 11 | 12 | set -euo pipefail 13 | 14 | make stylish 15 | 16 | # Note: we temporarily disable `-e`; 17 | # otherwise the script would exit when `git diff` returns 1. 18 | set +e 19 | diff=$(git diff --exit-code --name-only) 20 | exitCode=$? 21 | set -e 22 | 23 | if [ "$exitCode" != 0 ]; then 24 | echo "Found files that do not adhere to stylish-haskell." 25 | echo "Run 'make stylish' on the repository to fix this." 26 | echo "" 27 | echo "Offending files:" 28 | echo "$diff" 29 | exit 1 30 | fi 31 | -------------------------------------------------------------------------------- /src/Xrefcheck/Command.hs: -------------------------------------------------------------------------------- 1 | {- SPDX-FileCopyrightText: 2021 Serokell 2 | - 3 | - SPDX-License-Identifier: MPL-2.0 4 | -} 5 | 6 | module Xrefcheck.Command 7 | ( defaultAction 8 | ) where 9 | 10 | import Universum 11 | 12 | import Data.Reflection (Given, give) 13 | import Data.Yaml (decodeFileEither, prettyPrintParseException) 14 | import Fmt (build, fmt, fmtLn) 15 | import System.Console.Pretty (supportsPretty) 16 | import System.Directory (doesFileExist) 17 | import Text.Interpolation.Nyan 18 | 19 | import Xrefcheck.CLI (Options (..), addExclusionOptions, addNetworkingOptions, defaultConfigPaths) 20 | import Xrefcheck.Config 21 | (Config, Config' (..), ScannersConfig, ScannersConfig' (..), defConfig, overrideConfig) 22 | import Xrefcheck.Core (Flavor (..)) 23 | import Xrefcheck.Progress (allowRewrite) 24 | import Xrefcheck.Scan 25 | import Xrefcheck.Scanners.Markdown (markdownSupport) 26 | import Xrefcheck.Scanners.Symlink (symlinkSupport) 27 | import Xrefcheck.System (PrintUnixPaths (..), askWithinCI) 28 | import Xrefcheck.Util 29 | import Xrefcheck.Verify (reportVerifyErrs, verifyErrors, verifyRepo) 30 | 31 | readConfig :: FilePath -> IO Config 32 | readConfig path = fmap overrideConfig do 33 | decodeFileEither path 34 | >>= either (error . toText . prettyPrintParseException) pure 35 | 36 | configuredFileSupport :: Given PrintUnixPaths => ScannersConfig -> FileSupport 37 | configuredFileSupport ScannersConfig{..} = firstFileSupport 38 | [ markdownSupport scMarkdown 39 | , symlinkSupport 40 | ] 41 | 42 | findFirstExistingFile :: [FilePath] -> IO (Maybe FilePath) 43 | findFirstExistingFile = \case 44 | [] -> pure Nothing 45 | (file : files) -> do 46 | exists <- doesFileExist file 47 | if exists then pure (Just file) else findFirstExistingFile files 48 | 49 | defaultAction :: Options -> IO () 50 | defaultAction Options{..} = do 51 | withinCI <- askWithinCI 52 | coloringSupported <- supportsPretty 53 | let colorMode = oColorMode ?: 54 | if withinCI || coloringSupported 55 | then WithColors 56 | else WithoutColors 57 | 58 | give oPrintUnixPaths $ give colorMode $ do 59 | config <- case oConfigPath of 60 | Just configPath -> readConfig configPath 61 | Nothing -> do 62 | mConfigPath <- findFirstExistingFile defaultConfigPaths 63 | case mConfigPath of 64 | Just configPath -> readConfig configPath 65 | Nothing -> do 66 | hPutStrLn @Text stderr 67 | [int|| 68 | Configuration file not found, using default config \ 69 | for GitHub repositories 70 | |] 71 | pure $ defConfig GitHub 72 | 73 | let showProgressBar = oShowProgressBar ?: not withinCI 74 | 75 | (ScanResult scanErrs repoInfo) <- allowRewrite showProgressBar $ \rw -> do 76 | let fullConfig = addExclusionOptions (cExclusions config) oExclusionOptions 77 | fileSupport = configuredFileSupport $ cScanners config 78 | scanRepo oScanPolicy rw fileSupport fullConfig oRoot 79 | 80 | when oVerbose $ 81 | fmt [int|| 82 | === Repository data === 83 | 84 | #{interpolateIndentF 2 (build repoInfo)} 85 | |] 86 | 87 | whenJust (nonEmpty $ sortBy (compare `on` seFile) scanErrs) reportScanErrs 88 | 89 | verifyRes <- allowRewrite showProgressBar $ \rw -> do 90 | let fullConfig = config 91 | { cNetworking = addNetworkingOptions (cNetworking config) oNetworkingOptions } 92 | verifyRepo rw fullConfig oMode repoInfo 93 | 94 | case verifyErrors verifyRes of 95 | Nothing | null scanErrs -> 96 | fmtLn $ colorIfNeeded Green "All repository links are valid." 97 | Nothing -> exitFailure 98 | Just verifyErrs -> do 99 | unless (null scanErrs) $ fmt "\n" 100 | reportVerifyErrs verifyErrs 101 | exitFailure 102 | -------------------------------------------------------------------------------- /src/Xrefcheck/Data/Redirect.hs: -------------------------------------------------------------------------------- 1 | {- SPDX-FileCopyrightText: 2022 Serokell 2 | - 3 | - SPDX-License-Identifier: MPL-2.0 4 | -} 5 | 6 | {-# OPTIONS_GHC -fno-warn-orphans #-} 7 | 8 | module Xrefcheck.Data.Redirect 9 | ( RedirectChain 10 | , RedirectChainLink (..) 11 | , emptyChain 12 | , pushRequest 13 | , hasRequest 14 | , totalFollowed 15 | 16 | , RedirectRule (..) 17 | , RedirectRuleOn (..) 18 | , RedirectRuleOutcome (..) 19 | , redirectRule 20 | 21 | , isPermanentRedirectCode 22 | , isRedirectCode 23 | , isTemporaryRedirectCode 24 | ) where 25 | 26 | import Universum 27 | 28 | import Data.Aeson (genericParseJSON) 29 | import Data.Yaml (FromJSON (..), withText) 30 | import Fmt (Buildable (..)) 31 | import Text.Regex.TDFA.Text (Regex) 32 | 33 | import Data.Sequence ((|>)) 34 | import Xrefcheck.Scan () 35 | import Xrefcheck.Util 36 | 37 | -- | A custom redirect rule. 38 | data RedirectRule = RedirectRule 39 | { rrFrom :: Maybe Regex 40 | -- ^ Redirect source links that match to apply the rule. 41 | -- 42 | -- 'Nothing' matches any link. 43 | , rrTo :: Maybe Regex 44 | -- ^ Redirect target links that match to apply the rule. 45 | -- 46 | -- 'Nothing' matches any link. 47 | , rrOn :: Maybe RedirectRuleOn 48 | -- ^ HTTP code selector to apply the rule. 49 | -- 50 | -- 'Nothing' matches any code. 51 | , rrOutcome :: RedirectRuleOutcome 52 | -- ^ What to do when an HTTP response matches the rule. 53 | } deriving stock (Generic) 54 | 55 | -- | Rule selector depending on the response HTTP code. 56 | data RedirectRuleOn 57 | = RROCode Int 58 | -- ^ An exact HTTP code 59 | | RROPermanent 60 | -- ^ Any HTTP code considered as permanent according to 'isPermanentRedirectCode' 61 | | RROTemporary 62 | -- ^ Any HTTP code considered as permanent according to 'isTemporaryRedirectCode' 63 | deriving stock (Show, Eq) 64 | 65 | -- | What to do when receiving a redirect HTTP response. 66 | data RedirectRuleOutcome 67 | = RROValid 68 | -- ^ Consider it as valid 69 | | RROInvalid 70 | -- ^ Consider it as invalid 71 | | RROFollow 72 | -- ^ Try again by following the redirect 73 | deriving stock (Show, Eq) 74 | 75 | -- | Links in a redirection chain. 76 | newtype RedirectChain = RedirectChain 77 | { unRedirectChain :: Seq RedirectChainLink 78 | } deriving newtype (Show, Eq) 79 | 80 | -- | A single link in a redirection chain. 81 | newtype RedirectChainLink = RedirectChainLink 82 | { unRedirectChainLink :: Text 83 | } deriving newtype (Show, Eq) 84 | 85 | instance FromList RedirectChain where 86 | type ListElement RedirectChain = Text 87 | fromList = RedirectChain . fromList . fmap RedirectChainLink 88 | 89 | emptyChain :: RedirectChain 90 | emptyChain = RedirectChain mempty 91 | 92 | pushRequest :: RedirectChain -> RedirectChainLink -> RedirectChain 93 | pushRequest (RedirectChain chain) = RedirectChain . (chain |>) 94 | 95 | hasRequest :: RedirectChain -> RedirectChainLink -> Bool 96 | hasRequest (RedirectChain chain) = (`elem` chain) 97 | 98 | totalFollowed :: RedirectChain -> Int 99 | totalFollowed = length . unRedirectChain 100 | 101 | instance Buildable RedirectChain where 102 | build (RedirectChain linksStack) = build chainText 103 | where 104 | link (True, RedirectChainLink l) = "-| " <> l 105 | link (False, RedirectChainLink l) = "-> " <> l 106 | 107 | chainText = mconcat 108 | $ intersperse "\n" 109 | $ fmap link 110 | $ zip (True : repeat False) 111 | $ toList linksStack 112 | 113 | -- | Redirect rule to apply to a link when it has been responded with a given 114 | -- HTTP code. 115 | redirectRule :: Text -> Text -> Int -> [RedirectRule] -> Maybe RedirectRule 116 | redirectRule source target code rules = 117 | find (matchRule source target code) rules 118 | 119 | -- | Check if a 'RedirectRule' matches a given link and HTTP code. 120 | matchRule :: Text -> Text -> Int -> RedirectRule -> Bool 121 | matchRule source target code RedirectRule{..} = and 122 | [ matchCode 123 | , matchLink source rrFrom 124 | , matchLink target rrTo 125 | ] 126 | where 127 | matchCode = case rrOn of 128 | Nothing -> True 129 | Just RROPermanent -> isPermanentRedirectCode code 130 | Just RROTemporary -> isTemporaryRedirectCode code 131 | Just (RROCode other) -> code == other 132 | 133 | matchLink link = \case 134 | Nothing -> True 135 | Just regex -> doesMatchAnyRegex link [regex] 136 | 137 | isRedirectCode :: Int -> Bool 138 | isRedirectCode code = code >= 300 && code < 400 139 | 140 | isTemporaryRedirectCode :: Int -> Bool 141 | isTemporaryRedirectCode = flip elem [302, 303, 307] 142 | 143 | isPermanentRedirectCode :: Int -> Bool 144 | isPermanentRedirectCode = flip elem [301, 308] 145 | 146 | instance FromJSON (RedirectRule) where 147 | parseJSON = genericParseJSON aesonConfigOption 148 | 149 | instance FromJSON (RedirectRuleOutcome) where 150 | parseJSON = withText "Redirect rule outcome" $ 151 | \case 152 | "valid" -> pure RROValid 153 | "invalid" -> pure RROInvalid 154 | "follow" -> pure RROFollow 155 | _ -> fail "expected (valid|invalid|follow)" 156 | 157 | instance FromJSON (RedirectRuleOn) where 158 | parseJSON v = code v 159 | <|> text v 160 | <|> fail "expected a redirect (3XX) HTTP code or (permanent|temporary)" 161 | where 162 | code cv = do 163 | i <- parseJSON cv 164 | guard $ isRedirectCode i 165 | pure $ RROCode i 166 | text = withText "Redirect rule on" $ 167 | \case 168 | "permanent" -> pure RROPermanent 169 | "temporary" -> pure RROTemporary 170 | _ -> mzero 171 | -------------------------------------------------------------------------------- /src/Xrefcheck/Data/URI.hs: -------------------------------------------------------------------------------- 1 | {- SPDX-FileCopyrightText: 2023 Serokell 2 | - 3 | - SPDX-License-Identifier: MPL-2.0 4 | -} 5 | 6 | {-# LANGUAGE ExistentialQuantification #-} 7 | 8 | module Xrefcheck.Data.URI 9 | ( UriParseError (..) 10 | , parseUri 11 | ) where 12 | 13 | import Universum 14 | 15 | import Control.Exception.Safe (handleJust) 16 | import Control.Monad.Except (throwError) 17 | import Text.URI (ParseExceptionBs, URI, mkURIBs) 18 | import URI.ByteString qualified as URIBS 19 | 20 | data UriParseError 21 | = UPEInvalid URIBS.URIParseError 22 | | UPEConversion ParseExceptionBs 23 | deriving stock (Show, Eq) 24 | 25 | data AnyURIRef = forall a. AnyURIRef (URIBS.URIRef a) 26 | 27 | serializeAnyURIRef :: AnyURIRef -> ByteString 28 | serializeAnyURIRef (AnyURIRef uri) = URIBS.serializeURIRef' uri 29 | 30 | -- | Parse URI according to RFC 3986 extended by allowing non-encoded 31 | -- `[` and `]` in query string. 32 | -- 33 | -- The first parameter indicates whether the parsing should admit relative 34 | -- URIs or not. 35 | parseUri :: Bool -> Text -> ExceptT UriParseError IO URI 36 | parseUri canBeRelative link = do 37 | -- There exist two main standards of URL parsing: RFC 3986 and the Web 38 | -- Hypertext Application Technology Working Group's URL standard. Ideally, 39 | -- we want to be able to parse the URLs in accordance with the latter 40 | -- standard, because it provides a much less ambiguous set of rules for 41 | -- percent-encoding special characters, and is essentially a living 42 | -- standard that gets updated constantly. 43 | -- 44 | -- We have chosen the 'uri-bytestring' library for URI parsing because 45 | -- of the 'laxURIParseOptions' parsing configuration. 'mkURI' from 46 | -- the 'modern-uri' library parses URIs in accordance with RFC 3986 and does 47 | -- not provide a means of parsing customization, which contrasts with 48 | -- 'parseURI' that accepts a 'URIParserOptions'. One of the predefined 49 | -- configurations of this type is 'strictURIParserOptions', which follows 50 | -- RFC 3986, and the other -- 'laxURIParseOptions' -- allows brackets 51 | -- in the queries, which draws us closer to the WHATWG URL standard. 52 | -- 53 | -- The 'modern-uri' package can parse an URI deciding if it is absolute or 54 | -- relative depending on the success or failure of the scheme parsing. By 55 | -- contrast, in 'uri-bytestring' it has to be decided beforehand, resulting in 56 | -- different URI types. 57 | uri <- case URIBS.parseURI URIBS.laxURIParserOptions (encodeUtf8 link) of 58 | Left (URIBS.MalformedScheme _) | canBeRelative -> 59 | URIBS.parseRelativeRef URIBS.laxURIParserOptions (encodeUtf8 link) 60 | & either (throwError . UPEInvalid) (pure . AnyURIRef) 61 | Left err -> throwError $ UPEInvalid err 62 | Right uri -> pure $ AnyURIRef uri 63 | 64 | -- We stick to our infrastructure by continuing to operate on the datatypes 65 | -- from 'modern-uri', which are used in the 'req' library. First we 66 | -- serialize our URI parsed with 'parseURI' so it becomes a 'ByteString' 67 | -- with all the necessary special characters *percent-encoded*, and then 68 | -- call 'mkURIBs'. 69 | mkURIBs (serializeAnyURIRef uri) 70 | -- Ideally, this exception should never be thrown, as the URI 71 | -- already *percent-encoded* with 'parseURI' from 'uri-bytestring' 72 | -- and 'mkURIBs' is only used to convert to 'URI' type from 73 | -- 'modern-uri' package. 74 | & handleJust fromException (throwError . UPEConversion) 75 | -------------------------------------------------------------------------------- /src/Xrefcheck/Orphans.hs: -------------------------------------------------------------------------------- 1 | {- SPDX-FileCopyrightText: 2021 Serokell 2 | - 3 | - SPDX-License-Identifier: MPL-2.0 4 | -} 5 | 6 | {-# OPTIONS_GHC -fno-warn-orphans #-} 7 | 8 | -- | Orphan instances for types from other packages 9 | 10 | module Xrefcheck.Orphans () where 11 | 12 | import Universum 13 | 14 | import Data.ByteString.Char8 qualified as C 15 | 16 | import Fmt (Buildable (..)) 17 | import Network.FTP.Client 18 | (FTPException (..), FTPMessage (..), FTPResponse (..), ResponseStatus (..)) 19 | import Text.Interpolation.Nyan 20 | import Text.URI (RText, unRText) 21 | import URI.ByteString (SchemaError (..), URIParseError (..)) 22 | 23 | instance ToString (RText t) where 24 | toString = toString . unRText 25 | 26 | instance Buildable ResponseStatus where 27 | build = show 28 | 29 | instance Buildable FTPMessage where 30 | build message = build $ decodeUtf8 @Text ( 31 | case message of 32 | SingleLine s -> s 33 | MultiLine ss -> C.intercalate "\n" ss 34 | ) 35 | 36 | instance Buildable FTPResponse where 37 | build FTPResponse{..} = 38 | [int|| 39 | #{frStatus} (#{frCode}): 40 | #{frMessage} 41 | |] 42 | 43 | instance Buildable FTPException where 44 | build (BadProtocolResponseException _) = "Raw FTP exception" 45 | build (FailureRetryException e) = build e 46 | build (FailureException e) = build e 47 | build (UnsuccessfulException e) = build e 48 | build (BogusResponseFormatException e) = build e 49 | 50 | deriving stock instance Eq FTPException 51 | 52 | instance Buildable URIParseError where 53 | build = \case 54 | MalformedScheme e -> build e 55 | MalformedUserInfo -> "Malformed user info" 56 | MalformedQuery -> "Malformed query" 57 | MalformedFragment -> "Malformed fragment" 58 | MalformedHost -> "Malformed host" 59 | MalformedPort -> "Malformed port" 60 | MalformedPath -> "Malformed path" 61 | OtherError e -> build e 62 | 63 | instance Buildable SchemaError where 64 | build = \case 65 | NonAlphaLeading -> "Scheme must start with an alphabet character" 66 | InvalidChars -> "Subsequent characters in the schema were invalid" 67 | MissingColon -> "Schemas must be followed by a colon" 68 | -------------------------------------------------------------------------------- /src/Xrefcheck/Scanners.hs: -------------------------------------------------------------------------------- 1 | {- SPDX-FileCopyrightText: 2018-2020 Serokell 2 | - 3 | - SPDX-License-Identifier: MPL-2.0 4 | -} 5 | 6 | module Xrefcheck.Scanners 7 | ( module Xrefcheck.Scanners.Markdown 8 | ) where 9 | 10 | import Xrefcheck.Scanners.Markdown 11 | -------------------------------------------------------------------------------- /src/Xrefcheck/Scanners/Symlink.hs: -------------------------------------------------------------------------------- 1 | {- SPDX-FileCopyrightText: 2023 Serokell 2 | - 3 | - SPDX-License-Identifier: MPL-2.0 4 | -} 5 | 6 | -- | Scanner for gathering references to verify from symlinks. 7 | -- 8 | -- A symlink's reference corresponds to the file it points to. 9 | module Xrefcheck.Scanners.Symlink 10 | ( symlinkScanner 11 | , symlinkSupport 12 | ) where 13 | 14 | import Universum 15 | 16 | import Data.Reflection (Given) 17 | import System.Directory (getSymbolicLinkTarget) 18 | 19 | import Xrefcheck.Core 20 | import Xrefcheck.Scan 21 | import Xrefcheck.System 22 | 23 | symlinkScanner :: Given PrintUnixPaths => ScanAction 24 | symlinkScanner root relativePath = do 25 | let rootedPath = filePathFromRoot root relativePath 26 | pathForPrinting = mkPathForPrinting rootedPath 27 | rLink <- unRelPosixLink . mkRelPosixLink 28 | <$> getSymbolicLinkTarget rootedPath 29 | 30 | let rName = "Symbolic Link" 31 | rPos = Position (fromString pathForPrinting) 32 | rInfo = referenceInfo rLink 33 | 34 | pure (FileInfo [Reference {rName, rPos, rInfo}] [], []) 35 | 36 | symlinkSupport :: Given PrintUnixPaths => FileSupport 37 | symlinkSupport isSymlink _ = do 38 | guard isSymlink 39 | pure symlinkScanner 40 | -------------------------------------------------------------------------------- /src/Xrefcheck/Util.hs: -------------------------------------------------------------------------------- 1 | {- SPDX-FileCopyrightText: 2018-2019 Serokell 2 | - 3 | - SPDX-License-Identifier: MPL-2.0 4 | -} 5 | 6 | module Xrefcheck.Util 7 | ( Field 8 | , paren 9 | , postfixFields 10 | , (-:) 11 | , aesonConfigOption 12 | , doesMatchAnyRegex 13 | , posixTimeToTimeSecond 14 | , utcTimeToTimeSecond 15 | 16 | , module Xrefcheck.Util.Colorize 17 | , module Xrefcheck.Util.Interpolate 18 | ) where 19 | 20 | import Universum hiding ((.~)) 21 | 22 | import Control.Lens (LensRules, lensField, lensRules, mappingNamer, (.~)) 23 | import Data.Aeson qualified as Aeson 24 | import Data.Aeson.Casing (aesonPrefix, camelCase) 25 | import Data.Fixed (Fixed (MkFixed), HasResolution (resolution)) 26 | import Data.Ratio ((%)) 27 | import Data.Time (UTCTime) 28 | import Data.Time.Clock (nominalDiffTimeToSeconds) 29 | import Data.Time.Clock.POSIX (POSIXTime, utcTimeToPOSIXSeconds) 30 | import Fmt (Builder) 31 | import Text.Regex.TDFA.Text (Regex, regexec) 32 | import Time (Second, Time (..), sec) 33 | 34 | import Xrefcheck.Util.Colorize 35 | import Xrefcheck.Util.Interpolate 36 | 37 | paren :: Builder -> Builder 38 | paren a 39 | | a == "" = "" 40 | | otherwise = "(" <> a <> ")" 41 | 42 | postfixFields :: LensRules 43 | postfixFields = lensRules & lensField .~ mappingNamer (\n -> [n ++ "L"]) 44 | 45 | infixr 0 -: 46 | (-:) :: a -> b -> (a, b) 47 | (-:) = (,) 48 | 49 | -- | Options that we use to derive JSON instances for config types. 50 | aesonConfigOption :: Aeson.Options 51 | aesonConfigOption = (aesonPrefix camelCase){Aeson.rejectUnknownFields = True} 52 | 53 | -- | Config fields that may be abscent. 54 | type family Field f a where 55 | Field Identity a = a 56 | Field Maybe a = Maybe a 57 | 58 | posixTimeToTimeSecond :: POSIXTime -> Time Second 59 | posixTimeToTimeSecond posixTime = 60 | let picos@(MkFixed ps) = nominalDiffTimeToSeconds posixTime 61 | in sec . fromRational $ ps % resolution picos 62 | 63 | utcTimeToTimeSecond :: UTCTime -> Time Second 64 | utcTimeToTimeSecond = posixTimeToTimeSecond . utcTimeToPOSIXSeconds 65 | 66 | doesMatchAnyRegex :: Text -> ([Regex] -> Bool) 67 | doesMatchAnyRegex src = any $ \regex -> 68 | case regexec regex src of 69 | Right res -> case res of 70 | Just (before, match, after, _) -> 71 | null before && null after && not (null match) 72 | Nothing -> False 73 | Left _ -> False 74 | -------------------------------------------------------------------------------- /src/Xrefcheck/Util/Colorize.hs: -------------------------------------------------------------------------------- 1 | {- SPDX-FileCopyrightText: 2018-2019 Serokell 2 | - 3 | - SPDX-License-Identifier: MPL-2.0 4 | -} 5 | {-# OPTIONS_GHC -Wno-orphans #-} 6 | {-# OPTIONS_GHC -Wno-unrecognised-pragmas #-} 7 | 8 | module Xrefcheck.Util.Colorize 9 | ( ColorMode(..) 10 | , Color(..) 11 | , Style(..) 12 | , colorizeIfNeeded 13 | , colorIfNeeded 14 | , styleIfNeeded 15 | ) where 16 | 17 | import Universum 18 | 19 | import Data.Reflection (Given (..)) 20 | import Fmt (Buildable (build), Builder, fmt) 21 | import System.Console.Pretty (Color (..), Pretty (..), Section, Style (..)) 22 | 23 | {-# HLINT ignore "Avoid style function that ignore ColorMode" #-} 24 | {-# HLINT ignore "Avoid color function that ignore ColorMode"#-} 25 | {-# HLINT ignore "Avoid colorize function that ignore ColorMode"#-} 26 | 27 | data ColorMode = WithColors | WithoutColors 28 | 29 | instance Pretty Builder where 30 | colorize s c = build @Text . colorize s c . fmt 31 | style s = build @Text . style s . fmt 32 | 33 | colorIfNeeded :: (Pretty a, Given ColorMode) => Color -> a -> a 34 | colorIfNeeded = case given of 35 | WithColors -> color 36 | WithoutColors -> const id 37 | 38 | styleIfNeeded :: (Pretty a, Given ColorMode) => Style -> a -> a 39 | styleIfNeeded = case given of 40 | WithColors -> style 41 | WithoutColors -> const id 42 | 43 | colorizeIfNeeded :: (Pretty a, Given ColorMode) => Section -> Color -> a -> a 44 | colorizeIfNeeded section = case given of 45 | WithColors -> colorize section 46 | WithoutColors -> const id 47 | -------------------------------------------------------------------------------- /src/Xrefcheck/Util/Interpolate.hs: -------------------------------------------------------------------------------- 1 | {- SPDX-FileCopyrightText: 2018-2019 Serokell 2 | - 3 | - SPDX-License-Identifier: MPL-2.0 4 | -} 5 | {-# OPTIONS_GHC -Wno-unrecognised-pragmas #-} 6 | 7 | module Xrefcheck.Util.Interpolate 8 | ( -- $notes 9 | interpolateIndentF 10 | , interpolateBlockListF 11 | , interpolateBlockListF' 12 | , interpolateUnlinesF 13 | ) 14 | where 15 | 16 | import Universum 17 | 18 | import Data.Text.Lazy qualified as TL 19 | import Data.Text.Lazy.Builder (fromLazyText, toLazyText) 20 | import Fmt (Buildable, Builder, blockListF, blockListF', indentF, unlinesF) 21 | 22 | {- $notes 23 | The `blockListF` and `indentF` frunctions from @fmt@ add a trailing newline, which makes them unsuitable for string interpolation. 24 | Consider this case: 25 | > [int|| 26 | > aaa 27 | > #{indentF 2 "bbb"} 28 | > ccc 29 | > |] 30 | One would reasonably expect this to produce: 31 | > aaa 32 | > bbb 33 | > ccc 34 | But, in reality, it produces: 35 | > aaa 36 | > bbb 37 | > 38 | > ccc 39 | This module introduces versions of these functions that do not produce a trailing newline 40 | and can therefore be safely used in string interpolation. 41 | -} 42 | 43 | {-# HLINT ignore "Avoid functions that generate extra trailing newlines/whitespaces" #-} 44 | 45 | -- | Like @Fmt.indentF@, but strips trailing spaces and does not add a trailing newline. 46 | -- 47 | -- >>> import Fmt 48 | -- >>> indentF 2 "a\n\nb" 49 | -- " a\n \n b\n" 50 | -- 51 | -- >>> interpolateIndentF 2 "a\n\nb" 52 | -- " a\n\n b" 53 | interpolateIndentF :: HasCallStack => Int -> Builder -> Builder 54 | interpolateIndentF n b = (case TL.last (toLazyText b) of 55 | '\n' -> id 56 | _ -> stripLastNewline) $ stripTrailingSpaces $ indentF n b 57 | -- strips newline added by indentF 58 | 59 | -- | Like @Fmt.blockListF'@, but strips trailing spaces and does not add a trailing newline. 60 | interpolateBlockListF' :: HasCallStack => Text -> (a -> Builder) -> NonEmpty a -> Builder 61 | interpolateBlockListF' = stripLastNewline . stripTrailingSpaces ... blockListF' 62 | 63 | -- | Like @Fmt.blockListF@, but strips trailing spaces and does not add a trailing newline. 64 | interpolateBlockListF :: HasCallStack => Buildable a => NonEmpty a -> Builder 65 | interpolateBlockListF = stripLastNewline . stripTrailingSpaces . blockListF 66 | 67 | -- | Like @Fmt.unlinesF@, but strips trailing spaces and does not add a trailing newline. 68 | interpolateUnlinesF :: HasCallStack => Buildable a => NonEmpty a -> Builder 69 | interpolateUnlinesF = stripLastNewline . stripTrailingSpaces . unlinesF 70 | 71 | -- remove trailing whitespace from all lines. 72 | -- Note: output always ends with newline (adds trailing newline if there wasn't one). 73 | stripTrailingSpaces :: Builder -> Builder 74 | stripTrailingSpaces 75 | = fromLazyText 76 | . TL.unlines 77 | . map (TL.stripEnd) 78 | . TL.lines 79 | . toLazyText 80 | 81 | stripLastNewline :: HasCallStack => Builder -> Builder 82 | stripLastNewline 83 | = fromLazyText 84 | . fromMaybe (error "stripLastNewline: expected newline to strip") 85 | . TL.stripSuffix "\n" 86 | . toLazyText 87 | -------------------------------------------------------------------------------- /stack.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2018-2023 Serokell 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | # To update hackage and stackage indexes used by CI run: 6 | # $ niv update hackage.nix; niv update stackage.nix 7 | resolver: lts-22.32 8 | 9 | packages: 10 | - . 11 | 12 | extra-deps: 13 | - nyan-interpolation-core-0.9.2 14 | - nyan-interpolation-0.9.2 15 | - ftp-client-0.5.1.6@sha256:fab127defe1efb165af58f84dbc0f57a39334e39cca9829946149b363a71d1ca,1694 16 | -------------------------------------------------------------------------------- /stack.yaml.lock: -------------------------------------------------------------------------------- 1 | # This file was autogenerated by Stack. 2 | # You should not edit this file by hand. 3 | # For more information, please see the documentation at: 4 | # https://docs.haskellstack.org/en/stable/lock_files 5 | 6 | packages: 7 | - completed: 8 | hackage: cmark-gfm-0.2.5@sha256:a53b3c6ed20b5476ae18df5f28ababbb6ec8543f9a0758f0381a532d7a879fc0,5188 9 | pantry-tree: 10 | sha256: 8d137f125ee673d5c6b193c85657ea94124c90869edada72633df282e33058e0 11 | size: 4556 12 | original: 13 | hackage: cmark-gfm-0.2.5 14 | - completed: 15 | hackage: nyan-interpolation-core-0.9.2@sha256:930202fafc4e9472f9aed3d216a459e23454db500bfd0e0a5af2a4e5c5202096,4523 16 | pantry-tree: 17 | sha256: 5781e80383996a6bea95a545e4f0e70466bebf0fe2d388f334cd42e9f35469d5 18 | size: 1464 19 | original: 20 | hackage: nyan-interpolation-core-0.9.2 21 | - completed: 22 | hackage: nyan-interpolation-0.9.2@sha256:fb0b07ef6a9f8ca4d2e1db2f2df841c649556d9f6cff894ebf6b9ffbb7c25003,4276 23 | pantry-tree: 24 | sha256: 258878b8660782bef633fe3800c08fff2fa90165fd2a0793fa73836d8ff274c3 25 | size: 662 26 | original: 27 | hackage: nyan-interpolation-0.9.2 28 | - completed: 29 | hackage: ftp-client-0.5.1.6@sha256:fab127defe1efb165af58f84dbc0f57a39334e39cca9829946149b363a71d1ca,1694 30 | pantry-tree: 31 | sha256: 721799835406e36c6f14b032b1de9c95ae038261807c228a694342a1da8375bd 32 | size: 322 33 | original: 34 | hackage: ftp-client-0.5.1.6@sha256:fab127defe1efb165af58f84dbc0f57a39334e39cca9829946149b363a71d1ca,1694 35 | snapshots: 36 | - completed: 37 | sha256: 417fa04a2ed8916cdae74c475ff97ac80857fed5000f19dce4f9564b5e635294 38 | size: 720000 39 | url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/22/32.yaml 40 | original: lts-22.32 41 | -------------------------------------------------------------------------------- /stack.yaml.lock.license: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2019 Serokell 2 | # SPDX-License-Identifier: MPL-2.0 3 | -------------------------------------------------------------------------------- /tests/Main.hs: -------------------------------------------------------------------------------- 1 | -- SPDX-FileCopyrightText: 2022 Serokell 2 | -- 3 | -- SPDX-License-Identifier: MPL-2.0 4 | 5 | module Main 6 | ( main 7 | ) where 8 | 9 | import Universum 10 | 11 | import Test.Tasty 12 | import Test.Tasty.Ingredients (Ingredient) 13 | import Test.Xrefcheck.Util (mockServerOptions) 14 | import Tree (tests) 15 | 16 | main :: IO () 17 | main = tests >>= defaultMainWithIngredients ingredients 18 | 19 | ingredients :: [Ingredient] 20 | ingredients = includingOptions mockServerOptions : defaultIngredients 21 | -------------------------------------------------------------------------------- /tests/Test/Xrefcheck/AnchorsInHeadersSpec.hs: -------------------------------------------------------------------------------- 1 | {- SPDX-FileCopyrightText: 2019 Serokell 2 | - 3 | - SPDX-License-Identifier: MPL-2.0 4 | -} 5 | 6 | module Test.Xrefcheck.AnchorsInHeadersSpec where 7 | 8 | import Universum hiding ((^.)) 9 | 10 | import Control.Lens ((^.)) 11 | import Test.Tasty (TestTree, testGroup) 12 | import Test.Tasty.HUnit (testCase, (@?=)) 13 | 14 | import Test.Xrefcheck.Util 15 | import Xrefcheck.Core 16 | import Xrefcheck.System 17 | 18 | test_anchorsInHeaders :: TestTree 19 | test_anchorsInHeaders = testGroup "Anchors in headers" 20 | [ testCase "Check if anchors in headers are recognized" $ do 21 | (fi, errs) <- parse GitHub "" $ mkRelPosixLink "tests/markdowns/without-annotations/anchors_in_headers.md" 22 | getAnchors fi @?= ["some-stuff", "stuff-section"] 23 | errs @?= [] 24 | , testCase "Check if anchors with id attributes are recognized" $ do 25 | (fi, errs) <- parse GitHub "" $ mkRelPosixLink "tests/markdowns/without-annotations/anchors_in_headers_with_id_attribute.md" 26 | getAnchors fi @?= ["some-stuff-with-id-attribute", "stuff-section-with-id-attribute"] 27 | errs @?= [] 28 | ] 29 | where 30 | getAnchors :: FileInfo -> [Text] 31 | getAnchors fi = map aName $ fi ^. fiAnchors 32 | -------------------------------------------------------------------------------- /tests/Test/Xrefcheck/AnchorsSpec.hs: -------------------------------------------------------------------------------- 1 | {- SPDX-FileCopyrightText: 2019 Serokell 2 | - 3 | - SPDX-License-Identifier: MPL-2.0 4 | -} 5 | 6 | module Test.Xrefcheck.AnchorsSpec where 7 | 8 | import Universum hiding ((^.)) 9 | 10 | import Control.Lens ((^.)) 11 | import Test.Tasty (TestTree, testGroup) 12 | import Test.Tasty.HUnit (testCase, (@?=)) 13 | 14 | import Test.Xrefcheck.Util 15 | import Xrefcheck.Core 16 | import Xrefcheck.System 17 | 18 | checkHeaderConversions :: Flavor -> [(Text, Text)] -> TestTree 19 | checkHeaderConversions fl suites = 20 | testGroup (show fl) $ 21 | [testCase (show a <> " == " <> show b) $ headerToAnchor fl a @?= b | (a,b) <- suites] 22 | ++ 23 | [ testCase "Non-stripped header name should be stripped" $ do 24 | (fi, errs) <- parse fl "" $ mkRelPosixLink "tests/markdowns/without-annotations/non_stripped_spaces.md" 25 | getAnchors fi @?= [ case fl of GitHub -> "header--with-leading-spaces" 26 | GitLab -> "header-with-leading-spaces" 27 | , "edge-case" 28 | ] 29 | errs @?= [] 30 | ] 31 | where 32 | getAnchors :: FileInfo -> [Text] 33 | getAnchors fi = map aName $ fi ^. fiAnchors 34 | 35 | test_anchors :: TestTree 36 | test_anchors = do 37 | testGroup "Header-to-anchor conversion" 38 | [ checkHeaderConversions GitHub 39 | [ ( "Some header" 40 | , "some-header" 41 | ) 42 | , ( "Do +5 times" 43 | , "do-5-times" 44 | ) 45 | , ( "a # b" 46 | , "a--b" 47 | ) 48 | , ( "a ## b" 49 | , "a--b" 50 | ) 51 | , ( "a - b" 52 | , "a---b" 53 | ) 54 | , ( "a * b" 55 | , "a--b" 56 | ) 57 | , ( "a / b" 58 | , "a--b" 59 | ) 60 | , ( "a \\ b" 61 | , "a--b" 62 | ) 63 | , ( "a + b" 64 | , "a--b" 65 | ) 66 | , ( "a+b" 67 | , "ab" 68 | ) 69 | , ( "a & b" 70 | , "a--b" 71 | ) 72 | , ( "a &b" 73 | , "a-b" 74 | ) 75 | , ( "a - -- - b" 76 | , "a--------b" 77 | ) 78 | , ( "a -+--|- b" 79 | , "a------b" 80 | ) 81 | , ( "Some *italic* text" 82 | , "some-italic-text" 83 | ) 84 | , ( "-Some-text-with--many----hyphens-" 85 | , "-some-text-with--many----hyphens-" 86 | ) 87 | , ( "- A -" 88 | , "--a--" 89 | ) 90 | , ( "Some-+++++--mess++-mda" 91 | , "some---mess-mda" 92 | ) 93 | , ( ":white_check_mark: Checklist for your Pull Request" 94 | , "white_check_mark-checklist-for-your-pull-request" 95 | ) 96 | ] 97 | , checkHeaderConversions GitLab 98 | [ ( "a # b" 99 | , "a-b" 100 | ) 101 | , ( "a - b" 102 | , "a-b" 103 | ) 104 | , ( "a -- b" 105 | , "a-b" 106 | ) 107 | , ( "a & b" 108 | , "a-b" 109 | ) 110 | , ( "a + b" 111 | , "a-b" 112 | ) 113 | , ( "a+b" 114 | , "ab" 115 | ) 116 | , ( "a - -- - b" 117 | , "a-b" 118 | ) 119 | , ( "a -+--|- b" 120 | , "a-b" 121 | ) 122 | , ( "Some *italic* text" 123 | , "some-italic-text" 124 | ) 125 | , ( "-Some-text-with--many----hyphens-" 126 | , "-some-text-with-many-hyphens-" 127 | ) 128 | , ( "- A -" 129 | , "-a-" 130 | ) 131 | , ( "Some-+++++--mess++-mda" 132 | , "some-mess-mda" 133 | ) 134 | , ( ":white_check_mark: Checklist for your Pull Request" 135 | , "white_check_mark-checklist-for-your-pull-request" 136 | ) 137 | ] 138 | ] 139 | -------------------------------------------------------------------------------- /tests/Test/Xrefcheck/CanonicalRelPosixLinkSpec.hs: -------------------------------------------------------------------------------- 1 | {- SPDX-FileCopyrightText: 2023 Serokell 2 | - 3 | - SPDX-License-Identifier: MPL-2.0 4 | -} 5 | 6 | module Test.Xrefcheck.CanonicalRelPosixLinkSpec where 7 | 8 | import Universum 9 | 10 | import Test.Tasty (TestTree, testGroup) 11 | import Test.Tasty.HUnit (testCase, (@?), (@?=)) 12 | 13 | import Xrefcheck.System 14 | 15 | test_canonicalRelPosixLink :: TestTree 16 | test_canonicalRelPosixLink = 17 | testGroup "Canonical relative POSIX links" 18 | [ testGroup "Normalization" 19 | [ testCase "Trailing separator" $ 20 | on (@?=) mkCanonicalLink "./example/dir/" "example/dir" 21 | , testCase "Parent directory indirection" $ 22 | on (@?=) mkCanonicalLink "dir1/../dir2" "dir2" 23 | , testCase "Through parent directory indirection" $ 24 | hasUnexpanededParentIndirections (mkCanonicalLink "dir1/../../../dir2") @? "Unexpanded indirections" 25 | , testCase "Current directory indirection" $ 26 | on (@?=) mkCanonicalLink "././dir1/./././dir2/././" "dir1/dir2" 27 | , testCase "Mixed indirections result in current directory" $ 28 | on (@?=) mkCanonicalLink "././dir1/./.././dir2/./../" "." 29 | ] 30 | , testGroup "Intermediate directories" 31 | [ testCase "Current directory itself" $ 32 | on (@?=) (fmap canonicalizeRelPosixLink) (getIntermediateDirs (mkRelPosixLink ".")) $ 33 | fmap mkRelPosixLink ["."] 34 | , testCase "Current directory file" $ 35 | on (@?=) (fmap canonicalizeRelPosixLink) (getIntermediateDirs (mkRelPosixLink "./file")) $ 36 | fmap mkRelPosixLink ["."] 37 | , testCase "Parent directory itself" $ 38 | on (@?=) (fmap canonicalizeRelPosixLink) (getIntermediateDirs (mkRelPosixLink "..")) $ 39 | fmap mkRelPosixLink ["."] 40 | , testCase "Parent directory file" $ 41 | on (@?=) (fmap canonicalizeRelPosixLink) (getIntermediateDirs (mkRelPosixLink "../file")) $ 42 | fmap mkRelPosixLink [".", ".."] 43 | , testCase "Intermediate directories" $ 44 | on (@?=) (fmap canonicalizeRelPosixLink) (getIntermediateDirs (mkRelPosixLink "./example/dir/file")) $ 45 | fmap mkRelPosixLink [".", "example", "example/dir"] 46 | ] 47 | ] 48 | where 49 | mkCanonicalLink = canonicalizeRelPosixLink . mkRelPosixLink 50 | -------------------------------------------------------------------------------- /tests/Test/Xrefcheck/ConfigSpec.hs: -------------------------------------------------------------------------------- 1 | {- SPDX-FileCopyrightText: 2019-2021 Serokell 2 | - 3 | - SPDX-License-Identifier: MPL-2.0 4 | -} 5 | 6 | module Test.Xrefcheck.ConfigSpec where 7 | 8 | import Universum hiding ((.~)) 9 | 10 | import Control.Concurrent (forkIO, killThread) 11 | import Control.Exception qualified as E 12 | import Control.Lens ((.~)) 13 | 14 | import Data.List (isInfixOf) 15 | import Data.Yaml (ParseException (..), decodeEither') 16 | import Network.HTTP.Types (Status (..)) 17 | import Test.Tasty (TestTree, askOption, testGroup) 18 | import Test.Tasty.HUnit (assertFailure, testCase, (@?=)) 19 | import Test.Tasty.QuickCheck (ioProperty, testProperty) 20 | 21 | import Xrefcheck.Config 22 | import Xrefcheck.Core (Flavor (GitHub), allFlavors) 23 | import Xrefcheck.Scan (ecIgnoreExternalRefsToL) 24 | import Xrefcheck.Verify (VerifyError (..), checkExternalResource) 25 | 26 | import Test.Xrefcheck.Util (mockServer, mockServerUrl) 27 | 28 | test_config :: [TestTree] 29 | test_config = 30 | [ testGroup "Default config is valid" [ 31 | testProperty (show flavor) $ 32 | ioProperty $ evaluateWHNF_ @_ @Config (defConfig flavor) 33 | | flavor <- allFlavors] 34 | , testGroup "Filled default config matches the expected format" 35 | -- The config we match against can be regenerated with 36 | -- stack exec xrefcheck -- dump-config -t GitHub -o tests/configs/github-config.yaml --force 37 | [ testCase "Config matches" $ do 38 | config <- readFile "tests/configs/github-config.yaml" 39 | when (config /= defConfigText GitHub) $ 40 | assertFailure $ toString $ unwords 41 | [ "Config does not match the expected format." 42 | , "Run" 43 | , "`stack exec xrefcheck -- dump-config -t GitHub -o tests/configs/github-config.yaml --force`" 44 | , "and verify changes" 45 | ] 46 | ] 47 | , askOption $ \mockServerPort -> 48 | testGroup "`ignoreAuthFailures` working as expected" $ 49 | let config = defConfig GitHub & cExclusionsL . ecIgnoreExternalRefsToL .~ [] 50 | 51 | setIgnoreAuthFailures value = 52 | config & cNetworkingL . ncIgnoreAuthFailuresL .~ value 53 | in [ testCase "when True - assume 401 status is valid" $ 54 | checkLinkWithServer mockServerPort (setIgnoreAuthFailures True) 55 | "/401" $ Right () 56 | 57 | , testCase "when False - assume 401 status is invalid" $ 58 | checkLinkWithServer mockServerPort (setIgnoreAuthFailures False) 59 | "/401" $ 60 | Left $ ExternalHttpResourceUnavailable $ 61 | Status { statusCode = 401, statusMessage = "Unauthorized" } 62 | 63 | , testCase "when True - assume 403 status is valid" $ 64 | checkLinkWithServer mockServerPort (setIgnoreAuthFailures True) 65 | "/403" $ Right () 66 | 67 | , testCase "when False - assume 403 status is invalid" $ 68 | checkLinkWithServer mockServerPort (setIgnoreAuthFailures False) 69 | "/403" $ 70 | Left $ ExternalHttpResourceUnavailable $ 71 | Status { statusCode = 403, statusMessage = "Forbidden" } 72 | ] 73 | , testGroup "Config parser reject input with unknown fields" 74 | [ testCase "throws error with useful messages" $ do 75 | case decodeEither' @Config $ encodeUtf8 $ defConfigText GitHub <> "strangeField: []" of 76 | Left (AesonException str) -> 77 | if "unknown fields: [\"strangeField\"]" `isInfixOf` str 78 | then pure () 79 | else assertFailure $ "Bad error message: " <> str 80 | _ -> assertFailure "Config parser accepted config with unknown field" 81 | ] 82 | ] 83 | 84 | where 85 | checkLinkWithServer mockServerPort config link expectation = 86 | E.bracket (forkIO (mockServer mockServerPort)) killThread $ \_ -> do 87 | let url = mockServerUrl mockServerPort link 88 | result <- runExceptT $ checkExternalResource emptyChain config url 89 | result @?= expectation 90 | -------------------------------------------------------------------------------- /tests/Test/Xrefcheck/IgnoreAnnotationsSpec.hs: -------------------------------------------------------------------------------- 1 | {- SPDX-FileCopyrightText: 2019 Serokell 2 | - 3 | - SPDX-License-Identifier: MPL-2.0 4 | -} 5 | 6 | module Test.Xrefcheck.IgnoreAnnotationsSpec where 7 | 8 | import Universum hiding ((^.)) 9 | 10 | import CMarkGFM (PosInfo (..)) 11 | import Control.Lens ((^.)) 12 | import System.FilePath qualified as FP 13 | import Test.Tasty (TestTree, testGroup) 14 | import Test.Tasty.HUnit (testCase, (@?=)) 15 | 16 | import Test.Xrefcheck.Util 17 | import Xrefcheck.Core 18 | import Xrefcheck.Scan 19 | import Xrefcheck.Scanners.Markdown 20 | import Xrefcheck.System 21 | 22 | test_ignoreAnnotations :: [TestTree] 23 | test_ignoreAnnotations = 24 | [ testGroup "Parsing failures" 25 | [ testCase "Check if broken link annotation produce error" do 26 | let file = "tests" FP. "markdowns" FP. "with-annotations" FP. "no_link.md" 27 | errs <- getErrs file 28 | errs @?= makeError file (Just $ PosInfo 7 1 7 31) LinkErr 29 | , testCase "Check if broken paragraph annotation produce error" do 30 | let file = "tests" FP. "markdowns" FP. "with-annotations" FP. "no_paragraph.md" 31 | errs <- getErrs file 32 | errs @?= makeError file (Just $ PosInfo 7 1 7 35) (ParagraphErr "HEADING") 33 | , testCase "Check if broken ignore all annotation produce error" do 34 | let file = "tests" FP. "markdowns" FP. "with-annotations" FP. "unexpected_ignore_file.md" 35 | errs <- getErrs file 36 | errs @?= makeError file (Just $ PosInfo 9 1 9 29) FileErr 37 | , testCase "Check if broken unrecognised annotation produce error" do 38 | let file = "tests" FP. "markdowns" FP. "with-annotations" FP. "unrecognised_option.md" 39 | errs <- getErrs file 40 | errs @?= makeError file (Just $ PosInfo 7 1 7 46) (UnrecognisedErr "unrecognised-option") 41 | ] 42 | , testGroup "\"ignore link\" mode" 43 | [ testCase "Check \"ignore link\" performance" $ do 44 | let file = "tests" FP. "markdowns" FP. "with-annotations" FP. "ignore_link.md" 45 | (fi, errs) <- parse GitHub "" (mkRelPosixLink file) 46 | getRefs fi @?= 47 | ["team", "team", "team", "hire-us", "how-we-work", "privacy", "link2", "link2", "link3"] 48 | errs @?= makeError file (Just $ PosInfo 42 1 42 31) LinkErr 49 | ] 50 | , testGroup "\"ignore paragraph\" mode" 51 | [ testCase "Check \"ignore paragraph\" performance" $ do 52 | let file = mkRelPosixLink $ "tests" FP. "markdowns" FP. "with-annotations" FP. "ignore_paragraph.md" 53 | (fi, errs) <- parse GitHub "" file 54 | getRefs fi @?= ["blog", "contacts"] 55 | errs @?= [] 56 | ] 57 | , testGroup "\"ignore all\" mode" 58 | [ testCase "Check \"ignore all\" performance" $ do 59 | let file = mkRelPosixLink $ "tests" FP. "markdowns" FP. "with-annotations" FP. "ignore_file.md" 60 | (fi, errs) <- parse GitHub "" file 61 | getRefs fi @?= [] 62 | errs @?= [] 63 | ] 64 | ] 65 | where 66 | getRefs :: FileInfo -> [Text] 67 | getRefs fi = map rName $ fi ^. fiReferences 68 | 69 | getErrs :: FilePath -> IO [ScanError 'Parse] 70 | getErrs path = snd <$> parse GitHub "" (mkRelPosixLink path) 71 | -------------------------------------------------------------------------------- /tests/Test/Xrefcheck/IgnoreRegexSpec.hs: -------------------------------------------------------------------------------- 1 | {- SPDX-FileCopyrightText: 2019 Serokell 2 | - 3 | - SPDX-License-Identifier: MPL-2.0 4 | -} 5 | 6 | module Test.Xrefcheck.IgnoreRegexSpec where 7 | 8 | import Universum hiding ((.~), (^.)) 9 | 10 | import Control.Lens ((.~), (^.)) 11 | import Data.Reflection (give) 12 | import Data.Yaml (decodeEither') 13 | import Test.Tasty (TestTree, testGroup) 14 | import Test.Tasty.HUnit (assertFailure, testCase) 15 | import Text.Regex.TDFA (Regex) 16 | 17 | import Xrefcheck.Config 18 | import Xrefcheck.Core 19 | import Xrefcheck.Progress (allowRewrite) 20 | import Xrefcheck.Scan 21 | import Xrefcheck.Scanners.Markdown 22 | import Xrefcheck.System 23 | import Xrefcheck.Util (ColorMode (WithoutColors)) 24 | import Xrefcheck.Verify 25 | 26 | test_ignoreRegex :: TestTree 27 | test_ignoreRegex = give WithoutColors $ 28 | let root = "tests/markdowns/without-annotations" 29 | showProgressBar = False 30 | fileSupport = 31 | give (PrintUnixPaths False) $ 32 | firstFileSupport [markdownSupport defGithubMdConfig] 33 | verifyMode = ExternalOnlyMode 34 | 35 | linksTxt = 36 | [ "https://bad.((external.)?)reference(/?)" 37 | , "https://bad.reference.(org|com)" 38 | ] 39 | regexs = linksToRegexs linksTxt 40 | config = setIgnoreRefs regexs (defConfig GitHub) 41 | 42 | 43 | in testGroup "Regular expressions performance" 44 | [ testCase "Check that only not matched links are verified" $ do 45 | scanResult <- allowRewrite showProgressBar $ \rw -> 46 | scanRepo OnlyTracked rw fileSupport (config ^. cExclusionsL) root 47 | 48 | verifyRes <- allowRewrite showProgressBar $ \rw -> 49 | verifyRepo rw config verifyMode $ srRepoInfo scanResult 50 | 51 | let brokenLinks = pickBrokenLinks verifyRes 52 | 53 | let matchedLinks = 54 | [ "https://bad.referenc/" 55 | , "https://bad.reference" 56 | , "https://bad.external.reference/" 57 | , "https://bad.external.reference" 58 | , "https://bad.reference.org" 59 | , "https://bad.reference.com" 60 | ] 61 | 62 | let notMatchedLinks = 63 | [ "https://non-existent.reference/" 64 | , "https://bad.externall.reference" 65 | , "https://bad.reference.io" 66 | ] 67 | 68 | forM_ matchedLinks $ \link -> do 69 | when (link `elem` brokenLinks) $ 70 | assertFailure $ 71 | "Link \"" <> show link <> 72 | "\" is considered as broken but it should be ignored" 73 | 74 | forM_ notMatchedLinks $ \link -> do 75 | when (link `notElem` brokenLinks) $ 76 | assertFailure $ 77 | "Link \"" <> show link <> 78 | "\" is not considered as broken but it is (and shouldn't be ignored)" 79 | ] 80 | 81 | where 82 | pickBrokenLinks :: VerifyResult (WithReferenceLoc VerifyError) -> [Text] 83 | pickBrokenLinks verifyRes = 84 | case verifyErrors verifyRes of 85 | Just neWithRefLoc -> mapMaybe (rUrl . wrlReference) $ toList neWithRefLoc 86 | Nothing -> [] 87 | 88 | rUrl :: Reference -> Maybe Text 89 | rUrl Reference{..} = 90 | case rInfo of 91 | RIExternal (ELUrl url) -> Just url 92 | _ -> Nothing 93 | 94 | linksToRegexs :: [Text] -> [Regex] 95 | linksToRegexs links = 96 | let errOrRegexs = map (decodeEither' . encodeUtf8) links 97 | in map (either (error . show) id) errOrRegexs 98 | 99 | setIgnoreRefs :: [Regex] -> Config -> Config 100 | setIgnoreRefs regexs = (cExclusionsL . ecIgnoreExternalRefsToL) .~ regexs 101 | -------------------------------------------------------------------------------- /tests/Test/Xrefcheck/RedirectDefaultSpec.hs: -------------------------------------------------------------------------------- 1 | {- SPDX-FileCopyrightText: 2022 Serokell 2 | - 3 | - SPDX-License-Identifier: MPL-2.0 4 | -} 5 | 6 | module Test.Xrefcheck.RedirectDefaultSpec where 7 | 8 | import Universum 9 | 10 | import Data.CaseInsensitive qualified as CI 11 | import Data.Set qualified as S 12 | import Network.HTTP.Types (Status, mkStatus) 13 | import Network.HTTP.Types.Header (HeaderName, hLocation) 14 | import Network.Wai qualified as Web 15 | import Test.Tasty (TestName, TestTree, testGroup) 16 | import Test.Tasty.HUnit (Assertion, testCase) 17 | import Web.Scotty qualified as Web 18 | 19 | import Test.Xrefcheck.UtilRequests 20 | import Xrefcheck.Config 21 | import Xrefcheck.Progress 22 | import Xrefcheck.Verify 23 | 24 | test_redirectRequests :: TestTree 25 | test_redirectRequests = testGroup "Redirect response defaults" 26 | [ testGroup "Temporary" $ allowedRedirectTests <$> [302, 303, 307] 27 | , testGroup "Permanent" $ permanentRedirectTests <$> [301, 308] 28 | , testGroup "304 Not Modified" $ allowedRedirectTests <$> [304] 29 | ] 30 | where 31 | url :: Text 32 | url = "http://127.0.0.1:5000/redirect" 33 | 34 | location :: Maybe Text 35 | location = Just "http://127.0.0.1:5000/other" 36 | 37 | allowedRedirectTests :: Int -> TestTree 38 | allowedRedirectTests statusCode = 39 | redirectTests 40 | (show statusCode <> " passes by default") 41 | (mkStatus statusCode "Allowed redirect") 42 | (\case 43 | Nothing -> Just $ RedirectMissingLocation $ fromList [url] 44 | Just _ -> Nothing 45 | ) 46 | 47 | permanentRedirectTests :: Int -> TestTree 48 | permanentRedirectTests statusCode = 49 | redirectTests 50 | (show statusCode <> " fails by default") 51 | (mkStatus statusCode "Permanent redirect") 52 | (\case 53 | Nothing -> Just $ RedirectMissingLocation $ fromList [url] 54 | Just loc -> Just $ RedirectRuleError (fromList [url, loc]) (Just RROPermanent) 55 | ) 56 | 57 | redirectTests :: TestName -> Status -> (Maybe Text -> Maybe VerifyError) -> TestTree 58 | redirectTests name expectedStatus expectedError = 59 | testGroup name 60 | [ 61 | testCase "With no location" $ 62 | redirectAssertion expectedStatus Nothing (expectedError Nothing), 63 | testCase "With location" $ 64 | redirectAssertion expectedStatus location (expectedError location) 65 | ] 66 | 67 | redirectAssertion :: Status -> Maybe Text -> Maybe VerifyError -> Assertion 68 | redirectAssertion expectedStatus expectedLocation expectedError = do 69 | setRef <- newIORef S.empty 70 | checkLinkAndProgressWithServerDefault 71 | setRef 72 | (5000, mockRedirect expectedLocation expectedStatus) 73 | url 74 | ( (if isNothing expectedError then reportSuccess else reportError) "" $ 75 | initProgress 1 76 | ) 77 | (VerifyResult $ maybeToList expectedError) 78 | 79 | mockRedirect :: Maybe Text -> Status -> IO Web.Application 80 | mockRedirect expectedLocation expectedStatus = 81 | Web.scottyApp $ Web.matchAny "/redirect" $ do 82 | whenJust expectedLocation (setHeader hLocation) 83 | Web.status expectedStatus 84 | 85 | setHeader :: HeaderName -> Text -> Web.ActionM () 86 | setHeader hdr value = Web.setHeader (decodeUtf8 (CI.original hdr)) (fromStrict value) 87 | -------------------------------------------------------------------------------- /tests/Test/Xrefcheck/TrailingSlashSpec.hs: -------------------------------------------------------------------------------- 1 | {- SPDX-FileCopyrightText: 2019 Serokell 2 | - 3 | - SPDX-License-Identifier: MPL-2.0 4 | -} 5 | 6 | module Test.Xrefcheck.TrailingSlashSpec where 7 | 8 | import Universum hiding ((.~)) 9 | 10 | import Control.Lens ((.~)) 11 | import Data.Reflection (give) 12 | import System.Directory (doesFileExist) 13 | import Test.Tasty (TestTree, testGroup) 14 | import Test.Tasty.HUnit (assertFailure, testCase) 15 | import Text.Interpolation.Nyan 16 | 17 | import Xrefcheck.Config 18 | import Xrefcheck.Core 19 | import Xrefcheck.Progress 20 | import Xrefcheck.Scan 21 | import Xrefcheck.Scanners.Markdown 22 | import Xrefcheck.System 23 | import Xrefcheck.Util 24 | 25 | test_slash :: TestTree 26 | test_slash = testGroup "Trailing forward slash detection" $ 27 | let config = defConfig GitHub 28 | fileSupport = 29 | give (PrintUnixPaths False) $ 30 | firstFileSupport [markdownSupport (scMarkdown (cScanners config))] 31 | in roots <&> \root -> 32 | testCase ("All the files within the root \"" <> 33 | root <> 34 | "\" should exist") $ do 35 | (ScanResult _ RepoInfo{..}) <- allowRewrite False $ \rw -> 36 | scanRepo OnlyTracked rw fileSupport (cExclusions config & ecIgnoreL .~ []) root 37 | nonExistentFiles <- lefts <$> forM (fst . snd <$> toPairs riFiles) (\file -> do 38 | predicate <- doesFileExist . filePathFromRoot root $ file 39 | return $ if predicate 40 | then Right () 41 | else Left . filePathFromRoot root $ file) 42 | whenJust (nonEmpty nonExistentFiles) $ \files -> 43 | assertFailure 44 | [int|| 45 | Expected all filepaths to be valid, but these filepaths do not exist: 46 | #{interpolateBlockListF files} 47 | |] 48 | where 49 | roots :: [FilePath] 50 | roots = 51 | [ "tests/markdowns/without-annotations" 52 | , "tests/markdowns/without-annotations/" 53 | , "tests/markdowns/without-annotations/./" 54 | ] 55 | -------------------------------------------------------------------------------- /tests/Test/Xrefcheck/URIParsingSpec.hs: -------------------------------------------------------------------------------- 1 | {- SPDX-FileCopyrightText: 2022 Serokell 2 | - 3 | - SPDX-License-Identifier: MPL-2.0 4 | -} 5 | 6 | module Test.Xrefcheck.URIParsingSpec where 7 | 8 | import Universum 9 | 10 | import Test.Tasty (TestTree, testGroup) 11 | import Test.Tasty.HUnit (testCase, (@?=)) 12 | import Text.URI (URI) 13 | import Text.URI.QQ (uri) 14 | import URI.ByteString qualified as URIBS 15 | 16 | import Xrefcheck.Data.URI (UriParseError (..), parseUri) 17 | 18 | test_uri :: [TestTree] 19 | test_uri = 20 | [ testGroup "URI parsing should be successful" 21 | [ testCase "Without the special characters in the query strings" do 22 | parseUri' "https://example.com/?q=a&p=b#fragment" >>= 23 | (@?= Right [uri|https://example.com/?q=a&p=b#fragment|]) 24 | parseUri' "https://example.com/path/to/smth?q=a&p=b" >>= 25 | (@?= Right [uri|https://example.com/path/to/smth?q=a&p=b|]) 26 | 27 | , testCase "With the special characters in the query strings" do 28 | parseUri' "https://example.com/?q=[a]&

={b}#fragment" >>= 29 | (@?= Right 30 | [uri|https://example.com/?q=%5Ba%5D&%3Cp%3E=%7Bb%7D#fragment|]) 31 | 32 | parseUri' "https://example.com/path/to/smth?q=[a]&

={b}" >>= 33 | (@?= Right 34 | [uri|https://example.com/path/to/smth?q=%5Ba%5D&%3Cp%3E=%7Bb%7D|]) 35 | ] 36 | , testGroup "URI parsing should be unsuccessful" 37 | [ testCase "With the special characters anywhere else" do 38 | parseUri' "https://exam/?q=a&p=b#fra{g}ment" >>= 39 | (@?= Left (UPEInvalid URIBS.MalformedPath)) 40 | 41 | parseUri' "https://example.com/pa[t]h/to[/]smth?q=a&p=b" >>= 42 | (@?= Left (UPEInvalid URIBS.MalformedPath)) 43 | 44 | , testCase "With malformed scheme" do 45 | parseUri' "https//example.com/" >>= 46 | (@?= Left (UPEInvalid $ URIBS.MalformedScheme URIBS.MissingColon)) 47 | 48 | , testCase "With malformed fragment" do 49 | parseUri' "https://example.com/?q=a&p=b#fra{g}ment" >>= 50 | (@?= Left (UPEInvalid URIBS.MalformedFragment)) 51 | ] 52 | ] 53 | where 54 | parseUri' :: Text -> IO $ Either UriParseError URI 55 | parseUri' = runExceptT . parseUri False 56 | -------------------------------------------------------------------------------- /tests/Test/Xrefcheck/Util.hs: -------------------------------------------------------------------------------- 1 | {- SPDX-FileCopyrightText: 2019 Serokell 2 | - 3 | - SPDX-License-Identifier: MPL-2.0 4 | -} 5 | 6 | module Test.Xrefcheck.Util where 7 | 8 | import Universum 9 | 10 | import Data.Reflection (give) 11 | import Data.Tagged (untag) 12 | import Network.HTTP.Types (forbidden403, unauthorized401) 13 | import Network.Wai.Handler.Warp qualified as Web 14 | import Options.Applicative (auto, help, long, option) 15 | import Test.Tasty.Options as Tasty (IsOption (..), OptionDescription (Option), safeRead) 16 | import Web.Scotty qualified as Web 17 | 18 | import Xrefcheck.Core (Flavor) 19 | import Xrefcheck.Scan (ScanAction) 20 | import Xrefcheck.Scanners.Markdown (MarkdownConfig (MarkdownConfig, mcFlavor), markdownScanner) 21 | import Xrefcheck.System (PrintUnixPaths (..)) 22 | 23 | parse :: Flavor -> ScanAction 24 | parse fl path = 25 | give (PrintUnixPaths False) $ 26 | markdownScanner MarkdownConfig { mcFlavor = fl } path 27 | 28 | mockServerUrl :: MockServerPort -> Text -> Text 29 | mockServerUrl (MockServerPort port) s = toText ("http://127.0.0.1:" <> show port <> s) 30 | 31 | mockServer :: MockServerPort -> IO () 32 | mockServer (MockServerPort port) = 33 | Web.run port <=< Web.scottyApp $ do 34 | Web.matchAny "/401" $ Web.status unauthorized401 35 | Web.matchAny "/403" $ Web.status forbidden403 36 | 37 | -- | All options needed to configure the mock server. 38 | mockServerOptions :: [OptionDescription] 39 | mockServerOptions = 40 | [ Tasty.Option (Proxy @MockServerPort) 41 | ] 42 | 43 | -- | Option specifying FTP host. 44 | newtype MockServerPort = MockServerPort Int 45 | deriving stock (Show, Eq) 46 | 47 | instance IsOption MockServerPort where 48 | defaultValue = MockServerPort 3000 49 | optionName = "mock-server-port" 50 | optionHelp = "[Test.Xrefcheck.Util] Mock server port" 51 | parseValue v = MockServerPort <$> safeRead v 52 | optionCLParser = MockServerPort <$> option auto 53 | ( long (untag @MockServerPort optionName) 54 | <> help (untag @MockServerPort optionHelp) 55 | ) 56 | -------------------------------------------------------------------------------- /tests/Test/Xrefcheck/UtilRequests.hs: -------------------------------------------------------------------------------- 1 | {- SPDX-FileCopyrightText: 2019 Serokell 2 | - 3 | - SPDX-License-Identifier: MPL-2.0 4 | -} 5 | 6 | module Test.Xrefcheck.UtilRequests 7 | ( checkLinkAndProgressWithServer 8 | , verifyLink 9 | , verifyReferenceWithProgress 10 | , checkMultipleLinksWithServer 11 | , checkLinkAndProgressWithServerDefault 12 | , verifyLinkDefault 13 | , verifyReferenceWithProgressDefault 14 | , withServer 15 | , VerifyLinkTestEntry (..) 16 | ) where 17 | 18 | import Universum hiding ((.~)) 19 | 20 | import Control.Concurrent (forkIO, killThread) 21 | import Control.Exception qualified as E 22 | import Control.Lens ((.~)) 23 | import Data.Map qualified as M 24 | import Data.Set qualified as S 25 | import Network.Wai qualified as Web 26 | import Network.Wai.Handler.Warp qualified as Web 27 | import Test.Tasty.HUnit (assertBool) 28 | import Text.Interpolation.Nyan 29 | 30 | import Xrefcheck.Config 31 | import Xrefcheck.Core 32 | import Xrefcheck.Progress 33 | import Xrefcheck.Scan 34 | import Xrefcheck.System 35 | import Xrefcheck.Util 36 | import Xrefcheck.Verify 37 | 38 | withServer :: (Int, IO Web.Application) -> IO () -> IO () 39 | withServer (port, createApp) act = do 40 | app <- createApp 41 | ready :: MVar () <- newEmptyMVar 42 | -- In the forked thread: the server puts () as soon as it's ready to process requests. 43 | -- In the current therad: wait for () before running the action. 44 | -- 45 | -- This ensures that we don't encounter this error: 46 | -- ConnectionFailure Network.Socket.connect: : does not exist (Connection refused) 47 | E.bracket (serve ready app) killThread (\_ -> takeMVar ready >> act) 48 | where 49 | serve ready app = 50 | forkIO $ Web.runSettings settings app 51 | where 52 | settings = 53 | Web.setBeforeMainLoop (putMVar ready ()) $ 54 | Web.setPort port Web.defaultSettings 55 | 56 | checkMultipleLinksWithServer 57 | :: (Int, IO Web.Application) 58 | -> IORef (S.Set DomainName) 59 | -> [VerifyLinkTestEntry] 60 | -> IO () 61 | checkMultipleLinksWithServer mock setRef entries = 62 | withServer mock $ do 63 | forM_ entries $ \VerifyLinkTestEntry {..} -> 64 | checkLinkAndProgress 65 | vlteConfigModifier 66 | setRef 67 | vlteLink 68 | vlteExpectedProgress 69 | vlteExpectationErrors 70 | 71 | checkLinkAndProgressWithServer 72 | :: (Config -> Config) 73 | -> IORef (Set DomainName) 74 | -> (Int, IO Web.Application) 75 | -> Text 76 | -> Progress Int Text 77 | -> VerifyResult VerifyError 78 | -> IO () 79 | checkLinkAndProgressWithServer configModifier setRef mock link progress vrExpectation = 80 | withServer mock $ 81 | checkLinkAndProgress configModifier setRef link progress vrExpectation 82 | 83 | checkLinkAndProgress 84 | :: (Config -> Config) 85 | -> IORef (Set DomainName) 86 | -> Text 87 | -> Progress Int Text 88 | -> VerifyResult VerifyError 89 | -> IO () 90 | checkLinkAndProgress configModifier setRef link progress vrExpectation = do 91 | (result, progRes) <- verifyLink configModifier setRef link 92 | flip assertBool (result == vrExpectation) 93 | [int|| 94 | Verification results differ: expected 95 | #{interpolateIndentF 2 (show vrExpectation)} 96 | but got 97 | #{interpolateIndentF 2 (show result)} 98 | |] 99 | flip assertBool (progRes `sameProgress` progress) 100 | [int|| 101 | Expected the progress bar state to be 102 | #{interpolateIndentF 2 (show progress)} 103 | but got 104 | #{interpolateIndentF 2 (show progRes)} 105 | |] 106 | 107 | checkLinkAndProgressWithServerDefault 108 | :: IORef (Set DomainName) 109 | -> (Int, IO Web.Application) 110 | -> Text 111 | -> Progress Int Text 112 | -> VerifyResult VerifyError 113 | -> IO () 114 | checkLinkAndProgressWithServerDefault = checkLinkAndProgressWithServer id 115 | 116 | verifyLink 117 | :: (Config -> Config) 118 | -> IORef (S.Set DomainName) 119 | -> Text 120 | -> IO (VerifyResult VerifyError, Progress Int Text) 121 | verifyLink configModifier setRef link = do 122 | let reference = Reference "" (Position "") $ RIExternal $ ELUrl link 123 | progRef <- newIORef $ initVerifyProgress [reference] 124 | result <- verifyReferenceWithProgress configModifier reference setRef progRef 125 | progress <- readIORef progRef 126 | return (result, vrExternal progress) 127 | 128 | verifyLinkDefault 129 | :: IORef (Set DomainName) 130 | -> Text 131 | -> IO (VerifyResult VerifyError, Progress Int Text) 132 | verifyLinkDefault = verifyLink id 133 | 134 | verifyReferenceWithProgress 135 | :: (Config -> Config) 136 | -> Reference 137 | -> IORef (S.Set DomainName) 138 | -> IORef VerifyProgress 139 | -> IO (VerifyResult VerifyError) 140 | verifyReferenceWithProgress configModifier reference setRef progRef = 141 | fmap wrlItem <$> verifyReference 142 | (defConfig GitHub & cExclusionsL . ecIgnoreExternalRefsToL .~ [] 143 | & configModifier) 144 | FullMode setRef progRef (RepoInfo M.empty mempty) (mkRelPosixLink "") reference 145 | 146 | verifyReferenceWithProgressDefault 147 | :: Reference 148 | -> IORef (Set DomainName) 149 | -> IORef VerifyProgress 150 | -> IO (VerifyResult VerifyError) 151 | verifyReferenceWithProgressDefault = verifyReferenceWithProgress id 152 | 153 | data VerifyLinkTestEntry = VerifyLinkTestEntry 154 | { vlteConfigModifier :: Config -> Config 155 | , vlteLink :: Text 156 | , vlteExpectedProgress :: Progress Int Text 157 | , vlteExpectationErrors :: VerifyResult VerifyError 158 | } 159 | -------------------------------------------------------------------------------- /tests/Tree.hs: -------------------------------------------------------------------------------- 1 | -- SPDX-FileCopyrightText: 2022 Serokell 2 | -- 3 | -- SPDX-License-Identifier: MPL-2.0 4 | 5 | {-# OPTIONS_GHC -F -pgmF tasty-discover -optF --tree-display -optF --generated-module -optF Tree #-} 6 | -------------------------------------------------------------------------------- /tests/configs/github-config.yaml: -------------------------------------------------------------------------------- 1 | # Exclusion parameters. 2 | exclusions: 3 | # Ignore these files. References to them will fail verification, 4 | # and references from them will not be verified. 5 | # List of glob patterns. 6 | ignore: [] 7 | 8 | # References from these files will not be verified. 9 | # List of glob patterns. 10 | ignoreRefsFrom: 11 | - .github/pull_request_template.md 12 | - .github/issue_template.md 13 | - .github/PULL_REQUEST_TEMPLATE/**/* 14 | - .github/ISSUE_TEMPLATE/**/* 15 | 16 | # References to these paths will not be verified. 17 | # List of glob patterns. 18 | ignoreLocalRefsTo: 19 | - ../../../issues 20 | - ../../../issues/* 21 | - ../../../pulls 22 | - ../../../pulls/* 23 | 24 | # References to these URIs will not be verified. 25 | # List of POSIX extended regular expressions. 26 | ignoreExternalRefsTo: 27 | # Ignore localhost links by default 28 | - (https?|ftps?)://(localhost|127\.0\.0\.1).* 29 | 30 | # Networking parameters. 31 | networking: 32 | # When checking external references, how long to wait on request before 33 | # declaring "Response timeout". 34 | externalRefCheckTimeout: 10s 35 | 36 | # Skip links which return 403 or 401 code. 37 | ignoreAuthFailures: true 38 | 39 | # When a verification result is a "429 Too Many Requests" response 40 | # and it does not contain a "Retry-After" header, 41 | # wait this amount of time before attempting to verify the link again. 42 | defaultRetryAfter: 30s 43 | 44 | # How many attempts to retry an external link after getting 45 | # a "429 Too Many Requests" response. 46 | # Timeouts may also be accounted here, see the description 47 | # of `maxTimeoutRetries` field. 48 | 49 | # If a site once responded with 429 error code, subsequent 50 | # request timeouts will also be treated as hitting the site's 51 | # rate limiter and result in retry attempts, unless the 52 | # maximum retries number has been reached. 53 | # 54 | # On other errors xrefcheck fails immediately, without retrying. 55 | maxRetries: 3 56 | 57 | # Querying a given domain that ever returned 429 before, 58 | # this defines how many timeouts are allowed during retries. 59 | # 60 | # For such domains, timeouts likely mean hitting the rate limiter, 61 | # and so xrefcheck considers timeouts in the same way as 429 errors. 62 | # 63 | # For other domains, a timeout results in a respective error, no retry 64 | # attempts will be performed. Use `externalRefCheckTimeout` option 65 | # to increase the time after which timeout is declared. 66 | # 67 | # This option is similar to `maxRetries`, the difference is that 68 | # this `maxTimeoutRetries` option limits only the number of retries 69 | # caused by timeouts, and `maxRetries` limits the number of retries 70 | # caused both by 429s and timeouts. 71 | maxTimeoutRetries: 1 72 | 73 | # Maximum number of links that can be followed in a single redirect 74 | # chain. 75 | # 76 | # The link is considered as invalid if the limit is exceeded. 77 | maxRedirectFollows: 10 78 | 79 | # Rules to override the redirect behavior for external references that 80 | # match, where 81 | # - 'from' is a regular expression for the source link in a single 82 | # redirection step. Its absence means that every link matches. 83 | # - 'to' is a regular expression for the target link in a single 84 | # redirection step. Its absence also means that every link matches. 85 | # - 'on' accepts 'temporary', 'permanent' or a specific redirect HTTP code. 86 | # Its absence also means that every response code matches. 87 | # - 'outcome' accepts 'valid', 'invalid' or 'follow'. The last one follows 88 | # the redirect by applying the same configuration rules so, for instance, 89 | # exclusion rules would also apply to the following links. 90 | # 91 | # The first one that matches is applied, and the link is considered 92 | # as valid if none of them does match. 93 | externalRefRedirects: 94 | - on: permanent 95 | outcome: invalid 96 | 97 | # Parameters of scanners for various file types. 98 | scanners: 99 | # On 'anchor not found' error, how much similar anchors should be displayed as 100 | # hint. Number should be between 0 and 1, larger value means stricter filter. 101 | anchorSimilarityThreshold: 0.5 102 | 103 | markdown: 104 | # Flavor of markdown, e.g. GitHub-flavor. 105 | # 106 | # This affects which anchors are generated for headers. 107 | flavor: GitHub 108 | -------------------------------------------------------------------------------- /tests/golden/check-anchors/ambiguous-anchors/a.md: -------------------------------------------------------------------------------- 1 | 6 | # Some text 7 | 8 | # Some **text** 9 | 10 | # some-text-longer 11 | 12 | ## some-text 13 | 14 | # Other text 15 | 16 | [ambiguous anchor in this file](#some-text) 17 | -------------------------------------------------------------------------------- /tests/golden/check-anchors/ambiguous-anchors/b.md: -------------------------------------------------------------------------------- 1 | 6 | [valid](a.md#other-text) 7 | [ambiguous anchor in other file](a.md#some-text) 8 | -------------------------------------------------------------------------------- /tests/golden/check-anchors/check-anchors.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | # SPDX-FileCopyrightText: 2022 Serokell 4 | # 5 | # SPDX-License-Identifier: MPL-2.0 6 | 7 | load '../helpers/bats-support/load' 8 | load '../helpers/bats-assert/load' 9 | load '../helpers/bats-file/load' 10 | load '../helpers' 11 | 12 | @test "We report ambiguous anchor references" { 13 | golden_file=$(realpath expected1.gold) 14 | to_temp xrefcheck -u -r ambiguous-anchors 15 | assert_diff 16 | } 17 | 18 | @test "We report references to non-existing anchors, giving hints about similar ones" { 19 | golden_file=$(realpath expected2.gold) 20 | to_temp xrefcheck -u -r non-existing-anchors 21 | assert_diff 22 | } 23 | -------------------------------------------------------------------------------- /tests/golden/check-anchors/expected1.gold: -------------------------------------------------------------------------------- 1 | ambiguous-anchors/a.md:16:1-43: bad reference: 2 | The reference to "ambiguous anchor in this file" failed verification. 3 | Ambiguous reference to anchor 'some-text' 4 | in file a.md 5 | It could refer to either: 6 | - some-text (header I) at ambiguous-anchors/a.md:6:1-11 7 | - some-text (header I) at ambiguous-anchors/a.md:8:1-15 8 | - some-text (header II) at ambiguous-anchors/a.md:12:1-12 9 | Use of ambiguous anchors is discouraged because the target 10 | can change silently while the document containing it evolves. 11 | 12 | ambiguous-anchors/b.md:7:1-48: bad reference: 13 | The reference to "ambiguous anchor in other file" failed verification. 14 | Ambiguous reference to anchor 'some-text' 15 | in file a.md 16 | It could refer to either: 17 | - some-text (header I) at ambiguous-anchors/a.md:6:1-11 18 | - some-text (header I) at ambiguous-anchors/a.md:8:1-15 19 | - some-text (header II) at ambiguous-anchors/a.md:12:1-12 20 | Use of ambiguous anchors is discouraged because the target 21 | can change silently while the document containing it evolves. 22 | 23 | Invalid references dumped, 2 in total. 24 | -------------------------------------------------------------------------------- /tests/golden/check-anchors/expected2.gold: -------------------------------------------------------------------------------- 1 | non-existing-anchors/a.md:12:1-13: bad reference: 2 | The reference to "broken" failed verification. 3 | Anchor 'h3' is not present, did you mean: 4 | - h1 (header I) at non-existing-anchors/a.md:6:1-4 5 | - h2 (header II) at non-existing-anchors/a.md:8:1-5 6 | 7 | non-existing-anchors/a.md:14:1-18: bad reference: 8 | The reference to "broken" failed verification. 9 | Anchor 'heading' is not present, did you mean: 10 | - the-heading (header I) at non-existing-anchors/a.md:10:1-13 11 | 12 | non-existing-anchors/a.md:16:1-31: bad reference: 13 | The reference to "broken" failed verification. 14 | Anchor 'really-unique-anchor' is not present 15 | 16 | Invalid references dumped, 3 in total. 17 | -------------------------------------------------------------------------------- /tests/golden/check-anchors/non-existing-anchors/a.md: -------------------------------------------------------------------------------- 1 | 6 | # h1 7 | 8 | ## h2 9 | 10 | # The heading 11 | 12 | [broken](#h3) 13 | 14 | [broken](#heading) 15 | 16 | [broken](#really-unique-anchor) 17 | -------------------------------------------------------------------------------- /tests/golden/check-autolinks/check-autolinks.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | # SPDX-FileCopyrightText: 2022 Serokell 4 | # 5 | # SPDX-License-Identifier: MPL-2.0 6 | 7 | load '../helpers/bats-support/load' 8 | load '../helpers/bats-assert/load' 9 | load '../helpers/bats-file/load' 10 | load '../helpers' 11 | 12 | @test "We're finding and checking autolinks" { 13 | golden_file=$(realpath expected.gold) 14 | to_temp xrefcheck -u -v 15 | assert_diff 16 | } 17 | -------------------------------------------------------------------------------- /tests/golden/check-autolinks/expected.gold: -------------------------------------------------------------------------------- 1 | === Repository data === 2 | 3 | file-with-autolinks.md: 4 | - references: 5 | - reference (external) at file-with-autolinks.md:6:22-52: 6 | - text: "https://www.google.com/doodles" 7 | - link: https://www.google.com/doodles 8 | - reference (external) at file-with-autolinks.md:8:0-18: 9 | - text: "www.commonmark.org" 10 | - link: http://www.commonmark.org 11 | - anchors: 12 | none 13 | 14 | file-with-autolinks.md:8:0-18: bad reference: 15 | The reference to "www.commonmark.org" failed verification. 16 | Permanent redirect found: 17 | -| http://www.commonmark.org 18 | -> https://commonmark.org 19 | ^-- stopped before this one 20 | when processing an external link: 21 | http://www.commonmark.org 22 | 23 | Invalid references dumped, 1 in total. 24 | -------------------------------------------------------------------------------- /tests/golden/check-autolinks/file-with-autolinks.md: -------------------------------------------------------------------------------- 1 | 6 | So, first one is here https://www.google.com/doodles. 7 | 8 | www.commonmark.org 9 | 10 | Without www it is not a link - github.com/serokell/ 11 | -------------------------------------------------------------------------------- /tests/golden/check-backslash/a.md: -------------------------------------------------------------------------------- 1 | 6 | # Header 7 | 8 | [Reference to a\a](a\a.md#header) 9 | 10 | [Bad reference to a\b](a\b.md) 11 | -------------------------------------------------------------------------------- /tests/golden/check-backslash/check-backslash.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | # SPDX-FileCopyrightText: 2023 Serokell 4 | # 5 | # SPDX-License-Identifier: MPL-2.0 6 | 7 | load '../helpers/bats-support/load' 8 | load '../helpers/bats-assert/load' 9 | load '../helpers/bats-file/load' 10 | load '../helpers' 11 | 12 | 13 | @test "Checking files with backslash" { 14 | golden_file=$(realpath expected.gold) 15 | 16 | cp a.md $TEST_TEMP_DIR 17 | touch "$TEST_TEMP_DIR/a\a.md" || \ 18 | return 0 # Cannot be tested on Windows 19 | 20 | cat < "$TEST_TEMP_DIR/a\a.md" 21 | # Header 22 | [Reference to a](a.md) 23 | [Reference to myself](a\a.md) 24 | EOF 25 | 26 | cd $TEST_TEMP_DIR 27 | git init 28 | git add a.md 29 | git add "a\a.md" 30 | 31 | to_temp xrefcheck -u -v 32 | 33 | assert_diff 34 | } 35 | -------------------------------------------------------------------------------- /tests/golden/check-backslash/expected.gold: -------------------------------------------------------------------------------- 1 | === Repository data === 2 | 3 | a.md: 4 | - references: 5 | - reference (relative) at a.md:8:1-33: 6 | - text: "Reference to a\\a" 7 | - link: a\a.md 8 | - anchor: header 9 | - reference (relative) at a.md:10:1-30: 10 | - text: "Bad reference to a\\b" 11 | - link: a\b.md 12 | - anchor: - 13 | - anchors: 14 | - header (header I) at a.md:6:1-8 15 | 16 | a\a.md: 17 | - references: 18 | - reference (relative) at a\a.md:2:1-22: 19 | - text: "Reference to a" 20 | - link: a.md 21 | - anchor: - 22 | - reference (relative) at a\a.md:3:1-29: 23 | - text: "Reference to myself" 24 | - link: a\a.md 25 | - anchor: - 26 | - anchors: 27 | - header (header I) at a\a.md:1:1-8 28 | 29 | a.md:10:1-30: bad reference: 30 | The reference to "Bad reference to a\\b" failed verification. 31 | File does not exist: 32 | a\b.md 33 | Its reference contains a backslash. Maybe it uses the wrong path separator. 34 | 35 | Invalid references dumped, 1 in total. 36 | -------------------------------------------------------------------------------- /tests/golden/check-case-sensitivity-anchor/a.md: -------------------------------------------------------------------------------- 1 | 6 | # Some header 7 | 8 | Some text 9 | 10 | # Another header 11 | 12 | # Custom header 13 | 14 | # Custom header 15 | 16 | [Mixing case reference](#SomE-HEADer) 17 | 18 | [Mixing case reference](#SomE-HEADr) 19 | 20 | [Reference as it is](#UPPERCASE-NAME) 21 | 22 | [Reference lowered](#uppercase-name) 23 | 24 | [Maybe ambiguous reference](#another-header) 25 | -------------------------------------------------------------------------------- /tests/golden/check-case-sensitivity-anchor/check-case-sensitivity-anchor.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | # SPDX-FileCopyrightText: 2022 Serokell 4 | # 5 | # SPDX-License-Identifier: MPL-2.0 6 | 7 | load '../helpers/bats-support/load' 8 | load '../helpers/bats-assert/load' 9 | load '../helpers/bats-file/load' 10 | load '../helpers' 11 | 12 | 13 | @test "GitHub anchors: check, ambiguous and similar detection is case-insensitive" { 14 | golden_file=$(realpath expected1.gold) 15 | to_temp xrefcheck -u -c config-github.yaml 16 | assert_diff 17 | } 18 | 19 | @test "GitLab anchors: check and ambiguous detection is case-sensitive, but similar detection is not" { 20 | golden_file=$(realpath expected2.gold) 21 | to_temp xrefcheck -u -c config-gitlab.yaml 22 | assert_diff 23 | } 24 | -------------------------------------------------------------------------------- /tests/golden/check-case-sensitivity-anchor/config-github.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Serokell 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | scanners: 6 | markdown: 7 | flavor: GitHub 8 | -------------------------------------------------------------------------------- /tests/golden/check-case-sensitivity-anchor/config-gitlab.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Serokell 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | scanners: 6 | markdown: 7 | flavor: GitLab 8 | -------------------------------------------------------------------------------- /tests/golden/check-case-sensitivity-anchor/expected1.gold: -------------------------------------------------------------------------------- 1 | a.md:18:1-36: bad reference: 2 | The reference to "Mixing case reference" failed verification. 3 | Anchor 'SomE-HEADr' is not present, did you mean: 4 | - some-header (header I) at a.md:6:1-13 5 | - another-header (header I) at a.md:10:1-16 6 | - custom-header (header I) at a.md:12:1-43 7 | - Another-header (handmade) at a.md:12:3-27 8 | - custom-header (header I) at a.md:14:1-43 9 | 10 | a.md:24:1-44: bad reference: 11 | The reference to "Maybe ambiguous reference" failed verification. 12 | Ambiguous reference to anchor 'another-header' 13 | in file a.md 14 | It could refer to either: 15 | - another-header (header I) at a.md:10:1-16 16 | - Another-header (handmade) at a.md:12:3-27 17 | Use of ambiguous anchors is discouraged because the target 18 | can change silently while the document containing it evolves. 19 | 20 | Invalid references dumped, 2 in total. 21 | -------------------------------------------------------------------------------- /tests/golden/check-case-sensitivity-anchor/expected2.gold: -------------------------------------------------------------------------------- 1 | a.md:16:1-37: bad reference: 2 | The reference to "Mixing case reference" failed verification. 3 | Anchor 'SomE-HEADer' is not present, did you mean: 4 | - some-header (header I) at a.md:6:1-13 5 | - another-header (header I) at a.md:10:1-16 6 | - custom-header (header I) at a.md:12:1-43 7 | - Another-header (handmade) at a.md:12:3-27 8 | - custom-header (header I) at a.md:14:1-43 9 | 10 | a.md:18:1-36: bad reference: 11 | The reference to "Mixing case reference" failed verification. 12 | Anchor 'SomE-HEADr' is not present, did you mean: 13 | - some-header (header I) at a.md:6:1-13 14 | - another-header (header I) at a.md:10:1-16 15 | - custom-header (header I) at a.md:12:1-43 16 | - Another-header (handmade) at a.md:12:3-27 17 | - custom-header (header I) at a.md:14:1-43 18 | 19 | a.md:22:1-36: bad reference: 20 | The reference to "Reference lowered" failed verification. 21 | Anchor 'uppercase-name' is not present, did you mean: 22 | - UPPERCASE-NAME (handmade) at a.md:14:3-27 23 | 24 | Invalid references dumped, 3 in total. 25 | -------------------------------------------------------------------------------- /tests/golden/check-case-sensitivity-path/a.md: -------------------------------------------------------------------------------- 1 | 6 | # Header 7 | 8 | * [a](a.md) 9 | * [Header a](dir/b.md#header) 10 | * [b](dir/b.md) 11 | * [Header b](dir/b.md#header) 12 | * [Wrong a](A.md) 13 | * [Wrong a extension](a.Md) 14 | * [Wrong b](dir/B.md) 15 | * [Wrong b dir](/dIr/b.md) 16 | * [Wrong b extension](/dir/b.mD) 17 | -------------------------------------------------------------------------------- /tests/golden/check-case-sensitivity-path/check-case-sensitivity-path.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | # SPDX-FileCopyrightText: 2023 Serokell 4 | # 5 | # SPDX-License-Identifier: MPL-2.0 6 | 7 | load '../helpers/bats-support/load' 8 | load '../helpers/bats-assert/load' 9 | load '../helpers/bats-file/load' 10 | load '../helpers' 11 | 12 | 13 | @test "GitHub paths: case-sensitive" { 14 | golden_file=$(realpath expected.gold) 15 | to_temp xrefcheck -u -v -c config-github.yaml 16 | assert_diff 17 | } 18 | 19 | @test "GitLab paths: case-sensitive" { 20 | golden_file=$(realpath expected.gold) 21 | to_temp xrefcheck -u -v -c config-gitlab.yaml 22 | assert_diff 23 | } 24 | -------------------------------------------------------------------------------- /tests/golden/check-case-sensitivity-path/config-github.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Serokell 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | scanners: 6 | markdown: 7 | flavor: GitHub 8 | -------------------------------------------------------------------------------- /tests/golden/check-case-sensitivity-path/config-gitlab.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Serokell 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | scanners: 6 | markdown: 7 | flavor: GitLab 8 | -------------------------------------------------------------------------------- /tests/golden/check-case-sensitivity-path/dir/b.md: -------------------------------------------------------------------------------- 1 | 6 | # Header 7 | 8 | * [Right a](../a.md) 9 | * [Right b](./b.md) 10 | * [Wrong a](../A.md) 11 | * [Wrong b](./B.md) 12 | -------------------------------------------------------------------------------- /tests/golden/check-case-sensitivity-path/expected.gold: -------------------------------------------------------------------------------- 1 | === Repository data === 2 | 3 | a.md: 4 | - references: 5 | - reference (relative) at a.md:8:3-11: 6 | - text: "a" 7 | - link: a.md 8 | - anchor: - 9 | - reference (relative) at a.md:9:3-29: 10 | - text: "Header a" 11 | - link: dir/b.md 12 | - anchor: header 13 | - reference (relative) at a.md:10:3-15: 14 | - text: "b" 15 | - link: dir/b.md 16 | - anchor: - 17 | - reference (relative) at a.md:11:3-29: 18 | - text: "Header b" 19 | - link: dir/b.md 20 | - anchor: header 21 | - reference (relative) at a.md:12:3-17: 22 | - text: "Wrong a" 23 | - link: A.md 24 | - anchor: - 25 | - reference (relative) at a.md:13:3-27: 26 | - text: "Wrong a extension" 27 | - link: a.Md 28 | - anchor: - 29 | - reference (relative) at a.md:14:3-21: 30 | - text: "Wrong b" 31 | - link: dir/B.md 32 | - anchor: - 33 | - reference (absolute) at a.md:15:3-26: 34 | - text: "Wrong b dir" 35 | - link: /dIr/b.md 36 | - anchor: - 37 | - reference (absolute) at a.md:16:3-32: 38 | - text: "Wrong b extension" 39 | - link: /dir/b.mD 40 | - anchor: - 41 | - anchors: 42 | - header (header I) at a.md:6:1-8 43 | 44 | dir/b.md: 45 | - references: 46 | - reference (relative) at dir/b.md:8:3-20: 47 | - text: "Right a" 48 | - link: ../a.md 49 | - anchor: - 50 | - reference (relative) at dir/b.md:9:3-19: 51 | - text: "Right b" 52 | - link: ./b.md 53 | - anchor: - 54 | - reference (relative) at dir/b.md:10:3-20: 55 | - text: "Wrong a" 56 | - link: ../A.md 57 | - anchor: - 58 | - reference (relative) at dir/b.md:11:3-19: 59 | - text: "Wrong b" 60 | - link: ./B.md 61 | - anchor: - 62 | - anchors: 63 | - header (header I) at dir/b.md:6:1-8 64 | 65 | a.md:12:3-17: bad reference: 66 | The reference to "Wrong a" failed verification. 67 | File does not exist: 68 | A.md 69 | 70 | a.md:13:3-27: bad reference: 71 | The reference to "Wrong a extension" failed verification. 72 | File does not exist: 73 | a.Md 74 | 75 | a.md:14:3-21: bad reference: 76 | The reference to "Wrong b" failed verification. 77 | File does not exist: 78 | dir/B.md 79 | 80 | a.md:15:3-26: bad reference: 81 | The reference to "Wrong b dir" failed verification. 82 | File does not exist: 83 | dIr/b.md 84 | 85 | a.md:16:3-32: bad reference: 86 | The reference to "Wrong b extension" failed verification. 87 | File does not exist: 88 | dir/b.mD 89 | 90 | dir/b.md:10:3-20: bad reference: 91 | The reference to "Wrong a" failed verification. 92 | File does not exist: 93 | dir/../A.md 94 | 95 | dir/b.md:11:3-19: bad reference: 96 | The reference to "Wrong b" failed verification. 97 | File does not exist: 98 | dir/./B.md 99 | 100 | Invalid references dumped, 7 in total. 101 | -------------------------------------------------------------------------------- /tests/golden/check-cli/check-cli.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | # SPDX-FileCopyrightText: 2022 Serokell 4 | # 5 | # SPDX-License-Identifier: MPL-2.0 6 | 7 | load '../helpers/bats-support/load' 8 | load '../helpers/bats-assert/load' 9 | load '../helpers/bats-file/load' 10 | load '../helpers' 11 | 12 | @test "No redundant slashes" { 13 | run xrefcheck -u \ 14 | --ignore to-ignore/* \ 15 | --root . 16 | 17 | assert_output --partial "All repository links are valid." 18 | } 19 | 20 | @test "Redundant slashes in root and ignore" { 21 | run xrefcheck -u \ 22 | --ignore ./././././././//to-ignore/* \ 23 | --root ./ 24 | 25 | assert_output --partial "All repository links are valid." 26 | } 27 | 28 | @test "Redundant slashes in root" { 29 | run xrefcheck -u \ 30 | -c config-no-scan-ignored.yaml \ 31 | --root ./ 32 | 33 | assert_output --partial "All repository links are valid." 34 | } 35 | 36 | @test "Redundant slashes in ignore" { 37 | run xrefcheck -u \ 38 | --ignore ./././././././//to-ignore/* \ 39 | --root . 40 | 41 | assert_output --partial "All repository links are valid." 42 | } 43 | 44 | @test "Basic root, check errors report" { 45 | golden_file=$(realpath expected1.gold) 46 | to_temp xrefcheck -u --root . 47 | assert_diff 48 | } 49 | 50 | @test "Root with redundant slashes, check errors report" { 51 | golden_file=$(realpath expected2.gold) 52 | to_temp xrefcheck -u --root ././///././././//./ 53 | assert_diff 54 | } 55 | 56 | @test "No root, check errors report" { 57 | golden_file=$(realpath expected3.gold) 58 | to_temp xrefcheck -u 59 | assert_diff 60 | } 61 | 62 | @test "Single file as root" { 63 | run xrefcheck -u \ 64 | --root single-file.md 65 | 66 | assert_failure 67 | assert_output --partial "Repository's root does not seem to be a directory: single-file.md" 68 | } 69 | -------------------------------------------------------------------------------- /tests/golden/check-cli/config-no-scan-ignored.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Serokell 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | exclusions: 6 | ignoreRefsFrom: [ "to-ignore/broken-link.md" ] 7 | 8 | scanners: 9 | markdown: 10 | flavor: GitHub 11 | -------------------------------------------------------------------------------- /tests/golden/check-cli/expected1.gold: -------------------------------------------------------------------------------- 1 | to-ignore/broken-link.md:7:1-25: bad reference: 2 | The reference to "my link" failed verification. 3 | File does not exist: 4 | one/two/three 5 | 6 | Invalid references dumped, 1 in total. 7 | -------------------------------------------------------------------------------- /tests/golden/check-cli/expected2.gold: -------------------------------------------------------------------------------- 1 | to-ignore/broken-link.md:7:1-25: bad reference: 2 | The reference to "my link" failed verification. 3 | File does not exist: 4 | one/two/three 5 | 6 | Invalid references dumped, 1 in total. 7 | -------------------------------------------------------------------------------- /tests/golden/check-cli/expected3.gold: -------------------------------------------------------------------------------- 1 | to-ignore/broken-link.md:7:1-25: bad reference: 2 | The reference to "my link" failed verification. 3 | File does not exist: 4 | one/two/three 5 | 6 | Invalid references dumped, 1 in total. 7 | -------------------------------------------------------------------------------- /tests/golden/check-cli/single-file.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serokell/xrefcheck/03dda2ec2c20b359cb68ab15f034afcc7395f92d/tests/golden/check-cli/single-file.md -------------------------------------------------------------------------------- /tests/golden/check-cli/to-ignore/broken-link.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | [my link](/one/two/three) 8 | -------------------------------------------------------------------------------- /tests/golden/check-color/check-color.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | # SPDX-FileCopyrightText: 2023 Serokell 4 | # 5 | # SPDX-License-Identifier: MPL-2.0 6 | 7 | load '../helpers/bats-support/load' 8 | load '../helpers/bats-assert/load' 9 | load '../helpers/bats-file/load' 10 | load '../helpers' 11 | 12 | # The CI variable must be always explicitly set in these tests because they are checking for an 13 | # intended behavior regardless of where they are actually being run (e.g. "No color flag (not 14 | # in CI)") may be running in CI). 15 | 16 | @test "Color flag (not in CI)" { 17 | golden_file=$(realpath expected-color.gold) 18 | output_file="$TEST_TEMP_DIR/temp_file.test" 19 | CI=false xrefcheck -u -v --no-progress --color > $output_file 20 | assert_diff 21 | } 22 | 23 | @test "No color flag (not in CI)" { 24 | golden_file=$(realpath expected-no-color.gold) 25 | output_file="$TEST_TEMP_DIR/temp_file.test" 26 | CI=false xrefcheck -u -v --no-progress --no-color > $output_file 27 | assert_diff 28 | } 29 | 30 | @test "No color default when pipe (not in CI)" { 31 | golden_file=$(realpath expected-no-color.gold) 32 | output_file="$TEST_TEMP_DIR/temp_file.test" 33 | CI=false xrefcheck -u -v --no-progress > $output_file 34 | assert_diff 35 | } 36 | 37 | @test "Color default when CI" { 38 | golden_file=$(realpath expected-color.gold) 39 | output_file="$TEST_TEMP_DIR/temp_file.test" 40 | CI=true xrefcheck -u -v --no-progress > $output_file 41 | assert_diff 42 | } 43 | 44 | @test "No color flag in CI" { 45 | golden_file=$(realpath expected-no-color.gold) 46 | output_file="$TEST_TEMP_DIR/temp_file.test" 47 | CI=true xrefcheck -u -v --no-progress --no-color > $output_file 48 | assert_diff 49 | } 50 | -------------------------------------------------------------------------------- /tests/golden/check-color/color.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # Color 8 | 9 | [Color](#Color) 10 | -------------------------------------------------------------------------------- /tests/golden/check-color/expected-color.gold: -------------------------------------------------------------------------------- 1 | === Repository data === 2 | 3 | color.md: 4 | - references: 5 | - reference (file-local) at color.md:9:1-15: 6 | - text: "Color" 7 | - anchor: Color 8 | - anchors: 9 | - color (header I) at color.md:7:1-7 10 | 11 | All repository links are valid. 12 | -------------------------------------------------------------------------------- /tests/golden/check-color/expected-no-color.gold: -------------------------------------------------------------------------------- 1 | === Repository data === 2 | 3 | color.md: 4 | - references: 5 | - reference (file-local) at color.md:9:1-15: 6 | - text: "Color" 7 | - anchor: Color 8 | - anchors: 9 | - color (header I) at color.md:7:1-7 10 | 11 | All repository links are valid. 12 | -------------------------------------------------------------------------------- /tests/golden/check-dump-config/.config.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Serokell 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /tests/golden/check-dump-config/.xrefcheck.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Serokell 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | -------------------------------------------------------------------------------- /tests/golden/check-dump-config/check-dump-config.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | # SPDX-FileCopyrightText: 2022 Serokell 4 | # 5 | # SPDX-License-Identifier: MPL-2.0 6 | 7 | load '../helpers/bats-support/load' 8 | load '../helpers/bats-assert/load' 9 | load '../helpers/bats-file/load' 10 | load '../helpers' 11 | 12 | 13 | @test "Dump config to stdout" { 14 | golden_file=$(realpath ../../configs/github-config.yaml) 15 | to_temp xrefcheck dump-config --stdout -t GitHub 16 | assert_diff 17 | } 18 | 19 | @test "Dump config to existent default file error" { 20 | run xrefcheck dump-config -t GitHub 21 | 22 | assert_failure 23 | 24 | assert_output "Output file exists. Use --force to overwrite." 25 | } 26 | 27 | @test "Dump config to existent file error" { 28 | run xrefcheck dump-config -o .config.yaml -t GitHub 29 | 30 | assert_failure 31 | 32 | assert_output "Output file exists. Use --force to overwrite." 33 | } 34 | 35 | @test "Dump config to non existent default file" { 36 | cd $TEST_TEMP_DIR 37 | 38 | run xrefcheck dump-config -t GitHub 39 | 40 | assert_success 41 | 42 | assert_exists .xrefcheck.yaml 43 | } 44 | 45 | @test "Dump config to non existent file" { 46 | cd $TEST_TEMP_DIR 47 | 48 | run xrefcheck dump-config -o .config.yaml -t GitHub 49 | 50 | assert_success 51 | 52 | assert_exists .config.yaml 53 | } 54 | 55 | @test "Dump config to existent default file with force" { 56 | cp .xrefcheck.yaml $TEST_TEMP_DIR 57 | cd $TEST_TEMP_DIR 58 | 59 | run xrefcheck dump-config -t GitHub --force 60 | 61 | assert_success 62 | 63 | assert_exists .xrefcheck.yaml 64 | } 65 | 66 | @test "Dump config to existent file with force" { 67 | cp .config.yaml $TEST_TEMP_DIR 68 | cd $TEST_TEMP_DIR 69 | 70 | run xrefcheck dump-config -o .config.yaml -t GitHub --force 71 | 72 | assert_success 73 | 74 | assert_exists .config.yaml 75 | } 76 | -------------------------------------------------------------------------------- /tests/golden/check-footnotes/broken-link-in-footnote/file-with-footnote-with-broken-link.md: -------------------------------------------------------------------------------- 1 | 6 | foo [^bad] 7 | 8 | [^bad]: [bad link in footnote](./notExists) 9 | -------------------------------------------------------------------------------- /tests/golden/check-footnotes/check-footnotes.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | # SPDX-FileCopyrightText: 2022 Serokell 4 | # 5 | # SPDX-License-Identifier: MPL-2.0 6 | 7 | load '../helpers/bats-support/load' 8 | load '../helpers/bats-assert/load' 9 | load '../helpers/bats-file/load' 10 | load '../helpers' 11 | 12 | 13 | @test "We report broken links inside footnotes" { 14 | golden_file=$(realpath expected.gold) 15 | to_temp xrefcheck -u -r broken-link-in-footnote 16 | assert_diff 17 | } 18 | 19 | @test "We're not treating footnotes as 'shortcut reference links'" { 20 | # See: https://github.com/serokell/xrefcheck/issues/155 21 | run xrefcheck -u -r one-word-footnote 22 | 23 | assert_output --partial "All repository links are valid." 24 | } 25 | -------------------------------------------------------------------------------- /tests/golden/check-footnotes/expected.gold: -------------------------------------------------------------------------------- 1 | broken-link-in-footnote/file-with-footnote-with-broken-link.md:8:9-43: bad reference: 2 | The reference to "bad link in footnote" failed verification. 3 | File does not exist: 4 | ./notExists 5 | 6 | Invalid references dumped, 1 in total. 7 | -------------------------------------------------------------------------------- /tests/golden/check-footnotes/one-word-footnote/file-with-one-word-footnote.md: -------------------------------------------------------------------------------- 1 | 6 | foo [^good] 7 | 8 | [^good]: hey_I_am_footnote_not_a_link 9 | -------------------------------------------------------------------------------- /tests/golden/check-git/check-git.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | # SPDX-FileCopyrightText: 2022 Serokell 4 | # 5 | # SPDX-License-Identifier: MPL-2.0 6 | 7 | load '../helpers/bats-support/load' 8 | load '../helpers/bats-assert/load' 9 | load '../helpers/bats-file/load' 10 | load '../helpers' 11 | 12 | @test "Git: not a repo" { 13 | cd $TEST_TEMP_DIR 14 | 15 | export LANG=en_US 16 | run xrefcheck -u 17 | 18 | assert_output --partial "fatal: not a git repository" 19 | } 20 | 21 | @test "Git: bad file not tracked" { 22 | cd $TEST_TEMP_DIR 23 | 24 | git init 25 | 26 | echo "[a](/a.md)" >> "git.md" 27 | 28 | run xrefcheck -u 29 | 30 | assert_success 31 | 32 | assert_output --partial "All repository links are valid." 33 | 34 | # this is printed to stderr 35 | assert_output --partial "Those files are not added by Git, so we're not scanning them:" 36 | assert_output --partial "- git.md" 37 | assert_output --partial "Please run \"git add\" before running xrefcheck or enable --include-untracked CLI option to check these files." 38 | } 39 | 40 | @test "Git: bad file not tracked, --include-untracked enabled, check failure" { 41 | golden_file=$(realpath expected1.gold) 42 | 43 | cd $TEST_TEMP_DIR 44 | 45 | git init 46 | 47 | echo "[a](./a.md)" >> "git.md" 48 | 49 | to_temp xrefcheck -u --include-untracked 50 | 51 | assert_diff 52 | } 53 | 54 | @test "Git: bad file tracked, check failure" { 55 | golden_file=$(realpath expected2.gold) 56 | 57 | cd $TEST_TEMP_DIR 58 | 59 | git init 60 | 61 | echo "[a](./a.md)" >> "git.md" 62 | 63 | git add git.md 64 | 65 | to_temp xrefcheck -u 66 | 67 | assert_diff 68 | } 69 | 70 | 71 | @test "Git: link to untracked file, check failure" { 72 | golden_file=$(realpath expected3.gold) 73 | 74 | cd $TEST_TEMP_DIR 75 | 76 | git init 77 | 78 | echo "[a](./a.md)" >> "git.md" 79 | 80 | touch ./a.md 81 | 82 | git add git.md 83 | 84 | to_temp xrefcheck -u 85 | 86 | assert_diff 87 | } 88 | 89 | @test "Git: link to untracked file, --include-untracked enabled" { 90 | cd $TEST_TEMP_DIR 91 | 92 | git init 93 | 94 | echo "[a](./a.md)" >> "git.md" 95 | 96 | touch ./a.md 97 | 98 | git add git.md 99 | 100 | run xrefcheck -u --include-untracked 101 | 102 | assert_success 103 | 104 | assert_output --partial "All repository links are valid." 105 | } 106 | -------------------------------------------------------------------------------- /tests/golden/check-git/expected1.gold: -------------------------------------------------------------------------------- 1 | git.md:1:1-11: bad reference: 2 | The reference to "a" failed verification. 3 | File does not exist: 4 | ./a.md 5 | 6 | Invalid references dumped, 1 in total. 7 | -------------------------------------------------------------------------------- /tests/golden/check-git/expected2.gold: -------------------------------------------------------------------------------- 1 | git.md:1:1-11: bad reference: 2 | The reference to "a" failed verification. 3 | File does not exist: 4 | ./a.md 5 | 6 | Invalid references dumped, 1 in total. 7 | -------------------------------------------------------------------------------- /tests/golden/check-git/expected3.gold: -------------------------------------------------------------------------------- 1 | git.md:1:1-11: bad reference: 2 | The reference to "a" failed verification. 3 | Link (relative) targets a file not tracked by Git: 4 | ./a.md 5 | Please run "git add" before running xrefcheck or enable --include-untracked CLI option. 6 | 7 | Invalid references dumped, 1 in total. 8 | -------------------------------------------------------------------------------- /tests/golden/check-html/check-html.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | ## Title1 8 | 9 | [One](#one) 10 | [Two](#two) 11 | [Three](#three) 12 | [Four](#four) 13 | [Five](#five) 14 | -------------------------------------------------------------------------------- /tests/golden/check-html/check.html.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | # SPDX-FileCopyrightText: 2022 Serokell 4 | # 5 | # SPDX-License-Identifier: MPL-2.0 6 | 7 | load '../helpers/bats-support/load' 8 | load '../helpers/bats-assert/load' 9 | load '../helpers/bats-file/load' 10 | load '../helpers' 11 | 12 | 13 | @test "All HTML anchors should be valid" { 14 | run xrefcheck -u 15 | 16 | assert_output --partial "All repository links are valid." 17 | } 18 | -------------------------------------------------------------------------------- /tests/golden/check-ignore/check-ignore.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | # SPDX-FileCopyrightText: 2022 Serokell 4 | # 5 | # SPDX-License-Identifier: MPL-2.0 6 | 7 | load '../helpers/bats-support/load' 8 | load '../helpers/bats-assert/load' 9 | load '../helpers/bats-file/load' 10 | load '../helpers' 11 | 12 | 13 | @test "Ignore file with broken xrefcheck annotation: full path" { 14 | run xrefcheck -u --ignore ./to-ignore/inner-directory/broken_annotation.md 15 | 16 | assert_output --partial "All repository links are valid." 17 | } 18 | 19 | @test "Ignore file with broken xrefcheck annotation: glob wildcard" { 20 | run xrefcheck -u --ignore 'to-ignore/inner-directory/*' 21 | 22 | assert_output --partial "All repository links are valid." 23 | } 24 | 25 | @test "Ignore file with broken xrefcheck annotation: nested directories with glob wildcard" { 26 | run xrefcheck -u --ignore './**/*' 27 | 28 | assert_output --partial "All repository links are valid." 29 | } 30 | 31 | @test "Ignore file with broken xrefcheck annotation: config file" { 32 | run xrefcheck -u --config ./config-ignore.yaml 33 | 34 | assert_output --partial "All repository links are valid." 35 | } 36 | 37 | @test "Ignore file with broken xrefcheck annotation: directory, check failure" { 38 | golden_file=$(realpath expected1.gold) 39 | to_temp xrefcheck -u --ignore ./to-ignore/inner-directory/ 40 | assert_diff 41 | } 42 | 43 | @test "Ignore referenced file, check error" { 44 | golden_file=$(realpath expected2.gold) 45 | 46 | to_temp xrefcheck -u --ignore referenced-file.md 47 | 48 | assert_diff 49 | } 50 | 51 | @test "Config: Absolute fiepath in \"ignore\" error" { 52 | run xrefcheck -u --config ./config-ignore-bad-path-absolute.yaml 53 | 54 | assert_failure 55 | assert_output --partial "Expected a relative glob pattern, but got /to-ignore/inner-directory/broken_annotation.md" 56 | } 57 | 58 | @test "Config: Malformed glob in \"ignore\" error" { 59 | run xrefcheck -u --config ./config-ignore-malformed-glob.yaml 60 | 61 | assert_failure 62 | assert_output --partial "Glob pattern compilation failed." 63 | } 64 | 65 | @test "CLI: Absolute filepath in \"ignore\" yields to error" { 66 | run xrefcheck -u \ 67 | --ignore "/to-ignore/*" 68 | assert_failure 69 | assert_output --partial "option --ignore: Expected a relative glob pattern, but got /to-ignore/*" 70 | } 71 | 72 | @test "CLI: Malformed glob in arg \"ignore\" yields to error" { 73 | run xrefcheck -u \ 74 | --ignore "" 75 | assert_failure 76 | assert_output --partial "option --ignore: Glob pattern compilation failed." 77 | assert_output --partial "compile :: bad <>, expected number followed by - in to-ignore" 78 | } 79 | -------------------------------------------------------------------------------- /tests/golden/check-ignore/check-ignore.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | [Good reference](/referenced-file.md) 8 | -------------------------------------------------------------------------------- /tests/golden/check-ignore/config-ignore-bad-path-absolute.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Serokell 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | exclusions: 6 | ignore: 7 | - /to-ignore/inner-directory/broken_annotation.md 8 | 9 | scanners: 10 | markdown: 11 | flavor: GitHub 12 | -------------------------------------------------------------------------------- /tests/golden/check-ignore/config-ignore-malformed-glob.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Serokell 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | exclusions: 6 | ignore: 7 | - 8 | 9 | scanners: 10 | markdown: 11 | flavor: GitHub 12 | -------------------------------------------------------------------------------- /tests/golden/check-ignore/config-ignore.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Serokell 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | exclusions: 6 | ignore: 7 | - ./to-ignore/inner-directory/broken_annotation.md 8 | 9 | scanners: 10 | markdown: 11 | flavor: GitHub 12 | -------------------------------------------------------------------------------- /tests/golden/check-ignore/expected1.gold: -------------------------------------------------------------------------------- 1 | to-ignore/inner-directory/broken_annotation.md:9:1-29: scan error: 2 | Annotation "ignore all" must be at the top of markdown or right after comments at the top 3 | 4 | Scan errors dumped, 1 in total. 5 | -------------------------------------------------------------------------------- /tests/golden/check-ignore/expected2.gold: -------------------------------------------------------------------------------- 1 | to-ignore/inner-directory/broken_annotation.md:9:1-29: scan error: 2 | Annotation "ignore all" must be at the top of markdown or right after comments at the top 3 | 4 | Scan errors dumped, 1 in total. 5 | 6 | check-ignore.md:7:1-37: bad reference: 7 | The reference to "Good reference" failed verification. 8 | File does not exist: 9 | referenced-file.md 10 | 11 | Invalid references dumped, 1 in total. 12 | -------------------------------------------------------------------------------- /tests/golden/check-ignore/referenced-file.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | referenced file 8 | -------------------------------------------------------------------------------- /tests/golden/check-ignore/to-ignore/inner-directory/broken_annotation.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | One 8 | 9 | 10 | 11 | Two 12 | -------------------------------------------------------------------------------- /tests/golden/check-ignoreExternalRefsTo/check-ignoreExternalRefsTo.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | # SPDX-FileCopyrightText: 2021 Serokell 4 | # 5 | # SPDX-License-Identifier: MPL-2.0 6 | 7 | load '../helpers/bats-support/load' 8 | load '../helpers/bats-assert/load' 9 | load '../helpers/bats-file/load' 10 | load '../helpers' 11 | 12 | @test "Ignore localhost" { 13 | run xrefcheck -u \ 14 | -c config-check-disabled.yaml \ 15 | -r . 16 | 17 | assert_output --partial "All repository links are valid." 18 | } 19 | 20 | @test "Ignore localhost, check errors" { 21 | golden_file=$(realpath expected.gold) 22 | 23 | to_temp xrefcheck -u \ 24 | -c config-check-enabled.yaml \ 25 | -r . 26 | 27 | assert_diff 28 | } 29 | 30 | @test "Ignore localhost, no config specified" { 31 | run xrefcheck -u 32 | 33 | assert_output --partial "All repository links are valid." 34 | } 35 | -------------------------------------------------------------------------------- /tests/golden/check-ignoreExternalRefsTo/check-ignoreExternalRefsTo.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | Serokell [web-site](https://localhost:20000/web-site) 8 | 9 | Serokell [team](https://127.0.0.1:20000/team) 10 | 11 | Serokell [blog](http://localhost:20000/blog) 12 | 13 | Serokell [labs](http://127.0.0.1:20000/labs) 14 | -------------------------------------------------------------------------------- /tests/golden/check-ignoreExternalRefsTo/config-check-disabled.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Serokell 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | exclusions: 6 | ignoreExternalRefsTo: 7 | - (https?|ftps?)://(localhost|127\.0\.0\.1).* 8 | 9 | scanners: 10 | markdown: 11 | flavor: GitHub 12 | -------------------------------------------------------------------------------- /tests/golden/check-ignoreExternalRefsTo/config-check-enabled.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Serokell 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | exclusions: 6 | ignoreExternalRefsTo: [] 7 | 8 | scanners: 9 | markdown: 10 | flavor: GitHub 11 | -------------------------------------------------------------------------------- /tests/golden/check-ignoreExternalRefsTo/expected.gold: -------------------------------------------------------------------------------- 1 | check-ignoreExternalRefsTo.md:7:10-53: bad reference: 2 | The reference to "web-site" failed verification. 3 | Connection failure 4 | when processing an external link: 5 | https://localhost:20000/web-site 6 | 7 | check-ignoreExternalRefsTo.md:9:10-45: bad reference: 8 | The reference to "team" failed verification. 9 | Connection failure 10 | when processing an external link: 11 | https://127.0.0.1:20000/team 12 | 13 | check-ignoreExternalRefsTo.md:11:10-44: bad reference: 14 | The reference to "blog" failed verification. 15 | Connection failure 16 | when processing an external link: 17 | http://localhost:20000/blog 18 | 19 | check-ignoreExternalRefsTo.md:13:10-44: bad reference: 20 | The reference to "labs" failed verification. 21 | Connection failure 22 | when processing an external link: 23 | http://127.0.0.1:20000/labs 24 | 25 | Invalid references dumped, 4 in total. 26 | -------------------------------------------------------------------------------- /tests/golden/check-ignoreLocalRefsTo/check-ignoreLocalRefsTo.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | # SPDX-FileCopyrightText: 2022 Serokell 4 | # 5 | # SPDX-License-Identifier: MPL-2.0 6 | 7 | load '../helpers/bats-support/load' 8 | load '../helpers/bats-assert/load' 9 | load '../helpers/bats-file/load' 10 | load '../helpers' 11 | 12 | 13 | @test "IgnoreLocalRefsTo: all references should be valid" { 14 | run xrefcheck -u -c ./config-ignoreLocalRefsTo.yaml 15 | 16 | assert_output --partial "All repository links are valid." 17 | } 18 | 19 | @test "IgnoreLocalRefsTo: check failure" { 20 | golden_file=$(realpath expected.gold) 21 | to_temp xrefcheck -u 22 | assert_diff 23 | } 24 | -------------------------------------------------------------------------------- /tests/golden/check-ignoreLocalRefsTo/check-ignoreLocalRefsTo.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | [full path](/one/a.md) 8 | 9 | [glob wildcard](/two/b.md) 10 | 11 | [recursive directory glob](/three/c.md) 12 | 13 | [recursive nested directory glob](/three/four/d.md) 14 | 15 | [another recursive nested directory glob](/three/five/e.md) 16 | -------------------------------------------------------------------------------- /tests/golden/check-ignoreLocalRefsTo/config-ignoreLocalRefsTo.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Serokell 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | exclusions: 6 | ignoreLocalRefsTo: 7 | - ./one/a.md 8 | - ./two/* 9 | - ./three/**/* 10 | 11 | scanners: 12 | markdown: 13 | flavor: GitHub 14 | -------------------------------------------------------------------------------- /tests/golden/check-ignoreLocalRefsTo/expected.gold: -------------------------------------------------------------------------------- 1 | check-ignoreLocalRefsTo.md:7:1-22: bad reference: 2 | The reference to "full path" failed verification. 3 | File does not exist: 4 | one/a.md 5 | 6 | check-ignoreLocalRefsTo.md:9:1-26: bad reference: 7 | The reference to "glob wildcard" failed verification. 8 | File does not exist: 9 | two/b.md 10 | 11 | check-ignoreLocalRefsTo.md:11:1-39: bad reference: 12 | The reference to "recursive directory glob" failed verification. 13 | File does not exist: 14 | three/c.md 15 | 16 | check-ignoreLocalRefsTo.md:13:1-51: bad reference: 17 | The reference to "recursive nested directory glob" failed verification. 18 | File does not exist: 19 | three/four/d.md 20 | 21 | check-ignoreLocalRefsTo.md:15:1-59: bad reference: 22 | The reference to "another recursive nested directory glob" failed verification. 23 | File does not exist: 24 | three/five/e.md 25 | 26 | one/file.md:7:1-58: bad reference: 27 | The reference to "check ignoreLocalRefsTo are relative to the root" failed verification. 28 | File does not exist: 29 | one/./a.md 30 | 31 | one/file.md:9:1-23: bad reference: 32 | The reference to "one more" failed verification. 33 | File does not exist: 34 | one/../two/b.md 35 | 36 | Invalid references dumped, 7 in total. 37 | -------------------------------------------------------------------------------- /tests/golden/check-ignoreLocalRefsTo/one/file.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | [check ignoreLocalRefsTo are relative to the root](./a.md) 8 | 9 | [one more](../two/b.md) 10 | -------------------------------------------------------------------------------- /tests/golden/check-ignoreRefsFrom/check-ignoreRefsFrom.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | # SPDX-FileCopyrightText: 2022 Serokell 4 | # 5 | # SPDX-License-Identifier: MPL-2.0 6 | 7 | load '../helpers/bats-support/load' 8 | load '../helpers/bats-assert/load' 9 | load '../helpers/bats-file/load' 10 | load '../helpers' 11 | 12 | 13 | @test "ignoreRefsFrom: full path" { 14 | run xrefcheck -u -c config-full-path.yaml 15 | 16 | assert_output --partial "All repository links are valid." 17 | } 18 | 19 | @test "ignoreRefsFrom: glob wildcard" { 20 | run xrefcheck -u -c config-wildcard.yaml 21 | 22 | assert_output --partial "All repository links are valid." 23 | } 24 | 25 | @test "ignoreRefsFrom: nested directories with glob wildcard" { 26 | run xrefcheck -u -c config-nested-directories.yaml 27 | 28 | assert_output --partial "All repository links are valid." 29 | } 30 | 31 | @test "ignoreRefsFrom: directory, check failure" { 32 | golden_file=$(realpath expected.gold) 33 | to_temp xrefcheck -u -c config-directory.yaml 34 | assert_diff 35 | } 36 | -------------------------------------------------------------------------------- /tests/golden/check-ignoreRefsFrom/config-directory.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Serokell 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | exclusions: 6 | ignoreRefsFrom: 7 | - ignoreRefsFrom/inner-directory 8 | 9 | scanners: 10 | markdown: 11 | flavor: GitHub 12 | -------------------------------------------------------------------------------- /tests/golden/check-ignoreRefsFrom/config-full-path.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Serokell 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | exclusions: 6 | ignoreRefsFrom: 7 | - ./ignoreRefsFrom/inner-directory/bad-reference.md 8 | 9 | scanners: 10 | markdown: 11 | flavor: GitHub 12 | -------------------------------------------------------------------------------- /tests/golden/check-ignoreRefsFrom/config-nested-directories.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Serokell 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | exclusions: 6 | ignoreRefsFrom: 7 | - ./**/* 8 | 9 | scanners: 10 | markdown: 11 | flavor: GitHub 12 | -------------------------------------------------------------------------------- /tests/golden/check-ignoreRefsFrom/config-wildcard.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Serokell 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | exclusions: 6 | ignoreRefsFrom: 7 | - ./ignoreRefsFrom/inner-directory/* 8 | 9 | scanners: 10 | markdown: 11 | flavor: GitHub 12 | -------------------------------------------------------------------------------- /tests/golden/check-ignoreRefsFrom/expected.gold: -------------------------------------------------------------------------------- 1 | ignoreRefsFrom/inner-directory/bad-reference.md:7:1-28: bad reference: 2 | The reference to "Bad reference" failed verification. 3 | File does not exist: 4 | no-file.md 5 | 6 | Invalid references dumped, 1 in total. 7 | -------------------------------------------------------------------------------- /tests/golden/check-ignoreRefsFrom/ignoreRefsFrom/inner-directory/bad-reference.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | [Bad reference](/no-file.md) 8 | -------------------------------------------------------------------------------- /tests/golden/check-images/check-images.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | # SPDX-FileCopyrightText: 2022 Serokell 4 | # 5 | # SPDX-License-Identifier: MPL-2.0 6 | 7 | load '../helpers/bats-support/load' 8 | load '../helpers/bats-assert/load' 9 | load '../helpers/bats-file/load' 10 | load '../helpers' 11 | 12 | @test "Check images" { 13 | golden_file=$(realpath expected.gold) 14 | to_temp xrefcheck -u -v 15 | assert_diff 16 | } 17 | -------------------------------------------------------------------------------- /tests/golden/check-images/check-images.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | ![good image ref 1](https://avatars.githubusercontent.com/u/13840520 "text") 8 | 9 | ![good image ref 2][img-ref-good-2] 10 | 11 | [img-ref-good-2]: https://avatars.githubusercontent.com/u/13840520 "text" 12 | 13 | 14 | ![bad image ref 1](https://serokell.io/1.png "text") 15 | 16 | ![bad image ref 2][img-ref-bad-2] 17 | 18 | [img-ref-bad-2]: https://serokell.io/2.png "text" 19 | 20 | ![bad image ref 3](./3.png "text") 21 | ![bad image ref 4][img-ref-bad-4] 22 | 23 | [img-ref-bad-4]: ./4.png "text" 24 | 25 | 26 | ![bad image ref ignored](./3.png "text") 27 | -------------------------------------------------------------------------------- /tests/golden/check-images/expected.gold: -------------------------------------------------------------------------------- 1 | === Repository data === 2 | 3 | check-images.md: 4 | - references: 5 | - reference (external) at check-images.md:7:1-76: 6 | - text: "good image ref 1" 7 | - link: https://avatars.githubusercontent.com/u/13840520 8 | - reference (external) at check-images.md:9:1-35: 9 | - text: "good image ref 2" 10 | - link: https://avatars.githubusercontent.com/u/13840520 11 | - reference (external) at check-images.md:14:1-52: 12 | - text: "bad image ref 1" 13 | - link: https://serokell.io/1.png 14 | - reference (external) at check-images.md:16:1-33: 15 | - text: "bad image ref 2" 16 | - link: https://serokell.io/2.png 17 | - reference (relative) at check-images.md:20:1-34: 18 | - text: "bad image ref 3" 19 | - link: ./3.png 20 | - anchor: - 21 | - reference (relative) at check-images.md:21:1-33: 22 | - text: "bad image ref 4" 23 | - link: ./4.png 24 | - anchor: - 25 | - anchors: 26 | none 27 | 28 | check-images.md:14:1-52: bad reference: 29 | The reference to "bad image ref 1" failed verification. 30 | Resource unavailable (404 Not Found) 31 | when processing an external link: 32 | https://serokell.io/1.png 33 | 34 | check-images.md:16:1-33: bad reference: 35 | The reference to "bad image ref 2" failed verification. 36 | Resource unavailable (404 Not Found) 37 | when processing an external link: 38 | https://serokell.io/2.png 39 | 40 | check-images.md:20:1-34: bad reference: 41 | The reference to "bad image ref 3" failed verification. 42 | File does not exist: 43 | ./3.png 44 | 45 | check-images.md:21:1-33: bad reference: 46 | The reference to "bad image ref 4" failed verification. 47 | File does not exist: 48 | ./4.png 49 | 50 | Invalid references dumped, 4 in total. 51 | -------------------------------------------------------------------------------- /tests/golden/check-local-refs/check-local-refs.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | # SPDX-FileCopyrightText: 2022 Serokell 4 | # 5 | # SPDX-License-Identifier: MPL-2.0 6 | 7 | load '../helpers/bats-support/load' 8 | load '../helpers/bats-assert/load' 9 | load '../helpers/bats-file/load' 10 | load '../helpers' 11 | 12 | 13 | @test "Checking local references, root = \".\"" { 14 | golden_file=$(realpath expected1.gold) 15 | to_temp xrefcheck -u 16 | assert_diff 17 | } 18 | 19 | @test "Checking local references, root = \"dir1\"" { 20 | golden_file=$(realpath expected2.gold) 21 | to_temp xrefcheck -u -r dir1 22 | assert_diff 23 | } 24 | 25 | @test "Checking behavior when there are virtual files, root = \"dir1\"" { 26 | golden_file=$(realpath expected3.gold) 27 | to_temp xrefcheck -u -r dir1 -c config-with-virtual-files.yaml 28 | assert_diff 29 | } 30 | -------------------------------------------------------------------------------- /tests/golden/check-local-refs/config-with-virtual-files.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Serokell 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | exclusions: 6 | ignoreLocalRefsTo: 7 | - ../d0f1.md 8 | - ../../a.md 9 | - b/../../* 10 | - DIR2/ 11 | 12 | scanners: 13 | markdown: 14 | flavor: GitHub 15 | -------------------------------------------------------------------------------- /tests/golden/check-local-refs/d0f1.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | We are testing links in [this](dir1/dir2/d2f1.md) file 8 | -------------------------------------------------------------------------------- /tests/golden/check-local-refs/dir1/d1f1.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # Existing anchor d1f1 8 | -------------------------------------------------------------------------------- /tests/golden/check-local-refs/dir1/dir2/d2f1.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # File-local links 8 | [existing-cf-ref](#file-local-links) 9 | [bad-cf-ref](#bad) 10 | 11 | # Relative links 12 | [existing-file-rel-1](d2f2.md) 13 | [existing-file-rel-2](./d2f2.md) 14 | [existing-file-rel-3](../dir2/.././d1f1.md) 15 | [existing-file-rel-4](d2f3.yaml) 16 | 17 | [slash-file-rel](d2f2.md/) 18 | 19 | [existing-dir-rel-1](..) 20 | [existing-dir-rel-2](../dir2) 21 | [existing-dir-rel-3](../dir2/) 22 | 23 | [existing-anchor-rel-1](d2f2.md#existing-anchor-d2f2) 24 | [existing-anchor-rel-2](./d2f2.md#existing-anchor-d2f2) 25 | [existing-anchor-rel-3](../dir2/../d1f1.md#existing-anchor-d1f1) 26 | 27 | [bad-file-rel](../a/b/c/unexisting-file.md) 28 | [bad-casing-file-rel](D2F2.md/) 29 | [bad-casing-folder-rel](../DIR2) 30 | 31 | [bad-anchor-rel-1](d2f2.md#bad-anchor) 32 | [bad-anchor-rel-2](unexisting-file.md#bad-anchor) 33 | 34 | # Absolute links 35 | Should be correct when root is `/tests/golden/check-local-refs`: 36 | [file-abs-1](/dir1/./d1f1.md) 37 | [folder-abs-1](/dir1) 38 | [folder-abs-2](/dir1/dir2/../) 39 | [anchor-abs-1](/dir1/../dir1/d1f1.md#existing-anchor-d1f1) 40 | [anchor-abs-2](/dir1/dir2/../../dir1/./dir2/d2f2.md#existing-anchor-d2f2) 41 | Should be correct when root is `/tests/golden/check-local-refs/dir1`: 42 | [file-abs-2](/d1f1.md) 43 | [file-abs-3](/dir2/d2f2.md) 44 | [file-abs-4](/./dir2/../d1f1.md) 45 | [file-abs-slash](/./dir2/../d1f1.md/) 46 | [anchor-abs-3](/./dir2/../d1f1.md#existing-anchor-d1f1) 47 | 48 | # Test references outside repo 49 | 50 | This should be reported as "reference to file outside repo" when root is `dir1` 51 | [path-through-top-dir](../../dir1/d1f1.md) 52 | [path-through-top-dir-with-anchor](../../dir1/d1f1.md#existing-anchor-d1f1) 53 | [ref-to-d0](../../d0f1.md) 54 | 55 | Such absoulute paths should be reported, 56 | unless we specify in config that we ignore references to them 57 | [A](/../../a.md) 58 | [B](/b/../../b.md) 59 | -------------------------------------------------------------------------------- /tests/golden/check-local-refs/dir1/dir2/d2f2.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # Existing anchor d2f2 8 | -------------------------------------------------------------------------------- /tests/golden/check-local-refs/dir1/dir2/d2f3.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Serokell 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | p: q 5 | -------------------------------------------------------------------------------- /tests/golden/check-local-refs/expected1.gold: -------------------------------------------------------------------------------- 1 | dir1/dir2/d2f1.md:9:1-18: bad reference: 2 | The reference to "bad-cf-ref" failed verification. 3 | Anchor 'bad' is not present 4 | 5 | dir1/dir2/d2f1.md:27:1-43: bad reference: 6 | The reference to "bad-file-rel" failed verification. 7 | File does not exist: 8 | dir1/dir2/../a/b/c/unexisting-file.md 9 | 10 | dir1/dir2/d2f1.md:28:1-31: bad reference: 11 | The reference to "bad-casing-file-rel" failed verification. 12 | File does not exist: 13 | dir1/dir2/D2F2.md/ 14 | 15 | dir1/dir2/d2f1.md:29:1-32: bad reference: 16 | The reference to "bad-casing-folder-rel" failed verification. 17 | File does not exist: 18 | dir1/dir2/../DIR2 19 | 20 | dir1/dir2/d2f1.md:31:1-38: bad reference: 21 | The reference to "bad-anchor-rel-1" failed verification. 22 | Anchor 'bad-anchor' is not present 23 | 24 | dir1/dir2/d2f1.md:32:1-49: bad reference: 25 | The reference to "bad-anchor-rel-2" failed verification. 26 | File does not exist: 27 | dir1/dir2/unexisting-file.md 28 | 29 | dir1/dir2/d2f1.md:42:1-22: bad reference: 30 | The reference to "file-abs-2" failed verification. 31 | File does not exist: 32 | d1f1.md 33 | 34 | dir1/dir2/d2f1.md:43:1-27: bad reference: 35 | The reference to "file-abs-3" failed verification. 36 | File does not exist: 37 | dir2/d2f2.md 38 | 39 | dir1/dir2/d2f1.md:44:1-32: bad reference: 40 | The reference to "file-abs-4" failed verification. 41 | File does not exist: 42 | ./dir2/../d1f1.md 43 | 44 | dir1/dir2/d2f1.md:45:1-37: bad reference: 45 | The reference to "file-abs-slash" failed verification. 46 | File does not exist: 47 | ./dir2/../d1f1.md/ 48 | 49 | dir1/dir2/d2f1.md:46:1-55: bad reference: 50 | The reference to "anchor-abs-3" failed verification. 51 | File does not exist: 52 | ./dir2/../d1f1.md 53 | 54 | dir1/dir2/d2f1.md:57:1-16: bad reference: 55 | The reference to "A" failed verification. 56 | Link (absolute) targets a local file outside the repository: 57 | ../../a.md 58 | 59 | dir1/dir2/d2f1.md:58:1-18: bad reference: 60 | The reference to "B" failed verification. 61 | Link (absolute) targets a local file outside the repository: 62 | b/../../b.md 63 | 64 | Invalid references dumped, 13 in total. 65 | -------------------------------------------------------------------------------- /tests/golden/check-local-refs/expected2.gold: -------------------------------------------------------------------------------- 1 | dir1/dir2/d2f1.md:9:1-18: bad reference: 2 | The reference to "bad-cf-ref" failed verification. 3 | Anchor 'bad' is not present 4 | 5 | dir1/dir2/d2f1.md:27:1-43: bad reference: 6 | The reference to "bad-file-rel" failed verification. 7 | File does not exist: 8 | dir2/../a/b/c/unexisting-file.md 9 | 10 | dir1/dir2/d2f1.md:28:1-31: bad reference: 11 | The reference to "bad-casing-file-rel" failed verification. 12 | File does not exist: 13 | dir2/D2F2.md/ 14 | 15 | dir1/dir2/d2f1.md:29:1-32: bad reference: 16 | The reference to "bad-casing-folder-rel" failed verification. 17 | File does not exist: 18 | dir2/../DIR2 19 | 20 | dir1/dir2/d2f1.md:31:1-38: bad reference: 21 | The reference to "bad-anchor-rel-1" failed verification. 22 | Anchor 'bad-anchor' is not present 23 | 24 | dir1/dir2/d2f1.md:32:1-49: bad reference: 25 | The reference to "bad-anchor-rel-2" failed verification. 26 | File does not exist: 27 | dir2/unexisting-file.md 28 | 29 | dir1/dir2/d2f1.md:36:1-29: bad reference: 30 | The reference to "file-abs-1" failed verification. 31 | File does not exist: 32 | dir1/./d1f1.md 33 | 34 | dir1/dir2/d2f1.md:37:1-21: bad reference: 35 | The reference to "folder-abs-1" failed verification. 36 | File does not exist: 37 | dir1 38 | 39 | dir1/dir2/d2f1.md:38:1-30: bad reference: 40 | The reference to "folder-abs-2" failed verification. 41 | File does not exist: 42 | dir1/dir2/../ 43 | 44 | dir1/dir2/d2f1.md:39:1-58: bad reference: 45 | The reference to "anchor-abs-1" failed verification. 46 | File does not exist: 47 | dir1/../dir1/d1f1.md 48 | 49 | dir1/dir2/d2f1.md:40:1-73: bad reference: 50 | The reference to "anchor-abs-2" failed verification. 51 | File does not exist: 52 | dir1/dir2/../../dir1/./dir2/d2f2.md 53 | 54 | dir1/dir2/d2f1.md:51:1-42: bad reference: 55 | The reference to "path-through-top-dir" failed verification. 56 | Link (relative) targets a local file outside the repository: 57 | dir2/../../dir1/d1f1.md 58 | 59 | dir1/dir2/d2f1.md:52:1-75: bad reference: 60 | The reference to "path-through-top-dir-with-anchor" failed verification. 61 | Link (relative) targets a local file outside the repository: 62 | dir2/../../dir1/d1f1.md 63 | 64 | dir1/dir2/d2f1.md:53:1-26: bad reference: 65 | The reference to "ref-to-d0" failed verification. 66 | Link (relative) targets a local file outside the repository: 67 | dir2/../../d0f1.md 68 | 69 | dir1/dir2/d2f1.md:57:1-16: bad reference: 70 | The reference to "A" failed verification. 71 | Link (absolute) targets a local file outside the repository: 72 | ../../a.md 73 | 74 | dir1/dir2/d2f1.md:58:1-18: bad reference: 75 | The reference to "B" failed verification. 76 | Link (absolute) targets a local file outside the repository: 77 | b/../../b.md 78 | 79 | Invalid references dumped, 16 in total. 80 | -------------------------------------------------------------------------------- /tests/golden/check-local-refs/expected3.gold: -------------------------------------------------------------------------------- 1 | dir1/dir2/d2f1.md:9:1-18: bad reference: 2 | The reference to "bad-cf-ref" failed verification. 3 | Anchor 'bad' is not present 4 | 5 | dir1/dir2/d2f1.md:27:1-43: bad reference: 6 | The reference to "bad-file-rel" failed verification. 7 | File does not exist: 8 | dir2/../a/b/c/unexisting-file.md 9 | 10 | dir1/dir2/d2f1.md:28:1-31: bad reference: 11 | The reference to "bad-casing-file-rel" failed verification. 12 | File does not exist: 13 | dir2/D2F2.md/ 14 | 15 | dir1/dir2/d2f1.md:31:1-38: bad reference: 16 | The reference to "bad-anchor-rel-1" failed verification. 17 | Anchor 'bad-anchor' is not present 18 | 19 | dir1/dir2/d2f1.md:32:1-49: bad reference: 20 | The reference to "bad-anchor-rel-2" failed verification. 21 | File does not exist: 22 | dir2/unexisting-file.md 23 | 24 | dir1/dir2/d2f1.md:36:1-29: bad reference: 25 | The reference to "file-abs-1" failed verification. 26 | File does not exist: 27 | dir1/./d1f1.md 28 | 29 | dir1/dir2/d2f1.md:37:1-21: bad reference: 30 | The reference to "folder-abs-1" failed verification. 31 | File does not exist: 32 | dir1 33 | 34 | dir1/dir2/d2f1.md:38:1-30: bad reference: 35 | The reference to "folder-abs-2" failed verification. 36 | File does not exist: 37 | dir1/dir2/../ 38 | 39 | dir1/dir2/d2f1.md:39:1-58: bad reference: 40 | The reference to "anchor-abs-1" failed verification. 41 | File does not exist: 42 | dir1/../dir1/d1f1.md 43 | 44 | dir1/dir2/d2f1.md:40:1-73: bad reference: 45 | The reference to "anchor-abs-2" failed verification. 46 | File does not exist: 47 | dir1/dir2/../../dir1/./dir2/d2f2.md 48 | 49 | dir1/dir2/d2f1.md:51:1-42: bad reference: 50 | The reference to "path-through-top-dir" failed verification. 51 | Link (relative) targets a local file outside the repository: 52 | dir2/../../dir1/d1f1.md 53 | 54 | dir1/dir2/d2f1.md:52:1-75: bad reference: 55 | The reference to "path-through-top-dir-with-anchor" failed verification. 56 | Link (relative) targets a local file outside the repository: 57 | dir2/../../dir1/d1f1.md 58 | 59 | Invalid references dumped, 12 in total. 60 | -------------------------------------------------------------------------------- /tests/golden/check-redirect-parse/bad-code.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Serokell 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | scanners: 6 | markdown: 7 | flavor: GitHub 8 | 9 | networking: 10 | externalRefRedirects: 11 | - outcome: valid 12 | on: 404 13 | -------------------------------------------------------------------------------- /tests/golden/check-redirect-parse/bad-on.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Serokell 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | scanners: 6 | markdown: 7 | flavor: GitHub 8 | 9 | networking: 10 | externalRefRedirects: 11 | - outcome: valid 12 | on: premanent 13 | -------------------------------------------------------------------------------- /tests/golden/check-redirect-parse/bad-outcome.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Serokell 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | scanners: 6 | markdown: 7 | flavor: GitHub 8 | 9 | networking: 10 | externalRefRedirects: 11 | - outcome: flow 12 | -------------------------------------------------------------------------------- /tests/golden/check-redirect-parse/bad-rule.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Serokell 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | scanners: 6 | markdown: 7 | flavor: GitHub 8 | 9 | networking: 10 | externalRefRedirects: [Bad] 11 | -------------------------------------------------------------------------------- /tests/golden/check-redirect-parse/bad-rules.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Serokell 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | scanners: 6 | markdown: 7 | flavor: GitHub 8 | 9 | networking: 10 | externalRefRedirects: Bad 11 | -------------------------------------------------------------------------------- /tests/golden/check-redirect-parse/bad-to.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Serokell 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | scanners: 6 | markdown: 7 | flavor: GitHub 8 | 9 | networking: 10 | externalRefRedirects: 11 | - outcome: valid 12 | to: 42 13 | -------------------------------------------------------------------------------- /tests/golden/check-redirect-parse/check-redirect-parse.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | # SPDX-FileCopyrightText: 2022 Serokell 4 | # 5 | # SPDX-License-Identifier: MPL-2.0 6 | 7 | load '../helpers/bats-support/load' 8 | load '../helpers/bats-assert/load' 9 | load '../helpers/bats-file/load' 10 | load '../helpers' 11 | 12 | 13 | @test "No redirect rules" { 14 | golden_file=$(realpath expected1.gold) 15 | to_temp xrefcheck -u -c no-rules.yaml 16 | assert_diff 17 | } 18 | 19 | @test "Only outcome" { 20 | golden_file=$(realpath expected2.gold) 21 | to_temp xrefcheck -u -c only-outcome.yaml 22 | assert_diff 23 | } 24 | 25 | @test "Only outcome and to" { 26 | golden_file=$(realpath expected3.gold) 27 | to_temp xrefcheck -u -c only-outcome-to.yaml 28 | assert_diff 29 | } 30 | 31 | @test "Only outcome and on" { 32 | golden_file=$(realpath expected4.gold) 33 | to_temp xrefcheck -u -c only-outcome-to.yaml 34 | assert_diff 35 | } 36 | 37 | @test "Full rule" { 38 | golden_file=$(realpath expected5.gold) 39 | to_temp xrefcheck -u -c full-rule.yaml 40 | assert_diff 41 | } 42 | 43 | @test "Rules not an array error" { 44 | run xrefcheck -u -c bad-rules.yaml 45 | 46 | assert_output --partial "expected Array, but encountered String" 47 | } 48 | 49 | @test "Rule not an object error" { 50 | run xrefcheck -u -c bad-rule.yaml 51 | 52 | assert_output --partial "expected Object, but encountered String" 53 | } 54 | 55 | @test "Bad code error" { 56 | run xrefcheck -u -c bad-code.yaml 57 | 58 | assert_output --partial "expected a redirect (3XX) HTTP code or (permanent|temporary)" 59 | } 60 | 61 | @test "Bad on" { 62 | run xrefcheck -u -c bad-on.yaml 63 | 64 | assert_output --partial "expected a redirect (3XX) HTTP code or (permanent|temporary)" 65 | } 66 | 67 | @test "Bad to" { 68 | run xrefcheck -u -c bad-to.yaml 69 | 70 | assert_output --partial "expected String, but encountered Number" 71 | } 72 | 73 | @test "Bad outcome" { 74 | run xrefcheck -u -c bad-outcome.yaml 75 | 76 | assert_output --partial "expected (valid|invalid|follow)" 77 | } 78 | 79 | @test "No outcome error" { 80 | run xrefcheck -u -c no-outcome.yaml 81 | 82 | assert_output --partial "key \"outcome\" not found" 83 | } 84 | -------------------------------------------------------------------------------- /tests/golden/check-redirect-parse/expected1.gold: -------------------------------------------------------------------------------- 1 | All repository links are valid. 2 | -------------------------------------------------------------------------------- /tests/golden/check-redirect-parse/expected2.gold: -------------------------------------------------------------------------------- 1 | All repository links are valid. 2 | -------------------------------------------------------------------------------- /tests/golden/check-redirect-parse/expected3.gold: -------------------------------------------------------------------------------- 1 | All repository links are valid. 2 | -------------------------------------------------------------------------------- /tests/golden/check-redirect-parse/expected4.gold: -------------------------------------------------------------------------------- 1 | All repository links are valid. 2 | -------------------------------------------------------------------------------- /tests/golden/check-redirect-parse/expected5.gold: -------------------------------------------------------------------------------- 1 | All repository links are valid. 2 | -------------------------------------------------------------------------------- /tests/golden/check-redirect-parse/full-rule.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Serokell 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | scanners: 6 | markdown: 7 | flavor: GitHub 8 | 9 | networking: 10 | externalRefRedirects: 11 | - outcome: valid 12 | to: https://.* 13 | on: permanent 14 | -------------------------------------------------------------------------------- /tests/golden/check-redirect-parse/no-outcome.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Serokell 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | scanners: 6 | markdown: 7 | flavor: GitHub 8 | 9 | networking: 10 | externalRefRedirects: 11 | - outcome: valid 12 | on: temporary 13 | - to: https://.* 14 | on: temporary 15 | -------------------------------------------------------------------------------- /tests/golden/check-redirect-parse/no-rules.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Serokell 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | scanners: 6 | markdown: 7 | flavor: GitHub 8 | 9 | networking: 10 | externalRefRedirects: [] 11 | -------------------------------------------------------------------------------- /tests/golden/check-redirect-parse/only-outcome-on.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Serokell 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | scanners: 6 | markdown: 7 | flavor: GitHub 8 | 9 | networking: 10 | externalRefRedirects: 11 | - outcome: invalid 12 | on: 302 13 | -------------------------------------------------------------------------------- /tests/golden/check-redirect-parse/only-outcome-to.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Serokell 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | scanners: 6 | markdown: 7 | flavor: GitHub 8 | 9 | networking: 10 | externalRefRedirects: 11 | - outcome: valid 12 | to: https://.* 13 | -------------------------------------------------------------------------------- /tests/golden/check-redirect-parse/only-outcome.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Serokell 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | scanners: 6 | markdown: 7 | flavor: GitHub 8 | 9 | networking: 10 | externalRefRedirects: 11 | - outcome: follow 12 | -------------------------------------------------------------------------------- /tests/golden/check-scan-errors/check-scan-errors.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | # SPDX-FileCopyrightText: 2022 Serokell 4 | # 5 | # SPDX-License-Identifier: MPL-2.0 6 | 7 | load '../helpers/bats-support/load' 8 | load '../helpers/bats-assert/load' 9 | load '../helpers/bats-file/load' 10 | load '../helpers' 11 | 12 | 13 | @test "Dump all errors along with broken links" { 14 | golden_file=$(realpath expected.gold) 15 | to_temp xrefcheck -u 16 | assert_diff 17 | } 18 | -------------------------------------------------------------------------------- /tests/golden/check-scan-errors/check-scan-errors.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | one 8 | 9 | 10 | 11 | [Bad reference](/no-file.md) 12 | 13 | 14 | 15 | # not a paragraph 16 | 17 | 18 | 19 | # not a link 20 | 21 | 22 | 23 | [Bad link](bad.link.com) 24 | -------------------------------------------------------------------------------- /tests/golden/check-scan-errors/check-second-file.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | another file with broken annotation 8 | 9 | 10 | 11 | [Another bad reference](/a.md) 12 | -------------------------------------------------------------------------------- /tests/golden/check-scan-errors/expected.gold: -------------------------------------------------------------------------------- 1 | check-scan-errors.md:9:1-29: scan error: 2 | Annotation "ignore all" must be at the top of markdown or right after comments at the top 3 | 4 | check-scan-errors.md:13:1-36: scan error: 5 | Expected a PARAGRAPH after "ignore paragraph" annotation, but found HEADING 6 | 7 | check-scan-errors.md:17:1-31: scan error: 8 | Expected a LINK after "ignore link" annotation 9 | 10 | check-scan-errors.md:21:1-50: scan error: 11 | Unrecognised option "unrecognised-annotation" 12 | Perhaps you meant <"ignore link"|"ignore paragraph"|"ignore all"> 13 | 14 | check-second-file.md:9:1-29: scan error: 15 | Annotation "ignore all" must be at the top of markdown or right after comments at the top 16 | 17 | no_link_eof.md:9:1-31: scan error: 18 | Expected a LINK after "ignore link" annotation 19 | 20 | no_paragraph_eof.md:9:1-36: scan error: 21 | Expected a PARAGRAPH after "ignore paragraph" annotation, but found EOF 22 | 23 | Scan errors dumped, 7 in total. 24 | 25 | check-scan-errors.md:11:1-28: bad reference: 26 | The reference to "Bad reference" failed verification. 27 | File does not exist: 28 | no-file.md 29 | 30 | check-scan-errors.md:23:1-24: bad reference: 31 | The reference to "Bad link" failed verification. 32 | File does not exist: 33 | bad.link.com 34 | 35 | check-second-file.md:11:1-30: bad reference: 36 | The reference to "Another bad reference" failed verification. 37 | File does not exist: 38 | a.md 39 | 40 | Invalid references dumped, 3 in total. 41 | -------------------------------------------------------------------------------- /tests/golden/check-scan-errors/no_link_eof.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | ### Ignore link annotation placed at the end of the file 8 | 9 | 10 | -------------------------------------------------------------------------------- /tests/golden/check-scan-errors/no_paragraph_eof.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | ### Ignore paragragh annotation placed at the end of the file 8 | 9 | 10 | -------------------------------------------------------------------------------- /tests/golden/check-symlinks/check-symlinks.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | # SPDX-FileCopyrightText: 2023 Serokell 4 | # 5 | # SPDX-License-Identifier: MPL-2.0 6 | 7 | load '../helpers/bats-support/load' 8 | load '../helpers/bats-assert/load' 9 | load '../helpers/bats-file/load' 10 | load '../helpers' 11 | 12 | 13 | @test "Checking that symlinks are not processed as md files" { 14 | golden_file=$(realpath expected1.gold) 15 | 16 | cp config-ignore.yaml $TEST_TEMP_DIR 17 | cp -R dir $TEST_TEMP_DIR 18 | 19 | cd $TEST_TEMP_DIR 20 | touch dir/a 21 | 22 | # Required for Git Bash on Windows to have symlinks properly working, which also requires either 23 | # to be running as an admin or having developer mode turned on. 24 | export MSYS=winsymlinks:nativestrict 25 | 26 | ln -s ../d.md outside.md 27 | ln -s dir/b.md ok.md 28 | ln -s dir/c.md broken.md 29 | 30 | git init 31 | git add ./* 32 | 33 | to_temp xrefcheck -u -v -c config-ignore.yaml 34 | 35 | assert_diff $golden_file 36 | } 37 | 38 | @test "Symlinks validation" { 39 | golden_file=$(realpath expected2.gold) 40 | 41 | cp -R dir $TEST_TEMP_DIR 42 | 43 | cd $TEST_TEMP_DIR 44 | touch dir/a 45 | 46 | # Required for Git Bash on Windows to have symlinks properly working, which also requires either 47 | # to be running as an admin or having developer mode turned on. 48 | export MSYS=winsymlinks:nativestrict 49 | 50 | ln -s ../d.md outside.md 51 | ln -s dir/b.md ok.md 52 | ln -s dir/c.md broken.md 53 | 54 | git init 55 | git add ./* 56 | 57 | to_temp xrefcheck -u -v 58 | 59 | assert_diff 60 | } 61 | -------------------------------------------------------------------------------- /tests/golden/check-symlinks/config-ignore.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Serokell 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | exclusions: 6 | ignore: 7 | - broken.md 8 | - outside.md 9 | 10 | scanners: 11 | markdown: 12 | flavor: GitHub 13 | -------------------------------------------------------------------------------- /tests/golden/check-symlinks/dir/b.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | [Empty file](a) 8 | 9 | [Some symlink](../ok.md) 10 | -------------------------------------------------------------------------------- /tests/golden/check-symlinks/expected1.gold: -------------------------------------------------------------------------------- 1 | === Repository data === 2 | 3 | dir/b.md: 4 | - references: 5 | - reference (relative) at dir/b.md:7:1-15: 6 | - text: "Empty file" 7 | - link: a 8 | - anchor: - 9 | - reference (relative) at dir/b.md:9:1-24: 10 | - text: "Some symlink" 11 | - link: ../ok.md 12 | - anchor: - 13 | - anchors: 14 | none 15 | 16 | ok.md: 17 | - references: 18 | - reference (relative) at ok.md: 19 | - text: "Symbolic Link" 20 | - link: dir/b.md 21 | - anchor: - 22 | - anchors: 23 | none 24 | 25 | All repository links are valid. 26 | -------------------------------------------------------------------------------- /tests/golden/check-symlinks/expected2.gold: -------------------------------------------------------------------------------- 1 | === Repository data === 2 | 3 | broken.md: 4 | - references: 5 | - reference (relative) at broken.md: 6 | - text: "Symbolic Link" 7 | - link: dir/c.md 8 | - anchor: - 9 | - anchors: 10 | none 11 | 12 | dir/b.md: 13 | - references: 14 | - reference (relative) at dir/b.md:7:1-15: 15 | - text: "Empty file" 16 | - link: a 17 | - anchor: - 18 | - reference (relative) at dir/b.md:9:1-24: 19 | - text: "Some symlink" 20 | - link: ../ok.md 21 | - anchor: - 22 | - anchors: 23 | none 24 | 25 | ok.md: 26 | - references: 27 | - reference (relative) at ok.md: 28 | - text: "Symbolic Link" 29 | - link: dir/b.md 30 | - anchor: - 31 | - anchors: 32 | none 33 | 34 | outside.md: 35 | - references: 36 | - reference (relative) at outside.md: 37 | - text: "Symbolic Link" 38 | - link: ../d.md 39 | - anchor: - 40 | - anchors: 41 | none 42 | 43 | broken.md: bad reference: 44 | The reference to "Symbolic Link" failed verification. 45 | File does not exist: 46 | dir/c.md 47 | 48 | outside.md: bad reference: 49 | The reference to "Symbolic Link" failed verification. 50 | Link (relative) targets a local file outside the repository: 51 | ../d.md 52 | 53 | Invalid references dumped, 2 in total. 54 | -------------------------------------------------------------------------------- /tests/golden/helpers.bash: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Serokell 2 | # 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | setup () { 6 | # change working directory to the location of the running `bats` suite. 7 | cd "$( dirname "$BATS_TEST_FILENAME")" 8 | TEST_TEMP_DIR="$(temp_make)" 9 | } 10 | 11 | teardown() { 12 | temp_del "$TEST_TEMP_DIR" 13 | } 14 | 15 | # this function is used for: 16 | # - delete all color characters 17 | # - replace socket port with N 18 | # - replace multiple connection retry errors with single one 19 | # (because at some machine there are one error and at others - two) 20 | prepare () { 21 | sed -r "s/[[:cntrl:]]\[[0-9]{1,3}m//g" \ 22 | | sed 's/socket: [0-9]*/socket: N/g' \ 23 | | sed 's/Network.Socket.connect: : does not exist (Connection refused),//g' 24 | } 25 | 26 | # Create temporary file with program output, used with `assert_diff`. 27 | to_temp() { 28 | output_file="$TEST_TEMP_DIR/temp_file.test" 29 | $@ | prepare > $output_file 30 | } 31 | 32 | # Uses `diff` to compare output file created by `to_temp` against expected output. 33 | # Expected output must be given by setting the `golden_file` variable to an 34 | # absolute path. 35 | # 36 | # Usage example: 37 | # - filepath: 38 | # 39 | # @test "Ignore localhost, check errors" { 40 | # golden_file=$(realpath expected.gold) 41 | # 42 | # to_temp xrefcheck \ 43 | # -c config-check-enabled.yaml \ 44 | # -r . 45 | # 46 | # assert_diff 47 | # } 48 | assert_diff() { 49 | : "{golden_file?}" 50 | : "{output_file?}" 51 | 52 | if [ "${BATS_ACCEPT}" = "1" ]; then 53 | cp $output_file $golden_file 54 | fi 55 | 56 | diff $output_file $golden_file \ 57 | --ignore-tab-expansion \ 58 | --strip-trailing-cr 59 | } 60 | -------------------------------------------------------------------------------- /tests/markdowns/with-annotations/ignore_file.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | Serokell [web-site](https://serokell.io/) 13 | Serokell [team](https://serokell.io/team) 14 | 15 | Serokell [blog](https://serokell.io/blog) 16 | 17 | Serokell [labs](https://serokell.io/labs) 18 | 19 | Serokell [contacts](https://serokell.io/contacts) 20 | -------------------------------------------------------------------------------- /tests/markdowns/with-annotations/ignore_link.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | ### Ignore the first link in the paragraph 8 | 9 | 10 | Serokell [web-site](https://serokell.io/) 11 | Serokell [team](https://serokell.io/team) 12 | 13 | 14 | 15 | Serokell [blog](https://serokell.io/blog) 16 | 17 | Serokell [labs](https://serokell.io/labs) 18 | 19 | Serokell 20 | [contacts](https://serokell.io/contacts) and again 21 | [team](https://serokell.io/team) 22 | 23 | ### Ignore not the first link in the paragraph 24 | 25 | [team](https://serokell.io/team) again and [projects](https://serokell.io/projects) 26 | 27 | Also [hire-us](https://serokell.io/hire-us) and 28 | [fintech](https://serokell.io/fintech-development) 29 | development 30 | 31 | Here are [how-we-work](https://serokell.io/how-we-work) and [privacy](https://serokell.io/privacy) 32 | and [ml consulting](https://serokell.io/machine-learning-consulting) 33 | 34 | 35 | Ignore link bug _regression test_ [link1](link1) [link2](link2) 36 | 37 | 38 | Another ignore link bug _some [link1](link1) emphasis_ [link2](link2) 39 | 40 | ### Ignore pragma should be followed by 41 | 42 | 43 | 44 | This annotation expects link in paragraph right after it. 45 | 46 | So [link3](link3) is not ignored. 47 | 48 | Annotation inside paragraph allows 49 | softbreaks and __other *things*__ in paragraph, so [link4](link4) is ignored. 50 | -------------------------------------------------------------------------------- /tests/markdowns/with-annotations/ignore_paragraph.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | Serokell [web-site](https://serokell.io/) 9 | Serokell [team](https://serokell.io/team) 10 | 11 | Serokell [blog](https://serokell.io/blog) 12 | 13 | 14 | Serokell [labs](https://serokell.io/labs) 15 | 16 | Serokell [contacts](https://serokell.io/contacts) 17 | -------------------------------------------------------------------------------- /tests/markdowns/with-annotations/no_link.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | not a link 9 | -------------------------------------------------------------------------------- /tests/markdowns/with-annotations/no_paragraph.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | # not a paragraph -------------------------------------------------------------------------------- /tests/markdowns/with-annotations/unexpected_ignore_file.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | the first paragraph 8 | 9 | 10 | 11 | the second paragraph 12 | -------------------------------------------------------------------------------- /tests/markdowns/with-annotations/unrecognised_option.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | Serokell [web-site](https://serokell.io/) 9 | -------------------------------------------------------------------------------- /tests/markdowns/without-annotations/all_checked.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | [Non-existent reference](https://non-existent.reference/) 8 | 9 | [Bad externall reference](https://bad.externall.reference) 10 | 11 | [Bad 'io' reference](https://bad.reference.io) 12 | -------------------------------------------------------------------------------- /tests/markdowns/without-annotations/all_ignored.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | [Bad reference with '/'](https://bad.reference/) 8 | [Bad reference without '/'](https://bad.reference) 9 | 10 | [Bad external reference with '/'](https://bad.external.reference/) 11 | [Bad external reference without '/'](https://bad.external.reference) 12 | 13 | [Bad 'org' reference](https://bad.reference.org) 14 | [Bad 'com' reference](https://bad.reference.com) 15 | -------------------------------------------------------------------------------- /tests/markdowns/without-annotations/anchors_in_headers.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | ## Some stuff 8 | 9 | [My link](#stuff-section) 10 | -------------------------------------------------------------------------------- /tests/markdowns/without-annotations/anchors_in_headers_with_id_attribute.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | ## Some stuff with id attribute 8 | 9 | [My link](#stuff-section-with-id-attribute) 10 | -------------------------------------------------------------------------------- /tests/markdowns/without-annotations/non_stripped_spaces.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | ## Header , with leading spaces! 8 | 9 | [My link](#edge-case) --------------------------------------------------------------------------------