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]) [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 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 [![Stable](https://img.shields.io/badge/docs-stable-blue.svg)](https://juliatesting.github.io/Aqua.jl/stable) [![Dev](https://img.shields.io/badge/docs-dev-blue.svg)](https://juliatesting.github.io/Aqua.jl/dev) [![GitHub Actions](https://github.com/JuliaTesting/Aqua.jl/workflows/Run%20tests/badge.svg)](https://github.com/JuliaTesting/Aqua.jl/actions?query=workflow%3ARun+tests) [![Codecov](https://codecov.io/gh/JuliaTesting/Aqua.jl/branch/master/graph/badge.svg)](https://codecov.io/gh/JuliaTesting/Aqua.jl) [![Aqua QA](https://juliatesting.github.io/Aqua.jl/dev/assets/badge.svg)](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 [![Aqua QA](https://juliatesting.github.io/Aqua.jl/dev/assets/badge.svg)](https://github.com/JuliaTesting/Aqua.jl) ``` which is rendered as > [![Aqua QA](https://juliatesting.github.io/Aqua.jl/dev/assets/badge.svg)](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 [![Aqua QA](https://juliatesting.github.io/Aqua.jl/dev/assets/badge.svg)](https://github.com/JuliaTesting/Aqua.jl) ``` which is rendered as [![Aqua QA](https://juliatesting.github.io/Aqua.jl/dev/assets/badge.svg)](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