diff --git a/.JuliaFormatter.toml b/.JuliaFormatter.toml new file mode 100644 index 0000000..d9955b4 --- /dev/null +++ b/.JuliaFormatter.toml @@ -0,0 +1,10 @@ +style = "sciml" +margin = 80 +remove_extra_newlines = true +always_use_return = true +trailing_comma = false +join_lines_based_on_source = true +yas_style_nesting = true +always_for_in = true +annotate_untyped_fields_with_any = true +normalize_line_endings = "unix" diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml new file mode 100644 index 0000000..700707c --- /dev/null +++ b/.github/dependabot.yaml @@ -0,0 +1,7 @@ +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" # Location of package manifests + schedule: + interval: "weekly" diff --git a/.github/workflows/CompatHelper.yml b/.github/workflows/CompatHelper.yaml similarity index 87% rename from .github/workflows/CompatHelper.yml rename to .github/workflows/CompatHelper.yaml index 2f2eb51..5053ff7 100644 --- a/.github/workflows/CompatHelper.yml +++ b/.github/workflows/CompatHelper.yaml @@ -19,7 +19,7 @@ jobs: os: - ubuntu-latest steps: - - uses: julia-actions/setup-julia@latest + - uses: julia-actions/setup-julia@v2 with: version: ${{ matrix.julia-version }} arch: ${{ matrix.arch }} @@ -28,7 +28,7 @@ jobs: import Pkg name = "CompatHelper" uuid = "aa819f21-2bde-4658-8897-bab36330d9b7" - version = "2" + version = "3" Pkg.add(; name, uuid, version) shell: julia --color=yes {0} - name: "Run CompatHelper" @@ -38,3 +38,4 @@ jobs: shell: julia --color=yes {0} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + COMPATHELPER_PRIV: ${{ secrets.DOCUMENTER_KEY }} diff --git a/.github/workflows/TagBot.yaml b/.github/workflows/TagBot.yaml new file mode 100644 index 0000000..0cd3114 --- /dev/null +++ b/.github/workflows/TagBot.yaml @@ -0,0 +1,31 @@ +name: TagBot +on: + issue_comment: + types: + - created + workflow_dispatch: + inputs: + lookback: + default: "3" +permissions: + actions: read + checks: read + contents: write + deployments: read + issues: read + discussions: read + packages: read + pages: read + pull-requests: read + repository-projects: read + security-events: read + statuses: read +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.DOCUMENTER_KEY }} diff --git a/.github/workflows/TagBot.yml b/.github/workflows/TagBot.yml deleted file mode 100644 index 5a00d3a..0000000 --- a/.github/workflows/TagBot.yml +++ /dev/null @@ -1,15 +0,0 @@ -name: TagBot -on: - issue_comment: - types: - - created - workflow_dispatch: - -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 }} diff --git a/.github/workflows/metadata.yaml b/.github/workflows/metadata.yaml new file mode 100644 index 0000000..ffb064e --- /dev/null +++ b/.github/workflows/metadata.yaml @@ -0,0 +1,38 @@ +name: Metadata and hygene + +on: + push: + branches: + - main + tags: + - 'v*' + pull_request: + workflow_dispatch: + +permissions: # needed to allow julia-actions/cache to proactively delete old caches that it has created + actions: write + contents: read + +jobs: + metadata: + name: RSMD - ${{ github.event_name }} + timeout-minutes: 60 + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Set up julia + uses: julia-actions/setup-julia@v2 + with: + version: '1' + arch: x64 + - name: Cache + uses: julia-actions/cache@v2 + - name: Build package + uses: julia-actions/julia-buildpkg@v1 + - name: Running + uses: julia-actions/julia-runtest@v1 + env: + RSMD_CROSSWALK: TRUE diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yaml similarity index 62% rename from .github/workflows/nightly.yml rename to .github/workflows/nightly.yaml index 16e89d4..bb2a335 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yaml @@ -1,13 +1,10 @@ name: JuliaNightly # Nightly Scheduled Julia Nightly Run + on: - push: - branches: - - main - tags: - - 'v*' schedule: - cron: '0 2 * * 0' # Weekly at 2 AM UTC Sunday + workflow_dispatch: jobs: test: @@ -15,13 +12,15 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Set up julia - uses: julia-actions/setup-julia@v1 + uses: julia-actions/setup-julia@v2 with: version: nightly arch: x64 - name: Build package - uses: julia-actions/julia-buildpkg@latest + uses: julia-actions/julia-buildpkg@v1 + with: + ignore-no-cache: true - name: Run tests - uses: julia-actions/julia-runtest@latest + uses: julia-actions/julia-runtest@v1 diff --git a/.github/workflows/testing.yaml b/.github/workflows/testing.yaml new file mode 100644 index 0000000..03a28ba --- /dev/null +++ b/.github/workflows/testing.yaml @@ -0,0 +1,69 @@ +name: CI + +on: + push: + branches: + - main + tags: + - 'v*' + pull_request: + +permissions: # needed to allow julia-actions/cache to proactively delete old caches that it has created + actions: write + contents: read + +jobs: + Diversity-tests: + name: Julia ${{ matrix.julia-version }} - ${{ matrix.os }} - ${{ matrix.arch }} - ${{ github.event_name }} + timeout-minutes: 60 + runs-on: ${{ matrix.os }} + continue-on-error: false + strategy: + matrix: + julia-version: + - '1.6' + - '1.8' + - '1' + os: + - ubuntu-latest + - macOS-latest + - windows-latest + arch: + - x64 + - arm64 + exclude: + - os: macOS-latest + julia-version: '1.6' + arch: arm64 + - os: macOS-latest + julia-version: '1.8' + arch: arm64 + - os: macOS-latest + julia-version: '1' + arch: x64 + - os: ubuntu-latest + arch: arm64 + - os: windows-latest + arch: arm64 + fail-fast: false + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Set up julia + uses: julia-actions/setup-julia@v2 + with: + version: ${{ matrix.julia-version }} + arch: ${{ matrix.arch }} + - name: Cache + uses: julia-actions/cache@v2 + - name: Build package + uses: julia-actions/julia-buildpkg@v1 + - name: Running + uses: julia-actions/julia-runtest@v1 + - name: Process coverage + uses: julia-actions/julia-processcoverage@v1 + - name: Codecov + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + verbose: true diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml deleted file mode 100644 index dff91f5..0000000 --- a/.github/workflows/testing.yml +++ /dev/null @@ -1,50 +0,0 @@ -name: CI - -on: - push: - branches: - - main - tags: - - 'v*' - pull_request: - -jobs: - EcoBase-tests: - runs-on: ${{ matrix.os }} - continue-on-error: ${{ matrix.experimental }} - strategy: - matrix: - julia-version: - - '1' - os: - - ubuntu-latest - - macOS-latest - - windows-latest - R-version: - - 'release' - arch: - - x64 - experimental: - - false - fail-fast: false - steps: - - name: Checkout code - uses: actions/checkout@v2 - - name: Set up julia - uses: julia-actions/setup-julia@v1 - with: - version: ${{ matrix.julia-version }} - arch: ${{ matrix.arch }} - - name: Build package - uses: julia-actions/julia-buildpkg@master - - name: Running - uses: julia-actions/julia-runtest@master - - name: Process coverage - uses: julia-actions/julia-processcoverage@v1 - - name: Coveralls - uses: coverallsapp/github-action@master - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - path-to-lcov: ./lcov.info - - name: Codecov - uses: codecov/codecov-action@v1 diff --git a/.gitignore b/.gitignore index c35353a..df2fefe 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ *.jl.mem .DS_Store Manifest.toml +.vscode/settings.json diff --git a/.zenodo.json b/.zenodo.json index a4f6677..e269ea5 100644 --- a/.zenodo.json +++ b/.zenodo.json @@ -1,30 +1,41 @@ { - "title": "EcoBase.jl - A package implementing an abstract interface to data types used in ecological analyses", - "description": "This package implements an abstract interface to data types used in ecological analyses in Julia", + "title": "EcoBase.jl", "upload_type": "software", "creators": [ { - "affiliation": "GLOBE Institute, University of Copenhagen", - "name": "Borregaard, Michael Krabbe", - "orcid": "0000-0002-8146-8435" + "name": "Borregaard, Michael", + "orcid": "0000-0002-8146-8435", + "affiliation": "University of Copenhagen", + "ror": "035b05819" }, { - "affiliation": "University of Glasgow", "name": "Reeve, Richard", - "orcid": "0000-0003-2589-8091" + "orcid": "0000-0003-2589-8091", + "affiliation": "University of Glasgow", + "ror": "00vtgdb53" + }, + { + "name": "Bonham, Kevin", + "orcid": "0000-0003-3200-7533", + "affiliation": "Wellesley College", + "ror": "01srpnj69" } ], "access_right": "open", - "license": "mit-license", + "license": "MIT", "related_identifiers": [ { "scheme": "url", "identifier": "https://github.com/EcoJulia/EcoBase.jl", - "relation": "isIdenticalTo" + "relation": "isOriginalFormOf" } ], "keywords": [ + "EcoJulia", + "biology", + "ecology", + "geography", "julia", - "ecology" + "macroecology" ] } diff --git a/LICENSE b/LICENSE index 8d888f7..aca8d78 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,9 @@ MIT License -Copyright (c) 2018-2021: Michael K. Borregaard, Richard Reeve and EcoJulia +Copyright (c) 2018-2024 Michael Borregaard, Richard Reeve and Kevin Bonham -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: +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 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. +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. diff --git a/Project.toml b/Project.toml index 24bad1d..e9ade11 100644 --- a/Project.toml +++ b/Project.toml @@ -1,24 +1,55 @@ name = "EcoBase" uuid = "a58aae7d-b440-5a11-b283-399458f99aac" -keywords = ["macroecology", "ecology", "biology", "geography"] license = "MIT" +authors = ["Michael Borregaard", "Richard Reeve ", "Kevin Bonham "] +version = "0.1.7" +keywords = ["macroecology", "ecology", "biology", "geography", "julia", "EcoJulia"] desc = "Base package with abstract types for ecology" -authors = ["mkborregaard ", "Kevin Bonham "] -version = "0.1.6" [deps] RecipesBase = "3cdcf5f2-1ef4-517c-9805-6587b60abb01" [compat] Diversity = "0.5" +Git = "1" +JuliaFormatter = "1" +Logging = "1" +Pkg = "1" +Random = "1" RecipesBase = "0.7, 0.8, 0.9, 1" +ResearchSoftwareMetadata = "0.1.1" SpatialEcology = "0.9" julia = "1" +[[author_details]] +name = "Michael Borregaard" +orcid = "0000-0002-8146-8435" + + [[author_details.affiliation]] + ror = "035b05819" +[[author_details]] +name = "Richard Reeve" +orcid = "0000-0003-2589-8091" + + [[author_details.affiliation]] + ror = "00vtgdb53" +[[author_details]] +name = "Kevin Bonham" +orcid = "0000-0003-3200-7533" + + [[author_details.affiliation]] + ror = "01srpnj69" + [extras] Diversity = "d3d5718d-52de-57ab-b67a-eca7fd6175a4" +Git = "d7ba0133-e1db-5d97-8f8c-041e4b3a1eb2" +JuliaFormatter = "98e50ef6-434e-11e9-1051-2b60c6c9e899" +Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" +Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" +Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +ResearchSoftwareMetadata = "58378933-4625-47fa-851e-05ee27d397bd" SpatialEcology = "348f2d5d-71a3-5ad4-b565-8af070f99681" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["SpatialEcology", "Diversity", "Test"] +test = ["SpatialEcology", "Diversity", "Git", "JuliaFormatter", "Logging", "Pkg", "Random", "ResearchSoftwareMetadata", "Test"] diff --git a/codemeta.json b/codemeta.json new file mode 100644 index 0000000..580814f --- /dev/null +++ b/codemeta.json @@ -0,0 +1,78 @@ +{ + "@context": "https://w3id.org/codemeta/3.0", + "type": "SoftwareSourceCode", + "applicationCategory": "ecology", + "programmingLanguage": "julia", + "developmentStatus": "active", + "codeRepository": "https://github.com/EcoJulia/EcoBase.jl", + "name": "EcoBase.jl", + "issueTracker": "https://github.com/EcoJulia/EcoBase.jl/issues", + "readme": "https://github.com/EcoJulia/EcoBase.jl/blob/main/README.md", + "buildInstructions": "https://github.com/EcoJulia/EcoBase.jl/blob/main/README.md", + "dateCreated": "2018-01-30", + "operatingSystem": [ + "Linux", + "Windows", + "macOS" + ], + "version": "v0.1.7", + "dateModified": "2024-07-22", + "datePublished": "2018-09-20", + "downloadUrl": "https://github.com/EcoJulia/EcoBase.jl/archive/refs/tags/v0.1.7.tar.gz", + "license": "https://spdx.org/licenses/MIT", + "author": [ + { + "type": "Person", + "givenName": "Michael", + "familyName": "Borregaard", + "id": "https://orcid.org/0000-0002-8146-8435", + "affiliation": [ + { + "type": "Organization", + "name": "University of Copenhagen", + "identifier": "https://ror.org/035b05819" + } + ] + }, + { + "type": "Person", + "givenName": "Richard", + "familyName": "Reeve", + "email": "richard.reeve@glasgow.ac.uk", + "id": "https://orcid.org/0000-0003-2589-8091", + "affiliation": [ + { + "type": "Organization", + "name": "University of Glasgow", + "identifier": "https://ror.org/00vtgdb53" + } + ] + }, + { + "type": "Person", + "givenName": "Kevin", + "familyName": "Bonham", + "email": "kbonham@wellesley.edu", + "id": "https://orcid.org/0000-0003-3200-7533", + "affiliation": [ + { + "type": "Organization", + "name": "Wellesley College", + "identifier": "https://ror.org/01srpnj69" + } + ] + } + ], + "continuousIntegration": "https://github.com/EcoJulia/EcoBase.jl/actions/workflows/testing.yaml", + "codemeta:contIntegration": { + "id": "https://github.com/EcoJulia/EcoBase.jl/actions/workflows/testing.yaml" + }, + "keywords": [ + "EcoJulia", + "biology", + "ecology", + "geography", + "julia", + "macroecology" + ] +} diff --git a/docs/Project.toml b/docs/Project.toml new file mode 100644 index 0000000..510100f --- /dev/null +++ b/docs/Project.toml @@ -0,0 +1,8 @@ +[deps] +DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" +Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +EcoBase = "a58aae7d-b440-5a11-b283-399458f99aac" +JuliaFormatter = "98e50ef6-434e-11e9-1051-2b60c6c9e899" +Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" +ResearchSoftwareMetadata = "58378933-4625-47fa-851e-05ee27d397bd" diff --git a/docs/metadata.jl b/docs/metadata.jl new file mode 100644 index 0000000..e0e6d56 --- /dev/null +++ b/docs/metadata.jl @@ -0,0 +1,34 @@ +# SPDX-License-Identifier: MIT + +using Pkg + +# Update EcoBase folder packages +Pkg.activate(".") +Pkg.update() + +# Update examples folder packages +if isdir("examples") + if isfile("examples/Project.toml") + Pkg.activate("examples") + Pkg.update() + "EcoBase" ∈ [p.name for p in values(Pkg.dependencies())] && + Pkg.rm("EcoBase") + Pkg.develop("EcoBase") + end +end + +# Update docs folder packages +Pkg.activate("docs") +Pkg.update() +"EcoBase" ∈ [p.name for p in values(Pkg.dependencies())] && + Pkg.rm("EcoBase") +Pkg.develop("EcoBase") + +# Reformat files in package +using JuliaFormatter +using EcoBase +format(EcoBase) + +# Carry out crosswalk for metadata +using ResearchSoftwareMetadata +ResearchSoftwareMetadata.crosswalk() diff --git a/src/DataTypes.jl b/src/DataTypes.jl index aa7f531..25f46b3 100644 --- a/src/DataTypes.jl +++ b/src/DataTypes.jl @@ -1,3 +1,4 @@ +# SPDX-License-Identifier: MIT """ AbstractThings @@ -36,7 +37,8 @@ a subtype of AbstractLocationData if they have spatial data. Other metadata in the AbstractPlaces subtype should be in the AbstractPlaces subtype. """ -abstract type AbstractPlaces{LocationDataType <: Union{Nothing, AbstractLocationData}} end +abstract type AbstractPlaces{LocationDataType <: + Union{Nothing, AbstractLocationData}} end """ AbstractPoints <: AbstractLocationData diff --git a/src/EcoBase.jl b/src/EcoBase.jl index e3063e1..cb7e75e 100644 --- a/src/EcoBase.jl +++ b/src/EcoBase.jl @@ -1,8 +1,13 @@ +# SPDX-License-Identifier: MIT + module EcoBase import Base: show, view import RecipesBase +# Path into package +path(path...; dir::String = "test") = joinpath(@__DIR__, "..", dir, path...) + include("DataTypes.jl") include("Interface.jl") include("PlotRecipes.jl") @@ -10,7 +15,8 @@ include("PlotRecipes.jl") export nthings, nplaces, occupancy, richness, nrecords, placenames, thingnames export occurring, noccurring, occupied, noccupied, occurrences export placeoccurrences, thingoccurrences, cooccurring, places, things -export asindices, indices, coordinates, xcells, ycells, cells, xmin, xmax, ymin, ymax +export asindices, indices, coordinates, xcells, ycells, cells, xmin, xmax, ymin, + ymax export xrange, yrange, xcellsize, ycellsize, cellsize, getcoords end # module diff --git a/src/Interface.jl b/src/Interface.jl index b200d7a..e6b99d9 100644 --- a/src/Interface.jl +++ b/src/Interface.jl @@ -1,11 +1,28 @@ +# SPDX-License-Identifier: MIT + asindices(x::Integer) = x -asindices(x::AbstractArray{T}) where T <: Union{Missing, Integer} = x -asindices(x::AbstractArray{Union{Missing, Bool}}) = findall(y->!ismissing(y) && y, x) -asindices(x::AbstractArray{T}) where T <: Bool = findall(x) +asindices(x::AbstractArray{T}) where {T <: Union{Missing, Integer}} = x +function asindices(x::AbstractArray{Union{Missing, Bool}}) + return findall(y -> !ismissing(y) && y, x) +end +asindices(x::AbstractArray{T}) where {T <: Bool} = findall(x) asindices(x, y) = asindices(x) -asindices(x::AbstractArray{T}, y::AbstractArray{T}) where T <: Union{Missing, AbstractString} = [el for el in indexin(x, y) if el !== nothing] -asindices(x::AbstractArray{T}, y::AbstractArray{<:AbstractString}) where T <: Union{Missing, Symbol} = asindices(string.(x), y) -asindices(x::T, y::AbstractArray) where T <: Union{Missing, Symbol, AbstractString} = first(asindices([x], y)) +function asindices(x::AbstractArray{T}, + y::AbstractArray{T}) where {T <: + Union{Missing, AbstractString}} + return [el for el in indexin(x, y) if el !== nothing] +end +function asindices(x::AbstractArray{T}, + y::AbstractArray{<:AbstractString}) where {T <: + Union{Missing, + Symbol}} + return asindices(string.(x), y) +end +function asindices(x::T, + y::AbstractArray) where {T <: Union{Missing, Symbol, + AbstractString}} + return first(asindices([x], y)) +end # Functions - most have to be implemented with the concrete type occurrences(asm::AbstractAssemblage)::AbstractMatrix = error("function not defined for $(typeof(asm))") @@ -19,7 +36,6 @@ placekind(asm::AbstractAssemblage) = "place" thingkindplural(asm::AbstractAssemblage) = "$(thingkind(asm))s" placekindplural(asm::AbstractAssemblage) = "$(placekind(asm))s" - nplaces(plc::AbstractPlaces)::Integer = error("function not defined for $(typeof(plc))") nplaces(plc::AbstractAssemblage) = nplaces(places(plc)) placenames(plc::AbstractPlaces)::AbstractVector{<:String} = error("function not defined for $(typeof(plc))") @@ -37,11 +53,15 @@ colsum(x) = sum(x, dims = 1) rowsum(x) = sum(x, dims = 2) occurring(asm::AbstractAssemblage) = occurring(occurrences(asm)) -occurring(asm::AbstractAssemblage, idx) = occurring(occurrences(asm), asindices(idx, placenames(asm))) +function occurring(asm::AbstractAssemblage, idx) + return occurring(occurrences(asm), asindices(idx, placenames(asm))) +end occurring(a::AbstractMatrix) = nzrows(a) occupied(asm::AbstractAssemblage) = occupied(occurrences(asm)) -occupied(asm::AbstractAssemblage, idx) = occupied(occurrences(asm), asindices(idx, thingnames(asm))) +function occupied(asm::AbstractAssemblage, idx) + return occupied(occurrences(asm), asindices(idx, thingnames(asm))) +end occupied(a::AbstractMatrix) = nzcols(a) occupied(a::AbstractMatrix, idx) = findall(!iszero, a[idx, :]) @@ -54,9 +74,13 @@ noccupied(x) = length(occupied(x)) noccurring(x, idx) = length(occurring(x, idx)) noccupied(x, idx) = length(occupied(x, idx)) -thingoccurrences(asm::AbstractAssemblage, idx) = thingoccurrences(occurrences(asm), asindices(idx, thingnames(asm))) +function thingoccurrences(asm::AbstractAssemblage, idx) + return thingoccurrences(occurrences(asm), asindices(idx, thingnames(asm))) +end thingoccurrences(mat::AbstractMatrix, idx) = view(mat, idx, :) -placeoccurrences(asm::AbstractAssemblage, idx) = placeoccurrences(occurrences(asm), asindices(idx, placenames(asm))) +function placeoccurrences(asm::AbstractAssemblage, idx) + return placeoccurrences(occurrences(asm), asindices(idx, placenames(asm))) +end placeoccurrences(mat::AbstractMatrix, idx) = view(mat, :, idx) # make certain that the view implementation also takes thing or place names richness(asm::AbstractAssemblage) = richness(occurrences(asm)) @@ -65,7 +89,7 @@ richness(a::AbstractMatrix) = collect(vec(mapslices(nnz, a, dims = 1))) occupancy(asm::AbstractAssemblage) = occupancy(occurrences(asm)) occupancy(a::AbstractMatrix{Bool}) = collect(vec(rowsum(a))) -occupancy(a::AbstractMatrix) = collect(vec(mapslices(nnz, a, dims=2))) +occupancy(a::AbstractMatrix) = collect(vec(mapslices(nnz, a, dims = 2))) nrecords(asm::AbstractAssemblage) = nrecords(occurrences(asm)) nrecords(a::AbstractMatrix) = nnz(a) @@ -73,17 +97,17 @@ nrecords(a::AbstractMatrix) = nnz(a) cooccurring(asm, inds...) = cooccurring(asm, [inds...]) function cooccurring(asm, inds::AbstractVector) sub = view(asm, species = inds) - richness(sub) .== nthings(sub) + return richness(sub) .== nthings(sub) end function createsummaryline(vec::AbstractVector{<:AbstractString}) - linefunc(vec) = mapreduce(x -> x * ", ", *, vec[1:(end-1)]) * vec[end] + linefunc(vec) = mapreduce(x -> x * ", ", *, vec[1:(end - 1)]) * vec[end] length(vec) == 1 && return vec[1] length(vec) < 6 && return linefunc(vec) - linefunc(vec[1:3]) * "..." * linefunc(vec[(end-1):end]) + return linefunc(vec[1:3]) * "..." * linefunc(vec[(end - 1):end]) end -function show(io::IO, asm::T) where T <: AbstractAssemblage +function show(io::IO, asm::T) where {T <: AbstractAssemblage} tn = createsummaryline(thingnames(asm)) pn = createsummaryline(placenames(asm)) thing = titlecase(thingkind(asm)) @@ -91,14 +115,15 @@ function show(io::IO, asm::T) where T <: AbstractAssemblage place = titlecase(placekind(asm)) places = placekindplural(asm) println(io, - """$T with $(nthings(asm)) $things in $(nplaces(asm)) $places + """$T with $(nthings(asm)) $things in $(nplaces(asm)) $places - $thing names: - $(tn) + $thing names: + $(tn) - $place names: - $(pn) - """) + $place names: + $(pn) + """) + return nothing end nplaces(asm::AbstractAssemblage, args...) = nplaces(places(asm), args...) @@ -106,15 +131,17 @@ placenames(asm::AbstractAssemblage, args...) = placenames(places(asm), args...) nthings(asm::AbstractAssemblage, args...) = nthings(things(asm), args...) thingnames(asm::AbstractAssemblage, args...) = thingnames(things(asm), args...) - # TODO: # accessing cache # Methods for AbstractPlaces getcoords(plc::AbstractPlaces{Nothing}) = plc # Pure places generate their own fake location data -getcoords(plc::AbstractPlaces{<: AbstractLocationData}) = - error("function not defined for $(typeof(plc))") -coordinates(plc::AbstractPlaces) = error("function not defined for $(typeof(plc))") +function getcoords(plc::AbstractPlaces{<:AbstractLocationData}) + return error("function not defined for $(typeof(plc))") +end +function coordinates(plc::AbstractPlaces) + return error("function not defined for $(typeof(plc))") +end # Methods for AbstractGrid xmin(grd::AbstractGrid) = error("function not defined for $(typeof(grd))") @@ -131,5 +158,9 @@ xmax(grd) = xmin(grd) + xcellsize(grd) * (xcells(grd) - 1) ymax(grd) = ymin(grd) + ycellsize(grd) * (ycells(grd) - 1) indices(grd::AbstractGrid) = error("function not defined for $(typeof(grd))") -indices(grd::AbstractGrid, idx) = error("function not defined for $(typeof(grd))") -coordinates(grd::AbstractGrid) = error("function not defined for $(typeof(grd))") +function indices(grd::AbstractGrid, idx) + return error("function not defined for $(typeof(grd))") +end +function coordinates(grd::AbstractGrid) + return error("function not defined for $(typeof(grd))") +end diff --git a/src/PlotRecipes.jl b/src/PlotRecipes.jl index 56de9bd..192e099 100644 --- a/src/PlotRecipes.jl +++ b/src/PlotRecipes.jl @@ -1,18 +1,18 @@ - +# SPDX-License-Identifier: MIT function convert_to_image(var::AbstractVector, grd::AbstractGrid) x = Matrix{Float64}(undef, reverse(cells(grd))...) fill!(x, NaN) - xind, yind = indices(grd, 1), indices(grd,2) #since matrices are drawn from upper left corner + xind, yind = indices(grd, 1), indices(grd, 2) #since matrices are drawn from upper left corner [x[yind[i], xind[i]] = val for (i, val) in enumerate(var)] - x + return x end RecipesBase.@recipe function f(var::AbstractVector, grd::AbstractGrid) seriestype := :heatmap aspect_ratio --> :equal grid --> false - xrange(grd), yrange(grd), convert_to_image(var, grd) + return xrange(grd), yrange(grd), convert_to_image(var, grd) end # RecipesBase.@recipe function f(sit::SiteFields) # not sure what SiteFields are @@ -27,22 +27,22 @@ RecipesBase.@recipe function f(var::AbstractVector, pnt::AbstractPoints) legend --> false colorbar --> true cd = coordinates(pnt) - cd[:,1], cd[:,2] + return cd[:, 1], cd[:, 2] end RecipesBase.@recipe function f(asm::AbstractAssemblage; showempty = false) var = richness(asm) if !showempty var = [Float64(v) for v in var] - (var[var.==0] .= NaN) + (var[var .== 0] .= NaN) end - var, getcoords(places(asm)) + return var, getcoords(places(asm)) end RecipesBase.@recipe function f(var::AbstractVector, asm::AbstractAssemblage) - var, getcoords(places(asm)) + return var, getcoords(places(asm)) end RecipesBase.@recipe function f(g::Function, asm::AbstractAssemblage) - g(asm), getcoords(places(asm)) + return g(asm), getcoords(places(asm)) end diff --git a/test/clean_JuliaFormatter.jl b/test/clean_JuliaFormatter.jl new file mode 100644 index 0000000..b350161 --- /dev/null +++ b/test/clean_JuliaFormatter.jl @@ -0,0 +1,42 @@ +# SPDX-License-Identifier: MIT + +module CleanJuliaFormatter +using Test +using EcoBase +using Git +using Logging +using Pkg +using JuliaFormatter + +function is_repo_clean(repo_path; ignore_untracked = true) + # Get the status of the repository + statuses = readlines(`$(Git.git()) status -s $repo_path`) + + if ignore_untracked + # Repo must be clean except for untracked files + statuses = filter((!) ∘ contains("??"), statuses) + end + + is_clean = isempty(statuses) + + # If not clean then report on dirty files + is_clean || @error "\n" * join(statuses, "\n") + + return is_clean +end + +# Metadata crosswalk testing only works on Julia v1.8 and after due to Project.toml changes +# Also does not currently work on Windows runners on GitHub due to file writing issues +if VERSION ≥ VersionNumber("1.8.0") && + (!haskey(ENV, "RUNNER_OS") || ENV["RUNNER_OS"] ≠ "Windows") + @testset "JuliaFormatter" begin + git_dir = readchomp(`$(Git.git()) rev-parse --show-toplevel`) + @test_nowarn format(EcoBase) + @test is_repo_clean(git_dir) + end +else + @test_broken VERSION ≥ VersionNumber("1.8.0") && + (!haskey(ENV, "RUNNER_OS") || ENV["RUNNER_OS"] ≠ "Windows") +end + +end diff --git a/test/clean_ResearchSoftwareMetadata.jl b/test/clean_ResearchSoftwareMetadata.jl new file mode 100644 index 0000000..2d51443 --- /dev/null +++ b/test/clean_ResearchSoftwareMetadata.jl @@ -0,0 +1,44 @@ +# SPDX-License-Identifier: MIT + +module CleanRSMD +using Test +using EcoBase +using Git +using Logging +using Pkg +using ResearchSoftwareMetadata + +function is_repo_clean(repo_path; ignore_untracked = true) + # Get the status of the repository + statuses = readlines(`$(Git.git()) status -s $repo_path`) + + if ignore_untracked + # Repo must be clean except for untracked files + statuses = filter((!) ∘ contains("??"), statuses) + end + + is_clean = isempty(statuses) + + # If not clean then report on dirty files + is_clean || @error "\n" * join(statuses, "\n") + + return is_clean +end + +# Metadata crosswalk testing only works on Julia v1.8 and after due to Project.toml changes +# Also does not currently work on Windows runners on GitHub due to file writing issues +if VERSION ≥ VersionNumber("1.8.0") && + (!haskey(ENV, "RUNNER_OS") || ENV["RUNNER_OS"] ≠ "Windows") + @testset "RSMD" begin + git_dir = readchomp(`$(Git.git()) rev-parse --show-toplevel`) + @test isnothing(ResearchSoftwareMetadata.crosswalk()) + global_logger(SimpleLogger(stderr, Logging.Warn)) + @test_nowarn ResearchSoftwareMetadata.crosswalk() + @test is_repo_clean(git_dir) + end +else + @test_broken VERSION ≥ VersionNumber("1.8.0") && + (!haskey(ENV, "RUNNER_OS") || ENV["RUNNER_OS"] ≠ "Windows") +end + +end diff --git a/test/runtests.jl b/test/runtests.jl index bebe740..8f678b2 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,50 +1,114 @@ +# SPDX-License-Identifier: MIT + +using Random using Test +using EcoBase +using Pkg -# Identify files in test/ that are testing matching files in src/ -# - src/Source.jl will be matched by test/test_Source.jl +rsmd = get(ENV, "RSMD_CROSSWALK", "FALSE") -filebase = String[] -for (root, dirs, files) in walkdir("../src") - append!(filebase, - map(file -> replace(file, r"(.*).jl" => s"\1"), - filter(file -> occursin(r".*\.jl", file), files))) -end +if rsmd == "FALSE" + # Normal testing + + # Identify files in test/ that are testing matching files in src/ + # - src/Source.jl will be matched by test/test_Source.jl + filebase = String[] + for (root, dirs, files) in walkdir("../src") + append!(filebase, + map(file -> replace(file, r"(.*).jl" => s"\1"), + filter(file -> occursin(r".*\.jl", file), files))) + end -testbase = map(file -> replace(file, r"test_(.*).jl" => s"\1"), - filter(str -> occursin(r"^test_.*\.jl$", str), readdir())) + testbase = map(file -> replace(file, r"test_(.*).jl" => s"\1"), + filter(str -> occursin(r"^test_.*\.jl$", str), readdir())) -@testset "EcoBase.jl" begin - println() - @info "Running tests for files:" - for t in testbase - println(" = $t.jl") + # Identify tests with no matching file + superfluous = filter(f -> f ∉ filebase, testbase) + if length(superfluous) > 0 + println() + @info "Potentially superfluous tests:" + for f in superfluous + println(" + $f.jl") + end + println() end - println() - @info "Running tests..." - @testset for t in testbase - fn = "test_$t.jl" - println(" * Testing $t.jl ...") - include(fn) + # Identify files with no matching test + notest = filter(f -> f ∉ testbase, filebase) + if length(notest) > 0 + println() + @info "Potentially missing tests:" + for f in notest + println(" - $f.jl") + end + println() end -end -# Identify tests with no matching file -superfluous = filter(f -> f ∉ filebase, testbase) -if length(superfluous) > 0 - println() - @info "Potentially superfluous tests:" - for f in superfluous - println(" + $f.jl") + # Seed RNG to make tests reproducible + Random.seed!(1234) + + @testset "EcoBase.jl" begin + @test isfile(EcoBase.path("runtests.jl")) + println() + @info "Running tests for files:" + for t in testbase + println(" = $t.jl") + end + println() + + @info "Running tests..." + @testset for t in testbase + fn = "test_$t.jl" + println(" * Testing $t.jl ...") + include(fn) + end + end + + # Identify files that are cross-validating results against other packages + # test/pkg_Package.jl should validate results against the Package package + + pkgbase = map(file -> replace(file, r"pkg_(.*).jl$" => s"\1"), + filter(str -> occursin(r"^pkg_.*\.jl$", str), + readdir())) + + if length(pkgbase) > 0 + @info "Cross validation packages:" + @testset begin + for p in pkgbase + println(" = $p") + end + println() + + @testset for p in pkgbase + fn = "pkg_$p.jl" + println(" * Validating $p.jl ...") + include(fn) + end + end end end -# Identify files with no matching test -notest = filter(f -> f ∉ testbase, filebase) -if length(notest) > 0 - println() - @info "Potentially missing tests:" - for f in notest - println(" - $f.jl") +if rsmd == "TRUE" || !haskey(ENV, "RUNNER_OS") # Crosswalk runner or local testing + # Test RSMD crosswalk and other hygene issues + + # Identify files that are checking package hygene + cleanbase = map(file -> replace(file, r"clean_(.*).jl$" => s"\1"), + filter(str -> occursin(r"^clean_.*\.jl$", str), + readdir())) + + if length(cleanbase) > 0 + @info "Crosswalk and clean testing:" + @testset begin + for c in cleanbase + println(" = $c") + end + println() + + @testset for c in cleanbase + fn = "clean_$c.jl" + println(" * Verifying $c.jl ...") + include(fn) + end + end end end diff --git a/test/test_Interface.jl b/test/test_Interface.jl index ddd368d..74b32cb 100644 --- a/test/test_Interface.jl +++ b/test/test_Interface.jl @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: MIT + module TestInterface using Test @@ -21,8 +23,10 @@ using EcoBase @test all(EcoBase.thingnames(mc) .== species) @test all(EcoBase.placenames(mc) .== communities) @test all(EcoBase.occurrences(mc) .≈ manyweights) - @test all(EcoBase.richness(mc) .== repeat([numspecies], inner=numcommunities)) - @test all(EcoBase.occupancy(mc) .== repeat([numcommunities], inner=numspecies)) + @test all(EcoBase.richness(mc) .== + repeat([numspecies], inner = numcommunities)) + @test all(EcoBase.occupancy(mc) .== + repeat([numcommunities], inner = numspecies)) fewerweights = deepcopy(manyweights) fewerweights[1, 1] = 0 fewerweights /= sum(fewerweights) @@ -51,8 +55,8 @@ end @test all(thingnames(mc) .== species) @test all(placenames(mc) .== communities) @test all(occurrences(mc) .≈ manyweights) - @test all(richness(mc) .== repeat([numspecies], inner=numcommunities)) - @test all(occupancy(mc) .== repeat([numcommunities], inner=numspecies)) + @test all(richness(mc) .== repeat([numspecies], inner = numcommunities)) + @test all(occupancy(mc) .== repeat([numcommunities], inner = numspecies)) fewerweights = deepcopy(manyweights) fewerweights[1, 1] = 0 fewerweights /= sum(fewerweights)