From 42850af80e5501db308ce93de723eba20312f977 Mon Sep 17 00:00:00 2001 From: David Anthoff Date: Tue, 17 Jan 2023 11:21:51 -0800 Subject: [PATCH] Squashed 'packages/TestItemDetection/' content from commit 2c59c96 git-subtree-dir: packages/TestItemDetection git-subtree-split: 2c59c96cf62ab84bbaf9f94364fe0fb6c9a34979 --- .../workflows/jlpkgbutler-butler-workflow.yml | 22 ++ .../jlpkgbutler-ci-master-workflow.yml | 40 ++++ .../workflows/jlpkgbutler-ci-pr-workflow.yml | 36 ++++ .../jlpkgbutler-codeformat-pr-workflow.yml | 23 ++ .../jlpkgbutler-compathelper-workflow.yml | 20 ++ .../workflows/jlpkgbutler-tagbot-workflow.yml | 17 ++ .jlpkgbutler.toml | 1 + LICENSE | 21 ++ Project.toml | 18 ++ README.md | 5 + src/TestItemDetection.jl | 8 + src/packagedef.jl | 149 +++++++++++++ src/vendored_code.jl | 39 ++++ test/runtests.jl | 3 + test/test_detection.jl | 203 ++++++++++++++++++ 15 files changed, 605 insertions(+) create mode 100644 .github/workflows/jlpkgbutler-butler-workflow.yml create mode 100644 .github/workflows/jlpkgbutler-ci-master-workflow.yml create mode 100644 .github/workflows/jlpkgbutler-ci-pr-workflow.yml create mode 100644 .github/workflows/jlpkgbutler-codeformat-pr-workflow.yml create mode 100644 .github/workflows/jlpkgbutler-compathelper-workflow.yml create mode 100644 .github/workflows/jlpkgbutler-tagbot-workflow.yml create mode 100644 .jlpkgbutler.toml create mode 100644 LICENSE create mode 100644 Project.toml create mode 100644 README.md create mode 100644 src/TestItemDetection.jl create mode 100644 src/packagedef.jl create mode 100644 src/vendored_code.jl create mode 100644 test/runtests.jl create mode 100644 test/test_detection.jl diff --git a/.github/workflows/jlpkgbutler-butler-workflow.yml b/.github/workflows/jlpkgbutler-butler-workflow.yml new file mode 100644 index 0000000..70544cc --- /dev/null +++ b/.github/workflows/jlpkgbutler-butler-workflow.yml @@ -0,0 +1,22 @@ +name: Run the Julia Package Butler + +on: + push: + branches: + - main + - master + schedule: + - cron: '0 */1 * * *' + workflow_dispatch: + +jobs: + butler: + name: "Run Package Butler" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: davidanthoff/julia-pkgbutler@releases/v1 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + ssh-private-key: ${{ secrets.JLPKGBUTLER_TOKEN }} + channel: stable diff --git a/.github/workflows/jlpkgbutler-ci-master-workflow.yml b/.github/workflows/jlpkgbutler-ci-master-workflow.yml new file mode 100644 index 0000000..09cff08 --- /dev/null +++ b/.github/workflows/jlpkgbutler-ci-master-workflow.yml @@ -0,0 +1,40 @@ +name: Run CI on main + +on: + push: + branches: + - main + - master + workflow_dispatch: + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + matrix: + julia-version: ['1.0', '1.1', '1.2', '1.3', '1.4', '1.5', '1.6', '1.7', '1.8'] + julia-arch: [x64, x86] + os: [ubuntu-latest, windows-latest, macOS-latest] + exclude: + - os: macOS-latest + julia-arch: x86 + + steps: + - uses: actions/checkout@v3 + - uses: julia-actions/setup-julia@v1 + with: + version: ${{ matrix.julia-version }} + arch: ${{ matrix.julia-arch }} + - uses: julia-actions/julia-buildpkg@v1 + env: + PYTHON: "" + - uses: julia-actions/julia-runtest@v1 + env: + PYTHON: "" + - uses: julia-actions/julia-processcoverage@v1 + - uses: codecov/codecov-action@v3 + with: + files: ./lcov.info + flags: unittests + token: ${{ secrets.CODECOV_TOKEN }} + \ No newline at end of file diff --git a/.github/workflows/jlpkgbutler-ci-pr-workflow.yml b/.github/workflows/jlpkgbutler-ci-pr-workflow.yml new file mode 100644 index 0000000..9114217 --- /dev/null +++ b/.github/workflows/jlpkgbutler-ci-pr-workflow.yml @@ -0,0 +1,36 @@ +name: Run CI on PR + +on: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + matrix: + julia-version: ['1.0', '1.1', '1.2', '1.3', '1.4', '1.5', '1.6', '1.7', '1.8'] + julia-arch: [x64, x86] + os: [ubuntu-latest, windows-latest, macOS-latest] + exclude: + - os: macOS-latest + julia-arch: x86 + + steps: + - uses: actions/checkout@v3 + - uses: julia-actions/setup-julia@v1 + with: + version: ${{ matrix.julia-version }} + arch: ${{ matrix.julia-arch }} + - uses: julia-actions/julia-buildpkg@v1 + env: + PYTHON: "" + - uses: julia-actions/julia-runtest@v1 + env: + PYTHON: "" + - uses: julia-actions/julia-processcoverage@v1 + - uses: codecov/codecov-action@v3 + with: + files: ./lcov.info + flags: unittests + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/jlpkgbutler-codeformat-pr-workflow.yml b/.github/workflows/jlpkgbutler-codeformat-pr-workflow.yml new file mode 100644 index 0000000..411bed4 --- /dev/null +++ b/.github/workflows/jlpkgbutler-codeformat-pr-workflow.yml @@ -0,0 +1,23 @@ +name: Code Formatting + +on: + push: + branches: + - main + - master + workflow_dispatch: + +jobs: + format: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: julia-actions/julia-codeformat@releases/v1 + - name: Create Pull Request + uses: peter-evans/create-pull-request@v3 + with: + token: ${{ secrets.GITHUB_TOKEN }} + commit-message: Format files using DocumentFormat + title: '[AUTO] Format files using DocumentFormat' + body: '[DocumentFormat.jl](https://github.com/julia-vscode/DocumentFormat.jl) would suggest these formatting changes' + labels: no changelog diff --git a/.github/workflows/jlpkgbutler-compathelper-workflow.yml b/.github/workflows/jlpkgbutler-compathelper-workflow.yml new file mode 100644 index 0000000..b315831 --- /dev/null +++ b/.github/workflows/jlpkgbutler-compathelper-workflow.yml @@ -0,0 +1,20 @@ +name: Run CompatHelper + +on: + schedule: + - cron: '00 * * * *' + issues: + types: [opened, reopened] + workflow_dispatch: + +jobs: + CompatHelper: + name: "Run CompatHelper.jl" + runs-on: ubuntu-latest + steps: + - name: Pkg.add("CompatHelper") + run: julia -e 'using Pkg; Pkg.add("CompatHelper")' + - name: CompatHelper.main() + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: julia -e 'using CompatHelper; CompatHelper.main()' diff --git a/.github/workflows/jlpkgbutler-tagbot-workflow.yml b/.github/workflows/jlpkgbutler-tagbot-workflow.yml new file mode 100644 index 0000000..d3ca956 --- /dev/null +++ b/.github/workflows/jlpkgbutler-tagbot-workflow.yml @@ -0,0 +1,17 @@ +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 }} + ssh: ${{ secrets.JLPKGBUTLER_TOKEN }} + branches: true diff --git a/.jlpkgbutler.toml b/.jlpkgbutler.toml new file mode 100644 index 0000000..b72304f --- /dev/null +++ b/.jlpkgbutler.toml @@ -0,0 +1 @@ +template = "bach" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bfc8d63 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 David Anthoff + +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. diff --git a/Project.toml b/Project.toml new file mode 100644 index 0000000..e741d7d --- /dev/null +++ b/Project.toml @@ -0,0 +1,18 @@ +name = "TestItemDetection" +uuid = "76b0de8b-5c4b-48ef-a724-914b33ca988d" +authors = ["David Anthoff "] +version = "0.2.1-DEV" + +[deps] +CSTParser = "00ebfdb7-1f24-5e51-bd34-a7502290713f" + +[extras] +TestItemRunner = "f8b46487-2199-4994-9208-9a1283c18c0a" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + +[compat] +julia = "1" +CSTParser = "3" + +[targets] +test = ["Test", "TestItemRunner"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..e7fe296 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# TestItemDetection.jl + +## Overview + +This package provides low level features that are used by TestItemRunner.jl and LanguageServer.jl. diff --git a/src/TestItemDetection.jl b/src/TestItemDetection.jl new file mode 100644 index 0000000..30a1053 --- /dev/null +++ b/src/TestItemDetection.jl @@ -0,0 +1,8 @@ +module TestItemDetection + +import CSTParser +using CSTParser: EXPR + +include("packagedef.jl") + +end diff --git a/src/packagedef.jl b/src/packagedef.jl new file mode 100644 index 0000000..52740dc --- /dev/null +++ b/src/packagedef.jl @@ -0,0 +1,149 @@ +include("vendored_code.jl") + +function find_test_detail!(node, testitems, testsetups, errors) + node isa EXPR || return + + if node.head == :macrocall && length(node.args)>0 && CSTParser.valof(node.args[1]) == "@testitem" + pos = 1 + get_file_loc(node)[2] + range = pos:pos+node.span-1 + + # filter out line nodes + child_nodes = filter(i->!(isa(i, EXPR) && i.head==:NOTHING && i.args===nothing), node.args) + + # Check for various syntax errors + if length(child_nodes)==1 + push!(errors, (error="Your @testitem is missing a name and code block.", range=range)) + return + elseif length(child_nodes)>1 && !(child_nodes[2] isa EXPR && child_nodes[2].head==:STRING) + push!(errors, (error="Your @testitem must have a first argument that is of type String for the name.", range=range)) + return + elseif length(child_nodes)==2 + push!(errors, (error="Your @testitem is missing a code block argument.", range=range)) + return + elseif !(child_nodes[end] isa EXPR && child_nodes[end].head==:block) + push!(errors, (error="The final argument of a @testitem must be a begin end block.", range=range)) + return + else + option_tags = nothing + option_default_imports = nothing + option_setup = nothing + + # Now check our keyword args + for i in child_nodes[3:end-1] + if !(i isa EXPR && i.head isa EXPR && i.head.head==:OPERATOR && CSTParser.valof(i.head)=="=") + push!(errors, (error="The arguments to a @testitem must be in keyword format.", range=range)) + return + elseif !(length(i.args)==2) + error("This code path should not be possible.") + elseif CSTParser.valof(i.args[1])=="tags" + if option_tags!==nothing + push!(errors, (error="The keyword argument tags cannot be specified more than once.", range=range)) + return + end + + if !(i.args[2].head == :vect) + push!(errors, (error="The keyword argument tags only accepts a vector of symbols.", range=range)) + return + end + + option_tags = Symbol[] + + for j in i.args[2].args + if !(j isa EXPR && j.head==:quotenode && length(j.args)==1 && j.args[1] isa EXPR && j.args[1].head==:IDENTIFIER) + push!(errors, (error="The keyword argument tags only accepts a vector of symbols.", range=range)) + return + end + + push!(option_tags, Symbol(CSTParser.valof(j.args[1]))) + end + elseif CSTParser.valof(i.args[1])=="default_imports" + if option_default_imports!==nothing + push!(errors, (error="The keyword argument default_imports cannot be specified more than once.", range=range)) + return + end + + if !(CSTParser.valof(i.args[2]) in ("true", "false")) + push!(errors, (error="The keyword argument default_imports only accepts bool values.", range=range)) + return + end + + option_default_imports = parse(Bool, CSTParser.valof(i.args[2])) + elseif CSTParser.valof(i.args[1])=="setup" + if option_setup!==nothing + push!(errors, (error="The keyword argument setup cannot be specified more than once.", range=range)) + return + end + + if !(i.args[2].head == :vect) + push!(errors, (error="The keyword argument `setup` only accepts a vector of `@testsetup` names.", range=range)) + return + end + option_setup = Symbol[] + + for j in i.args[2].args + if !(j isa EXPR && j.head==:IDENTIFIER) + push!(errors, (error="The keyword argument `setup` only accepts a vector of `@testsetup` names.", range=range)) + return + end + + push!(option_setup, Symbol(CSTParser.valof(j))) + end + else + push!(errors, (error="Unknown keyword argument.", range=range)) + return + end + end + + if option_tags===nothing + option_tags = Symbol[] + end + + if option_default_imports===nothing + option_default_imports = true + end + + if option_setup===nothing + option_setup = Symbol[] + end + + # TODO + 1 here is from the space before the begin end block. We might have to detect that, + # not sure whether that is always assigned to the begin end block EXPR + code_pos = get_file_loc(child_nodes[end])[2] + 1 + length("begin") + + code_range = code_pos:code_pos+child_nodes[end].span - 1 - length("begin") - length("end") + + push!(testitems, (name=CSTParser.valof(node.args[3]), range=range, code_range=code_range, option_default_imports=option_default_imports, option_tags=option_tags, option_setup=option_setup)) + end + elseif node.head == :macrocall && length(node.args)>0 && CSTParser.valof(node.args[1]) == "@testsetup" + pos = 1 + get_file_loc(node)[2] + range = pos:pos+node.span-1 + + # filter out line nodes + child_nodes = filter(i->!(isa(i, EXPR) && i.head==:NOTHING && i.args===nothing), node.args) + + # Check for various syntax errors + if length(child_nodes)==1 + push!(errors, (error="Your @testsetup is missing a name and code block.", range=range)) + return + elseif length(child_nodes)>1 && !(child_nodes[2] isa EXPR && child_nodes[2].head==:IDENTIFIER) + push!(errors, (error="Your @testsetup must have a first argument that is a valid identifier for the name.", range=range)) + return + elseif length(child_nodes)==2 + push!(errors, (error="Your @testsetup is missing a code block argument.", range=range)) + return + elseif !(child_nodes[end] isa EXPR && child_nodes[end].head==:block) + push!(errors, (error="The final argument of a @testsetup must be a begin end block.", range=range)) + return + else + # TODO + 1 here is from the space before the begin end block. We might have to detect that, + # not sure whether that is always assigned to the begin end block EXPR + code_pos = get_file_loc(child_nodes[end])[2] + 1 + length("begin") + code_range = code_pos:code_pos+child_nodes[end].span - 1 - length("begin") - length("end") + push!(testsetups, (name=CSTParser.valof(node.args[3]), range=range, code_range=code_range)) + end + elseif node.head == :module && length(node.args)>=3 && node.args[3] isa EXPR && node.args[3].head==:block + for i in node.args[3].args + find_test_detail!(i, testitems, testsetups, errors) + end + end +end diff --git a/src/vendored_code.jl b/src/vendored_code.jl new file mode 100644 index 0000000..840ce0d --- /dev/null +++ b/src/vendored_code.jl @@ -0,0 +1,39 @@ +# Vendored from LanguageServer.jl +# TODO Can we move this into CSTParser, where it really should be? Problem is +function descend(x::EXPR, target::EXPR, offset=0) + x == target && return (true, offset) + for c in x + if c == target + return true, offset + end + + found, o = descend(c, target, offset) + if found + return true, o + end + offset += c.fullspan + end + return false, offset +end + +# Vendored from LanguageServer.jl +# TODO Can we move this into CSTParser, where it really should be? Problem is +# the part that is commented out right now +function get_file_loc(x::EXPR, offset=0, c=nothing) + parent = x + while CSTParser.parentof(parent) !== nothing + parent = CSTParser.parentof(parent) + end + + if parent === nothing + return nothing, offset + end + + _, offset = descend(parent, x) + + # TODO Unclear what this was for but don't want to take dep on StaticLint + # if headof(parent) === :file && StaticLint.hasmeta(parent) + # return parent.meta.error, offset + # end + return nothing, offset +end diff --git a/test/runtests.jl b/test/runtests.jl new file mode 100644 index 0000000..b9e874d --- /dev/null +++ b/test/runtests.jl @@ -0,0 +1,3 @@ +using TestItemRunner + +@run_package_tests diff --git a/test/test_detection.jl b/test/test_detection.jl new file mode 100644 index 0000000..f7d4233 --- /dev/null +++ b/test/test_detection.jl @@ -0,0 +1,203 @@ +@testitem "@testitem macro missing all args" begin + import CSTParser + + code = CSTParser.parse("""@testitem + """) + + test_items = [] + test_setups = [] + errors = [] + TestItemDetection.find_test_detail!(code, test_items, test_setups, errors) + + @test length(test_items) == 0 + @test length(errors) == 1 + + @test errors[1] == (error="Your @testitem is missing a name and code block.", range=1:9) +end + +@testitem "Wrong type for name" begin + import CSTParser + + code = CSTParser.parse("""@testitem :foo + """) + + test_items = [] + test_setups = [] + errors = [] + TestItemDetection.find_test_detail!(code, test_items, test_setups, errors) + + @test length(test_items) == 0 + @test length(errors) == 1 + + @test errors[1] == (error="Your @testitem must have a first argument that is of type String for the name.", range=1:14) +end + +@testitem "Code block missing" begin + import CSTParser + + code = CSTParser.parse("""@testitem "foo" + """) + + test_items = [] + test_setups = [] + errors = [] + TestItemDetection.find_test_detail!(code, test_items, test_setups, errors) + + @test length(test_items) == 0 + @test length(errors) == 1 + + @test errors[1] == (error="Your @testitem is missing a code block argument.", range=1:15) +end + +@testitem "Final arg not a code block" begin + import CSTParser + + code = CSTParser.parse("""@testitem "foo" 3 + """) + + test_items = [] + test_setups = [] + errors = [] + TestItemDetection.find_test_detail!(code, test_items, test_setups, errors) + + @test length(test_items) == 0 + @test length(errors) == 1 + + @test errors[1] == (error="The final argument of a @testitem must be a begin end block.", range=1:17) +end + +@testitem "None kw arg" begin + import CSTParser + + code = CSTParser.parse("""@testitem "foo" bar begin end + """) + + test_items = [] + test_setups = [] + errors = [] + TestItemDetection.find_test_detail!(code, test_items, test_setups, errors) + + @test length(test_items) == 0 + @test length(errors) == 1 + + @test errors[1] == (error="The arguments to a @testitem must be in keyword format.", range=1:29) +end + +@testitem "Duplicate kw arg" begin + import CSTParser + + code = CSTParser.parse("""@testitem "foo" default_imports=true default_imports=false begin end + """) + + test_items = [] + test_setups = [] + errors = [] + TestItemDetection.find_test_detail!(code, test_items, test_setups, errors) + + @test length(test_items) == 0 + @test length(errors) == 1 + + @test errors[1] == (error="The keyword argument default_imports cannot be specified more than once.", range=1:68) +end + +@testitem "Incomplete kw arg" begin + import CSTParser + + code = CSTParser.parse("""@testitem "foo" default_imports= begin end + """) + + test_items = [] + test_setups = [] + errors = [] + TestItemDetection.find_test_detail!(code, test_items, test_setups, errors) + + @test length(test_items) == 0 + @test length(errors) == 1 + + @test errors[1] == (error="The final argument of a @testitem must be a begin end block.", range=1:42) +end + +@testitem "Wrong default_imports type kw arg" begin + import CSTParser + + code = CSTParser.parse("""@testitem "foo" default_imports=4 begin end + """) + + test_items = [] + test_setups = [] + errors = [] + TestItemDetection.find_test_detail!(code, test_items, test_setups, errors) + + @test length(test_items) == 0 + @test length(errors) == 1 + + @test errors[1] == (error="The keyword argument default_imports only accepts bool values.", range=1:43) +end + +@testitem "non vector arg for tags kw" begin + import CSTParser + + code = CSTParser.parse("""@testitem "foo" tags=4 begin end + """) + + test_items = [] + test_setups = [] + errors = [] + TestItemDetection.find_test_detail!(code, test_items, test_setups, errors) + + @test length(test_items) == 0 + @test length(errors) == 1 + + @test errors[1] == (error="The keyword argument tags only accepts a vector of symbols.", range=1:32) +end + +@testitem "Wrong types in tags kw arg" begin + import CSTParser + + code = CSTParser.parse("""@testitem "foo" tags=[4, 8] begin end + """) + + test_items = [] + test_setups = [] + errors = [] + TestItemDetection.find_test_detail!(code, test_items, test_setups, errors) + + @test length(test_items) == 0 + @test length(errors) == 1 + + @test errors[1] == (error="The keyword argument tags only accepts a vector of symbols.", range=1:37) +end + +@testitem "Unknown keyword arg" begin + import CSTParser + + code = CSTParser.parse("""@testitem "foo" bar=true begin end + """) + + test_items = [] + test_setups = [] + errors = [] + TestItemDetection.find_test_detail!(code, test_items, test_setups, errors) + + @test length(test_items) == 0 + @test length(errors) == 1 + + @test errors[1] == (error="Unknown keyword argument.", range=1:34) +end + +@testitem "All parts correctly there" begin + import CSTParser + + code = CSTParser.parse("""@testitem "foo" tags=[:a, :b] default_imports=true begin println() end + """) + + test_items = [] + test_setups = [] + errors = [] + TestItemDetection.find_test_detail!(code, test_items, test_setups, errors) + + @test length(test_items) == 1 + @test length(errors) == 0 + + @test test_items[1] == (name="foo", range=1:70, code_range=57:67, option_default_imports=true, option_tags=[:a, :b], option_setup=Symbol[]) +end