Repository: JuliaTesting/Aqua.jl
Branch: master
Commit: cdc679fb3809
Files: 85
Total size: 111.2 KB
Directory structure:
gitextract_3yp6x4rr/
├── .JuliaFormatter.toml
├── .github/
│ ├── dependabot.yml
│ └── workflows/
│ ├── DocPreviewCleanup.yml
│ ├── TagBot.yml
│ ├── code-style.yml
│ ├── docs.yml
│ ├── enforce-labels.yml
│ └── test.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── Makefile
├── Project.toml
├── README.md
├── docs/
│ ├── Project.toml
│ ├── changelog.jl
│ ├── instantiate.jl
│ ├── make.jl
│ └── src/
│ ├── ambiguities.md
│ ├── deps_compat.md
│ ├── exports.md
│ ├── index.md
│ ├── persistent_tasks.md
│ ├── piracies.md
│ ├── project_extras.md
│ ├── stale_deps.md
│ ├── test_all.md
│ ├── unbound_args.md
│ └── undocumented_names.md
├── src/
│ ├── Aqua.jl
│ ├── ambiguities.jl
│ ├── deps_compat.jl
│ ├── exports.jl
│ ├── persistent_tasks.jl
│ ├── piracies.jl
│ ├── precompile.jl
│ ├── project_extras.jl
│ ├── stale_deps.jl
│ ├── unbound_args.jl
│ ├── undocumented_names.jl
│ └── utils.jl
└── test/
├── pkgs/
│ ├── AquaTesting.jl
│ ├── PersistentTasks/
│ │ ├── PersistentTask/
│ │ │ ├── Project.toml
│ │ │ └── src/
│ │ │ └── PersistentTask.jl
│ │ ├── TransientTask/
│ │ │ ├── Project.toml
│ │ │ └── src/
│ │ │ └── TransientTask.jl
│ │ └── UsesBoth/
│ │ ├── Project.toml
│ │ └── src/
│ │ └── UsesBoth.jl
│ ├── PiracyForeignProject/
│ │ ├── Project.toml
│ │ └── src/
│ │ └── PiracyForeignProject.jl
│ ├── PkgUnboundArgs.jl
│ ├── PkgWithAmbiguities.jl
│ ├── PkgWithUndefinedExports.jl
│ ├── PkgWithUndocumentedNames.jl
│ ├── PkgWithUndocumentedNamesInSubmodule.jl
│ ├── PkgWithoutUndocumentedNames.jl
│ └── sample/
│ ├── PkgWithCompatibleTestProject/
│ │ ├── Project.toml
│ │ ├── src/
│ │ │ └── PkgWithCompatibleTestProject.jl
│ │ └── test/
│ │ └── Project.toml
│ ├── PkgWithIncompatibleTestProject/
│ │ ├── Project.toml
│ │ ├── src/
│ │ │ └── PkgWithIncompatibleTestProject.jl
│ │ └── test/
│ │ └── Project.toml
│ ├── PkgWithPostJulia12Support/
│ │ ├── Project.toml
│ │ ├── src/
│ │ │ └── PkgWithPostJulia12Support.jl
│ │ └── test/
│ │ └── Project.toml
│ ├── PkgWithoutDeps/
│ │ ├── Project.toml
│ │ ├── src/
│ │ │ └── PkgWithoutDeps.jl
│ │ └── test/
│ │ └── .gitkeep
│ └── PkgWithoutTestProject/
│ ├── Project.toml
│ ├── src/
│ │ └── PkgWithoutTestProject.jl
│ └── test/
│ └── .gitkeep
├── preamble.jl
├── runtests.jl
├── test_ambiguities.jl
├── test_deps_compat.jl
├── test_exclude.jl
├── test_persistent_tasks.jl
├── test_piracy.jl
├── test_project_extras.jl
├── test_smoke.jl
├── test_stale_deps.jl
├── test_unbound_args.jl
├── test_undefined_exports.jl
├── test_undocumented_names.jl
└── test_utils.jl
================================================
FILE CONTENTS
================================================
================================================
FILE: .JuliaFormatter.toml
================================================
================================================
FILE: .github/dependabot.yml
================================================
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
================================================
FILE: .github/workflows/DocPreviewCleanup.yml
================================================
name: Doc Preview Cleanup
on:
pull_request:
types: [closed]
jobs:
doc-preview-cleanup:
runs-on: ubuntu-slim
permissions:
contents: write
steps:
- name: Checkout gh-pages branch
uses: actions/checkout@v6
with:
ref: gh-pages
- name: Delete preview and history + push changes
run: |
if [ -d "previews/PR$PRNUM" ]; then
git config user.name "Documenter.jl"
git config user.email "documenter@juliadocs.github.io"
git rm -rf "previews/PR$PRNUM"
git commit -m "delete preview"
git branch gh-pages-new $(echo "delete history" | git commit-tree HEAD^{tree})
git push --force origin gh-pages-new:gh-pages
fi
env:
PRNUM: ${{ github.event.number }}
# copied from here:
# https://juliadocs.github.io/Documenter.jl/stable/man/hosting/#gh-pages-Branch
================================================
FILE: .github/workflows/TagBot.yml
================================================
name: TagBot
on:
issue_comment:
types:
- created
workflow_dispatch:
inputs:
lookback:
default: 3
jobs:
TagBot:
if: github.event_name == 'workflow_dispatch' || github.actor == 'JuliaTagBot'
runs-on: ubuntu-latest
steps:
- uses: JuliaRegistries/TagBot@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
ssh: ${{ secrets.SSH_KEY }}
changelog: |
{% if custom %}
{{ custom }}
{% endif %}
The changes are documented in the [`CHANGELOG.md`](https://github.com/JuliaTesting/Aqua.jl/blob/{{ version }}/CHANGELOG.md) file.
{% if previous_release %}
[Diff since {{ previous_release }}]({{ compare_url }})
{% endif %}
================================================
FILE: .github/workflows/code-style.yml
================================================
name: Code style
on:
pull_request:
jobs:
code-style:
runs-on: ubuntu-slim
steps:
- uses: tkf/julia-code-style-suggesters@v1
================================================
FILE: .github/workflows/docs.yml
================================================
name: Documentation
on:
push:
branches:
- master
- 'release-*'
tags: '*'
pull_request:
workflow_dispatch:
# needed to allow julia-actions/cache to delete old caches that it has created
permissions:
actions: write
contents: read
concurrency:
# group by workflow and ref; the last slightly strange component ensures that for pull
# requests, we limit to 1 concurrent job, but for the master branch we don't
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.ref != 'refs/heads/master' || github.run_number }}
# Cancel intermediate builds, but only if it is a pull request build.
cancel-in-progress: ${{ startsWith(github.ref, 'refs/pull/') }}
jobs:
Documenter:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v6
- uses: julia-actions/setup-julia@v2
with:
version: 1
- uses: julia-actions/cache@v3
- uses: julia-actions/julia-docdeploy@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
DOCUMENTER_KEY: ${{ secrets.SSH_KEY }}
================================================
FILE: .github/workflows/enforce-labels.yml
================================================
name: Enforce PR labels
permissions:
contents: read
on:
pull_request:
types: [labeled, unlabeled, opened, reopened, edited, synchronize]
jobs:
enforce-labels:
name: Check for blocking labels
runs-on: ubuntu-latest
timeout-minutes: 2
steps:
- uses: yogevbd/enforce-label-action@2.2.2
with:
REQUIRED_LABELS_ANY: "changelog: added,changelog: not needed,release"
REQUIRED_LABELS_ANY_DESCRIPTION: "Select at least one label ['changelog: added','changelog: not needed','release']"
BANNED_LABELS: "changelog: missing,DO NOT MERGE"
BANNED_LABELS_DESCRIPTION: "A PR should not be merged with `DO NOT MERGE` or `changelog: missing` labels"
================================================
FILE: .github/workflows/test.yml
================================================
name: Run tests
on:
push:
branches:
- master
- 'release-*'
tags: '*'
pull_request:
workflow_dispatch:
# needed to allow julia-actions/cache to delete old caches that it has created
permissions:
actions: write
contents: read
concurrency:
# group by workflow and ref; the last slightly strange component ensures that for pull
# requests, we limit to 1 concurrent job, but for the master branch we don't
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.ref != 'refs/heads/master' || github.run_number }}
# Cancel intermediate builds, but only if it is a pull request build.
cancel-in-progress: ${{ startsWith(github.ref, 'refs/pull/') }}
jobs:
test:
name: Test Julia ${{ matrix.julia-version }} ${{ matrix.os }}
runs-on: ${{ matrix.os }}
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
os: ["ubuntu-latest"]
julia-version:
- '1.12-nightly'
- '1.11'
- '1.10'
- '1.9'
- '1.8'
- '1.7'
- '1.6'
- 'nightly'
include:
- os: windows-latest
julia-version: '1'
- os: windows-latest
julia-version: '1.6'
- os: windows-latest
julia-version: '1.12-nightly'
- os: windows-latest
julia-version: 'nightly'
- os: macOS-latest
julia-version: '1'
- os: macOS-latest
julia-version: '1.12-nightly'
- os: macOS-latest
julia-version: 'nightly'
steps:
- uses: actions/checkout@v6
with:
# For Codecov, we must also fetch the parent of the HEAD commit to
# be able to properly deal with PRs / merges
fetch-depth: 2
- name: Setup julia
uses: julia-actions/setup-julia@v2
with:
version: ${{ matrix.julia-version }}
- uses: julia-actions/cache@v3
- uses: julia-actions/julia-runtest@v1
with:
depwarn: error
- uses: julia-actions/julia-processcoverage@v1
- uses: codecov/codecov-action@v6
with:
token: ${{ secrets.CODECOV_TOKEN }}
================================================
FILE: .gitignore
================================================
*.jl.*.cov
*.jl.cov
*.jl.mem
.DS_Store
/docs/build/
/docs/site/
/docs/src/release-notes.md
Manifest.toml
================================================
FILE: CHANGELOG.md
================================================
# Release notes
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Version [v1.0.0] - unreleased
### Changed
- Fix some world age issues in `test_ambiguities`. ([#366])
- The minimum supported julia version is increased to 1.6. ([#328])
## Version [v0.8.14] - 2025-08-04
### Changed
- Adapt to internal method table changes in Julia 1.12 and later. ([#344])
## Version [v0.8.13] - 2025-05-28
### Changed
- Adapt to internal method table changes in Julia 1.12 and later. ([#334])
## Version [v0.8.12] - 2025-05-05
### Changed
- Add `test_undocumented_names` to verify that all public symbols have docstrings (not including the module itself). This test is not enabled by default in `test_all`. ([#313])
## Version [v0.8.11] - 2025-02-06
### Changed
- Avoid deprecation warnings concerning `Base.isbindingresolved` with julia nightly. ([#322])
## Version [v0.8.10] - 2025-01-26
### Changed
- No longer call `@testset` for testsets that are skipped. ([#319])
## Version [v0.8.9] - 2024-10-15
### Changed
- Change `test_ambiguities` to only return ambiguities that happen in the target package. ([#309])
## Version [v0.8.8] - 2024-10-10
### Changed
- Improved the documentation of `test_persisten_tasks`. ([#297])
## Version [v0.8.7] - 2024-04-09
- Reverted [#285], which was originally released in [v0.8.6], but caused a regression. ([#287], [#288])
## Version [v0.8.6] - 2024-04-09
### Changed
- The output of `test_ambiguities` now gets printed to stderr instead of stdout. ([#281])
## Version [v0.8.5] - 2024-04-03
### Changed
- When supplying `broken = true` to `test_ambiguities`, `test_undefined_exports`, `test_piracies`, or `test_unbound_args`, the output is shortened. In particular, the list of offending instances is no longer printed. To get the full output, set `broken = false`. ([#272])
- Use [Changelog.jl](https://github.com/JuliaDocs/Changelog.jl) to generate the changelog, and add it to the documentation. ([#277], [#279])
- `test_project_extras` prints failures the same on all julia versions. In particular, 1.11 and nightly are no outliers. ([#275])
## Version [v0.8.4] - 2023-12-01
### Added
- `test_persistent_tasks` now accepts an optional `expr` to run in the precompile package. ([#255])
+ The `expr` option lets you test whether your precompile script leaves any dangling Tasks
or Timers, which would make it unsafe to use as a dependency for downstream packages.
## Version [v0.8.3] - 2023-11-29
### Changed
- `test_persistent_tasks` is now less noisy. ([#256])
- Completely overhauled the documentation. Every test now has its dedicated page. ([#250])
## Version [v0.8.2] - 2023-11-16
### Changed
- `test_persistent_tasks` no longer clears the environment of the subtask. Instead, it modifies `LOAD_PATH` directly to make stdlibs work. ([#241])
## Version [v0.8.1] - 2023-11-16
### Changed
- `test_persistent_tasks` now redirects stdout and stderr of the created subtask. Furthermore, the environment of the subtask gets cleared to allow default values for `JULIA_LOAD_PATH` to work. ([#240])
## Version [v0.8.0] - 2023-11-15
### Added
- Two additions check whether packages might block precompilation on Julia 1.10 or higher: ([#174])
+ `test_persistent_tasks` tests whether "your" package can safely be used as a dependency for downstream packages. This test is enabled for the default testsuite `test_all`, but you can opt-out by supplying `persistent_tasks=false` to `test_all`. [BREAKING]
+ `find_persistent_tasks_deps` is useful if "your" package hangs upon precompilation: it runs `test_persistent_tasks` on all the things you depend on, and may help isolate the culprit(s).
### Changed
- In `test_deps_compat`, the two subtests `check_extras` and `check_weakdeps` are now run by default. ([#202]) [BREAKING]
- `test_deps_compat` now requires compat entries for all dependencies. Stdlibs no longer get ignored. This change is motivated by similar changes in the General registry. ([#215]) [BREAKING]
- `test_ambiguities` now excludes the keyword sorter of all `exclude`d functions with keyword arguments as well. ([#203])
- `test_piracy` is renamed to `test_piracies`. ([#230]) [BREAKING]
- `test_ambiguities` and `test_piracies` now return issues in a defined order. This order may change in a patch release of Aqua.jl. ([#233])
- Improved the message for `test_project_extras` failures. ([#234])
- `test_deps_compat` now requires a compat entry for `julia` This can be disabling by setting `compat_julia = false`. ([#236]) [BREAKING]
### Removed
- `test_project_toml_formatting` has been removed. Thus, the kwarg `project_toml_formatting` to `test_all` no longer exists. ([#209]) [BREAKING]
## Version [v0.7.4] - 2023-10-24
### Added
- `test_deps_compat` has two new kwargs `check_extras` and `check_weakdeps` to extend the test to these dependency categories. They are not run by default. ([#200])
### Changed
- The docstring for `test_stale_deps` explains the situation with package extensions. ([#203])
- The logo of Aqua.jl has been updated. ([#128])
## Version [v0.7.3] - 2023-09-25
### Added
- `test_deps_compat` has a new kwarg `broken` to mark the test as broken using `Test.@test_broken`. ([#193])
### Fixed
- `test_piracy` no longer prints warnings for methods where the third argument is a `TypeVar`. ([#188])
## Version [v0.7.2] - 2023-09-19
### Changed
- `test_undefined_exports` additionally prints the modules of the undefined exports in the failure report. ([#177])
## Version [v0.7.1] - 2023-09-05
### Fixed
- `test_piracy` no longer reports type piracy in the kwsorter, i.e. `kwcall` should no longer appear in the report. ([#171])
## Version [v0.7.0] - 2023-08-29
### Added
- Installation and usage instructions to the documentation. ([#159])
### Changed
- `test_ambiguities` now allows to exclude non-singleton callables. Excluding a type means to exclude all methods of the callable (sometimes also called "functor") and the constructor. ([#144]) [BREAKING]
- `test_piracy` considers more functions. Callables and qualified names are now also checked. ([#156]) [BREAKING]
### Fixed
- `test_ambiguities` prints less unnecessary whitespace. ([#158])
- `test_ambiguities` no longer hangs indefinitely when there are many ambiguities. ([#166])
## Version [v0.6.7] - 2023-09-19
### Changed
- `test_undefined_exports` additionally prints the modules of the undefined exports in the failure report. ([#177])
### Fixed
- `test_ambiguities` prints less unnecessary whitespace. ([#158])
- Fix `test_piracy` for some methods with arguments of custom subtypes of `Function`. ([#170])
## Version [v0.6.6] - 2023-08-24
### Fixed
- `test_ambiguities` no longer hangs indefinitely when there are many ambiguities. ([#166])
## Version [v0.6.5] - 2023-06-26
### Fixed
- Typo when calling kwargs. ([#153])
## Version [v0.6.4] - 2023-06-25
### Added
- `test_piracy` has a new kwarg `treat_as_own`. It is useful for testing packages that deliberately commit some type piracy, e.g. modules adding higher-level functionality to a lightweight C-wrapper, or packages that are extending `StatsAPI.jl`. ([#140])
### Changed
- Explanation of `test_unbound_args` in the docstring. ([#146])
### Fixed
- Callable objects with type parameters no longer error in `test_ambiguities`' kwarg `exclude`. ([#142])
## Version [v0.6.3] - 2023-06-05
### Changed
- When installing a method for a union type, it is only reported by `test_piracy` if *all* types in the union are foreign (instead of *any* for arguments). ([#131])
### Fixed
- `test_deps_compat`'s kwarg `ignore` now works as intended. ([#130])
- Weakdeps are not reported as stale by `test_stale_deps` anymore. ([#135])
## Version [v0.6.2] - 2023-06-02
### Added
- `test_ambiguities`, `test_undefined_exports`, `test_piracy`, and `test_unbound_args` each have a new kwarg `broken` to mark the test as broken using `Test.@test_broken`. ([#124])
### Changed
- `test_piracy` now prints the offending methods in a more readable way. ([#93])
- Extend `test_project_toml_formatting` to `docs/Project.toml`. ([#115])
### Fixed
- `test_stale_deps` no longer fails if any of the loaded packages prints during loading. ([#113])
- Clarified the error message of `test_unbound_args`. ([#103])
- Clarified the error message of `test_project_toml_formatting`. ([#122])
<!-- Links generated by Changelog.jl -->
[v0.6.2]: https://github.com/JuliaTesting/Aqua.jl/releases/tag/v0.6.2
[v0.6.3]: https://github.com/JuliaTesting/Aqua.jl/releases/tag/v0.6.3
[v0.6.4]: https://github.com/JuliaTesting/Aqua.jl/releases/tag/v0.6.4
[v0.6.5]: https://github.com/JuliaTesting/Aqua.jl/releases/tag/v0.6.5
[v0.6.6]: https://github.com/JuliaTesting/Aqua.jl/releases/tag/v0.6.6
[v0.6.7]: https://github.com/JuliaTesting/Aqua.jl/releases/tag/v0.6.7
[v0.7.0]: https://github.com/JuliaTesting/Aqua.jl/releases/tag/v0.7.0
[v0.7.1]: https://github.com/JuliaTesting/Aqua.jl/releases/tag/v0.7.1
[v0.7.2]: https://github.com/JuliaTesting/Aqua.jl/releases/tag/v0.7.2
[v0.7.3]: https://github.com/JuliaTesting/Aqua.jl/releases/tag/v0.7.3
[v0.7.4]: https://github.com/JuliaTesting/Aqua.jl/releases/tag/v0.7.4
[v0.8.0]: https://github.com/JuliaTesting/Aqua.jl/releases/tag/v0.8.0
[v0.8.1]: https://github.com/JuliaTesting/Aqua.jl/releases/tag/v0.8.1
[v0.8.2]: https://github.com/JuliaTesting/Aqua.jl/releases/tag/v0.8.2
[v0.8.3]: https://github.com/JuliaTesting/Aqua.jl/releases/tag/v0.8.3
[v0.8.4]: https://github.com/JuliaTesting/Aqua.jl/releases/tag/v0.8.4
[v0.8.5]: https://github.com/JuliaTesting/Aqua.jl/releases/tag/v0.8.5
[v0.8.6]: https://github.com/JuliaTesting/Aqua.jl/releases/tag/v0.8.6
[v0.8.7]: https://github.com/JuliaTesting/Aqua.jl/releases/tag/v0.8.7
[v0.8.8]: https://github.com/JuliaTesting/Aqua.jl/releases/tag/v0.8.8
[v0.8.9]: https://github.com/JuliaTesting/Aqua.jl/releases/tag/v0.8.9
[v0.8.10]: https://github.com/JuliaTesting/Aqua.jl/releases/tag/v0.8.10
[v0.8.11]: https://github.com/JuliaTesting/Aqua.jl/releases/tag/v0.8.11
[v0.8.12]: https://github.com/JuliaTesting/Aqua.jl/releases/tag/v0.8.12
[v0.8.13]: https://github.com/JuliaTesting/Aqua.jl/releases/tag/v0.8.13
[v0.8.14]: https://github.com/JuliaTesting/Aqua.jl/releases/tag/v0.8.14
[v1.0.0]: https://github.com/JuliaTesting/Aqua.jl/releases/tag/v1.0.0
[#93]: https://github.com/JuliaTesting/Aqua.jl/issues/93
[#103]: https://github.com/JuliaTesting/Aqua.jl/issues/103
[#113]: https://github.com/JuliaTesting/Aqua.jl/issues/113
[#115]: https://github.com/JuliaTesting/Aqua.jl/issues/115
[#122]: https://github.com/JuliaTesting/Aqua.jl/issues/122
[#124]: https://github.com/JuliaTesting/Aqua.jl/issues/124
[#128]: https://github.com/JuliaTesting/Aqua.jl/issues/128
[#130]: https://github.com/JuliaTesting/Aqua.jl/issues/130
[#131]: https://github.com/JuliaTesting/Aqua.jl/issues/131
[#135]: https://github.com/JuliaTesting/Aqua.jl/issues/135
[#140]: https://github.com/JuliaTesting/Aqua.jl/issues/140
[#142]: https://github.com/JuliaTesting/Aqua.jl/issues/142
[#144]: https://github.com/JuliaTesting/Aqua.jl/issues/144
[#146]: https://github.com/JuliaTesting/Aqua.jl/issues/146
[#153]: https://github.com/JuliaTesting/Aqua.jl/issues/153
[#156]: https://github.com/JuliaTesting/Aqua.jl/issues/156
[#158]: https://github.com/JuliaTesting/Aqua.jl/issues/158
[#159]: https://github.com/JuliaTesting/Aqua.jl/issues/159
[#166]: https://github.com/JuliaTesting/Aqua.jl/issues/166
[#170]: https://github.com/JuliaTesting/Aqua.jl/issues/170
[#171]: https://github.com/JuliaTesting/Aqua.jl/issues/171
[#174]: https://github.com/JuliaTesting/Aqua.jl/issues/174
[#177]: https://github.com/JuliaTesting/Aqua.jl/issues/177
[#188]: https://github.com/JuliaTesting/Aqua.jl/issues/188
[#193]: https://github.com/JuliaTesting/Aqua.jl/issues/193
[#200]: https://github.com/JuliaTesting/Aqua.jl/issues/200
[#202]: https://github.com/JuliaTesting/Aqua.jl/issues/202
[#203]: https://github.com/JuliaTesting/Aqua.jl/issues/203
[#209]: https://github.com/JuliaTesting/Aqua.jl/issues/209
[#215]: https://github.com/JuliaTesting/Aqua.jl/issues/215
[#230]: https://github.com/JuliaTesting/Aqua.jl/issues/230
[#233]: https://github.com/JuliaTesting/Aqua.jl/issues/233
[#234]: https://github.com/JuliaTesting/Aqua.jl/issues/234
[#236]: https://github.com/JuliaTesting/Aqua.jl/issues/236
[#240]: https://github.com/JuliaTesting/Aqua.jl/issues/240
[#241]: https://github.com/JuliaTesting/Aqua.jl/issues/241
[#250]: https://github.com/JuliaTesting/Aqua.jl/issues/250
[#255]: https://github.com/JuliaTesting/Aqua.jl/issues/255
[#256]: https://github.com/JuliaTesting/Aqua.jl/issues/256
[#272]: https://github.com/JuliaTesting/Aqua.jl/issues/272
[#275]: https://github.com/JuliaTesting/Aqua.jl/issues/275
[#277]: https://github.com/JuliaTesting/Aqua.jl/issues/277
[#279]: https://github.com/JuliaTesting/Aqua.jl/issues/279
[#281]: https://github.com/JuliaTesting/Aqua.jl/issues/281
[#285]: https://github.com/JuliaTesting/Aqua.jl/issues/285
[#287]: https://github.com/JuliaTesting/Aqua.jl/issues/287
[#288]: https://github.com/JuliaTesting/Aqua.jl/issues/288
[#297]: https://github.com/JuliaTesting/Aqua.jl/issues/297
[#309]: https://github.com/JuliaTesting/Aqua.jl/issues/309
[#313]: https://github.com/JuliaTesting/Aqua.jl/issues/313
[#319]: https://github.com/JuliaTesting/Aqua.jl/issues/319
[#322]: https://github.com/JuliaTesting/Aqua.jl/issues/322
[#328]: https://github.com/JuliaTesting/Aqua.jl/issues/328
[#334]: https://github.com/JuliaTesting/Aqua.jl/issues/334
[#344]: https://github.com/JuliaTesting/Aqua.jl/issues/344
================================================
FILE: LICENSE
================================================
Copyright (c) 2019 Takafumi Arakaki
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: Makefile
================================================
JULIA:=julia
default: help
.PHONY: default changelog docs docs-instantiate generate_badge generate_favicon help test
docs-instantiate:
${JULIA} docs/instantiate.jl
changelog: docs-instantiate
${JULIA} --project=docs docs/changelog.jl
docs: docs-instantiate
${JULIA} --project=docs docs/make.jl
generate_badge:
SVG_BASE64=$(shell base64 -w 0 docs/src/assets/logo.svg); \
curl -o "badge.svg" "https://img.shields.io/badge/tested_with-Aqua.jl-05C3DD.svg?logo=data:image/svg+xml;base64,$$SVG_BASE64"
generate_favicon:
convert -background none docs/src/assets/logo.svg -resize 256x256 -gravity center -extent 256x256 logo.png
convert logo.png -define icon:auto-resize=256,64,48,32,16 docs/src/assets/favicon.ico
rm logo.png
test:
${JULIA} --project -e 'using Pkg; Pkg.test()'
help:
@echo "The following make commands are available:"
@echo " - make changelog: update all links in CHANGELOG.md's footer"
@echo " - make docs: build the documentation"
@echo " - make generate_badge: generate the Aqua.jl badge"
@echo " - make generate_favicon: generate the Aqua.jl favicon"
@echo " - make test: run the tests"
================================================
FILE: Project.toml
================================================
name = "Aqua"
uuid = "4c88cf16-eb10-579e-8560-4a9242c79595"
authors = ["Takafumi Arakaki <aka.tkf@gmail.com> and contributors"]
version = "1.0.0-DEV"
[deps]
Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
[compat]
Pkg = "1.6"
PrecompileTools = "1"
Test = "<0.0.1, 1"
julia = "1.6"
[extras]
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
[targets]
test = ["Test"]
================================================
FILE: README.md
================================================
# Aqua.jl: *A*uto *QU*ality *A*ssurance for Julia packages
[](https://juliatesting.github.io/Aqua.jl/stable)
[](https://juliatesting.github.io/Aqua.jl/dev)
[](https://github.com/JuliaTesting/Aqua.jl/actions?query=workflow%3ARun+tests)
[](https://codecov.io/gh/JuliaTesting/Aqua.jl)
[](https://github.com/JuliaTesting/Aqua.jl)
Aqua.jl provides functions to run a few automatable checks for Julia packages:
* There are no method ambiguities.
* There are no undefined `export`s.
* There are no unbound type parameters.
* There are no stale dependencies listed in `Project.toml`.
* Check that test target of the root project `Project.toml` and test project (`test/Project.toml`) are consistent.
* Check that all external packages listed in `deps` have corresponding `compat` entries.
* There are no "obvious" type piracies.
* The package does not create any persistent Tasks that might block precompilation of dependencies.
See more in the [documentation](https://juliatesting.github.io/Aqua.jl/).
For a detailed list of changes please refer to the [changelog](CHANGELOG.md).
## Setup
Please consult the [stable documentation](https://juliatesting.github.io/Aqua.jl/) and the the [dev documentation](https://juliatesting.github.io/Aqua.jl/dev/) for the latest instructions.
## Badge
You can add the following line in README.md to include Aqua.jl badge:
```markdown
[](https://github.com/JuliaTesting/Aqua.jl)
```
which is rendered as
> [](https://github.com/JuliaTesting/Aqua.jl)
================================================
FILE: docs/Project.toml
================================================
[deps]
Changelog = "5217a498-cd5d-4ec6-b8c2-9b85a09b6e3e"
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
[compat]
Changelog = "1"
Documenter = "1"
================================================
FILE: docs/changelog.jl
================================================
using Changelog
Changelog.generate(
Changelog.CommonMark(),
joinpath(@__DIR__, "..", "CHANGELOG.md");
repo = "JuliaTesting/Aqua.jl",
)
================================================
FILE: docs/instantiate.jl
================================================
# This script can be used to quickly instantiate the docs/Project.toml environment.
using Pkg, TOML
package_directory = joinpath(@__DIR__, "..")
docs_directory = isempty(ARGS) ? @__DIR__() : joinpath(pwd(), ARGS[1])
cd(docs_directory) do
Pkg.activate(docs_directory)
Pkg.develop(PackageSpec(path = package_directory))
Pkg.instantiate()
# Remove Aqua again from docs/Project.toml
lines = readlines(joinpath(docs_directory, "Project.toml"))
open(joinpath(docs_directory, "Project.toml"), "w") do io
for line in lines
if line == "Aqua = \"4c88cf16-eb10-579e-8560-4a9242c79595\""
continue
end
println(io, line)
end
end
end
================================================
FILE: docs/make.jl
================================================
using Documenter, Aqua, Changelog
# Generate a Documenter-friendly changelog from CHANGELOG.md
Changelog.generate(
Changelog.Documenter(),
joinpath(@__DIR__, "..", "CHANGELOG.md"),
joinpath(@__DIR__, "src", "release-notes.md");
repo = "JuliaTesting/Aqua.jl",
)
makedocs(;
sitename = "Aqua.jl",
format = Documenter.HTML(;
repolink = "https://github.com/JuliaTesting/Aqua.jl",
assets = ["assets/favicon.ico"],
size_threshold_ignore = ["release-notes.md"],
),
authors = "Takafumi Arakaki",
modules = [Aqua],
pages = [
"Home" => "index.md",
"Tests" => [
"test_all.md",
"ambiguities.md",
"unbound_args.md",
"exports.md",
"project_extras.md",
"stale_deps.md",
"deps_compat.md",
"piracies.md",
"persistent_tasks.md",
"undocumented_names.md",
],
"release-notes.md",
],
)
deploydocs(; repo = "github.com/JuliaTesting/Aqua.jl", push_preview = true)
================================================
FILE: docs/src/ambiguities.md
================================================
# Ambiguities
Method ambiguities are cases where multiple methods are applicable to a given set of arguments, without having a most specific method.
## Examples
One easy example is the following:
```@repl
f(x::Int, y::Integer) = 1
f(x::Integer, y::Int) = 2
println(f(1, 2))
```
This will throw an `MethodError` because both methods are equally specific. The solution is to add a third method:
```julia
f(x::Int, y::Int) = ? # `?` is dependent on the use case, most times it will be `1` or `2`
```
## [Test function](@id test_ambiguities)
```@docs
Aqua.test_ambiguities
```
================================================
FILE: docs/src/deps_compat.md
================================================
# Compat entries
In your `Project.toml` you can (and should) use compat entries to specify
with which versions of Julia and your dependencies your package is compatible with.
This is important to ease the installation and upgrade of your package for users,
and to keep everything working in the case of breaking changes in Julia or your dependencies.
For more details, see the [Pkg docs](https://julialang.github.io/Pkg.jl/v1/compatibility/).
## [Test function](@id test_deps_compat)
```@docs
Aqua.test_deps_compat
```
================================================
FILE: docs/src/exports.md
================================================
# Undefined exports
## [Test function](@id test_undefined_exports)
```@docs
Aqua.test_undefined_exports
```
================================================
FILE: docs/src/index.md
================================================
# Aqua.jl: *A*uto *QU*ality *A*ssurance for Julia packages
Aqua.jl provides functions to run a few automatable checks for Julia packages:
* There are no method ambiguities.
* There are no undefined `export`s.
* There are no unbound type parameters.
* There are no stale dependencies listed in `Project.toml`.
* Check that test target of the root project `Project.toml` and test project (`test/Project.toml`) are consistent.
* Check that all external packages listed in `deps` have corresponding `compat` entries.
* There are no "obvious" type piracies.
* The package does not create any persistent Tasks that might block precompilation of dependencies.
## Quick usage
Call `Aqua.test_all(YourPackage)` from the REPL, e.g.,
```julia
using YourPackage
using Aqua
Aqua.test_all(YourPackage)
```
## How to add Aqua.jl...
### ...as a test dependency?
There are two ways to add Aqua.jl as a test dependency to your package.
To avoid breaking tests when a new Aqua.jl version is released, it is
recommended to add a version bound for Aqua.jl.
1. In `YourPackage/test/Project.toml`, add Aqua.jl to `[dep]` and `[compat]` sections, like
```toml
[deps]
Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
[compat]
Aqua = "0.8"
```
2. In `YourPackage/Project.toml`, add Aqua.jl to `[compat]` and `[extras]` section and the `test` target, like
```toml
[compat]
Aqua = "0.8"
[extras]
Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
[targets]
test = ["Aqua", "Test"]
```
If your package supports Julia pre-1.2, you need to use the second approach,
although you can use both approaches at the same time.
!!! warning
In normal use, `Aqua.jl` should not be added to `[deps]` in `YourPackage/Project.toml`!
### ...to your tests?
It is recommended to create a separate file `YourPackage/test/Aqua.jl` that gets included in `YourPackage/test/runtests.jl`
with either
```julia
using Aqua
Aqua.test_all(YourPackage)
```
or some fine-grained checks with options, e.g.,
```julia
using Aqua
@testset "Aqua.jl" begin
Aqua.test_all(
YourPackage;
ambiguities=(exclude=[SomePackage.some_function], broken=true),
stale_deps=(ignore=[:SomePackage],),
deps_compat=(ignore=[:SomeOtherPackage],),
piracies=false,
)
end
```
Note, that for all tests with no explicit options provided, the default options are used.
For more details on the options, see the respective functions [here](@ref test_all).
## Examples
The following is a small selection of packages that use Aqua.jl:
- [GAP.jl](https://github.com/oscar-system/GAP.jl)
- [Hecke.jl](https://github.com/thofma/Hecke.jl)
- [Oscar.jl](https://github.com/oscar-system/Oscar.jl)
## Badge
You can add the following line in README.md to include Aqua.jl badge:
```markdown
[](https://github.com/JuliaTesting/Aqua.jl)
```
which is rendered as
[](https://github.com/JuliaTesting/Aqua.jl)
================================================
FILE: docs/src/persistent_tasks.md
================================================
# Persistent Tasks
## Motivation
Julia 1.10 and higher wait for all running `Task`s to finish
before writing out the precompiled (cached) version of the package.
One consequence is that a package that launches
`Task`s in its `__init__` function may precompile successfully,
but block precompilation of any packages that depend on it.
The symptom of this problem is a message
```
◐ MyPackage: Waiting for background task / IO / timer. Interrupt to inspect...
```
that may appear during precompilation, with that precompilation process
"hanging" until you press Ctrl-C.
Aqua has checks to determine whether your package *causes* this problem.
Conversely, if you're a *victim* of this problem, it also has tools to help you
determine which of your dependencies is causing the problem.
## Example
Let's create a dummy package, `PkgA`, that launches a persistent `Task`:
```julia
module PkgA
const t = Ref{Timer}() # used to prevent the Timer from being garbage-collected
__init__() = t[] = Timer(0.1; interval=1) # create a persistent `Timer` `Task`
end
```
`PkgA` will precompile successfully, because `PkgA.__init__()` does not
run when `PkgA` is precompiled. However,
```julia
module PkgB
using PkgA
end
```
fails to precompile: `using PkgA` runs `PkgA.__init__()`, which
leaves the `Timer` `Task` running, and that causes precompilation
of `PkgB` to hang.
Without Aqua's tests, the developers of `PkgA` might not realize that their
package is essentially unusable with any other package.
## Checking for persistent tasks
Running all of Aqua's tests will automatically check whether your package falls
into this trap. In addition, there are ways to manually run (or tweak) this
specific test.
### Manually running the persistent-tasks check
[`Aqua.test_persistent_tasks(MyPackage)`](@ref) will check whether `MyPackage` blocks
precompilation for any packages that depend on it.
### Using an `expr` to check more than just `__init__`
By default, `Aqua.test_persistent_tasks` only checks whether a package's
`__init__` function leaves persistent tasks running. To check whether other
package functions leave persistent tasks running, pass a quoted expression:
```julia
Aqua.test_persistent_tasks(MyPackage; expr = quote
# Code to run after loading MyPackage
server = MyPackage.start_server()
MyPackage.stop_server!(server) # ideally, this this should cleanly shut everything down. Does it?
end)
```
Here is an example test with a dummy `expr` which will obviously fail, because it's explicitly
spawning a Task that never dies.
```@repl
using Aqua
Aqua.test_persistent_tasks(Aqua, expr = quote
Threads.@spawn while true sleep(0.5) end
end)
```
## How the test works
This test works by launching a Julia process that tries to precompile a
dummy package similar to `PkgB` above, modified to signal back to Aqua when
`PkgA` has finished loading. The test fails if the gap between loading `PkgA`
and finishing precompilation exceeds time `tmax`.
## How to fix failing packages
Often, the easiest fix is to modify the `__init__` function to check whether the
Julia process is precompiling some other package; if so, don't launch the
persistent `Task`s.
```julia
function __init__()
# Other setup code here
if ccall(:jl_generating_output, Cint, ()) == 0 # if we're not precompiling...
# launch persistent tasks here
end
end
```
In more complex cases, you may need to modify the task to support a clean
shutdown. For example, if you have a `Task` that runs a never-terminating
`while` loop, you could change
```
while true
⋮
end
```
to
```
while task_should_run[]
⋮
end
```
where
```
const task_should_run = Ref(true)
```
is a global constant in your module. Setting `task_should_run[] = false` from
outside that `while` loop will cause it to terminate on its next iteration,
allowing the `Task` to finish.
## Additional information
[Julia's devdocs](https://docs.julialang.org/en/v1/devdocs/precompile_hang/)
also discuss this issue.
## [Test functions](@id test_persistent_tasks)
```@docs
Aqua.test_persistent_tasks
Aqua.find_persistent_tasks_deps
```
================================================
FILE: docs/src/piracies.md
================================================
# Type piracy
Type piracy is a term used to describe adding methods to a foreign function
with only foreign arguments.
This is considered bad practice because it can cause unexpected behavior
when the function is called, in particular, it can change the behavior of
one of your dependencies depending on if your package is loaded or not.
This makes it hard to reason about the behavior of your code, and may
introduce bugs that are hard to track down.
See [Julia documentation](https://docs.julialang.org/en/v1/manual/style-guide/#Avoid-type-piracy) for more information about type piracy.
## Examples
Say that `PkgA` is foreign, and let's look at the different ways that `PkgB` extends its function `bar`.
```julia
module PkgA
struct C end
bar(x::C) = 42
bar(x::Vector) = 43
end
module PkgB
import PkgA: bar, C
struct D end
bar(x::C) = 1
bar(xs::D...) = 2
bar(x::Vector{<:D}) = 3
bar(x::Vector{D}) = 4 # slightly bad (may cause invalidations)
bar(x::Union{C,D}) = 5 # slightly bad (a change in PkgA may turn it into piracy)
# (for example changing bar(x::C) = 1 to bar(x::Union{C,Int}) = 1)
end
```
The following cases are enumerated by the return values in the example above:
1. This is the worst case of type piracy. The value of `bar(C())` can be
either `1` or `42` and will depend on whether `PkgB` is loaded or not.
2. This is also a bad case of type piracy. `bar()` throws a `MethodError` with
only `PkgA` available, and returns `2` with `PkgB` loaded. `PkgA` may add
a method for `bar()` that takes no arguments in the future, and then this
is equivalent to case 1.
3. This is a moderately bad case of type piracy. `bar(Union{}[])` returns `3`
when `PkgB` is loaded, and `43` when `PkgB` is not loaded, although neither
of the occurring types are defined in `PkgB`. This case is not as bad as
cases 1 and 2, because it is only about behavior around `Union{}`, which has
no instances.
4. Depending on ones understanding of type piracy, this could be considered piracy
as well. In particular, this may cause invalidations.
5. This is a slightly bad case of type piracy. In the current form, `bar(C())`
returns `42` as the dispatch on `Union{C,D}` is less specific. However, a
future change in `PkgA` may change this behavior, e.g. by changing `bar(x::C)`
to `bar(x::Union{C,Int})` the call `bar(C())` would become ambiguous.
!!! note
The test function below currently only checks for cases 1 and 2.
## [Test function](@id test_piracies)
```@docs
Aqua.test_piracies
```
================================================
FILE: docs/src/project_extras.md
================================================
# Project.toml extras
There are two different ways to specify test-only dependencies (see [the Pkg docs](https://julialang.github.io/Pkg.jl/v1/creating-packages/#Test-specific-dependencies)):
1. Add the test-only dependencies to the `[extras]` section of your `Project.toml` file
and use a test target.
2. Add the test-only dependencies to the `[deps]` section of your `test/Project.toml` file.
This is only available in Julia 1.2 and later.
This test checks checks that, in case you use both methods, the set of test-only dependencies
is the same in both ways.
## [Test function](@id test_project_extras)
```@docs
Aqua.test_project_extras
```
================================================
FILE: docs/src/stale_deps.md
================================================
# Stale dependencies
## [Test function](@id test_stale_deps)
```@docs
Aqua.test_stale_deps
```
================================================
FILE: docs/src/test_all.md
================================================
# [Test everything](@id test_all)
This test runs most of the other tests in this module.
The defaults should be fine for most packages.
If you have a package that needs to customize the test, you can do so by providing appropriate keyword arguments to `Aqua.test_all()` (see below)
```@docs
Aqua.test_all
```
================================================
FILE: docs/src/unbound_args.md
================================================
# Unbound Type Parameters
An unbound type parameter is a type parameter with a `where`,
that does not occur in the signature of some dispatch of the method.
## Examples
The following methods each have `T` as an unbound type parameter:
```@repl unbound
f(x::Int) where {T} = do_something(x)
g(x::T...) where {T} = println(T)
```
In the cases of `f` above, the unbound type parameter `T` is neither
present in the signature of the methods nor as a bound of another type parameter.
Here, the type parameter `T` can be removed without changing any semantics.
For signatures with `Vararg` (cf. `g` above), the type parameter is unbound for the
zero-argument case (e.g. `g()`).
```@repl unbound
g(1.0, 2.0)
g(1)
g()
```
A possible fix would be to replace `g` by two methods.
```@repl unbound2
g() = println(Int) # Defaults to `Int`
g(x1::T, x2::T...) where {T} = println(T)
g(1.0, 2.0)
g(1)
g()
```
## [Test function](@id test_unbound_args)
```@docs
Aqua.test_unbound_args
```
================================================
FILE: docs/src/undocumented_names.md
================================================
# Undocumented names
## [Test function](@id test_undocumented_names)
```@docs
Aqua.test_undocumented_names
```
================================================
FILE: src/Aqua.jl
================================================
module Aqua
using Base: Docs, PkgId, UUID
using Pkg: Pkg, TOML, PackageSpec
using Pkg.Types: VersionSpec, semver_spec
using Test
include("utils.jl")
include("ambiguities.jl")
include("unbound_args.jl")
include("exports.jl")
include("project_extras.jl")
include("stale_deps.jl")
include("deps_compat.jl")
include("piracies.jl")
include("persistent_tasks.jl")
include("undocumented_names.jl")
"""
test_all(testtarget::Module)
Run the following tests on the module `testtarget`:
* [`test_ambiguities([testtarget])`](@ref test_ambiguities)
* [`test_unbound_args(testtarget)`](@ref test_unbound_args)
* [`test_undefined_exports(testtarget)`](@ref test_undefined_exports)
* [`test_project_extras(testtarget)`](@ref test_project_extras)
* [`test_stale_deps(testtarget)`](@ref test_stale_deps)
* [`test_deps_compat(testtarget)`](@ref test_deps_compat)
* [`test_piracies(testtarget)`](@ref test_piracies)
* [`test_persistent_tasks(testtarget)`](@ref test_persistent_tasks)
* [`test_undocumented_names(testtarget)`](@ref test_undocumented_names)
The keyword argument `\$x` (e.g., `ambiguities`) can be used to
control whether or not to run `test_\$x` (e.g., `test_ambiguities`).
If `test_\$x` supports keyword arguments, a `NamedTuple` can also be
passed to `\$x` to specify the keyword arguments for `test_\$x`.
# Keyword Arguments
- `ambiguities = true`
- `unbound_args = true`
- `undefined_exports = true`
- `project_extras = true`
- `stale_deps = true`
- `deps_compat = true`
- `piracies = true`
- `persistent_tasks = true`
- `undocumented_names = false`
"""
function test_all(
testtarget::Module;
ambiguities = true,
unbound_args = true,
undefined_exports = true,
project_extras = true,
stale_deps = true,
deps_compat = true,
piracies = true,
persistent_tasks = true,
undocumented_names = false,
)
if ambiguities !== false
@testset "Method ambiguity" begin
test_ambiguities([testtarget]; askwargs(ambiguities)...)
end
end
if unbound_args !== false
@testset "Unbound type parameters" begin
test_unbound_args(testtarget; askwargs(unbound_args)...)
end
end
if undefined_exports !== false
@testset "Undefined exports" begin
test_undefined_exports(testtarget; askwargs(undefined_exports)...)
end
end
if project_extras !== false
@testset "Compare Project.toml and test/Project.toml" begin
isempty(askwargs(project_extras)) || error("Keyword arguments not supported")
test_project_extras(testtarget)
end
end
if stale_deps !== false
@testset "Stale dependencies" begin
test_stale_deps(testtarget; askwargs(stale_deps)...)
end
end
if deps_compat !== false
@testset "Compat bounds" begin
test_deps_compat(testtarget; askwargs(deps_compat)...)
end
end
if piracies !== false
@testset "Piracy" begin
test_piracies(testtarget; askwargs(piracies)...)
end
end
if persistent_tasks !== false
@testset "Persistent tasks" begin
test_persistent_tasks(testtarget; askwargs(persistent_tasks)...)
end
end
if undocumented_names !== false
@testset "Undocumented names" begin
isempty(askwargs(undocumented_names)) ||
error("Keyword arguments not supported")
test_undocumented_names(testtarget; askwargs(undocumented_names)...)
end
end
end
include("precompile.jl")
end # module
================================================
FILE: src/ambiguities.jl
================================================
"""
test_ambiguities(package::Union{Module, PkgId})
test_ambiguities(packages::Vector{Union{Module, PkgId}})
Test that there is no method ambiguities in given package(s).
It calls `Test.detect_ambiguities` in a separated clean process to avoid
false-positives.
# Keyword Arguments
- `broken::Bool = false`: If true, it uses `@test_broken` instead of
`@test` and shortens the error message.
- `color::Union{Bool, Nothing} = nothing`: Enable/disable colorful
output if a `Bool`. `nothing` (default) means to inherit the
setting in the current process.
- `exclude::AbstractVector = []`: A vector of functions or types to be
excluded from ambiguity testing. A function means to exclude _all_
its methods. A type means to exclude _all_ its methods of the
callable (sometimes also called "functor") and the constructor.
That is to say, `MyModule.MyType` means to ignore ambiguities between
`(::MyType)(x, y::Int)` and `(::MyType)(x::Int, y)`.
- `recursive::Bool = true`: Passed to `Test.detect_ambiguities`.
Note that the default here (`true`) is different from
`detect_ambiguities`. This is for testing ambiguities in methods
defined in all sub-modules.
- Other keyword arguments such as `imported` and `ambiguous_bottom`
are passed to `Test.detect_ambiguities` as-is.
"""
test_ambiguities(packages; kwargs...) = _test_ambiguities(aspkgids(packages); kwargs...)
const ExcludeSpec = Pair{Base.PkgId,String}
rootmodule(x::Type) = rootmodule(parentmodule(x))
rootmodule(m::Module) = Base.require(PkgId(m)) # this handles Base/Core well
# get the Type associated with x
normalize_exclude_obj(@nospecialize x) = x isa Type ? x : typeof(x)
function normalize_exclude(@nospecialize x)
x = normalize_exclude_obj(x)
return Base.PkgId(rootmodule(x)) => join((fullname(parentmodule(x))..., string(nameof(x))), ".")
end
function getexclude((pkgid, name)::ExcludeSpec)
nameparts = Symbol.(split(name, "."))
m = Base.require(pkgid)
for name in nameparts
m = getproperty(m, name)
end
return normalize_exclude_obj(m)
end
function normalize_and_check_exclude(exclude::AbstractVector)
exspecs = ExcludeSpec[normalize_exclude(exspec) for exspec in exclude]
for (i, exspec) in enumerate(exspecs)
if getexclude(exspec) != normalize_exclude_obj(exclude[i])
error("Name `$(exspec[2])` is resolved to a different object.")
end
end
return exspecs::Vector{ExcludeSpec}
end
function reprexclude(exspecs::Vector{ExcludeSpec})
itemreprs = map(exspecs) do (pkgid, name)
string("(", reprpkgid(pkgid), " => ", repr(name), ")")
end
return string("Aqua.ExcludeSpec[", join(itemreprs, ", "), "]")
end
function _test_ambiguities(packages::Vector{PkgId}; broken::Bool = false, kwargs...)
num_ambiguities, strout, strerr =
_find_ambiguities(packages; skipdetails = broken, kwargs...)
print(stderr, strerr)
print(stdout, strout)
if broken
@test_broken iszero(num_ambiguities)
else
@test iszero(num_ambiguities)
end
end
function _find_ambiguities(
packages::Vector{PkgId};
skipdetails::Bool = false,
color::Union{Bool,Nothing} = nothing,
exclude::AbstractVector = [],
# Options to be passed to `Test.detect_ambiguities`:
detect_ambiguities_options...,
)
packages_repr = reprpkgids(collect(packages))
options_repr = checked_repr((; recursive = true, detect_ambiguities_options...))
exclude_repr = reprexclude(normalize_and_check_exclude(exclude))
# Ambiguity test is run inside a clean process.
# https://github.com/JuliaLang/julia/issues/28804
code = """
$(Base.load_path_setup_code())
using Aqua
Aqua.test_ambiguities_impl(
$packages_repr,
$options_repr,
$exclude_repr,
$skipdetails,
) || exit(1)
"""
cmd = Base.julia_cmd()
if something(color, Base.JLOptions().color == 1)
cmd = `$cmd --color=yes`
end
cmd = `$cmd --startup-file=no -e $code`
mktemp() do outfile, out
mktemp() do errfile, err
succ = success(pipeline(cmd; stdout = out, stderr = err))
strout = read(outfile, String)
strerr = read(errfile, String)
num_ambiguities = if succ
0
else
reg_match = match(r"(\d+) ambiguities found", strerr)
reg_match === nothing && error(
"Failed to parse output of `detect_ambiguities`.\nThe stdout was:\n" *
strout *
"\n\nThe stderr was:\n" *
strerr,
)
parse(Int, reg_match.captures[1]::AbstractString)
end
return num_ambiguities, strout, strerr
end
end
end
function reprpkgids(packages::Vector{PkgId})
packages_repr = sprint() do io
println(io, '[')
for pkg in packages
println(io, reprpkgid(pkg))
end
println(io, ']')
end
@assert Base.eval(Main, Meta.parse(packages_repr)) == packages
return packages_repr
end
function reprpkgid(pkg::PkgId)
name = pkg.name
uuid = pkg.uuid
if uuid === nothing
return "Base.PkgId($(repr(name)))"
end
return "Base.PkgId(Base.UUID($(repr(uuid.value))), $(repr(name)))"
end
# try to extract the called function, or nothing if it is hard to analyze
function trygetft(m::Method)
sig = Base.unwrap_unionall(m.sig)::DataType
ft = sig.parameters[is_kwcall(sig) ? 3 : 1]
ft = Base.unwrap_unionall(ft)
if ft isa DataType && ft.name === Type.body.name
ft = Base.unwrap_unionall(ft.parameters[1])
end
if ft isa DataType
return ft.name.wrapper
end
return nothing # cannot exclude signatures with Union
end
function test_ambiguities_impl(
packages::Vector{PkgId},
options::NamedTuple,
exspecs::Vector{ExcludeSpec},
skipdetails::Bool,
)
modules = map(Base.require, packages)
@debug "Testing method ambiguities" modules
ambiguities = detect_ambiguities(modules...; options...)
if !isempty(exspecs)
exclude_ft = Any[getexclude(spec) for spec in exspecs] # vector of Type objects
ambiguities = filter(ambiguities) do (m1, m2)
trygetft(m1) ∉ exclude_ft && trygetft(m2) ∉ exclude_ft
end
end
sort!(ambiguities, by = (ms -> (ms[1].name, ms[2].name)))
if !isempty(ambiguities)
printstyled(
stderr,
"$(length(ambiguities)) ambiguities found. To get a list, set `broken = false`.\n";
bold = true,
color = Base.error_color(),
)
end
if !skipdetails
for (i, (m1, m2)) in enumerate(ambiguities)
println(stderr, "Ambiguity #", i)
println(stderr, m1)
println(stderr, m2)
@static if isdefined(Base, :morespecific)
ambiguity_hint(stderr, m1, m2)
println(stderr)
end
println(stderr)
end
end
return isempty(ambiguities)
end
function ambiguity_hint(io::IO, m1::Method, m2::Method)
# based on base/errorshow.jl#showerror_ambiguous
# https://github.com/JuliaLang/julia/blob/v1.7.2/base/errorshow.jl#L327-L353
sigfix = Any
sigfix = typeintersect(m1.sig, sigfix)
sigfix = typeintersect(m2.sig, sigfix)
if isa(Base.unwrap_unionall(sigfix), DataType) && sigfix <: Tuple
let sigfix = sigfix
if all(m -> Base.morespecific(sigfix, m.sig), [m1, m2])
print(io, "\nPossible fix, define\n ")
# Use `invokelatest` to not throw because of world age problems due to new types.
invokelatest(Base.show_tuple_as_call, io, :function, sigfix)
else
println(io)
print(
io,
"""To resolve the ambiguity, try making one of the methods more specific, or
adding a new method more specific than any of the existing applicable methods.""",
)
end
end
end
end
================================================
FILE: src/deps_compat.jl
================================================
"""
Aqua.test_deps_compat(package)
Test that the `Project.toml` of `package` has a `compat` entry for
each package listed under `deps` and for `julia`.
# Arguments
- `packages`: a top-level `Module`, a `Base.PkgId`, or a collection of
them.
# Keyword Arguments
## Test choosers
- `check_julia = true`: If true, additionally check for a compat entry for "julia".
- `check_extras = true`: If true, additionally check "extras". A NamedTuple
can be used to pass keyword arguments with test options (see below).
- `check_weakdeps = true`: If true, additionally check "weakdeps". A NamedTuple
can be used to pass keyword arguments with test options (see below).
## Test options
If these keyword arguments are set directly, they only apply to the standard test
for "deps". To apply them to "extras" and "weakdeps", pass them as a NamedTuple
to the corresponding `check_\$x` keyword argument.
- `broken::Bool = false`: If true, it uses `@test_broken` instead of
`@test` for "deps".
- `ignore::Vector{Symbol}`: names of dependent packages to be ignored.
"""
function test_deps_compat(
pkg::PkgId;
check_julia = true,
check_extras = true,
check_weakdeps = true,
kwargs...,
)
if check_julia !== false
@testset "julia" begin
isempty(askwargs(check_julia)) || error("Keyword arguments not supported")
test_julia_compat(pkg)
end
end
@testset "$pkg deps" begin
test_deps_compat(pkg, "deps"; kwargs...)
end
if check_extras !== false
@testset "$pkg extras" begin
test_deps_compat(pkg, "extras"; askwargs(check_extras)...)
end
end
if check_weakdeps !== false
@testset "$pkg weakdeps" begin
test_deps_compat(pkg, "weakdeps"; askwargs(check_weakdeps)...)
end
end
end
function test_deps_compat(pkg::PkgId, deps_type::String; broken::Bool = false, kwargs...)
result = find_missing_deps_compat(pkg, deps_type; kwargs...)
if broken
@test_broken isempty(result)
else
@test isempty(result)
end
end
# Remove with next breaking version
function test_deps_compat(packages::Vector{<:Union{Module,PkgId}}; kwargs...)
@testset "$pkg" for pkg in packages
test_deps_compat(pkg; kwargs...)
end
end
function test_deps_compat(mod::Module; kwargs...)
test_deps_compat(aspkgid(mod); kwargs...)
end
function test_julia_compat(pkg::PkgId; broken::Bool = false)
if broken
@test_broken has_julia_compat(pkg)
else
@test has_julia_compat(pkg)
end
end
function has_julia_compat(pkg::PkgId)
root_project_path, found = root_project_toml(pkg)
found || error("Unable to locate Project.toml")
prj = TOML.parsefile(root_project_path)
return has_julia_compat(prj)
end
function has_julia_compat(prj::Dict{String,Any})
return "julia" in keys(get(prj, "compat", Dict{String,Any}()))
end
function find_missing_deps_compat(pkg::PkgId, deps_type::String = "deps"; kwargs...)
root_project_path, found = root_project_toml(pkg)
found || error("Unable to locate Project.toml")
missing_compat =
find_missing_deps_compat(TOML.parsefile(root_project_path), deps_type; kwargs...)
if !isempty(missing_compat)
printstyled(
stderr,
"$pkg does not declare a compat entry for the following $deps_type:\n";
bold = true,
color = Base.error_color(),
)
show(stderr, MIME"text/plain"(), missing_compat)
println(stderr)
end
return missing_compat
end
function find_missing_deps_compat(
prj::Dict{String,Any},
deps_type::String;
ignore::AbstractVector{Symbol} = Symbol[],
)
deps = get(prj, deps_type, Dict{String,Any}())
compat = get(prj, "compat", Dict{String,Any}())
missing_compat = sort!(
PkgId[
d for d in map(d -> PkgId(UUID(last(d)), first(d)), collect(deps)) if
!(d.name in keys(compat)) && !(d.name in String.(ignore))
];
by = (pkg -> pkg.name),
)
return missing_compat
end
================================================
FILE: src/exports.jl
================================================
# avoid Base.isbindingresolved deprecation in https://github.com/JuliaLang/julia/pull/57253
function isbindingresolved(m::Module, s::Symbol)
@static if VERSION >= v"1.12.0-"
return true
else
return Base.isbindingresolved(m, s)
end
end
function walkmodules(f, x::Module)
f(x)
for n in names(x; all = true)
# `isdefined` and `getproperty` can trigger deprecation warnings
if isbindingresolved(x, n) && !Base.isdeprecated(x, n)
isdefined(x, n) || continue
y = getproperty(x, n)
if y isa Module && y !== x && parentmodule(y) === x
walkmodules(f, y)
end
end
end
end
function undefined_exports(m::Module)
undefined = Symbol[]
walkmodules(m) do x
for n in names(x)
isdefined(x, n) || push!(undefined, Symbol(join([fullname(x)...; n], '.')))
end
end
return undefined
end
"""
test_undefined_exports(m::Module; broken::Bool = false)
Test that all `export`ed names in `m` actually exist.
# Keyword Arguments
- `broken`: If true, it uses `@test_broken` instead of
`@test` and shortens the error message.
"""
function test_undefined_exports(m::Module; broken::Bool = false)
exports = undefined_exports(m)
if broken
if !isempty(exports)
printstyled(
stderr,
"$(length(exports)) undefined exports detected. To get a list, set `broken = false`.\n";
bold = true,
color = Base.error_color(),
)
end
@test_broken isempty(exports)
else
if !isempty(exports)
printstyled(
stderr,
"Undefined exports detected:\n";
bold = true,
color = Base.error_color(),
)
show(stderr, MIME"text/plain"(), exports)
println(stderr)
end
@test isempty(exports)
end
end
================================================
FILE: src/persistent_tasks.jl
================================================
"""
Aqua.test_persistent_tasks(package)
Test whether loading `package` creates persistent `Task`s
which may block precompilation of dependent packages.
See also [`Aqua.find_persistent_tasks_deps`](@ref).
If you provide an optional `expr`, this tests whether loading `package` and running `expr`
creates persistent `Task`s. For example, you might start and shutdown a web server, and
this will test that there aren't any persistent `Task`s.
On Julia version 1.9 and before, this test always succeeds.
# Arguments
- `package`: a top-level `Module` or `Base.PkgId`.
# Keyword Arguments
- `broken::Bool = false`: If true, it uses `@test_broken` instead of
`@test`.
- `tmax::Real = 5`: the maximum time (in seconds) to wait after loading the
package before forcibly shutting down the precompilation process (triggering
a test failure).
- `expr::Expr = quote end`: An expression to run in the precompile package.
!!! note
`Aqua.test_persistent_tasks(package)` creates a package with `package`
as a dependency and runs the precompilation process.
This requires that `package` is instantiable with the information in the
`Project.toml` file alone.
In particular, this will not work if some of `package`'s dependencies are `dev`ed
packages or are given as a local path or a git repository in the `Manifest.toml`.
"""
function test_persistent_tasks(package::PkgId; broken::Bool = false, kwargs...)
if broken
@test_broken !has_persistent_tasks(package; kwargs...)
else
@test !has_persistent_tasks(package; kwargs...)
end
end
function test_persistent_tasks(package::Module; kwargs...)
test_persistent_tasks(PkgId(package); kwargs...)
end
function has_persistent_tasks(package::PkgId; expr::Expr = quote end, tmax = 10)
root_project_path, found = root_project_toml(package)
found || error("Unable to locate Project.toml")
return !precompile_wrapper(root_project_path, tmax, expr)
end
"""
Aqua.find_persistent_tasks_deps(package; kwargs...)
Test all the dependencies of `package` with [`Aqua.test_persistent_tasks`](@ref).
On Julia 1.10 and higher, it returns a list of all dependencies failing the test.
These are likely the ones blocking precompilation of your package.
Any `kwargs` are passed to [`Aqua.test_persistent_tasks`](@ref).
"""
function find_persistent_tasks_deps(package::PkgId; kwargs...)
root_project_path, found = root_project_toml(package)
found || error("Unable to locate Project.toml")
prj = TOML.parsefile(root_project_path)
deps = get(prj, "deps", Dict{String,Any}())
filter!(deps) do (name, uuid)
id = PkgId(UUID(uuid), name)
return has_persistent_tasks(id; kwargs...)
end
return String[name for (name, _) in deps]
end
function find_persistent_tasks_deps(package::Module; kwargs...)
find_persistent_tasks_deps(PkgId(package); kwargs...)
end
function precompile_wrapper(project, tmax, expr)
@static if VERSION < v"1.10.0-"
return true
end
prev_project = Base.active_project()::String
isdefined(Pkg, :respect_sysimage_versions) && Pkg.respect_sysimage_versions(false)
try
pkgdir = dirname(project)
pkgname = get(TOML.parsefile(project), "name", "")::String
if isempty(pkgname)
@error "Unable to locate package name in $project"
return false
end
wrapperdir = tempname()
wrappername, _ = only(Pkg.generate(wrapperdir; io = devnull))
Pkg.activate(wrapperdir; io = devnull)
Pkg.develop(PackageSpec(path = pkgdir); io = devnull)
statusfile = joinpath(wrapperdir, "done.log")
open(joinpath(wrapperdir, "src", wrappername * ".jl"), "w") do io
println(
io,
"""
module $wrappername
using $pkgname
$expr
# Signal Aqua from the precompilation process that we've finished loading the package
open("$(escape_string(statusfile))", "w") do io
println(io, "done")
flush(io)
end
end
""",
)
end
# Precompile the wrapper package
currently_precompiling = @ccall(jl_generating_output()::Cint) == 1
cmd = if currently_precompiling
# During precompilation we run a dummy command that just touches the
# status file to keep things simple.
code = """touch("$(escape_string(statusfile))")"""
`$(Base.julia_cmd()) -e $code`
else
`$(Base.julia_cmd()) --project=$wrapperdir -e 'push!(LOAD_PATH, "@stdlib"); using Pkg; Pkg.precompile(; io = devnull)'`
end
cmd = pipeline(cmd; stdout, stderr)
proc = run(cmd; wait = false)::Base.Process
while !isfile(statusfile) && process_running(proc)
sleep(0.5)
end
if !isfile(statusfile)
@error "Unexpected error: $statusfile was not created, but precompilation exited"
return false
end
# Check whether precompilation finishes in the required time
t = time()
while process_running(proc) && time() - t < tmax
sleep(0.1)
end
success = !process_running(proc)
if !success
# SIGKILL to prevent julia from printing the SIG 15 handler, which can
# misleadingly look like it's caused by an issue in the user's program.
kill(proc, Base.SIGKILL)
end
return success
finally
isdefined(Pkg, :respect_sysimage_versions) && Pkg.respect_sysimage_versions(true)
Pkg.activate(prev_project; io = devnull)
end
end
================================================
FILE: src/piracies.jl
================================================
module Piracy
using ..Aqua: is_kwcall
using Test: is_in_mods
# based on Test/Test.jl#detect_ambiguities
# https://github.com/JuliaLang/julia/blob/v1.9.1/stdlib/Test/src/Test.jl#L1838-L1896
function all_methods(mods::Module...; skip_deprecated::Bool = true)
meths = Method[]
mods = collect(mods)::Vector{Module}
function examine_def(m::Method)
is_in_mods(m.module, true, mods) && push!(meths, m)
nothing
end
examine(mt::Core.MethodTable) = Base.visit(examine_def, mt)
if VERSION >= v"1.12-" && isdefined(Core, :methodtable)
examine(Core.methodtable)
elseif VERSION >= v"1.12-" && isdefined(Core, :GlobalMethods)
# for all versions between JuliaLang/julia#58131 and JuliaLang/julia#59158
# so just some 1.12 rcs and 1.13 DEVs
examine(Core.GlobalMethods)
else
work = Base.loaded_modules_array()
filter!(mod -> mod === parentmodule(mod), work) # some items in loaded_modules_array are not top modules (really just Base)
while !isempty(work)
mod = pop!(work)
for name in names(mod; all = true)
(skip_deprecated && Base.isdeprecated(mod, name)) && continue
isdefined(mod, name) || continue
f = Base.unwrap_unionall(getfield(mod, name))
if isa(f, Module) && f !== mod && parentmodule(f) === mod && nameof(f) === name
push!(work, f)
elseif isa(f, DataType) &&
isdefined(f.name, :mt) &&
parentmodule(f) === mod &&
nameof(f) === name &&
f.name.mt !== Symbol.name.mt &&
f.name.mt !== DataType.name.mt
examine(f.name.mt)
end
end
end
examine(Symbol.name.mt)
examine(DataType.name.mt)
end
return meths
end
##################################
# Generic fallback for type parameters that are instances, like the 1 in
# Array{T, 1}
is_foreign(@nospecialize(x), pkg::Base.PkgId; treat_as_own) =
is_foreign(typeof(x), pkg; treat_as_own = treat_as_own)
# Symbols can be used as type params - we assume these are unique and not
# piracy. This implies that we have
#
# julia> Aqua.Piracy.is_foreign(1, Base.PkgId(Aqua))
# true
#
# julia> Aqua.Piracy.is_foreign(:hello, Base.PkgId(Aqua))
# false
#
# and thus
#
# julia> Aqua.Piracy.is_foreign(Val{1}, Base.PkgId(Aqua))
# true
#
# julia> Aqua.Piracy.is_foreign(Val{:hello}, Base.PkgId(Aqua))
# false
#
# Admittedly, this asymmetry is rather worrisome. We do need to treat 1 foreign
# to consider `Vector{Char}` (i.e., `Array{Char,1}`) foreign. This may suggest
# to treat the `Symbol` type foreign as well. However, it means that we treat
# definition such as
#
# ForeignModule.api_function(::Val{:MyPackageName}) = ...
#
# as a type piracy even if this is actually the intended use-case (which is not
# a crazy API). The symbol name may also come from `gensym`. Since the aim of
# `Aqua.test_piracies` is to detect only "obvious" piracies, let us play on the
# safe side.
is_foreign(x::Symbol, pkg::Base.PkgId; treat_as_own) = false
is_foreign_module(mod::Module, pkg::Base.PkgId) = Base.PkgId(mod) != pkg
function is_foreign(@nospecialize(T::DataType), pkg::Base.PkgId; treat_as_own)
params = T.parameters
# For Type{Foo}, we consider it to originate from the same as Foo
C = getfield(parentmodule(T), nameof(T))
if C === Type
@assert length(params) == 1
return is_foreign(first(params), pkg; treat_as_own = treat_as_own)
else
# Both the type itself and all of its parameters must be foreign
return !((C in treat_as_own)::Bool) &&
is_foreign_module(parentmodule(T), pkg) &&
all(param -> is_foreign(param, pkg; treat_as_own = treat_as_own), params)
end
end
function is_foreign(@nospecialize(U::UnionAll), pkg::Base.PkgId; treat_as_own)
# We do not consider extending Set{T} to be piracies, if T is not foreign.
# Extending it goes against Julia style, but it's not piracies IIUC.
is_foreign(U.body, pkg; treat_as_own = treat_as_own) &&
is_foreign(U.var, pkg; treat_as_own = treat_as_own)
end
is_foreign(@nospecialize(T::TypeVar), pkg::Base.PkgId; treat_as_own) =
is_foreign(T.ub, pkg; treat_as_own = treat_as_own)
# Before 1.7, Vararg was a UnionAll, so the UnionAll method will work
@static if VERSION >= v"1.7-"
is_foreign(@nospecialize(T::Core.TypeofVararg), pkg::Base.PkgId; treat_as_own) =
is_foreign(T.T, pkg; treat_as_own = treat_as_own)
end
function is_foreign(@nospecialize(U::Union), pkg::Base.PkgId; treat_as_own)
# Even if Foo is local, overloading f(::Union{Foo, Int}) with foreign f is piracy.
any(T -> is_foreign(T, pkg; treat_as_own = treat_as_own), Base.uniontypes(U))
end
function is_foreign_method(@nospecialize(U::Union), pkg::Base.PkgId; treat_as_own)
# When installing a method for a union type, then we only consider it as
# foreign if *all* parameters of the union are foreign, i.e. overloading
# Union{Foo, Int}() is not piracy.
all(T -> is_foreign(T, pkg; treat_as_own = treat_as_own), Base.uniontypes(U))
end
function is_foreign_method(@nospecialize(x::Any), pkg::Base.PkgId; treat_as_own)
is_foreign(x, pkg; treat_as_own = treat_as_own)
end
function is_foreign_method(@nospecialize(T::DataType), pkg::Base.PkgId; treat_as_own)
params = T.parameters
# For Type{Foo}, we consider it to originate from the same as Foo
C = getfield(parentmodule(T), nameof(T))
if C === Type
@assert length(params) == 1
return is_foreign_method(first(params), pkg; treat_as_own = treat_as_own)
end
# fallback to general code
return !((T in treat_as_own)::Bool) &&
!(
T <: Function &&
isdefined(T, :instance) &&
(T.instance in treat_as_own)::Bool
) &&
is_foreign(T, pkg; treat_as_own = treat_as_own)
end
function is_pirate(meth::Method; treat_as_own = Union{Function,Type}[])
method_pkg = Base.PkgId(meth.module)
signature = Base.unwrap_unionall(meth.sig)
function_type_index = 1
if is_kwcall(meth.sig)
# kwcall is a special case, since it is not a real function
# but a wrapper around a function, the third parameter is the original
# function, its positional arguments follow.
function_type_index += 2
end
# the first parameter in the signature is the function type, and it
# follows slightly other rules if it happens to be a Union type
is_foreign_method(
signature.parameters[function_type_index],
method_pkg;
treat_as_own = treat_as_own,
) || return false
return all(
param -> is_foreign(param, method_pkg; treat_as_own = treat_as_own),
signature.parameters[function_type_index+1:end],
)
end
function hunt(mod::Module; skip_deprecated::Bool = true, kwargs...)
piracies = filter(all_methods(mod; skip_deprecated = skip_deprecated)) do method
method.module === mod && is_pirate(method; kwargs...)
end
sort!(piracies, by = (m -> m.name))
return piracies
end
end # module
"""
test_piracies(m::Module)
Test that `m` does not commit type piracies.
# Keyword Arguments
- `broken::Bool = false`: If true, it uses `@test_broken` instead of
`@test` and shortens the error message.
- `skip_deprecated::Bool = true`: If true, it does not check deprecated methods.
- `treat_as_own = Union{Function, Type}[]`: The types in this container
are considered to be "owned" by the module `m`. This is useful for
testing packages that deliberately commit some type piracies, e.g. modules
adding higher-level functionality to a lightweight C-wrapper, or packages
that are extending `StatsAPI.jl`.
"""
function test_piracies(m::Module; broken::Bool = false, kwargs...)
v = Piracy.hunt(m; kwargs...)
if broken
if !isempty(v)
printstyled(
stderr,
"$(length(v)) instances of possible type-piracy detected. To get a list, set `broken = false`.\n";
bold = true,
color = Base.error_color(),
)
end
@test_broken isempty(v)
else
if !isempty(v)
printstyled(
stderr,
"Possible type-piracy detected:\n";
bold = true,
color = Base.error_color(),
)
show(stderr, MIME"text/plain"(), v)
println(stderr)
end
@test isempty(v)
end
end
================================================
FILE: src/precompile.jl
================================================
using PrecompileTools: @compile_workload
# Create a minimal fake package to test. Needs to be at the top-level because
# it's a module.
module _FakePackage
export fake_function
fake_function() = 1
end
@compile_workload begin
redirect_stdout(devnull) do
test_all(
_FakePackage;
ambiguities = false,
project_extras = false,
stale_deps = false,
deps_compat = false,
persistent_tasks = false,
)
end
# Explicitly precompile the tests that need a real package module
precompile(test_ambiguities, (Vector{Module},))
precompile(test_project_extras, (Module,))
precompile(test_stale_deps, (Module,))
precompile(test_deps_compat, (Module,))
# Create a fake package directory for testing persistent_tasks. We go to
# some effort to precompile this because it takes the longest due to Pkg
# calls and running precompilation in a subprocess.
mktempdir() do dir
project_file = joinpath(dir, "Project.toml")
write(
project_file,
"""
name = "AquaFakePackage"
uuid = "5a23b2e7-8c45-4b1c-9d3f-7a6b4c8d9e0f"
version = "0.1.0"
[deps]
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
[compat]
Test = "1"
julia = "1"
""",
)
srcdir = joinpath(dir, "src")
mkdir(srcdir)
write(joinpath(srcdir, "AquaFakePackage.jl"), "module AquaFakePackage end")
# The meat of the compilation latency comes from this function. Running
# test_persistent_tasks() directly is more difficult because it takes a
# Module and then gets the package directory, and we don't want to load
# the fake package.
precompile_wrapper(project_file, 10, quote end)
end
end
================================================
FILE: src/project_extras.jl
================================================
"""
test_project_extras(package::Union{Module, PkgId})
test_project_extras(packages::Vector{Union{Module, PkgId}})
Check that test target of the root project and test project
(`test/Project.toml`) are consistent.
This is useful for supporting Julia < 1.2 while recording test-only dependency
compatibility in `test/Project.toml`.
"""
function test_project_extras(pkg::PkgId; kwargs...)
msgs = analyze_project_extras(pkg; kwargs...)
@test isempty(msgs)
end
# Remove with next breaking version
function test_project_extras(packages::Vector{<:Union{Module,PkgId}}; kwargs...)
@testset "$pkg" for pkg in packages
test_project_extras(pkg; kwargs...)
end
end
function test_project_extras(mod::Module; kwargs...)
test_project_extras(aspkgid(mod); kwargs...)
end
is_julia12_or_later(compat::AbstractString) = is_julia12_or_later(semver_spec(compat))
is_julia12_or_later(compat::VersionSpec) = isempty(compat ∩ semver_spec("1.0 - 1.1"))
function analyze_project_extras(pkg::PkgId)
root_project_path, found = root_project_toml(pkg)
found || error("Unable to locate Project.toml")
test_project_path, found =
project_toml_path(joinpath(dirname(root_project_path), "test"))
found || return String[] # having no test/Project.toml is fine
root_project = TOML.parsefile(root_project_path)
test_project = TOML.parsefile(test_project_path)
# Ignore root project's extras if only supporting julia 1.2 or later.
# See: # https://julialang.github.io/Pkg.jl/v1/creating-packages/#Test-specific-dependencies-in-Julia-1.2-and-above-1
julia_version = get(get(root_project, "compat", Dict{String,Any}()), "julia", nothing)
isnothing(julia_version) && return String["Could not find `julia` compat."]
is_julia12_or_later(julia_version) && return String[]
# `extras_test_deps`: test-only dependencies according to Project.toml
deps =
PkgId[PkgId(UUID(v), k) for (k, v) in get(root_project, "deps", Dict{String,Any}())]
target =
Set{String}(get(get(root_project, "targets", Dict{String,Any}()), "test", String[]))
extras_test_deps = setdiff(
PkgId[
PkgId(UUID(v), k) for
(k, v) in get(root_project, "extras", Dict{String,Any}()) if k in target
],
deps,
)
# `test_deps`: test-only dependencies according to test/Project.toml:
test_deps = setdiff(
PkgId[
PkgId(UUID(v), k) for (k, v) in get(test_project, "deps", Dict{String,Any}())
],
deps,
[PkgId(UUID(root_project["uuid"]), root_project["name"])],
)
not_in_extras = setdiff(test_deps, extras_test_deps)
not_in_test = setdiff(extras_test_deps, test_deps)
if isempty(not_in_extras) && isempty(not_in_test)
return String[]
else
msgs = String[]
push!(
msgs,
"Root and test projects should be consistent for projects supporting Julia <= 1.1.",
)
if !isempty(not_in_extras)
msg = "Test dependencies not in root project ($root_project_path):"
for pkg in sort!(collect(not_in_extras); by = (pkg -> pkg.name))
msg *= "\n\t$(pkg.name) [$(pkg.uuid)]"
end
push!(msgs, msg)
end
if !isempty(not_in_test)
msg = "Dependencies not in test project ($test_project_path):"
for pkg in sort!(collect(not_in_test); by = (pkg -> pkg.name))
msg *= "\n\t$(pkg.name) [$(pkg.uuid)]"
end
push!(msgs, msg)
end
return msgs
end
end
================================================
FILE: src/stale_deps.jl
================================================
"""
Aqua.test_stale_deps(package; ignore::AbstractVector{Symbol} = Symbol[])
Test that `package` loads all dependencies listed in `Project.toml`.
Note that this does not imply that `package` loads the dependencies
directly, this can be achieved via transitivity as well.
!!! note "Weak dependencies and extensions"
Due to the automatic loading of package extensions once all of
their trigger dependencies are loaded, Aqua.jl can, by design of julia,
not check if a package extension indeed loads all of its trigger
dependencies using `import` or `using`.
!!! warning "Known bug"
Currently, `Aqua.test_stale_deps` does not detect stale
dependencies when they are in the sysimage. This is considered a
bug and may be fixed in the future. Such a release is considered
non-breaking.
# Arguments
- `packages`: a top-level `Module`, a `Base.PkgId`, or a collection of
them.
# Keyword Arguments
- `ignore`: names of dependent packages to be ignored.
"""
function test_stale_deps(pkg::PkgId; kwargs...)
stale_deps = find_stale_deps(pkg; kwargs...)
@test isempty(stale_deps)
end
function test_stale_deps(mod::Module; kwargs...)
test_stale_deps(aspkgid(mod); kwargs...)
end
# Remove in next breaking release
function test_stale_deps(packages::Vector{<:Union{Module,PkgId}}; kwargs...)
@testset "$pkg" for pkg in packages
test_stale_deps(pkg; kwargs...)
end
end
function find_stale_deps(pkg::PkgId; ignore::AbstractVector{Symbol} = Symbol[])
root_project_path, found = root_project_toml(pkg)
found || error("Unable to locate Project.toml")
prj = TOML.parsefile(root_project_path)
deps::Vector{PkgId} =
PkgId[PkgId(UUID(v), k) for (k::String, v::String) in get(prj, "deps", Dict())]
weakdeps::Vector{PkgId} =
PkgId[PkgId(UUID(v), k) for (k::String, v::String) in get(prj, "weakdeps", Dict())]
marker = "_START_MARKER_"
code = """
$(Base.load_path_setup_code())
Base.require($(reprpkgid(pkg)))
print("$marker")
for pkg in keys(Base.loaded_modules)
pkg.uuid === nothing || println(pkg.uuid)
end
"""
cmd = Base.julia_cmd()
output = read(`$cmd --startup-file=no --color=no -e $code`, String)
pos = findfirst(marker, output)
@assert !isnothing(pos)
output = output[pos.stop+1:end]
loaded_uuids = map(UUID, eachline(IOBuffer(output)))
return find_stale_deps_2(;
deps = deps,
weakdeps = weakdeps,
loaded_uuids = loaded_uuids,
ignore = ignore,
)
end
# Side-effect -free part of stale dependency analysis.
function find_stale_deps_2(;
deps::AbstractVector{PkgId},
weakdeps::AbstractVector{PkgId},
loaded_uuids::AbstractVector{UUID},
ignore::AbstractVector{Symbol},
)
deps_uuids = [p.uuid for p in deps]
pkgid_from_uuid = Dict(p.uuid => p for p in deps)
stale_uuids = setdiff(deps_uuids, loaded_uuids)
stale_pkgs = PkgId[pkgid_from_uuid[uuid] for uuid in stale_uuids]
stale_pkgs = setdiff(stale_pkgs, weakdeps)
stale_pkgs = PkgId[p for p in stale_pkgs if !(Symbol(p.name) in ignore)]
return stale_pkgs
end
================================================
FILE: src/unbound_args.jl
================================================
"""
test_unbound_args(m::Module; broken::Bool = false)
Test that all methods in `m` and its submodules do not have
unbound type parameters.
An unbound type parameter is a type parameter with a `where`, that does not
occur in the signature of some dispatch of the method.
# Keyword Arguments
- `broken`: If true, it uses `@test_broken` instead of
`@test` and shortens the error message.
"""
function test_unbound_args(m::Module; broken::Bool = false)
unbounds = detect_unbound_args_recursively(m)
if broken
if !isempty(unbounds)
printstyled(
stderr,
"$(length(unbounds)) instances of unbound type parameters detected. To get a list, set `broken = false`.\n";
bold = true,
color = Base.error_color(),
)
end
@test_broken isempty(unbounds)
else
if !isempty(unbounds)
printstyled(
stderr,
"Unbound type parameters detected:\n";
bold = true,
color = Base.error_color(),
)
show(stderr, MIME"text/plain"(), unbounds)
println(stderr)
end
@test isempty(unbounds)
end
end
detect_unbound_args_recursively(m) = Test.detect_unbound_args(m; recursive = true)
================================================
FILE: src/undocumented_names.jl
================================================
"""
test_undocumented_names(m::Module; broken::Bool = false)
Test that all public names in `m` and its recursive submodules have a docstring
(not including `m` itself).
!!! tip
On all Julia versions, public names include the exported names.
On Julia versions >= 1.11, public names also include the names annotated with the
`public` keyword.
!!! warning
When running this Aqua test in Julia versions before 1.11, it does nothing.
Thus if you use continuous integration tests, make sure those are configured
to use Julia >= 1.11 in order to benefit from this test.
# Keyword Arguments
- `broken`: If true, it uses `@test_broken` instead of
`@test` and shortens the error message.
"""
function test_undocumented_names(m::Module; broken::Bool = false)
@static if VERSION >= v"1.11"
# exclude the module name itself because it has the README as auto-generated docstring (https://github.com/JuliaLang/julia/pull/39093)
undocumented_names = Symbol[]
walkmodules(m) do x
append!(undocumented_names, Docs.undocumented_names(x))
end
undocumented_names = filter(n -> n != nameof(m), undocumented_names)
if broken
@test_broken isempty(undocumented_names)
else
@test isempty(undocumented_names)
end
else
undocumented_names = Symbol[]
end
if !isempty(undocumented_names)
printstyled(
stderr,
"Undocumented names detected:\n";
bold = true,
color = Base.error_color(),
)
!broken && show(stderr, MIME"text/plain"(), undocumented_names)
println(stderr)
end
end
================================================
FILE: src/utils.jl
================================================
askwargs(kwargs) = (; kwargs...)
function askwargs(flag::Bool)
if !flag
throw(ArgumentError("expect `true`"))
end
return NamedTuple()
end
aspkgids(pkg::Union{Module,PkgId}) = aspkgids([pkg])
aspkgids(packages) = mapfoldl(aspkgid, push!, packages, init = PkgId[])
aspkgid(pkg::PkgId) = pkg
function aspkgid(m::Module)
if !ispackage(m)
error("Non-package (non-toplevel) module is not supported. Got: $m")
end
return PkgId(m)
end
function aspkgid(name::Symbol)
# Maybe `Base.depwarn()`
return Base.identify_package(String(name))::PkgId
end
ispackage(m::Module) =
if m in (Base, Core)
true
else
parentmodule(m) == m
end
function project_toml_path(dir)
candidates = joinpath.(dir, ["Project.toml", "JuliaProject.toml"])
i = findfirst(isfile, candidates)
i === nothing && return candidates[1], false
return candidates[i], true
end
function root_project_toml(pkg::PkgId)
srcpath = Base.locate_package(pkg)
srcpath === nothing && return "", false
pkgpath = dirname(dirname(srcpath))
root_project_path, found = project_toml_path(pkgpath)
return root_project_path, found
end
module _TempModule end
eval_string(code::AbstractString) = include_string(_TempModule, code)
function checked_repr(obj)
code = repr(obj)
if !isequal(eval_string(code), obj)
error("`$repr` is not `repr`-safe")
end
return code
end
function is_kwcall(signature::Type)
@static if VERSION < v"1.9"
signature = Base.unwrap_unionall(signature)::DataType
try
length(signature.parameters) >= 3 || return false
signature <: Tuple{Function,Any,Any,Vararg} || return false
(signature.parameters[3] isa DataType && signature.parameters[3] <: Type) ||
isconcretetype(signature.parameters[3]) ||
return false
return signature.parameters[1] === Core.kwftype(signature.parameters[3])
catch err
@warn "Please open an issue on JuliaTesting/Aqua.jl for \"is_kwcall\" and the following data:" signature err
return false
end
else
return signature <: Tuple{typeof(Core.kwcall), Any, Any, Vararg}
end
end
================================================
FILE: test/pkgs/AquaTesting.jl
================================================
module AquaTesting
using Base: PkgId, UUID
using Pkg
using Test
# Taken from Test/test/runtests.jl
mutable struct NoThrowTestSet <: Test.AbstractTestSet
results::Vector
NoThrowTestSet(desc) = new([])
end
Test.record(ts::NoThrowTestSet, t::Test.Result) = (push!(ts.results, t); t)
Test.finish(ts::NoThrowTestSet) = ts.results
macro testtestset(args...)
@gensym TestSet
expr = quote
$TestSet = $NoThrowTestSet
$Test.@testset($TestSet, $(args...))
end
esc(expr)
end
const SAMPLE_PKGIDS = [
PkgId(UUID("1649c42c-2196-4c52-9963-79822cd6227b"), "PkgWithIncompatibleTestProject"),
PkgId(UUID("6e4a843a-fdff-4fa3-bb5a-e4ae67826963"), "PkgWithCompatibleTestProject"),
PkgId(UUID("7231ce0e-e308-4079-b49f-19e33cc3ac6e"), "PkgWithPostJulia12Support"),
PkgId(UUID("8981f3dd-97fd-4684-8ec7-7b0c42f64e2e"), "PkgWithoutTestProject"),
PkgId(UUID("3922d3f4-c8f6-c8a8-00da-60b44ed8eac6"), "PkgWithoutDeps"),
]
const SAMPLE_PKG_BY_NAME = Dict(pkg.name => pkg for pkg in SAMPLE_PKGIDS)
function with_sample_pkgs(f)
sampledir = joinpath(@__DIR__, "sample")
original_load_path = copy(LOAD_PATH)
try
pushfirst!(LOAD_PATH, sampledir)
f()
finally
append!(empty!(LOAD_PATH), original_load_path)
end
end
end # module
================================================
FILE: test/pkgs/PersistentTasks/PersistentTask/Project.toml
================================================
name = "PersistentTask"
uuid = "e5c298b6-d81d-47aa-a9ed-5ea983e22986"
================================================
FILE: test/pkgs/PersistentTasks/PersistentTask/src/PersistentTask.jl
================================================
module PersistentTask
const t = Ref{Any}()
__init__() = t[] = Timer(0.1; interval = 1) # create a persistent `Timer` `Task`
end # module PersistentTask
================================================
FILE: test/pkgs/PersistentTasks/TransientTask/Project.toml
================================================
name = "TransientTask"
uuid = "94ae9332-58b0-4342-989c-0a7e44abcca9"
================================================
FILE: test/pkgs/PersistentTasks/TransientTask/src/TransientTask.jl
================================================
module TransientTask
__init__() = Timer(0.1) # create a transient `Timer` `Task`
end # module TransientTask
================================================
FILE: test/pkgs/PersistentTasks/UsesBoth/Project.toml
================================================
name = "UsesBoth"
uuid = "96f12b6e-60f8-43dc-b131-049a88a2f499"
[deps]
PersistentTask = "e5c298b6-d81d-47aa-a9ed-5ea983e22986"
TransientTask = "94ae9332-58b0-4342-989c-0a7e44abcca9"
================================================
FILE: test/pkgs/PersistentTasks/UsesBoth/src/UsesBoth.jl
================================================
module UsesBoth
using TransientTask
using PersistentTask
end # module UsesBoth
================================================
FILE: test/pkgs/PiracyForeignProject/Project.toml
================================================
name = "PiracyForeignProject"
uuid = "f592ac8b-a2e8-4dd0-be7a-e4053dab5b76"
================================================
FILE: test/pkgs/PiracyForeignProject/src/PiracyForeignProject.jl
================================================
module PiracyForeignProject
struct ForeignType end
struct ForeignParameterizedType{T} end
struct ForeignNonSingletonType
x::Int
end
end
================================================
FILE: test/pkgs/PkgUnboundArgs.jl
================================================
module PkgUnboundArgs
# Putting it in a submodule to test that `recursive=true` is used.
module M25341
_totuple(::Type{Tuple{Vararg{E}}}, itr, s...) where {E} = E
end
# `_totuple` is taken from
# https://github.com/JuliaLang/julia/blob/48634f9f8669e1dc1be0a1589cd5be880c04055a/test/ambiguous.jl#L257-L259
end # module
================================================
FILE: test/pkgs/PkgWithAmbiguities.jl
================================================
module PkgWithAmbiguities
# 1 ambiguity
f(::Any, ::Int) = 1
f(::Int, ::Any) = 2
const num_ambs_f = 1
# 2 ambiguities:
# 1 for g
# 1 for Core.kwfunc(g)
g(::Any, ::Int; kw) = 1
g(::Int, ::Any; kw) = 2
const num_ambs_g = 2
abstract type AbstractType end
struct SingletonType <: AbstractType end
struct ConcreteType <: AbstractType
x::Int
end
# 2 ambiguities
SingletonType(::Any, ::Any, ::Int) = 1
SingletonType(::Any, ::Int, ::Int) = 2
SingletonType(::Int, ::Any, ::Any) = 3
# 1 ambiguity
(::SingletonType)(::Any, ::Float64) = 1
(::SingletonType)(::Float64, ::Any) = 2
const num_ambs_SingletonType = 3
# 3 ambiguities
ConcreteType(::Any, ::Any, ::Int) = 1
ConcreteType(::Any, ::Int, ::Any) = 2
ConcreteType(::Int, ::Any, ::Any) = 3
# 1 ambiguity
(::ConcreteType)(::Any, ::Float64) = 1
(::ConcreteType)(::Float64, ::Any) = 2
const num_ambs_ConcreteType = 4
# 1 ambiguity
abstract type AbstractParameterizedType{T} end
struct ConcreteParameterizedType{T} <: AbstractParameterizedType{T} end
(::AbstractParameterizedType{T})(::Tuple{Tuple{Int}}) where {T} = 1
(::ConcreteParameterizedType)(::Tuple) = 2
const num_ambs_ParameterizedType = 1
end # module
================================================
FILE: test/pkgs/PkgWithUndefinedExports.jl
================================================
module PkgWithUndefinedExports
export undefined_name
end # module
================================================
FILE: test/pkgs/PkgWithUndocumentedNames.jl
================================================
module PkgWithUndocumentedNames
"""
documented_function
"""
function documented_function end
function undocumented_function end
"""
DocumentedStruct
"""
struct DocumentedStruct end
struct UndocumentedStruct end
export documented_function, DocumentedStruct
export undocumented_function, UndocumentedStruct
end # module
================================================
FILE: test/pkgs/PkgWithUndocumentedNamesInSubmodule.jl
================================================
module PkgWithUndocumentedNamesInSubmodule
"""
DocumentedStruct
"""
struct DocumentedStruct end
module SubModule
struct UndocumentedStruct end
end
end # module
================================================
FILE: test/pkgs/PkgWithoutUndocumentedNames.jl
================================================
"""
PkgWithoutUndocumentedNames
"""
module PkgWithoutUndocumentedNames
"""
documented_function
"""
function documented_function end
"""
DocumentedStruct
"""
struct DocumentedStruct end
export documented_function, DocumentedStruct
end # module
================================================
FILE: test/pkgs/sample/PkgWithCompatibleTestProject/Project.toml
================================================
name = "PkgWithCompatibleTestProject"
uuid = "6e4a843a-fdff-4fa3-bb5a-e4ae67826963"
[compat]
julia = "1"
[extras]
Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
[targets]
test = ["Pkg", "Test"]
================================================
FILE: test/pkgs/sample/PkgWithCompatibleTestProject/src/PkgWithCompatibleTestProject.jl
================================================
module PkgWithCompatibleTestProject end
================================================
FILE: test/pkgs/sample/PkgWithCompatibleTestProject/test/Project.toml
================================================
[deps]
Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
================================================
FILE: test/pkgs/sample/PkgWithIncompatibleTestProject/Project.toml
================================================
name = "PkgWithIncompatibleTestProject"
uuid = "1649c42c-2196-4c52-9963-79822cd6227b"
[compat]
julia = "1"
[extras]
REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
[targets]
test = ["REPL", "Test"]
================================================
FILE: test/pkgs/sample/PkgWithIncompatibleTestProject/src/PkgWithIncompatibleTestProject.jl
================================================
module PkgWithIncompatibleTestProject end
================================================
FILE: test/pkgs/sample/PkgWithIncompatibleTestProject/test/Project.toml
================================================
[deps]
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
================================================
FILE: test/pkgs/sample/PkgWithPostJulia12Support/Project.toml
================================================
name = "PkgWithPostJulia12Support"
uuid = "7231ce0e-e308-4079-b49f-19e33cc3ac6e"
[compat]
julia = "1.2"
[extras]
Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
[targets]
test = ["Pkg", "Test"]
================================================
FILE: test/pkgs/sample/PkgWithPostJulia12Support/src/PkgWithPostJulia12Support.jl
================================================
module PkgWithPostJulia12Support end
================================================
FILE: test/pkgs/sample/PkgWithPostJulia12Support/test/Project.toml
================================================
[deps]
REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
================================================
FILE: test/pkgs/sample/PkgWithoutDeps/Project.toml
================================================
name = "PkgWithoutDeps"
uuid = "3922d3f4-c8f6-c8a8-00da-60b44ed8eac6"
[compat]
julia = "1"
[extras]
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
[targets]
test = ["Test"]
================================================
FILE: test/pkgs/sample/PkgWithoutDeps/src/PkgWithoutDeps.jl
================================================
module PkgWithoutDeps end
================================================
FILE: test/pkgs/sample/PkgWithoutDeps/test/.gitkeep
================================================
================================================
FILE: test/pkgs/sample/PkgWithoutTestProject/Project.toml
================================================
name = "PkgWithoutTestProject"
uuid = "8981f3dd-97fd-4684-8ec7-7b0c42f64e2e"
[compat]
julia = "1"
[extras]
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
[targets]
test = ["Test"]
================================================
FILE: test/pkgs/sample/PkgWithoutTestProject/src/PkgWithoutTestProject.jl
================================================
module PkgWithoutTestProject end
================================================
FILE: test/pkgs/sample/PkgWithoutTestProject/test/.gitkeep
================================================
================================================
FILE: test/preamble.jl
================================================
let path = joinpath(@__DIR__, "pkgs")
if path ∉ LOAD_PATH
pushfirst!(LOAD_PATH, path)
end
end
using Test
using Aqua
using AquaTesting: @testtestset, AquaTesting, with_sample_pkgs
================================================
FILE: test/runtests.jl
================================================
module TestAqua
using Test
@testset "$file" for file in sort([
file for file in readdir(@__DIR__) if match(r"^test_.*\.jl$", file) !== nothing
])
include(file)
end
end # module
================================================
FILE: test/test_ambiguities.jl
================================================
module TestAmbiguities
include("preamble.jl")
@testset begin
using PkgWithAmbiguities
using PkgWithAmbiguities:
num_ambs_f,
num_ambs_g,
num_ambs_SingletonType,
num_ambs_ConcreteType,
num_ambs_ParameterizedType
total =
num_ambs_f +
num_ambs_g +
num_ambs_SingletonType +
num_ambs_ConcreteType +
num_ambs_ParameterizedType
function check_testcase(exclude, num_ambiguities::Int; broken::Bool = false)
pkgids = Aqua.aspkgids([PkgWithAmbiguities, Core]) # include Core to find constructor ambiguities
num_ambiguities_, strout, strerr = Aqua._find_ambiguities(pkgids; exclude = exclude)
if broken
@test_broken num_ambiguities_ == num_ambiguities
else
if num_ambiguities_ != num_ambiguities
@show exclude
println(strout)
println(strerr)
end
@test num_ambiguities_ == num_ambiguities
end
end
check_testcase([], total)
# exclude just anything irrelevant, see #49
check_testcase([convert], total)
# exclude function
check_testcase([PkgWithAmbiguities.f], total - num_ambs_f)
# exclude function and kwsorter
check_testcase([PkgWithAmbiguities.g], total - num_ambs_g)
# exclude callables and constructors
check_testcase([PkgWithAmbiguities.SingletonType], total - num_ambs_SingletonType)
check_testcase([PkgWithAmbiguities.ConcreteType], total - num_ambs_ConcreteType)
# exclude abstract supertype without callables and constructors
check_testcase([PkgWithAmbiguities.AbstractType], total)
# for ambiguities between abstract and concrete type callables, only one needs to be excluded
check_testcase(
[PkgWithAmbiguities.AbstractParameterizedType],
total - num_ambs_ParameterizedType,
)
check_testcase(
[PkgWithAmbiguities.ConcreteParameterizedType],
total - num_ambs_ParameterizedType,
)
# exclude everything
check_testcase(
[
PkgWithAmbiguities.f,
PkgWithAmbiguities.g,
PkgWithAmbiguities.SingletonType,
PkgWithAmbiguities.ConcreteType,
PkgWithAmbiguities.ConcreteParameterizedType,
],
0,
)
# It works with other tests:
Aqua.test_unbound_args(PkgWithAmbiguities)
Aqua.test_undefined_exports(PkgWithAmbiguities)
end
end # module
================================================
FILE: test/test_deps_compat.jl
================================================
module TestDepsCompat
include("preamble.jl")
using Aqua: find_missing_deps_compat, has_julia_compat
const DictSA = Dict{String,Any}
@testset "has_julia_compat" begin
@test has_julia_compat(DictSA("compat" => DictSA("julia" => "1")))
@test has_julia_compat(DictSA("compat" => DictSA("julia" => "1.0")))
@test has_julia_compat(DictSA("compat" => DictSA("julia" => "1.6")))
@test has_julia_compat(
DictSA(
"deps" => DictSA("PkgA" => "229717a1-0d13-4dfb-ba8f-049672e31205"),
"compat" => DictSA("julia" => "1", "PkgA" => "1.0"),
),
)
@test !has_julia_compat(DictSA())
@test !has_julia_compat(DictSA("compat" => DictSA()))
@test !has_julia_compat(DictSA("compat" => DictSA("PkgA" => "1.0")))
@test !has_julia_compat(
DictSA(
"deps" => DictSA("PkgA" => "229717a1-0d13-4dfb-ba8f-049672e31205"),
"compat" => DictSA("PkgA" => "1.0"),
),
)
end
@testset "find_missing_deps_compat" begin
@testset "pass" begin
result = find_missing_deps_compat(
DictSA("deps" => DictSA(), "compat" => DictSA("julia" => "1")),
"deps",
)
@test isempty(result)
result = find_missing_deps_compat(
DictSA(
"deps" => DictSA("PkgA" => "229717a1-0d13-4dfb-ba8f-049672e31205"),
"compat" => DictSA("julia" => "1", "PkgA" => "1.0"),
),
"deps",
)
@test isempty(result)
@testset "does not have `deps`" begin
result = find_missing_deps_compat(DictSA(), "deps")
@test isempty(result)
end
end
@testset "failure" begin
@testset "does not have `compat`" begin
result = find_missing_deps_compat(
DictSA("deps" => DictSA("PkgA" => "229717a1-0d13-4dfb-ba8f-049672e31205")),
"deps",
)
@test length(result) == 1
@test [pkg.name for pkg in result] == ["PkgA"]
end
@testset "does not specify `compat` for PkgA" begin
result = find_missing_deps_compat(
DictSA(
"deps" => DictSA("PkgA" => "229717a1-0d13-4dfb-ba8f-049672e31205"),
"compat" => DictSA("julia" => "1"),
),
"deps",
)
@test length(result) == 1
@test [pkg.name for pkg in result] == ["PkgA"]
end
@testset "does not specify `compat` for PkgB" begin
result = find_missing_deps_compat(
DictSA(
"deps" => DictSA(
"PkgA" => "229717a1-0d13-4dfb-ba8f-049672e31205",
"PkgB" => "3d97d89c-7c41-49ae-981c-14fe13cc7943",
),
"compat" => DictSA("julia" => "1", "PkgA" => "1.0"),
),
"deps",
)
@test length(result) == 1
@test [pkg.name for pkg in result] == ["PkgB"]
end
@testset "does not specify `compat` for stdlib" begin
result = find_missing_deps_compat(
DictSA(
"deps" => DictSA(
"LinearAlgebra" => "37e2e46d-f89d-539d-b4ee-838fcccc9c8e",
),
"compat" => DictSA("julia" => "1"),
),
"deps",
)
@test length(result) == 1
@test [pkg.name for pkg in result] == ["LinearAlgebra"]
end
end
end
end # module
================================================
FILE: test/test_exclude.jl
================================================
module TestExclude
include("preamble.jl")
using Base: PkgId
using Aqua: getexclude, normalize_exclude, normalize_exclude_obj, normalize_and_check_exclude, rootmodule, reprexclude
@assert parentmodule(Tuple) === Core
@assert parentmodule(foldl) === Base
@assert parentmodule(Some) === Base
@assert parentmodule(Broadcast.Broadcasted) === Base.Broadcast
@assert rootmodule(Broadcast.Broadcasted) === Base
@testset "roundtrip" begin
@testset for x in Any[
foldl
Some
Tuple
Broadcast.Broadcasted
nothing
Any
]
@test getexclude(normalize_exclude(x)) === normalize_exclude_obj(x)
end
@test_throws ErrorException normalize_and_check_exclude(Any[Pair{Int}])
@testset "$(repr(last(spec)))" for spec in [
(PkgId(Base) => "Base.#foldl")
(PkgId(Base) => "Base.Some")
(PkgId(Core) => "Core.Tuple")
(PkgId(Base) => "Base.Broadcast.Broadcasted")
(PkgId(Core) => "Core.Nothing")
(PkgId(Core) => "Core.Any")
]
@test normalize_exclude(getexclude(spec)) === spec
end
end
@testset "normalize_and_check_exclude" begin
@testset "$i" for (i, exclude) in enumerate([Any[foldl], Any[foldl, Some], Any[foldl, Tuple]])
local specs
@test (specs = normalize_and_check_exclude(exclude)) isa Vector
@test Base.include_string(@__MODULE__, reprexclude(specs)) == specs
end
end
end # module
================================================
FILE: test/test_persistent_tasks.jl
================================================
module TestPersistentTasks
include("preamble.jl")
using Base: PkgId, UUID
using Pkg: TOML
function getid(name)
path = joinpath(@__DIR__, "pkgs", "PersistentTasks", name)
if path ∉ LOAD_PATH
pushfirst!(LOAD_PATH, path)
end
prj = TOML.parsefile(joinpath(path, "Project.toml"))
return PkgId(UUID(prj["uuid"]), prj["name"])
end
@testset "PersistentTasks" begin
@test !Aqua.has_persistent_tasks(getid("TransientTask"))
result = Aqua.find_persistent_tasks_deps(getid("TransientTask"))
@test result == []
if Base.VERSION >= v"1.10-"
@test Aqua.has_persistent_tasks(getid("PersistentTask"))
result = Aqua.find_persistent_tasks_deps(getid("UsesBoth"))
@test result == ["PersistentTask"]
end
filter!(str -> !occursin("PersistentTasks", str), LOAD_PATH)
end
@testset "test_persistent_tasks(expr)" begin
if Base.VERSION >= v"1.10-"
@test !Aqua.has_persistent_tasks(
getid("TransientTask"),
expr = quote
fetch(Threads.@spawn nothing)
end,
)
@test Aqua.has_persistent_tasks(getid("TransientTask"), expr = quote
Threads.@spawn while true
sleep(0.5)
end
end)
end
end
end
================================================
FILE: test/test_piracy.jl
================================================
push!(LOAD_PATH, joinpath(@__DIR__, "pkgs", "PiracyForeignProject"))
baremodule PiracyModule
using PiracyForeignProject: ForeignType, ForeignParameterizedType, ForeignNonSingletonType
using Base:
Base,
Set,
AbstractSet,
Integer,
Val,
Vararg,
Vector,
Unsigned,
UInt,
String,
Tuple,
AbstractChar
struct Foo end
struct Bar{T<:AbstractSet{<:Integer}} end
# Not piracy: Function defined here
f(::Int, ::Union{String,Char}) = 1
f(::Int) = 2
Foo(::Int) = Foo()
# Not piracy: At least one argument is local
Base.findlast(::Foo, x::Int) = x + 1
Base.findlast(::Set{Foo}, x::Int) = x + 1
Base.findlast(::Type{Val{Foo}}, x::Int) = x + 1
Base.findlast(::Tuple{Vararg{Bar{Set{Int}}}}, x::Int) = x + 1
Base.findlast(::Val{:foo}, x::Int) = x + 1
Base.findlast(::ForeignParameterizedType{Foo}, x::Int) = x + 1
# Not piracy
const MyUnion = Union{Int,Foo}
MyUnion(x::Int) = x
MyUnion(; x::Int) = x
export MyUnion
# Piracy
Base.findfirst(::Set{Vector{Char}}, ::Int) = 1
Base.findfirst(::Union{Foo,Bar{Set{Unsigned}},UInt}, ::Tuple{Vararg{String}}) = 1
Base.findfirst(::AbstractChar, ::Set{T}) where {Int <: T <: Integer} = 1
(::ForeignType)(x::Int8) = x + 1
(::ForeignNonSingletonType)(x::Int8) = x + 1
# Piracy, but not for `ForeignType in treat_as_own`
Base.findmax(::ForeignType, x::Int) = x + 1
Base.findmax(::Set{Vector{ForeignType}}, x::Int) = x + 1
Base.findmax(::Union{Foo,ForeignType}, x::Int) = x + 1
# Piracy, but not for `ForeignParameterizedType in treat_as_own`
Base.findmin(::ForeignParameterizedType{Int}, x::Int) = x + 1
Base.findmin(::Set{Vector{ForeignParameterizedType{Int}}}, x::Int) = x + 1
Base.findmin(::Union{Foo,ForeignParameterizedType{Int}}, x::Int) = x + 1
end # PiracyModule
using Aqua: Piracy
using PiracyForeignProject: ForeignType, ForeignParameterizedType, ForeignNonSingletonType
# Get all methods - test length
meths = filter(Piracy.all_methods(PiracyModule)) do m
m.module == PiracyModule
end
@test length(meths) ==
2 + # Foo constructors
1 + # Bar constructor
2 + # f
4 + # MyUnion (incl. kwcall)
6 + # findlast
3 + # findfirst
1 + # ForeignType callable
1 + # ForeignNonSingletonType callable
3 + # findmax
3 # findmin
# Test what is foreign
BasePkg = Base.PkgId(Base)
CorePkg = Base.PkgId(Core)
ThisPkg = Base.PkgId(PiracyModule)
@test Piracy.is_foreign(Int, BasePkg; treat_as_own = []) # from Core
@test !Piracy.is_foreign(Int, CorePkg; treat_as_own = []) # from Core
@test !Piracy.is_foreign(Set{Int}, BasePkg; treat_as_own = [])
@test !Piracy.is_foreign(Set{Int}, CorePkg; treat_as_own = [])
# Test what is pirate
pirates = Piracy.hunt(PiracyModule)
@test length(pirates) ==
3 + # findfirst
3 + # findmax
3 + # findmin
1 + # ForeignType callable
1 # ForeignNonSingletonType callable
@test all(pirates) do m
m.name in [:findfirst, :findmax, :findmin, :ForeignType, :ForeignNonSingletonType]
end
# Test what is pirate (with treat_as_own=[ForeignType])
pirates = Piracy.hunt(PiracyModule, treat_as_own = [ForeignType])
@test length(pirates) ==
3 + # findfirst
3 + # findmin
1 # ForeignNonSingletonType callable
@test all(pirates) do m
m.name in [:findfirst, :findmin, :ForeignNonSingletonType]
end
# Test what is pirate (with treat_as_own=[ForeignParameterizedType])
pirates = Piracy.hunt(PiracyModule, treat_as_own = [ForeignParameterizedType])
@test length(pirates) ==
3 + # findfirst
3 + # findmax
1 + # ForeignType callable
1 # ForeignNonSingletonType callable
@test all(pirates) do m
m.name in [:findfirst, :findmax, :ForeignType, :ForeignNonSingletonType]
end
# Test what is pirate (with treat_as_own=[ForeignType, ForeignParameterizedType])
pirates = filter(
m -> Piracy.is_pirate(m; treat_as_own = [ForeignType, ForeignParameterizedType]),
meths,
)
@test length(pirates) ==
3 + # findfirst
1 # ForeignNonSingletonType callable
@test all(pirates) do m
m.name in [:findfirst, :ForeignNonSingletonType]
end
# Test what is pirate (with treat_as_own=[Base.findfirst, Base.findmax])
pirates = Piracy.hunt(PiracyModule, treat_as_own = [Base.findfirst, Base.findmax])
@test length(pirates) ==
3 + # findmin
1 + # ForeignType callable
1 # ForeignNonSingletonType callable
@test all(pirates) do m
m.name in [:findmin, :ForeignType, :ForeignNonSingletonType]
end
# Test what is pirate (excluding a cover of everything)
pirates = filter(
m -> Piracy.is_pirate(
m;
treat_as_own = [
ForeignType,
ForeignParameterizedType,
ForeignNonSingletonType,
Base.findfirst,
],
),
meths,
)
@test length(pirates) == 0
================================================
FILE: test/test_project_extras.jl
================================================
module TestProjectExtras
include("preamble.jl")
using Aqua: is_julia12_or_later
using Base: PkgId, UUID
@testset "is_julia12_or_later" begin
@test is_julia12_or_later("1.2")
@test is_julia12_or_later("1.3")
@test is_julia12_or_later("1.3, 1.4")
@test is_julia12_or_later("1.3 - 1.4, 1.6")
@test !is_julia12_or_later("1")
@test !is_julia12_or_later("1.1")
@test !is_julia12_or_later("1.0 - 1.1")
@test !is_julia12_or_later("1.0 - 1.3")
end
with_sample_pkgs() do
@testset "PkgWithIncompatibleTestProject" begin
pkg = AquaTesting.SAMPLE_PKG_BY_NAME["PkgWithIncompatibleTestProject"]
result = Aqua.analyze_project_extras(pkg)
@test !isempty(result)
@test any(
msg -> occursin(
"Root and test projects should be consistent for projects supporting Julia <= 1.1.",
msg,
),
result,
)
@test any(
msg ->
occursin("Test dependencies not in root project", msg) &&
occursin("Random [9a3f8284-a2c9-5f02-9a11-845980a1fd5c]", msg),
result,
)
@test any(
msg ->
occursin("Dependencies not in test project", msg) &&
occursin("REPL [3fa0cd96-eef1-5676-8a61-b3b8758bbffb]", msg),
result,
)
@test !any(msg -> occursin("Test [", msg), result)
end
@testset "PkgWithCompatibleTestProject" begin
pkg = AquaTesting.SAMPLE_PKG_BY_NAME["PkgWithCompatibleTestProject"]
result = Aqua.analyze_project_extras(pkg)
@test isempty(result)
end
@testset "PkgWithPostJulia12Support" begin
pkg = AquaTesting.SAMPLE_PKG_BY_NAME["PkgWithPostJulia12Support"]
result = Aqua.analyze_project_extras(pkg)
@test isempty(result)
end
@testset "PkgWithoutTestProject" begin
pkg = AquaTesting.SAMPLE_PKG_BY_NAME["PkgWithoutTestProject"]
result = Aqua.analyze_project_extras(pkg)
@test isempty(result)
end
end
end # module
================================================
FILE: test/test_smoke.jl
================================================
module TestSmoke
using Aqua
# test defaults
Aqua.test_all(Aqua)
# test everything else
Aqua.test_all(
Aqua;
ambiguities = false,
unbound_args = false,
undefined_exports = false,
project_extras = false,
stale_deps = false,
deps_compat = false,
piracies = false,
persistent_tasks = false,
)
end # module
================================================
FILE: test/test_stale_deps.jl
================================================
module TestStaleDeps
include("preamble.jl")
using Base: PkgId, UUID
using Aqua: find_stale_deps_2
@testset "find_stale_deps_2" begin
pkg = PkgId(UUID(42), "TargetPkg")
dep1 = PkgId(UUID(1), "Dep1")
dep2 = PkgId(UUID(2), "Dep2")
dep3 = PkgId(UUID(3), "Dep3")
@testset "pass" begin
result = find_stale_deps_2(;
deps = PkgId[],
weakdeps = PkgId[],
loaded_uuids = UUID[],
ignore = Symbol[],
)
@test isempty(result)
result = find_stale_deps_2(;
deps = PkgId[dep1],
weakdeps = PkgId[],
loaded_uuids = UUID[dep1.uuid, dep2.uuid, dep3.uuid],
ignore = Symbol[],
)
@test isempty(result)
result = find_stale_deps_2(;
deps = PkgId[dep1],
weakdeps = PkgId[],
loaded_uuids = UUID[dep2.uuid, dep3.uuid],
ignore = Symbol[:Dep1],
)
@test isempty(result)
result = find_stale_deps_2(;
deps = PkgId[dep1],
weakdeps = PkgId[dep2],
loaded_uuids = UUID[dep1.uuid],
ignore = Symbol[],
)
@test isempty(result)
result = find_stale_deps_2(;
deps = PkgId[dep1, dep2],
weakdeps = PkgId[dep2],
loaded_uuids = UUID[dep1.uuid],
ignore = Symbol[],
)
@test isempty(result)
result = find_stale_deps_2(;
deps = PkgId[dep1, dep2],
weakdeps = PkgId[dep2],
loaded_uuids = UUID[],
ignore = Symbol[:Dep1],
)
@test isempty(result)
end
@testset "failure" begin
result = find_stale_deps_2(;
deps = PkgId[dep1],
weakdeps = PkgId[],
loaded_uuids = UUID[],
ignore = Symbol[],
)
@test length(result) == 1
@test dep1 in result
result = find_stale_deps_2(;
deps = PkgId[dep1],
weakdeps = PkgId[],
loaded_uuids = UUID[dep2.uuid, dep3.uuid],
ignore = Symbol[],
)
@test length(result) == 1
@test dep1 in result
result = find_stale_deps_2(;
deps = PkgId[dep1, dep2],
weakdeps = PkgId[],
loaded_uuids = UUID[dep3.uuid],
ignore = Symbol[:Dep1],
)
@test length(result) == 1
@test dep2 in result
end
end
with_sample_pkgs() do
@testset "Package without `deps`" begin
pkg = AquaTesting.SAMPLE_PKG_BY_NAME["PkgWithoutDeps"]
results = Aqua.find_stale_deps(pkg)
@test isempty(results)
end
end
end # module
================================================
FILE: test/test_unbound_args.jl
================================================
module TestUnboundArgs
include("preamble.jl")
using PkgUnboundArgs
@testset begin
println("### Expected output START ###")
results = @testtestset begin
Aqua.test_unbound_args(PkgUnboundArgs)
end
println("### Expected output END ###")
@test length(results) == 1
@test results[1] isa Test.Fail
# It works with other tests:
Aqua.test_ambiguities(PkgUnboundArgs)
Aqua.test_undefined_exports(PkgUnboundArgs)
end
end # module
================================================
FILE: test/test_undefined_exports.jl
================================================
module TestUndefinedExports
include("preamble.jl")
using PkgWithUndefinedExports
@testset begin
@test Aqua.undefined_exports(PkgWithUndefinedExports) ==
[Symbol("PkgWithUndefinedExports.undefined_name")]
println("### Expected output START ###")
results = @testtestset begin
Aqua.test_undefined_exports(PkgWithUndefinedExports)
end
println("### Expected output END ###")
@test length(results) == 1
@test results[1] isa Test.Fail
# It works with other tests:
Aqua.test_ambiguities(PkgWithUndefinedExports)
Aqua.test_unbound_args(PkgWithUndefinedExports)
end
end # module
================================================
FILE: test/test_undocumented_names.jl
================================================
module TestUndocumentedNames
include("preamble.jl")
import PkgWithUndocumentedNames
import PkgWithUndocumentedNamesInSubmodule
import PkgWithoutUndocumentedNames
# Pass
results = @testtestset begin
Aqua.test_undocumented_names(PkgWithoutUndocumentedNames)
end
if VERSION >= v"1.11"
@test length(results) == 1
@test results[1] isa Test.Pass
else
@test length(results) == 0
end
# Fail
println("### Expected output START ###")
results = @testtestset begin
Aqua.test_undocumented_names(PkgWithUndocumentedNames)
end
println("### Expected output END ###")
if VERSION >= v"1.11"
@test length(results) == 1
@test results[1] isa Test.Fail
else
@test length(results) == 0
end
println("### Expected output START ###")
results = @testtestset begin
Aqua.test_undocumented_names(PkgWithUndocumentedNamesInSubmodule)
end
println("### Expected output END ###")
if VERSION >= v"1.11"
@test length(results) == 1
@test results[1] isa Test.Fail
else
@test length(results) == 0
end
# Broken
println("### Expected output START ###")
results = @testtestset begin
Aqua.test_undocumented_names(PkgWithUndocumentedNames; broken = true)
end
println("### Expected output END ###")
if VERSION >= v"1.11"
@test length(results) == 1
@test results[1] isa Test.Broken
else
@test length(results) == 0
end
end # module
================================================
FILE: test/test_utils.jl
================================================
module TestUtils
using Aqua: askwargs
using Test
@testset "askwargs" begin
@test_throws ArgumentError("expect `true`") askwargs(false)
@test askwargs(true) === NamedTuple()
@test askwargs(()) === NamedTuple()
@test askwargs((a = 1,)) === (a = 1,)
end
end # module
gitextract_3yp6x4rr/
├── .JuliaFormatter.toml
├── .github/
│ ├── dependabot.yml
│ └── workflows/
│ ├── DocPreviewCleanup.yml
│ ├── TagBot.yml
│ ├── code-style.yml
│ ├── docs.yml
│ ├── enforce-labels.yml
│ └── test.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── Makefile
├── Project.toml
├── README.md
├── docs/
│ ├── Project.toml
│ ├── changelog.jl
│ ├── instantiate.jl
│ ├── make.jl
│ └── src/
│ ├── ambiguities.md
│ ├── deps_compat.md
│ ├── exports.md
│ ├── index.md
│ ├── persistent_tasks.md
│ ├── piracies.md
│ ├── project_extras.md
│ ├── stale_deps.md
│ ├── test_all.md
│ ├── unbound_args.md
│ └── undocumented_names.md
├── src/
│ ├── Aqua.jl
│ ├── ambiguities.jl
│ ├── deps_compat.jl
│ ├── exports.jl
│ ├── persistent_tasks.jl
│ ├── piracies.jl
│ ├── precompile.jl
│ ├── project_extras.jl
│ ├── stale_deps.jl
│ ├── unbound_args.jl
│ ├── undocumented_names.jl
│ └── utils.jl
└── test/
├── pkgs/
│ ├── AquaTesting.jl
│ ├── PersistentTasks/
│ │ ├── PersistentTask/
│ │ │ ├── Project.toml
│ │ │ └── src/
│ │ │ └── PersistentTask.jl
│ │ ├── TransientTask/
│ │ │ ├── Project.toml
│ │ │ └── src/
│ │ │ └── TransientTask.jl
│ │ └── UsesBoth/
│ │ ├── Project.toml
│ │ └── src/
│ │ └── UsesBoth.jl
│ ├── PiracyForeignProject/
│ │ ├── Project.toml
│ │ └── src/
│ │ └── PiracyForeignProject.jl
│ ├── PkgUnboundArgs.jl
│ ├── PkgWithAmbiguities.jl
│ ├── PkgWithUndefinedExports.jl
│ ├── PkgWithUndocumentedNames.jl
│ ├── PkgWithUndocumentedNamesInSubmodule.jl
│ ├── PkgWithoutUndocumentedNames.jl
│ └── sample/
│ ├── PkgWithCompatibleTestProject/
│ │ ├── Project.toml
│ │ ├── src/
│ │ │ └── PkgWithCompatibleTestProject.jl
│ │ └── test/
│ │ └── Project.toml
│ ├── PkgWithIncompatibleTestProject/
│ │ ├── Project.toml
│ │ ├── src/
│ │ │ └── PkgWithIncompatibleTestProject.jl
│ │ └── test/
│ │ └── Project.toml
│ ├── PkgWithPostJulia12Support/
│ │ ├── Project.toml
│ │ ├── src/
│ │ │ └── PkgWithPostJulia12Support.jl
│ │ └── test/
│ │ └── Project.toml
│ ├── PkgWithoutDeps/
│ │ ├── Project.toml
│ │ ├── src/
│ │ │ └── PkgWithoutDeps.jl
│ │ └── test/
│ │ └── .gitkeep
│ └── PkgWithoutTestProject/
│ ├── Project.toml
│ ├── src/
│ │ └── PkgWithoutTestProject.jl
│ └── test/
│ └── .gitkeep
├── preamble.jl
├── runtests.jl
├── test_ambiguities.jl
├── test_deps_compat.jl
├── test_exclude.jl
├── test_persistent_tasks.jl
├── test_piracy.jl
├── test_project_extras.jl
├── test_smoke.jl
├── test_stale_deps.jl
├── test_unbound_args.jl
├── test_undefined_exports.jl
├── test_undocumented_names.jl
└── test_utils.jl
Condensed preview — 85 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (124K chars).
[
{
"path": ".JuliaFormatter.toml",
"chars": 0,
"preview": ""
},
{
"path": ".github/dependabot.yml",
"chars": 464,
"preview": "# To get started with Dependabot version updates, you'll need to specify which\n# package ecosystems to update and where "
},
{
"path": ".github/workflows/DocPreviewCleanup.yml",
"chars": 940,
"preview": "name: Doc Preview Cleanup\n\non:\n pull_request:\n types: [closed]\n\njobs:\n doc-preview-cleanup:\n runs-on: ubuntu-sli"
},
{
"path": ".github/workflows/TagBot.yml",
"chars": 777,
"preview": "name: TagBot\n\non:\n issue_comment:\n types:\n - created\n workflow_dispatch:\n inputs:\n lookback:\n d"
},
{
"path": ".github/workflows/code-style.yml",
"chars": 144,
"preview": "name: Code style\n\non:\n pull_request:\n\njobs:\n code-style:\n runs-on: ubuntu-slim\n steps:\n - uses: tkf/julia-c"
},
{
"path": ".github/workflows/docs.yml",
"chars": 1085,
"preview": "name: Documentation\n\non:\n push:\n branches:\n - master\n - 'release-*'\n tags: '*'\n pull_request:\n workfl"
},
{
"path": ".github/workflows/enforce-labels.yml",
"chars": 699,
"preview": "name: Enforce PR labels\n\npermissions:\n contents: read\non:\n pull_request:\n types: [labeled, unlabeled, opened, reope"
},
{
"path": ".github/workflows/test.yml",
"chars": 2188,
"preview": "name: Run tests\n\non:\n push:\n branches:\n - master\n - 'release-*'\n tags: '*'\n pull_request:\n workflow_d"
},
{
"path": ".gitignore",
"chars": 105,
"preview": "*.jl.*.cov\n*.jl.cov\n*.jl.mem\n.DS_Store\n/docs/build/\n/docs/site/\n/docs/src/release-notes.md\nManifest.toml\n"
},
{
"path": "CHANGELOG.md",
"chars": 13747,
"preview": "# Release notes\n\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Ch"
},
{
"path": "LICENSE",
"chars": 1060,
"preview": "Copyright (c) 2019 Takafumi Arakaki\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof thi"
},
{
"path": "Makefile",
"chars": 1125,
"preview": "JULIA:=julia\n\ndefault: help\n\n.PHONY: default changelog docs docs-instantiate generate_badge generate_favicon help test\n\n"
},
{
"path": "Project.toml",
"chars": 466,
"preview": "name = \"Aqua\"\nuuid = \"4c88cf16-eb10-579e-8560-4a9242c79595\"\nauthors = [\"Takafumi Arakaki <aka.tkf@gmail.com> and contrib"
},
{
"path": "README.md",
"chars": 1988,
"preview": "# Aqua.jl: *A*uto *QU*ality *A*ssurance for Julia packages\n\n[,\n joinpath(@__DIR__, \"..\", \"CHANGELOG.md\");\n repo "
},
{
"path": "docs/instantiate.jl",
"chars": 717,
"preview": "# This script can be used to quickly instantiate the docs/Project.toml environment.\nusing Pkg, TOML\n\npackage_directory ="
},
{
"path": "docs/make.jl",
"chars": 1062,
"preview": "using Documenter, Aqua, Changelog\n\n# Generate a Documenter-friendly changelog from CHANGELOG.md\nChangelog.generate(\n "
},
{
"path": "docs/src/ambiguities.md",
"chars": 578,
"preview": "# Ambiguities\n\nMethod ambiguities are cases where multiple methods are applicable to a given set of arguments, without h"
},
{
"path": "docs/src/deps_compat.md",
"chars": 523,
"preview": "# Compat entries\n\nIn your `Project.toml` you can (and should) use compat entries to specify\nwith which versions of Julia"
},
{
"path": "docs/src/exports.md",
"chars": 110,
"preview": "# Undefined exports\n\n## [Test function](@id test_undefined_exports)\n\n```@docs\nAqua.test_undefined_exports\n```\n"
},
{
"path": "docs/src/index.md",
"chars": 3147,
"preview": "# Aqua.jl: *A*uto *QU*ality *A*ssurance for Julia packages\n\nAqua.jl provides functions to run a few automatable checks f"
},
{
"path": "docs/src/persistent_tasks.md",
"chars": 4153,
"preview": "# Persistent Tasks\n\n## Motivation\n\nJulia 1.10 and higher wait for all running `Task`s to finish\nbefore writing out the p"
},
{
"path": "docs/src/piracies.md",
"chars": 2593,
"preview": "# Type piracy\n\nType piracy is a term used to describe adding methods to a foreign function\nwith only foreign arguments.\n"
},
{
"path": "docs/src/project_extras.md",
"chars": 654,
"preview": "# Project.toml extras\n\nThere are two different ways to specify test-only dependencies (see [the Pkg docs](https://julial"
},
{
"path": "docs/src/stale_deps.md",
"chars": 97,
"preview": "# Stale dependencies\n\n## [Test function](@id test_stale_deps)\n\n```@docs\nAqua.test_stale_deps\n```\n"
},
{
"path": "docs/src/test_all.md",
"chars": 311,
"preview": "# [Test everything](@id test_all)\n\nThis test runs most of the other tests in this module.\nThe defaults should be fine fo"
},
{
"path": "docs/src/unbound_args.md",
"chars": 984,
"preview": "# Unbound Type Parameters\n\nAn unbound type parameter is a type parameter with a `where`,\nthat does not occur in the sign"
},
{
"path": "docs/src/undocumented_names.md",
"chars": 113,
"preview": "# Undocumented names\n\n## [Test function](@id test_undocumented_names)\n\n```@docs\nAqua.test_undocumented_names\n```\n"
},
{
"path": "src/Aqua.jl",
"chars": 3563,
"preview": "module Aqua\n\nusing Base: Docs, PkgId, UUID\nusing Pkg: Pkg, TOML, PackageSpec\nusing Pkg.Types: VersionSpec, semver_spec\nu"
},
{
"path": "src/ambiguities.jl",
"chars": 8133,
"preview": "\"\"\"\n test_ambiguities(package::Union{Module, PkgId})\n test_ambiguities(packages::Vector{Union{Module, PkgId}})\n\nTe"
},
{
"path": "src/deps_compat.jl",
"chars": 4078,
"preview": "\"\"\"\n Aqua.test_deps_compat(package)\n\nTest that the `Project.toml` of `package` has a `compat` entry for\neach package "
},
{
"path": "src/exports.jl",
"chars": 1973,
"preview": "# avoid Base.isbindingresolved deprecation in https://github.com/JuliaLang/julia/pull/57253\nfunction isbindingresolved(m"
},
{
"path": "src/persistent_tasks.jl",
"chars": 5579,
"preview": "\"\"\"\n Aqua.test_persistent_tasks(package)\n\nTest whether loading `package` creates persistent `Task`s\nwhich may block p"
},
{
"path": "src/piracies.jl",
"chars": 8719,
"preview": "module Piracy\n\nusing ..Aqua: is_kwcall\n\nusing Test: is_in_mods\n\n# based on Test/Test.jl#detect_ambiguities\n# https://git"
},
{
"path": "src/precompile.jl",
"chars": 1767,
"preview": "using PrecompileTools: @compile_workload\n\n# Create a minimal fake package to test. Needs to be at the top-level because\n"
},
{
"path": "src/project_extras.jl",
"chars": 3605,
"preview": "\"\"\"\n test_project_extras(package::Union{Module, PkgId})\n test_project_extras(packages::Vector{Union{Module, PkgId}"
},
{
"path": "src/stale_deps.jl",
"chars": 3151,
"preview": "\"\"\"\n Aqua.test_stale_deps(package; ignore::AbstractVector{Symbol} = Symbol[])\n\nTest that `package` loads all dependen"
},
{
"path": "src/unbound_args.jl",
"chars": 1319,
"preview": "\"\"\"\n test_unbound_args(m::Module; broken::Bool = false)\n\nTest that all methods in `m` and its submodules do not have\n"
},
{
"path": "src/undocumented_names.jl",
"chars": 1685,
"preview": "\"\"\"\n test_undocumented_names(m::Module; broken::Bool = false)\n\nTest that all public names in `m` and its recursive su"
},
{
"path": "src/utils.jl",
"chars": 2247,
"preview": "askwargs(kwargs) = (; kwargs...)\nfunction askwargs(flag::Bool)\n if !flag\n throw(ArgumentError(\"expect `true`\")"
},
{
"path": "test/pkgs/AquaTesting.jl",
"chars": 1305,
"preview": "module AquaTesting\n\nusing Base: PkgId, UUID\nusing Pkg\nusing Test\n\n# Taken from Test/test/runtests.jl\nmutable struct NoTh"
},
{
"path": "test/pkgs/PersistentTasks/PersistentTask/Project.toml",
"chars": 70,
"preview": "name = \"PersistentTask\"\nuuid = \"e5c298b6-d81d-47aa-a9ed-5ea983e22986\"\n"
},
{
"path": "test/pkgs/PersistentTasks/PersistentTask/src/PersistentTask.jl",
"chars": 156,
"preview": "module PersistentTask\n\nconst t = Ref{Any}()\n__init__() = t[] = Timer(0.1; interval = 1) # create a persistent `Timer` "
},
{
"path": "test/pkgs/PersistentTasks/TransientTask/Project.toml",
"chars": 69,
"preview": "name = \"TransientTask\"\nuuid = \"94ae9332-58b0-4342-989c-0a7e44abcca9\"\n"
},
{
"path": "test/pkgs/PersistentTasks/TransientTask/src/TransientTask.jl",
"chars": 112,
"preview": "module TransientTask\n\n__init__() = Timer(0.1) # create a transient `Timer` `Task`\n\nend # module TransientTask\n"
},
{
"path": "test/pkgs/PersistentTasks/UsesBoth/Project.toml",
"chars": 183,
"preview": "name = \"UsesBoth\"\nuuid = \"96f12b6e-60f8-43dc-b131-049a88a2f499\"\n\n[deps]\nPersistentTask = \"e5c298b6-d81d-47aa-a9ed-5ea983"
},
{
"path": "test/pkgs/PersistentTasks/UsesBoth/src/UsesBoth.jl",
"chars": 81,
"preview": "module UsesBoth\n\nusing TransientTask\nusing PersistentTask\n\nend # module UsesBoth\n"
},
{
"path": "test/pkgs/PiracyForeignProject/Project.toml",
"chars": 76,
"preview": "name = \"PiracyForeignProject\"\nuuid = \"f592ac8b-a2e8-4dd0-be7a-e4053dab5b76\"\n"
},
{
"path": "test/pkgs/PiracyForeignProject/src/PiracyForeignProject.jl",
"chars": 143,
"preview": "module PiracyForeignProject\n\nstruct ForeignType end\nstruct ForeignParameterizedType{T} end\n\nstruct ForeignNonSingletonTy"
},
{
"path": "test/pkgs/PkgUnboundArgs.jl",
"chars": 321,
"preview": "module PkgUnboundArgs\n\n# Putting it in a submodule to test that `recursive=true` is used.\nmodule M25341\n_totuple(::Type{"
},
{
"path": "test/pkgs/PkgWithAmbiguities.jl",
"chars": 1169,
"preview": "module PkgWithAmbiguities\n\n# 1 ambiguity\nf(::Any, ::Int) = 1\nf(::Int, ::Any) = 2\nconst num_ambs_f = 1\n\n# 2 ambiguities:\n"
},
{
"path": "test/pkgs/PkgWithUndefinedExports.jl",
"chars": 69,
"preview": "module PkgWithUndefinedExports\n\nexport undefined_name\n\nend # module\n"
},
{
"path": "test/pkgs/PkgWithUndocumentedNames.jl",
"chars": 333,
"preview": "module PkgWithUndocumentedNames\n\n\"\"\"\n documented_function\n\"\"\"\nfunction documented_function end\n\nfunction undocumented"
},
{
"path": "test/pkgs/PkgWithUndocumentedNamesInSubmodule.jl",
"chars": 170,
"preview": "module PkgWithUndocumentedNamesInSubmodule\n\n\"\"\"\n DocumentedStruct\n\"\"\"\nstruct DocumentedStruct end\n\nmodule SubModule\n\n"
},
{
"path": "test/pkgs/PkgWithoutUndocumentedNames.jl",
"chars": 260,
"preview": "\"\"\"\n PkgWithoutUndocumentedNames\n\"\"\"\nmodule PkgWithoutUndocumentedNames\n\n\"\"\"\n documented_function\n\"\"\"\nfunction doc"
},
{
"path": "test/pkgs/sample/PkgWithCompatibleTestProject/Project.toml",
"chars": 241,
"preview": "name = \"PkgWithCompatibleTestProject\"\nuuid = \"6e4a843a-fdff-4fa3-bb5a-e4ae67826963\"\n\n[compat]\njulia = \"1\"\n\n[extras]\nPkg "
},
{
"path": "test/pkgs/sample/PkgWithCompatibleTestProject/src/PkgWithCompatibleTestProject.jl",
"chars": 40,
"preview": "module PkgWithCompatibleTestProject end\n"
},
{
"path": "test/pkgs/sample/PkgWithCompatibleTestProject/test/Project.toml",
"chars": 98,
"preview": "[deps]\nPkg = \"44cfe95a-1eb2-52ea-b672-e2afdf69b78f\"\nTest = \"8dfed614-e22c-5e08-85e1-65c5234f0b40\"\n"
},
{
"path": "test/pkgs/sample/PkgWithIncompatibleTestProject/Project.toml",
"chars": 245,
"preview": "name = \"PkgWithIncompatibleTestProject\"\nuuid = \"1649c42c-2196-4c52-9963-79822cd6227b\"\n\n[compat]\njulia = \"1\"\n\n[extras]\nRE"
},
{
"path": "test/pkgs/sample/PkgWithIncompatibleTestProject/src/PkgWithIncompatibleTestProject.jl",
"chars": 42,
"preview": "module PkgWithIncompatibleTestProject end\n"
},
{
"path": "test/pkgs/sample/PkgWithIncompatibleTestProject/test/Project.toml",
"chars": 101,
"preview": "[deps]\nRandom = \"9a3f8284-a2c9-5f02-9a11-845980a1fd5c\"\nTest = \"8dfed614-e22c-5e08-85e1-65c5234f0b40\"\n"
},
{
"path": "test/pkgs/sample/PkgWithPostJulia12Support/Project.toml",
"chars": 240,
"preview": "name = \"PkgWithPostJulia12Support\"\nuuid = \"7231ce0e-e308-4079-b49f-19e33cc3ac6e\"\n\n[compat]\njulia = \"1.2\"\n\n[extras]\nPkg ="
},
{
"path": "test/pkgs/sample/PkgWithPostJulia12Support/src/PkgWithPostJulia12Support.jl",
"chars": 37,
"preview": "module PkgWithPostJulia12Support end\n"
},
{
"path": "test/pkgs/sample/PkgWithPostJulia12Support/test/Project.toml",
"chars": 99,
"preview": "[deps]\nREPL = \"3fa0cd96-eef1-5676-8a61-b3b8758bbffb\"\nTest = \"8dfed614-e22c-5e08-85e1-65c5234f0b40\"\n"
},
{
"path": "test/pkgs/sample/PkgWithoutDeps/Project.toml",
"chars": 175,
"preview": "name = \"PkgWithoutDeps\"\nuuid = \"3922d3f4-c8f6-c8a8-00da-60b44ed8eac6\"\n\n[compat]\njulia = \"1\"\n\n[extras]\nTest = \"8dfed614-e"
},
{
"path": "test/pkgs/sample/PkgWithoutDeps/src/PkgWithoutDeps.jl",
"chars": 26,
"preview": "module PkgWithoutDeps end\n"
},
{
"path": "test/pkgs/sample/PkgWithoutDeps/test/.gitkeep",
"chars": 0,
"preview": ""
},
{
"path": "test/pkgs/sample/PkgWithoutTestProject/Project.toml",
"chars": 182,
"preview": "name = \"PkgWithoutTestProject\"\nuuid = \"8981f3dd-97fd-4684-8ec7-7b0c42f64e2e\"\n\n[compat]\njulia = \"1\"\n\n[extras]\nTest = \"8df"
},
{
"path": "test/pkgs/sample/PkgWithoutTestProject/src/PkgWithoutTestProject.jl",
"chars": 33,
"preview": "module PkgWithoutTestProject end\n"
},
{
"path": "test/pkgs/sample/PkgWithoutTestProject/test/.gitkeep",
"chars": 0,
"preview": ""
},
{
"path": "test/preamble.jl",
"chars": 196,
"preview": "let path = joinpath(@__DIR__, \"pkgs\")\n if path ∉ LOAD_PATH\n pushfirst!(LOAD_PATH, path)\n end\nend\n\nusing Tes"
},
{
"path": "test/runtests.jl",
"chars": 189,
"preview": "module TestAqua\n\nusing Test\n\n@testset \"$file\" for file in sort([\n file for file in readdir(@__DIR__) if match(r\"^test"
},
{
"path": "test/test_ambiguities.jl",
"chars": 2476,
"preview": "module TestAmbiguities\n\ninclude(\"preamble.jl\")\n\n@testset begin\n using PkgWithAmbiguities\n\n using PkgWithAmbiguitie"
},
{
"path": "test/test_deps_compat.jl",
"chars": 3582,
"preview": "module TestDepsCompat\n\ninclude(\"preamble.jl\")\nusing Aqua: find_missing_deps_compat, has_julia_compat\n\nconst DictSA = Dic"
},
{
"path": "test/test_exclude.jl",
"chars": 1436,
"preview": "module TestExclude\n\ninclude(\"preamble.jl\")\nusing Base: PkgId\nusing Aqua: getexclude, normalize_exclude, normalize_exclud"
},
{
"path": "test/test_persistent_tasks.jl",
"chars": 1272,
"preview": "module TestPersistentTasks\n\ninclude(\"preamble.jl\")\nusing Base: PkgId, UUID\nusing Pkg: TOML\n\nfunction getid(name)\n pat"
},
{
"path": "test/test_piracy.jl",
"chars": 4781,
"preview": "push!(LOAD_PATH, joinpath(@__DIR__, \"pkgs\", \"PiracyForeignProject\"))\n\nbaremodule PiracyModule\n\nusing PiracyForeignProjec"
},
{
"path": "test/test_project_extras.jl",
"chars": 2082,
"preview": "module TestProjectExtras\n\ninclude(\"preamble.jl\")\nusing Aqua: is_julia12_or_later\nusing Base: PkgId, UUID\n\n@testset \"is_j"
},
{
"path": "test/test_smoke.jl",
"chars": 343,
"preview": "module TestSmoke\n\nusing Aqua\n\n# test defaults\nAqua.test_all(Aqua)\n\n# test everything else\nAqua.test_all(\n Aqua;\n a"
},
{
"path": "test/test_stale_deps.jl",
"chars": 2693,
"preview": "module TestStaleDeps\n\ninclude(\"preamble.jl\")\nusing Base: PkgId, UUID\nusing Aqua: find_stale_deps_2\n\n@testset \"find_stale"
},
{
"path": "test/test_unbound_args.jl",
"chars": 470,
"preview": "module TestUnboundArgs\n\ninclude(\"preamble.jl\")\n\nusing PkgUnboundArgs\n\n@testset begin\n println(\"### Expected output ST"
},
{
"path": "test/test_undefined_exports.jl",
"chars": 633,
"preview": "module TestUndefinedExports\n\ninclude(\"preamble.jl\")\n\nusing PkgWithUndefinedExports\n\n@testset begin\n @test Aqua.undefi"
},
{
"path": "test/test_undocumented_names.jl",
"chars": 1357,
"preview": "module TestUndocumentedNames\n\ninclude(\"preamble.jl\")\n\nimport PkgWithUndocumentedNames\nimport PkgWithUndocumentedNamesInS"
},
{
"path": "test/test_utils.jl",
"chars": 284,
"preview": "module TestUtils\n\nusing Aqua: askwargs\nusing Test\n\n@testset \"askwargs\" begin\n @test_throws ArgumentError(\"expect `tru"
}
]
About this extraction
This page contains the full source code of the JuliaTesting/Aqua.jl GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 85 files (111.2 KB), approximately 34.2k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.